mirror of https://github.com/tasks/tasks
New purchase activity
parent
38e7c810ea
commit
c1db57d1e3
@ -0,0 +1,112 @@
|
|||||||
|
package org.tasks.billing
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.google.android.material.composethemeadapter.MdcTheme
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.tasks.LocalBroadcastManager
|
||||||
|
import org.tasks.Tasks.Companion.IS_GENERIC
|
||||||
|
import org.tasks.compose.PurchaseText.PurchaseText
|
||||||
|
import org.tasks.injection.InjectingAppCompatActivity
|
||||||
|
import org.tasks.themes.Theme
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class PurchaseActivity : InjectingAppCompatActivity(), OnPurchasesUpdated {
|
||||||
|
@Inject lateinit var theme: Theme
|
||||||
|
@Inject lateinit var billingClient: BillingClient
|
||||||
|
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
|
||||||
|
@Inject lateinit var inventory: Inventory
|
||||||
|
|
||||||
|
private var currentSubscription: Purchase? = null
|
||||||
|
private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val nameYourPrice = mutableStateOf(false)
|
||||||
|
private val sliderPosition = mutableStateOf(-1f)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val github = intent?.extras?.getBoolean(EXTRA_GITHUB) ?: false
|
||||||
|
|
||||||
|
theme.applyToContext(this)
|
||||||
|
|
||||||
|
savedInstanceState?.let {
|
||||||
|
nameYourPrice.value = it.getBoolean(EXTRA_NAME_YOUR_PRICE)
|
||||||
|
sliderPosition.value = it.getFloat(EXTRA_PRICE)
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
MdcTheme {
|
||||||
|
Dialog(onDismissRequest = { finish() }) {
|
||||||
|
PurchaseText(
|
||||||
|
nameYourPrice,
|
||||||
|
sliderPosition,
|
||||||
|
github,
|
||||||
|
IS_GENERIC,
|
||||||
|
this::purchase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
|
||||||
|
localBroadcastManager.registerPurchaseReceiver(purchaseReceiver)
|
||||||
|
billingClient.queryPurchases()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
localBroadcastManager.unregisterReceiver(purchaseReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setup() {
|
||||||
|
currentSubscription = inventory.subscription.value
|
||||||
|
if (sliderPosition.value < 0) {
|
||||||
|
sliderPosition.value =
|
||||||
|
currentSubscription
|
||||||
|
?.subscriptionPrice
|
||||||
|
?.coerceAtMost(25)
|
||||||
|
?.toFloat() ?: 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun purchase(price: Int, monthly: Boolean) {
|
||||||
|
val newSku = String.format("%s_%02d", if (monthly) "monthly" else "annual", price)
|
||||||
|
billingClient.initiatePurchaseFlow(
|
||||||
|
this,
|
||||||
|
newSku,
|
||||||
|
BillingClientImpl.TYPE_SUBS,
|
||||||
|
currentSubscription?.sku?.takeIf { it != newSku })
|
||||||
|
billingClient.addPurchaseCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPurchasesUpdated(success: Boolean) {
|
||||||
|
if (success) {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putFloat(EXTRA_PRICE, sliderPosition.value)
|
||||||
|
outState.putBoolean(EXTRA_NAME_YOUR_PRICE, nameYourPrice.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_GITHUB = "extra_github"
|
||||||
|
private const val EXTRA_PRICE = "extra_price"
|
||||||
|
private const val EXTRA_NAME_YOUR_PRICE = "extra_name_your_price"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,375 +0,0 @@
|
|||||||
package org.tasks.billing
|
|
||||||
|
|
||||||
import android.app.Activity.RESULT_OK
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.text.style.TextAppearanceSpan
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import butterknife.ButterKnife
|
|
||||||
import butterknife.OnClick
|
|
||||||
import com.google.android.material.slider.Slider
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin
|
|
||||||
import io.noties.markwon.Markwon
|
|
||||||
import io.noties.markwon.MarkwonSpansFactory
|
|
||||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
|
|
||||||
import org.commonmark.ext.gfm.strikethrough.Strikethrough
|
|
||||||
import org.tasks.LocalBroadcastManager
|
|
||||||
import org.tasks.R
|
|
||||||
import org.tasks.Tasks.Companion.IS_GENERIC
|
|
||||||
import org.tasks.Tasks.Companion.IS_GOOGLE_PLAY
|
|
||||||
import org.tasks.analytics.Firebase
|
|
||||||
import org.tasks.databinding.ActivityPurchaseBinding
|
|
||||||
import org.tasks.dialogs.DialogBuilder
|
|
||||||
import org.tasks.locale.Locale
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class PurchaseDialog : DialogFragment(), OnPurchasesUpdated {
|
|
||||||
|
|
||||||
interface PurchaseHandler {
|
|
||||||
fun onPurchaseDialogDismissed()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
setup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject lateinit var inventory: Inventory
|
|
||||||
@Inject lateinit var dialogBuilder: DialogBuilder
|
|
||||||
@Inject lateinit var billingClient: BillingClient
|
|
||||||
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
|
|
||||||
@Inject lateinit var locale: Locale
|
|
||||||
@Inject lateinit var firebase: Firebase
|
|
||||||
|
|
||||||
private lateinit var binding: ActivityPurchaseBinding
|
|
||||||
private lateinit var markwon: Markwon
|
|
||||||
|
|
||||||
private var currentSubscription: Purchase? = null
|
|
||||||
private var priceChanged = false
|
|
||||||
private var nameYourPrice = false
|
|
||||||
|
|
||||||
private val hasTasksSubscription
|
|
||||||
get() = inventory.subscription.value?.isTasksSubscription == true
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
binding = ActivityPurchaseBinding.inflate(layoutInflater)
|
|
||||||
ButterKnife.bind(this, binding.root)
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
nameYourPrice = !isTasksPayment && !hasTasksSubscription
|
|
||||||
} else {
|
|
||||||
binding.slider.value = savedInstanceState.getFloat(EXTRA_PRICE)
|
|
||||||
priceChanged = savedInstanceState.getBoolean(EXTRA_PRICE_CHANGED)
|
|
||||||
nameYourPrice = savedInstanceState.getBoolean(EXTRA_NAME_YOUR_PRICE)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.slider.addOnChangeListener(this::onPriceChanged)
|
|
||||||
binding.slider.setLabelFormatter { "$${it - .01}" }
|
|
||||||
binding.text.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
|
|
||||||
markwon = Markwon.builder(requireContext())
|
|
||||||
.usePlugin(StrikethroughPlugin.create())
|
|
||||||
.usePlugin(object : AbstractMarkwonPlugin() {
|
|
||||||
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
|
|
||||||
builder.appendFactory(Strikethrough::class.java) { _, _ ->
|
|
||||||
TextAppearanceSpan(requireContext(), R.style.RedText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
|
|
||||||
setWaitScreen(!IS_GENERIC)
|
|
||||||
|
|
||||||
return if (IS_GENERIC) {
|
|
||||||
if (isGitHub) {
|
|
||||||
getPurchaseDialog()
|
|
||||||
} else {
|
|
||||||
getMessageDialog(R.string.no_google_play_subscription)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isGitHub) {
|
|
||||||
getMessageDialog(R.string.insufficient_sponsorship)
|
|
||||||
} else {
|
|
||||||
getPurchaseDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPurchaseDialog(): AlertDialog =
|
|
||||||
dialogBuilder.newDialog().setView(binding.root).show()
|
|
||||||
|
|
||||||
private fun getMessageDialog(res: Int): AlertDialog =
|
|
||||||
dialogBuilder.newDialog()
|
|
||||||
.setMessage(res)
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.setNeutralButton(R.string.help) { _, _ ->
|
|
||||||
val url = Uri.parse(getString(R.string.subscription_help_url))
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, url))
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
|
|
||||||
private fun updateText() {
|
|
||||||
var benefits = "### ${getString(when {
|
|
||||||
nameYourPrice -> R.string.name_your_price
|
|
||||||
!inventory.hasPro -> R.string.upgrade_to_pro
|
|
||||||
!hasTasksSubscription -> R.string.button_upgrade
|
|
||||||
else -> R.string.manage_subscription
|
|
||||||
})}"
|
|
||||||
benefits += if (nameYourPrice) {
|
|
||||||
"""
|
|
||||||
---
|
|
||||||
#### ~~${getString(R.string.tasks_org_account)}~~
|
|
||||||
|
|
||||||
_${getString(R.string.account_not_included)}_
|
|
||||||
"""
|
|
||||||
} else {
|
|
||||||
"""
|
|
||||||
---
|
|
||||||
#### ${getString(R.string.tasks_org_account)}
|
|
||||||
* ${getString(R.string.tasks_org_description)}
|
|
||||||
* [${getString(R.string.tasks_org_share)}](${getString(R.string.url_sharing)})
|
|
||||||
* [${getString(R.string.upgrade_third_party_apps)}](${getString(R.string.url_app_passwords)})
|
|
||||||
* ${getString(R.string.upgrade_google_places)}
|
|
||||||
* [${getString(R.string.upgrade_coming_soon)}](${getString(R.string.help_url_sync)})
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
if (IS_GOOGLE_PLAY) {
|
|
||||||
benefits += """
|
|
||||||
---
|
|
||||||
#### ${getString(R.string.upgrade_sync_self_hosted)}
|
|
||||||
* [${getString(R.string.davx5)}](${getString(R.string.url_davx5)})
|
|
||||||
* [${getString(R.string.caldav)}](${getString(R.string.url_caldav)})
|
|
||||||
* [${getString(R.string.etesync)}](${getString(R.string.url_etesync)})
|
|
||||||
* [${getString(R.string.decsync)}](${getString(R.string.url_decsync)})
|
|
||||||
* ${getString(R.string.upgrade_google_tasks)}
|
|
||||||
---
|
|
||||||
#### ${getString(R.string.upgrade_additional_features)}
|
|
||||||
* ${getString(R.string.upgrade_themes)}
|
|
||||||
* [${getString(R.string.upgrade_tasker)}](${getString(R.string.url_tasker)})
|
|
||||||
---
|
|
||||||
* ${getString(R.string.upgrade_free_trial)}
|
|
||||||
* ${getString(R.string.upgrade_downgrade)}
|
|
||||||
* ${getString(R.string.upgrade_support_development)}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
binding.text.text = markwon.toMarkdown(benefits)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.pay_annually)
|
|
||||||
fun subscribeAnnually() {
|
|
||||||
initiatePurchase(false, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.pay_monthly)
|
|
||||||
fun subscribeMonthly() {
|
|
||||||
initiatePurchase(true, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.sponsor)
|
|
||||||
fun sponsor() {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_sponsor))))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initiatePurchase(isMonthly: Boolean, price: Int) {
|
|
||||||
val newSku = String.format("%s_%02d", if (isMonthly) "monthly" else "annual", price)
|
|
||||||
billingClient.initiatePurchaseFlow(
|
|
||||||
requireActivity(),
|
|
||||||
newSku,
|
|
||||||
BillingClientImpl.TYPE_SUBS,
|
|
||||||
currentSubscription?.sku?.takeIf { it != newSku })
|
|
||||||
billingClient.addPurchaseCallback(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.pay_other)
|
|
||||||
fun nameYourPrice() {
|
|
||||||
if (isTasksPayment) {
|
|
||||||
dismiss()
|
|
||||||
} else {
|
|
||||||
nameYourPrice = !nameYourPrice
|
|
||||||
setWaitScreen(false)
|
|
||||||
binding.scroll.scrollTo(0, 0)
|
|
||||||
updateSubscribeButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putFloat(EXTRA_PRICE, binding.slider.value)
|
|
||||||
outState.putBoolean(EXTRA_PRICE_CHANGED, priceChanged)
|
|
||||||
outState.putBoolean(EXTRA_NAME_YOUR_PRICE, nameYourPrice)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setWaitScreen(isWaitScreen: Boolean) {
|
|
||||||
Timber.d("setWaitScreen(%s)", isWaitScreen)
|
|
||||||
binding.sliderContainer.isVisible = !isWaitScreen && nameYourPrice
|
|
||||||
binding.payOther.isVisible = !isWaitScreen
|
|
||||||
binding.payOther.setText(when {
|
|
||||||
isTasksPayment -> R.string.cancel
|
|
||||||
nameYourPrice -> R.string.get_tasks_org_account
|
|
||||||
else -> R.string.name_your_price
|
|
||||||
})
|
|
||||||
binding.tasksOrgButtonPanel.isVisible = !isWaitScreen && !IS_GENERIC
|
|
||||||
binding.screenWait.isVisible = isWaitScreen && !IS_GENERIC
|
|
||||||
binding.sponsor.isVisible = IS_GENERIC
|
|
||||||
updateText()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
localBroadcastManager.registerPurchaseReceiver(purchaseReceiver)
|
|
||||||
billingClient.queryPurchases()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
localBroadcastManager.unregisterReceiver(purchaseReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setup() {
|
|
||||||
currentSubscription = inventory.subscription.value
|
|
||||||
if (!priceChanged) {
|
|
||||||
binding.slider.value =
|
|
||||||
currentSubscription
|
|
||||||
?.subscriptionPrice
|
|
||||||
?.coerceAtMost(25)
|
|
||||||
?.toFloat() ?: 10f
|
|
||||||
}
|
|
||||||
updateSubscribeButton()
|
|
||||||
setWaitScreen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSubscribeButton() {
|
|
||||||
val sliderValue = binding.slider.value.toInt()
|
|
||||||
val annualPrice = if (nameYourPrice) sliderValue else 30
|
|
||||||
val monthlyPrice = if (nameYourPrice) sliderValue else 3
|
|
||||||
val constrained = resources.getBoolean(R.bool.width_constrained)
|
|
||||||
val aboveAverage = "${getString(R.string.above_average)} $POPPER"
|
|
||||||
binding.avgAnnual.text = when {
|
|
||||||
!nameYourPrice -> "${getString(
|
|
||||||
R.string.save_percent,
|
|
||||||
((1 - (annualPrice / (12.0 * monthlyPrice))) * 100).toInt()
|
|
||||||
)} $POPPER"
|
|
||||||
sliderValue < firebase.averageSubscription() -> "" //getString(R.string.below_average)
|
|
||||||
else -> aboveAverage
|
|
||||||
}
|
|
||||||
binding.avgAnnual.setTextColor(
|
|
||||||
if (nameYourPrice && sliderValue < firebase.averageSubscription()) {
|
|
||||||
ContextCompat.getColor(requireContext(), R.color.text_secondary)
|
|
||||||
} else {
|
|
||||||
ContextCompat.getColor(requireContext(), R.color.purchase_highlight)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
binding.avgMonthly.setTextColor(
|
|
||||||
ContextCompat.getColor(requireContext(), R.color.purchase_highlight)
|
|
||||||
)
|
|
||||||
binding.avgMonthly.text = aboveAverage
|
|
||||||
with(binding.payAnnually) {
|
|
||||||
isEnabled = true
|
|
||||||
text = getString(
|
|
||||||
if (constrained) R.string.price_per_year_abbreviated else R.string.price_per_year,
|
|
||||||
annualPrice - .01
|
|
||||||
)
|
|
||||||
setOnClickListener {
|
|
||||||
initiatePurchase(false, if (nameYourPrice) sliderValue else 30)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
with(binding.payMonthly) {
|
|
||||||
isEnabled = true
|
|
||||||
text = getString(
|
|
||||||
if (constrained) R.string.price_per_month_abbreviated else R.string.price_per_month,
|
|
||||||
monthlyPrice - .01
|
|
||||||
)
|
|
||||||
setOnClickListener {
|
|
||||||
initiatePurchase(true, if (nameYourPrice) sliderValue else 3)
|
|
||||||
}
|
|
||||||
isVisible = !nameYourPrice || sliderValue < 3
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.avgMonthly.isVisible = nameYourPrice && binding.payMonthly.isVisible
|
|
||||||
currentSubscription?.let {
|
|
||||||
binding.payMonthly.isEnabled =
|
|
||||||
it.isCanceled || !it.isMonthly || monthlyPrice != it.subscriptionPrice
|
|
||||||
binding.payAnnually.isEnabled =
|
|
||||||
it.isCanceled || it.isMonthly || annualPrice != it.subscriptionPrice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onPriceChanged(slider: Slider, value: Float, fromUser: Boolean) {
|
|
||||||
if (fromUser) {
|
|
||||||
priceChanged = true
|
|
||||||
}
|
|
||||||
updateSubscribeButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPurchasesUpdated(success: Boolean) {
|
|
||||||
if (success) {
|
|
||||||
dismiss()
|
|
||||||
targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
|
|
||||||
activity.takeIf { it is PurchaseHandler }?.let {
|
|
||||||
(it as PurchaseHandler).onPurchaseDialogDismissed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val isTasksPayment: Boolean
|
|
||||||
get() = arguments?.getBoolean(EXTRA_TASKS_PAYMENT, false) ?: false
|
|
||||||
|
|
||||||
private val isGitHub: Boolean
|
|
||||||
get() = arguments?.getBoolean(EXTRA_GITHUB, false) ?: false
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val POPPER = "\uD83C\uDF89"
|
|
||||||
private const val EXTRA_PRICE = "extra_price"
|
|
||||||
private const val EXTRA_PRICE_CHANGED = "extra_price_changed"
|
|
||||||
private const val EXTRA_NAME_YOUR_PRICE = "extra_name_your_price"
|
|
||||||
private const val EXTRA_TASKS_PAYMENT = "extra_tasks_payment"
|
|
||||||
private const val EXTRA_GITHUB = "extra_github"
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
val FRAG_TAG_PURCHASE_DIALOG = "frag_tag_purchase_dialog"
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@JvmOverloads
|
|
||||||
fun newPurchaseDialog(
|
|
||||||
tasksPayment: Boolean = false,
|
|
||||||
github: Boolean = IS_GENERIC
|
|
||||||
): PurchaseDialog {
|
|
||||||
val dialog = PurchaseDialog()
|
|
||||||
val args = Bundle()
|
|
||||||
args.putBoolean(EXTRA_TASKS_PAYMENT, tasksPayment)
|
|
||||||
args.putBoolean(EXTRA_GITHUB, github)
|
|
||||||
dialog.arguments = args
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
fun newPurchaseDialog(
|
|
||||||
target: Fragment,
|
|
||||||
rc: Int,
|
|
||||||
tasksPayment: Boolean = false
|
|
||||||
): PurchaseDialog {
|
|
||||||
val dialog = newPurchaseDialog(tasksPayment)
|
|
||||||
dialog.setTargetFragment(target, rc)
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.tasks.compose
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.structuralEqualityPolicy
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.ParentDataModifier
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a modified version of:
|
||||||
|
* https://gist.github.com/adamp/07d468f4bcfe632670f305ce3734f511
|
||||||
|
*/
|
||||||
|
|
||||||
|
class PagerState(
|
||||||
|
currentPage: Int = 0,
|
||||||
|
minPage: Int = 0,
|
||||||
|
maxPage: Int = 0
|
||||||
|
) {
|
||||||
|
private var _minPage by mutableStateOf(minPage)
|
||||||
|
var minPage: Int
|
||||||
|
get() = _minPage
|
||||||
|
set(value) {
|
||||||
|
_minPage = value.coerceAtMost(_maxPage)
|
||||||
|
_currentPage = _currentPage.coerceIn(_minPage, _maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _maxPage by mutableStateOf(maxPage, structuralEqualityPolicy())
|
||||||
|
var maxPage: Int
|
||||||
|
get() = _maxPage
|
||||||
|
set(value) {
|
||||||
|
_maxPage = value.coerceAtLeast(_minPage)
|
||||||
|
_currentPage = _currentPage.coerceIn(_minPage, maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _currentPage by mutableStateOf(currentPage.coerceIn(minPage, maxPage))
|
||||||
|
var currentPage: Int
|
||||||
|
get() = _currentPage
|
||||||
|
set(value) {
|
||||||
|
_currentPage = value.coerceIn(minPage, maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SelectionState { Selected, Undecided }
|
||||||
|
|
||||||
|
var selectionState by mutableStateOf(SelectionState.Selected)
|
||||||
|
|
||||||
|
suspend inline fun <R> selectPage(block: PagerState.() -> R): R = try {
|
||||||
|
selectionState = SelectionState.Undecided
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
selectPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun selectPage() {
|
||||||
|
currentPage -= currentPageOffset.roundToInt()
|
||||||
|
snapToOffset(0f)
|
||||||
|
selectionState = SelectionState.Selected
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _currentPageOffset = Animatable(0f).apply {
|
||||||
|
updateBounds(-1f, 1f)
|
||||||
|
}
|
||||||
|
val currentPageOffset: Float
|
||||||
|
get() = _currentPageOffset.value
|
||||||
|
|
||||||
|
suspend fun snapToOffset(offset: Float) {
|
||||||
|
val max = if (currentPage == minPage) 0f else 1f
|
||||||
|
val min = if (currentPage == maxPage) 0f else -1f
|
||||||
|
_currentPageOffset.snapTo(offset.coerceIn(min, max))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fling(velocity: Float) {
|
||||||
|
if (velocity < 0 && currentPage == maxPage) return
|
||||||
|
if (velocity > 0 && currentPage == minPage) return
|
||||||
|
|
||||||
|
_currentPageOffset.animateTo(currentPageOffset.roundToInt().toFloat())
|
||||||
|
selectPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "PagerState{minPage=$minPage, maxPage=$maxPage, " +
|
||||||
|
"currentPage=$currentPage, currentPageOffset=$currentPageOffset}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
private data class PageData(val page: Int) : ParentDataModifier {
|
||||||
|
override fun Density.modifyParentData(parentData: Any?): Any = this@PageData
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Measurable.page: Int
|
||||||
|
get() = (parentData as? PageData)?.page ?: error("no PageData for measurable $this")
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Pager(
|
||||||
|
state: PagerState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
offscreenLimit: Int = 2,
|
||||||
|
pageContent: @Composable PagerScope.() -> Unit
|
||||||
|
) {
|
||||||
|
var pageSize by remember { mutableStateOf(0) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
Layout(
|
||||||
|
content = {
|
||||||
|
val minPage = (state.currentPage - offscreenLimit).coerceAtLeast(state.minPage)
|
||||||
|
val maxPage = (state.currentPage + offscreenLimit).coerceAtMost(state.maxPage)
|
||||||
|
|
||||||
|
for (page in minPage..maxPage) {
|
||||||
|
val pageData = PageData(page)
|
||||||
|
val scope = PagerScope(state, page)
|
||||||
|
key(pageData) {
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = pageData) {
|
||||||
|
scope.pageContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.draggable(
|
||||||
|
orientation = Orientation.Horizontal,
|
||||||
|
onDragStarted = {
|
||||||
|
state.selectionState = PagerState.SelectionState.Undecided
|
||||||
|
},
|
||||||
|
onDragStopped = { velocity ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
// Velocity is in pixels per second, but we deal in percentage offsets, so we
|
||||||
|
// need to scale the velocity to match
|
||||||
|
state.fling(velocity / pageSize)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state = rememberDraggableState { dy ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
with(state) {
|
||||||
|
val pos = pageSize * currentPageOffset
|
||||||
|
val max = if (currentPage == minPage) 0 else pageSize * offscreenLimit
|
||||||
|
val min = if (currentPage == maxPage) 0 else -pageSize * offscreenLimit
|
||||||
|
val newPos = (pos + dy).coerceIn(min.toFloat(), max.toFloat())
|
||||||
|
snapToOffset(newPos / pageSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
) { measurables, constraints ->
|
||||||
|
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||||
|
val currentPage = state.currentPage
|
||||||
|
val offset = state.currentPageOffset
|
||||||
|
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
|
|
||||||
|
measurables
|
||||||
|
.map {
|
||||||
|
it.measure(childConstraints) to it.page
|
||||||
|
}
|
||||||
|
.forEach { (placeable, page) ->
|
||||||
|
// TODO: current this centers each page. We should investigate reading
|
||||||
|
// gravity modifiers on the child, or maybe as a param to Pager.
|
||||||
|
val xCenterOffset = (constraints.maxWidth - placeable.width) / 2
|
||||||
|
val yCenterOffset = (constraints.maxHeight - placeable.height) / 2
|
||||||
|
|
||||||
|
if (currentPage == page) {
|
||||||
|
pageSize = placeable.width
|
||||||
|
}
|
||||||
|
|
||||||
|
val xItemOffset = ((page + offset - currentPage) * placeable.width).roundToInt()
|
||||||
|
|
||||||
|
placeable.place(
|
||||||
|
x = xCenterOffset + xItemOffset,
|
||||||
|
y = yCenterOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for [Pager] content.
|
||||||
|
*/
|
||||||
|
class PagerScope(
|
||||||
|
private val state: PagerState,
|
||||||
|
val page: Int
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Returns the current selected page
|
||||||
|
*/
|
||||||
|
val currentPage: Int
|
||||||
|
get() = state.currentPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current selected page offset
|
||||||
|
*/
|
||||||
|
val currentPageOffset: Float
|
||||||
|
get() = state.currentPageOffset
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current selection state
|
||||||
|
*/
|
||||||
|
val selectionState: PagerState.SelectionState
|
||||||
|
get() = state.selectionState
|
||||||
|
}
|
||||||
@ -0,0 +1,419 @@
|
|||||||
|
package org.tasks.compose
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedButton
|
||||||
|
import androidx.compose.material.Slider
|
||||||
|
import androidx.compose.material.SliderDefaults
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.Tasks.Companion.IS_GENERIC
|
||||||
|
import org.tasks.compose.Constants.HALF_KEYLINE
|
||||||
|
import org.tasks.compose.Constants.KEYLINE_FIRST
|
||||||
|
import org.tasks.compose.PurchaseText.PurchaseText
|
||||||
|
|
||||||
|
@Preview(showBackground = true, backgroundColor = 0xFFFFFF)
|
||||||
|
@Composable
|
||||||
|
private fun LightPreview() {
|
||||||
|
PurchaseText { _, _ -> }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, backgroundColor = 0x202124)
|
||||||
|
@Composable
|
||||||
|
private fun DarkPreview() {
|
||||||
|
PurchaseText { _, _ -> }
|
||||||
|
}
|
||||||
|
|
||||||
|
object PurchaseText {
|
||||||
|
private const val POPPER = "\uD83C\uDF89"
|
||||||
|
|
||||||
|
data class CarouselItem(
|
||||||
|
val title: Int,
|
||||||
|
val icon: Int,
|
||||||
|
val description: Int,
|
||||||
|
val tint: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
private val featureList = listOf(
|
||||||
|
CarouselItem(
|
||||||
|
R.string.tasks_org_account,
|
||||||
|
R.drawable.ic_round_icon,
|
||||||
|
R.string.upgrade_tasks_org_account_description,
|
||||||
|
tint = false
|
||||||
|
),
|
||||||
|
CarouselItem(
|
||||||
|
R.string.upgrade_more_customization,
|
||||||
|
R.drawable.ic_outline_palette_24px,
|
||||||
|
R.string.upgrade_more_customization_description
|
||||||
|
),
|
||||||
|
CarouselItem(
|
||||||
|
R.string.open_source,
|
||||||
|
R.drawable.ic_octocat,
|
||||||
|
R.string.upgrade_open_source_description
|
||||||
|
),
|
||||||
|
CarouselItem(
|
||||||
|
R.string.upgrade_desktop_access,
|
||||||
|
R.drawable.ic_outline_computer_24px,
|
||||||
|
R.string.upgrade_desktop_access_description
|
||||||
|
),
|
||||||
|
CarouselItem(
|
||||||
|
R.string.gtasks_GPr_header,
|
||||||
|
R.drawable.ic_google,
|
||||||
|
R.string.upgrade_google_tasks,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
CarouselItem(
|
||||||
|
R.string.davx5,
|
||||||
|
R.drawable.ic_davx5_icon_green_bg,
|
||||||
|
R.string.davx5_selection_description,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
CarouselItem(
|
||||||
|
R.string.caldav,
|
||||||
|
R.drawable.ic_webdav_logo,
|
||||||
|
R.string.caldav_selection_description
|
||||||
|
),
|
||||||
|
CarouselItem(
|
||||||
|
R.string.etesync,
|
||||||
|
R.drawable.ic_etesync,
|
||||||
|
R.string.etesync_selection_description,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
CarouselItem(
|
||||||
|
R.string.decsync,
|
||||||
|
R.drawable.ic_decsync,
|
||||||
|
R.string.decsync_selection_description,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
CarouselItem(
|
||||||
|
R.string.upgrade_automation,
|
||||||
|
R.drawable.ic_tasker,
|
||||||
|
R.string.upgrade_automation_description,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PurchaseText(
|
||||||
|
nameYourPrice: MutableState<Boolean> = mutableStateOf(false),
|
||||||
|
sliderPosition: MutableState<Float> = mutableStateOf(0f),
|
||||||
|
github: Boolean = false,
|
||||||
|
fdroid: Boolean = IS_GENERIC,
|
||||||
|
subscribe: (Int, Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.background(color = colorResource(R.color.content_background)),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
GreetingText(R.string.upgrade_blurb_1)
|
||||||
|
GreetingText(R.string.upgrade_blurb_2)
|
||||||
|
Spacer(Modifier.height(KEYLINE_FIRST))
|
||||||
|
val pagerState = remember {
|
||||||
|
PagerState(maxPage = (featureList.size - 1).coerceAtLeast(0))
|
||||||
|
}
|
||||||
|
Pager(state = pagerState, modifier = Modifier.fillMaxWidth().height(200.dp)) {
|
||||||
|
PagerItem(featureList[page], nameYourPrice.value && page == 0)
|
||||||
|
}
|
||||||
|
if (github) {
|
||||||
|
SponsorButton()
|
||||||
|
} else {
|
||||||
|
GooglePlayButtons(nameYourPrice, sliderPosition, pagerState, subscribe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SponsorButton() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse(context.getString(R.string.url_sponsor))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.secondary,
|
||||||
|
contentColor = MaterialTheme.colors.onSecondary
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(KEYLINE_FIRST, 0.dp, KEYLINE_FIRST, KEYLINE_FIRST)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_outline_favorite_border_24px),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.github_sponsor),
|
||||||
|
color = MaterialTheme.colors.onSecondary,
|
||||||
|
style = MaterialTheme.typography.body1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GreetingText(resId: Int) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(KEYLINE_FIRST, KEYLINE_FIRST, KEYLINE_FIRST, 0.dp),
|
||||||
|
text = stringResource(resId),
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GooglePlayButtons(
|
||||||
|
nameYourPrice: MutableState<Boolean>,
|
||||||
|
sliderPosition: MutableState<Float>,
|
||||||
|
pagerState: PagerState,
|
||||||
|
subscribe: (Int, Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Divider(color = MaterialTheme.colors.onSurface, thickness = 0.25.dp)
|
||||||
|
Spacer(Modifier.height(KEYLINE_FIRST))
|
||||||
|
if (nameYourPrice.value) {
|
||||||
|
NameYourPrice(sliderPosition, subscribe)
|
||||||
|
} else {
|
||||||
|
TasksAccount(subscribe)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(KEYLINE_FIRST))
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
nameYourPrice.value = !nameYourPrice.value
|
||||||
|
pagerState.currentPage = 0
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
backgroundColor = Color.Transparent
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
if (nameYourPrice.value)
|
||||||
|
R.string.back
|
||||||
|
else
|
||||||
|
R.string.more_options
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colors.secondary,
|
||||||
|
style = MaterialTheme.typography.body1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.pro_free_trial),
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(.75f)
|
||||||
|
.padding(KEYLINE_FIRST),
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PagerItem(
|
||||||
|
feature: CarouselItem,
|
||||||
|
disabled: Boolean = false
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(.5f),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(HALF_KEYLINE),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(feature.icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.requiredSize(72.dp),
|
||||||
|
alignment = Alignment.Center,
|
||||||
|
colorFilter = if (feature.tint) {
|
||||||
|
ColorFilter.tint(colorResource(R.color.icon_tint_with_alpha))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(feature.title),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(0.dp, 4.dp),
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
style = TextStyle(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
letterSpacing = 0.25.sp
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(if (disabled) R.string.account_not_included else feature.description),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = if (disabled) Color.Red else MaterialTheme.colors.onBackground,
|
||||||
|
style = TextStyle(
|
||||||
|
fontWeight = if (disabled) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
letterSpacing = 0.4.sp
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TasksAccount(subscribe: (Int, Boolean) -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(KEYLINE_FIRST, 0.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
PurchaseButton(
|
||||||
|
price = 30,
|
||||||
|
popperText = "${stringResource(R.string.save_percent, 16)} $POPPER",
|
||||||
|
onClick = subscribe
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(KEYLINE_FIRST))
|
||||||
|
PurchaseButton(
|
||||||
|
price = 3,
|
||||||
|
monthly = true,
|
||||||
|
onClick = subscribe
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PurchaseButton(
|
||||||
|
price: Int,
|
||||||
|
monthly: Boolean = false,
|
||||||
|
popperText: String = "",
|
||||||
|
onClick: (Int, Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { onClick(price, monthly) },
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
if (monthly) R.string.price_per_month else R.string.price_per_year,
|
||||||
|
price
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colors.onSecondary,
|
||||||
|
style = MaterialTheme.typography.body1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = popperText,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NameYourPrice(sliderPosition: MutableState<Float>, subscribe: (Int, Boolean) -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Row(Modifier.fillMaxWidth()) {
|
||||||
|
Slider(
|
||||||
|
modifier = Modifier.padding(KEYLINE_FIRST, 0.dp, KEYLINE_FIRST, HALF_KEYLINE),
|
||||||
|
value = sliderPosition.value,
|
||||||
|
onValueChange = { sliderPosition.value = it },
|
||||||
|
valueRange = 1f..25f,
|
||||||
|
steps = 25,
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = MaterialTheme.colors.secondary,
|
||||||
|
activeTrackColor = MaterialTheme.colors.secondary,
|
||||||
|
inactiveTrackColor = colorResource(R.color.text_tertiary),
|
||||||
|
activeTickColor = Color.Transparent,
|
||||||
|
inactiveTickColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
PurchaseButton(
|
||||||
|
price = sliderPosition.value.toInt(),
|
||||||
|
popperText = if (sliderPosition.value.toInt() >= 5)
|
||||||
|
"${stringResource(R.string.above_average, 16)} $POPPER"
|
||||||
|
else
|
||||||
|
"",
|
||||||
|
onClick = subscribe
|
||||||
|
)
|
||||||
|
if (sliderPosition.value.toInt() < 3) {
|
||||||
|
Spacer(Modifier.width(KEYLINE_FIRST))
|
||||||
|
PurchaseButton(
|
||||||
|
price = sliderPosition.value.toInt(),
|
||||||
|
monthly = true,
|
||||||
|
popperText = "${stringResource(R.string.above_average)} $POPPER",
|
||||||
|
onClick = subscribe
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@ -1,175 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<ScrollView
|
|
||||||
android:id="@+id/scroll"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_above="@id/bottom_panel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text"
|
|
||||||
style="@style/TextAppearance"
|
|
||||||
android:scrollbars="none"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:lineSpacingMultiplier="1.2"
|
|
||||||
android:padding="@dimen/keyline_second"/>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider"
|
|
||||||
android:layout_below="@id/scroll"
|
|
||||||
style="@style/task_edit_row_divider"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/bottom_panel"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingTop="@dimen/keyline_first"
|
|
||||||
android:paddingBottom="@dimen/keyline_first"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/screen_wait"
|
|
||||||
style="?android:attr/progressBarStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/slider_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible"
|
|
||||||
android:layout_marginStart="@dimen/keyline_first"
|
|
||||||
android:layout_marginEnd="@dimen/keyline_first">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/price_low"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_below="@id/slider"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
||||||
android:text="$0.99"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/price_high"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_below="@id/slider"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
||||||
android:text="$24.99"/>
|
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
|
||||||
android:id="@+id/slider"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:valueFrom="1"
|
|
||||||
android:valueTo="25"
|
|
||||||
android:value="10"
|
|
||||||
android:stepSize="1"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
app:trackColorActive="?attr/colorSecondary"
|
|
||||||
app:thumbColor="?attr/colorSecondary"
|
|
||||||
app:tickColorActive="?attr/colorOnSecondary"
|
|
||||||
app:tickColorInactive="?attr/colorSecondary"
|
|
||||||
app:tickColor="@android:color/transparent"
|
|
||||||
app:trackColorInactive="@color/text_tertiary"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/tasks_org_button_panel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/pay_annually"
|
|
||||||
style="@style/OutlineButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
tools:text="$2.99/year"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/pay_monthly"
|
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_chainStyle="packed" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/pay_monthly"
|
|
||||||
style="@style/OutlineButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
tools:text="$2.99/month"
|
|
||||||
android:layout_marginStart="@dimen/keyline_first"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/pay_annually"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/avg_annual"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/pay_annually"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/pay_annually"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/pay_annually"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textAllCaps="true"
|
|
||||||
tools:text="@string/above_average"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/avg_monthly"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/pay_monthly"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/pay_monthly"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/pay_monthly"
|
|
||||||
android:textAllCaps="true"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible"
|
|
||||||
android:textColor="?attr/colorAccent"
|
|
||||||
android:textAlignment="center"
|
|
||||||
tools:text="@string/above_average"/>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/pay_other"
|
|
||||||
style="@style/OutlineButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textColor="@color/text_secondary"
|
|
||||||
android:drawableTint="@color/icon_tint_with_alpha"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/avg_annual"
|
|
||||||
tools:text="@string/back"/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/sponsor"
|
|
||||||
style="@style/OutlineButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:text="@string/github_sponsor"
|
|
||||||
android:textColor="@color/text_secondary"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible"
|
|
||||||
app:icon="@drawable/ic_outline_favorite_border_24px"
|
|
||||||
app:iconTint="@color/github_sponsor"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
Loading…
Reference in New Issue