Add app details stream (Related, Similar, More by dev)

This commit is contained in:
Rahul Kumar Patel
2021-02-20 03:57:26 +05:30
parent c6f528c82e
commit 209e3f69dd
8 changed files with 315 additions and 60 deletions

View File

@@ -172,7 +172,7 @@ dependencies {
implementation "com.github.topjohnwu.libsu:core:${versions.libsu}" implementation "com.github.topjohnwu.libsu:core:${versions.libsu}"
//Love <3 //Love <3
api("com.gitlab.AuroraOSS:gplayapi:9d327499ae") api("com.gitlab.AuroraOSS:gplayapi:101c69ecde")
} }
Properties props = new Properties() Properties props = new Properties()

View File

@@ -0,0 +1,70 @@
/*
* 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.controller
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.store.view.epoxy.groups.CarouselModelGroup
import com.aurora.store.view.epoxy.groups.CarouselShimmerGroup
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
open class DetailsCarouselController(private val callbacks: Callbacks) :
GenericCarouselController(callbacks) {
override fun buildModels(streamBundle: StreamBundle?) {
setFilterDuplicates(true)
if (streamBundle == null) {
for (i in 1..2) {
add(
CarouselShimmerGroup()
.id(i)
)
}
} else {
if (streamBundle.streamClusters.isNotEmpty()) {
if (streamBundle.streamClusters.size == 1) {
streamBundle
.streamClusters
.values
.filter { applyFilter(it) }
.forEach { streamCluster ->
streamCluster.clusterAppList.forEach {
add(
AppListViewModel_()
.id(it.id)
.app(it)
.click { _ -> callbacks.onAppClick(it) }
)
}
}
} else {
streamBundle
.streamClusters
.values
.filter { applyFilter(it) }
.forEach { streamCluster ->
add(CarouselModelGroup(streamCluster, callbacks))
}
}
}
}
}
}

View File

@@ -324,6 +324,8 @@ class AppDetailsActivity : BaseDetailsActivity() {
inflateBetaSubscription(B.layoutDetailsBeta, app) inflateBetaSubscription(B.layoutDetailsBeta, app)
} }
inflateAppStream(B.epoxyRecyclerStream, app)
} }
} }

View File

@@ -29,12 +29,14 @@ import android.widget.RelativeLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.lifecycle.ViewModelProvider
import com.airbnb.epoxy.EpoxyRecyclerView
import com.aurora.Constants import com.aurora.Constants
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.*
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.ViewState
import com.aurora.store.data.model.ExodusReport import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.Report import com.aurora.store.data.model.Report
import com.aurora.store.data.network.HttpClient import com.aurora.store.data.network.HttpClient
@@ -47,11 +49,14 @@ import com.aurora.store.util.extensions.load
import com.aurora.store.util.extensions.show import com.aurora.store.util.extensions.show
import com.aurora.store.util.extensions.toast import com.aurora.store.util.extensions.toast
import com.aurora.store.view.custom.RatingView import com.aurora.store.view.custom.RatingView
import com.aurora.store.view.epoxy.controller.DetailsCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.epoxy.views.* import com.aurora.store.view.epoxy.views.*
import com.aurora.store.view.epoxy.views.details.ReviewViewModel_ import com.aurora.store.view.epoxy.views.details.ReviewViewModel_
import com.aurora.store.view.epoxy.views.details.ScreenshotView import com.aurora.store.view.epoxy.views.details.ScreenshotView
import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_ import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
import com.aurora.store.view.ui.commons.BaseActivity import com.aurora.store.view.ui.commons.BaseActivity
import com.aurora.store.viewmodel.details.DetailsClusterViewModel
import nl.komponents.kovenant.task import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
@@ -69,7 +74,8 @@ abstract class BaseDetailsActivity : BaseActivity() {
B.txtInstalls.text = CommonUtil.addDiPrefix(app.installs) B.txtInstalls.text = CommonUtil.addDiPrefix(app.installs)
B.txtSize.text = CommonUtil.addSiPrefix(app.size) B.txtSize.text = CommonUtil.addSiPrefix(app.size)
B.txtRating.text = app.labeledRating B.txtRating.text = app.labeledRating
B.txtSdk.text = String.format("Target SDK %s", "${app.targetSdk}") B.txtSdk.text = ("Target SDK ${app.targetSdk}")
B.txtUpdated.text = app.updatedOn
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
B.txtDescription.justificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD B.txtDescription.justificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD
@@ -280,6 +286,57 @@ abstract class BaseDetailsActivity : BaseActivity() {
} }
} }
fun inflateAppStream(epoxyRecyclerView: EpoxyRecyclerView, app: App) {
app.detailsStreamUrl?.let {
val VM = ViewModelProvider(this).get(DetailsClusterViewModel::class.java)
val carouselController =
DetailsCarouselController(object : GenericCarouselController.Callbacks {
override fun onHeaderClicked(streamCluster: StreamCluster) {
if (streamCluster.clusterBrowseUrl.isNotEmpty())
openStreamBrowseActivity(streamCluster.clusterBrowseUrl)
else
toast("Browse page unavailable")
}
override fun onClusterScrolled(streamCluster: StreamCluster) {
VM.observeCluster(streamCluster)
}
override fun onAppClick(app: App) {
openDetailsActivity(app)
}
override fun onAppLongClick(app: App) {
}
})
VM.liveData.observe(this, {
when (it) {
is ViewState.Empty -> {
}
is ViewState.Loading -> {
}
is ViewState.Error -> {
}
is ViewState.Status -> {
}
is ViewState.Success<*> -> {
carouselController.setData(it.data as StreamBundle)
}
}
})
epoxyRecyclerView.setController(carouselController)
VM.getStreamBundle(it)
}
}
private fun updateBetaActions(B: LayoutDetailsBetaBinding, isSubscribed: Boolean) { private fun updateBetaActions(B: LayoutDetailsBetaBinding, isSubscribed: Boolean) {
if (isSubscribed) { if (isSubscribed) {
B.btnBetaAction.text = getString(R.string.action_leave) B.btnBetaAction.text = getString(R.string.action_leave)
@@ -305,7 +362,6 @@ abstract class BaseDetailsActivity : BaseActivity() {
return null return null
} }
private fun openScreenshotActivity(app: App, position: Int) { private fun openScreenshotActivity(app: App, position: Int) {
val intent = Intent( val intent = Intent(
this, this,

View File

@@ -0,0 +1,99 @@
/*
* 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.details
import android.app.Application
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.StreamHelper
import com.aurora.store.data.RequestState
import com.aurora.store.data.ViewState
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.Log
import com.aurora.store.viewmodel.BaseAndroidViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
class DetailsClusterViewModel(application: Application) : BaseAndroidViewModel(application) {
private var authData: AuthData = AuthProvider.with(application).getAuthData()
private var appDetailsHelper = AppDetailsHelper(authData).using(HttpClient.getPreferredClient())
private var streamHelper = StreamHelper(authData)
val liveData: MutableLiveData<ViewState> = MutableLiveData()
var streamBundle: StreamBundle = StreamBundle()
lateinit var type: StreamHelper.Type
lateinit var category: StreamHelper.Category
override fun observe() {
}
fun getStreamBundle(
streamUrl: String
) {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
streamBundle = appDetailsHelper.getDetailsStream(streamUrl)
liveData.postValue(ViewState.Success(streamBundle))
requestState = RequestState.Complete
} catch (e: Exception) {
requestState = RequestState.Pending
liveData.postValue(ViewState.Error(e.message))
}
}
}
}
fun observeCluster(streamCluster: StreamCluster) {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
if (streamCluster.hasNext()) {
val newCluster =
streamHelper.getNextStreamCluster(streamCluster.clusterNextPageUrl)
updateCluster(newCluster)
liveData.postValue(ViewState.Success(streamBundle))
} else {
Log.i("End of cluster")
streamCluster.clusterNextPageUrl = String()
}
} catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message))
}
}
}
}
private fun updateCluster(newCluster: StreamCluster) {
streamBundle.streamClusters[newCluster.id]?.apply {
clusterAppList.addAll(newCluster.clusterAppList)
clusterNextPageUrl = newCluster.clusterNextPageUrl
}
}
}

View File

@@ -21,6 +21,6 @@
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="?colorAccent" /> <solid android:color="?colorAccent" />
<size <size
android:width="1dp" android:width="1.25dp"
android:height="12dp" /> android:height="12dp" />
</shape> </shape>

View File

@@ -18,6 +18,8 @@
--> -->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container" android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -88,6 +90,18 @@
android:id="@+id/layout_details_beta" android:id="@+id/layout_details_beta"
layout="@layout/layout_details_beta" /> layout="@layout/layout_details_beta" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxy_recycler_stream"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/padding_small"
android:paddingEnd="@dimen/padding_small"
app:itemSpacing="@dimen/margin_small"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_app" />
<include <include
android:id="@+id/layout_details_dev" android:id="@+id/layout_details_dev"
layout="@layout/layout_details_dev" /> layout="@layout/layout_details_dev" />

View File

@@ -33,70 +33,84 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.flexbox.FlexboxLayout <HorizontalScrollView
android:id="@+id/layout_extras" android:id="@+id/layout_extras"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/padding_small" android:padding="@dimen/padding_small"
app:alignContent="space_between" android:scrollbars="none">
app:alignItems="center"
app:dividerDrawableHorizontal="@drawable/divider"
app:dividerDrawableVertical="@drawable/divider_line"
app:flexDirection="row"
app:flexWrap="wrap"
app:showDividerHorizontal="middle"
app:showDividerVertical="middle">
<androidx.appcompat.widget.AppCompatTextView <LinearLayout
android:id="@+id/txt_rating"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small" android:divider="@drawable/divider_line"
android:layout_marginEnd="@dimen/margin_small" android:orientation="horizontal"
android:drawablePadding="@dimen/padding_small" android:showDividers="middle">
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableStartCompat="@drawable/ic_star"
app:drawableTint="?colorAccent"
tools:text="3.5" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_installs" android:id="@+id/txt_rating"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small" android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small" android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small" android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1" android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableStartCompat="@drawable/ic_download" app:drawableStartCompat="@drawable/ic_star"
app:drawableTint="?colorAccent" app:drawableTint="?colorAccent"
tools:text="2500" /> tools:text="3.5" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_size" android:id="@+id/txt_installs"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small" android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small" android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small" android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1" android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableTint="?colorAccent" app:drawableStartCompat="@drawable/ic_download"
tools:text="25 MB" /> app:drawableTint="?colorAccent"
tools:text="2500" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_sdk" android:id="@+id/txt_size"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small" android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small" android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small" android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1" android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableTint="?colorAccent" app:drawableTint="?colorAccent"
tools:text="Android 7+" /> tools:text="25 MB" />
</com.google.android.flexbox.FlexboxLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_sdk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableTint="?colorAccent"
tools:text="Android 7+" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_updated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableTint="?colorAccent"
tools:text="Jan 21 2020" />
</LinearLayout>
</HorizontalScrollView>
<LinearLayout <LinearLayout
android:id="@+id/layout_description" android:id="@+id/layout_description"