New purchase activity

pull/1421/head
Alex Baker 3 years ago
parent 38e7c810ea
commit c1db57d1e3

@ -187,7 +187,6 @@ dependencies {
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
implementation("androidx.paging:paging-runtime:2.1.2")
implementation("io.noties.markwon:core:${Versions.markwon}")
implementation("io.noties.markwon:ext-strikethrough:${Versions.markwon}")
kapt("com.jakewharton:butterknife-compiler:${Versions.butterknife}")
implementation("com.jakewharton:butterknife:${Versions.butterknife}")

@ -707,12 +707,6 @@
license: The Apache Software License, Version 2.0
licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/google/dagger
- artifact: io.noties.markwon:ext-strikethrough:+
name: ext-strikethrough
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: io.noties.markwon:core:+
name: core
copyrightHolder: Dimitry Ivanov

@ -302,6 +302,10 @@
</intent-filter>
</activity>
<activity
android:name=".billing.PurchaseActivity"
android:theme="@style/TranslucentDialog" />
<!-- ======================================================= Receivers = -->
<!-- widgets -->

@ -1691,20 +1691,6 @@
"url": "https://github.com/google/dagger",
"libraryName": "hilt-core"
},
{
"artifactId": {
"name": "ext-strikethrough",
"group": "io.noties.markwon",
"version": "+"
},
"copyrightHolder": "Dimitry Ivanov",
"copyrightStatement": "Copyright &copy; Dimitry Ivanov. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "https://github.com/noties/Markwon",
"libraryName": "ext-strikethrough"
},
{
"artifactId": {
"name": "core",

@ -29,13 +29,19 @@ import androidx.lifecycle.lifecycleScope
import at.bitfire.dav4jvm.exception.HttpException
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import net.openid.appauth.*
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.ClientSecretBasic
import net.openid.appauth.RegistrationRequest
import net.openid.appauth.RegistrationResponse
import net.openid.appauth.ResponseTypeValues
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseDialog
import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG
import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog
import org.tasks.billing.PurchaseActivity
import org.tasks.billing.PurchaseActivity.Companion.EXTRA_GITHUB
import org.tasks.dialogs.DialogBuilder
import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.themes.ThemeColor
@ -56,7 +62,7 @@ import javax.inject.Inject
* - Initiate the authorization request using the built-in heuristics or a user-selected browser.
*/
@AndroidEntryPoint
class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHandler {
class SignInActivity : InjectingAppCompatActivity() {
@Inject lateinit var themeColor: ThemeColor
@Inject lateinit var inventory: Inventory
@Inject lateinit var dialogBuilder: DialogBuilder
@ -166,8 +172,11 @@ class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHand
private fun handleError(e: Throwable) {
if (e is HttpException && e.code == 402) {
newPurchaseDialog(tasksPayment = true, github = authService.isGitHub)
.show(supportFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
startActivityForResult(
Intent(this, PurchaseActivity::class.java)
.putExtra(EXTRA_GITHUB, authService.isGitHub),
RC_PURCHASE
)
} else {
returnError(e)
}
@ -180,24 +189,39 @@ class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHand
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == RC_AUTH) {
if (resultCode == RESULT_OK) {
lifecycleScope.launch {
val account = try {
viewModel.handleResult(authService, data!!)
} catch (e: Exception) {
returnError(e)
when (requestCode) {
RC_PURCHASE ->
if (inventory.subscription.value?.isTasksSubscription == true) {
lifecycleScope.launch {
val account = viewModel.setupAccount(authService)
if (account != null) {
setResult(RESULT_OK)
finish()
}
}
if (account != null) {
setResult(RESULT_OK)
finish()
} else {
finish()
}
RC_AUTH ->
if (resultCode == RESULT_OK) {
lifecycleScope.launch {
val account = try {
viewModel.handleResult(authService, data!!)
} catch (e: Exception) {
returnError(e)
}
if (account != null) {
setResult(RESULT_OK)
finish()
}
}
} else {
returnError(
Exception(getString(R.string.authorization_cancelled)),
report = false
)
}
} else {
returnError(Exception(getString(R.string.authorization_cancelled)), report = false)
}
} else {
super.onActivityResult(requestCode, resultCode, data)
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
@ -364,19 +388,6 @@ class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHand
const val EXTRA_ERROR = "extra_error"
const val EXTRA_SELECT_SERVICE = "extra_select_service"
private const val RC_AUTH = 100
}
override fun onPurchaseDialogDismissed() {
if (inventory.subscription.value?.isTasksSubscription == true) {
lifecycleScope.launch {
val account = viewModel.setupAccount(authService)
if (account != null) {
setResult(RESULT_OK)
finish()
}
}
} else {
finish()
}
private const val RC_PURCHASE = 101
}
}

@ -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
}
}
}

@ -26,8 +26,7 @@ import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG
import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog
import org.tasks.billing.PurchaseActivity
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavDao
import org.tasks.databinding.ActivityCaldavAccountSettingsBinding
@ -104,7 +103,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
newSnackbar(getString(R.string.this_feature_requires_a_subscription))
.setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE)
.setAction(R.string.button_subscribe) {
newPurchaseDialog().show(supportFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
startActivity(Intent(this, PurchaseActivity::class.java))
}
.show()
}

@ -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
)
}
}
}
}
}

@ -16,8 +16,7 @@ import butterknife.ButterKnife
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG
import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog
import org.tasks.billing.PurchaseActivity
import org.tasks.dialogs.ColorPickerAdapter.Palette
import org.tasks.dialogs.ColorWheelPicker.Companion.newColorWheel
import org.tasks.themes.ColorProvider
@ -108,7 +107,7 @@ class ColorPalettePicker : DialogFragment() {
builder.setNegativeButton(R.string.cancel, null)
} else {
builder.setPositiveButton(R.string.upgrade_to_pro) { _: DialogInterface?, _: Int ->
newPurchaseDialog().show(parentFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
startActivity(Intent(context, PurchaseActivity::class.java))
}
}
return builder.show()

@ -15,8 +15,7 @@ import com.flask.colorpicker.builder.ColorPickerDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG
import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog
import org.tasks.billing.PurchaseActivity
import javax.inject.Inject
@AndroidEntryPoint
@ -65,8 +64,10 @@ class ColorWheelPicker : DialogFragment() {
if (inventory.purchasedThemes()) {
deliverSelection()
} else {
newPurchaseDialog(this, REQUEST_PURCHASE)
.show(parentFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
startActivityForResult(
Intent(context, PurchaseActivity::class.java),
REQUEST_PURCHASE
)
}
}
.setNegativeButton(R.string.cancel, null)

@ -1,26 +1,29 @@
package org.tasks.dialogs;
import static org.tasks.billing.PurchaseDialog.newPurchaseDialog;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.hilt.android.AndroidEntryPoint;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.billing.Inventory;
import org.tasks.billing.PurchaseDialog;
import org.tasks.billing.PurchaseActivity;
import org.tasks.themes.CustomIcons;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class IconPickerDialog extends DialogFragment {
@ -65,8 +68,8 @@ public class IconPickerDialog extends DialogFragment {
if (!inventory.getHasPro()) {
builder.setPositiveButton(
R.string.upgrade_to_pro,
(dialog, which) -> newPurchaseDialog()
.show(getParentFragmentManager(), PurchaseDialog.getFRAG_TAG_PURCHASE_DIALOG()));
(dialog, which) -> startActivity(new Intent(getContext(), PurchaseActivity.class))
);
}
return builder.show();
}

@ -6,23 +6,16 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.fragment.app.DialogFragment
import butterknife.BindView
import butterknife.ButterKnife
import butterknife.OnClick
import com.google.android.material.button.MaterialButton
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.Markwon
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.Tasks.Companion.IS_GENERIC
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG
import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog
import org.tasks.billing.PurchaseActivity
import org.tasks.databinding.DialogWhatsNewBinding
import org.tasks.preferences.Preferences
import java.io.BufferedReader
import javax.inject.Inject
@ -36,26 +29,17 @@ class WhatsNewDialog : DialogFragment() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var inventory: Inventory
@BindView(R.id.changelog) lateinit var changelog: TextView
@BindView(R.id.action_question) lateinit var actionQuestion: TextView
@BindView(R.id.action_text) lateinit var actionText: TextView
@BindView(R.id.action_button) lateinit var actionButton: MaterialButton
@BindView(R.id.dismiss_button) lateinit var dismissButton: MaterialButton
private var displayedRate = false
private var displayedSubscribe = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val view: View = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_whats_new, null)
ButterKnife.bind(this, view)
val binding = DialogWhatsNewBinding.inflate(layoutInflater)
val textStream = requireContext().assets.open("CHANGELOG.md")
val text = BufferedReader(textStream.reader()).readText()
val markwon = Markwon.builder(requireContext())
.usePlugin(StrikethroughPlugin.create())
.build()
changelog.movementMethod = LinkMovementMethod.getInstance()
changelog.text = markwon.toMarkdown(text)
val markwon = Markwon.builder(requireContext()).build()
binding.changelog.movementMethod = LinkMovementMethod.getInstance()
binding.changelog.text = markwon.toMarkdown(text)
val begForSubscription = !inventory.hasPro
val begForRating = !preferences.getBoolean(R.string.p_clicked_rate, false)
@ -63,43 +47,51 @@ class WhatsNewDialog : DialogFragment() {
&& (!begForSubscription || Random.nextBoolean())
when {
BuildConfig.FLAVOR == "generic" -> {
actionText.text = getString(R.string.upgrade_blurb_4)
actionButton.text = getString(R.string.TLA_menu_donate)
actionButton.setOnClickListener { onDonateClick() }
IS_GENERIC -> {
binding.actionQuestion.setText(R.string.enjoying_tasks)
binding.actionText.setText(R.string.upgrade_blurb_4)
binding.actionButton.text = getString(R.string.TLA_menu_donate)
binding.actionButton.setOnClickListener { onDonateClick() }
}
begForRating -> {
displayedRate = true
actionButton.text = getString(R.string.rate_tasks)
actionButton.setOnClickListener { onRateClick() }
binding.actionQuestion.setText(R.string.enjoying_tasks)
binding.actionButton.setText(R.string.rate_tasks)
binding.actionButton.setOnClickListener { onRateClick() }
}
begForSubscription -> {
displayedSubscribe = true
actionText.text = getString(R.string.support_development_subscribe)
actionButton.text = getString(R.string.name_your_price)
actionButton.setOnClickListener { onSubscribeClick() }
binding.actionQuestion.setText(R.string.tasks_needs_your_support)
binding.actionText.setText(R.string.support_development_subscribe)
binding.actionButton.setText(R.string.name_your_price)
binding.actionButton.setOnClickListener { onSubscribeClick() }
}
else -> {
actionQuestion.visibility = View.GONE
actionText.visibility = View.GONE
actionButton.visibility = View.GONE
dismissButton.text = getString(R.string.got_it)
binding.actionQuestion.visibility = View.GONE
binding.actionText.visibility = View.GONE
binding.actionButton.visibility = View.GONE
binding.dismissButton.text = getString(R.string.got_it)
}
}
if (!resources.getBoolean(R.bool.whats_new_action)) {
actionText.visibility = View.GONE
binding.actionText.visibility = View.GONE
}
binding.dismissButton.setOnClickListener {
logClick(false)
dismiss()
}
return dialogBuilder.newDialog()
.setView(view)
.setView(binding.root)
.show()
}
private fun onSubscribeClick() {
logClick(true)
dismiss()
newPurchaseDialog().show(parentFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
startActivity(Intent(context, PurchaseActivity::class.java))
}
private fun onRateClick() {
@ -119,12 +111,6 @@ class WhatsNewDialog : DialogFragment() {
super.onCancel(dialog)
}
@OnClick(R.id.dismiss_button)
fun onDismissClick() {
logClick(false)
dismiss()
}
private fun logClick(click: Boolean) {
firebase.logEvent(
R.string.event_whats_new,

@ -1,22 +1,25 @@
package org.tasks.locale.ui.activity;
import static org.tasks.billing.PurchaseDialog.newPurchaseDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.widget.Toolbar;
import dagger.hilt.android.AndroidEntryPoint;
import javax.inject.Inject;
import net.dinglisch.android.tasker.TaskerPlugin;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.billing.Inventory;
import org.tasks.billing.PurchaseDialog;
import org.tasks.billing.PurchaseActivity;
import org.tasks.databinding.ActivityTaskerCreateBinding;
import org.tasks.locale.bundle.TaskCreationBundle;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCompatActivity
implements Toolbar.OnMenuItemClickListener {
@ -55,8 +58,7 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom
}
private void showPurchaseDialog() {
newPurchaseDialog()
.show(getSupportFragmentManager(), PurchaseDialog.getFRAG_TAG_PURCHASE_DIALOG());
startActivity(new Intent(this, PurchaseActivity.class));
}
@Override

@ -9,7 +9,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.R
import org.tasks.billing.BillingClient
import org.tasks.billing.PurchaseDialog
import org.tasks.billing.PurchaseActivity
import org.tasks.injection.InjectingPreferenceFragment
import javax.inject.Inject
@ -38,10 +38,11 @@ abstract class BaseAccountPreference : InjectingPreferenceFragment() {
protected abstract suspend fun removeAccount()
protected fun showPurchaseDialog(tasksPayment: Boolean = false): Boolean {
PurchaseDialog
.newPurchaseDialog(this, REQUEST_PURCHASE, tasksPayment)
.show(parentFragmentManager, PurchaseDialog.FRAG_TAG_PURCHASE_DIALOG)
protected fun showPurchaseDialog(): Boolean {
startActivityForResult(
Intent(context, PurchaseActivity::class.java),
REQUEST_PURCHASE
)
return false
}

@ -19,8 +19,7 @@ import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.activities.FilterSelectionActivity
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG
import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog
import org.tasks.billing.PurchaseActivity
import org.tasks.dialogs.ColorPalettePicker
import org.tasks.dialogs.ColorPalettePicker.Companion.newColorPalette
import org.tasks.dialogs.ColorPickerAdapter
@ -182,8 +181,10 @@ class LookAndFeel : InjectingPreferenceFragment() {
if (inventory.purchasedThemes() || ThemeBase(index).isFree) {
setBaseTheme(index)
} else {
newPurchaseDialog(this, REQUEST_PURCHASE)
.show(parentFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
startActivityForResult(
Intent(context, PurchaseActivity::class.java),
REQUEST_PURCHASE
)
}
} else {
setBaseTheme(index)

@ -13,7 +13,7 @@ import org.tasks.R
import org.tasks.billing.BillingClient
import org.tasks.billing.Inventory
import org.tasks.billing.Purchase
import org.tasks.billing.PurchaseDialog
import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.data.CaldavAccount
import org.tasks.data.GoogleTaskAccount
@ -47,9 +47,7 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
findPreference(R.string.add_account).setOnPreferenceClickListener { addAccount() }
findPreference(R.string.name_your_price).setOnPreferenceClickListener {
PurchaseDialog
.newPurchaseDialog()
.show(parentFragmentManager, PurchaseDialog.FRAG_TAG_PURCHASE_DIALOG)
startActivity(Intent(context, PurchaseActivity::class.java))
false
}

@ -8,8 +8,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.activities.FilterSelectionActivity
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG
import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog
import org.tasks.billing.PurchaseActivity
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.locale.bundle.ListNotificationBundle
import org.tasks.preferences.DefaultFilterProvider
@ -58,8 +57,10 @@ class TaskerListNotification : InjectingPreferenceFragment() {
}
if (!inventory.purchasedTasker()) {
newPurchaseDialog(this, REQUEST_SUBSCRIPTION)
.show(parentFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
startActivityForResult(
Intent(context, PurchaseActivity::class.java),
REQUEST_SUBSCRIPTION
)
}
}

@ -175,9 +175,7 @@ class TasksAccount : BaseAccountPreference() {
}
}
} else {
setOnPreferenceClickListener {
showPurchaseDialog(tasksPayment = true)
}
setOnPreferenceClickListener { showPurchaseDialog() }
if (subscription == null || subscription.isTasksSubscription) {
setTitle(R.string.button_subscribe)
setSummary(R.string.your_subscription_expired)

@ -24,8 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG
import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog
import org.tasks.billing.PurchaseActivity
import org.tasks.data.TaskDao
import org.tasks.dialogs.NewFilterDialog.Companion.newFilterDialog
import org.tasks.filters.FilterProvider
@ -85,7 +84,7 @@ class NavigationDrawerFragment : Fragment() {
} else if (item is NavigationDrawerAction) {
when (item.requestCode) {
REQUEST_PURCHASE ->
newPurchaseDialog().show(parentFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
startActivity(Intent(context, PurchaseActivity::class.java))
REQUEST_DONATE -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_donate))))
REQUEST_NEW_FILTER -> newFilterDialog().show(parentFragmentManager, FRAG_TAG_NEW_FILTER)
else -> activity?.startActivityForResult(item.intent, item.requestCode)

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>

@ -45,6 +45,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingTop="@dimen/half_keyline_first"
android:paddingStart="@dimen/keyline_first"
android:paddingEnd="@dimen/keyline_first"
android:text="@string/tell_me_how_im_doing"

@ -553,6 +553,8 @@
<string name="on_launch">Při spuštění</string>
<string name="lists">Seznamy</string>
<string name="add_filter">Přidat filtr</string>
<string name="upgrade_blurb_2">Strávil jsem tisíce hodin prací na Tasks a bezplatně zveřejňuji veškerý zdrojový kód online. Za účelem podpory mé práce některé funkce vyžadují předplatné</string>
<string name="upgrade_blurb_1">Ahoj! Jmenuji se Alex. Jsem nezávislý vývojář Tasks</string>
<string name="color_wheel">Více barev</string>
<string name="invalid_username_or_password">Neplatné uživatelské jméno nebo heslo</string>
<string name="davx5_selection_description">Synchronizujte své úkoly pomocí aplikace DAVx⁵</string>

@ -163,6 +163,8 @@
<string name="chip_style">Boble-stil</string>
<string name="back">Tilbage</string>
<string name="upgrade_blurb_4">Din støtte betydet meget for mig, tak!</string>
<string name="upgrade_blurb_2">Jeg har brugt tusindvis af arbejdstimer på Tasks, og jeg udgiver hele kildekoden gratis online. For at støtte mit arbejde kræver nogle funktioner et abonnement</string>
<string name="upgrade_blurb_1">Hej! Jeg hedder Alex. Jeg er den selvstændige udvikler bag Tasks</string>
<string name="color_wheel">Farvehjul</string>
<string name="invalid_username_or_password">Ugyldigt brugernavn eller kodeord</string>
<string name="more_notification_settings_summary">Ringetone, vibrér og mere</string>
@ -641,8 +643,6 @@
<string name="your_subscription_expired">Dit abonnement er udløbet. Abonner nu for at genoptage tjenesten.</string>
<string name="custom_filter_is_subtask">Er underopgaver</string>
<string name="custom_filter_has_subtask">Har underopgaver</string>
<string name="price_per_month_abbreviated">$%s/m</string>
<string name="price_per_year_abbreviated">$%s/år</string>
<string name="price_per_year">$%s/år</string>
<string name="no_google_play_subscription">Ingen kvalificeret Google Play abonnement fundet</string>
</resources>

@ -493,6 +493,8 @@
<string name="more_notification_settings_summary">Klingelton, Vibration und mehr</string>
<string name="invalid_username_or_password">Ungültiger Benutzername oder Passwort</string>
<string name="color_wheel">Farbkreis</string>
<string name="upgrade_blurb_1">Hallo! Mein Name ist Alex. Ich bin der unabhängige Entwickler hinter Tasks</string>
<string name="upgrade_blurb_2">Ich habe Tausende von Stunden mit der Arbeit an Tasks verbracht und veröffentliche den gesamten Quellcode kostenlos online. Um meine Arbeit zu unterstützen, erfordern einige Funktionen ein Abonnement</string>
<string name="upgrade_blurb_4">Ihre Unterstützung bedeutet mir sehr viel, danke!</string>
<string name="back">Zurück</string>
<string name="chip_style">Stil der Marken</string>
@ -621,9 +623,7 @@
<string name="your_subscription_expired">Ihr Abonnement ist abgelaufen. Abonnieren Sie jetzt, um den Dienst fortzusetzen.</string>
<string name="background_location_permission_required">Tasks sammelt Standortdaten, um standortbezogene Erinnerungen zu ermöglichen, auch wenn die App geschlossen oder nicht in Gebrauch ist.</string>
<string name="repeat_monthly_fifth_week">fünften</string>
<string name="price_per_month_abbreviated">%s $/Monat</string>
<string name="price_per_month">%s $/Monat</string>
<string name="price_per_year_abbreviated">%s $/Jahr</string>
<string name="price_per_year">%s $/Jahr</string>
<string name="save_percent">%d %% sparen</string>
<string name="no_google_play_subscription">Kein qualifiziertes Googl Play -Abo gefunden</string>

@ -492,6 +492,8 @@
<string name="invalid_username_or_password">Nombre de usuario o contraseña inválidos</string>
<string name="theme_system_default">El sistema por defecto</string>
<string name="color_wheel">Rueda de colores</string>
<string name="upgrade_blurb_1">¡Hola! Me llamo Alex. Soy el desarrollador independiente detrás de Tasks</string>
<string name="upgrade_blurb_2">He pasado miles de horas trabajando en Tareas, y publico todo el código fuente en línea de forma gratuita. Para apoyar mi trabajo, algunas características requieren una suscripción</string>
<string name="upgrade_blurb_4">Su apoyo significa mucho para mí, ¡gracias!</string>
<string name="back">Volver</string>
<string name="chip_style">Estilo de chip</string>
@ -616,9 +618,7 @@
<string name="custom_filter_is_subtask">Es una subtarea</string>
<string name="custom_filter_has_subtask">Tiene subtareas</string>
<string name="current_subscription">Suscripción actual: %s</string>
<string name="price_per_month_abbreviated">$%s/mes</string>
<string name="price_per_month">$%s/mes</string>
<string name="price_per_year_abbreviated">$%s/año</string>
<string name="price_per_year">$%s/año</string>
<string name="insufficient_subscription">Nivel de suscripción insuficiente. Actualice su suscripción para reanudar el servicio.</string>
<string name="your_subscription_expired">Vuestra suscripción ha expirado. Suscribe ahora a resume servicio.</string>

@ -492,6 +492,8 @@
<string name="invalid_username_or_password">Erabiltzaile-izen edo pasahitz okerra</string>
<string name="theme_system_default">Sisteman lehenetsia</string>
<string name="color_wheel">Kolore-gurpila</string>
<string name="upgrade_blurb_1">Kaixo! Nire izana Alex da. Ni naiz Tasks aplikazioaren garatzaile independentea</string>
<string name="upgrade_blurb_2">Milaka ordu eman ditut Tsks aplikazioan lanean, eta kode guztia argitaratzen dut doan. Nire lana babesteko ezaugarri batzuk harpidetza eskatzen dute</string>
<string name="upgrade_blurb_4">Zure babesa asko da niretzat, eskerrik asko!</string>
<string name="back">Atzera</string>
<string name="chip_style">Txip estiloa</string>
@ -592,9 +594,7 @@
<string name="authorization_cancelled">Autorizazioa ezeztatuta</string>
<string name="follow_reddit">Egin bat r/task-ekin</string>
<string name="current_subscription">Uneko harpidetza: %s</string>
<string name="price_per_month_abbreviated">$%s hilean</string>
<string name="price_per_month">$%s hilean</string>
<string name="price_per_year_abbreviated">$%s urtean</string>
<string name="price_per_year">$%s urtean</string>
<string name="insufficient_subscription">Harpidetza maila ez da nahikoa. Hobetu zure harpidetza zerbitzua berrekiteko.</string>
<string name="your_subscription_expired">Zure harpidetza agortu da. Harpidetu orain zerbitzua berrekiteko.</string>

@ -545,6 +545,8 @@
<string name="auto_dismiss_datetime_list_summary">Sulje automaattisesti kun valitaan tehtävä listalta</string>
<string name="place_settings">Paikka asetukset</string>
<string name="places">Paikat</string>
<string name="upgrade_blurb_2">Olen käyttänyt tuhansia tunteja työskennellen Task ohjelman parissa, ja julkaisen koko lähdekoodin netissä ilmaiseksi. Työni tueksi joidenkin ohjelman ominaisuuksien käyttö vaatii tilauksen</string>
<string name="upgrade_blurb_1">Moi! Nimeni on Alex. Olen itsenäinen ohjelmistontekijä Task ohjelman takana</string>
<string name="troubleshooting">Ongelmien ratkaisu</string>
<string name="wearable_notifications_summary">Näytä ilmoitukset puettavassa laitteessasi</string>
<string name="wearable_notifications">Puettavan laitteen ilmoitukset</string>

@ -487,6 +487,8 @@
<string name="invalid_username_or_password">Nom d\'utilisateur ou mot de passe invalide</string>
<string name="theme_system_default">Défaut du système</string>
<string name="color_wheel">Palette de couleurs</string>
<string name="upgrade_blurb_1">Salut ! Je m\'appelle Alex. Je suis le développeur indépendant à l\'origine de Tasks</string>
<string name="upgrade_blurb_2">J\'ai passé des milliers d\'heures à travailler sur Tasks, et je publie gratuitement tout le code source en ligne. Afin de soutenir mon travail, certaines fonctionnalités nécessitent un abonnement</string>
<string name="upgrade_blurb_4">Votre soutien est très important pour moi, merci !</string>
<string name="back">Retour</string>
<string name="chip_style">Style d\'étiquette</string>
@ -611,9 +613,7 @@
<string name="custom_filter_is_subtask">Est une sous-tâche</string>
<string name="custom_filter_has_subtask">A des sous-tâches</string>
<string name="current_subscription">Abonnement actuel : %s</string>
<string name="price_per_month_abbreviated">%s $/mois</string>
<string name="price_per_month">%s $/mois</string>
<string name="price_per_year_abbreviated">%s $/an</string>
<string name="price_per_year">%s $/an</string>
<string name="insufficient_subscription">Niveau d\'abonnement insuffisant. Veuillez mettre votre abonnement à niveau pour reprendre le service.</string>
<string name="your_subscription_expired">Votre abonnement a expiré. Abonnez-vous dès maintenant pour reprendre le service.</string>

@ -490,6 +490,8 @@
<string name="invalid_username_or_password">Hibás felhasználónév vagy jelszó</string>
<string name="theme_system_default">Alapértelmezett</string>
<string name="color_wheel">Színkör</string>
<string name="upgrade_blurb_1">Szia! Alexnek hívnak. Én vagyok a Tasks mögött álló független fejlesztő</string>
<string name="upgrade_blurb_2">Több ezer órát dolgoztam a Tasks appon, és a teljes forráskódot ingyenesen elérhetővé tettem. A munkám támogatása érdekében néhány funkció eléréséhez előfizetés szükséges</string>
<string name="upgrade_blurb_4">A támogatásod sokat jelent nekem, köszönöm!</string>
<string name="back">Vissza</string>
<string name="chip_style">Jelölő stílusa</string>
@ -611,8 +613,6 @@
<string name="always_display_full_date">Teljes dátum mutatása</string>
<string name="current_subscription">Jelenlegi előfizetés: %s</string>
<string name="price_per_month">%s $/hónap</string>
<string name="price_per_month_abbreviated">%s $/hó</string>
<string name="price_per_year_abbreviated">%s $/év</string>
<string name="price_per_year">%s $/év</string>
<string name="insufficient_subscription">Elégtelen előfizetési szint. Kérem, upgrade-elje az előfizetést a szolgáltatás folytatásához.</string>
<string name="your_subscription_expired">Az előfizetés lejárt. Fizessen elő most a szolgáltatás folytatásához.</string>

@ -523,9 +523,7 @@
<string name="authorization_cancelled">Otorisasi dibatalkan</string>
<string name="follow_reddit">Gabung r/tasks</string>
<string name="current_subscription">Langganan saat ini: %s</string>
<string name="price_per_month_abbreviated">$%s/Bln</string>
<string name="price_per_month">$%s/Bulan</string>
<string name="price_per_year_abbreviated">$%s/Thn</string>
<string name="price_per_year">$%s/Tahun</string>
<string name="no_google_play_subscription">Tidak ditemukan langganan Google Play yang memenuhi syarat</string>
<string name="insufficient_sponsorship">Tidak ditemukan sponsor GitHub yang memenuhi syarat</string>

@ -467,6 +467,8 @@
<string name="chip_style_outlined">Contornato</string>
<string name="back">Indietro</string>
<string name="upgrade_blurb_4">Il tuo supporto significa molto per me, grazie!</string>
<string name="upgrade_blurb_2">Ho dedicato a Tasks migliaia di ore di lavoro, pubblicando tutto il codice sorgente online, gratuitamente. Per supportare il mio lavoro alcune funzioni richiedono un abbonamento</string>
<string name="upgrade_blurb_1">Ciao! Mi chiamo Alex. Sono lo sviluppatore, indipendente, di Tasks</string>
<string name="color_wheel">Cerchio cromatico</string>
<string name="invalid_username_or_password">Nome utente o password non validi</string>
<string name="more_notification_settings_summary">Suoneria, vibrazione ed altro</string>
@ -614,9 +616,7 @@
<string name="custom_filter_has_subtask">Contiene attività secondarie</string>
<string name="custom_filter_is_subtask">È un\'attività secondaria</string>
<string name="current_subscription">Abbonamento attuale: %s</string>
<string name="price_per_month_abbreviated">%s $/mese</string>
<string name="price_per_month">%s $/mese</string>
<string name="price_per_year_abbreviated">%s $/anno</string>
<string name="price_per_year">%s $/anno</string>
<string name="insufficient_subscription">Livello di abbonamento insufficiente. Per favore aggiornalo per riattivare il servizio.</string>
<string name="your_subscription_expired">Il tuo abbonamento è scaduto. Abbonati ora per riattivare il servizio.</string>

@ -575,6 +575,8 @@
<string name="permission_read_tasks">גישה מלאה למסד הנתונים של Tasks</string>
<string name="reset_sort_order">איפוס אופן הסידור</string>
<string name="upgrade_blurb_4">התמיכה שלך יקרה ללבי, תודה רבה לך!</string>
<string name="upgrade_blurb_2">השקעתי אלפי שעות בעבודה על Tasks ואני מפרסם את כל קוד המקור באינטרנט בחינם. כדי לתמוך בעבודה שלי חלק מהתכונות דורשות הרשמה</string>
<string name="upgrade_blurb_1">היי! אני אלכס, המתכנת העצמאי שמאחורי Tasks</string>
<string name="invalid_username_or_password">שם המשתמש או הססמה שגויים</string>
<string name="more_notification_settings_summary">צלצול, סוגי רטט ועוד</string>
<string name="disable_battery_optimizations">השבתת שיפורי סוללה</string>
@ -648,9 +650,7 @@
<string name="background_location_permission_required">האפליקצייה הזו אוספת נתוני מיקום כדי לאפשר תזכורות על בסיס מיקום אפילו כשהיישומון סגור ולא בשימוש.</string>
<string name="follow_reddit">מעקב אחר r/tasks</string>
<string name="current_subscription">מינוי נוכחי: %s</string>
<string name="price_per_month_abbreviated">$%s/חודש</string>
<string name="price_per_month">$%s לחודש</string>
<string name="price_per_year_abbreviated">$%s/שנה</string>
<string name="price_per_year">$%s לשנה</string>
<string name="your_subscription_expired">תוקף המינוי שלך פג. ניתן להירשם כעת להמשיך את השירות.</string>
<string name="custom_filter_is_subtask">היא תת־משימה</string>

@ -485,6 +485,7 @@
<string name="navigation_drawer">네비게이션 서랍</string>
<string name="place_settings">위치 설정</string>
<string name="places">위치</string>
<string name="upgrade_blurb_1">안녕하세요! Tasks의 1인 개발자 알렉스입니다</string>
<string name="desaturate_colors_summary_on">다크 테마 사용 시 채도를 낮춥니다</string>
<string name="desaturate_colors_summary_off">다크 테마 사용 시 채도를 낮추지 않습니다</string>
<string name="desaturate_colors">저채도 색상</string>
@ -493,6 +494,7 @@
<string name="chips"></string>
<string name="chip_style_filled">배경색 채움</string>
<string name="chip_style">칩 스타일</string>
<string name="upgrade_blurb_2">저는 Tasks 개발에 엄청나게 많은 시간을 쏟고 있으며, 모든 소스코드를 웹에 무상으로 공개하고 있습니다. 저의 작업을 후원하기 위해 일부 기능은 구독이 필요합니다</string>
<string name="upgrade_blurb_4">당신의 후원은 저에게 큰 힘이 됩니다. 감사합니다!</string>
<string name="color_wheel">색상환</string>
<string name="invalid_username_or_password">유효하지 않은 사용자명과 비밀번호</string>
@ -603,8 +605,6 @@
<string name="authorization_cancelled">인증 취소됨</string>
<string name="current_subscription">현재 구독: %s</string>
<string name="price_per_year">$%s/년</string>
<string name="price_per_year_abbreviated">$%s/년</string>
<string name="price_per_month_abbreviated">$%s/월</string>
<string name="price_per_month">$%s/월</string>
<string name="your_subscription_expired">구독이 만료되었습니다. 서비스를 재개하려면 지금 구독하세요.</string>
<string name="background_location">백그라운드 위치정보</string>

@ -539,6 +539,8 @@
<string name="chips">Flis</string>
<string name="chip_style_outlined">Omrisset</string>
<string name="chip_style">Flisstil</string>
<string name="upgrade_blurb_2">Jeg har brukt tusenvis av timer på å jobbe med Tasks, og jeg offentliggjør all kildekoden på nettet gratis. For å støtte mitt arbeid krever noen funksjoner et abonnement</string>
<string name="upgrade_blurb_1">Hei, jeg heter Alex. Jeg er den uavhengige utvikleren bak Tasks</string>
<string name="caldav_account_description">Krever en konto med en CalDAV-tjenestetilbyder, eller en selvdrevet tjener. Finn en tjenestetilbyder ved å besøke tasks.org/caldav</string>
<string name="chip_style_filled">Fylt</string>
<string name="wearable_notifications_summary">Vis merknader på din ikledbare</string>
@ -617,9 +619,7 @@
<string name="ok">OK</string>
<string name="insufficient_subscription">Utilstrekkelig abonnementsnivå. Oppgrader ditt abonnement for å fortsette tjenesten.</string>
<string name="current_subscription">Nåværende abonnement: %s</string>
<string name="price_per_month_abbreviated">$%s/md.</string>
<string name="price_per_month">$%s/måned</string>
<string name="price_per_year_abbreviated">$%s/år</string>
<string name="price_per_year">$%s/år</string>
<string name="your_subscription_expired">Ditt abonnement har utløpt. Abonner nå for å fortsette tjenesten.</string>
<string name="follow_reddit">Ta del i r/tasks</string>

@ -487,6 +487,8 @@
<string name="invalid_username_or_password">Ongeldige gebruikersnaam of wachtwoord</string>
<string name="theme_system_default">Systeeminstelling</string>
<string name="color_wheel">Kleurenwiel</string>
<string name="upgrade_blurb_1">Hoi! Mijn naam is Alex. Ik ben de onafhankelijke ontwikkelaar achter Tasks.</string>
<string name="upgrade_blurb_2">Ik heb duizenden uren aan Tasks gewerkt, en ik publiceer de volledige broncode gratis online. Om mijn werk te steunen vereisen sommige functies een abonnement.</string>
<string name="upgrade_blurb_4">Je steun betekent veel voor me, bedankt!</string>
<string name="back">Terug</string>
<string name="chip_style">Fiche-stijl</string>
@ -611,9 +613,7 @@
<string name="custom_filter_is_subtask">Is deeltaak</string>
<string name="custom_filter_has_subtask">Heeft deeltaken</string>
<string name="current_subscription">Huidige abonnement: %s</string>
<string name="price_per_month_abbreviated">$%s/ma</string>
<string name="price_per_month">$%s/maand</string>
<string name="price_per_year_abbreviated">$%s/jr</string>
<string name="price_per_year">$%s/jaar</string>
<string name="insufficient_subscription">Abonnementsniveau onvoldoende. Verhoog a.u.b. je abonnement om deze dienst te hervatten.</string>
<string name="your_subscription_expired">Je abonnement is verlopen. Abonneer je nu om deze dienst te hervatten.</string>

@ -503,6 +503,8 @@
<string name="invalid_username_or_password">Nieprawidłowa nazwa użytkownika lub hasło</string>
<string name="theme_system_default">Domyślny systemowy</string>
<string name="color_wheel">Paleta</string>
<string name="upgrade_blurb_1">Cześć! Mam na imię Alex. Jestem niezależnym deweloperem stojącym za Tasks</string>
<string name="upgrade_blurb_2">Spędziłem tysiące godzin pracując nad Tasks i publikuję cały kod źródłowy online za darmo. Aby wesprzeć moją pracę, niektóre funkcję wymagają subskrypcji</string>
<string name="upgrade_blurb_4">Twoje wsparcie wiele dla mnie znaczy, dziękuję!</string>
<string name="back">Wstecz</string>
<string name="chip_style">Styl chipa</string>
@ -660,9 +662,7 @@
<string name="authorization_cancelled">Autoryzacja anulowana</string>
<string name="follow_reddit">Dołącz do r/tasks</string>
<string name="current_subscription">Aktualna subskrypcja: %s</string>
<string name="price_per_month_abbreviated">$%s/mies.</string>
<string name="price_per_month">$%s/miesiąc</string>
<string name="price_per_year_abbreviated">$%s/rok</string>
<string name="price_per_year">$%s/rok</string>
<string name="no_google_play_subscription">Nie znaleziono kwalifikującej się subskrypcji Google Play</string>
<string name="insufficient_sponsorship">Nie znaleziono kwalifikującego się sponsoringu GitHub</string>

@ -462,6 +462,8 @@
<string name="chip_style">Estilo de notificação</string>
<string name="back">Voltar</string>
<string name="upgrade_blurb_4">Seu suporte significa muito para mim, obrigado!</string>
<string name="upgrade_blurb_2">Eu investi centenas de horas trabalhando no Tasks, e eu publico todo o código fonte online de graça. Para apoiar meu trabalho, algumas funcionalidades precisam de um plano de subscrição</string>
<string name="upgrade_blurb_1">Olá! Meu nome é Alex e eu sou o desenvolvedor independente por trás do Tasks</string>
<string name="color_wheel">Roda de cores</string>
<string name="invalid_username_or_password">Nome de usuário ou senha inválido</string>
<string name="more_notification_settings_summary">Toque, vibrações e mais</string>

@ -518,6 +518,8 @@
<string name="chip_style">Estilo de notificação</string>
<string name="back">Voltar</string>
<string name="upgrade_blurb_4">Seu suporte significa muito para mim, obrigado!</string>
<string name="upgrade_blurb_2">Eu investi centenas de horas a trabalhar no Tasks e publico todo o código-fonte online de graça. Para apoiar meu trabalho, algumas funcionalidades precisam de uma assinatura</string>
<string name="upgrade_blurb_1">Olá! O meu nome é Alex e sou o programador independente por trás do Tasks</string>
<string name="color_wheel">Roda de cores</string>
<string name="theme_system_default">Predefinição do Sistema</string>
<string name="invalid_username_or_password">Nome de utilizador ou palavra-passe inválido</string>
@ -596,9 +598,7 @@
<string name="authorization_cancelled">Autorização</string>
<string name="follow_reddit">Segue r/</string>
<string name="current_subscription">Subscrição atual: %s</string>
<string name="price_per_month_abbreviated">%s€/mês</string>
<string name="price_per_month">%s€/mês</string>
<string name="price_per_year_abbreviated">%s€/ano</string>
<string name="price_per_year">%s€/</string>
<string name="custom_filter_is_subtask">É</string>
<string name="delete_comment">Apagar este comentário\?</string>

@ -508,6 +508,8 @@
<string name="invalid_username_or_password">Неверное имя пользователя или пароль</string>
<string name="theme_system_default">Системная по умолчанию</string>
<string name="color_wheel">Палитра</string>
<string name="upgrade_blurb_1">Привет! Меня зовут Алекс. Я - независимый разработчик, стоящий за программой Tasks</string>
<string name="upgrade_blurb_2">Я потратил тысячи часов, работая над Tasks, и я публикую весь исходный код онлайн, бесплатно. Для того, чтобы поддержать мою работу, некоторые функциональности требуют подписки</string>
<string name="upgrade_blurb_4">Ваша поддержка много значит для меня, спасибо!</string>
<string name="back">Назад</string>
<string name="chip_style">Стиль индикаторов списков</string>
@ -633,9 +635,7 @@
<string name="multi_select_reschedule">Перенести</string>
<string name="follow_reddit">Подписаться на r/tasks</string>
<string name="current_subscription">Текущая подписка: %s</string>
<string name="price_per_month_abbreviated">$%s/мес.</string>
<string name="price_per_month">$%s/месяц</string>
<string name="price_per_year_abbreviated">$%s/год</string>
<string name="price_per_year">$%s/год</string>
<string name="insufficient_subscription">Недостаточный уровень подписки. Обновите подписку, чтобы возобновить обслуживание.</string>
<string name="your_subscription_expired">Срок действия вашей подписки истек. Подпишитесь сейчас, чтобы возобновить обслуживание.</string>

@ -222,6 +222,8 @@
<string name="chip_style_outlined">கோடிட்டுக் காட்டப்பட்டுள்ளது</string>
<string name="chip_style">சிப் பாணி</string>
<string name="back">மீண்டும்</string>
<string name="upgrade_blurb_2">நான் பணிகளில் ஆயிரக்கணக்கான மணிநேரங்களை செலவிட்டேன், மேலும் மூலக் குறியீடு அனைத்தையும் ஆன்லைனில் இலவசமாக வெளியிடுகிறேன். எனது பணியை ஆதரிக்க சில அம்சங்களுக்கு சந்தா தேவை</string>
<string name="upgrade_blurb_1">வணக்கம்! என் பெயர் அலெக்ஸ். பணிகளுக்குப் பின்னால் உள்ள சுயாதீன டெவலப்பர் நான்</string>
<string name="color_wheel">வண்ண சக்கரம்</string>
<string name="invalid_username_or_password">தவறான பயனர்பெயர் அல்லது கடவுச்சொல்</string>
<string name="more_notification_settings_summary">ரிங்டோன், அதிர்வுகள் மற்றும் பல</string>

@ -492,6 +492,8 @@
<string name="invalid_username_or_password">Geçersiz kullanıcı adı veya parola</string>
<string name="theme_system_default">Sistem öntanımlısı</string>
<string name="color_wheel">Renk tekeri</string>
<string name="upgrade_blurb_1">Hey! Ben Alex. Tasks\'ın arkasındaki bağımsız geliştiriciyim</string>
<string name="upgrade_blurb_2">Binlerce saatimi Tasks\'ta çalışarak geçirdim, kaynak kodun tümünü çevrim içi olarak ücretsiz yayımladım. Çalışmamı desteklemek için bazı özellikler abonelik gerektirir</string>
<string name="upgrade_blurb_4">Desteğiniz çok şey ifade ediyor, teşekkürler!</string>
<string name="back">Geri</string>
<string name="chip_style">Yonga biçimi</string>
@ -618,9 +620,7 @@
<string name="background_location_permission_required">Tasks, kapalıyken veya kullanılmazken de konuma dayalı anımsatmaları etkinleştirmek için konum verisi toplar.</string>
<string name="follow_reddit">r/tasks topluluğuna katıl</string>
<string name="current_subscription">Geçerli abonelik: %s</string>
<string name="price_per_month_abbreviated">%s$/ay</string>
<string name="price_per_month">%s$/ay</string>
<string name="price_per_year_abbreviated">%s$/yıl</string>
<string name="price_per_year">%s$/yıl</string>
<string name="insufficient_subscription">Yetersiz abonelik düzeyi. Hizmeti sürdürmek için lütfen aboneliğinizi yükseltin.</string>
<string name="your_subscription_expired">Aboneliğinizin süresi doldu. Hizmeti sürdürmek için şimdi abone olun.</string>

@ -531,6 +531,8 @@
<string name="chip_style">Стиль індикаторів списків</string>
<string name="back">Назад</string>
<string name="upgrade_blurb_4">Ваша підтримка багато означає для мене. Дякую!</string>
<string name="upgrade_blurb_2">Я витратив тисячі годин, працюючи над Tasks, і я публікую весь код онлайн безоплатно. Щоб підтримати мою роботу, деякі функції потребують підписки</string>
<string name="upgrade_blurb_1">Привіт! Моє ім\'я Алекс. Я - незалежний розробник Tasks</string>
<string name="color_wheel">Палітра</string>
<string name="invalid_username_or_password">Невірне ім\'я користувача або пароль</string>
<string name="more_notification_settings_summary">Мелодія, вібрація та інше</string>
@ -566,8 +568,6 @@
<string name="authorization_cancelled">Авторизацію скасовано</string>
<string name="follow_reddit">Долучитися до r/tasks</string>
<string name="current_subscription">Поточна підписка: %s</string>
<string name="price_per_year_abbreviated">%s $/рік</string>
<string name="price_per_month_abbreviated">%s$/міс.</string>
<string name="price_per_month">%s $ на місяць</string>
<string name="price_per_year">%s $ на рік</string>
<string name="no_google_play_subscription">Відповідної вимогам підписки Google Play не знайдено</string>

@ -483,6 +483,8 @@
<string name="invalid_username_or_password">无效的用户名或密码</string>
<string name="theme_system_default">系统默认</string>
<string name="color_wheel">给滚轮着色</string>
<string name="upgrade_blurb_1">你好我叫Alex是Tasks背后的独立开发者</string>
<string name="upgrade_blurb_2">我已经花了数千个小时用于开发Tasks并且在网上免费发布了所有源代码。 为了支持我的工作,某些功能需要订阅</string>
<string name="upgrade_blurb_4">您的支持对我很重要,谢谢!</string>
<string name="back">返回</string>
<string name="chip_style">流式布局样式</string>
@ -607,9 +609,7 @@
<string name="custom_filter_is_subtask">是子任务</string>
<string name="custom_filter_has_subtask">有子任务</string>
<string name="current_subscription">当前订阅:%s</string>
<string name="price_per_month_abbreviated">$%s/月</string>
<string name="price_per_month">$%s/月</string>
<string name="price_per_year_abbreviated">$%s/年</string>
<string name="price_per_year">$%s/年</string>
<string name="insufficient_subscription">订阅等级不够。请升级你的订阅来恢复服务。</string>
<string name="your_subscription_expired">你的订阅已到期。现在订阅恢复服务。</string>

@ -345,9 +345,7 @@
<string name="not_signed_in">未登入</string>
<string name="authorization_cancelled">取消授權</string>
<string name="current_subscription">目前訂閱: %s</string>
<string name="price_per_month_abbreviated">$%s/每月</string>
<string name="price_per_month">$%s/每月</string>
<string name="price_per_year_abbreviated">$%s/每年</string>
<string name="custom_filter_is_subtask">為副工作</string>
<string name="custom_filter_has_subtask">有副工作</string>
<string name="backups_ignore_warnings">忽略警告</string>

@ -149,7 +149,6 @@
<color name="divider">@color/black_12</color>
<color name="default_chip_background">@color/grey_300</color>
<color name="default_chip_text_color">@color/black_87</color>
<color name="purchase_highlight">@color/basil</color>
<color name="github_sponsor">#eb52ae</color>

@ -32,19 +32,6 @@
<string name="url_app_passwords">https://tasks.org/passwords</string>
<string name="url_sharing">https://tasks.org/sharing</string>
<!-- Eventually these should be moved to strings.xml for translation -->
<string name="upgrade_sync_self_hosted">Sync with third-party apps and services</string>
<string name="upgrade_third_party_apps">Compatible with Outlook, Thunderbird, Apple Reminders, and more</string>
<string name="upgrade_coming_soon">Many new features coming soon!</string>
<string name="upgrade_google_tasks">Multiple Google Task accounts</string>
<string name="upgrade_additional_features">Unlock additional features</string>
<string name="upgrade_themes">All themes, colors, and icons</string>
<string name="upgrade_google_places">Improved location search with Google Places</string>
<string name="upgrade_tasker">Tasker plugins</string>
<string name="upgrade_free_trial">7-day free trial for new subscribers</string>
<string name="upgrade_downgrade">Upgrade, downgrade, or cancel your subscription at any time</string>
<string name="upgrade_support_development">Your subscription supports open source software!</string>
<string name="p_date_shortcut_morning">date_shortcut_morning</string>
<string name="p_date_shortcut_afternoon">date_shortcut_afternoon</string>
<string name="p_date_shortcut_evening">date_shortcut_evening</string>

@ -536,8 +536,8 @@ File %1$s contained %2$s.\n\n
<string name="tasks_org_share">Share lists with other users</string>
<string name="google_tasks_selection_description">Basic service that synchronizes with your Google account</string>
<string name="caldav_selection_description">Synchronization based on open internet standards</string>
<string name="etesync_selection_description">Open source, end-to-end encrypted synchronization</string>
<string name="decsync_selection_description">Synchronize your tasks with the DecSync CC app</string>
<string name="etesync_selection_description">End-to-end encrypted synchronization</string>
<string name="decsync_selection_description">File-based synchronization</string>
<string name="davx5_selection_description">Synchronize your tasks with the DAVx⁵ app</string>
<string name="show_advanced_settings">Show advanced settings</string>
<string name="caldav_account_description">Requires an account with a CalDAV service provider or a self-hosted server. Find a service provider by visiting tasks.org/caldav</string>
@ -555,6 +555,9 @@ File %1$s contained %2$s.\n\n
<string name="more_notification_settings_summary">Ringtone, vibrations, and more</string>
<string name="invalid_username_or_password">Invalid username or password</string>
<string name="color_wheel">Color wheel</string>
<string name="upgrade_blurb_1">Hi! My name is Alex. I am the independent developer behind Tasks</string>
<string name="upgrade_blurb_2">I have spent thousands of hours working on Tasks, and I publish all of the source code online for free. In order to support my work some features require a subscription</string>
<string name="subscription_benefits">Subscription benefits</string>
<string name="upgrade_blurb_4">Your support means a lot to me, thank you!</string>
<string name="back">Back</string>
<string name="chip_style">Chip style</string>
@ -612,6 +615,7 @@ File %1$s contained %2$s.\n\n
<string name="filter_eisenhower_box_3">Not important and urgent</string>
<string name="filter_eisenhower_box_4">Not important and not urgent</string>
<string name="enjoying_tasks">Enjoying Tasks?</string>
<string name="tasks_needs_your_support">Tasks needs your support!</string>
<string name="tell_me_how_im_doing">Please tell me how I\'m doing</string>
<string name="support_development_subscribe">Unlock additional features and support open source software</string>
<string name="no_thanks">No thanks</string>
@ -648,9 +652,7 @@ File %1$s contained %2$s.\n\n
<string name="insufficient_sponsorship">No eligible GitHub sponsorship found</string>
<string name="no_google_play_subscription">No eligible Google Play subscription found</string>
<string name="price_per_year">$%s/year</string>
<string name="price_per_year_abbreviated">$%s/yr</string>
<string name="price_per_month">$%s/month</string>
<string name="price_per_month_abbreviated">$%s/mo</string>
<string name="current_subscription">Current subscription: %s</string>
<string name="follow_reddit">Join r/tasks</string>
<string name="follow_twitter">Follow @tasks_org</string>
@ -703,4 +705,18 @@ File %1$s contained %2$s.\n\n
<string name="picker_mode_calendar">Calendar</string>
<string name="picker_mode_clock">Clock</string>
<string name="picker_mode_text">Text</string>
<string name="pro_free_trial">New subscribers receive a 7-day free trial. Cancel at any time</string>
<string name="upgrade_more_customization">More customization</string>
<string name="upgrade_more_customization_description">Unlock all themes, colors, and icons</string>
<string name="upgrade_customization_themes">Wallpaper and day/night themes</string>
<string name="upgrade_customization_colors">Launcher icons, color palettes, and color wheel</string>
<string name="upgrade_customization_icons">List icons</string>
<string name="upgrade_google_tasks">Synchronize multiple accounts</string>
<string name="upgrade_tasks_org_account_description">Sync with Tasks.org and collaborate with other users</string>
<string name="upgrade_desktop_access">Desktop access</string>
<string name="upgrade_desktop_access_description">Sync with third-party clients like Outlook and Apple Reminders</string>
<string name="upgrade_open_source_description">Your subscription supports continued development</string>
<string name="upgrade_automation">Automation</string>
<string name="upgrade_automation_description">Plugins for Tasker, Automate, and Locale</string>
<string name="more_options">More options</string>
</resources>

@ -292,10 +292,6 @@
++--- io.noties.markwon:core:4.6.2
+| +--- androidx.annotation:annotation:1.1.0 -> 1.2.0-beta01
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- io.noties.markwon:ext-strikethrough:4.6.2
+| +--- io.noties.markwon:core:4.6.2 (*)
+| \--- com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:0.13.0
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- com.jakewharton:butterknife:10.2.3
+| \--- com.jakewharton:butterknife-runtime:10.2.3
+| +--- com.jakewharton:butterknife-annotations:10.2.3

@ -406,10 +406,6 @@
++--- io.noties.markwon:core:4.6.2
+| +--- androidx.annotation:annotation:1.1.0 -> 1.2.0-beta01
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- io.noties.markwon:ext-strikethrough:4.6.2
+| +--- io.noties.markwon:core:4.6.2 (*)
+| \--- com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:0.13.0
+| \--- com.atlassian.commonmark:commonmark:0.13.0
++--- com.jakewharton:butterknife:10.2.3
+| \--- com.jakewharton:butterknife-runtime:10.2.3
+| +--- com.jakewharton:butterknife-annotations:10.2.3

Loading…
Cancel
Save