diff --git a/.idea/navEditor.xml b/.idea/navEditor.xml
index 95e3f92..0f602e7 100644
--- a/.idea/navEditor.xml
+++ b/.idea/navEditor.xml
@@ -146,10 +146,19 @@
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 818f8ff..8cee1d4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -118,6 +118,9 @@ dependencies {
// intro tutorial
implementation 'com.github.AppIntro:AppIntro:5.1.0'
+
+ // fav button
+ implementation 'com.github.ivbaranov:materialfavoritebutton:0.1.5'
}
// ktlint
diff --git a/app/src/main/java/com/esp/localjobs/adapters/JobItem.kt b/app/src/main/java/com/esp/localjobs/adapters/JobItem.kt
index e58e1d4..8771244 100644
--- a/app/src/main/java/com/esp/localjobs/adapters/JobItem.kt
+++ b/app/src/main/java/com/esp/localjobs/adapters/JobItem.kt
@@ -13,6 +13,8 @@ import com.esp.localjobs.data.repository.userFirebaseRepository
import com.esp.localjobs.databinding.ItemJobBinding
import com.esp.localjobs.fragments.JobDetailsFragment
import com.esp.localjobs.fragments.JobsFragmentDirections
+import com.esp.localjobs.utils.IFavoritesManager
+import com.esp.localjobs.utils.favoritesManager
import com.xwray.groupie.databinding.BindableItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -22,6 +24,10 @@ import kotlinx.coroutines.launch
@InternalCoroutinesApi
class JobItem(val job: Job) : BindableItem() {
+ companion object {
+ private val favManager: IFavoritesManager = favoritesManager
+ }
+
override fun getId() = job.uid.hashCode().toLong()
@InternalCoroutinesApi
@@ -35,6 +41,19 @@ class JobItem(val job: Job) : BindableItem() {
setAuthor(author)
}
+ GlobalScope.launch(Dispatchers.IO) {
+ val favorites = favManager.get()
+ if (favorites.contains(this@JobItem.job)) {
+ favToggle.isFavorite = true
+ }
+ favToggle.setOnFavoriteChangeListener { _, isChecked ->
+ if (!isChecked)
+ favManager.remove(this@JobItem.job)
+ else
+ favManager.add(this@JobItem.job)
+ }
+ }
+
this@JobItem.job.imagesUri.firstOrNull()?.let {
Glide.with(cardView.context).load(it).placeholder(R.drawable.placeholder).into(imageView)
} ?: Glide.with(cardView.context).load("https://picsum.photos/400").placeholder(R.drawable.placeholder).into(
diff --git a/app/src/main/java/com/esp/localjobs/data/models/User.kt b/app/src/main/java/com/esp/localjobs/data/models/User.kt
index b16667b..47e942b 100644
--- a/app/src/main/java/com/esp/localjobs/data/models/User.kt
+++ b/app/src/main/java/com/esp/localjobs/data/models/User.kt
@@ -1,14 +1,17 @@
package com.esp.localjobs.data.models
+import android.os.Parcelable
import com.google.firebase.auth.FirebaseUser
+import kotlinx.android.parcel.Parcelize
+@Parcelize
data class User(
val uid: String = "",
val displayName: String = "",
val phoneNumber: String = "",
val photoUrl: String = "",
val mail: String = ""
-)
+) : Parcelable
fun FirebaseUser.toUser() = User(
uid = uid,
diff --git a/app/src/main/java/com/esp/localjobs/fragments/JobsFragment.kt b/app/src/main/java/com/esp/localjobs/fragments/JobsFragment.kt
index 25f1f2e..bd7b80e 100644
--- a/app/src/main/java/com/esp/localjobs/fragments/JobsFragment.kt
+++ b/app/src/main/java/com/esp/localjobs/fragments/JobsFragment.kt
@@ -8,18 +8,22 @@ import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.widget.SearchView
+import androidx.core.view.forEach
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import com.esp.localjobs.R
import com.esp.localjobs.adapters.JobItem
import com.esp.localjobs.data.models.Location
+import com.esp.localjobs.data.models.User
import com.esp.localjobs.data.repository.JobsRepository
import com.esp.localjobs.fragments.FiltersFragment.Companion.FILTER_FRAGMENT_TAG
import com.esp.localjobs.fragments.map.LocationPickerFragment
+import com.esp.localjobs.utils.favoritesManager
import com.esp.localjobs.viewModels.FilterViewModel
import com.esp.localjobs.viewModels.JobsViewModel
import com.xwray.groupie.GroupAdapter
@@ -27,17 +31,28 @@ import com.xwray.groupie.ViewHolder
import kotlinx.android.synthetic.main.fragment_filter_status.*
import kotlinx.android.synthetic.main.fragment_jobs.*
import kotlinx.android.synthetic.main.fragment_jobs.view.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlin.coroutines.CoroutineContext
/**
- * Fragment used to display a list of jobs
+ * Fragment used to display a list of jobs. If arguments include an User then the fragment
+ * shows the user's jobs/proposals
*/
@InternalCoroutinesApi
-class JobsFragment : Fragment(), LocationPickerFragment.OnLocationPickedListener {
+class JobsFragment : Fragment(), LocationPickerFragment.OnLocationPickedListener, CoroutineScope {
+ private lateinit var mJob: kotlinx.coroutines.Job
+ override val coroutineContext: CoroutineContext
+ get() = mJob + Dispatchers.Main
private val jobsViewModel: JobsViewModel by activityViewModels()
private val filterViewModel: FilterViewModel by activityViewModels()
+ private val args: JobsFragmentArgs by navArgs()
+
val adapter = GroupAdapter()
override fun onCreateView(
@@ -56,7 +71,17 @@ class JobsFragment : Fragment(), LocationPickerFragment.OnLocationPickedListener
setupAdapter()
observeChangesInJobList()
- observeFilters()
+
+ when {
+ args.user != null -> setupUserJobsView(args.user as User)
+ args.showFavorites -> showFavorites()
+ else -> observeFilters()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ mJob = kotlinx.coroutines.Job()
}
private fun setupUI(view: View) = with(view) {
@@ -137,6 +162,11 @@ class JobsFragment : Fragment(), LocationPickerFragment.OnLocationPickedListener
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ // hide menu actions if we are showing some user's jobs/proposals or favorites
+ if (args.user != null || args.showFavorites) {
+ menu.forEach { it.isVisible = false }
+ return
+ }
inflater.inflate(R.menu.menu_search, menu)
val searchView = menu.findItem(R.id.action_search_item).actionView as SearchView
setupSearchView(searchView)
@@ -181,6 +211,55 @@ class JobsFragment : Fragment(), LocationPickerFragment.OnLocationPickedListener
}
}
+ private fun setupUserJobsView(user: User) {
+ activity?.title = getString(R.string.user_jobs_title, user.displayName)
+ val fromJobs = filterViewModel.filteringJobs ?: true
+
+ val toCheck = if (fromJobs)
+ R.id.radio_job
+ else
+ R.id.radio_proposal
+
+ jobs_type_radio_group.check(toCheck)
+ jobs_type_radio_group.setOnCheckedChangeListener { _, checkedId ->
+ if (checkedId == R.id.radio_job) {
+ loadJobs(JobsRepository.JobFilter(
+ uid = user.uid,
+ filteringJobs = true
+ ))
+ } else {
+ loadJobs(JobsRepository.JobFilter(
+ uid = user.uid,
+ filteringJobs = false
+ ))
+ }
+ }
+
+ fabAdd.visibility = View.GONE
+ active_filters.visibility = View.GONE
+ jobs_type_radio_group.visibility = View.VISIBLE
+
+ loadJobs(JobsRepository.JobFilter(
+ uid = user.uid,
+ filteringJobs = fromJobs
+ ))
+ }
+
+ private fun showFavorites() = launch {
+ activity?.title = getString(R.string.favorites_title)
+ fabAdd.visibility = View.GONE
+ active_filters.visibility = View.GONE
+
+ val deferredJobs = async(Dispatchers.IO) { favoritesManager.get() }
+ adapter.clear()
+ val jobs = deferredJobs.await()
+ if (jobs.isEmpty()) {
+ no_jobs_title.text = getString(R.string.empty_favorites_title)
+ no_jobs_message.text = ""
+ } else
+ adapter.update(jobs.map { JobItem(it) })
+ }
+
override fun onLocationPicked(location: Location, distance: Int?) {
Log.d(TAG, "location: $location")
filterViewModel.setLocation(location)
diff --git a/app/src/main/java/com/esp/localjobs/fragments/UserProfileFragment.kt b/app/src/main/java/com/esp/localjobs/fragments/UserProfileFragment.kt
index b3fd2a5..d707c83 100644
--- a/app/src/main/java/com/esp/localjobs/fragments/UserProfileFragment.kt
+++ b/app/src/main/java/com/esp/localjobs/fragments/UserProfileFragment.kt
@@ -13,6 +13,7 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.esp.localjobs.R
+import com.esp.localjobs.data.models.User
import com.esp.localjobs.data.repository.userFirebaseRepository
import com.esp.localjobs.databinding.FragmentUserProfileBinding
import com.esp.localjobs.viewModels.LoginViewModel
@@ -60,22 +61,27 @@ class UserProfileFragment : Fragment(), CoroutineScope {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val userId = args.userID
- if (userId == null)
- setupCurrentUserProfile()
- else
- setupUserDetails(userId)
+ args.userID?.let {
+ setupUserDetails(it)
+ } ?: setupCurrentUserProfile()
}
private fun setupCurrentUserProfile() {
- name.text = getString(R.string.not_logged_in)
- logout.visibility = View.GONE
- login.visibility = View.VISIBLE
+ val user = loginViewModel.getCurrentUser()
+
+ if (user == null) {
+ name.text = getString(R.string.not_logged_in)
+ logout.visibility = View.GONE
+ login.visibility = View.VISIBLE
+ } else {
+ binding.user = user
+ setupUserJobsButton(user)
+ setupFavoritesButton()
- loginViewModel.getCurrentUser()?.let {
- binding.user = it
logout.visibility = View.VISIBLE
+ favorites_button.visibility = View.VISIBLE
+
login.visibility = View.GONE
}
@@ -93,7 +99,30 @@ class UserProfileFragment : Fragment(), CoroutineScope {
if (!isActive)
return@launch
- binding.user = user
+ user?.let {
+ binding.user = it
+ setupUserJobsButton(it)
+ }
+ }
+
+ private fun setupUserJobsButton(user: User) {
+ user_jobs.visibility = View.VISIBLE
+ user_jobs.setOnClickListener {
+ val action =
+ UserProfileFragmentDirections.actionDestinationUserProfileToDestinationJobs(user)
+ findNavController().navigate(action)
+ }
+ }
+
+ private fun setupFavoritesButton() {
+ favorites_button.setOnClickListener {
+ val action =
+ UserProfileFragmentDirections.actionDestinationUserProfileToDestinationJobs(
+ null,
+ true
+ )
+ findNavController().navigate(action)
+ }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
diff --git a/app/src/main/java/com/esp/localjobs/utils/IFavoritesManager.kt b/app/src/main/java/com/esp/localjobs/utils/IFavoritesManager.kt
new file mode 100644
index 0000000..5ec259a
--- /dev/null
+++ b/app/src/main/java/com/esp/localjobs/utils/IFavoritesManager.kt
@@ -0,0 +1,9 @@
+package com.esp.localjobs.utils
+
+import com.esp.localjobs.data.models.Job
+
+interface IFavoritesManager {
+ fun add(job: Job)
+ fun remove(job: Job)
+ suspend fun get(): Set
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/esp/localjobs/utils/favoritesManager.kt b/app/src/main/java/com/esp/localjobs/utils/favoritesManager.kt
new file mode 100644
index 0000000..bad52f7
--- /dev/null
+++ b/app/src/main/java/com/esp/localjobs/utils/favoritesManager.kt
@@ -0,0 +1,66 @@
+package com.esp.localjobs.utils
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+import androidx.core.content.edit
+import com.esp.localjobs.LocalJobsApplication
+import com.esp.localjobs.data.base.BaseRepository
+import com.esp.localjobs.data.models.Job
+import com.esp.localjobs.data.repository.JobsRepository
+
+object favoritesManager : IFavoritesManager {
+ private const val FAV_KEY = "favourites_ids"
+
+ private val loader: BaseRepository by lazy { JobsRepository() }
+ private val sharedPreferences: SharedPreferences by lazy {
+ LocalJobsApplication.applicationContext()
+ .getSharedPreferences("favorites", Context.MODE_PRIVATE)
+ }
+
+ private var favorites: MutableSet? = null
+
+ override fun add(job: Job) {
+ val favKeys = sharedPreferences.getStringSet(FAV_KEY, mutableSetOf())
+ ?: mutableSetOf()
+ favKeys.add(job.id)
+ Log.d("favorites", "adding: ${job.id}")
+ sharedPreferences.edit(commit = true) {
+ // stringSet is bugged so i must do this :/
+ remove(FAV_KEY)
+ apply()
+ putStringSet(FAV_KEY, favKeys)
+ apply()
+ }
+ favorites?.add(job)
+ }
+
+ override fun remove(job: Job) {
+ val favKeys = sharedPreferences.getStringSet(FAV_KEY, mutableSetOf()) ?: return
+ favKeys.remove(job.id)
+ Log.d("favorites", "removing: ${job.id}")
+ sharedPreferences.edit(commit = true) {
+ // stringSet is bugged so i must do this :/
+ remove(FAV_KEY)
+ apply()
+ putStringSet(FAV_KEY, favKeys)
+ apply()
+ }
+ favorites?.remove(job)
+ }
+
+ override suspend fun get(): Set {
+ if (favorites == null) {
+ favorites = load()
+ }
+ return (favorites as MutableSet).toSet()
+ }
+
+ private suspend fun load(): MutableSet {
+ val favKeys = sharedPreferences.getStringSet(FAV_KEY, mutableSetOf())
+ Log.d("favorites", "loading: $favKeys")
+ val favList = mutableSetOf()
+ favKeys?.forEach { key -> loader.get(key)?.let { favList.add(it) } }
+ return favList
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/esp/localjobs/viewModels/FilterViewModel.kt b/app/src/main/java/com/esp/localjobs/viewModels/FilterViewModel.kt
index c1cfc36..ec7b9ae 100644
--- a/app/src/main/java/com/esp/localjobs/viewModels/FilterViewModel.kt
+++ b/app/src/main/java/com/esp/localjobs/viewModels/FilterViewModel.kt
@@ -39,6 +39,9 @@ class FilterViewModel : ViewModel() {
val query: String?
get() = activeFilters.value?.query
+ val filteringJobs: Boolean?
+ get() = activeFilters.value?.filteringJobs
+
init {
val context = LocalJobsApplication.applicationContext()
val filter = retrieveLastUsedFilter(context)
diff --git a/app/src/main/res/layout/fragment_jobs.xml b/app/src/main/res/layout/fragment_jobs.xml
index 198b07e..bd18727 100644
--- a/app/src/main/res/layout/fragment_jobs.xml
+++ b/app/src/main/res/layout/fragment_jobs.xml
@@ -20,6 +20,36 @@
android:layout_height="50dp"
android:id="@+id/active_filters"/>
+
+
+
+
+
+
+
+
+
+
@@ -57,5 +57,23 @@
android:visibility="gone"
android:text="@string/login"/>
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_job.xml b/app/src/main/res/layout/item_job.xml
index 30f3133..e6beb6b 100644
--- a/app/src/main/res/layout/item_job.xml
+++ b/app/src/main/res/layout/item_job.xml
@@ -72,6 +72,18 @@
android:background="@drawable/gradient_shape"
app:layout_constraintBottom_toBottomOf="@+id/imageView"/>
+
+
+
+
+
+
Interested people:
Author:
In the past
+ %1$s\'s stuff
+ Jobs and proposals
+ Favorites
+ Favorites
+ Empty favorites!