Improve dev profile page

This commit is contained in:
Rahul Kumar Patel
2021-02-20 04:35:34 +05:30
parent 209e3f69dd
commit 833bbb554f
6 changed files with 380 additions and 85 deletions

View File

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

View File

@@ -0,0 +1,54 @@
/*
* 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.gplayapi.data.models.StreamCluster
import com.aurora.store.view.epoxy.groups.CarouselShimmerGroup
import com.aurora.store.view.epoxy.groups.DeveloperModelGroup
open class DeveloperCarouselController(private val callbacks: Callbacks) :
GenericCarouselController(callbacks) {
override fun applyFilter(streamBundle: StreamCluster): Boolean {
return streamBundle.clusterTitle.isNotBlank() //Filter noisy cluster
&& streamBundle.clusterAppList.isNotEmpty() //Filter empty clusters
}
override fun buildModels(streamBundle: StreamBundle?) {
setFilterDuplicates(true)
if (streamBundle == null) {
for (i in 1..2) {
add(
CarouselShimmerGroup()
.id(i)
)
}
} else {
streamBundle
.streamClusters
.values
.filter { applyFilter(it) }
.forEach { streamCluster ->
add(DeveloperModelGroup(streamCluster, callbacks))
}
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.groups
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyModelGroup
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.util.Log
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.app.AppViewModel_
import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
class DeveloperModelGroup(
streamCluster: StreamCluster,
callbacks: GenericCarouselController.Callbacks
) :
EpoxyModelGroup(
R.layout.model_developer_carousel_group, buildModels(
streamCluster,
callbacks
)
) {
companion object {
private fun buildModels(
streamCluster: StreamCluster,
callbacks: GenericCarouselController.Callbacks
): List<EpoxyModel<*>> {
val models = ArrayList<EpoxyModel<*>>()
val clusterViewModels = mutableListOf<EpoxyModel<*>>()
val screenshotsViewModels = mutableListOf<EpoxyModel<*>>()
val idPrefix = streamCluster.id
models.add(
HeaderViewModel_()
.id("${idPrefix}_header")
.title(streamCluster.clusterTitle)
.browseUrl(streamCluster.clusterBrowseUrl)
.click { _ ->
callbacks.onHeaderClicked(streamCluster)
}
)
if (streamCluster.clusterAppList.size == 1) {
val app = streamCluster.clusterAppList[0]
for (artwork in app.screenshots) {
screenshotsViewModels.add(
ScreenshotViewModel_()
.id(artwork.url)
.artwork(artwork)
)
}
clusterViewModels.add(
AppListViewModel_()
.id(app.id)
.app(app)
.click { _ ->
callbacks.onAppClick(app)
}
)
} else {
for (app in streamCluster.clusterAppList) {
clusterViewModels.add(
AppViewModel_()
.id(app.id)
.app(app)
.click { _ ->
callbacks.onAppClick(app)
}
.longClick { _ ->
callbacks.onAppLongClick(app)
false
}
.onBind { _, _, position ->
val itemCount = clusterViewModels.count()
if (itemCount >= 2) {
if (position == clusterViewModels.count() - 2) {
callbacks.onClusterScrolled(streamCluster)
Log.i("Cluster %s Scrolled", streamCluster.clusterTitle)
}
}
}
)
}
}
if (screenshotsViewModels.isNotEmpty()) {
models.add(
CarouselHorizontalModel_()
.id("${idPrefix}_screenshots")
.models(screenshotsViewModels)
)
}
models.add(
CarouselHorizontalModel_()
.id("${idPrefix}_cluster")
.models(clusterViewModels)
)
return models
}
}
}

View File

@@ -21,29 +21,28 @@ package com.aurora.store.view.ui.details
import android.content.Intent
import android.os.Bundle
import com.airbnb.epoxy.EpoxyModel
import androidx.lifecycle.ViewModelProvider
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.data.models.details.DevStream
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.store.R
import com.aurora.store.data.ViewState
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.databinding.ActivityDevProfileBinding
import com.aurora.store.util.extensions.close
import com.aurora.store.util.extensions.load
import com.aurora.store.util.extensions.toast
import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.app.AppViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
import com.aurora.store.view.epoxy.controller.DeveloperCarouselController
import com.aurora.store.view.epoxy.controller.EarlyAccessCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.ui.commons.BaseActivity
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import com.aurora.store.viewmodel.details.DevProfileViewModel
class DevProfileActivity : BaseActivity() {
class DevProfileActivity : BaseActivity(), GenericCarouselController.Callbacks {
private lateinit var B: ActivityDevProfileBinding
private lateinit var VM: DevProfileViewModel
private lateinit var C: DeveloperCarouselController
private lateinit var authData: AuthData
override fun onConnected() {
@@ -60,11 +59,35 @@ class DevProfileActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
B = ActivityDevProfileBinding.inflate(layoutInflater)
C = DeveloperCarouselController(this)
VM = ViewModelProvider(this).get(DevProfileViewModel::class.java)
authData = AuthProvider.with(this).getAuthData()
setContentView(B.root)
attachToolbar()
attachRecycler()
VM.liveData.observe(this, {
when (it) {
is ViewState.Empty -> {
}
is ViewState.Loading -> {
}
is ViewState.Error -> {
}
is ViewState.Status -> {
}
is ViewState.Success<*> -> {
updateInfo(it.data as DevStream)
updateController(it.data as DevStream)
}
}
})
B.viewFlipper.displayedChild = 1
@@ -79,7 +102,7 @@ class DevProfileActivity : BaseActivity() {
if (devId.isNullOrEmpty()) {
close()
} else {
fetchDevProfile(devId)
VM.getStreamBundle(devId)
}
}
} else {
@@ -94,86 +117,35 @@ class DevProfileActivity : BaseActivity() {
B.layoutToolbarAction.txtTitle.text = getString(R.string.details_dev_profile)
}
private fun attachRecycler() {
B.recycler.setController(C)
}
private fun updateInfo(devStream: DevStream) {
B.layoutToolbarAction.txtTitle.text = devStream.title
B.txtDevName.text = devStream.title
B.txtDevDescription.text = devStream.description
B.imgIcon.load(devStream.imgUrl)
B.viewFlipper.displayedChild = 0
}
private fun updateController(devStream: DevStream) {
B.recycler
.withModels {
setFilterDuplicates(true)
for (entry in devStream.appListMap) {
val clusterViewModels = mutableListOf<EpoxyModel<*>>()
val screenshotsViewModels = mutableListOf<EpoxyModel<*>>()
if (entry.value.size == 1) {
val app = entry.value[0]
for (artwork in app.screenshots) {
screenshotsViewModels.add(
ScreenshotViewModel_()
.id(artwork.url)
.artwork(artwork)
)
}
clusterViewModels.add(
AppListViewModel_()
.id(app.id)
.app(app)
.click { _ ->
openDetailsActivity(app)
}
)
} else {
for (app in entry.value) {
clusterViewModels.add(
AppViewModel_()
.id(app.id)
.app(app)
.click { _ ->
openDetailsActivity(app)
}
)
}
}
add(
HeaderViewModel_()
.id(entry.key)
.title(entry.key)
)
if (screenshotsViewModels.isNotEmpty()) {
add(
CarouselHorizontalModel_()
.id("${entry.key}_screenshots")
.models(screenshotsViewModels)
)
}
add(
CarouselHorizontalModel_()
.id("${entry.key}_cluster")
.models(clusterViewModels)
)
}
}
C.setData(devStream.streamBundle)
}
private fun fetchDevProfile(devId: String) {
task {
AppDetailsHelper(authData).getDeveloperStream(devId)
} successUi {
B.viewFlipper.displayedChild = 0
updateInfo(it)
updateController(it)
} failUi {
toast("Dev profile unavailable")
close()
}
override fun onHeaderClicked(streamCluster: StreamCluster) {
openStreamBrowseActivity(streamCluster.clusterBrowseUrl)
}
override fun onClusterScrolled(streamCluster: StreamCluster) {
VM.observeCluster(streamCluster)
}
override fun onAppClick(app: App) {
openDetailsActivity(app)
}
override fun onAppLongClick(app: App) {
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.data.models.details.DevStream
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 DevProfileViewModel(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 devStream:DevStream = DevStream()
var streamBundle: StreamBundle = StreamBundle()
lateinit var type: StreamHelper.Type
lateinit var category: StreamHelper.Category
override fun observe() {
}
fun getStreamBundle(
devId: String
) {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
devStream = appDetailsHelper.getDeveloperStream(devId)
streamBundle = devStream.streamBundle
liveData.postValue(ViewState.Success(devStream))
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)
devStream.streamBundle = streamBundle
liveData.postValue(ViewState.Success(devStream))
} 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

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/header_view" />
<ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/recycler_view_01" />
<ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/recycler_view_02" />
</LinearLayout>