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