Add favourites apps [1/2]

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Rahul Patel
2024-07-02 02:19:51 +05:30
committed by Aayush Gupta
parent 8bc2545067
commit bbedb30d46
18 changed files with 523 additions and 16 deletions

View File

@@ -6,9 +6,12 @@ import androidx.room.TypeConverters
import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.data.room.download.DownloadConverter
import com.aurora.store.data.room.favourites.Favourite
import com.aurora.store.data.room.favourites.FavouriteDao
@Database(entities = [Download::class], version = 1, exportSchema = false)
@Database(entities = [Download::class, Favourite::class], version = 2, exportSchema = false)
@TypeConverters(DownloadConverter::class)
abstract class AuroraDatabase : RoomDatabase() {
abstract fun downloadDao(): DownloadDao
abstract fun favouriteDao(): FavouriteDao
}

View File

@@ -4,6 +4,7 @@ import android.content.Context
import androidx.room.Room
import com.aurora.store.data.room.download.DownloadConverter
import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.data.room.favourites.FavouriteDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -33,4 +34,9 @@ object RoomModule {
fun providesDownloadDao(auroraDatabase: AuroraDatabase): DownloadDao {
return auroraDatabase.downloadDao()
}
@Provides
fun providesFavouriteDao(auroraDatabase: AuroraDatabase): FavouriteDao {
return auroraDatabase.favouriteDao()
}
}

View File

@@ -0,0 +1,23 @@
package com.aurora.store.data.room.favourites
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "favourite")
data class Favourite(
@PrimaryKey
val packageName: String,
val displayName: String,
val iconURL: String,
val added: Long,
val mode: Mode
) : Parcelable {
enum class Mode {
MANUAL,
IMPORT
}
}

View File

@@ -0,0 +1,32 @@
package com.aurora.store.data.room.favourites;
import androidx.room.Dao;
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface FavouriteDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(favourite: Favourite)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(favourites: List<Favourite>)
@Query("SELECT * FROM favourite")
fun favourites(): Flow<List<Favourite>>
@Query("SELECT EXISTS(SELECT 1 FROM favourite WHERE packageName = :packageName)")
suspend fun isFavourite(packageName: String): Boolean
@Query("DELETE FROM favourite WHERE packageName = :packageName")
suspend fun delete(packageName: String)
@Query("DELETE FROM favourite")
suspend fun deleteAll()
@Query("SELECT COUNT(*) FROM favourite")
suspend fun count(): Int
}

View File

@@ -0,0 +1,84 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
import coil.load
import coil.transform.RoundedCornersTransformation
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.data.room.favourites.Favourite
import com.aurora.store.databinding.ViewFavBinding
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class FavouriteView : RelativeLayout {
private lateinit var binding: ViewFavBinding
constructor(context: Context?) : super(context) {
init(context)
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init(context)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context)
}
private fun init(context: Context?) {
val view = inflate(context, R.layout.view_fav, this)
binding = ViewFavBinding.bind(view)
}
@ModelProp
fun favourite(favourite: Favourite) {
binding.imgIcon.load(favourite.iconURL) {
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(25F))
}
binding.txtLine1.text = favourite.displayName
binding.txtLine2.text = favourite.packageName
}
@CallbackProp
fun onClick(onClickListener: OnClickListener?) {
binding.root.setOnClickListener(onClickListener)
}
@CallbackProp
fun onFavourite(onClickListener: OnClickListener?) {
binding.btnFavourite.setOnClickListener(onClickListener)
}
}

View File

@@ -0,0 +1,98 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.commons
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.store.R
import com.aurora.store.data.room.favourites.Favourite
import com.aurora.store.databinding.FragmentFavouriteBinding
import com.aurora.store.view.epoxy.views.FavouriteViewModel_
import com.aurora.store.view.epoxy.views.app.NoAppViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.viewmodel.all.FavouriteViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class FavouriteFragment : BaseFragment(R.layout.fragment_favourite) {
private var _binding: FragmentFavouriteBinding? = null
private val binding: FragmentFavouriteBinding
get() = _binding!!
private val viewModel: FavouriteViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentFavouriteBinding.bind(view)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.favouritesList.collect {
updateController(it)
}
}
// Toolbar
binding.toolbar.setNavigationOnClickListener {
findNavController().navigateUp()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun updateController(favourites: List<Favourite>?) {
binding.recycler.withModels {
setFilterDuplicates(true)
if (favourites == null) {
for (i in 1..10) {
add(
AppListViewShimmerModel_()
.id(i)
)
}
} else if(favourites.isEmpty()) {
add(
NoAppViewModel_()
.id("no_app")
.icon(R.drawable.ic_favorite_unchecked)
.message(getString(R.string.details_no_favourites))
)
} else {
favourites.forEach {
add(
FavouriteViewModel_()
.id(it.packageName.hashCode())
.favourite(it)
.onClick { _ -> openDetailsFragment(it.packageName) }
.onFavourite { _ -> viewModel.removeFavourite(it.packageName) }
)
}
}
}
}
}

View File

@@ -280,6 +280,11 @@ class MoreDialogFragment : DialogFragment() {
icon = R.drawable.ic_blacklist,
destinationID = R.id.blacklistFragment
),
Option(
title = R.string.title_favourites_manager,
icon = R.drawable.ic_favorite_unchecked,
destinationID = R.id.favouriteFragment
),
Option(
title = R.string.title_spoof_manager,
icon = R.drawable.ic_spoof,

View File

@@ -368,6 +368,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
findNavController().navigateUp()
}
}
inflateMenu(R.menu.menu_details)
setOnMenuItemClickListener {
@@ -383,6 +384,10 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
view.context.share(app)
}
R.id.action_favourite -> {
viewModel.toggleFavourite(app)
}
R.id.action_uninstall -> {
AppInstaller.uninstall(requireContext(), app.packageName)
}
@@ -428,6 +433,20 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
uninstallActionEnabled = app.isInstalled
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.favourite.collect {
if (it) {
binding.layoutDetailsToolbar.toolbar.menu
?.findItem(R.id.action_favourite)
?.setIcon(R.drawable.ic_favorite_checked)
} else {
binding.layoutDetailsToolbar.toolbar.menu
?.findItem(R.id.action_favourite)
?.setIcon(R.drawable.ic_favorite_unchecked)
}
}
}
}
override fun onResume() {

View File

@@ -0,0 +1,45 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.viewmodel.all
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.data.room.favourites.FavouriteDao
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class FavouriteViewModel @Inject constructor(
private val favouriteDao: FavouriteDao,
) : ViewModel() {
val favouritesList = favouriteDao.favourites()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun removeFavourite(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
favouriteDao.delete(packageName)
}
}
}

View File

@@ -13,12 +13,16 @@ import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.Report
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.room.favourites.Favourite
import com.aurora.store.data.room.favourites.FavouriteDao
import com.aurora.store.util.DownloadWorkerUtil
import com.google.gson.GsonBuilder
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.lang.reflect.Modifier
@@ -27,7 +31,8 @@ import javax.inject.Inject
@HiltViewModel
class AppDetailsViewModel @Inject constructor(
private val downloadWorkerUtil: DownloadWorkerUtil,
private val authProvider: AuthProvider
private val authProvider: AuthProvider,
private val favouriteDao: FavouriteDao
) : ViewModel() {
private val TAG = AppDetailsViewModel::class.java.simpleName
@@ -50,11 +55,16 @@ class AppDetailsViewModel @Inject constructor(
private val _testingProgramStatus = MutableSharedFlow<TestingProgramStatus?>()
val testingProgramStatus = _testingProgramStatus.asSharedFlow()
private val _favourite = MutableStateFlow<Boolean>(false)
val favourite = _favourite.asStateFlow()
val downloadsList get() = downloadWorkerUtil.downloadsList
fun fetchAppDetails(context: Context, packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
isFavourite(packageName)
_app.emit(
AppDetailsHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
@@ -70,9 +80,11 @@ class AppDetailsViewModel @Inject constructor(
fun fetchAppReviews(context: Context, packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_reviews.emit(ReviewsHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
.getReviewSummary(packageName))
_reviews.emit(
ReviewsHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
.getReviewSummary(packageName)
)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch app reviews", exception)
_reviews.emit(emptyList())
@@ -83,15 +95,17 @@ class AppDetailsViewModel @Inject constructor(
fun postAppReview(context: Context, packageName: String, review: Review, isBeta: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
_userReview.emit(ReviewsHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
.addOrEditReview(
packageName,
review.title,
review.comment,
review.rating,
isBeta
)!!)
_userReview.emit(
ReviewsHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
.addOrEditReview(
packageName,
review.title,
review.comment,
review.rating,
isBeta
)!!
)
} catch (exception: Exception) {
Log.e(TAG, "Failed to post review", exception)
_userReview.emit(Review())
@@ -100,7 +114,7 @@ class AppDetailsViewModel @Inject constructor(
}
fun fetchAppReport(context: Context,packageName: String) {
fun fetchAppReport(context: Context, packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val headers: MutableMap<String, String> = mutableMapOf()
@@ -143,10 +157,36 @@ class AppDetailsViewModel @Inject constructor(
viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) }
}
private fun isFavourite(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_favourite.value = favouriteDao.isFavourite(packageName)
}
}
fun toggleFavourite(app: App) {
viewModelScope.launch(Dispatchers.IO) {
if (favourite.value) {
favouriteDao.delete(app.packageName)
} else {
favouriteDao.insert(
Favourite(
packageName = app.packageName,
displayName = app.displayName,
iconURL = app.iconArtwork.url,
mode = Favourite.Mode.MANUAL,
added = System.currentTimeMillis(),
)
)
}
_favourite.value = !favourite.value
}
}
private fun parseResponse(response: String, packageName: String): List<Report> {
try {
val gson = GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC)
.create()
.create()
val jsonObject = JSONObject(response)
val exodusObject = jsonObject.getJSONObject(packageName)