Skip to content

Commit 8f57d5e

Browse files
committed
Albums functionality
1 parent a7f50c1 commit 8f57d5e

File tree

51 files changed

+5094
-14
lines changed

Some content is hidden

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

51 files changed

+5094
-14
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,9 @@
598598
android:launchMode="singleTop"
599599
android:theme="@style/Theme.ownCloud.Dialog.NoTitle"
600600
android:windowSoftInputMode="adjustResize" />
601+
<activity
602+
android:name=".ui.activity.AlbumsPickerActivity"
603+
android:exported="false" />
601604
<activity
602605
android:name=".ui.activity.ShareActivity"
603606
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;
@@ -113,6 +115,9 @@
113115
import com.owncloud.android.ui.fragment.OCFileListFragment;
114116
import com.owncloud.android.ui.fragment.SharedListFragment;
115117
import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
118+
import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment;
119+
import com.owncloud.android.ui.fragment.albums.AlbumsFragment;
120+
import com.owncloud.android.ui.activity.AlbumsPickerActivity;
116121
import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment;
117122
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
118123
import com.owncloud.android.ui.preview.FileDownloadFragment;
@@ -501,4 +506,19 @@ abstract class ComponentsModule {
501506

502507
@ContributesAndroidInjector
503508
abstract TermsOfServiceDialog termsOfServiceDialog();
509+
510+
@ContributesAndroidInjector
511+
abstract AlbumsPickerActivity albumsPickerActivity();
512+
513+
@ContributesAndroidInjector
514+
abstract CreateAlbumDialogFragment createAlbumDialogFragment();
515+
516+
@ContributesAndroidInjector
517+
abstract AlbumsFragment albumsFragment();
518+
519+
@ContributesAndroidInjector
520+
abstract AlbumItemsFragment albumItemsFragment();
521+
522+
@ContributesAndroidInjector
523+
abstract AlbumItemActionsBottomSheet albumItemActionsBottomSheet();
504524
}
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: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.core.os.bundleOf
16+
import androidx.core.view.isEmpty
17+
import androidx.fragment.app.FragmentManager
18+
import androidx.fragment.app.setFragmentResult
19+
import androidx.lifecycle.LifecycleOwner
20+
import com.google.android.material.bottomsheet.BottomSheetBehavior
21+
import com.google.android.material.bottomsheet.BottomSheetDialog
22+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
23+
import com.nextcloud.client.di.Injectable
24+
import com.owncloud.android.databinding.FileActionsBottomSheetBinding
25+
import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding
26+
import com.owncloud.android.utils.theme.ViewThemeUtils
27+
import javax.inject.Inject
28+
29+
class AlbumItemActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
30+
31+
@Inject
32+
lateinit var viewThemeUtils: ViewThemeUtils
33+
34+
private var _binding: FileActionsBottomSheetBinding? = null
35+
val binding
36+
get() = _binding!!
37+
38+
fun interface ResultListener {
39+
fun onResult(@IdRes actionId: Int)
40+
}
41+
42+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
43+
_binding = FileActionsBottomSheetBinding.inflate(inflater, container, false)
44+
45+
val bottomSheetDialog = dialog as BottomSheetDialog
46+
bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
47+
bottomSheetDialog.behavior.skipCollapsed = true
48+
return binding.root
49+
}
50+
51+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
52+
super.onViewCreated(view, savedInstanceState)
53+
binding.bottomSheetHeader.visibility = View.GONE
54+
binding.bottomSheetLoading.visibility = View.GONE
55+
displayActions()
56+
}
57+
58+
override fun onDestroyView() {
59+
super.onDestroyView()
60+
_binding = null
61+
}
62+
63+
fun setResultListener(
64+
fragmentManager: FragmentManager,
65+
lifecycleOwner: LifecycleOwner,
66+
listener: ResultListener
67+
): AlbumItemActionsBottomSheet {
68+
fragmentManager.setFragmentResultListener(REQUEST_KEY, lifecycleOwner) { _, result ->
69+
@IdRes val actionId = result.getInt(RESULT_KEY_ACTION_ID, -1)
70+
if (actionId != -1) {
71+
listener.onResult(actionId)
72+
}
73+
}
74+
return this
75+
}
76+
77+
private fun displayActions() {
78+
if (binding.fileActionsList.isEmpty()) {
79+
AlbumItemAction.SORTED_VALUES.forEach { action ->
80+
val view = inflateActionView(action)
81+
binding.fileActionsList.addView(view)
82+
}
83+
}
84+
}
85+
86+
private fun inflateActionView(action: AlbumItemAction): View {
87+
val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false)
88+
.apply {
89+
root.setOnClickListener {
90+
dispatchActionClick(action.id)
91+
}
92+
text.setText(action.title)
93+
if (action.icon != null) {
94+
icon.setImageResource(action.icon)
95+
}
96+
}
97+
return itemBinding.root
98+
}
99+
100+
private fun dispatchActionClick(id: Int?) {
101+
if (id != null) {
102+
setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_ACTION_ID to id))
103+
parentFragmentManager.clearFragmentResultListener(REQUEST_KEY)
104+
dismiss()
105+
}
106+
}
107+
108+
companion object {
109+
private const val REQUEST_KEY = "REQUEST_KEY_ACTION"
110+
private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID"
111+
112+
@JvmStatic
113+
fun newInstance(): AlbumItemActionsBottomSheet {
114+
return AlbumItemActionsBottomSheet()
115+
}
116+
}
117+
}

app/src/main/java/com/nextcloud/utils/extensions/DrawerActivityExtensions.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ fun DrawerActivity.getMenuItemIdFromTitle(): Int? {
4242
getString(R.string.drawer_item_assistant) -> R.id.nav_assistant
4343
getString(R.string.drawer_item_uploads_list) -> R.id.nav_uploads
4444
getString(R.string.drawer_item_trashbin) -> R.id.nav_trashbin
45+
getString(R.string.drawer_item_album) -> R.id.nav_album
4546
else -> {
4647
if (MainApp.isOnlyPersonFiles()) {
4748
R.id.nav_personal_files

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)