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"/>