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
This commit is contained in:
Aditya-Srivastav 2021-06-10 13:02:00 +05:30 committed by GitHub
parent 3463a19446
commit dc26621185
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1060 additions and 41 deletions

View file

@ -108,6 +108,9 @@
<activity android:name=".quiz.QuizResultActivity"
android:label="@string/result"/>
<activity android:name=".customselector.ui.selector.CustomSelectorActivity"
android:label="CustomSelector"/>
<activity
android:name=".category.CategoryDetailsActivity"
android:label="@string/title_activity_featured_images"

View file

@ -5,6 +5,7 @@ import static android.view.View.VISIBLE;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
@ -29,13 +30,16 @@ import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import androidx.recyclerview.widget.SimpleItemAnimator;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.SystemThemeUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.Locale;
import javax.inject.Inject;
@ -67,6 +71,11 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
TextView noContributionsYet;
@BindView(R.id.fab_layout)
LinearLayout fab_layout;
@BindView(R.id.fab_custom_gallery)
FloatingActionButton fabCustomGallery;
@Inject
SystemThemeUtils systemThemeUtils;
@Inject
ContributionController controller;
@ -248,6 +257,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
});
}
@OnClick(R.id.fab_custom_gallery)
void launchCustomSelector(){
Intent intent = new Intent(getActivity(), CustomSelectorActivity.class);
startActivity(intent);
}
private void animateFAB(final boolean isFabOpen) {
this.isFabOpen = !isFabOpen;
if (fabPlus.isShown()) {
@ -255,14 +270,18 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close);
fabGallery.startAnimation(fab_close);
fabCustomGallery.startAnimation(fab_close);
fabCamera.hide();
fabGallery.hide();
fabCustomGallery.hide();
} else {
fabPlus.startAnimation(rotate_forward);
fabCamera.startAnimation(fab_open);
fabGallery.startAnimation(fab_open);
fabCustomGallery.startAnimation(fab_open);
fabCamera.show();
fabGallery.show();
fabCustomGallery.show();
}
this.isFabOpen = !isFabOpen;
}

View file

@ -0,0 +1,7 @@
package fr.free.nrw.commons.customselector.listeners
import fr.free.nrw.commons.customselector.model.Folder
interface FolderClickListener {
fun onFolderClick(folder : Folder)
}

View file

@ -0,0 +1,8 @@
package fr.free.nrw.commons.customselector.listeners
import fr.free.nrw.commons.customselector.model.Image
interface ImageLoaderListener {
fun onImageLoaded(images: ArrayList<Image>)
fun onFailed(throwable: Throwable)
}

View file

@ -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<Image>)
}

View file

@ -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()
}

View file

@ -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<Image> = arrayListOf<Image>()
) {
/**
* 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
}
}

View file

@ -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<Image> {
override fun createFromParcel(parcel: Parcel): Image {
return Image(parcel)
}
override fun newArray(size: Int): Array<Image?> {
return arrayOfNulls(size)
}
}
}

View file

@ -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<Image>) {
}

View file

@ -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<FolderAdapter.FolderViewHolder?>(context) {
/**
* Image Loader for loading images.
*/
private val imageLoader = ImageLoader()
/**
* List of folders.
*/
private var folders: MutableList<Folder> = 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<Folder>) {
val oldFolderList: MutableList<Folder> = 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<Folder>,
var newFolders: MutableList<Folder>
) : 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))
}
}
}

View file

@ -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<ImageAdapter.ImageViewHolder>(context) {
/**
* Currently selected images.
*/
private var selectedImages = arrayListOf<Image>()
/**
* List of all images in adapter.
*/
private var images: ArrayList<Image> = 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<Image>) {
val oldImageList:ArrayList<Image> = images
val newImageList:ArrayList<Image> = 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<Image>,
var newImageList: ArrayList<Image>
) : 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])
}
}
}

View file

@ -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<T : RecyclerView.ViewHolder?>(val context: Context): RecyclerView.Adapter<T>() {
val inflater: LayoutInflater = LayoutInflater.from(context)
}

View file

@ -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<TextView>(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<Image>) {
// 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.
*
*
*/
}

View file

@ -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<Image>) {
result.postValue(Result(CallbackStatus.SUCCESS, images))
}
override fun onFailed(throwable: Throwable) {
result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf()))
}
})
}
}

View file

@ -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<CustomSelectorViewModel: ViewModel?> create(modelClass: Class<CustomSelectorViewModel>) : CustomSelectorViewModel {
return CustomSelectorViewModel(context,imageFileLoader) as CustomSelectorViewModel
}
}

View file

@ -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<Folder>()
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.
}
}

View file

@ -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<Image> = 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).
*
*/
}

View file

@ -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.
}
}

View file

@ -0,0 +1,7 @@
package fr.free.nrw.commons.customselector.ui.selector
/**
* Image Loader class, loads images, depending on API results.
*/
class ImageLoader {
}

View file

@ -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();

View file

@ -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;

View file

@ -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();

View file

@ -15,10 +15,11 @@
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/fragment_custom_selector"
app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
/>
app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<ImageView
<ImageButton
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
@ -13,6 +13,8 @@
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:focusable="true"
app:srcCompat="?attr/custom_selector_back" />
@ -29,7 +31,7 @@
android:text="@string/custom_selector_title"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
<ImageView
<ImageButton
android:id="@+id/done"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -37,6 +39,7 @@
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:clickable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:focusable="true"
android:padding="@dimen/standard_gap"
app:srcCompat="?attr/custom_selector_done"/>

View file

@ -69,6 +69,19 @@
app:fabSize="mini"
app:srcCompat="@drawable/ic_photo_white_24dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_custom_gallery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/button_blue"
android:visibility="gone"
app:backgroundTint="@color/main_background_light"
app:useCompatPadding="true"
app:elevation="@dimen/tiny_margin"
app:fabSize="mini"
app:srcCompat="@drawable/commons_logo"
android:background="@drawable/commons"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_plus"
android:layout_width="wrap_content"

View file

@ -7,6 +7,7 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/selector_rv"
android:background="?attr/mainBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout 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">
<androidx.cardview.widget.CardView
@ -11,7 +11,6 @@
app:cardCornerRadius="@dimen/dimen_6"
app:cardElevation="@dimen/dimen_2"
android:id="@+id/view"
app:cardUseCompatPadding="true"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintTop_toTopOf="parent">
@ -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" />
<View
android:id="@+id/album_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/album_overlay"
android:alpha="0.05"
/>
android:alpha="0.05" />
<LinearLayout
android:id="@+id/folder_details"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/album_name"
android:id="@+id/folder_name"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="@dimen/dimen_6"
android:padding="@dimen/dimen_6"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
android:layout_weight="1"
android:textSize="16sp"
android:ellipsize="end"
android:padding="@dimen/dimen_6"
android:singleLine="true"
android:ellipsize="end"/>
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<TextView
android:id="@+id/album_count"
android:id="@+id/folder_count"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="5dp"
android:padding="5dp"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="16sp"
/>
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</LinearLayout>
@ -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" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -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">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
app:layout_constraintDimensionRatio="H,1:1"
android:layout_height="0dp"
app:cardCornerRadius="@dimen/dimen_6"
app:cardElevation="@dimen/dimen_2"
android:id="@+id/view"
app:cardUseCompatPadding="true"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
@ -29,7 +29,7 @@
android:id="@+id/selected_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.4"
android:alpha="0.25"
android:background="@color/divider_grey"
/>
@ -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"/>
<androidx.constraintlayout.widget.Group
android:id="@+id/selected_view"
android:id="@+id/selected_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
@ -66,15 +67,15 @@
<ImageView
android:id="@+id/uploaded_overlay_icon"
android:layout_width="@dimen/dimen_140"
android:layout_height="@dimen/dimen_140"
android:layout_width="@dimen/dimen_72"
android:layout_height="@dimen/dimen_72"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/commons"
/>
<androidx.constraintlayout.widget.Group
android:id="@+id/uploaded"
android:id="@+id/uploaded_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"