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!