From 833bbb554fb2354df040e3fa882fd863601245d1 Mon Sep 17 00:00:00 2001 From: Rahul Kumar Patel Date: Sat, 20 Feb 2021 04:35:34 +0530 Subject: [PATCH] Improve dev profile page --- app/build.gradle | 2 +- .../controller/DeveloperCarouselController.kt | 54 +++++++ .../view/epoxy/groups/DeveloperModelGroup.kt | 127 ++++++++++++++++ .../view/ui/details/DevProfileActivity.kt | 140 +++++++----------- .../viewmodel/details/DevProfileViewModel.kt | 102 +++++++++++++ .../layout/model_developer_carousel_group.xml | 40 +++++ 6 files changed, 380 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/view/epoxy/controller/DeveloperCarouselController.kt create mode 100644 app/src/main/java/com/aurora/store/view/epoxy/groups/DeveloperModelGroup.kt create mode 100644 app/src/main/java/com/aurora/store/viewmodel/details/DevProfileViewModel.kt create mode 100644 app/src/main/res/layout/model_developer_carousel_group.xml diff --git a/app/build.gradle b/app/build.gradle index 7bf91e44b..6ff2be0b2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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() diff --git a/app/src/main/java/com/aurora/store/view/epoxy/controller/DeveloperCarouselController.kt b/app/src/main/java/com/aurora/store/view/epoxy/controller/DeveloperCarouselController.kt new file mode 100644 index 000000000..cf5b1c697 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/epoxy/controller/DeveloperCarouselController.kt @@ -0,0 +1,54 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * 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 . + * + */ + +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)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/view/epoxy/groups/DeveloperModelGroup.kt b/app/src/main/java/com/aurora/store/view/epoxy/groups/DeveloperModelGroup.kt new file mode 100644 index 000000000..2a0352981 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/epoxy/groups/DeveloperModelGroup.kt @@ -0,0 +1,127 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * 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 . + * + */ + +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> { + val models = ArrayList>() + val clusterViewModels = mutableListOf>() + val screenshotsViewModels = mutableListOf>() + + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DevProfileActivity.kt b/app/src/main/java/com/aurora/store/view/ui/details/DevProfileActivity.kt index 05e7dba22..00eec1b12 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DevProfileActivity.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DevProfileActivity.kt @@ -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>() - val screenshotsViewModels = mutableListOf>() - - 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) { + } } \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/DevProfileViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/DevProfileViewModel.kt new file mode 100644 index 000000000..98912ebf7 --- /dev/null +++ b/app/src/main/java/com/aurora/store/viewmodel/details/DevProfileViewModel.kt @@ -0,0 +1,102 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * 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 . + * + */ + +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 = 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 + } + } +} diff --git a/app/src/main/res/layout/model_developer_carousel_group.xml b/app/src/main/res/layout/model_developer_carousel_group.xml new file mode 100644 index 000000000..8804d25f5 --- /dev/null +++ b/app/src/main/res/layout/model_developer_carousel_group.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file