Merge pull request #10050 from nextcloud/mediaTimeline

Group photos as timeline by month
This commit is contained in:
Álvaro Brey 2022-04-19 15:28:31 +02:00 committed by GitHub
commit c944203d93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1627 additions and 798 deletions

View file

@ -0,0 +1,25 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel
data class GalleryItems(val date: Long, val files: List<OCFile>)

View file

@ -0,0 +1,52 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import com.nextcloud.client.account.User
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.utils.FileSortOrder
interface CommonOCFileListAdapterInterface {
fun isMultiSelect(): Boolean
fun cancelAllPendingTasks()
fun getItemPosition(file: OCFile): Int
fun swapDirectory(
user: User,
directory: OCFile,
storageManager: FileDataStorageManager,
onlyOnDevice: Boolean,
mLimitToMimeType: String
)
fun setHighlightedItem(file: OCFile)
fun setSortOrder(mFile: OCFile, sortOrder: FileSortOrder)
fun addCheckedFile(file: OCFile)
fun isCheckedFile(file: OCFile): Boolean
fun getCheckedItems(): Set<OCFile>
fun removeCheckedFile(file: OCFile)
fun notifyItemChanged(file: OCFile)
fun getFilesCount(): Int
fun setMultiSelect(boolean: Boolean)
fun clearCheckedItems()
}

View file

@ -0,0 +1,249 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
import com.nextcloud.client.account.User
import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.databinding.GalleryHeaderBinding
import com.owncloud.android.databinding.GridImageBinding
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.GalleryItems
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.ui.activity.ComponentsGetter
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.FileSortOrder
import com.owncloud.android.utils.FileStorageUtils
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
import java.util.Calendar
import java.util.Date
class GalleryAdapter(
val context: Context,
user: User,
ocFileListFragmentInterface: OCFileListFragmentInterface,
preferences: AppPreferences,
transferServiceGetter: ComponentsGetter
) : SectionedRecyclerViewAdapter<SectionedViewHolder>(), CommonOCFileListAdapterInterface, SectionedAdapter {
private var files: List<GalleryItems> = mutableListOf()
private var ocFileListDelegate: OCFileListDelegate
private var storageManager: FileDataStorageManager
init {
storageManager = transferServiceGetter.storageManager
ocFileListDelegate = OCFileListDelegate(
context,
ocFileListFragmentInterface,
user,
storageManager,
false,
preferences,
true,
transferServiceGetter,
showMetadata = false,
showShareAvatar = false
)
}
override fun showFooters(): Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder {
return if (viewType == VIEW_TYPE_HEADER) {
GalleryHeaderViewHolder(
GalleryHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
} else {
GalleryItemViewHolder(
GridImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
}
override fun onBindViewHolder(
holder: SectionedViewHolder?,
section: Int,
relativePosition: Int,
absolutePosition: Int
) {
if (holder != null) {
val itemViewHolder = holder as GalleryItemViewHolder
val ocFile = files[section].files[relativePosition]
ocFileListDelegate.bindGridViewHolder(itemViewHolder, ocFile)
}
}
override fun getItemCount(section: Int): Int {
return files[section].files.size
}
override fun getSectionCount(): Int {
return files.size
}
override fun getSectionName(position: Int): String {
return DisplayUtils.getDateByPattern(
files[getRelativePosition(position).section()].date,
context,
DisplayUtils.MONTH_YEAR_PATTERN
)
}
override fun onBindHeaderViewHolder(holder: SectionedViewHolder?, section: Int, expanded: Boolean) {
if (holder != null) {
val headerViewHolder = holder as GalleryHeaderViewHolder
val galleryItem = files[section]
headerViewHolder.binding.month.text = DisplayUtils.getDateByPattern(
galleryItem.date,
context,
DisplayUtils.MONTH_PATTERN
)
headerViewHolder.binding.year.text = DisplayUtils.getDateByPattern(
galleryItem.date,
context, DisplayUtils.YEAR_PATTERN
)
}
}
override fun onBindFooterViewHolder(holder: SectionedViewHolder?, section: Int) {
TODO("Not yet implemented")
}
@SuppressLint("NotifyDataSetChanged")
fun showAllGalleryItems(storageManager: FileDataStorageManager) {
val items = storageManager.allGalleryItems
files = items
.groupBy { firstOfMonth(it.modificationTimestamp) }
.map { GalleryItems(it.key, FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(it.value)) }
.sortedBy { it.date }.reversed()
Handler(Looper.getMainLooper()).post { notifyDataSetChanged() }
}
private fun firstOfMonth(timestamp: Long): Long {
val cal = Calendar.getInstance()
cal.time = Date(timestamp)
cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH))
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
return cal.timeInMillis
}
fun isEmpty(): Boolean {
return files.isEmpty()
}
fun getItem(position: Int): OCFile {
val itemCoord = getRelativePosition(position)
return files[itemCoord.section()].files[itemCoord.relativePos()]
}
override fun isMultiSelect(): Boolean {
return ocFileListDelegate.isMultiSelect
}
override fun cancelAllPendingTasks() {
ocFileListDelegate.cancelAllPendingTasks()
}
override fun getItemPosition(file: OCFile): Int {
val item = files.find { it.files.contains(file) }
return getAbsolutePosition(files.indexOf(item), item?.files?.indexOf(file) ?: 0)
}
override fun swapDirectory(
user: User,
directory: OCFile,
storageManager: FileDataStorageManager,
onlyOnDevice: Boolean,
mLimitToMimeType: String
) {
TODO("Not yet implemented")
}
override fun setHighlightedItem(file: OCFile) {
TODO("Not yet implemented")
}
override fun setSortOrder(mFile: OCFile, sortOrder: FileSortOrder) {
TODO("Not yet implemented")
}
override fun addCheckedFile(file: OCFile) {
ocFileListDelegate.addCheckedFile(file)
}
override fun isCheckedFile(file: OCFile): Boolean {
return ocFileListDelegate.isCheckedFile(file)
}
override fun getCheckedItems(): Set<OCFile> {
return ocFileListDelegate.checkedItems
}
override fun removeCheckedFile(file: OCFile) {
ocFileListDelegate.removeCheckedFile(file)
}
override fun notifyItemChanged(file: OCFile) {
notifyItemChanged(getItemPosition(file))
}
override fun getFilesCount(): Int {
return files.fold(0) { acc, item -> acc + item.files.size }
}
@SuppressLint("NotifyDataSetChanged")
override fun setMultiSelect(boolean: Boolean) {
ocFileListDelegate.isMultiSelect = boolean
notifyDataSetChanged()
}
override fun clearCheckedItems() {
ocFileListDelegate.clearCheckedItems()
}
@VisibleForTesting
fun addFiles(items: List<GalleryItems>) {
files = items
}
}

View file

@ -0,0 +1,28 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
import com.owncloud.android.databinding.GalleryHeaderBinding
class GalleryHeaderViewHolder(val binding: GalleryHeaderBinding) : SectionedViewHolder(binding.root)

View file

@ -0,0 +1,56 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.view.View
import android.widget.ImageView
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.owncloud.android.databinding.GridImageBinding
class GalleryItemViewHolder(val binding: GridImageBinding) :
SectionedViewHolder(binding.root), ListGridImageViewHolder {
override val thumbnail: ImageView
get() = binding.thumbnail
override val shimmerThumbnail: LoaderImageView
get() = binding.thumbnailShimmer
override val favorite: ImageView
get() = binding.favoriteAction
override val localFileIndicator: ImageView
get() = binding.localFileIndicator
override val shared: ImageView
get() = binding.sharedIcon
override val checkbox: ImageView
get() = binding.customCheckbox
override val itemLayout: View
get() = binding.ListItemLayout
override val unreadComments: ImageView
get() = binding.unreadComments
}

View file

@ -0,0 +1,37 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.view.View
import android.widget.ImageView
import com.elyeproj.loaderviewlibrary.LoaderImageView
interface ListGridImageViewHolder {
val thumbnail: ImageView
val shimmerThumbnail: LoaderImageView
val favorite: ImageView
val localFileIndicator: ImageView
val shared: ImageView
val checkbox: ImageView
val itemLayout: View
val unreadComments: ImageView
}

View file

@ -0,0 +1,28 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.widget.TextView
internal interface ListGridItemViewHolder : ListGridImageViewHolder {
val fileName: TextView
}

View file

@ -0,0 +1,35 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.owncloud.android.ui.AvatarGroupLayout
internal interface ListItemViewHolder : ListGridItemViewHolder {
val fileSize: TextView
val fileSizeSeparator: View
val lastModification: TextView
val overflowMenu: ImageView
val sharedAvatars: AvatarGroupLayout
}

View file

@ -0,0 +1,253 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.content.Context
import android.view.View
import com.nextcloud.client.account.User
import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.R
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.activity.ComponentsGetter
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.theme.ThemeColorUtils
import com.owncloud.android.utils.theme.ThemeDrawableUtils
@Suppress("LongParameterList", "TooManyFunctions")
class OCFileListDelegate(
private val context: Context,
private val ocFileListFragmentInterface: OCFileListFragmentInterface,
private val user: User,
private val storageManager: FileDataStorageManager,
private val hideItemOptions: Boolean,
private val preferences: AppPreferences,
private val gridView: Boolean,
private val transferServiceGetter: ComponentsGetter,
private val showMetadata: Boolean,
private var showShareAvatar: Boolean
) {
private val checkedFiles: MutableSet<OCFile> = HashSet()
private var highlightedItem: OCFile? = null
var isMultiSelect = false
private val asyncTasks: MutableList<ThumbnailGenerationTask> = ArrayList()
fun setHighlightedItem(highlightedItem: OCFile?) {
this.highlightedItem = highlightedItem
}
fun isCheckedFile(file: OCFile): Boolean {
return checkedFiles.contains(file)
}
fun addCheckedFile(file: OCFile) {
checkedFiles.add(file)
highlightedItem = null
}
fun removeCheckedFile(file: OCFile) {
checkedFiles.remove(file)
}
fun addToCheckedFiles(files: List<OCFile>?) {
checkedFiles.addAll(files!!)
}
val checkedItems: Set<OCFile>
get() = checkedFiles
fun setCheckedItem(files: Set<OCFile>?) {
checkedFiles.clear()
checkedFiles.addAll(files!!)
}
fun clearCheckedItems() {
checkedFiles.clear()
}
fun bindGridViewHolder(gridViewHolder: ListGridImageViewHolder, file: OCFile) {
// thumbnail
gridViewHolder.thumbnail.tag = file.fileId
DisplayUtils.setThumbnail(
file,
gridViewHolder.thumbnail,
user,
storageManager,
asyncTasks,
gridView,
context,
gridViewHolder.shimmerThumbnail,
preferences
)
// item layout + click listeners
bindGridItemLayout(file, gridViewHolder)
// unread comments
bindUnreadComments(file, gridViewHolder)
// multiSelect (Checkbox)
if (isMultiSelect) {
gridViewHolder.checkbox.visibility = View.VISIBLE
} else {
gridViewHolder.checkbox.visibility = View.GONE
}
// download state
gridViewHolder.localFileIndicator.visibility = View.INVISIBLE // default first
// metadata (downloaded, favorite)
bindGridMetadataViews(file, gridViewHolder)
// shares
val shouldHideShare = gridView || hideItemOptions || file.isFolder && !file.canReshare()
if (shouldHideShare) {
gridViewHolder.shared.visibility = View.GONE
} else {
showShareIcon(gridViewHolder, file)
}
}
private fun bindUnreadComments(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
if (file.unreadCommentsCount > 0) {
gridViewHolder.unreadComments.visibility = View.VISIBLE
gridViewHolder.unreadComments.setOnClickListener {
ocFileListFragmentInterface
.showActivityDetailView(file)
}
} else {
gridViewHolder.unreadComments.visibility = View.GONE
}
}
private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
if (highlightedItem != null && file.fileId == highlightedItem!!.fileId) {
gridViewHolder.itemLayout.setBackgroundColor(
context.resources
.getColor(R.color.selected_item_background)
)
} else if (isCheckedFile(file)) {
gridViewHolder.itemLayout.setBackgroundColor(
context.resources
.getColor(R.color.selected_item_background)
)
gridViewHolder.checkbox.setImageDrawable(
ThemeDrawableUtils.tintDrawable(
R.drawable.ic_checkbox_marked,
ThemeColorUtils.primaryColor(context)
)
)
} else {
gridViewHolder.itemLayout.setBackgroundColor(context.resources.getColor(R.color.bg_default))
gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline)
}
gridViewHolder.itemLayout.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) }
if (!hideItemOptions) {
gridViewHolder.itemLayout.isLongClickable = true
gridViewHolder.itemLayout.setOnLongClickListener {
ocFileListFragmentInterface.onLongItemClicked(
file
)
}
}
}
private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
if (showMetadata) {
showLocalFileIndicator(file, gridViewHolder)
gridViewHolder.favorite.visibility = if (file.isFavorite) View.VISIBLE else View.GONE
} else {
gridViewHolder.localFileIndicator.visibility = View.GONE
gridViewHolder.favorite.visibility = View.GONE
}
}
private fun showLocalFileIndicator(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
val operationsServiceBinder = transferServiceGetter.operationsServiceBinder
val fileDownloaderBinder = transferServiceGetter.fileDownloaderBinder
val fileUploaderBinder = transferServiceGetter.fileUploaderBinder
when {
operationsServiceBinder?.isSynchronizing(user, file) == true ||
fileDownloaderBinder?.isDownloading(user, file) == true ||
fileUploaderBinder?.isUploading(user, file) == true -> {
// synchronizing, downloading or uploading
gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing)
gridViewHolder.localFileIndicator.visibility = View.VISIBLE
}
file.etagInConflict != null -> {
// conflict
gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing_error)
gridViewHolder.localFileIndicator.visibility = View.VISIBLE
}
file.isDown -> {
// downloaded
gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synced)
gridViewHolder.localFileIndicator.visibility = View.VISIBLE
}
}
}
private fun showShareIcon(gridViewHolder: ListGridImageViewHolder, file: OCFile) {
val sharedIconView = gridViewHolder.shared
if (gridViewHolder is OCFileListItemViewHolder || file.unreadCommentsCount == 0) {
sharedIconView.visibility = View.VISIBLE
if (file.isSharedWithSharee || file.isSharedWithMe) {
if (showShareAvatar) {
sharedIconView.visibility = View.GONE
} else {
sharedIconView.visibility = View.VISIBLE
sharedIconView.setImageResource(R.drawable.shared_via_users)
sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared)
}
} else if (file.isSharedViaLink) {
sharedIconView.setImageResource(R.drawable.shared_via_link)
sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared_via_link)
} else {
sharedIconView.setImageResource(R.drawable.ic_unshared)
sharedIconView.contentDescription = context.getString(R.string.shared_icon_share)
}
sharedIconView.setOnClickListener { ocFileListFragmentInterface.onShareIconClick(file) }
} else {
sharedIconView.visibility = View.GONE
}
}
fun cancelAllPendingTasks() {
for (task in asyncTasks) {
task.cancel(true)
if (task.getMethod != null) {
Log_OC.d(TAG, "cancel: abort get method directly")
task.getMethod.abort()
}
}
asyncTasks.clear()
}
fun setShowShareAvatar(bool: Boolean) {
showShareAvatar = bool
}
companion object {
private val TAG = OCFileListDelegate::class.java.simpleName
}
}

View file

@ -0,0 +1,35 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import androidx.recyclerview.widget.RecyclerView
import com.owncloud.android.databinding.ListFooterBinding
internal class OCFileListFooterViewHolder(var binding: ListFooterBinding) : RecyclerView.ViewHolder(
binding.root
) {
val footerText
get() = binding.footerText
val loadingProgressBar
get() = binding.loadingProgressBar
}

View file

@ -0,0 +1,55 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.view.View
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.owncloud.android.databinding.GridImageBinding
internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) :
RecyclerView.ViewHolder(
binding.root
),
ListGridImageViewHolder {
override val thumbnail: ImageView
get() = binding.thumbnail
override val shimmerThumbnail: LoaderImageView
get() = binding.thumbnailShimmer
override val favorite: ImageView
get() = binding.favoriteAction
override val localFileIndicator: ImageView
get() = binding.localFileIndicator
override val shared: ImageView
get() = binding.sharedIcon
override val checkbox: ImageView
get() = binding.customCheckbox
override val itemLayout: View
get() = binding.ListItemLayout
override val unreadComments: ImageView
get() = binding.unreadComments
init {
binding.favoriteAction.drawable.mutate()
}
}

View file

@ -0,0 +1,58 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.owncloud.android.databinding.GridItemBinding
internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) :
RecyclerView.ViewHolder(
binding.root
),
ListGridItemViewHolder {
override val fileName: TextView
get() = binding.Filename
override val thumbnail: ImageView
get() = binding.thumbnail
override val shimmerThumbnail: LoaderImageView
get() = binding.thumbnailShimmer
override val favorite: ImageView
get() = binding.favoriteAction
override val localFileIndicator: ImageView
get() = binding.localFileIndicator
override val shared: ImageView
get() = binding.sharedIcon
override val checkbox: ImageView
get() = binding.customCheckbox
override val itemLayout: View
get() = binding.ListItemLayout
override val unreadComments: ImageView
get() = binding.unreadComments
init {
binding.favoriteAction.drawable.mutate()
}
}

View file

@ -0,0 +1,35 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import androidx.recyclerview.widget.RecyclerView
import com.owncloud.android.databinding.ListHeaderBinding
internal class OCFileListHeaderViewHolder(var binding: ListHeaderBinding) : RecyclerView.ViewHolder(
binding.root
) {
val headerText
get() = binding.headerText
val headerView
get() = binding.headerView
}

View file

@ -0,0 +1,69 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.owncloud.android.databinding.ListItemBinding
import com.owncloud.android.ui.AvatarGroupLayout
internal class OCFileListItemViewHolder(private var binding: ListItemBinding) :
RecyclerView.ViewHolder(
binding.root
),
ListItemViewHolder {
override val fileSize: TextView
get() = binding.fileSize
override val fileSizeSeparator: View
get() = binding.fileSeparator
override val lastModification: TextView
get() = binding.lastMod
override val overflowMenu: ImageView
get() = binding.overflowMenu
override val sharedAvatars: AvatarGroupLayout
get() = binding.sharedAvatars
override val fileName: TextView
get() = binding.Filename
override val thumbnail: ImageView
get() = binding.thumbnail
override val shimmerThumbnail: LoaderImageView
get() = binding.thumbnailShimmer
override val favorite: ImageView
get() = binding.favoriteAction
override val localFileIndicator: ImageView
get() = binding.localFileIndicator
override val shared: ImageView
get() = binding.sharedIcon
override val checkbox: ImageView
get() = binding.customCheckbox
override val itemLayout: View
get() = binding.ListItemLayout
override val unreadComments: ImageView
get() = binding.unreadComments
init {
binding.favoriteAction.drawable.mutate()
}
}

View file

@ -100,7 +100,7 @@ public class GallerySearchTask extends AsyncTask<Void, Void, GallerySearchTask.R
boolean emptySearch = parseMedia(startDate, endDate, result.getData()); boolean emptySearch = parseMedia(startDate, endDate, result.getData());
long lastTimeStamp = findLastTimestamp(result.getData()); long lastTimeStamp = findLastTimestamp(result.getData());
photoFragment.getAdapter().showAllGalleryItems(storageManager); photoFragment.showAllGalleryItems();
return new Result(result.isSuccess(), emptySearch, lastTimeStamp); return new Result(result.isSuccess(), emptySearch, lastTimeStamp);
} else { } else {

View file

@ -37,7 +37,6 @@ import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.datamodel.ThumbnailsCacheManager;
import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.adapter.LocalFileListAdapter; import com.owncloud.android.ui.adapter.LocalFileListAdapter;
import com.owncloud.android.ui.adapter.OCFileListAdapter;
import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.theme.ThemeButtonUtils; import com.owncloud.android.utils.theme.ThemeButtonUtils;
import com.owncloud.android.utils.theme.ThemeCheckableUtils; import com.owncloud.android.utils.theme.ThemeCheckableUtils;
@ -197,7 +196,7 @@ public class ConflictsResolveDialog extends DialogFragment {
existingFile.getModificationTimestamp())); existingFile.getModificationTimestamp()));
binding.existingThumbnail.setTag(existingFile.getFileId()); binding.existingThumbnail.setTag(existingFile.getFileId());
OCFileListAdapter.setThumbnail(existingFile, DisplayUtils.setThumbnail(existingFile,
binding.existingThumbnail, binding.existingThumbnail,
user, user,
new FileDataStorageManager(user, new FileDataStorageManager(user,

View file

@ -31,6 +31,8 @@ import com.owncloud.android.R;
import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.adapter.CommonOCFileListAdapterInterface;
import com.owncloud.android.ui.adapter.GalleryAdapter;
import com.owncloud.android.ui.asynctasks.GallerySearchTask; import com.owncloud.android.ui.asynctasks.GallerySearchTask;
import com.owncloud.android.ui.events.ChangeMenuEvent; import com.owncloud.android.ui.events.ChangeMenuEvent;
@ -50,6 +52,7 @@ public class GalleryFragment extends OCFileListFragment {
private long endDate; private long endDate;
private long daySpan = 30; private long daySpan = 30;
private int limit = 300; private int limit = 300;
private GalleryAdapter mAdapter;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -87,18 +90,45 @@ public class GalleryFragment extends OCFileListFragment {
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
mAdapter.setShowMetadata(false);
currentSearchType = SearchType.GALLERY_SEARCH; currentSearchType = SearchType.GALLERY_SEARCH;
switchToGridView();
menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT; menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT;
requireActivity().invalidateOptionsMenu(); requireActivity().invalidateOptionsMenu();
handleSearchEvent(); handleSearchEvent();
} }
@Override
protected void setAdapter(Bundle args) {
mAdapter = new GalleryAdapter(requireContext(),
accountManager.getUser(),
this,
preferences,
mContainerActivity);
// val spacing = resources.getDimensionPixelSize(R.dimen.media_grid_spacing)
// binding.list.addItemDecoration(MediaGridItemDecoration(spacing))
setRecyclerViewAdapter(mAdapter);
GridLayoutManager layoutManager = new GridLayoutManager(getContext(), getColumnsCount());
// ((GridLayoutManager) layoutManager).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
// @Override
// public int getSpanSize(int position) {
// if (position == getAdapter().getItemCount() - 1 ||
// position == 0 && getAdapter().shouldShowHeader()) {
// return ((GridLayoutManager) layoutManager).getSpanCount();
// } else {
// return 1;
// }
// }
// });
mAdapter.setLayoutManager(layoutManager);
getRecyclerView().setLayoutManager(layoutManager);
}
@Override @Override
public void onRefresh() { public void onRefresh() {
super.onRefresh(); super.onRefresh();
@ -106,6 +136,11 @@ public class GalleryFragment extends OCFileListFragment {
handleSearchEvent(); handleSearchEvent();
} }
@Override
public CommonOCFileListAdapterInterface getCommonAdapter() {
return mAdapter;
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
@ -161,7 +196,7 @@ public class GalleryFragment extends OCFileListFragment {
setEmptyListMessage(SearchType.GALLERY_SEARCH); setEmptyListMessage(SearchType.GALLERY_SEARCH);
} }
if (emptySearch && getAdapter().getItemCount() > 0) { if (emptySearch && mAdapter.getItemCount() > 0) {
Log_OC.d(this, "End gallery search"); Log_OC.d(this, "End gallery search");
return; return;
} }
@ -234,8 +269,7 @@ public class GalleryFragment extends OCFileListFragment {
} }
} }
@Override public void showAllGalleryItems() {
public boolean isGalleryFragment() { mAdapter.showAllGalleryItems(mContainerActivity.getStorageManager());
return true;
} }
} }

View file

@ -78,6 +78,7 @@ import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.activity.FolderPickerActivity; import com.owncloud.android.ui.activity.FolderPickerActivity;
import com.owncloud.android.ui.activity.OnEnforceableRefreshListener; import com.owncloud.android.ui.activity.OnEnforceableRefreshListener;
import com.owncloud.android.ui.activity.UploadFilesActivity; import com.owncloud.android.ui.activity.UploadFilesActivity;
import com.owncloud.android.ui.adapter.CommonOCFileListAdapterInterface;
import com.owncloud.android.ui.adapter.OCFileListAdapter; import com.owncloud.android.ui.adapter.OCFileListAdapter;
import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment; import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment;
import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment; import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment;
@ -189,7 +190,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
protected FileFragment.ContainerActivity mContainerActivity; protected FileFragment.ContainerActivity mContainerActivity;
protected OCFile mFile; protected OCFile mFile;
protected OCFileListAdapter mAdapter; private OCFileListAdapter mAdapter;
protected boolean mOnlyFoldersClickable; protected boolean mOnlyFoldersClickable;
protected boolean mFileSelectable; protected boolean mFileSelectable;
@ -326,7 +327,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (mAdapter != null) {
mAdapter.cancelAllPendingTasks(); mAdapter.cancelAllPendingTasks();
}
if (getActivity() != null) { if (getActivity() != null) {
getActivity().getIntent().removeExtra(OCFileListFragment.SEARCH_EVENT); getActivity().getIntent().removeExtra(OCFileListFragment.SEARCH_EVENT);
@ -349,18 +352,8 @@ public class OCFileListFragment extends ExtendedListFragment implements
mOnlyFoldersClickable = args != null && args.getBoolean(ARG_ONLY_FOLDERS_CLICKABLE, false); mOnlyFoldersClickable = args != null && args.getBoolean(ARG_ONLY_FOLDERS_CLICKABLE, false);
mFileSelectable = args != null && args.getBoolean(ARG_FILE_SELECTABLE, false); mFileSelectable = args != null && args.getBoolean(ARG_FILE_SELECTABLE, false);
mLimitToMimeType = args != null ? args.getString(ARG_MIMETYPE, "") : ""; mLimitToMimeType = args != null ? args.getString(ARG_MIMETYPE, "") : "";
boolean hideItemOptions = args != null && args.getBoolean(ARG_HIDE_ITEM_OPTIONS, false);
mAdapter = new OCFileListAdapter( setAdapter(args);
getActivity(),
accountManager.getUser(),
preferences,
mContainerActivity,
this,
hideItemOptions,
isGridViewPreferred(mFile)
);
setRecyclerViewAdapter(mAdapter);
mHideFab = args != null && args.getBoolean(ARG_HIDE_FAB, false); mHideFab = args != null && args.getBoolean(ARG_HIDE_FAB, false);
@ -406,6 +399,22 @@ public class OCFileListFragment extends ExtendedListFragment implements
listDirectory(false, false); listDirectory(false, false);
} }
protected void setAdapter(Bundle args) {
boolean hideItemOptions = args != null && args.getBoolean(ARG_HIDE_ITEM_OPTIONS, false);
mAdapter = new OCFileListAdapter(
getActivity(),
accountManager.getUser(),
preferences,
mContainerActivity,
this,
hideItemOptions,
isGridViewPreferred(mFile)
);
setRecyclerViewAdapter(mAdapter);
}
protected void prepareCurrentSearch(SearchEvent event) { protected void prepareCurrentSearch(SearchEvent event) {
if (isSearchEventSet(event)) { if (isSearchEventSet(event)) {
@ -691,7 +700,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
// hide FAB in multi selection mode // hide FAB in multi selection mode
setFabVisible(false); setFabVisible(false);
mAdapter.setMultiSelect(true); getCommonAdapter().setMultiSelect(true);
return true; return true;
} }
@ -700,12 +709,12 @@ public class OCFileListFragment extends ExtendedListFragment implements
*/ */
@Override @Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) { public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
final int checkedCount = mAdapter.getCheckedItems().size(); Set<OCFile> checkedFiles = getCommonAdapter().getCheckedItems();
Set<OCFile> checkedFiles = mAdapter.getCheckedItems(); final int checkedCount = checkedFiles.size();
String title = getResources().getQuantityString(R.plurals.items_selected_count, checkedCount, checkedCount); String title = getResources().getQuantityString(R.plurals.items_selected_count, checkedCount, checkedCount);
mode.setTitle(title); mode.setTitle(title);
FileMenuFilter mf = new FileMenuFilter( FileMenuFilter mf = new FileMenuFilter(
mAdapter.getFiles().size(), getCommonAdapter().getFilesCount(),
checkedFiles, checkedFiles,
mContainerActivity, mContainerActivity,
getActivity(), getActivity(),
@ -728,7 +737,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
*/ */
@Override @Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Set<OCFile> checkedFiles = mAdapter.getCheckedItems(); Set<OCFile> checkedFiles = getCommonAdapter().getCheckedItems();
return onFileActionChosen(item, checkedFiles); return onFileActionChosen(item, checkedFiles);
} }
@ -744,8 +753,8 @@ public class OCFileListFragment extends ExtendedListFragment implements
setFabVisible(true); setFabVisible(true);
} }
mAdapter.setMultiSelect(false); getCommonAdapter().setMultiSelect(false);
mAdapter.clearCheckedItems(); getCommonAdapter().clearCheckedItems();
} }
public void storeStateIn(Bundle outState) { public void storeStateIn(Bundle outState) {
@ -865,10 +874,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
* @param file The concerned OCFile by the selection/deselection * @param file The concerned OCFile by the selection/deselection
*/ */
private void toggleItemToCheckedList(OCFile file) { private void toggleItemToCheckedList(OCFile file) {
if (getAdapter().isCheckedFile(file)) { if (getCommonAdapter().isCheckedFile(file)) {
getAdapter().removeCheckedFile(file); getCommonAdapter().removeCheckedFile(file);
} else { } else {
getAdapter().addCheckedFile(file); getCommonAdapter().addCheckedFile(file);
} }
updateActionModeFile(file); updateActionModeFile(file);
} }
@ -881,7 +890,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
mIsActionModeNew = false; mIsActionModeNew = false;
if (mActiveActionMode != null) { if (mActiveActionMode != null) {
mActiveActionMode.invalidate(); mActiveActionMode.invalidate();
mAdapter.notifyItemChanged(getAdapter().getItemPosition(file)); getCommonAdapter().notifyItemChanged(file);
} }
} }
@ -894,7 +903,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
toggleItemToCheckedList(file); toggleItemToCheckedList(file);
} else { } else {
actionBarActivity.startActionMode(mMultiChoiceModeListener); actionBarActivity.startActionMode(mMultiChoiceModeListener);
getAdapter().addCheckedFile(file); getCommonAdapter().addCheckedFile(file);
} }
updateActionModeFile(file); updateActionModeFile(file);
} }
@ -904,11 +913,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
@Override @Override
public void onItemClicked(OCFile file) { public void onItemClicked(OCFile file) {
if (getAdapter().isMultiSelect()) { if (getCommonAdapter().isMultiSelect()) {
toggleItemToCheckedList(file); toggleItemToCheckedList(file);
} else { } else {
if (file != null) { if (file != null) {
int position = mAdapter.getItemPosition(file); int position = getCommonAdapter().getItemPosition(file);
if (file.isFolder()) { if (file.isFolder()) {
resetHeaderScrollingState(); resetHeaderScrollingState();
@ -1229,6 +1238,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
if (!directory.isFolder()) { if (!directory.isFolder()) {
Log_OC.w(TAG, "You see, that is not a directory -> " + directory); Log_OC.w(TAG, "You see, that is not a directory -> " + directory);
directory = storageManager.getFileById(directory.getParentId()); directory = storageManager.getFileById(directory.getParentId());
if (directory == null) {
return; // no files, wait for sync
}
} }
if (searchView != null && !searchView.isIconified() && !fromSearch) { if (searchView != null && !searchView.isIconified() && !fromSearch) {
@ -1379,6 +1392,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
getAdapter().notifyDataSetChanged(); getAdapter().notifyDataSetChanged();
} }
public CommonOCFileListAdapterInterface getCommonAdapter() {
return mAdapter;
}
public OCFileListAdapter getAdapter() { public OCFileListAdapter getAdapter() {
return mAdapter; return mAdapter;
} }
@ -1587,7 +1604,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
FileDataStorageManager storageManager = mContainerActivity.getStorageManager(); FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
int position = mAdapter.getItemPosition(storageManager.getFileByRemoteId(event.remoteId)); OCFile file = storageManager.getFileByRemoteId(event.remoteId);
int position = -1;
if (file != null) {
position = mAdapter.getItemPosition(file);
}
SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position); SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position);
dialog.setTargetFragment(this, SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE); dialog.setTargetFragment(this, SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE);
dialog.show(getParentFragmentManager(), SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG); dialog.show(getParentFragmentManager(), SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG);
@ -1656,11 +1677,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
return searchFragment; return searchFragment;
} }
@Override
public boolean isGalleryFragment() {
return false;
}
/** /**
* De-/select all elements in the current list view. * De-/select all elements in the current list view.
* *
@ -1672,7 +1688,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
if (select) { if (select) {
ocFileListAdapter.addAllFilesToCheckedFiles(); ocFileListAdapter.addAllFilesToCheckedFiles();
} else { } else {
ocFileListAdapter.removeAllFilesFromCheckedFiles(); ocFileListAdapter.clearCheckedItems();
} }
for (int i = 0; i < mAdapter.getItemCount(); i++) { for (int i = 0; i < mAdapter.getItemCount(); i++) {

View file

@ -69,7 +69,7 @@ class OCFileListSearchAsyncTask(
if (remoteOperationResult.resultData.isNullOrEmpty()) { if (remoteOperationResult.resultData.isNullOrEmpty()) {
fragment.setEmptyView(event) fragment.setEmptyView(event)
} else { } else {
fragment.mAdapter.setData( fragment.adapter.setData(
remoteOperationResult.resultData, remoteOperationResult.resultData,
fragment.currentSearchType, fragment.currentSearchType,
fileDataStorageManager, fileDataStorageManager,
@ -85,7 +85,7 @@ class OCFileListSearchAsyncTask(
fragmentReference.get()?.let { fragment -> fragmentReference.get()?.let { fragment ->
fragment.isLoading = false fragment.isLoading = false
if (!isCancelled) { if (!isCancelled) {
fragment.mAdapter.notifyDataSetChanged() fragment.adapter.notifyDataSetChanged()
} }
} }
} }

View file

@ -59,7 +59,7 @@ class SharedListFragment : OCFileListFragment(), Injectable {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
mAdapter.setShowMetadata(false) adapter.setShowMetadata(false)
currentSearchType = SearchType.SHARED_FILTER currentSearchType = SearchType.SHARED_FILTER
searchEvent = SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER) searchEvent = SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER)
menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT

View file

@ -48,8 +48,4 @@ public interface OCFileListFragmentInterface {
boolean isLoading(); boolean isLoading();
void onHeaderClicked(); void onHeaderClicked();
boolean isSearchFragment();
boolean isGalleryFragment();
} }

View file

@ -27,6 +27,8 @@ import java.security.SecureRandom;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/** /**
* Data holder utility to store & retrieve stuff * Data holder utility to store & retrieve stuff
*/ */
@ -39,6 +41,7 @@ public class DataHolderUtil {
@SuppressLint("TrulyRandom") @SuppressLint("TrulyRandom")
private SecureRandom random = new SecureRandom(); private SecureRandom random = new SecureRandom();
@SuppressFBWarnings("MS_EXPOSE_REP")
public static synchronized DataHolderUtil getInstance() { public static synchronized DataHolderUtil getInstance() {
if (instance == null) { if (instance == null) {
instance = new DataHolderUtil(); instance = new DataHolderUtil();

View file

@ -46,6 +46,9 @@ import android.text.style.StyleSpan;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Toast; import android.widget.Toast;
import com.bumptech.glide.GenericRequestBuilder; import com.bumptech.glide.GenericRequestBuilder;
@ -56,13 +59,16 @@ import com.bumptech.glide.load.resource.file.FileToStreamDecoder;
import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.target.Target;
import com.caverock.androidsvg.SVG; import com.caverock.androidsvg.SVG;
import com.elyeproj.loaderviewlibrary.LoaderImageView;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.account.CurrentAccountProvider;
import com.nextcloud.client.account.User; import com.nextcloud.client.account.User;
import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.network.ClientFactory;
import com.nextcloud.client.preferences.AppPreferences;
import com.owncloud.android.MainApp; import com.owncloud.android.MainApp;
import com.owncloud.android.R; import com.owncloud.android.R;
import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.datamodel.ThumbnailsCacheManager;
import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.OwnCloudAccount;
@ -89,17 +95,19 @@ import java.math.BigDecimal;
import java.net.IDN; import java.net.IDN;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.TimeZone;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.widget.AppCompatDrawableManager; import androidx.appcompat.widget.AppCompatDrawableManager;
import androidx.core.content.res.ResourcesCompat; import androidx.core.content.res.ResourcesCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
@ -130,6 +138,10 @@ public final class DisplayUtils {
private static final double BYTE_SIZE_DIVIDER_DOUBLE = 1024.0; private static final double BYTE_SIZE_DIVIDER_DOUBLE = 1024.0;
private static final int DATE_TIME_PARTS_SIZE = 2; private static final int DATE_TIME_PARTS_SIZE = 2;
public static final String MONTH_YEAR_PATTERN = "MMMM yyyy";
public static final String MONTH_PATTERN = "MMMM";
public static final String YEAR_PATTERN = "yyyy";
private static Map<String, String> mimeType2HumanReadable; private static Map<String, String> mimeType2HumanReadable;
static { static {
@ -820,4 +832,178 @@ public final class DisplayUtils {
return R.string.menu_item_sort_by_name_a_z; return R.string.menu_item_sort_by_name_a_z;
} }
} }
public static String getDateByPattern(long timestamp, Context context, String pattern) {
DateFormat df = new SimpleDateFormat(pattern, context.getResources().getConfiguration().locale);
df.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
return df.format(timestamp);
}
public static void setThumbnail(OCFile file,
ImageView thumbnailView,
User user,
FileDataStorageManager storageManager,
List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks,
boolean gridView,
Context context) {
setThumbnail(file,
thumbnailView,
user,
storageManager,
asyncTasks,
gridView,
context,
null,
null);
}
public static void setThumbnail(OCFile file,
ImageView thumbnailView,
User user,
FileDataStorageManager storageManager,
List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks,
boolean gridView,
Context context,
LoaderImageView shimmerThumbnail,
AppPreferences preferences) {
if (file.isFolder()) {
stopShimmer(shimmerThumbnail, thumbnailView);
thumbnailView.setImageDrawable(MimeTypeUtil
.getFolderTypeIcon(file.isSharedWithMe() || file.isSharedWithSharee(),
file.isSharedViaLink(), file.isEncrypted(),
file.getMountType(), context));
} else {
if (file.getRemoteId() != null && file.isPreviewAvailable()) {
// Thumbnail in cache?
Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId()
);
if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
stopShimmer(shimmerThumbnail, thumbnailView);
if (MimeTypeUtil.isVideo(file)) {
Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
thumbnailView.setImageBitmap(withOverlay);
} else {
if (gridView) {
BitmapUtils.setRoundedBitmapForGridMode(thumbnail, thumbnailView);
} else {
BitmapUtils.setRoundedBitmap(thumbnail, thumbnailView);
}
}
} else {
// generate new thumbnail
if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) {
try {
final ThumbnailsCacheManager.ThumbnailGenerationTask task =
new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView,
storageManager,
user,
asyncTasks,
gridView);
if (thumbnail == null) {
Drawable drawable = MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
file.getFileName(),
user,
context);
if (drawable == null) {
drawable = ResourcesCompat.getDrawable(context.getResources(),
R.drawable.file_image,
null);
}
int px = ThumbnailsCacheManager.getThumbnailDimension();
thumbnail = BitmapUtils.drawableToBitmap(drawable, px, px);
}
final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
new ThumbnailsCacheManager.AsyncThumbnailDrawable(context.getResources(),
thumbnail, task);
if (shimmerThumbnail != null && shimmerThumbnail.getVisibility() == View.GONE) {
if (gridView) {
configShimmerGridImageSize(shimmerThumbnail, preferences.getGridColumns());
}
startShimmer(shimmerThumbnail, thumbnailView);
}
task.setListener(new ThumbnailsCacheManager.ThumbnailGenerationTask.Listener() {
@Override
public void onSuccess() {
stopShimmer(shimmerThumbnail, thumbnailView);
}
@Override
public void onError() {
stopShimmer(shimmerThumbnail, thumbnailView);
}
});
thumbnailView.setImageDrawable(asyncDrawable);
asyncTasks.add(task);
task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file,
file.getRemoteId()));
} catch (IllegalArgumentException e) {
Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage());
}
}
}
if ("image/png".equalsIgnoreCase(file.getMimeType())) {
thumbnailView.setBackgroundColor(context.getResources().getColor(R.color.bg_default));
}
} else {
stopShimmer(shimmerThumbnail, thumbnailView);
thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
file.getFileName(),
user,
context));
}
}
}
private static void startShimmer(LoaderImageView thumbnailShimmer, ImageView thumbnailView) {
thumbnailShimmer.setImageResource(R.drawable.background);
thumbnailShimmer.resetLoader();
thumbnailView.setVisibility(View.GONE);
thumbnailShimmer.setVisibility(View.VISIBLE);
}
private static void stopShimmer(@Nullable LoaderImageView thumbnailShimmer, ImageView thumbnailView) {
if (thumbnailShimmer != null) {
thumbnailShimmer.setVisibility(View.GONE);
}
thumbnailView.setVisibility(View.VISIBLE);
}
private static void configShimmerGridImageSize(LoaderImageView thumbnailShimmer, float gridColumns) {
FrameLayout.LayoutParams targetLayoutParams = (FrameLayout.LayoutParams) thumbnailShimmer.getLayoutParams();
try {
final Point screenSize = getScreenSize(thumbnailShimmer.getContext());
final int marginLeftAndRight = targetLayoutParams.leftMargin + targetLayoutParams.rightMargin;
final int size = Math.round(screenSize.x / gridColumns - marginLeftAndRight);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
params.setMargins(targetLayoutParams.leftMargin,
targetLayoutParams.topMargin,
targetLayoutParams.rightMargin,
targetLayoutParams.bottomMargin);
thumbnailShimmer.setLayoutParams(params);
} catch (Exception exception) {
Log_OC.e("ConfigShimmer", exception.getMessage());
}
}
private static Point getScreenSize(Context context) throws Exception {
final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (windowManager != null) {
final Point displaySize = new Point();
windowManager.getDefaultDisplay().getSize(displaySize);
return displaySize;
} else {
throw new Exception("WindowManager not found");
}
}
} }

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?><!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2022 Tobias Kaminsky
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/alternate_half_padding"
android:paddingTop="@dimen/alternate_half_padding"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="@+id/month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_half_margin"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textStyle="bold"
tools:text="July" />
<TextView
android:id="@+id/year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_margin"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:text="2016" />
</LinearLayout>

View file

@ -0,0 +1,94 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.content.Context
import com.nextcloud.client.account.User
import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.GalleryItems
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.ui.activity.ComponentsGetter
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
import junit.framework.Assert.assertEquals
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
class GalleryAdapterTest {
@Mock
lateinit var context: Context
@Mock
lateinit var user: User
@Mock
lateinit var ocFileListFragmentInterface: OCFileListFragmentInterface
@Mock
lateinit var preferences: AppPreferences
@Mock
lateinit var transferServiceGetter: ComponentsGetter
@Mock
lateinit var storageManager: FileDataStorageManager
private lateinit var mocks: AutoCloseable
@Before
fun setUp() {
mocks = MockitoAnnotations.openMocks(this)
}
@After
fun tearDown() {
mocks.close()
}
@Test
fun testItemCount() {
whenever(transferServiceGetter.storageManager) doReturn storageManager
val sut = GalleryAdapter(
context,
user,
ocFileListFragmentInterface,
preferences,
transferServiceGetter
)
val list = listOf(
GalleryItems(1649317247, listOf(OCFile("/1.md"), OCFile("/2.md"))),
GalleryItems(1649317247, listOf(OCFile("/1.md"), OCFile("/2.md")))
)
sut.addFiles(list)
assertEquals(4, sut.getFilesCount())
}
}

View file

@ -1 +1 @@
592 536

View file

@ -1,2 +1,2 @@
DO NOT TOUCH; GENERATED BY DRONE DO NOT TOUCH; GENERATED BY DRONE
<span class="mdl-layout-title">Lint Report: 92 warnings</span> <span class="mdl-layout-title">Lint Report: 89 warnings</span>

View file

@ -56,6 +56,7 @@
<Bug pattern="ANDROID_EXTERNAL_FILE_ACCESS" /> <Bug pattern="ANDROID_EXTERNAL_FILE_ACCESS" />
<Bug pattern="BAS_BLOATED_ASSIGNMENT_SCOPE" /> <Bug pattern="BAS_BLOATED_ASSIGNMENT_SCOPE" />
<Bug pattern="IMC_IMMATURE_CLASS_BAD_SERIALVERSIONUID" /> <Bug pattern="IMC_IMMATURE_CLASS_BAD_SERIALVERSIONUID" />
<Bug pattern="EI_EXPOSE_REP" />
<Bug pattern="EI_EXPOSE_REP2" /> <Bug pattern="EI_EXPOSE_REP2" />
<!-- This is unmanageable for now due to large amount of interconnected static state --> <!-- This is unmanageable for now due to large amount of interconnected static state -->