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}" implementation "com.github.topjohnwu.libsu:core:${versions.libsu}"
//Love <3 //Love <3
api("com.gitlab.AuroraOSS:gplayapi:101c69ecde") api("com.gitlab.AuroraOSS:gplayapi:-SNAPSHOT")
} }
Properties props = new Properties() 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.content.Intent
import android.os.Bundle 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.AuthData
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.data.models.details.DevStream import com.aurora.gplayapi.data.models.details.DevStream
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.ViewState
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.databinding.ActivityDevProfileBinding import com.aurora.store.databinding.ActivityDevProfileBinding
import com.aurora.store.util.extensions.close import com.aurora.store.util.extensions.close
import com.aurora.store.util.extensions.load import com.aurora.store.util.extensions.load
import com.aurora.store.util.extensions.toast import com.aurora.store.view.epoxy.controller.DeveloperCarouselController
import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_ import com.aurora.store.view.epoxy.controller.EarlyAccessCarouselController
import com.aurora.store.view.epoxy.views.app.AppListViewModel_ import com.aurora.store.view.epoxy.controller.GenericCarouselController
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.ui.commons.BaseActivity import com.aurora.store.view.ui.commons.BaseActivity
import nl.komponents.kovenant.task import com.aurora.store.viewmodel.details.DevProfileViewModel
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
class DevProfileActivity : BaseActivity() { class DevProfileActivity : BaseActivity(), GenericCarouselController.Callbacks {
private lateinit var B: ActivityDevProfileBinding private lateinit var B: ActivityDevProfileBinding
private lateinit var VM: DevProfileViewModel
private lateinit var C: DeveloperCarouselController
private lateinit var authData: AuthData private lateinit var authData: AuthData
override fun onConnected() { override fun onConnected() {
@@ -60,11 +59,35 @@ class DevProfileActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
B = ActivityDevProfileBinding.inflate(layoutInflater) B = ActivityDevProfileBinding.inflate(layoutInflater)
C = DeveloperCarouselController(this)
VM = ViewModelProvider(this).get(DevProfileViewModel::class.java)
authData = AuthProvider.with(this).getAuthData() authData = AuthProvider.with(this).getAuthData()
setContentView(B.root) setContentView(B.root)
attachToolbar() 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 B.viewFlipper.displayedChild = 1
@@ -79,7 +102,7 @@ class DevProfileActivity : BaseActivity() {
if (devId.isNullOrEmpty()) { if (devId.isNullOrEmpty()) {
close() close()
} else { } else {
fetchDevProfile(devId) VM.getStreamBundle(devId)
} }
} }
} else { } else {
@@ -94,86 +117,35 @@ class DevProfileActivity : BaseActivity() {
B.layoutToolbarAction.txtTitle.text = getString(R.string.details_dev_profile) B.layoutToolbarAction.txtTitle.text = getString(R.string.details_dev_profile)
} }
private fun attachRecycler() {
B.recycler.setController(C)
}
private fun updateInfo(devStream: DevStream) { private fun updateInfo(devStream: DevStream) {
B.layoutToolbarAction.txtTitle.text = devStream.title B.layoutToolbarAction.txtTitle.text = devStream.title
B.txtDevName.text = devStream.title B.txtDevName.text = devStream.title
B.txtDevDescription.text = devStream.description B.txtDevDescription.text = devStream.description
B.imgIcon.load(devStream.imgUrl) B.imgIcon.load(devStream.imgUrl)
B.viewFlipper.displayedChild = 0
} }
private fun updateController(devStream: DevStream) { private fun updateController(devStream: DevStream) {
B.recycler C.setData(devStream.streamBundle)
.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)
)
}
}
} }
private fun fetchDevProfile(devId: String) { override fun onHeaderClicked(streamCluster: StreamCluster) {
task { openStreamBrowseActivity(streamCluster.clusterBrowseUrl)
AppDetailsHelper(authData).getDeveloperStream(devId) }
} successUi {
B.viewFlipper.displayedChild = 0 override fun onClusterScrolled(streamCluster: StreamCluster) {
updateInfo(it) VM.observeCluster(streamCluster)
updateController(it) }
} failUi {
toast("Dev profile unavailable") override fun onAppClick(app: App) {
close() 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>