diff --git a/app/build.gradle b/app/build.gradle index 49662afdf..7bf91e44b 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:9d327499ae") + api("com.gitlab.AuroraOSS:gplayapi:101c69ecde") } Properties props = new Properties() diff --git a/app/src/main/java/com/aurora/store/view/epoxy/controller/DetailsCarouselController.kt b/app/src/main/java/com/aurora/store/view/epoxy/controller/DetailsCarouselController.kt new file mode 100644 index 000000000..a7ba410cf --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/epoxy/controller/DetailsCarouselController.kt @@ -0,0 +1,70 @@ +/* + * 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.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)) + } + + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt index 4c6a871a4..39591de8b 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt @@ -324,6 +324,8 @@ class AppDetailsActivity : BaseDetailsActivity() { inflateBetaSubscription(B.layoutDetailsBeta, app) } + + inflateAppStream(B.epoxyRecyclerStream, app) } } diff --git a/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt b/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt index 2e06ef2ce..ae385c979 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt @@ -29,12 +29,14 @@ import android.widget.RelativeLayout import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat +import androidx.lifecycle.ViewModelProvider +import com.airbnb.epoxy.EpoxyRecyclerView import com.aurora.Constants -import com.aurora.gplayapi.data.models.App -import com.aurora.gplayapi.data.models.Review +import com.aurora.gplayapi.data.models.* import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.store.R +import com.aurora.store.data.ViewState import com.aurora.store.data.model.ExodusReport import com.aurora.store.data.model.Report 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.toast 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.details.ReviewViewModel_ import com.aurora.store.view.epoxy.views.details.ScreenshotView import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_ import com.aurora.store.view.ui.commons.BaseActivity +import com.aurora.store.viewmodel.details.DetailsClusterViewModel import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi @@ -69,7 +74,8 @@ abstract class BaseDetailsActivity : BaseActivity() { B.txtInstalls.text = CommonUtil.addDiPrefix(app.installs) B.txtSize.text = CommonUtil.addSiPrefix(app.size) 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) { 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) { if (isSubscribed) { B.btnBetaAction.text = getString(R.string.action_leave) @@ -305,7 +362,6 @@ abstract class BaseDetailsActivity : BaseActivity() { return null } - private fun openScreenshotActivity(app: App, position: Int) { val intent = Intent( this, diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/DetailsClusterViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/DetailsClusterViewModel.kt new file mode 100644 index 000000000..3684208ce --- /dev/null +++ b/app/src/main/java/com/aurora/store/viewmodel/details/DetailsClusterViewModel.kt @@ -0,0 +1,99 @@ +/* + * 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.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 = 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 + } + } +} diff --git a/app/src/main/res/drawable-v21/divider_line.xml b/app/src/main/res/drawable-v21/divider_line.xml index 1211d8fba..fe70d55df 100644 --- a/app/src/main/res/drawable-v21/divider_line.xml +++ b/app/src/main/res/drawable-v21/divider_line.xml @@ -21,6 +21,6 @@ android:shape="rectangle"> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml index dc8f1c160..c223b1e03 100644 --- a/app/src/main/res/layout/activity_details.xml +++ b/app/src/main/res/layout/activity_details.xml @@ -18,6 +18,8 @@ --> + + diff --git a/app/src/main/res/layout/layout_details_description.xml b/app/src/main/res/layout/layout_details_description.xml index 0e7d783ba..d158e00f0 100644 --- a/app/src/main/res/layout/layout_details_description.xml +++ b/app/src/main/res/layout/layout_details_description.xml @@ -33,70 +33,84 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + android:scrollbars="none"> - + android:divider="@drawable/divider_line" + android:orientation="horizontal" + android:showDividers="middle"> - + - + - - + + + + + + + +