diff --git a/app/src/main/java/org/permanent/permanent/Constants.kt b/app/src/main/java/org/permanent/permanent/Constants.kt index f8180c40..62bb1419 100644 --- a/app/src/main/java/org/permanent/permanent/Constants.kt +++ b/app/src/main/java/org/permanent/permanent/Constants.kt @@ -13,6 +13,7 @@ class Constants { const val LOGIN_URL_SUFFIX = "auth/login" const val VERIFY_2FA_URL_SUFFIX = "auth/verify" const val SIGN_UP_URL_SUFFIX = "account/post" + const val SHARE_TOKENS_URL_SUFFIX = "shareTokens" const val S3_BASE_URL = "https://permanent-prod.s3.us-west-2.amazonaws.com" const val HOW_TO_PUBLISH_ARCHIVE_URL = "https://permanent.zohodesk.com/portal/en/kb/articles/public-archives-mobile" const val STRIPE_URL = "https://api.stripe.com/v1/payment_intents" diff --git a/app/src/main/java/org/permanent/permanent/mapper/ItemMapper.kt b/app/src/main/java/org/permanent/permanent/mapper/ItemMapper.kt new file mode 100644 index 00000000..08c9ea99 --- /dev/null +++ b/app/src/main/java/org/permanent/permanent/mapper/ItemMapper.kt @@ -0,0 +1,27 @@ +package org.permanent.permanent.mapper + +import org.permanent.permanent.models.Record +import org.permanent.permanent.models.RecordType +import org.permanent.permanent.network.models.ItemDTO + + +fun ItemDTO.toRecord(): Record { + val rec = Record( + recordId = recordId?.toIntOrNull() ?: 0, + folderLinkId = folderLinkId?.toIntOrNull() ?: 0 + ) + + rec.id = folderId?.toIntOrNull() ?: recordId?.toIntOrNull() + rec.folderId = folderId?.toIntOrNull() + rec.recordId = recordId?.toIntOrNull() + rec.displayName = displayName + rec.displayDate = displayDate?.replace("T", " ") + rec.thumbURL200 = thumbUrl200 + rec.thumbURL2000 = thumbUrl2000 + rec.archiveId = archive?.id?.toIntOrNull() + rec.archiveNr = archive?.name + rec.type = if (folderId != null) RecordType.FOLDER else RecordType.FILE + rec.size = size ?: -1L + + return rec +} \ No newline at end of file diff --git a/app/src/main/java/org/permanent/permanent/models/Record.kt b/app/src/main/java/org/permanent/permanent/models/Record.kt index b61df17b..e7a61e6d 100644 --- a/app/src/main/java/org/permanent/permanent/models/Record.kt +++ b/app/src/main/java/org/permanent/permanent/models/Record.kt @@ -39,6 +39,7 @@ open class Record : Parcelable { var isProcessing = false var displayInShares = false var fileData: FileData? = null + var localDrawableRes: Int? = null constructor(parcel: Parcel) { id = parcel.readValue(Int::class.java.classLoader) as? Int diff --git a/app/src/main/java/org/permanent/permanent/network/NetworkClient.kt b/app/src/main/java/org/permanent/permanent/network/NetworkClient.kt index 7f6a5aa8..d764a0ee 100644 --- a/app/src/main/java/org/permanent/permanent/network/NetworkClient.kt +++ b/app/src/main/java/org/permanent/permanent/network/NetworkClient.kt @@ -37,6 +37,7 @@ import org.permanent.permanent.network.models.AccountVO import org.permanent.permanent.network.models.ArchiveSteward import org.permanent.permanent.network.models.ChecklistResponse import org.permanent.permanent.network.models.FileData +import org.permanent.permanent.network.models.FolderChildrenResponse import org.permanent.permanent.network.models.GetPresignedUrlResponse import org.permanent.permanent.network.models.LegacyContact import org.permanent.permanent.network.models.LocnVO @@ -115,12 +116,11 @@ class NetworkClient(private var okHttpClient: OkHttpClient?, context: Context) { val requestURL = request.url.toString() if ((uploadURL.isNullOrEmpty() || !requestURL.contains(uploadURL)) && !requestURL.contains(Constants.SIGN_UP_URL_SUFFIX) && - !requestURL.contains(Constants.STRIPE_URL) + !requestURL.contains(Constants.STRIPE_URL) && + !requestURL.contains(Constants.SHARE_TOKENS_URL_SUFFIX) ) { val requestBuilder: Request.Builder = request.newBuilder() - requestBuilder.header( - "Authorization", "Bearer ${prefsHelper.getAuthToken()}" - ) + requestBuilder.header("Authorization", "Bearer ${prefsHelper.getAuthToken()}") chain.proceed(requestBuilder.build()) } else chain.proceed(request) }).build() @@ -788,7 +788,16 @@ class NetworkClient(private var okHttpClient: OkHttpClient?, context: Context) { fun disableTwoFactor(twoFAVO: TwoFAVO): Call = stelaAccountService.disableTwoFactor(twoFAVO) - fun getShareLink(shareLinkIds: List): Call = stelaAccountService.getShareLink(shareLinkIds) + fun getShareLink( + shareLinkIds: List? = null, + shareTokens: List? = null + ): Call = stelaAccountService.getShareLink(shareLinkIds, shareTokens) + + fun getFolderChildren( + shareToken: String?, + folderId: Int, + pageSize: Int = 99999999 + ): Call = stelaAccountService.getFolderChildren(shareToken, folderId, pageSize) fun generateShareLink(shareLink: ShareLinkVO): Call = stelaAccountService.generateShareLink(shareLink) diff --git a/app/src/main/java/org/permanent/permanent/network/StelaAccountService.kt b/app/src/main/java/org/permanent/permanent/network/StelaAccountService.kt index d8fd16ea..ea791247 100644 --- a/app/src/main/java/org/permanent/permanent/network/StelaAccountService.kt +++ b/app/src/main/java/org/permanent/permanent/network/StelaAccountService.kt @@ -3,6 +3,7 @@ package org.permanent.permanent.network import okhttp3.RequestBody import okhttp3.ResponseBody import org.permanent.permanent.models.Tags +import org.permanent.permanent.network.models.FolderChildrenResponse import org.permanent.permanent.network.models.ResponseVO import org.permanent.permanent.network.models.ShareLinkResponse import org.permanent.permanent.network.models.ShareLinkVO @@ -12,6 +13,7 @@ import retrofit2.Call import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.PUT @@ -21,7 +23,17 @@ import retrofit2.http.Query interface StelaAccountService { @GET("api/v2/share-links") - fun getShareLink(@Query("shareLinkIds[]") shareLinkIds: List): Call + fun getShareLink( + @Query("shareLinkIds[]") shareLinkIds: List? = null, + @Query("shareTokens[]") shareTokens: List? = null + ): Call + + @GET("api/v2/folder/{folderId}/children") + fun getFolderChildren( + @Header("X-Permanent-Share-Token") shareToken: String?, + @Path("folderId") folderId: Int, + @Query("pageSize") pageSize: Int = 99999999 + ): Call @POST("api/v2/share-links") fun generateShareLink(@Body shareLink: ShareLinkVO): Call diff --git a/app/src/main/java/org/permanent/permanent/network/models/ArchiveDTO.kt b/app/src/main/java/org/permanent/permanent/network/models/ArchiveDTO.kt new file mode 100644 index 00000000..51d5bb48 --- /dev/null +++ b/app/src/main/java/org/permanent/permanent/network/models/ArchiveDTO.kt @@ -0,0 +1,7 @@ +package org.permanent.permanent.network.models + +data class ArchiveDTO( + val id: String?, + val archiveNumber: String?, + val name: String? +) \ No newline at end of file diff --git a/app/src/main/java/org/permanent/permanent/network/models/FolderChildrenResponse.kt b/app/src/main/java/org/permanent/permanent/network/models/FolderChildrenResponse.kt new file mode 100644 index 00000000..b2bda63d --- /dev/null +++ b/app/src/main/java/org/permanent/permanent/network/models/FolderChildrenResponse.kt @@ -0,0 +1,5 @@ +package org.permanent.permanent.network.models + +data class FolderChildrenResponse( + val items: List? +) \ No newline at end of file diff --git a/app/src/main/java/org/permanent/permanent/network/models/IFolderChildrenListener.kt b/app/src/main/java/org/permanent/permanent/network/models/IFolderChildrenListener.kt new file mode 100644 index 00000000..5c174636 --- /dev/null +++ b/app/src/main/java/org/permanent/permanent/network/models/IFolderChildrenListener.kt @@ -0,0 +1,8 @@ +package org.permanent.permanent.network.models + +import org.permanent.permanent.models.Record + +interface IFolderChildrenListener { + fun onSuccess(records: List) + fun onFailed(error: String?) +} \ No newline at end of file diff --git a/app/src/main/java/org/permanent/permanent/network/models/ItemDTO.kt b/app/src/main/java/org/permanent/permanent/network/models/ItemDTO.kt new file mode 100644 index 00000000..c2f9cac3 --- /dev/null +++ b/app/src/main/java/org/permanent/permanent/network/models/ItemDTO.kt @@ -0,0 +1,13 @@ +package org.permanent.permanent.network.models + +data class ItemDTO( + val folderId: String?, + val recordId: String?, + val folderLinkId: String?, + val displayName: String?, + val displayDate: String?, + val size: Long?, + val thumbUrl200: String?, + var thumbUrl2000: String?, + val archive: ArchiveDTO?, +) \ No newline at end of file diff --git a/app/src/main/java/org/permanent/permanent/repositories/StelaAccountRepository.kt b/app/src/main/java/org/permanent/permanent/repositories/StelaAccountRepository.kt index bc8630ab..fdbc89aa 100644 --- a/app/src/main/java/org/permanent/permanent/repositories/StelaAccountRepository.kt +++ b/app/src/main/java/org/permanent/permanent/repositories/StelaAccountRepository.kt @@ -4,6 +4,7 @@ import org.permanent.permanent.models.Tags import org.permanent.permanent.network.ILinkListener import org.permanent.permanent.network.IResponseListener import org.permanent.permanent.network.ITwoFAListener +import org.permanent.permanent.network.models.IFolderChildrenListener import org.permanent.permanent.network.models.ShareLinkVO import org.permanent.permanent.network.models.TwoFAVO @@ -21,7 +22,18 @@ interface StelaAccountRepository { fun disableTwoFactor(twoFAVO: TwoFAVO, listener: IResponseListener) - fun getShareLink(shareLinkIds: List, listener: ILinkListener) + fun getShareLink( + shareLinkIds: List? = null, + shareTokens: List? = null, + listener: ILinkListener + ) + + fun getFolderChildren( + shareToken: String?, + folderId: Int, + pageSize: Int = 99999999, + listener: IFolderChildrenListener + ) fun generateShareLink(shareLinkVO: ShareLinkVO, listener: ILinkListener) diff --git a/app/src/main/java/org/permanent/permanent/repositories/StelaAccountRepositoryImpl.kt b/app/src/main/java/org/permanent/permanent/repositories/StelaAccountRepositoryImpl.kt index 1753bab1..04e70b0c 100644 --- a/app/src/main/java/org/permanent/permanent/repositories/StelaAccountRepositoryImpl.kt +++ b/app/src/main/java/org/permanent/permanent/repositories/StelaAccountRepositoryImpl.kt @@ -4,12 +4,15 @@ import android.content.Context import com.google.gson.Gson import okhttp3.ResponseBody import org.permanent.permanent.R +import org.permanent.permanent.mapper.toRecord import org.permanent.permanent.models.Tags import org.permanent.permanent.network.ILinkListener import org.permanent.permanent.network.IResponseListener import org.permanent.permanent.network.ITwoFAListener import org.permanent.permanent.network.NetworkClient import org.permanent.permanent.network.models.ErrorResponse +import org.permanent.permanent.network.models.FolderChildrenResponse +import org.permanent.permanent.network.models.IFolderChildrenListener import org.permanent.permanent.network.models.ResponseVO import org.permanent.permanent.network.models.ShareLinkResponse import org.permanent.permanent.network.models.ShareLinkVO @@ -197,8 +200,12 @@ class StelaAccountRepositoryImpl(context: Context) : StelaAccountRepository { }) } - override fun getShareLink(shareLinkIds: List, listener: ILinkListener) { - NetworkClient.instance().getShareLink(shareLinkIds).enqueue( object : Callback { + override fun getShareLink( + shareLinkIds: List?, + shareTokens: List?, + listener: ILinkListener + ) { + NetworkClient.instance().getShareLink(shareLinkIds, shareTokens).enqueue( object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { @@ -219,6 +226,46 @@ class StelaAccountRepositoryImpl(context: Context) : StelaAccountRepository { }) } + override fun getFolderChildren( + shareToken: String?, + folderId: Int, + pageSize: Int, + listener: IFolderChildrenListener + ) { + NetworkClient.instance() + .getFolderChildren(shareToken, folderId, pageSize) + .enqueue(object : Callback { + + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + val items = response.body()?.items + + if (items != null) { + val records = items.map { it.toRecord() } + listener.onSuccess(records) + } else { + listener.onSuccess(emptyList()) + } + } else { + listener.onFailed( + response.errorBody()?.string() + ?: appContext.getString(R.string.generic_error) + ) + } + } + + override fun onFailure( + call: Call, + t: Throwable + ) { + listener.onFailed(t.message) + } + }) + } + override fun generateShareLink(shareLinkVO: ShareLinkVO, listener: ILinkListener) { NetworkClient.instance().generateShareLink(shareLinkVO).enqueue( object : Callback { diff --git a/app/src/main/java/org/permanent/permanent/ui/activities/MainActivity.kt b/app/src/main/java/org/permanent/permanent/ui/activities/MainActivity.kt index db21cf45..49e4ba7a 100644 --- a/app/src/main/java/org/permanent/permanent/ui/activities/MainActivity.kt +++ b/app/src/main/java/org/permanent/permanent/ui/activities/MainActivity.kt @@ -10,6 +10,7 @@ import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.Toast import androidx.appcompat.widget.Toolbar @@ -19,8 +20,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.core.os.bundleOf import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding +import androidx.core.view.updateLayoutParams import androidx.databinding.DataBindingUtil import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.Observer @@ -81,6 +83,7 @@ class MainActivity : PermanentBaseActivity(), Toolbar.OnMenuItemClickListener { private lateinit var appBarConfig: AppBarConfiguration private var bottomSheetFragment: ChecklistBottomSheetFragment? = null private var isSubmenuVisible = false + private var currentDestinationId: Int? = null private val onArchiveSwitched = Observer { startWithCustomDestination(false) @@ -98,6 +101,10 @@ class MainActivity : PermanentBaseActivity(), Toolbar.OnMenuItemClickListener { private val onDestinationChangedListener = NavController.OnDestinationChangedListener { _, destination, _ -> + + currentDestinationId = destination.id + invalidateOptionsMenu() + when (destination.id) { R.id.editArchiveBasicInfoFragment, R.id.editArchiveFullDetailsFragment, R.id.onlinePresenceListFragment, R.id.milestoneListFragment -> { binding.toolbar.menu?.findItem(R.id.settingsItem)?.isVisible = false @@ -121,7 +128,7 @@ class MainActivity : PermanentBaseActivity(), Toolbar.OnMenuItemClickListener { binding.toolbar.menu?.findItem(R.id.moreItem)?.isVisible = true } - R.id.accountFragment, R.id.storageMenuFragment, R.id.addStorageFragment, R.id.giftStorageFragment, R.id.redeemCodeFragment, R.id.archivesFragment, R.id.invitationsFragment, R.id.activityFeedFragment, R.id.loginAndSecurityFragment, R.id.changePasswordFragment, R.id.twoStepVerificationFragment, R.id.legacyLoadingFragment -> { + R.id.accountFragment, R.id.storageMenuFragment, R.id.addStorageFragment, R.id.giftStorageFragment, R.id.redeemCodeFragment, R.id.archivesFragment, R.id.invitationsFragment, R.id.activityFeedFragment, R.id.loginAndSecurityFragment, R.id.changePasswordFragment, R.id.twoStepVerificationFragment, R.id.legacyLoadingFragment, R.id.sharePreviewFragment -> { binding.toolbar.menu?.findItem(R.id.settingsItem)?.isVisible = false } @@ -138,6 +145,7 @@ class MainActivity : PermanentBaseActivity(), Toolbar.OnMenuItemClickListener { } override fun onCreate(savedInstanceState: Bundle?) { + WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState) // Setup orientation @@ -165,13 +173,24 @@ class MainActivity : PermanentBaseActivity(), Toolbar.OnMenuItemClickListener { headerMainBinding.lifecycleOwner = this headerMainBinding.viewModel = viewModel - // Apply insets top for status bar and bottom for navigation/gesture area - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.mainNavigationView)) { view, insets -> - val sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { view, insets -> + val statusBar = insets.getInsets(WindowInsetsCompat.Type.statusBars()) + + view.updateLayoutParams { + topMargin = statusBar.top + } + + insets + } - view.updatePadding( - top = sysBars.top, - bottom = sysBars.bottom + ViewCompat.setOnApplyWindowInsetsListener(binding.mainNavigationView) { view, insets -> + val statusBar = insets.getInsets(WindowInsetsCompat.Type.statusBars()) + + view.setPadding( + view.paddingLeft, + statusBar.top, + view.paddingRight, + view.paddingBottom ) insets @@ -372,6 +391,16 @@ class MainActivity : PermanentBaseActivity(), Toolbar.OnMenuItemClickListener { .makeGooglePlayServicesAvailable(this) } + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + val settingsItem = menu.findItem(R.id.settingsItem) + + if (currentDestinationId == R.id.sharePreviewFragment) { + settingsItem?.isVisible = false + } + + return super.onPrepareOptionsMenu(menu) + } + private val onChecklistItemClickObserver = Observer { when (it.toChecklistType()) { ChecklistItemType.STORAGE_REDEEMED -> navController.navigate(R.id.redeemCodeFragment) diff --git a/app/src/main/java/org/permanent/permanent/ui/myFiles/MyFilesFragment.kt b/app/src/main/java/org/permanent/permanent/ui/myFiles/MyFilesFragment.kt index eb11fb9a..057f6df6 100644 --- a/app/src/main/java/org/permanent/permanent/ui/myFiles/MyFilesFragment.kt +++ b/app/src/main/java/org/permanent/permanent/ui/myFiles/MyFilesFragment.kt @@ -54,7 +54,6 @@ import org.permanent.permanent.ui.recordMenu.RecordMenuFragment import org.permanent.permanent.ui.recordMenu.RecordUiModel import org.permanent.permanent.ui.recordMenu.SelectionMenuFragment import org.permanent.permanent.ui.shareManagement.ShareManagementFragment -import org.permanent.permanent.ui.shareManagement.ShareLinkFragment import org.permanent.permanent.ui.shares.PreviewState import org.permanent.permanent.ui.shares.SHOW_SCREEN_SIMPLIFIED_KEY import org.permanent.permanent.ui.shares.URL_TOKEN_KEY @@ -471,8 +470,6 @@ class MyFilesFragment : PermanentBaseFragment() { viewModel.getIsRelocationMode(), viewModel.getIsSelectionMode(), MutableLiveData(PreviewState.ACCESS_GRANTED), - isForSharePreviewScreen = false, - isForSharesScreen = false, recordListener = viewModel ) val isListViewMode = prefsHelper.isListViewMode() diff --git a/app/src/main/java/org/permanent/permanent/ui/myFiles/RecordGridViewHolder.kt b/app/src/main/java/org/permanent/permanent/ui/myFiles/RecordGridViewHolder.kt index 51da6b81..10e1980c 100644 --- a/app/src/main/java/org/permanent/permanent/ui/myFiles/RecordGridViewHolder.kt +++ b/app/src/main/java/org/permanent/permanent/ui/myFiles/RecordGridViewHolder.kt @@ -19,8 +19,6 @@ class RecordGridViewHolder( private val context: Context, private val binding: ItemGridRecordBinding, private val showMyFilesSimplified: Boolean, - private val isForSharePreviewScreen: Boolean, - private val isForSharesScreen: Boolean, private val recordListener: RecordListener ) : RecyclerView.ViewHolder(binding.root) { @@ -35,8 +33,7 @@ class RecordGridViewHolder( binding.root.setOnClickListener { recordListener.onRecordClick(record) } binding.btnOptions.setOnClickListener { recordListener.onRecordOptionsClick(record) } binding.btnOptions.visibility = - if (CurrentArchivePermissionsManager.instance.getAccessRole() == AccessRole.VIEWER && record.type == RecordType.FOLDER || - isForSharePreviewScreen || showMyFilesSimplified) View.GONE else View.VISIBLE + if (CurrentArchivePermissionsManager.instance.getAccessRole() == AccessRole.VIEWER && record.type == RecordType.FOLDER || showMyFilesSimplified) View.GONE else View.VISIBLE if (record.isThumbBlurred != null && record.isThumbBlurred!! diff --git a/app/src/main/java/org/permanent/permanent/ui/myFiles/RecordsGridAdapter.kt b/app/src/main/java/org/permanent/permanent/ui/myFiles/RecordsGridAdapter.kt index 6aa316f5..a18ec2ea 100644 --- a/app/src/main/java/org/permanent/permanent/ui/myFiles/RecordsGridAdapter.kt +++ b/app/src/main/java/org/permanent/permanent/ui/myFiles/RecordsGridAdapter.kt @@ -15,8 +15,6 @@ class RecordsGridAdapter( private val isRelocateMode: MutableLiveData, private val isSelectMode: MutableLiveData, private val previewState: MutableLiveData, - private val isForSharePreviewScreen: Boolean, - private val isForSharesScreen: Boolean, private val recordListener: RecordListener ) : RecordsAdapter() { private var records: MutableList = ArrayList() @@ -31,8 +29,6 @@ class RecordsGridAdapter( parent.context, binding, showMyFilesSimplified, - isForSharePreviewScreen, - isForSharesScreen, recordListener ) } diff --git a/app/src/main/java/org/permanent/permanent/ui/public/PublicArchiveFragment.kt b/app/src/main/java/org/permanent/permanent/ui/public/PublicArchiveFragment.kt index 31af261d..2b4757bb 100644 --- a/app/src/main/java/org/permanent/permanent/ui/public/PublicArchiveFragment.kt +++ b/app/src/main/java/org/permanent/permanent/ui/public/PublicArchiveFragment.kt @@ -105,8 +105,6 @@ class PublicArchiveFragment : PermanentBaseFragment(), RecordListener { MutableLiveData(false), MutableLiveData(false), MutableLiveData(PreviewState.ACCESS_GRANTED), - isForSharePreviewScreen = false, - isForSharesScreen = false, recordListener = this ) recordsRecyclerView.apply { diff --git a/app/src/main/java/org/permanent/permanent/ui/public/PublicFilesFragment.kt b/app/src/main/java/org/permanent/permanent/ui/public/PublicFilesFragment.kt index d2710df6..18df9716 100644 --- a/app/src/main/java/org/permanent/permanent/ui/public/PublicFilesFragment.kt +++ b/app/src/main/java/org/permanent/permanent/ui/public/PublicFilesFragment.kt @@ -405,8 +405,6 @@ class PublicFilesFragment : PermanentBaseFragment() { viewModel.getIsRelocationMode(), viewModel.getIsSelectionMode(), MutableLiveData(PreviewState.ACCESS_GRANTED), - isForSharePreviewScreen = false, - isForSharesScreen = false, recordListener = viewModel ) val isListViewMode = prefsHelper.isListViewMode() diff --git a/app/src/main/java/org/permanent/permanent/ui/public/PublicFolderFragment.kt b/app/src/main/java/org/permanent/permanent/ui/public/PublicFolderFragment.kt index 71e38f67..b5d131f3 100644 --- a/app/src/main/java/org/permanent/permanent/ui/public/PublicFolderFragment.kt +++ b/app/src/main/java/org/permanent/permanent/ui/public/PublicFolderFragment.kt @@ -81,8 +81,6 @@ class PublicFolderFragment : PermanentBaseFragment(), RecordListener { MutableLiveData(false), MutableLiveData(false), MutableLiveData(PreviewState.ACCESS_GRANTED), - isForSharePreviewScreen = false, - isForSharesScreen = false, recordListener = this ) recordsRecyclerView.apply { diff --git a/app/src/main/java/org/permanent/permanent/ui/settings/compose/SettingsMenuScreen.kt b/app/src/main/java/org/permanent/permanent/ui/settings/compose/SettingsMenuScreen.kt index ae1840e6..59bf729f 100644 --- a/app/src/main/java/org/permanent/permanent/ui/settings/compose/SettingsMenuScreen.kt +++ b/app/src/main/java/org/permanent/permanent/ui/settings/compose/SettingsMenuScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -88,8 +89,9 @@ fun SettingsMenuScreen( }, sheetState = sheetState, dragHandle = null, + windowInsets = WindowInsets(0, 0, 0, 0), modifier = Modifier - .fillMaxHeight(0.95f) + .fillMaxHeight(0.9f) .then( if (viewModel.isTablet()) Modifier.width(configuration.screenWidthDp.dp * 0.5f) // 50% of the screen width for tablets else Modifier.fillMaxWidth() // Full width for phones @@ -201,7 +203,7 @@ fun SettingsMenuScreen( colorResource(R.color.error500), ) { onSignOutClick() } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(24.dp)) } } } diff --git a/app/src/main/java/org/permanent/permanent/ui/shareManagement/compose/GeneralAccessPage.kt b/app/src/main/java/org/permanent/permanent/ui/shareManagement/compose/GeneralAccessPage.kt index 8fbc5743..2b009d3c 100644 --- a/app/src/main/java/org/permanent/permanent/ui/shareManagement/compose/GeneralAccessPage.kt +++ b/app/src/main/java/org/permanent/permanent/ui/shareManagement/compose/GeneralAccessPage.kt @@ -78,7 +78,7 @@ enum class AccessType(val value: Int) { } companion object { - fun fromBackendValue(value: String): AccessType? = - entries.firstOrNull { it.backendValue == value } + fun fromBackendValue(value: String): AccessType = + entries.firstOrNull { it.backendValue == value } ?: RESTRICTED } } \ No newline at end of file diff --git a/app/src/main/java/org/permanent/permanent/ui/shares/SharePreviewFragment.kt b/app/src/main/java/org/permanent/permanent/ui/shares/SharePreviewFragment.kt index c193553a..f3189f45 100644 --- a/app/src/main/java/org/permanent/permanent/ui/shares/SharePreviewFragment.kt +++ b/app/src/main/java/org/permanent/permanent/ui/shares/SharePreviewFragment.kt @@ -5,22 +5,22 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView import org.permanent.permanent.Constants import org.permanent.permanent.R -import org.permanent.permanent.databinding.FragmentSharePreviewBinding -import org.permanent.permanent.models.Record import org.permanent.permanent.ui.PermanentBaseFragment import org.permanent.permanent.ui.PreferencesHelper import org.permanent.permanent.ui.archives.ArchivesContainerFragment import org.permanent.permanent.ui.login.AuthenticationActivity -import org.permanent.permanent.ui.myFiles.RecordsGridAdapter +import org.permanent.permanent.ui.shares.compose.SharePreviewScreen import org.permanent.permanent.viewmodels.SharePreviewViewModel @@ -32,9 +32,6 @@ const val SHOW_SCREEN_SIMPLIFIED_KEY = "show_screen_simplified" class SharePreviewFragment : PermanentBaseFragment() { private lateinit var prefsHelper: PreferencesHelper - private lateinit var recordsRecyclerView: RecyclerView - private lateinit var recordsAdapter: RecordsGridAdapter - private lateinit var binding: FragmentSharePreviewBinding private lateinit var viewModel: SharePreviewViewModel private var archivesContainerFragment: ArchivesContainerFragment? = null private var urlToken: String? = null @@ -45,12 +42,6 @@ class SharePreviewFragment : PermanentBaseFragment() { savedInstanceState: Bundle? ): View { viewModel = ViewModelProvider(this)[SharePreviewViewModel::class.java] - binding = FragmentSharePreviewBinding.inflate(inflater, container, false) - binding.executePendingBindings() - binding.lifecycleOwner = this - binding.viewModel = viewModel - initRecordsRecyclerView(binding.rvRecords) - prefsHelper = PreferencesHelper( requireContext().getSharedPreferences( org.permanent.permanent.ui.PREFS_NAME, android.content.Context.MODE_PRIVATE @@ -70,11 +61,30 @@ class SharePreviewFragment : PermanentBaseFragment() { } } } - return binding.root + + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MaterialTheme { + SharePreviewScreen( + viewModel = viewModel, +// onChangeArchiveClick = { viewModel.onChangeArchiveBtnClick() }, +// onViewInArchiveClick = { viewModel.onViewInArchiveBtnClick() }, +// onOkClick = { viewModel.onOkBtnClick() } + ) + } + } + } } - private val onRecordsRetrieved = Observer> { - recordsAdapter.setRecords(it) + private val onRecordDisplayName = Observer { title -> + val toolbar = requireActivity().findViewById(R.id.toolbar) + + lifecycleScope.launchWhenStarted { + if (!title.isNullOrEmpty()) { + toolbar.title = title + } + } } private val onChangeArchive = Observer { @@ -106,33 +116,15 @@ class SharePreviewFragment : PermanentBaseFragment() { findNavController().navigateUp() } - private fun initRecordsRecyclerView(rvRecords: RecyclerView) { - recordsRecyclerView = rvRecords - recordsAdapter = RecordsGridAdapter( - this, false, - MutableLiveData(false), - MutableLiveData(false), - viewModel.getCurrentState(), - isForSharePreviewScreen = true, - isForSharesScreen = false, - recordListener = viewModel - ) - recordsRecyclerView.apply { - setHasFixedSize(true) - layoutManager = GridLayoutManager(context, 2) - adapter = recordsAdapter - } - } - override fun connectViewModelEvents() { - viewModel.getOnRecordsRetrieved().observe(this, onRecordsRetrieved) + viewModel.getRecordDisplayName().observe(this, onRecordDisplayName) viewModel.getOnChangeArchive().observe(this, onChangeArchive) viewModel.getOnViewInArchive().observe(this, onViewInArchive) viewModel.getOnNavigateUp().observe(this, onNavigateUp) } override fun disconnectViewModelEvents() { - viewModel.getOnRecordsRetrieved().removeObserver(onRecordsRetrieved) + viewModel.getRecordDisplayName().removeObserver(onRecordDisplayName) viewModel.getOnChangeArchive().removeObserver(onChangeArchive) viewModel.getOnViewInArchive().removeObserver(onViewInArchive) viewModel.getOnNavigateUp().removeObserver(onNavigateUp) diff --git a/app/src/main/java/org/permanent/permanent/ui/shares/SharedXMeFragment.kt b/app/src/main/java/org/permanent/permanent/ui/shares/SharedXMeFragment.kt index ba61faab..857baf07 100644 --- a/app/src/main/java/org/permanent/permanent/ui/shares/SharedXMeFragment.kt +++ b/app/src/main/java/org/permanent/permanent/ui/shares/SharedXMeFragment.kt @@ -414,8 +414,6 @@ class SharedXMeFragment : PermanentBaseFragment() { viewModel.getIsRelocationMode(), viewModel.getIsSelectionMode(), MutableLiveData(PreviewState.ACCESS_GRANTED), - isForSharePreviewScreen = false, - isForSharesScreen = true, recordListener = viewModel ) val isListViewMode = prefsHelper.isListViewMode() diff --git a/app/src/main/java/org/permanent/permanent/ui/shares/compose/SharePreviewScreen.kt b/app/src/main/java/org/permanent/permanent/ui/shares/compose/SharePreviewScreen.kt new file mode 100644 index 00000000..ba0b661b --- /dev/null +++ b/app/src/main/java/org/permanent/permanent/ui/shares/compose/SharePreviewScreen.kt @@ -0,0 +1,395 @@ +package org.permanent.permanent.ui.shares.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import org.permanent.permanent.R +import org.permanent.permanent.models.Record +import org.permanent.permanent.models.RecordType +import org.permanent.permanent.ui.composeComponents.CircularProgressIndicator +import org.permanent.permanent.ui.composeComponents.OverlayColor +import org.permanent.permanent.ui.shareManagement.compose.AccessType +import org.permanent.permanent.ui.shares.PreviewState +import org.permanent.permanent.viewmodels.SharePreviewViewModel + +@Composable +fun SharePreviewScreen( + viewModel: SharePreviewViewModel +) { + val archiveThumbURL by viewModel.archiveThumbURL.collectAsState() + val rawAccountName by viewModel.rawAccountName.collectAsState() + val rawArchiveName by viewModel.rawArchiveName.collectAsState() + val currentState by viewModel.currentState.collectAsState() + val records by viewModel.records.collectAsState() + val isBusy by viewModel.isBusy.collectAsState() + val accessType by viewModel.accessType.collectAsState() + + val boldFont = FontFamily(Font(R.font.usual_bold)) + val mediumFont = FontFamily(Font(R.font.usual_medium)) + val regularFont = FontFamily(Font(R.font.usual_regular)) + + val hasBlurredContent = accessType != AccessType.ANYONE_CAN_VIEW + + Box( + modifier = Modifier + .fillMaxSize() + .background(colorResource(R.color.blue25)) + ) { + + if (currentState == PreviewState.ERROR) { + ErrorState(boldFont) + } else { + Column(modifier = Modifier.fillMaxSize()) { + + SharedByHeader( + archiveThumbURL = archiveThumbURL, + rawAccountName = rawAccountName, + rawArchiveName = rawArchiveName, + mediumFont = mediumFont, + regularFont = regularFont + ) + + Box(modifier = Modifier.fillMaxWidth()) { + + RecordsLayout( + records = records, + accessType = accessType, + isBusy = isBusy, + modifier = Modifier + .fillMaxWidth() + .then(if (hasBlurredContent) Modifier.blur(12.dp) else Modifier) + ) + + if (hasBlurredContent) { + Box( + modifier = Modifier + .matchParentSize() + .background(Color(0xA3F4F6FD)) + ) + } + } + } + } + + if (isBusy) { + CircularProgressIndicator(overlayColor = OverlayColor.LIGHT) + } + } +} + +@Composable +private fun ErrorState( + boldFont: FontFamily +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.share_preview_link_unavailable), + fontSize = 18.sp, + fontFamily = boldFont, + color = colorResource(R.color.blue900), + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 32.dp) + ) + } +} + +@Composable +private fun SharedByHeader( + archiveThumbURL: String, + rawAccountName: String, + rawArchiveName: String, + mediumFont: FontFamily, + regularFont: FontFamily +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 24.dp, top = 24.dp, end = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + if (archiveThumbURL.isNotEmpty()) { + AsyncImage( + model = archiveThumbURL, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(6.dp)) + ) + } else { + Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(6.dp)) + .background(colorResource(R.color.blue100)) + ) + } + + Spacer(modifier = Modifier.width(10.dp)) + + Column { + val text = buildAnnotatedString { + withStyle(SpanStyle(fontFamily = regularFont)) { + append(stringResource(R.string.share_preview_shared_by)) + append(" ") + } + withStyle(SpanStyle(fontFamily = mediumFont, fontWeight = FontWeight.Bold)) { + append(rawAccountName) + } + withStyle(SpanStyle(fontFamily = regularFont)) { + append(" ") + append(stringResource(R.string.share_preview_from)) + } + } + + Text( + text = text, + fontSize = 12.sp, + lineHeight = 16.sp, + color = colorResource(R.color.blue600), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = rawArchiveName, + fontSize = 14.sp, + lineHeight = 24.sp, + fontFamily = regularFont, + color = colorResource(R.color.blue900), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun RecordsLayout( + records: List, + accessType: AccessType?, + isBusy: Boolean, + modifier: Modifier = Modifier +) { + val gap = 8.dp + + if (!isBusy && records.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.folder_is_empty), + fontFamily = FontFamily(Font(R.font.usual_regular)), + fontSize = 18.sp, + color = colorResource(R.color.blue900), + textAlign = TextAlign.Center + ) + } + return + } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(start = 24.dp, top = 24.dp, end = 24.dp), + verticalArrangement = Arrangement.spacedBy(gap) + ) { + + // ✅ Single item special case + if (records.size == 1 && records.first().type != RecordType.FOLDER) { + val record = records.first() + + RecordGridItem( + record = record, + accessType = accessType, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) + return@Column + } + + // Always use 4-slot layout + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.spacedBy(gap) + ) { + + // Left column (2 stacked) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(gap) + ) { + + // Slot 0 + records.getOrNull(0)?.let { + RecordGridItem( + record = it, + accessType = accessType, + modifier = Modifier.aspectRatio(1f) + ) + } ?: Spacer(modifier = Modifier.aspectRatio(1f)) + + // Slot 2 + records.getOrNull(2)?.let { + RecordGridItem( + record = it, + accessType = accessType, + modifier = Modifier.aspectRatio(1f) + ) + } ?: Spacer(modifier = Modifier.aspectRatio(1f)) + } + + // Right tall item (slot 1) + records.getOrNull(1)?.let { + RecordGridItem( + record = it, + accessType = accessType, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) + } ?: Spacer( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) + } + + // Bottom item (slot 3) + records.getOrNull(3)?.let { + RecordGridItem( + record = it, + accessType = accessType, + isLastItem = true, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Composable +private fun RecordGridItem( + record: Record, + accessType: AccessType?, + isLastItem: Boolean = false, + modifier: Modifier = Modifier +) { + val thumbUrl = record.thumbURL2000 ?: "" + + val isBlurred = accessType != AccessType.ANYONE_CAN_VIEW + + Box( + modifier = modifier + .clip( + if (isLastItem) { + RoundedCornerShape( + topStart = 12.dp, + topEnd = 12.dp, + bottomStart = 0.dp, + bottomEnd = 0.dp + ) + } else { + RoundedCornerShape(12.dp) + } + ) + .background(colorResource(R.color.white)) + ) { + + when { + isBlurred && record.localDrawableRes != null -> { + Image( + painter = painterResource(record.localDrawableRes!!), + contentDescription = record.displayName, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } + + thumbUrl.isNotEmpty() && !isBlurred -> { + AsyncImage( + model = thumbUrl, + contentDescription = record.displayName, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } + + else -> { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(R.drawable.ic_folder_purple_gradient), + contentDescription = record.displayName, + tint = Color.Unspecified, + modifier = Modifier.size(72.dp) + ) + + if (!record.displayName.isNullOrEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = record.displayName ?: "", + fontFamily = FontFamily(Font(R.font.usual_regular)), + fontSize = 12.sp, + lineHeight = 16.sp, + color = colorResource(R.color.blue900), + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/permanent/permanent/viewmodels/ShareManagementViewModel.kt b/app/src/main/java/org/permanent/permanent/viewmodels/ShareManagementViewModel.kt index bf593b22..81d22e5e 100644 --- a/app/src/main/java/org/permanent/permanent/viewmodels/ShareManagementViewModel.kt +++ b/app/src/main/java/org/permanent/permanent/viewmodels/ShareManagementViewModel.kt @@ -173,8 +173,8 @@ class ShareManagementViewModel(application: Application) : ObservableAndroidView shareByUrlVO.shareby_urlId?.let { shareByUrlId -> _isBusyState.value = true stelaAccountRepository.getShareLink( - mutableListOf(shareByUrlId), - object : ILinkListener { + shareLinkIds = mutableListOf(shareByUrlId), + listener = object : ILinkListener { override fun onSuccess(shareLink: ShareLinkVO?) { _isBusyState.value = false @@ -204,8 +204,7 @@ class ShareManagementViewModel(application: Application) : ObservableAndroidView private fun initLinkSettings(shareLink: ShareLinkVO) { shareLink.accessRestrictions?.let { accessRestriction -> - _selectedGeneralAccessType.value = - AccessType.fromBackendValue(accessRestriction) ?: AccessType.ANYONE_CAN_VIEW + _selectedGeneralAccessType.value = AccessType.fromBackendValue(accessRestriction) } _selectedAccessRole.value = if (shareLink.permissionsLevel == AccessRole.MANAGER.lowerCase()) AccessRole.CURATOR else AccessRole.fromStelaBackendValue( diff --git a/app/src/main/java/org/permanent/permanent/viewmodels/SharePreviewViewModel.kt b/app/src/main/java/org/permanent/permanent/viewmodels/SharePreviewViewModel.kt index 3cdad270..2e24867d 100644 --- a/app/src/main/java/org/permanent/permanent/viewmodels/SharePreviewViewModel.kt +++ b/app/src/main/java/org/permanent/permanent/viewmodels/SharePreviewViewModel.kt @@ -1,159 +1,291 @@ package org.permanent.permanent.viewmodels import android.app.Application -import android.content.Context -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import org.permanent.permanent.models.Archive -import org.permanent.permanent.models.Folder +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.permanent.permanent.R import org.permanent.permanent.models.Record +import org.permanent.permanent.models.RecordType import org.permanent.permanent.models.Share import org.permanent.permanent.models.Status -import org.permanent.permanent.network.IDataListener -import org.permanent.permanent.network.models.Datum +import org.permanent.permanent.network.ILinkListener +import org.permanent.permanent.network.models.IFolderChildrenListener +import org.permanent.permanent.network.models.ShareLinkVO import org.permanent.permanent.network.models.ShareVO import org.permanent.permanent.network.models.Shareby_urlVO import org.permanent.permanent.repositories.ArchiveRepositoryImpl import org.permanent.permanent.repositories.IArchiveRepository import org.permanent.permanent.repositories.IShareRepository import org.permanent.permanent.repositories.ShareRepositoryImpl -import org.permanent.permanent.ui.PREFS_NAME -import org.permanent.permanent.ui.PreferencesHelper +import org.permanent.permanent.repositories.StelaAccountRepository +import org.permanent.permanent.repositories.StelaAccountRepositoryImpl import org.permanent.permanent.ui.myFiles.RecordListener +import org.permanent.permanent.ui.shareManagement.compose.AccessType import org.permanent.permanent.ui.shares.PreviewState class SharePreviewViewModel(application: Application) : ObservableAndroidViewModel(application), RecordListener { - private val prefsHelper = PreferencesHelper( - application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - ) private lateinit var urlToken: String private var recordIdToView: Int? = null - private var archiveThumbURL = MutableLiveData() - private var recordDisplayName = MutableLiveData() - private var accountDisplayName = MutableLiveData() - private var archiveDisplayName = MutableLiveData() - private val currentState = MutableLiveData(PreviewState.NO_ACCESS) + private val _archiveThumbURL = MutableStateFlow("") + private val recordDisplayName = SingleLiveEvent() + private val _rawAccountName = MutableStateFlow("") + private val _rawArchiveName = MutableStateFlow("") + private val _records = MutableStateFlow>(emptyList()) + private val _accessType = MutableStateFlow(null) + private val _currentState = MutableStateFlow(PreviewState.NO_ACCESS) private val currentArchiveThumb = MutableLiveData() - private val currentArchiveName = MutableLiveData() + private val _currentArchiveName = MutableStateFlow("") private val isCurrentArchiveDefault = MutableLiveData(false) - private var showChangeArchiveButton = MutableLiveData(false) + private val _showChangeArchiveButton = MutableStateFlow(false) private val onRecordsRetrieved = SingleLiveEvent>() private val onChangeArchive = SingleLiveEvent() private val onViewInArchive = SingleLiveEvent() private val onNavigateUp = SingleLiveEvent() - private val isBusy = MutableLiveData() + private val _isBusy = MutableStateFlow(false) private val errorMessage = MutableLiveData() private var shareRepository: IShareRepository = ShareRepositoryImpl(application) private var archiveRepository: IArchiveRepository = ArchiveRepositoryImpl(application) + private var stelaAccountRepository: StelaAccountRepository = + StelaAccountRepositoryImpl(application) fun checkShareLink(urlToken: String) { this.urlToken = urlToken - if (isBusy.value != null && isBusy.value!!) { + if (_isBusy.value) { return } - isBusy.value = true + _isBusy.value = true shareRepository.checkShareLink(urlToken, object : IShareRepository.IShareByUrlListener { override fun onSuccess(shareByUrlVO: Shareby_urlVO?) { - isBusy.value = false + _isBusy.value = false // Loading data in the header - archiveThumbURL.value = shareByUrlVO?.ArchiveVO?.thumbURL200 - accountDisplayName.value = "Shared by ${shareByUrlVO?.AccountVO?.fullName}" - archiveDisplayName.value = "From the ${shareByUrlVO?.ArchiveVO?.fullName} Archive" - // Loading data in the list + _archiveThumbURL.value = shareByUrlVO?.ArchiveVO?.thumbURL200 ?: "" + _rawAccountName.value = shareByUrlVO?.AccountVO?.fullName ?: "" + _rawArchiveName.value = "The ${shareByUrlVO?.ArchiveVO?.fullName} Archive" + + // Loading toolbar title when { shareByUrlVO?.RecordVO != null -> { - recordIdToView = shareByUrlVO.RecordVO?.recordId - recordDisplayName.value = shareByUrlVO.RecordVO?.displayName - onRecordsRetrieved.value = listOf(Record(shareByUrlVO)) + recordDisplayName.value = shareByUrlVO.RecordVO?.displayName ?: "" } shareByUrlVO?.FolderVO != null -> { - recordIdToView = shareByUrlVO.FolderVO?.folderId - recordDisplayName.value = shareByUrlVO.FolderVO?.displayName - onRecordsRetrieved.value = Folder(shareByUrlVO).records + recordDisplayName.value = shareByUrlVO.FolderVO?.displayName ?: "" } } - isBusy.value = true - archiveRepository.getAllArchives(object : IDataListener { - override fun onSuccess(dataList: List?) { - isBusy.value = false - if (!dataList.isNullOrEmpty()) { - var notPendingArchives = 0 + // Get link from Stela for accessRestrictions (determines blur) + _isBusy.value = true + stelaAccountRepository.getShareLink( + shareTokens = mutableListOf(urlToken), + listener = object : ILinkListener { + + override fun onSuccess(shareLink: ShareLinkVO?) { + _isBusy.value = false + + val access = + shareLink?.accessRestrictions?.let { AccessType.fromBackendValue(it) } + + _accessType.value = access - for (datum in dataList) { - val archive = Archive(datum.ArchiveVO) - if (archive.status != Status.PENDING) notPendingArchives++ + if (access != AccessType.ANYONE_CAN_VIEW) { + _records.value = getFakeBlurredRecords() + + } else if (shareByUrlVO?.RecordVO != null) { + _records.value = listOf(Record(shareByUrlVO)) + + } else if (shareByUrlVO?.FolderVO?.folderId != null) { + loadRealFolderChildren(shareByUrlVO.FolderVO?.folderId!!) } + } - showChangeArchiveButton.value = notPendingArchives > 1 + override fun onFailed(error: String?) { + _isBusy.value = false + _currentState.value = PreviewState.ERROR + errorMessage.value = error } - } - override fun onFailed(error: String?) { - isBusy.value = false - errorMessage.value = error - } - }) - - // Loading data in the footer - val shareVO = shareByUrlVO?.ShareVO - if (shareVO != null) { - val share = Share(shareVO) - - if (share.status.value == Status.PENDING) { - // Showing 'Awaiting for Access' text - currentState.value = PreviewState.AWAITING_ACCESS - } else { // Showing 'View in Archive' button - currentState.value = PreviewState.ACCESS_GRANTED - } - } else { - // Showing 'Request Access' button - currentState.value = PreviewState.NO_ACCESS - } - currentArchiveThumb.value = prefsHelper.getCurrentArchiveThumbURL() - currentArchiveName.value = prefsHelper.getCurrentArchiveFullName() - isCurrentArchiveDefault.value = - prefsHelper.getCurrentArchiveId() == prefsHelper.getDefaultArchiveId() + }) + +// _isBusy.value = true +// archiveRepository.getAllArchives(object : IDataListener { +// override fun onSuccess(dataList: List?) { +// _isBusy.value = false +// if (!dataList.isNullOrEmpty()) { +// var notPendingArchives = 0 +// +// for (datum in dataList) { +// val archive = Archive(datum.ArchiveVO) +// if (archive.status != Status.PENDING) notPendingArchives++ +// } +// +// _showChangeArchiveButton.value = notPendingArchives > 1 +// } +// } +// +// override fun onFailed(error: String?) { +// _isBusy.value = false +// _currentState.value = PreviewState.ERROR +// errorMessage.value = error +// } +// }) +// +// // Loading data in the footer +// val shareVO = shareByUrlVO?.ShareVO +// if (shareVO != null) { +// val share = Share(shareVO) +// +// if (share.status.value == Status.PENDING) { +// // Showing 'Awaiting for Access' text +// _currentState.value = PreviewState.AWAITING_ACCESS +// } else { // Showing 'View in Archive' button +// _currentState.value = PreviewState.ACCESS_GRANTED +// } +// } else { +// // Showing 'Request Access' button +// _currentState.value = PreviewState.NO_ACCESS +// } +// currentArchiveThumb.value = prefsHelper.getCurrentArchiveThumbURL() +// _currentArchiveName.value = prefsHelper.getCurrentArchiveFullName() ?: "" +// isCurrentArchiveDefault.value = +// prefsHelper.getCurrentArchiveId() == prefsHelper.getDefaultArchiveId() } override fun onFailed(error: String?) { - isBusy.value = false - currentState.value = PreviewState.ERROR + _isBusy.value = false + _currentState.value = PreviewState.ERROR errorMessage.value = error } }) } + private fun loadRealFolderChildren(folderId: Int) { + _isBusy.value = true + + stelaAccountRepository.getFolderChildren( + shareToken = urlToken, + folderId = folderId, + listener = object : IFolderChildrenListener { + + override fun onSuccess(records: List) { + _isBusy.value = false + _records.value = mapToPreviewLayout(records) + } + + override fun onFailed(error: String?) { + _isBusy.value = false + _currentState.value = PreviewState.ERROR + errorMessage.value = error + } + } + ) + } + + private fun mapToPreviewLayout(records: List): List { + val folders = records.filter { it.type == RecordType.FOLDER } + val images = records.filter { it.type == RecordType.FILE } + + // Case 1: No images → show folders only (max 4) + if (images.isEmpty()) { + return folders.take(4) + } + + val result = MutableList(4) { null } + + // Put first folder in position 0 (if exists) + var usedFolders = 0 + if (folders.isNotEmpty()) { + result[0] = folders[0] + usedFolders = 1 + } + + when (images.size) { + 1 -> { + result[3] = images[0] + } + + 2 -> { + result[2] = images[0] + result[3] = images[1] + } + + else -> { + val startIndex = if (folders.isNotEmpty()) 1 else 0 + val maxImages = 4 - startIndex + + images.take(maxImages).forEachIndexed { index, image -> + result[startIndex + index] = image + } + } + } + + // ✅ Fill remaining empty slots with remaining folders + val remainingFolders = folders.drop(usedFolders).iterator() + + for (i in result.indices) { + if (result[i] == null && remainingFolders.hasNext()) { + result[i] = remainingFolders.next() + } + } + + return result.filterNotNull() + } + fun onChangeArchiveBtnClick() { onChangeArchive.call() } fun onRequestAccessBtnClick() { - if (isBusy.value != null && isBusy.value!!) { + if (_isBusy.value) { return } - isBusy.value = true + _isBusy.value = true shareRepository.requestShareAccess(urlToken, object : IShareRepository.IShareListener { override fun onSuccess(shareVO: ShareVO?) { - isBusy.value = false + _isBusy.value = false if (shareVO != null && Share(shareVO).status.value == Status.OK) { - currentState.value = PreviewState.ACCESS_GRANTED + _currentState.value = PreviewState.ACCESS_GRANTED } else { - currentState.value = PreviewState.AWAITING_ACCESS + _currentState.value = PreviewState.AWAITING_ACCESS } } override fun onFailed(error: String?) { - isBusy.value = false - currentState.value = PreviewState.ERROR + _isBusy.value = false + _currentState.value = PreviewState.ERROR errorMessage.value = error } }) } + private fun getFakeBlurredRecords(): List { + return listOf( + // Fake folder (top-left) + Record(recordId = -1, folderLinkId = -1).apply { + type = RecordType.FOLDER + displayName = "Folder" + isThumbBlurred = true + }, + + // Fake images + Record(recordId = -2, folderLinkId = -1).apply { + isThumbBlurred = true + localDrawableRes = R.drawable.img_share_preview_tall + }, + Record(recordId = -3, folderLinkId = -1).apply { + isThumbBlurred = true + localDrawableRes = R.drawable.img_share_preview_small + }, + Record(recordId = -4, folderLinkId = -1).apply { + isThumbBlurred = true + localDrawableRes = R.drawable.img_share_preview_large + } + ) + } + fun onViewInArchiveBtnClick() { onViewInArchive.value = recordIdToView } @@ -170,23 +302,19 @@ class SharePreviewViewModel(application: Application) : ObservableAndroidViewMod override fun onRecordDeleteClick(record: Record) {} - fun getArchiveThumbURL(): MutableLiveData = archiveThumbURL + val archiveThumbURL: StateFlow = _archiveThumbURL.asStateFlow() fun getRecordDisplayName(): MutableLiveData = recordDisplayName - fun getAccountDisplayName(): MutableLiveData = accountDisplayName - - fun getArchiveDisplayName(): MutableLiveData = archiveDisplayName - - fun getCurrentState(): MutableLiveData = currentState + val currentState: StateFlow = _currentState.asStateFlow() fun getCurrentArchiveThumb(): MutableLiveData = currentArchiveThumb - fun getCurrentArchiveName(): MutableLiveData = currentArchiveName + val currentArchiveName: StateFlow = _currentArchiveName.asStateFlow() fun getIsCurrentArchiveDefault(): MutableLiveData = isCurrentArchiveDefault - fun getShowChangeArchiveButton(): MutableLiveData = showChangeArchiveButton + val showChangeArchiveButton: StateFlow = _showChangeArchiveButton.asStateFlow() fun getOnRecordsRetrieved(): MutableLiveData> = onRecordsRetrieved @@ -196,7 +324,15 @@ class SharePreviewViewModel(application: Application) : ObservableAndroidViewMod fun getOnNavigateUp(): MutableLiveData = onNavigateUp - fun getIsBusy(): MutableLiveData = isBusy + val isBusy: StateFlow = _isBusy.asStateFlow() + + fun getErrorMessage(): MutableLiveData = errorMessage + + val rawAccountName: StateFlow = _rawAccountName.asStateFlow() + + val rawArchiveName: StateFlow = _rawArchiveName.asStateFlow() + + val records: StateFlow> = _records.asStateFlow() - fun getErrorMessage(): LiveData = errorMessage + val accessType: StateFlow = _accessType.asStateFlow() } diff --git a/app/src/main/res/drawable/img_share_preview_large.png b/app/src/main/res/drawable/img_share_preview_large.png new file mode 100644 index 00000000..af7e4214 Binary files /dev/null and b/app/src/main/res/drawable/img_share_preview_large.png differ diff --git a/app/src/main/res/drawable/img_share_preview_small.png b/app/src/main/res/drawable/img_share_preview_small.png new file mode 100644 index 00000000..625143c0 Binary files /dev/null and b/app/src/main/res/drawable/img_share_preview_small.png differ diff --git a/app/src/main/res/drawable/img_share_preview_tall.png b/app/src/main/res/drawable/img_share_preview_tall.png new file mode 100644 index 00000000..afa935e4 Binary files /dev/null and b/app/src/main/res/drawable/img_share_preview_tall.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6c15d6fd..206c92b6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -14,13 +14,13 @@ + android:layout_height="match_parent" + android:background="@color/blue900"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -