From 5cc05ba3a40a80cf7a440b7ff31f3e96c1237a4a Mon Sep 17 00:00:00 2001
From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com>
Date: Thu, 10 Jun 2021 13:02:00 +0530
Subject: [PATCH] Created models, adapters and view models (#4441)
* created models, adapters and view models
* Added Image Fragment
* back button linked
* Documentation and refractor
* spaces
* Butterknife annotation
* DiffUtil
* Added Examples
* Extended Custom selector From Base Activity
* made view model injectable
---
app/src/main/AndroidManifest.xml | 3 +
.../ContributionsListFragment.java | 83 +++++++----
.../listeners/FolderClickListener.kt | 7 +
.../listeners/ImageLoaderListener.kt | 8 +
.../listeners/ImageSelectListener.kt | 7 +
.../customselector/model/CallbackStatus.kt | 18 +++
.../commons/customselector/model/Folder.kt | 44 ++++++
.../nrw/commons/customselector/model/Image.kt | 125 ++++++++++++++++
.../commons/customselector/model/Result.kt | 13 ++
.../ui/adapter/FolderAdapter.kt | 141 ++++++++++++++++++
.../customselector/ui/adapter/ImageAdapter.kt | 135 +++++++++++++++++
.../ui/adapter/RecyclerViewAdapter.kt | 12 ++
.../ui/selector/CustomSelectorActivity.kt | 121 +++++++++++++++
.../ui/selector/CustomSelectorViewModel.kt | 36 +++++
.../CustomSelectorViewModelFactory.kt | 17 +++
.../ui/selector/FolderFragment.kt | 108 ++++++++++++++
.../ui/selector/ImageFileLoader.kt | 38 +++++
.../ui/selector/ImageFragment.kt | 126 ++++++++++++++++
.../customselector/ui/selector/ImageLoader.kt | 7 +
.../nrw/commons/di/ActivityBuilderModule.java | 4 +
.../commons/di/CommonsApplicationModule.java | 6 +
.../nrw/commons/di/FragmentBuilderModule.java | 8 +
.../res/layout/activity_custom_selector.xml | 9 +-
.../res/layout/custom_selector_toolbar.xml | 7 +-
.../layout/fragment_contributions_list.xml | 13 ++
.../res/layout/fragment_custom_selector.xml | 1 +
.../layout/item_custom_selector_folder.xml | 45 +++---
.../res/layout/item_custom_selector_image.xml | 23 +--
28 files changed, 1092 insertions(+), 73 deletions(-)
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageLoaderListener.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt
create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cc6576aad..1c1068327 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -110,6 +110,9 @@
+
+
)
+ fun onFailed(throwable: Throwable)
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt
new file mode 100644
index 000000000..c29aa21e2
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt
@@ -0,0 +1,7 @@
+package fr.free.nrw.commons.customselector.listeners
+
+import fr.free.nrw.commons.customselector.model.Image
+
+interface ImageSelectListener {
+ fun onSelectedImagesChanged(selectedImages: ArrayList)
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt
new file mode 100644
index 000000000..257b39a95
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt
@@ -0,0 +1,18 @@
+package fr.free.nrw.commons.customselector.model
+
+sealed class CallbackStatus {
+ /**
+ IDLE : The callback is idle , doing nothing.
+ */
+ object IDLE : CallbackStatus()
+
+ /**
+ FETCHING : Fetching images.
+ */
+ object FETCHING : CallbackStatus()
+
+ /**
+ SUCCESS : Success fetching images.
+ */
+ object SUCCESS : CallbackStatus()
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt
new file mode 100644
index 000000000..0ce95ec22
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt
@@ -0,0 +1,44 @@
+package fr.free.nrw.commons.customselector.model
+
+data class Folder(
+ /**
+ bucketId : Unique directory id, eg 540528482
+ */
+ var bucketId: Long,
+
+ /**
+ name : bucket/folder name, eg Camera
+ */
+ var name: String,
+
+ /**
+ images : folder images, list of all images under this folder.
+ */
+ var images: ArrayList = arrayListOf()
+
+
+) {
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ */
+ override fun equals(other: Any?): Boolean {
+
+ if (javaClass != other?.javaClass) {
+ return false
+ }
+
+ other as Folder
+
+ if (bucketId != other.bucketId) {
+ return false
+ }
+ if (name != other.name) {
+ return false
+ }
+ if (images != other.images) {
+ return false
+ }
+
+ return true
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt
new file mode 100644
index 000000000..d6d296f29
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt
@@ -0,0 +1,125 @@
+package fr.free.nrw.commons.customselector.model
+
+import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
+
+data class Image(
+ /**
+ id : Unique image id, primary key of image in device, eg 104950
+ */
+ var id: Long,
+
+ /**
+ name : Name of the image with extension, eg CommonsLogo.jpeg
+ */
+ var name: String,
+
+ /**
+ uri : Uri of the image, points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
+ */
+ var uri: Uri,
+
+ /**
+ path : System path of the image, eg storage/emulated/0/camera/CommonsLogo.jpeg
+ */
+ var path: String,
+
+ /**
+ bucketId : bucketId of folder, eg 540528482
+ */
+ var bucketId: Long = 0,
+
+ /**
+ bucketName : name of folder, eg Camera
+ */
+ var bucketName: String = "",
+
+ /**
+ sha1 : sha1 of original image.
+ */
+ var sha1: String = ""
+) : Parcelable {
+
+ /**
+ default parcelable constructor.
+ */
+ constructor(parcel: Parcel):
+ this(parcel.readLong(),
+ parcel.readString()!!,
+ parcel.readParcelable(Uri::class.java.classLoader)!!,
+ parcel.readString()!!,
+ parcel.readLong(),
+ parcel.readString()!!,
+ parcel.readString()!!
+ )
+
+ /**
+ Write to parcel method.
+ */
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeLong(id)
+ parcel.writeString(name)
+ parcel.writeParcelable(uri, flags)
+ parcel.writeString(path)
+ parcel.writeLong(bucketId)
+ parcel.writeString(bucketName)
+ parcel.writeString(sha1)
+ }
+
+ /**
+ * Describe the kinds of special objects contained in this Parcelable
+ */
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ */
+ override fun equals(other: Any?): Boolean {
+
+ if(javaClass != other?.javaClass) {
+ return false
+ }
+
+ other as Image
+
+ if(id != other.id) {
+ return false;
+ }
+ if(name != other.name) {
+ return false;
+ }
+ if(uri != other.uri) {
+ return false;
+ }
+ if(path != other.path) {
+ return false;
+ }
+ if(bucketId != other.bucketId) {
+ return false;
+ }
+ if(bucketName != other.bucketName) {
+ return false;
+ }
+ if(sha1 != other.sha1) {
+ return false;
+ }
+
+ return true
+ }
+
+ /**
+ * Parcelable companion object
+ */
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): Image {
+ return Image(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt
new file mode 100644
index 000000000..0eb4decbd
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt
@@ -0,0 +1,13 @@
+package fr.free.nrw.commons.customselector.model
+
+data class Result(
+ /**
+ * CallbackStatus : stores the result status
+ */
+ val status:CallbackStatus,
+
+ /**
+ * Images : images retrieved
+ */
+ val images: ArrayList) {
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt
new file mode 100644
index 000000000..11450549c
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt
@@ -0,0 +1,141 @@
+package fr.free.nrw.commons.customselector.ui.adapter
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.customselector.listeners.FolderClickListener
+import fr.free.nrw.commons.customselector.model.Folder
+import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
+
+class FolderAdapter(
+ /**
+ * Application context.
+ */
+ context: Context,
+
+ /**
+ * Folder Click listener for click events.
+ */
+ private val itemClickListener: FolderClickListener
+) : RecyclerViewAdapter(context) {
+
+ /**
+ * Image Loader for loading images.
+ */
+ private val imageLoader = ImageLoader()
+
+ /**
+ * List of folders.
+ */
+ private var folders: MutableList = mutableListOf()
+
+ /**
+ * Create view holder, returns View holder item.
+ */
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
+ val itemView = inflater.inflate(R.layout.item_custom_selector_folder, parent, false)
+ return FolderViewHolder(itemView)
+ }
+
+ /**
+ * Bind view holder, setup the item view, title, count and click listener
+ */
+ override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
+ val folder = folders[position]
+ val count = folder.images.size
+ val previewImage = folder.images[0]
+ holder.name.text = folder.name
+ holder.count.text= count.toString()
+ holder.itemView.setOnClickListener{
+ itemClickListener.onFolderClick(folder)
+ }
+
+ //todo load image thumbnail.
+ }
+
+ /**
+ * Initialise the data set.
+ */
+ fun init(newFolders: List) {
+ val oldFolderList: MutableList = folders
+ val newFolderList = newFolders.toMutableList()
+ val diffResult = DiffUtil.calculateDiff(
+ FoldersDiffCallback(oldFolderList, newFolderList)
+ )
+ folders = newFolderList
+ diffResult.dispatchUpdatesTo(this)
+ }
+
+
+ /**
+ * returns item count.
+ */
+ override fun getItemCount(): Int {
+ return folders.size
+ }
+
+ /**
+ * Folder view holder.
+ */
+ class FolderViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) {
+
+ /**
+ * Folder thumbnail image view.
+ */
+ val image: ImageView = itemView.findViewById(R.id.folder_thumbnail)
+
+ /**
+ * Folder/album name
+ */
+ val name: TextView = itemView.findViewById(R.id.folder_name)
+
+ /**
+ * Item count in Folder/Item
+ */
+ val count: TextView = itemView.findViewById(R.id.folder_count)
+ }
+
+ /**
+ * DiffUtilCallback.
+ */
+ class FoldersDiffCallback(
+ var oldFolders: MutableList,
+ var newFolders: MutableList
+ ) : DiffUtil.Callback() {
+ /**
+ * Returns the size of the old list.
+ */
+ override fun getOldListSize(): Int {
+ return oldFolders.size
+ }
+
+ /**
+ * Returns the size of the new list.
+ */
+ override fun getNewListSize(): Int {
+ return newFolders.size
+ }
+
+ /**
+ * Called by the DiffUtil to decide whether two object represent the same Item.
+ */
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ return oldFolders.get(oldItemPosition).bucketId == newFolders.get(newItemPosition).bucketId
+ }
+
+ /**
+ * Called by the DiffUtil when it wants to check whether two items have the same data.
+ * DiffUtil uses this information to detect if the contents of an item has changed.
+ */
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ return oldFolders.get(oldItemPosition).equals(newFolders.get(newItemPosition))
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt
new file mode 100644
index 000000000..b29910c00
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt
@@ -0,0 +1,135 @@
+package fr.free.nrw.commons.customselector.ui.adapter
+
+import android.content.Context
+import android.view.ViewGroup
+import fr.free.nrw.commons.R
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.Group
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
+import fr.free.nrw.commons.customselector.model.Image
+
+class ImageAdapter(
+ /**
+ * Application Context.
+ */
+ context: Context,
+
+ /**
+ * Image select listener for click events on image.
+ */
+ private var imageSelectListener: ImageSelectListener ):
+
+ RecyclerViewAdapter(context) {
+
+ /**
+ * Currently selected images.
+ */
+ private var selectedImages = arrayListOf()
+
+ /**
+ * List of all images in adapter.
+ */
+ private var images: ArrayList = ArrayList()
+
+ /**
+ * create View holder.
+ */
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
+ val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false)
+ return ImageViewHolder(itemView)
+ }
+
+ /**
+ * Bind View holder, load image, selected view, click listeners.
+ */
+ override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
+ val image=images[position]
+ // todo load image thumbnail, set selected view.
+ holder.itemView.setOnClickListener {
+ selectOrRemoveImage(image, position)
+ }
+ }
+
+ /**
+ * Handle click event on an image, update counter on images.
+ */
+ private fun selectOrRemoveImage(image:Image, position:Int){
+ // todo select the image if not selected and remove it if already selected
+ }
+
+ /**
+ * Initialize the data set.
+ */
+ fun init(newImages:List) {
+ val oldImageList:ArrayList = images
+ val newImageList:ArrayList = ArrayList(newImages)
+ val diffResult = DiffUtil.calculateDiff(
+ ImagesDiffCallback(oldImageList, newImageList)
+ )
+ images = newImageList
+ diffResult.dispatchUpdatesTo(this)
+ }
+
+ /**
+ * Returns the total number of items in the data set held by the adapter.
+ *
+ * @return The total number of items in this adapter.
+ */
+ override fun getItemCount(): Int {
+ return images.size
+ }
+
+ /**
+ * Image view holder.
+ */
+ class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
+ val image: ImageView = itemView.findViewById(R.id.image_thumbnail)
+ val selectedNumber: TextView = itemView.findViewById(R.id.selected_count)
+ val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group)
+ val selectedGroup: Group = itemView.findViewById(R.id.selected_group)
+ }
+
+ /**
+ * DiffUtilCallback.
+ */
+ class ImagesDiffCallback(
+ var oldImageList: ArrayList,
+ var newImageList: ArrayList
+ ) : DiffUtil.Callback(){
+
+ /**
+ * Returns the size of the old list.
+ */
+ override fun getOldListSize(): Int {
+ return oldImageList.size
+ }
+
+ /**
+ * Returns the size of the new list.
+ */
+ override fun getNewListSize(): Int {
+ return newImageList.size
+ }
+
+ /**
+ * Called by the DiffUtil to decide whether two object represent the same Item.
+ */
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ return newImageList[newItemPosition].id == oldImageList[oldItemPosition].id
+ }
+
+ /**
+ * Called by the DiffUtil when it wants to check whether two items have the same data.
+ * DiffUtil uses this information to detect if the contents of an item has changed.
+ */
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ return oldImageList[oldItemPosition].equals(newImageList[newItemPosition])
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt
new file mode 100644
index 000000000..75f935302
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt
@@ -0,0 +1,12 @@
+package fr.free.nrw.commons.customselector.ui.adapter
+
+import android.content.Context
+import android.view.LayoutInflater
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Generic Recycler view adapter.
+ */
+abstract class RecyclerViewAdapter(val context: Context): RecyclerView.Adapter() {
+ val inflater: LayoutInflater = LayoutInflater.from(context)
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
new file mode 100644
index 000000000..1ab30f67e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
@@ -0,0 +1,121 @@
+package fr.free.nrw.commons.customselector.ui.selector
+
+import android.os.Bundle
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.lifecycle.ViewModelProvider
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.customselector.listeners.FolderClickListener
+import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
+import fr.free.nrw.commons.customselector.model.Folder
+import fr.free.nrw.commons.customselector.model.Image
+import fr.free.nrw.commons.theme.BaseActivity
+import javax.inject.Inject
+
+class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener {
+
+ /**
+ * View model.
+ */
+ private lateinit var viewModel: CustomSelectorViewModel
+
+ /**
+ * View Model Factory.
+ */
+ @Inject lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory
+
+ /**
+ * onCreate Activity, sets theme, initialises the view model, setup view.
+ */
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_custom_selector)
+
+ viewModel = ViewModelProvider(this,customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java)
+
+ setupViews()
+ }
+
+ /**
+ * Set up view, default folder view.
+ */
+ private fun setupViews() {
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.fragment_container, FolderFragment.newInstance())
+ .commit()
+ fetchData()
+ setUpToolbar()
+
+ // todo : open image fragment depending on the last user visit.
+ }
+
+ /**
+ * Start data fetch in view model.
+ */
+ private fun fetchData() {
+ viewModel.fetchImages()
+ }
+
+ /**
+ * Change the title of the toolbar.
+ */
+ private fun changeTitle(title:String) {
+ val titleText = findViewById(R.id.title)
+ if(titleText != null) {
+ titleText.text = title
+ }
+ }
+
+ /**
+ * Set up the toolbar, back listener, done listener.
+ */
+ private fun setUpToolbar() {
+ val back : ImageButton = findViewById(R.id.back)
+ back.setOnClickListener { onBackPressed() }
+
+ // todo done listener.
+ }
+
+ /**
+ * override on folder click, change the toolbar title on folder click.
+ */
+ override fun onFolderClick(folder: Folder) {
+ supportFragmentManager.beginTransaction()
+ .add(R.id.fragment_container, ImageFragment.newInstance(folder.bucketId))
+ .addToBackStack(null)
+ .commit()
+ changeTitle(folder.name)
+ }
+
+ /**
+ * override Selected Images Change, update view model selected images.
+ */
+ override fun onSelectedImagesChanged(selectedImages: ArrayList) {
+ // todo update selected images in view model.
+ }
+
+ /**
+ * Back pressed.
+ * Change toolbar title.
+ */
+ override fun onBackPressed() {
+ super.onBackPressed()
+ val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
+ if(fragment != null && fragment is FolderFragment){
+ changeTitle(getString(R.string.custom_selector_title))
+ }
+ }
+
+
+ /**
+ *
+ * TODO
+ * Permission check.
+ * OnDone
+ * Activity Result.
+ *
+ *
+ */
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt
new file mode 100644
index 000000000..a5f7cf6e5
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt
@@ -0,0 +1,36 @@
+package fr.free.nrw.commons.customselector.ui.selector
+
+import android.content.Context
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener
+import fr.free.nrw.commons.customselector.model.CallbackStatus
+import fr.free.nrw.commons.customselector.model.Image
+import fr.free.nrw.commons.customselector.model.Result
+
+class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() {
+
+ /**
+ * Result Live Data
+ */
+ val result = MutableLiveData(Result(CallbackStatus.IDLE, arrayListOf()))
+
+ /**
+ * Fetch Images and supply to result.
+ */
+ fun fetchImages() {
+ result.postValue(Result(CallbackStatus.FETCHING, arrayListOf()))
+ imageFileLoader.abortLoadImage()
+ imageFileLoader.loadDeviceImages(object: ImageLoaderListener {
+
+ override fun onImageLoaded(images: ArrayList) {
+ result.postValue(Result(CallbackStatus.SUCCESS, images))
+ }
+
+ override fun onFailed(throwable: Throwable) {
+ result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf()))
+ }
+
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt
new file mode 100644
index 000000000..d7a7d42f4
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt
@@ -0,0 +1,17 @@
+package fr.free.nrw.commons.customselector.ui.selector
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import javax.inject.Inject
+
+/**
+ * View Model Factory.
+ */
+class CustomSelectorViewModelFactory @Inject constructor(val context: Context,val imageFileLoader: ImageFileLoader) : ViewModelProvider.Factory {
+
+ override fun create(modelClass: Class) : CustomSelectorViewModel {
+ return CustomSelectorViewModel(context,imageFileLoader) as CustomSelectorViewModel
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt
new file mode 100644
index 000000000..a3db47571
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt
@@ -0,0 +1,108 @@
+package fr.free.nrw.commons.customselector.ui.selector
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.GridLayoutManager
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.customselector.model.Result
+import fr.free.nrw.commons.customselector.listeners.FolderClickListener
+import fr.free.nrw.commons.customselector.model.CallbackStatus
+import fr.free.nrw.commons.customselector.model.Folder
+import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
+import kotlinx.android.synthetic.main.fragment_custom_selector.*
+import kotlinx.android.synthetic.main.fragment_custom_selector.view.*
+import javax.inject.Inject
+
+class FolderFragment : CommonsDaggerSupportFragment() {
+
+ /**
+ * View Model for images.
+ */
+ private var viewModel: CustomSelectorViewModel? = null
+
+ /**
+ * View Model Factory.
+ */
+ var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null
+ @Inject set
+
+
+ /**
+ * Folder Adapter.
+ */
+ private lateinit var folderAdapter: FolderAdapter
+
+ /**
+ * Grid Layout Manager for recycler view.
+ */
+ private lateinit var gridLayoutManager: GridLayoutManager
+
+ /**
+ * Companion newInstance.
+ */
+ companion object{
+ fun newInstance(): FolderFragment {
+ return FolderFragment()
+ }
+ }
+
+ /**
+ * OnCreate Fragment, get the view model.
+ */
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory!!).get(CustomSelectorViewModel::class.java)
+
+ }
+
+ /**
+ * OnCreateView.
+ * Inflate Layout, init adapter, init gridLayoutManager, setUp recycler view, observe the view model for result.
+ */
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val root = inflater.inflate(R.layout.fragment_custom_selector, container, false)
+ folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener)
+ gridLayoutManager = GridLayoutManager(context, columnCount())
+ with(root.selector_rv){
+ this.layoutManager = gridLayoutManager
+ setHasFixedSize(true)
+ this.adapter = folderAdapter
+ }
+ viewModel?.result?.observe(viewLifecycleOwner, Observer {
+ handleResult(it)
+ })
+ return root
+ }
+
+ /**
+ * Handle view model result.
+ * Get folders from images.
+ * Load adapter.
+ */
+ private fun handleResult(result: Result) {
+ if(result.status is CallbackStatus.SUCCESS){
+ val folders = arrayListOf()
+ for( i in 1..12) {
+ folders.add(Folder(i.toLong(), "Folder$i",result.images))
+ }
+ folderAdapter.init(folders)
+ folderAdapter.notifyDataSetChanged()
+ selector_rv.visibility = View.VISIBLE
+ }
+ loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE
+ }
+
+ /**
+ * Return Column count ie span count for grid view adapter.
+ */
+ private fun columnCount(): Int {
+ return 2
+ // todo change column count depending on the orientation of the device.
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt
new file mode 100644
index 000000000..738c40e98
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt
@@ -0,0 +1,38 @@
+package fr.free.nrw.commons.customselector.ui.selector
+
+import android.content.Context
+import android.net.Uri
+import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener
+import fr.free.nrw.commons.customselector.model.Image
+
+class ImageFileLoader(val context: Context) {
+
+ /**
+ * Load Device Images.
+ */
+ fun loadDeviceImages(listener: ImageLoaderListener) {
+ var tempImage = Image(0, "temp", Uri.parse("http://www.google.com"), "path", 0, "bucket", "1223")
+ var array: ArrayList = ArrayList()
+ for(i in 1..100) {
+ array.add(tempImage)
+ }
+ listener.onImageLoaded(array)
+
+ // todo load images from device using cursor.
+ }
+
+ /**
+ * Abort loading images.
+ */
+ fun abortLoadImage(){
+ //todo Abort loading images.
+ }
+
+ /**
+ *
+ * TODO
+ * Runnable Thread for image loading.
+ * Sha1 for image (original image).
+ *
+ */
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt
new file mode 100644
index 000000000..c22313d65
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt
@@ -0,0 +1,126 @@
+package fr.free.nrw.commons.customselector.ui.selector
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.GridLayoutManager
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
+import fr.free.nrw.commons.customselector.model.CallbackStatus
+import fr.free.nrw.commons.customselector.model.Result
+import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
+import kotlinx.android.synthetic.main.fragment_custom_selector.*
+import kotlinx.android.synthetic.main.fragment_custom_selector.view.*
+import javax.inject.Inject
+
+class ImageFragment: CommonsDaggerSupportFragment() {
+
+ /**
+ * Current bucketId.
+ */
+ private var bucketId: Long? = null
+
+ /**
+ * View model for images.
+ */
+ private lateinit var viewModel: CustomSelectorViewModel
+
+ /**
+ * View model Factory.
+ */
+ lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory
+ @Inject set
+
+ /**
+ * Image Adapter for recycle view.
+ */
+ private lateinit var imageAdapter: ImageAdapter
+
+ /**
+ * GridLayoutManager for recycler view.
+ */
+ private lateinit var gridLayoutManager: GridLayoutManager
+
+
+ companion object {
+
+ /**
+ * BucketId args name
+ */
+ const val BUCKET_ID = "BucketId"
+
+ /**
+ * newInstance from bucketId.
+ */
+ fun newInstance(bucketId: Long): ImageFragment {
+ val fragment = ImageFragment()
+ val args = Bundle()
+ args.putLong(BUCKET_ID, bucketId)
+ fragment.arguments = args
+ return fragment
+ }
+ }
+
+ /**
+ * OnCreate
+ * Get BucketId, view Model.
+ */
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ bucketId = arguments?.getLong(BUCKET_ID)
+ viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java)
+ }
+
+ /**
+ * OnCreateView
+ * Init imageAdapter, gridLayoutManger.
+ * SetUp recycler view.
+ */
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+
+ val root = inflater.inflate(R.layout.fragment_custom_selector, container, false)
+ imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener)
+ gridLayoutManager = GridLayoutManager(context,getSpanCount())
+ with(root.selector_rv){
+ this.layoutManager = gridLayoutManager
+ setHasFixedSize(true)
+ this.adapter = imageAdapter
+ }
+
+ viewModel.result.observe(viewLifecycleOwner, Observer{
+ handleResult(it)
+ })
+
+ return root
+ }
+
+ /**
+ * Handle view model result.
+ */
+ private fun handleResult(result:Result){
+ if(result.status is CallbackStatus.SUCCESS){
+ val images = result.images
+ if(images.isNotEmpty()) {
+ imageAdapter.init(images)
+ selector_rv.visibility = View.VISIBLE
+ }
+ else{
+ selector_rv.visibility = View.GONE
+ }
+ }
+ loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE
+ }
+
+ /**
+ * getSpanCount for GridViewManager.
+ */
+ private fun getSpanCount(): Int {
+ return 3
+ // todo change span count depending on the device orientation and other factos.
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt
new file mode 100644
index 000000000..22da8cbbb
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt
@@ -0,0 +1,7 @@
+package fr.free.nrw.commons.customselector.ui.selector
+
+/**
+ * Image Loader class, loads images, depending on API results.
+ */
+class ImageLoader {
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
index 28c79c612..6381bdc8e 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
@@ -8,6 +8,7 @@ import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.MainActivity;
+import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
@@ -34,6 +35,9 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract MainActivity bindContributionsActivity();
+ @ContributesAndroidInjector
+ abstract CustomSelectorActivity bindCustomSelectorActivity();
+
@ContributesAndroidInjector
abstract SettingsActivity bindSettingsActivity();
diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
index 1e19de5f4..bca71de98 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
@@ -17,6 +17,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionDao;
+import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.db.AppDatabase;
import fr.free.nrw.commons.kvstore.JsonKvStore;
@@ -66,6 +67,11 @@ public class CommonsApplicationModule {
this.applicationContext = applicationContext;
}
+ @Provides
+ public ImageFileLoader providesImageFileLoader() {
+ return new ImageFileLoader(this.applicationContext);
+ }
+
@Provides
public Context providesApplicationContext() {
return this.applicationContext;
diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
index 3757a2147..f255134ea 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
@@ -8,6 +8,8 @@ import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
+import fr.free.nrw.commons.customselector.ui.selector.FolderFragment;
+import fr.free.nrw.commons.customselector.ui.selector.ImageFragment;
import fr.free.nrw.commons.explore.ExploreFragment;
import fr.free.nrw.commons.explore.ExploreListRootFragment;
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
@@ -49,6 +51,12 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract MediaDetailFragment bindMediaDetailFragment();
+ @ContributesAndroidInjector
+ abstract FolderFragment bindFolderFragment();
+
+ @ContributesAndroidInjector
+ abstract ImageFragment bindImageFragment();
+
@ContributesAndroidInjector
abstract MediaDetailPagerFragment bindMediaDetailPagerFragment();
diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml
index f90ca51e2..9587e7c0a 100644
--- a/app/src/main/res/layout/activity_custom_selector.xml
+++ b/app/src/main/res/layout/activity_custom_selector.xml
@@ -15,10 +15,11 @@
app:layout_constraintTop_toTopOf="parent"/>
-
+ app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/custom_selector_toolbar.xml b/app/src/main/res/layout/custom_selector_toolbar.xml
index 45ebdc923..29b9ab66b 100644
--- a/app/src/main/res/layout/custom_selector_toolbar.xml
+++ b/app/src/main/res/layout/custom_selector_toolbar.xml
@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
>
-
@@ -29,7 +31,7 @@
android:text="@string/custom_selector_title"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
-
diff --git a/app/src/main/res/layout/fragment_contributions_list.xml b/app/src/main/res/layout/fragment_contributions_list.xml
index 49e4e60c7..e9852f49a 100644
--- a/app/src/main/res/layout/fragment_contributions_list.xml
+++ b/app/src/main/res/layout/fragment_contributions_list.xml
@@ -69,6 +69,19 @@
app:fabSize="mini"
app:srcCompat="@drawable/ic_photo_white_24dp" />
+
+
diff --git a/app/src/main/res/layout/item_custom_selector_folder.xml b/app/src/main/res/layout/item_custom_selector_folder.xml
index 4dcc78c1a..077968c6a 100644
--- a/app/src/main/res/layout/item_custom_selector_folder.xml
+++ b/app/src/main/res/layout/item_custom_selector_folder.xml
@@ -1,8 +1,8 @@
-
@@ -24,49 +23,47 @@
android:id="@+id/folder_thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:scaleType="centerCrop"/>
+ android:background="@color/black"
+ android:alpha="0.15"
+ android:scaleType="centerCrop" />
+ android:alpha="0.05" />
+ app:layout_constraintBottom_toBottomOf="parent">
+ android:textSize="16sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent" />
+ android:textStyle="bold"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintRight_toRightOf="parent" />
@@ -75,7 +72,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
- app:constraint_referenced_ids="folder_details,album_overlay"/>
+ app:constraint_referenced_ids="folder_details,album_overlay" />
diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml
index e3240e90e..eec1eb9d9 100644
--- a/app/src/main/res/layout/item_custom_selector_image.xml
+++ b/app/src/main/res/layout/item_custom_selector_image.xml
@@ -3,16 +3,16 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
+ android:padding="@dimen/dimen_2"
android:layout_height="wrap_content">
@@ -38,18 +38,19 @@
android:layout_width="@dimen/dimen_20"
android:layout_height="@dimen/dimen_20"
app:layout_constraintDimensionRatio="H,1:1"
- android:layout_margin="@dimen/dimen_10"
- android:gravity="center|center_vertical"
- android:includeFontPadding="false"
+ android:textSize="11sp"
android:textStyle="bold"
- android:textColor="@color/black"
+ android:layout_margin="@dimen/dimen_6"
+ android:gravity="center|center_vertical"
+ style="@style/TextAppearance.AppCompat.Small"
+ android:text="12"
android:background="@drawable/circle_shape"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>