From ca514a8a8a6cf1c4bd7837fdc7c2699a309df7b5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 7 Sep 2026 09:16:39 +0200 Subject: [PATCH] fix(player): 3-dot menu Signed-off-by: alperozturk96 --- app/src/main/AndroidManifest.xml | 3 +- .../client/player/ui/PlayerActivity.kt | 8 +- .../client/player/ui/PlayerScreenEvent.kt | 6 +- .../client/player/ui/PlayerViewModel.kt | 29 ++-- .../ui/preview/PreviewPlaybackFragment.kt | 156 ++++++++++++++++++ 5 files changed, 186 insertions(+), 16 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a59e98831e5e..a9300e8e3afd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -393,7 +393,8 @@ android:name="com.nextcloud.client.player.ui.PlayerActivity" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:launchMode="singleTask" - android:supportsPictureInPicture="true" /> + android:supportsPictureInPicture="true" + android:theme="@style/Theme.ownCloud.NoActionBar" /> showFileActions(event.file, event.actionIds) + is PlayerScreenEvent.ShowFileActions -> showFileActions(event.file, event.actionsToHide) is PlayerScreenEvent.ShowFileDetails -> showFileDetails(event.file) is PlayerScreenEvent.ShowFileExportStartedMessage -> showFileExportStartedMessage() is PlayerScreenEvent.ShowShareFileDialog -> fileOperationsHelper.sendShareFile(event.file) is PlayerScreenEvent.ShowRemoveFileDialog -> showRemoveFileDialog(event.file) is PlayerScreenEvent.LaunchOpenFileIntent -> fileOperationsHelper.openFile(event.file) is PlayerScreenEvent.LaunchStreamFileIntent -> fileOperationsHelper.streamMediaFile(event.file) + is PlayerScreenEvent.ToggleFileLock -> fileOperationsHelper.toggleFileLock(event.file, event.shouldBeLocked) + is PlayerScreenEvent.AddFileToAlbum -> fileOperationsHelper.addFileToAlbum(listOf(event.file)) } } - private fun showFileActions(file: OCFile, actionIds: List) { - val actionsToHide = FileAction.entries.map(FileAction::id).filter { it !in actionIds } + private fun showFileActions(file: OCFile, actionsToHide: List) { FileActionsBottomSheet.newInstance(file, false, actionsToHide) .setResultListener(supportFragmentManager, this) { viewModel.onFileActionChosen(file, it) } .show(supportFragmentManager, "actions") diff --git a/app/src/main/java/com/nextcloud/client/player/ui/PlayerScreenEvent.kt b/app/src/main/java/com/nextcloud/client/player/ui/PlayerScreenEvent.kt index c5b0d23f066f..7f8f56c57202 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/PlayerScreenEvent.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/PlayerScreenEvent.kt @@ -11,7 +11,7 @@ import com.owncloud.android.datamodel.OCFile sealed interface PlayerScreenEvent { - data class ShowFileActions(val file: OCFile, val actionIds: List) : PlayerScreenEvent + data class ShowFileActions(val file: OCFile, val actionsToHide: List) : PlayerScreenEvent data class ShowFileDetails(val file: OCFile) : PlayerScreenEvent @@ -24,4 +24,8 @@ sealed interface PlayerScreenEvent { data class LaunchOpenFileIntent(val file: OCFile) : PlayerScreenEvent data class LaunchStreamFileIntent(val file: OCFile) : PlayerScreenEvent + + data class ToggleFileLock(val file: OCFile, val shouldBeLocked: Boolean) : PlayerScreenEvent + + data class AddFileToAlbum(val file: OCFile) : PlayerScreenEvent } diff --git a/app/src/main/java/com/nextcloud/client/player/ui/PlayerViewModel.kt b/app/src/main/java/com/nextcloud/client/player/ui/PlayerViewModel.kt index df14c565c931..640a32f02e21 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/PlayerViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/PlayerViewModel.kt @@ -15,6 +15,7 @@ import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.download.FileDownloadHelper import com.nextcloud.client.logger.Logger import com.nextcloud.client.player.media3.PlaybackModel +import com.nextcloud.ui.fileactions.FileAction import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile @@ -41,28 +42,36 @@ class PlayerViewModel @Inject constructor( fun onMoreButtonClick() { viewModelScope.launch { val file = getCurrentOCFile() ?: return@launch - val actionIds = listOf( - R.id.action_see_details, - R.id.action_download_file, - R.id.action_export_file, - R.id.action_send_share_file, - R.id.action_remove_file, - R.id.action_open_file_with, - R.id.action_stream_media - ) - eventChannel.trySend(PlayerScreenEvent.ShowFileActions(file, actionIds)) + val actionsToHide = FileAction.getFilePreviewActions(file) + eventChannel.trySend(PlayerScreenEvent.ShowFileActions(file, actionsToHide)) } } fun onFileActionChosen(file: OCFile, actionId: Int) { when (actionId) { R.id.action_see_details -> eventChannel.trySend(PlayerScreenEvent.ShowFileDetails(file)) + R.id.action_download_file -> startFileDownloading(file) + R.id.action_export_file -> startFileExport(file) + R.id.action_send_share_file -> eventChannel.trySend(PlayerScreenEvent.ShowShareFileDialog(file)) + R.id.action_remove_file -> eventChannel.trySend(PlayerScreenEvent.ShowRemoveFileDialog(file)) + R.id.action_open_file_with -> onOpenFileWithClick(file) + R.id.action_stream_media -> onStreamFileClick(file) + + R.id.action_lock_file -> eventChannel.trySend( + PlayerScreenEvent.ToggleFileLock(file, shouldBeLocked = true) + ) + + R.id.action_unlock_file -> eventChannel.trySend( + PlayerScreenEvent.ToggleFileLock(file, shouldBeLocked = false) + ) + + R.id.action_add_to_album -> eventChannel.trySend(PlayerScreenEvent.AddFileToAlbum(file)) } } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewPlaybackFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewPlaybackFragment.kt index f0b154c4d32b..ff50d702006b 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewPlaybackFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewPlaybackFragment.kt @@ -9,14 +9,23 @@ package com.owncloud.android.ui.preview import android.os.Bundle 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.activity.OnBackPressedCallback import androidx.activity.addCallback +import androidx.core.content.ContextCompat import androidx.core.os.bundleOf +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.player.media3.PlaybackModel import com.nextcloud.client.player.model.ThumbnailLoader import com.nextcloud.client.player.model.file.PlaybackCollection @@ -29,14 +38,24 @@ import com.nextcloud.client.player.ui.PlayerLauncher import com.nextcloud.client.player.util.PlayerUtil.applyVideoSize import com.nextcloud.client.player.util.PlayerUtil.isPictureInPictureAllowed import com.nextcloud.client.player.util.PlayerUtil.ownsPlayback +import com.nextcloud.ui.fileactions.FileAction +import com.nextcloud.ui.fileactions.FileActionsBottomSheet import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.getSerializableArgument import com.owncloud.android.R import com.owncloud.android.databinding.PreviewPlaybackFragmentBinding import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.FetchRemoteFileOperation +import com.owncloud.android.ui.dialog.ConfirmationDialogFragment +import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment +import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils import dagger.android.support.AndroidSupportInjection +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject /** @@ -49,6 +68,7 @@ class PreviewPlaybackFragment : PlaybackModel.Listener { companion object { + private val TAG = PreviewPlaybackFragment::class.java.simpleName private const val ARGUMENT_FILE = "ARGUMENT_FILE" private const val ARGUMENT_COLLECTION = "ARGUMENT_COLLECTION" private const val ARGUMENT_AUTOPLAY = "ARGUMENT_AUTOPLAY" @@ -81,6 +101,15 @@ class PreviewPlaybackFragment : @Inject lateinit var thumbnailLoader: ThumbnailLoader + @Inject + lateinit var accountManager: UserAccountManager + + @Inject + lateinit var backgroundJobManager: BackgroundJobManager + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + private lateinit var binding: PreviewPlaybackFragmentBinding private lateinit var file: OCFile private lateinit var playbackFile: PlaybackFile @@ -116,6 +145,133 @@ class PreviewPlaybackFragment : return binding.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + requireActivity().addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.custom_menu_placeholder, menu) + val item = menu.findItem(R.id.custom_menu_placeholder_item) + item.icon?.let { + item.setIcon( + viewThemeUtils.platform.colorDrawable( + it, + ContextCompat.getColor(requireContext(), R.color.white) + ) + ) + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.custom_menu_placeholder_item -> { + onOverflowClick() + true + } + + else -> false + } + }, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) + } + + private fun onOverflowClick(isManualClick: Boolean = false) { + val storageManager = previewActivity()?.storageManager ?: return + val updatedFile = storageManager.getFileById(file.fileId) + + // check for albums file for album file both local and remoteId will be same configured at operation level + if (!isManualClick && updatedFile != null && updatedFile.localId.toString() == updatedFile.remoteId) { + fetchFileMetaDataIfAbsent(updatedFile) + return + } + + updatedFile?.let { actionsFile -> + val additionalFilter = FileAction.getFilePreviewActions(actionsFile) + FileActionsBottomSheet.newInstance(actionsFile, false, additionalFilter) + .setResultListener(childFragmentManager, viewLifecycleOwner) { itemId: Int -> + onFileActionChosen(itemId) + } + .show(childFragmentManager, "actions") + } + } + + private fun fetchFileMetaDataIfAbsent(ocFile: OCFile) { + val previewActivity = previewActivity() ?: return + val context = context ?: return + + previewActivity.showLoadingDialog(getString(R.string.wait_a_moment)) + lifecycleScope.launch(Dispatchers.IO) { + val operation = FetchRemoteFileOperation( + context, + accountManager.user, + ocFile, + removeFileFromDb = true, + storageManager = previewActivity.storageManager + ) + val result = operation.execute(context) + + withContext(Dispatchers.Main) { + previewActivity.dismissLoadingDialog() + + if (result?.isSuccess == true && result.resultData != null) { + file = result.resultData as OCFile + onOverflowClick(isManualClick = true) + } else { + Log_OC.d(TAG, result?.logMessage) + DisplayUtils.showSnackMessage(binding.root, result.getLogMessage(context)) + } + } + } + } + + @Suppress("CyclomaticComplexMethod") + private fun onFileActionChosen(itemId: Int) { + val previewActivity = previewActivity() ?: return + val fileOperationsHelper = previewActivity.fileOperationsHelper + + when (itemId) { + R.id.action_see_details -> previewActivity.showDetails(file) + + R.id.action_download_file -> previewActivity.requestForDownload(file) + + R.id.action_export_file -> fileOperationsHelper.exportFiles( + arrayListOf(file), + context, + view, + backgroundJobManager + ) + + R.id.action_send_share_file -> if (file.isSharedWithMe && !file.canReshare()) { + Snackbar.make(requireView(), R.string.resharing_is_not_allowed, Snackbar.LENGTH_LONG).show() + } else { + fileOperationsHelper.sendShareFile(file) + } + + R.id.action_send_file -> fileOperationsHelper.sendShareFile(file, true) + + R.id.action_open_file_with -> fileOperationsHelper.openFile(file) + + R.id.action_stream_media -> { + playbackModel.pause() + fileOperationsHelper.streamMediaFile(file) + } + + R.id.action_remove_file -> { + playbackModel.pause() + RemoveFilesDialogFragment.newInstance( + file + ).show(parentFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) + } + + R.id.action_add_to_album -> fileOperationsHelper.addFileToAlbum(listOf(file)) + + R.id.action_lock_file -> fileOperationsHelper.toggleFileLock(file, true) + + R.id.action_unlock_file -> fileOperationsHelper.toggleFileLock(file, false) + } + } + private fun registerPictureInPictureOnBack() { if (!pictureInPictureOnBack || !MimeTypeUtil.isVideo(file)) { return