diff --git a/app/src/androidTest/java/com/nmc/android/AlbumsResourceTest.kt b/app/src/androidTest/java/com/nmc/android/AlbumsResourceTest.kt new file mode 100644 index 000000000000..34a8c3413ab1 --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/AlbumsResourceTest.kt @@ -0,0 +1,275 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nmc.android + +import android.content.Context +import android.content.res.Configuration +import android.util.DisplayMetrics +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.owncloud.android.R +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Locale + +/** + * Test class to verify the strings and dimens customized in this branch PR for NMC + */ +@RunWith(AndroidJUnit4::class) +class AlbumsResourceTest { + + private val baseContext = ApplicationProvider.getApplicationContext() + + private val localizedStringMap = mapOf( + R.string.drawer_item_album to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Albums", + Locale.GERMAN to "Alben" + ) + ), R.string.create_album to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Create album", + Locale.GERMAN to "Album erstellen" + ) + ), R.string.create_album_dialog_title to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "New album", + Locale.GERMAN to "Neues Album" + ) + ), R.string.rename_album_dialog_title to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Rename album", + Locale.GERMAN to "Album umbenennen" + ) + ), R.string.rename_dialog_button to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Rename", + Locale.GERMAN to "Speichern" + ) + ), R.string.create_album_dialog_message to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Enter your new Album name", + Locale.GERMAN to "Gib einen Namen für das Album ein" + ) + ), R.string.album_name_empty to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Album name cannot be empty", + Locale.GERMAN to "Der Albumname darf nicht leer sein" + ) + ), R.string.add_more to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Add more", + Locale.GERMAN to "Mehr hinzufügen" + ) + ), R.string.album_rename to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Rename Album", + Locale.GERMAN to "Album umbenennen" + ) + ), R.string.album_delete to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Delete Album", + Locale.GERMAN to "Album löschen" + ) + ), R.string.album_delete_failed_message to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Failed to delete few of the files.", + Locale.GERMAN to "Einige Dateien konnten nicht gelöscht werden." + ) + ), R.string.album_already_exists to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Album already exists", + Locale.GERMAN to "Das Album existiert bereits" + ) + ), R.string.album_picker_toolbar_title to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Pick Album", + Locale.GERMAN to "Album auswählen" + ) + ), R.string.media_picker_toolbar_title to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Pick Media Files", + Locale.GERMAN to "Mediendateien auswählen" + ) + ), R.string.empty_albums_title to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Create Albums for your Photos", + Locale.GERMAN to "Erstelle Alben für deine Fotos" + ) + ), R.string.empty_albums_message to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "You can organize all your photos in as many albums as you like. You haven\'t created an album yet.", + Locale.GERMAN to "Sie können all Ihre Fotos in beliebig vielen Alben organisieren. Bisher haben Sie noch kein Album erstellt." + ) + ), R.string.add_to_album to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Add to album", + Locale.GERMAN to "Zum Album hinzufügen" + ) + ), R.string.album_file_added_message to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "File added successfully", + Locale.GERMAN to "Datei erfolgreich hinzugefügt" + ) + ), R.string.empty_album_detailed_view_title to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "All that\'s missing are your photos", + Locale.GERMAN to "Es fehlen nur noch Ihre Fotos" + ) + ), R.string.empty_album_detailed_view_message to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "You can add as many photos as you like. A photo can also belong to more than one album.", + Locale.GERMAN to "Sie können so viele Fotos hinzufügen, wie Sie möchten. Ein Foto kann auch mehreren Alben zugeordnet werden." + ) + ), R.string.add_photos to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Add photos", + Locale.GERMAN to "Fotos hinzufügen" + ) + ), R.string.album_unsupported_file to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Unsupported media", + Locale.GERMAN to "Nicht unterstützte Medien" + ) + ), R.string.album_upload_from_camera_roll to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Upload from cameraroll", + Locale.GERMAN to "Dateien hochladen" + ) + ), R.string.album_upload_from_account to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Select images from account", + Locale.GERMAN to "Dateien auswählen" + ) + ), R.string.album_rename_conflict to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "This name is already in use.", + Locale.GERMAN to "Dieser Name wird bereits verwendet." + ) + ), R.string.album_copy_file_conflict to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Already exists.", + Locale.GERMAN to "Existiert bereits." + ) + ), + ) + + @Test + fun verifyLocalizedStrings() { + localizedStringMap.forEach { (stringRes, expected) -> + expected.translations.forEach { (locale, expectedText) -> + + val config = Configuration(baseContext.resources.configuration) + config.setLocale(locale) + + val localizedContext = baseContext.createConfigurationContext(config) + val actualText = localizedContext.getString(stringRes) + + assertEquals( + "Mismatch for ${baseContext.resources.getResourceEntryName(stringRes)} in $locale", + expectedText, + actualText + ) + } + } + } + + data class ExpectedLocalizedString(val translations: Map) + + private val expectedDimenMap = mapOf( + R.dimen.album_list_image_width to ExpectedDimen( + default = 78f, + unit = DimenUnit.DP + ), + R.dimen.album_list_image_height to ExpectedDimen( + default = 56f, + unit = DimenUnit.DP + ), + R.dimen.album_grid_image_height to ExpectedDimen( + default = 140f, + unit = DimenUnit.DP + ), + R.dimen.album_grid_image_corner_radius to ExpectedDimen( + default = 8f, + unit = DimenUnit.DP + ), + R.dimen.album_list_image_corner_radius to ExpectedDimen( + default = 4f, + unit = DimenUnit.DP + ), + R.dimen.album_grid_spacing to ExpectedDimen( + default = 4f, + unit = DimenUnit.DP + ), + R.dimen.album_recycler_view_grid_padding to ExpectedDimen( + default = 8f, + unit = DimenUnit.DP + ), + ) + + @Test + fun validateDefaultDimens() { + validateDimens( + configModifier = { it }, // no change → default values + ) { it.default to it.unit } + } + + @Test + fun validate_sw600dp_Dimens() { + validateDimens(configModifier = { config -> + config.smallestScreenWidthDp = 600 + config + }) { it.alt to it.unit } + } + + private fun validateDimens( + configModifier: (Configuration) -> Configuration, + selector: (ExpectedDimen) -> Pair + ) { + val baseConfig = Configuration(baseContext.resources.configuration) + val testConfig = configModifier(baseConfig) + val testContext = baseContext.createConfigurationContext(testConfig) + val dm = testContext.resources.displayMetrics + val config = testContext.resources.configuration + expectedDimenMap.forEach { (resId, entry) -> + val (value, unit) = selector(entry) + val actualPx = testContext.resources.getDimension(resId) + value?.let { + val expectedPx = convertToPx(value, unit, dm, config) + assertEquals( + "Mismatch for ${testContext.resources.getResourceEntryName(resId)} ($unit)", + expectedPx, + actualPx, + 0.01f + ) + } + } + } + + private fun convertToPx( + value: Float, + unit: DimenUnit, + dm: DisplayMetrics, + config: Configuration + ): Float { + return when (unit) { + DimenUnit.DP -> value * dm.density + DimenUnit.SP -> value * dm.density * config.fontScale + DimenUnit.PX -> value + } + } + + data class ExpectedDimen( + val default: Float, + val alt: Float? = null, + val unit: DimenUnit, + ) + + enum class DimenUnit { DP, SP, PX } +} diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index f1a2ac752b30..39d6797dbf23 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -26,11 +26,11 @@ import com.nextcloud.client.widget.DashboardWidgetService; import com.nextcloud.ui.ChooseAccountDialogFragment; import com.nextcloud.ui.ChooseStorageLocationDialogFragment; -import com.nextcloud.ui.fileInfo.FileInfoFragment; import com.nextcloud.ui.SetOnlineStatusBottomSheet; import com.nextcloud.ui.SetStatusMessageBottomSheet; import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet; import com.nextcloud.ui.composeActivity.ComposeActivity; +import com.nextcloud.ui.fileInfo.FileInfoFragment; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nextcloud.ui.tags.TagManagementBottomSheet; import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet; @@ -110,17 +110,17 @@ import com.owncloud.android.ui.fragment.GalleryFragment; import com.owncloud.android.ui.fragment.GalleryFragmentBottomSheetDialog; import com.owncloud.android.ui.fragment.GroupfolderListFragment; -import com.owncloud.android.ui.fragment.localfilelist.LocalFileListFragment; import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.SharedListFragment; import com.owncloud.android.ui.fragment.UnifiedSearchFragment; import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment; -import com.owncloud.android.ui.fragment.albums.bottomsheet.AlbumSharingBottomSheet; import com.owncloud.android.ui.fragment.albums.AlbumsFragment; +import com.owncloud.android.ui.fragment.albums.bottomsheet.AlbumSharingBottomSheet; import com.owncloud.android.ui.fragment.community.CommunityFragment; import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment; import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment; +import com.owncloud.android.ui.fragment.localfilelist.LocalFileListFragment; import com.owncloud.android.ui.fragment.notifications.NotificationsFragment; import com.owncloud.android.ui.navigation.NavigatorActivity; import com.owncloud.android.ui.preview.FileDownloadFragment; diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 06fda4b00a25..56a495e7415d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -104,6 +104,7 @@ class BackgroundJobFactory @Inject constructor( FileUploadWorker::class -> createFilesUploadWorker(context, workerParameters) AlbumFileUploadWorker::class -> createAlbumsFilesUploadWorker(context, workerParameters) FileDownloadWorker::class -> createFilesDownloadWorker(context, workerParameters) + AlbumFileUploadWorker::class -> createAlbumsFilesUploadWorker(context, workerParameters) GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) TestJob::class -> createTestJob(context, workerParameters) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index a9b678592512..19623431b042 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -462,7 +462,11 @@ class FileUploadHelper { @Suppress("ReturnCount") fun isUploadingNow(upload: OCUpload?): Boolean { - val currentUploadFileOperation = FileUploadWorker.getCurrentUpload(upload?.uploadId) + var currentUploadFileOperation = FileUploadWorker.getCurrentUpload(upload?.uploadId) + // NMC Customization: to check for Files uploaded through albums + if (currentUploadFileOperation == null) { + currentUploadFileOperation = AlbumFileUploadWorker.getCurrentUpload(upload?.uploadId) + } if (currentUploadFileOperation == null || currentUploadFileOperation.user == null) return false if (upload == null || upload.accountName != currentUploadFileOperation.user.accountName) return false diff --git a/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemAction.kt b/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemAction.kt index f584b31006d1..56c8b1731496 100644 --- a/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemAction.kt +++ b/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemAction.kt @@ -7,25 +7,25 @@ package com.nextcloud.ui.albumItemActions +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes +import androidx.annotation.StringRes import com.owncloud.android.R -enum class AlbumItemAction(val id: Int, val titleId: Int, val iconId: Int) { - UPLOAD_FROM_CAMERA_ROLL( - R.id.action_upload_from_camera_roll, - R.string.upload_direct_camera_upload, - R.drawable.ic_camera - ), - SELECT_IMAGES_FROM_ACCOUNT( - R.id.action_select_images_from_account, - R.string.album_upload_from_account, - R.drawable.file_image - ); +enum class AlbumItemAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRes val icon: Int? = null) { + RENAME_ALBUM(R.id.action_rename_album, R.string.album_rename, R.drawable.ic_edit), + SHARE_ALBUM(R.id.action_share_album, R.string.album_share, R.drawable.ic_share), + DELETE_ALBUM(R.id.action_delete_album, R.string.album_delete, R.drawable.ic_delete); companion object { + /** + * All file actions, in the order they should be displayed + */ @JvmField val SORTED_VALUES = listOf( - UPLOAD_FROM_CAMERA_ROLL, - SELECT_IMAGES_FROM_ACCOUNT + RENAME_ALBUM, + SHARE_ALBUM, + DELETE_ALBUM ) } } diff --git a/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemActionsBottomSheet.kt index 8a31fd89355f..837f114b1ee6 100644 --- a/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemActionsBottomSheet.kt @@ -49,9 +49,6 @@ class AlbumItemActionsBottomSheet : val bottomSheetDialog = dialog as BottomSheetDialog bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED bottomSheetDialog.behavior.skipCollapsed = true - - viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE) - return binding.root } @@ -96,13 +93,10 @@ class AlbumItemActionsBottomSheet : root.setOnClickListener { dispatchActionClick(action.id) } - text.setText(action.titleId) - val drawable = - viewThemeUtils.platform.tintDrawable( - requireContext(), - AppCompatResources.getDrawable(requireContext(), action.iconId)!! - ) - icon.setImageDrawable(drawable) + text.setText(action.title) + if (action.icon != null) { + icon.setImageResource(action.icon) + } } return itemBinding.root } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt index eb69869263bb..33f30d92103b 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt @@ -69,7 +69,7 @@ enum class FileAction( RETRY(R.id.action_retry, R.string.retry, R.drawable.ic_retry), // Add to Album operation for image and video files - ADD_TO_ALBUM(R.id.action_add_to_album, R.string.add_to_album, R.drawable.ic_album); + ADD_TO_ALBUM(R.id.action_add_to_album, R.string.add_to_album, R.drawable.nav_albums); constructor(id: Int, title: Int) : this(id, title, null) diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java index c328008b7b19..def5851c82b7 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -457,7 +457,7 @@ private void nextOperation() { if (!result.isSuccess()) { final var code = "code: " + result.getCode(); final var httpCode = "HTTP_CODE: " + result.getHttpCode(); - Log_OC.e(TAG,"Operation failed " + code + httpCode); + Log_OC.e(TAG, "Operation failed " + code + httpCode); } } catch (UnsupportedOperationException e) { // TODO remove - added to aid in transition to NextcloudClient diff --git a/app/src/main/java/com/owncloud/android/ui/activity/AlbumsPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/AlbumsPickerActivity.kt index 0c0df32e2298..8c9e3b19f142 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/AlbumsPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/AlbumsPickerActivity.kt @@ -58,6 +58,7 @@ class AlbumsPickerActivity : initBinding() setupToolbar() + showHideDefaultToolbarDivider(true) setupAction() setupActionBar() initExtras() @@ -100,14 +101,8 @@ class AlbumsPickerActivity : captionText = resources.getText(R.string.media_picker_toolbar_title).toString() } - folderPickerBinding.run { - folderPickerBtnCopy.visibility = View.GONE - folderPickerBtnMove.visibility = View.GONE - folderPickerBtnChoose.visibility = View.GONE - folderPickerBtnCancel.visibility = View.GONE - chooseButtonSpacer.visibility = View.GONE - moveOrCopyButtonSpacer.visibility = View.GONE - } + folderPickerBinding.bottomLayout.visibility = View.GONE + folderPickerBinding.divider.visibility = View.GONE } private fun createFragments() { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index a097a6b2d47a..2a748a02f725 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -106,6 +106,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Objects; import javax.inject.Inject; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index e48cdc9f8912..d4fbc56afd60 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -820,7 +820,6 @@ class FileDisplayActivity : var leftFragment: Fragment? get() = supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) - // Replaces the first fragment managed by the activity with the received as a parameter. private set(fragment) { setLeftFragment(fragment, true) @@ -2533,6 +2532,113 @@ class FileDisplayActivity : } } + private fun onRemoveAlbumOperationFinish(operation: RemoveAlbumRemoteOperation, result: RemoteOperationResult<*>) { + if (result.isSuccess) { + + val fragment = supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG) + if (fragment is AlbumItemsFragment) { + fragment.onAlbumDeleted() + } + } else { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + + if (result.isSslRecoverableException) { + mLastSslUntrustedServerResult = result + showUntrustedCertDialog(mLastSslUntrustedServerResult) + } + } + } + + private fun onAlbumPublicLinkOperationFinish( + operation: PublicShareLinkAlbumRemoteOperation, + result: RemoteOperationResult<*> + ) { + if (result.isSuccess) { + val fragment = supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG) + if (fragment is AlbumItemsFragment) { + fragment.refreshAlbumMetaData() + } + } else { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + + if (result.isSslRecoverableException) { + mLastSslUntrustedServerResult = result + showUntrustedCertDialog(mLastSslUntrustedServerResult) + } + } + } + + private fun onCopyAlbumFileOperationFinish(operation: CopyFileToAlbumOperation, result: RemoteOperationResult<*>) { + if (result.isSuccess) { + // when item added from inside of Album + val fragment = supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG) + if (fragment is AlbumItemsFragment) { + fragment.refreshData() + } + Log_OC.e(TAG, "Files copied successfully") + } else { + try { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + } catch (e: Resources.NotFoundException) { + Log_OC.e(TAG, "Error while trying to show fail message ", e) + } + } + } + + private fun onRenameAlbumOperationFinish(operation: RenameAlbumRemoteOperation, result: RemoteOperationResult<*>) { + if (result.isSuccess) { + + val fragment = supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG) + if (fragment is AlbumItemsFragment) { + fragment.onAlbumRenamed(operation.newAlbumName) + } + } else { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + + if (result.isSslRecoverableException) { + mLastSslUntrustedServerResult = result + showUntrustedCertDialog(mLastSslUntrustedServerResult) + } + } + } + + private fun onCreateAlbumOperationFinish( + operation: CreateNewAlbumRemoteOperation, + result: RemoteOperationResult<*> + ) { + if (result.isSuccess) { + val fragment = supportFragmentManager.findFragmentByTag(AlbumsFragment.TAG) + if (fragment is AlbumsFragment) { + fragment.navigateToAlbumItemsFragment(operation.newAlbumName, true) + } + } else { + try { + if (RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS == result.code) { + DisplayUtils.showSnackMessage(this, R.string.album_already_exists) + } else { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + } + } catch (e: Resources.NotFoundException) { + Log_OC.e(TAG, "Error while trying to show fail message ", e) + } + } + } + /** * {@inheritDoc} */ diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumGridItemViewHolder.kt index 152a3d3137ac..c2dea316e85e 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumGridItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumGridItemViewHolder.kt @@ -18,6 +18,8 @@ internal class AlbumGridItemViewHolder(private var binding: AlbumsGridItemBindin AlbumItemViewHolder { override val thumbnail: ImageView get() = binding.thumbnail + override val shimmerThumbnail: LoaderImageView + get() = binding.thumbnailShimmer override val albumName: TextView get() = binding.Filename override val albumInfo: TextView diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumItemViewHolder.kt index 19c8e7114d38..a531404e8e99 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumItemViewHolder.kt @@ -13,6 +13,7 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView interface AlbumItemViewHolder { val thumbnail: ImageView + val shimmerThumbnail: LoaderImageView val albumName: TextView val albumInfo: TextView } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumListItemViewHolder.kt index 8797bf3aa9ef..a1bfa865942b 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumListItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumListItemViewHolder.kt @@ -18,6 +18,8 @@ internal class AlbumListItemViewHolder(private var binding: AlbumsListItemBindin AlbumItemViewHolder { override val thumbnail: ImageView get() = binding.thumbnail + override val shimmerThumbnail: LoaderImageView + get() = binding.thumbnailShimmer override val albumName: TextView get() = binding.Filename override val albumInfo: TextView diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumsAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumsAdapter.kt index cef073bba01e..669a6ada2dcf 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumsAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumsAdapter.kt @@ -77,11 +77,17 @@ class AlbumsAdapter( thumbnailGenerator.setThumbnail( ocLocal, gridViewHolder.thumbnail, - ThumbnailArguments(isGrid = gridView, hideVideoOverlay = true, null) + ThumbnailArguments( + isGrid = gridView, + hideVideoOverlay = true, + shimmer = gridViewHolder.shimmerThumbnail, + isMediaGallery = true + ) ) } else { - gridViewHolder.thumbnail.setImageResource(R.drawable.file_image) + gridViewHolder.thumbnail.setImageResource(R.drawable.album_no_photo_placeholder) gridViewHolder.thumbnail.visibility = View.VISIBLE + gridViewHolder.shimmerThumbnail.visibility = View.GONE } holder.itemView.setOnClickListener { albumFragmentInterface.onItemClick(file) } @@ -95,6 +101,7 @@ class AlbumsAdapter( fun setAlbumItems(albumItems: List?) { albumList.clear() albumItems?.let { + // NMC-4843 fix // alphabetically sorting albumList.addAll(it.sortedBy { album -> album.albumName.lowercase() }) } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index b473910fd773..a05d794d960e 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -24,7 +24,10 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.text.Spannable; +import android.text.SpannableString; import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; @@ -888,6 +891,16 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { final MenuItem addAlbumItem = menu.findItem(R.id.add_to_album); // show add to album button when picking files from media to add to album addAlbumItem.setVisible(true); + if (addAlbumItem.getTitle() != null) { + SpannableString coloredTitle = new SpannableString(addAlbumItem.getTitle()); + coloredTitle.setSpan( + new ForegroundColorSpan(ContextCompat.getColor(requireContext(), R.color.primary)), + 0, + addAlbumItem.getTitle().length(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE + ); + addAlbumItem.setTitle(coloredTitle); + } } final var adapter = getCommonAdapter(); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt index 4c4d1cbd8e8f..e9fbded8f2a1 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt @@ -25,20 +25,22 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.RelativeLayout +import android.widget.ImageView import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.IdRes import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat import androidx.core.view.MenuHost import androidx.core.view.MenuProvider +import androidx.core.view.get +import androidx.core.view.size import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.floatingactionbutton.FloatingActionButton import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable @@ -52,11 +54,11 @@ import com.nextcloud.ui.fileactions.FileActionsBottomSheet import com.nextcloud.utils.extensions.getTypedActivity import com.nextcloud.utils.extensions.isDialogFragmentReady import com.nextcloud.utils.extensions.isLandscape -import com.nextcloud.utils.extensions.setVisibleIf import com.nextcloud.utils.extensions.toAlbumItem import com.nextcloud.utils.thumbnail.ThumbnailGenerator +import com.nmc.android.utils.SwipeRefreshThemeUtils import com.owncloud.android.R -import com.owncloud.android.databinding.ListFragmentBinding +import com.owncloud.android.databinding.AlbumsFragmentBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.datamodel.VirtualFolderType @@ -72,7 +74,6 @@ import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.lib.resources.status.Type import com.owncloud.android.operations.albums.ReadAlbumItemsOperation import com.owncloud.android.ui.activity.AlbumsPickerActivity -import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.adapter.GalleryAdapter import com.owncloud.android.ui.dialog.ConfirmationDialogFragment @@ -82,7 +83,6 @@ import com.owncloud.android.ui.events.FavoriteEvent import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.ui.fragment.albums.bottomsheet.AlbumSharingBottomSheet import com.owncloud.android.ui.fragment.albums.bottomsheet.AlbumSharingBottomSheetActions -import com.owncloud.android.ui.fragment.albums.model.AlbumItemsEmptyState import com.owncloud.android.ui.fragment.albums.util.AlbumCollageLayout import com.owncloud.android.ui.fragment.helper.ColumnCount import com.owncloud.android.ui.helpers.UriUploader @@ -130,11 +130,10 @@ class AlbumItemsFragment : @Inject lateinit var thumbnailGenerator: ThumbnailGenerator - private lateinit var binding: ListFragmentBinding + private lateinit var binding: AlbumsFragmentBinding private lateinit var albumName: String private var adapter: GalleryAdapter? = null - private var addMediaFab: FloatingActionButton? = null private var client: OwnCloudClient? = null private var optionalUser: Optional? = null private var containerActivity: FileFragment.ContainerActivity? = null @@ -182,7 +181,7 @@ class AlbumItemsFragment : } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = ListFragmentBinding.inflate(inflater, container, false) + binding = AlbumsFragmentBinding.inflate(inflater, container, false) return binding.root } @@ -194,8 +193,8 @@ class AlbumItemsFragment : createMenu() setupSwipeRefresh() setupList() - createAddMediaButton() observeRefreshRequests() + setUpEmptyView() if (isNewAlbum) { openGalleryToAddMedia() @@ -215,12 +214,15 @@ class AlbumItemsFragment : showSortListGroup(false) setMainFabVisible(false) clearToolbarSubtitle() + showHideDefaultToolbarDivider(true) + highlightNavigationViewItem(R.id.nav_album) } } override fun onPause() { super.onPause() adapter?.cancelAllPendingTasks() + getTypedActivity(FileDisplayActivity::class.java)?.showHideDefaultToolbarDivider(false) } override fun onStop() { @@ -230,8 +232,6 @@ class AlbumItemsFragment : override fun onDestroyView() { lastMediaItemPosition = 0 - addMediaFab = null - binding.listRoot.adapter = null super.onDestroyView() } @@ -252,40 +252,17 @@ class AlbumItemsFragment : } private fun setupList() { - binding.listRoot.setEmptyView(binding.emptyList.emptyListView) binding.listRoot.layoutManager = GridLayoutManager(requireContext(), SINGLE_SPAN) } private fun setupSwipeRefresh() { - viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) + SwipeRefreshThemeUtils.themeSwipeRefreshLayout(requireContext(), binding.swipeContainingList) binding.swipeContainingList.setOnRefreshListener { binding.swipeContainingList.isRefreshing = true refreshData() } } - private fun createAddMediaButton() { - addMediaFab = FloatingActionButton(requireContext()).apply { - id = View.generateViewId() - setImageResource(R.drawable.ic_plus) - contentDescription = getString(R.string.add_media) - viewThemeUtils.material.themeFAB(this) - setOnClickListener { openAddMediaMenu() } - } - - val layoutParams = RelativeLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ).apply { - addRule(RelativeLayout.ALIGN_PARENT_END) - addRule(RelativeLayout.ALIGN_PARENT_BOTTOM) - marginEnd = resources.getDimensionPixelSize(R.dimen.standard_margin) - bottomMargin = resources.getDimensionPixelSize(R.dimen.bottom_navigation_view_margin) - } - - binding.listFragmentLayout.addView(addMediaFab, layoutParams) - } - private fun initializeClient() { if (client != null) { return @@ -334,7 +311,7 @@ class AlbumItemsFragment : adapter = adapter, viewThemeUtils = viewThemeUtils, openActionsMenu = { filesCount, checkedFiles -> openActionsMenu(filesCount, checkedFiles) }, - onSelectionModeChanged = { isActive -> addMediaFab.setVisibleIf(!isActive) } + onSelectionModeChanged = { } ) (requireActivity() as FileDisplayActivity).addDrawerListener(selectionMode) @@ -362,7 +339,7 @@ class AlbumItemsFragment : binding.swipeContainingList.isRefreshing = true selectionMode?.exitSelectionMode() initializeAdapter() - showLoadingMessageWhenReachable() + updateEmptyView(false) lifecycleScope.launch(Dispatchers.IO) { val client = client ?: run { @@ -404,7 +381,7 @@ class AlbumItemsFragment : private fun onAlbumItemsLoaded() { if (albumItems.isEmpty()) { - setMessageForEmptyList(AlbumItemsEmptyState.NO_ITEMS) + updateEmptyView(true) } populateList(albumItems) @@ -414,7 +391,7 @@ class AlbumItemsFragment : private fun onAlbumItemsFailed(result: RemoteOperationResult<*>?) { Log_OC.e(TAG, "reading album items failed: ${result?.logMessage}") - setMessageForEmptyList(AlbumItemsEmptyState.fromFailure(result)) + updateEmptyView(true) refreshAlbumMetaData() hideRefreshLayoutLoader() } @@ -453,29 +430,23 @@ class AlbumItemsFragment : //endregion //region Empty state - private fun showLoadingMessageWhenReachable() { - val connectivityService = getTypedActivity(FileActivity::class.java)?.connectivityService ?: return - - connectivityService.isNetworkAndServerAvailable { available -> - if (!available) { - return@isNetworkAndServerAvailable - } - - with(binding.emptyList) { - emptyListViewHeadline.setText(R.string.file_list_loading) - emptyListViewText.text = "" - emptyListIcon.visibility = View.GONE - } + private fun setUpEmptyView() { + binding.albumEmptyView.albumsBgImage.setImageResource(R.drawable.empty_album_detailed_view) + binding.albumEmptyView.albumsBgImage.scaleType = ImageView.ScaleType.FIT_CENTER + binding.albumEmptyView.emptyAlbumLabel.text = resources.getString(R.string.empty_album_detailed_view_title) + binding.albumEmptyView.emptyAlbumMessageLabel.text = + resources.getString(R.string.empty_album_detailed_view_message) + binding.albumEmptyView.createAlbum.text = resources.getString(R.string.add_photos) + + binding.albumEmptyView.createAlbum.setOnClickListener { + // open Gallery fragment as selection then add items to current album + openGalleryToAddMedia() } } - private fun setMessageForEmptyList(state: AlbumItemsEmptyState) = with(binding.emptyList) { - emptyListViewHeadline.setText(state.headline) - emptyListViewText.setText(state.message) - emptyListIcon.setImageResource(state.icon) - - emptyListIcon.visibility = View.VISIBLE - emptyListViewText.visibility = View.VISIBLE + private fun updateEmptyView(isEmpty: Boolean) { + binding.albumEmptyView.emptyViewLayout.visibility = if (isEmpty) View.VISIBLE else View.GONE + binding.listRoot.visibility = if (isEmpty) View.GONE else View.VISIBLE } //endregion @@ -485,11 +456,46 @@ class AlbumItemsFragment : menuHost.addMenuProvider( object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menu.clear() + menu.clear() // important: clears any existing activity menu menuInflater.inflate(R.menu.fragment_album_items, menu) } - override fun onMenuItemSelected(menuItem: MenuItem): Boolean = onAlbumActionChosen(menuItem.itemId) + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_three_dot_icon -> { + openAddMediaMenu() + true + } + + R.id.action_add_from_camera_roll -> { + addFromCameraRoll() + true + } + + R.id.action_add_from_account -> { + // open Gallery fragment as selection then add items to current album + openGalleryToAddMedia() + true + } + + else -> false + } + } + + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + for (i in 0 until menu.size) { + val item = menu[i] + item.icon?.let { + item.setIcon( + viewThemeUtils.platform.colorDrawable( + it, + ContextCompat.getColor(requireContext(), R.color.fontAppbar) + ) + ) + } + } + } }, viewLifecycleOwner, Lifecycle.State.RESUMED @@ -674,8 +680,10 @@ class AlbumItemsFragment : return } - Handler(Looper.getMainLooper()).post { selectionMode?.exitSelectionMode() } - adapter?.markAsFavorite(event.remotePath, event.shouldFavorite) + Handler(Looper.getMainLooper()).post { + selectionMode?.exitSelectionMode() + adapter?.markAsFavorite(event.remotePath, event.shouldFavorite) + } } catch (e: CreationException) { Log_OC.e(TAG, "Error processing event", e) } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsMultiChoiceModeListener.kt b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsMultiChoiceModeListener.kt index 2c90789e7583..c9bdbd25ee68 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsMultiChoiceModeListener.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsMultiChoiceModeListener.kt @@ -146,6 +146,6 @@ internal class AlbumItemsMultiChoiceModeListener( val item = menu.findItem(R.id.custom_menu_placeholder_item) val icon = item.icon ?: return - item.icon = viewThemeUtils.platform.colorDrawable(icon, ContextCompat.getColor(activity, R.color.white)) + item.icon = viewThemeUtils.platform.colorDrawable(icon, ContextCompat.getColor(activity, R.color.fontAppbar)) } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumsFragment.kt index 690e28f9bb4b..4fa48a5d54c1 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumsFragment.kt @@ -11,12 +11,16 @@ import android.content.Context import android.content.res.Configuration import android.os.Bundle import android.os.Parcelable +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.MenuHost import androidx.core.view.MenuProvider @@ -25,16 +29,16 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide import com.google.android.material.appbar.AppBarLayout -import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.client.utils.Throttler import com.nextcloud.utils.extensions.getTypedActivity import com.nextcloud.utils.extensions.isLandscape -import com.nextcloud.utils.extensions.setVisibleIf import com.nextcloud.utils.thumbnail.ThumbnailGenerator +import com.nmc.android.utils.SwipeRefreshThemeUtils import com.owncloud.android.R import com.owncloud.android.databinding.AlbumsFragmentBinding import com.owncloud.android.lib.common.utils.Log_OC @@ -45,9 +49,9 @@ import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.adapter.albums.AlbumFragmentInterface import com.owncloud.android.ui.adapter.albums.AlbumsAdapter +import com.owncloud.android.ui.decoration.MediaGridItemDecoration import com.owncloud.android.ui.dialog.CreateAlbumDialogFragment import com.owncloud.android.ui.fragment.FileFragment -import com.owncloud.android.ui.fragment.albums.model.AlbumsEmptyState import com.owncloud.android.ui.fragment.helper.ColumnCount import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers @@ -126,11 +130,7 @@ class AlbumsFragment : setupContainingList() setupContent() createMenu() - - viewThemeUtils.material.themeFAB(binding.addMediaFab) - binding.addMediaFab.setOnClickListener { - showCreateAlbumDialog() - } + setUpEmptyView() } private fun showAppBar() { @@ -140,6 +140,15 @@ class AlbumsFragment : } } + private fun setUpEmptyView() { + Glide.with(requireContext()).load(R.drawable.bg_image_albums) + .into(binding.albumEmptyView.albumsBgImage) + + binding.albumEmptyView.createAlbum.setOnClickListener { + showCreateAlbumDialog() + } + } + private fun showCreateAlbumDialog() { throttler.run("onCreateAlbumClick") { val fragment = requireActivity().supportFragmentManager.findFragmentByTag(CreateAlbumDialogFragment.TAG) @@ -158,6 +167,9 @@ class AlbumsFragment : if (isGridView) { val layoutManager = GridLayoutManager(requireContext(), maxColumnSize) binding.listRoot.layoutManager = layoutManager + binding.listRoot.addItemDecoration(MediaGridItemDecoration(resources.getDimensionPixelSize(R.dimen.album_grid_spacing))) + val padding = resources.getDimensionPixelSize(R.dimen.album_recycler_view_grid_padding) + binding.listRoot.setPadding(padding, padding, padding, padding) } else { val layoutManager = LinearLayoutManager(requireContext()) binding.listRoot.layoutManager = layoutManager @@ -166,7 +178,7 @@ class AlbumsFragment : } private fun setupContainingList() { - viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) + SwipeRefreshThemeUtils.themeSwipeRefreshLayout(requireContext(), binding.swipeContainingList) binding.swipeContainingList.setOnRefreshListener { fetchAndSetData() } @@ -182,7 +194,7 @@ class AlbumsFragment : private fun fetchAndSetData() { binding.swipeContainingList.isRefreshing = true initializeAdapter() - setEmptyListMessage(null) + updateEmptyView(false) readAlbums() } @@ -198,12 +210,12 @@ class AlbumsFragment : withContext(Dispatchers.Main) { if (result?.isSuccess == true && result.resultData != null) { if (result.resultData.isEmpty()) { - setEmptyListMessage(AlbumsEmptyState.NO_ALBUMS) + updateEmptyView(true) } populateList(result.resultData) } else { Log_OC.d(TAG, "read album operation failed") - setEmptyListMessage(AlbumsEmptyState.LOAD_FAILED) + updateEmptyView(true) } hideRefreshLayoutLoader() @@ -217,10 +229,31 @@ class AlbumsFragment : menuHost.addMenuProvider( object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menu.clear() + menu.clear() // important: clears any existing activity menu + menuInflater.inflate(R.menu.fragment_create_album, menu) + + val addItem = menu.findItem(R.id.action_create_new_album) + val coloredTitle = SpannableString(addItem.title).apply { + setSpan( + ForegroundColorSpan(ContextCompat.getColor(requireContext(), R.color.primary)), + 0, + length, + Spannable.SPAN_INCLUSIVE_INCLUSIVE + ) + } + addItem.title = coloredTitle } - override fun onMenuItemSelected(menuItem: MenuItem): Boolean = true + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_create_new_album -> { + showCreateAlbumDialog() + true + } + + else -> false + } + } }, viewLifecycleOwner, Lifecycle.State.RESUMED @@ -250,23 +283,9 @@ class AlbumsFragment : } } - private fun setEmptyListMessage(state: AlbumsEmptyState?) { - binding.emptyList.emptyListView.setVisibleIf(state != null) - binding.listRoot.setVisibleIf(state == null) - - if (state == null) { - return - } - - with(binding.emptyList) { - emptyListViewHeadline.setText(state.headline) - emptyListViewText.setText(state.message) - emptyListIcon.setImageDrawable( - viewThemeUtils.platform.tintDrawable(requireContext(), state.icon, ColorRole.PRIMARY) - ) - emptyListViewText.visibility = View.VISIBLE - emptyListIcon.visibility = View.VISIBLE - } + private fun updateEmptyView(isEmpty: Boolean) { + binding.albumEmptyView.emptyViewLayout.visibility = if (isEmpty) View.VISIBLE else View.GONE + binding.listRoot.visibility = if (isEmpty) View.GONE else View.VISIBLE } override fun onResume() { @@ -287,9 +306,14 @@ class AlbumsFragment : } showSortListGroup(false) setMainFabVisible(false) + showHideDefaultToolbarDivider(true) // clear the subtitle while navigating to any other screen from Media screen clearToolbarSubtitle() + // highlight album menu item + if (!isSelectionMode) { + highlightNavigationViewItem(R.id.nav_album) + } } } @@ -313,6 +337,7 @@ class AlbumsFragment : super.onPause() adapter?.cancelAllPendingTasks() listState = binding.listRoot.layoutManager?.onSaveInstanceState() + getTypedActivity(FileDisplayActivity::class.java)?.showHideDefaultToolbarDivider(false) } private val isGridEnabled: Boolean diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/albums/bottomsheet/AlbumSharingBottomSheet.kt b/app/src/main/java/com/owncloud/android/ui/fragment/albums/bottomsheet/AlbumSharingBottomSheet.kt index 0abc95ceec54..49f202b8b5d6 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/albums/bottomsheet/AlbumSharingBottomSheet.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/albums/bottomsheet/AlbumSharingBottomSheet.kt @@ -16,7 +16,6 @@ import androidx.annotation.StringRes import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.di.Injectable import com.nextcloud.utils.date.DateFormatPattern import com.nextcloud.utils.extensions.setVisibleIf @@ -72,7 +71,6 @@ class AlbumSharingBottomSheet( state = BottomSheetBehavior.STATE_EXPANDED skipCollapsed = true } - viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE) return binding.root } @@ -129,7 +127,7 @@ class AlbumSharingBottomSheet( thumbnailGenerator.setThumbnail( file, target.thumbnail, - ThumbnailArguments(isGrid = false, hideVideoOverlay = true, target.thumbnailShimmer) + ThumbnailArguments(isGrid = false, hideVideoOverlay = false, target.thumbnailShimmer, isMediaGallery = true) ) } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt index 465bbf594c42..6dc0a2ae2e07 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt @@ -12,6 +12,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.res.Configuration +import android.content.res.Resources import android.os.Bundle import android.view.MenuItem import androidx.activity.OnBackPressedCallback @@ -55,6 +56,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.operations.SynchronizeFileOperation +import com.owncloud.android.operations.albums.CopyFileToAlbumOperation import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.activity.OnFilesRemovedListener @@ -64,6 +66,7 @@ import com.owncloud.android.ui.fragment.GalleryFragment import com.owncloud.android.ui.fragment.GalleryFragmentBottomSheetDialog.MediaState import com.owncloud.android.ui.preview.model.PreviewImageActivityState import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.ErrorMessageAdapter import com.owncloud.android.utils.MimeTypeUtil import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import kotlinx.coroutines.Job @@ -386,6 +389,8 @@ class PreviewImageActivity : } } else if (operation is SynchronizeFileOperation) { onSynchronizeFileOperationFinish(result) + } else if(operation is CopyFileToAlbumOperation){ + onCopyAlbumFileOperationFinish(operation, result) } } @@ -395,6 +400,22 @@ class PreviewImageActivity : } } + private fun onCopyAlbumFileOperationFinish(operation: CopyFileToAlbumOperation, result: RemoteOperationResult<*>) { + if (result.isSuccess) { + DisplayUtils.showSnackMessage(this, getResources().getString(R.string.album_file_added_message)) + Log_OC.e(TAG, "Files copied successfully") + } else { + try { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + } catch (e: Resources.NotFoundException) { + Log_OC.e(TAG, "Error while trying to show fail message ", e) + } + } + } + private fun observeWorkerState() { observeWorker { state: WorkerState? -> when (state) { diff --git a/app/src/main/res/drawable-xxxhdpi/bg_image_albums.png b/app/src/main/res/drawable-xxxhdpi/bg_image_albums.png new file mode 100644 index 000000000000..a387dc95fe5a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/bg_image_albums.png differ diff --git a/app/src/main/res/drawable/album_no_photo_placeholder.xml b/app/src/main/res/drawable/album_no_photo_placeholder.xml new file mode 100644 index 000000000000..4b31ce242b9a --- /dev/null +++ b/app/src/main/res/drawable/album_no_photo_placeholder.xml @@ -0,0 +1,564 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/empty_album_detailed_view.xml b/app/src/main/res/drawable/empty_album_detailed_view.xml new file mode 100644 index 000000000000..1ebcd208a454 --- /dev/null +++ b/app/src/main/res/drawable/empty_album_detailed_view.xml @@ -0,0 +1,568 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/nav_albums.xml b/app/src/main/res/drawable/nav_albums.xml new file mode 100644 index 000000000000..64c7d5aec430 --- /dev/null +++ b/app/src/main/res/drawable/nav_albums.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/layout/album_sharing_bottom_sheet.xml b/app/src/main/res/layout/album_sharing_bottom_sheet.xml index 6addd121ff3b..a0ed02bbeadb 100644 --- a/app/src/main/res/layout/album_sharing_bottom_sheet.xml +++ b/app/src/main/res/layout/album_sharing_bottom_sheet.xml @@ -270,21 +270,23 @@ android:id="@+id/action_stop_sharing" layout="@layout/album_share_action" android:layout_width="@dimen/standard_quadruple_icon_size" - android:layout_height="wrap_content" + android:layout_height="0dp" android:layout_marginEnd="@dimen/standard_margin" android:layout_marginBottom="@dimen/standard_half_margin" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/action_copy" /> + app:layout_constraintEnd_toStartOf="@+id/action_copy" + app:layout_constraintTop_toTopOf="@+id/action_share_link" /> + app:layout_constraintEnd_toStartOf="@+id/action_share_link" + app:layout_constraintTop_toTopOf="@+id/action_share_link" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/albums_fragment.xml b/app/src/main/res/layout/albums_fragment.xml index f66524028784..9ad8fc4a758f 100644 --- a/app/src/main/res/layout/albums_fragment.xml +++ b/app/src/main/res/layout/albums_fragment.xml @@ -6,7 +6,6 @@ --> @@ -29,23 +28,9 @@ android:layout_height="match_parent" /> + android:id="@+id/album_empty_view" + layout="@layout/albums_empty_view" /> - - diff --git a/app/src/main/res/layout/albums_grid_item.xml b/app/src/main/res/layout/albums_grid_item.xml index 72f89820e41f..2c9a6d4c7a39 100644 --- a/app/src/main/res/layout/albums_grid_item.xml +++ b/app/src/main/res/layout/albums_grid_item.xml @@ -4,76 +4,89 @@ ~ SPDX-FileCopyrightText: 2026 TSI-mc ~ SPDX-License-Identifier: AGPL-3.0-or-later --> - + android:layout_height="wrap_content" + android:orientation="vertical"> + android:layout_height="wrap_content"> - - - + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> - + - + android:layout_height="@dimen/album_grid_image_height" + android:contentDescription="@null" + android:scaleType="centerCrop" + android:src="@drawable/file_image" + app:layout_constraintBottom_toTopOf="@+id/Filename" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + - + + + - + diff --git a/app/src/main/res/layout/albums_list_item.xml b/app/src/main/res/layout/albums_list_item.xml index 7d409511c971..839c94fdc281 100644 --- a/app/src/main/res/layout/albums_list_item.xml +++ b/app/src/main/res/layout/albums_list_item.xml @@ -17,16 +17,15 @@ android:paddingBottom="@dimen/standard_half_padding"> + android:layout_width="@dimen/album_list_image_width" + android:layout_height="@dimen/album_list_image_height" + android:layout_marginStart="@dimen/standard_half_padding" + android:layout_marginEnd="@dimen/standard_half_padding"> - + app:strokeColor="@android:color/transparent" + app:strokeWidth="0dp"> + + + @@ -56,9 +66,8 @@ android:layout_weight="1" android:gravity="top" android:orientation="vertical" - android:paddingStart="@dimen/standard_quarter_padding" - android:paddingTop="@dimen/standard_half_padding" - android:paddingEnd="@dimen/zero"> + android:paddingStart="@dimen/standard_half_padding" + android:paddingTop="@dimen/standard_half_padding"> @@ -37,6 +38,7 @@ - - - + android:title="@string/add_more" + app:showAsAction="always"> + + + + + + android:id="@+id/action_three_dot_icon" + android:contentDescription="@string/more" + android:icon="@drawable/ic_dots_vertical" + android:orderInCategory="1" + android:title="@string/more" + app:showAsAction="always" /> diff --git a/app/src/main/res/menu/fragment_create_album.xml b/app/src/main/res/menu/fragment_create_album.xml new file mode 100644 index 000000000000..dd9b52acc789 --- /dev/null +++ b/app/src/main/res/menu/fragment_create_album.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/partial_drawer_entries.xml b/app/src/main/res/menu/partial_drawer_entries.xml index 7fbb91bee21d..abafab7960fd 100644 --- a/app/src/main/res/menu/partial_drawer_entries.xml +++ b/app/src/main/res/menu/partial_drawer_entries.xml @@ -34,6 +34,11 @@ android:icon="@drawable/selector_favorites" android:orderInCategory="0" android:title="@string/drawer_item_favorites" /> + + + + Album erstellen + Mehr hinzufügen + Es fehlen nur noch Ihre Fotos + Sie können so viele Fotos hinzufügen, wie Sie möchten. Ein Foto kann auch mehreren Alben zugeordnet werden. + Fotos hinzufügen + Dateien hochladen + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 576b1f29753d..029848fdf15f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -906,7 +906,7 @@ Entfernen Gelöscht Album umbenennen - Umbenennen + Speichern Geben Sie einen neuen Namen ein Die lokale Kopie konnte nicht umbenannt werden. Versuche es mit einem anderen Namen. Umbenennen nicht möglich. Der Name wird bereits verwendet. diff --git a/app/src/main/res/values/nmc_album_dims.xml b/app/src/main/res/values/nmc_album_dims.xml new file mode 100644 index 000000000000..cb34458581c6 --- /dev/null +++ b/app/src/main/res/values/nmc_album_dims.xml @@ -0,0 +1,15 @@ + + + + 78dp + 56dp + 140dp + 8dp + 4dp + 8dp + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_album_strings.xml b/app/src/main/res/values/nmc_album_strings.xml new file mode 100644 index 000000000000..6502814a02ee --- /dev/null +++ b/app/src/main/res/values/nmc_album_strings.xml @@ -0,0 +1,15 @@ + + + + Create album + Add more + All that\'s missing are your photos + You can add as many photos as you like. A photo can also belong to more than one album. + Add photos + Upload from cameraroll + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1589ad6f91d..f9fcd0756023 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,25 +30,25 @@ Add media New album Rename album - Enter your new album name + Enter your new Album name Album name cannot be empty - Rename album - Share album + Rename Album + Share Album Rename - Delete album + Delete Album Failed to delete few of the files. - %1$d item — %2$s - %1$d items — %2$s + %1$d Item — %2$s + %1$d Items — %2$s - %d item - %d items + %d Item + %d Items Album already exists - Pick album - Pick media files - Create albums for your photos + Pick Album + Pick Media Files + Create Albums for your Photos You can organize all your photos in as many albums as you like. You haven\'t created an album yet. Add to album File added successfully