Skip to content

Commit 7ad3b5e

Browse files
committed
Albums functionality
1 parent 91e2440 commit 7ad3b5e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+4962
-18
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,9 @@
592592
android:launchMode="singleTop"
593593
android:theme="@style/Theme.ownCloud.Dialog.NoTitle"
594594
android:windowSoftInputMode="adjustResize" />
595+
<activity
596+
android:name=".ui.activity.AlbumsPickerActivity"
597+
android:exported="false" />
595598
<activity
596599
android:name=".ui.activity.ShareActivity"
597600
android:exported="false"

app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.nextcloud.ui.ChooseStorageLocationDialogFragment;
3232
import com.nextcloud.ui.ImageDetailFragment;
3333
import com.nextcloud.ui.SetStatusDialogFragment;
34+
import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet;
3435
import com.nextcloud.ui.composeActivity.ComposeActivity;
3536
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
3637
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet;
@@ -81,6 +82,7 @@
8182
import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment;
8283
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
8384
import com.owncloud.android.ui.dialog.ConflictsResolveDialog;
85+
import com.owncloud.android.ui.dialog.CreateAlbumDialogFragment;
8486
import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
8587
import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment;
8688
import com.owncloud.android.ui.dialog.IndeterminateProgressDialog;
@@ -114,6 +116,9 @@
114116
import com.owncloud.android.ui.fragment.OCFileListFragment;
115117
import com.owncloud.android.ui.fragment.SharedListFragment;
116118
import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
119+
import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment;
120+
import com.owncloud.android.ui.fragment.albums.AlbumsFragment;
121+
import com.owncloud.android.ui.activity.AlbumsPickerActivity;
117122
import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment;
118123
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
119124
import com.owncloud.android.ui.preview.FileDownloadFragment;
@@ -505,4 +510,19 @@ abstract class ComponentsModule {
505510

506511
@ContributesAndroidInjector
507512
abstract TermsOfServiceDialog termsOfServiceDialog();
513+
514+
@ContributesAndroidInjector
515+
abstract AlbumsPickerActivity albumsPickerActivity();
516+
517+
@ContributesAndroidInjector
518+
abstract CreateAlbumDialogFragment createAlbumDialogFragment();
519+
520+
@ContributesAndroidInjector
521+
abstract AlbumsFragment albumsFragment();
522+
523+
@ContributesAndroidInjector
524+
abstract AlbumItemsFragment albumItemsFragment();
525+
526+
@ContributesAndroidInjector
527+
abstract AlbumItemActionsBottomSheet albumItemActionsBottomSheet();
508528
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.ui.albumItemActions
9+
10+
import androidx.annotation.DrawableRes
11+
import androidx.annotation.IdRes
12+
import androidx.annotation.StringRes
13+
import com.owncloud.android.R
14+
15+
enum class AlbumItemAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRes val icon: Int? = null) {
16+
RENAME_ALBUM(R.id.action_rename_file, R.string.album_rename, R.drawable.ic_edit),
17+
DELETE_ALBUM(R.id.action_delete, R.string.album_delete, R.drawable.ic_delete);
18+
19+
companion object {
20+
/**
21+
* All file actions, in the order they should be displayed
22+
*/
23+
@JvmField
24+
val SORTED_VALUES = listOf(
25+
RENAME_ALBUM,
26+
DELETE_ALBUM
27+
)
28+
}
29+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.ui.albumItemActions
9+
10+
import android.os.Bundle
11+
import android.view.LayoutInflater
12+
import android.view.View
13+
import android.view.ViewGroup
14+
import androidx.annotation.IdRes
15+
import androidx.appcompat.content.res.AppCompatResources
16+
import androidx.core.os.bundleOf
17+
import androidx.core.view.isEmpty
18+
import androidx.fragment.app.FragmentManager
19+
import androidx.fragment.app.setFragmentResult
20+
import androidx.lifecycle.LifecycleOwner
21+
import com.google.android.material.bottomsheet.BottomSheetBehavior
22+
import com.google.android.material.bottomsheet.BottomSheetDialog
23+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
24+
import com.nextcloud.android.common.ui.theme.utils.ColorRole
25+
import com.nextcloud.client.di.Injectable
26+
import com.owncloud.android.databinding.FileActionsBottomSheetBinding
27+
import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding
28+
import com.owncloud.android.utils.theme.ViewThemeUtils
29+
import javax.inject.Inject
30+
31+
class AlbumItemActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
32+
33+
@Inject
34+
lateinit var viewThemeUtils: ViewThemeUtils
35+
36+
private var _binding: FileActionsBottomSheetBinding? = null
37+
val binding
38+
get() = _binding!!
39+
40+
fun interface ResultListener {
41+
fun onResult(@IdRes actionId: Int)
42+
}
43+
44+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
45+
_binding = FileActionsBottomSheetBinding.inflate(inflater, container, false)
46+
47+
val bottomSheetDialog = dialog as BottomSheetDialog
48+
bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
49+
bottomSheetDialog.behavior.skipCollapsed = true
50+
51+
viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE)
52+
53+
return binding.root
54+
}
55+
56+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
57+
super.onViewCreated(view, savedInstanceState)
58+
binding.bottomSheetHeader.visibility = View.GONE
59+
binding.bottomSheetLoading.visibility = View.GONE
60+
displayActions()
61+
}
62+
63+
override fun onDestroyView() {
64+
super.onDestroyView()
65+
_binding = null
66+
}
67+
68+
fun setResultListener(
69+
fragmentManager: FragmentManager,
70+
lifecycleOwner: LifecycleOwner,
71+
listener: ResultListener
72+
): AlbumItemActionsBottomSheet {
73+
fragmentManager.setFragmentResultListener(REQUEST_KEY, lifecycleOwner) { _, result ->
74+
@IdRes val actionId = result.getInt(RESULT_KEY_ACTION_ID, -1)
75+
if (actionId != -1) {
76+
listener.onResult(actionId)
77+
}
78+
}
79+
return this
80+
}
81+
82+
private fun displayActions() {
83+
if (binding.fileActionsList.isEmpty()) {
84+
AlbumItemAction.SORTED_VALUES.forEach { action ->
85+
val view = inflateActionView(action)
86+
binding.fileActionsList.addView(view)
87+
}
88+
}
89+
}
90+
91+
private fun inflateActionView(action: AlbumItemAction): View {
92+
val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false)
93+
.apply {
94+
root.setOnClickListener {
95+
dispatchActionClick(action.id)
96+
}
97+
text.setText(action.title)
98+
if (action.icon != null) {
99+
val drawable =
100+
viewThemeUtils.platform.tintDrawable(
101+
requireContext(),
102+
AppCompatResources.getDrawable(requireContext(), action.icon)!!
103+
)
104+
icon.setImageDrawable(drawable)
105+
}
106+
}
107+
return itemBinding.root
108+
}
109+
110+
private fun dispatchActionClick(id: Int?) {
111+
if (id != null) {
112+
setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_ACTION_ID to id))
113+
parentFragmentManager.clearFragmentResultListener(REQUEST_KEY)
114+
dismiss()
115+
}
116+
}
117+
118+
companion object {
119+
private const val REQUEST_KEY = "REQUEST_KEY_ACTION"
120+
private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID"
121+
122+
@JvmStatic
123+
fun newInstance(): AlbumItemActionsBottomSheet {
124+
return AlbumItemActionsBottomSheet()
125+
}
126+
}
127+
}

app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
* Type for virtual folders
1313
*/
1414
public enum VirtualFolderType {
15-
FAVORITE, GALLERY, NONE
15+
FAVORITE, GALLERY, ALBUM, NONE
1616
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.operations.albums
8+
9+
import com.owncloud.android.datamodel.FileDataStorageManager
10+
import com.owncloud.android.datamodel.OCFile
11+
import com.owncloud.android.lib.common.OwnCloudClient
12+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
13+
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
14+
import com.owncloud.android.operations.UploadFileOperation
15+
import com.owncloud.android.operations.common.SyncOperation
16+
17+
/**
18+
* Constructor
19+
*
20+
* @param srcPath Remote path of the [OCFile] to move.
21+
* @param targetParentPath Path to the folder where the file will be copied into.
22+
*/
23+
class CopyFileToAlbumOperation(
24+
private val srcPath: String,
25+
private var targetParentPath: String,
26+
storageManager: FileDataStorageManager
27+
) :
28+
SyncOperation(storageManager) {
29+
init {
30+
if (!targetParentPath.endsWith(OCFile.PATH_SEPARATOR)) {
31+
this.targetParentPath += OCFile.PATH_SEPARATOR
32+
}
33+
}
34+
35+
/**
36+
* Performs the operation.
37+
*
38+
* @param client Client object to communicate with the remote ownCloud server.
39+
*/
40+
@Deprecated("Deprecated in Java")
41+
@Suppress("NestedBlockDepth")
42+
override fun run(client: OwnCloudClient): RemoteOperationResult<Any> {
43+
/** 1. check copy validity */
44+
val result: RemoteOperationResult<Any>
45+
46+
if (targetParentPath.startsWith(srcPath)) {
47+
result = RemoteOperationResult<Any>(ResultCode.INVALID_COPY_INTO_DESCENDANT)
48+
} else {
49+
val file = storageManager.getFileByPath(srcPath)
50+
if (file == null) {
51+
result = RemoteOperationResult(ResultCode.FILE_NOT_FOUND)
52+
} else {
53+
/** 2. remote copy */
54+
var targetPath = "$targetParentPath${file.fileName}"
55+
if (file.isFolder) {
56+
targetPath += OCFile.PATH_SEPARATOR
57+
}
58+
59+
// auto rename, to allow copy
60+
if (targetPath == srcPath) {
61+
if (file.isFolder) {
62+
targetPath = "$targetParentPath${file.fileName}"
63+
}
64+
targetPath = UploadFileOperation.getNewAvailableRemotePath(client, targetPath, null, false)
65+
66+
if (file.isFolder) {
67+
targetPath += OCFile.PATH_SEPARATOR
68+
}
69+
}
70+
71+
result = CopyFileToAlbumRemoteOperation(srcPath, targetPath).execute(client)
72+
}
73+
}
74+
return result
75+
}
76+
}

0 commit comments

Comments
 (0)