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..168a07be88c7 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..7a6e8b461205 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -39,17 +39,17 @@ Neuen Link erstellen Jeder kann nur ansehen Album löschen - Einige der Dateien konnten nicht gelöscht werden. - Datei hinzugefügt - Albumname darf nicht leer sein + Einige Dateien konnten nicht gelöscht werden. + Datei erfolgreich hinzugefügt + Der Albumname darf nicht leer sein Album auswählen Album umbenennen Dieser Name wird bereits verwendet. Album teilen - Link zum Album teilen - Freigabe löschen + Album teilen + Teilen beenden Nicht unterstützte Medien - Wähle Bilder vom Konto aus + Dateien auswählen Basis-URL Zwischenablage deaktivieren Intro deaktivieren @@ -284,7 +284,7 @@ Freigaben konnten nicht abgerufen werden URL konnte nicht abgerufen werden Erstellen - Den neuen Albumnamen eingeben + Gib einen Namen für das Album ein Neues Album Verzeichnis konnte nicht erstellt werden Dokument @@ -388,9 +388,9 @@ Mehr Weitere Nextcloud-Apps Dateiauswahl konnte nicht geöffnet werden - E-Mail-Adresse konnte nicht ausgewählt werden. - Sie können alle Ihre Fotos in so vielen Alben organisieren, wie Sie möchten. Sie haben noch kein Album erstellt. - Alben für Ihre Fotos erstellen + Fehler beim Zugriff auf E-Mail Adresse. + Sie können all Ihre Fotos in beliebig vielen Alben organisieren. Bisher haben Sie noch kein Album erstellt. + Erstelle Alben für deine Fotos Als verschlüsselt festlegen Die Überprüfung Ihrer Verschlüsselungsschlüssel war fehlgeschlagen. Versuchen, den Ordner erneut zu verschlüsseln. Der Server ist nicht erreichbar. Später erneut versuchen, den Ordner zu verschlüsseln. @@ -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