Beast mode banner

pull/1826/head
Alex Baker 2 years ago
parent 1de4b220c3
commit 46a6996982

@ -5,6 +5,7 @@ import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import at.bitfire.cert4android.CustomCertManager.Companion.resetCertificates import at.bitfire.cert4android.CustomCertManager.Companion.resetCertificates
import com.todoroo.andlib.utility.DateUtilities.now
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
@ -12,13 +13,17 @@ import org.tasks.billing.BillingClient
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.toast
import org.tasks.injection.InjectingPreferenceFragment import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.preferences.Preferences
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.min
@AndroidEntryPoint @AndroidEntryPoint
class Debug : InjectingPreferenceFragment() { class Debug : InjectingPreferenceFragment() {
@Inject lateinit var inventory: Inventory @Inject lateinit var inventory: Inventory
@Inject lateinit var billingClient: BillingClient @Inject lateinit var billingClient: BillingClient
@Inject lateinit var preferences: Preferences
override fun getPreferenceXml() = R.xml.preferences_debug override fun getPreferenceXml() = R.xml.preferences_debug
@ -54,6 +59,15 @@ class Debug : InjectingPreferenceFragment() {
findPreference(R.string.debug_crash_app).setOnPreferenceClickListener { findPreference(R.string.debug_crash_app).setOnPreferenceClickListener {
throw RuntimeException("Crashed app from debug preferences") throw RuntimeException("Crashed app from debug preferences")
} }
findPreference(R.string.debug_clear_hints).setOnPreferenceClickListener {
preferences.installDate =
min(preferences.installDate, now() - TimeUnit.DAYS.toMillis(14))
preferences.lastSubscribeRequest = 0L
preferences.lastReviewRequest = 0L
preferences.shownBeastModeHint = false
true
}
} }
private fun setupIap(@StringRes prefId: Int, sku: String) { private fun setupIap(@StringRes prefId: Int, sku: String) {

@ -14,5 +14,6 @@
<string name="debug_crash_app">Crash app now</string> <string name="debug_crash_app">Crash app now</string>
<string name="debug_main_queries">Crash on violation</string> <string name="debug_main_queries">Crash on violation</string>
<string name="debug_force_restart">Restart app</string> <string name="debug_force_restart">Restart app</string>
<string name="debug_clear_hints">Clear hints</string>
<string name="google_oauth_scheme">com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf</string> <string name="google_oauth_scheme">com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf</string>
</resources> </resources>

@ -49,4 +49,8 @@
<Preference <Preference
android:key="@string/debug_tasker"/> android:key="@string/debug_tasker"/>
<Preference
android:key="@string/debug_clear_hints"
android:title="@string/debug_clear_hints" />
</PreferenceScreen> </PreferenceScreen>

@ -7,6 +7,7 @@ package com.todoroo.astrid.activity
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Paint import android.graphics.Paint
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -16,7 +17,14 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -25,6 +33,7 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback
import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
@ -43,6 +52,7 @@ import kotlinx.coroutines.withContext
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.compose.BeastModeBanner
import org.tasks.data.Alarm import org.tasks.data.Alarm
import org.tasks.data.Location import org.tasks.data.Location
import org.tasks.data.TagData import org.tasks.data.TagData
@ -225,6 +235,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
.launchIn(viewLifecycleOwner.lifecycleScope) .launchIn(viewLifecycleOwner.lifecycleScope)
} }
@OptIn(ExperimentalAnimationApi::class)
private suspend fun process(event: TaskEditEvent) { private suspend fun process(event: TaskEditEvent) {
when (event) { when (event) {
is TaskEditEvent.Discard -> is TaskEditEvent.Discard ->
@ -234,6 +245,37 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
} }
private val beastMode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val transaction = childFragmentManager.beginTransaction()
taskEditControlSetFragmentManager.getOrCreateFragments(childFragmentManager).forEach {
transaction.remove(it)
}
transaction.commit()
activity?.recreate()
}
@OptIn(ExperimentalAnimationApi::class)
private fun showBeastModeHint() {
binding.banner.setContent {
var visible by rememberSaveable { mutableStateOf(true) }
val context = LocalContext.current
MdcTheme {
BeastModeBanner(
visible,
showSettings = {
visible = false
preferences.shownBeastModeHint = true
beastMode.launch(Intent(context, BeastModePreferences::class.java))
},
dismiss = {
visible = false
preferences.shownBeastModeHint = true
}
)
}
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -242,6 +284,9 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.title, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(binding.title, InputMethodManager.SHOW_IMPLICIT)
} }
if (!preferences.shownBeastModeHint) {
showBeastModeHint()
}
} }
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {

@ -24,8 +24,10 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.view.forEach import androidx.core.view.forEach
@ -89,7 +91,7 @@ import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.billing.PurchaseActivity import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.AnimatedBanner import org.tasks.compose.SubscriptionNagBanner
import org.tasks.data.CaldavDao import org.tasks.data.CaldavDao
import org.tasks.data.TagDataDao import org.tasks.data.TagDataDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
@ -186,17 +188,17 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
?.show() ?.show()
is TaskListEvent.BegForSubscription -> { is TaskListEvent.BegForSubscription -> {
binding.banner.setContent { binding.banner.setContent {
val showBanner = rememberSaveable { mutableStateOf(true) } var showBanner by rememberSaveable { mutableStateOf(true) }
MdcTheme { MdcTheme {
AnimatedBanner( SubscriptionNagBanner(
isVisible = showBanner, visible = showBanner,
dismiss = {
preferences.lastSubscribeRequest = now()
showBanner.value = false
},
subscribe = { subscribe = {
showBanner = false
purchase() purchase()
showBanner.value = false },
dismiss = {
showBanner = false
preferences.lastSubscribeRequest = now()
}, },
) )
} }

@ -1,5 +1,6 @@
package org.tasks.compose package org.tasks.compose
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
@ -16,51 +17,60 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay import com.google.android.material.composethemeadapter.MdcTheme
import org.tasks.R import org.tasks.R
import org.tasks.Tasks.Companion.IS_GENERIC import org.tasks.Tasks
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun AnimatedBanner( fun AnimatedBanner(
isVisible: MutableState<Boolean>, visible: Boolean,
dismiss: () -> Unit, content: @Composable () -> Unit,
subscribe: () -> Unit, buttons: @Composable () -> Unit,
) { ) {
var show by rememberSaveable { mutableStateOf(false) }
if (isVisible.value) {
LaunchedEffect(key1 = isVisible, block = {
delay(500)
show = true
})
} else {
show = false
}
AnimatedVisibility( AnimatedVisibility(
visible = show, visible = visible,
enter = expandVertically( enter = expandVertically(),
expandFrom = Alignment.Top
),
exit = shrinkVertically(), exit = shrinkVertically(),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
) { ) {
Divider()
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
content()
Row(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp)
.align(Alignment.End),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
buttons()
}
Divider()
}
}
}
@ExperimentalAnimationApi
@Composable
fun SubscriptionNagBanner(
visible: Boolean,
subscribe: () -> Unit,
dismiss: () -> Unit,
) {
AnimatedBanner(
visible = visible,
content = {
Text( Text(
text = stringResource( text = stringResource(
id = if (IS_GENERIC) { id = if (Tasks.IS_GENERIC) {
R.string.enjoying_tasks R.string.enjoying_tasks
} else { } else {
R.string.tasks_needs_your_support R.string.tasks_needs_your_support
@ -72,7 +82,7 @@ fun AnimatedBanner(
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = stringResource( text = stringResource(
id = if (IS_GENERIC) { id = if (Tasks.IS_GENERIC) {
R.string.tasks_needs_your_support R.string.tasks_needs_your_support
} else { } else {
R.string.support_development_subscribe R.string.support_development_subscribe
@ -81,28 +91,69 @@ fun AnimatedBanner(
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
) )
Row( },
modifier = Modifier buttons = {
.padding(vertical = 8.dp, horizontal = 16.dp) TextButton(onClick = subscribe) {
.align(Alignment.End),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(onClick = dismiss) {
Text(text = stringResource(id = R.string.dismiss)) Text(text = stringResource(id = R.string.dismiss))
} }
TextButton(onClick = subscribe) { TextButton(onClick = dismiss) {
Text( val res = if (Tasks.IS_GENERIC) {
text = stringResource(
id = if (IS_GENERIC) {
R.string.TLA_menu_donate R.string.TLA_menu_donate
} else { } else {
R.string.button_subscribe R.string.button_subscribe
} }
Text(text = stringResource(id = res))
}
}
) )
}
@ExperimentalAnimationApi
@Composable
fun BeastModeBanner(
visible: Boolean,
showSettings: () -> Unit,
dismiss: () -> Unit,
) {
AnimatedBanner(
visible = visible,
content = {
Text(
text = stringResource(id = R.string.hint_customize_edit_title),
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(horizontal = 16.dp),
) )
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.hint_customize_edit_body),
style = MaterialTheme.typography.body2,
modifier = Modifier.padding(horizontal = 16.dp),
)
},
buttons = {
TextButton(onClick = dismiss) {
Text(text = stringResource(id = R.string.dismiss))
} }
TextButton(onClick = showSettings) {
Text(text = stringResource(id = R.string.TLA_menu_settings))
} }
Divider()
} }
)
} }
@ExperimentalAnimationApi
@Preview(showBackground = true)
@Preview(showBackground = true, backgroundColor = 0x202124, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BeastModePreview() = MdcTheme {
BeastModeBanner(visible = true, showSettings = {}, dismiss = {})
} }
@ExperimentalAnimationApi
@Preview(showBackground = true)
@Preview(showBackground = true, backgroundColor = 0x202124, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SubscriptionNagPreview() = MdcTheme {
SubscriptionNagBanner(visible = true, subscribe = {}, dismiss = {})
}

@ -540,6 +540,10 @@ class Preferences @JvmOverloads constructor(
get() = getLong(R.string.p_last_subscribe_request, 0L) get() = getLong(R.string.p_last_subscribe_request, 0L)
set(value) = setLong(R.string.p_last_subscribe_request, value) set(value) = setLong(R.string.p_last_subscribe_request, value)
var shownBeastModeHint: Boolean
get() = getBoolean(R.string.p_shown_beast_mode_hint, false)
set(value) = setBoolean(R.string.p_shown_beast_mode_hint, value)
companion object { companion object {
private const val PREF_SORT_SORT = "sort_sort" // $NON-NLS-1$ private const val PREF_SORT_SORT = "sort_sort" // $NON-NLS-1$

@ -66,6 +66,11 @@
android:orientation="vertical" android:orientation="vertical"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:scrollbarStyle="outsideOverlay" android:scrollbarStyle="outsideOverlay"
android:layout_width="fill_parent" android:layout_width="fill_parent"

@ -439,4 +439,5 @@
<string name="p_app_bar_collapse">app_bar_collapse</string> <string name="p_app_bar_collapse">app_bar_collapse</string>
<string name="p_completed_tasks_at_bottom">completed_tasks_at_bottom</string> <string name="p_completed_tasks_at_bottom">completed_tasks_at_bottom</string>
<string name="p_completed_tasks_sort">completed_tasks_sort</string> <string name="p_completed_tasks_sort">completed_tasks_sort</string>
<string name="p_shown_beast_mode_hint">shown_beast_mode_hint</string>
</resources> </resources>

@ -735,4 +735,6 @@ File %1$s contained %2$s.\n\n
<string name="caldav_server_type">Server type</string> <string name="caldav_server_type">Server type</string>
<string name="filter_snoozed">Snoozed</string> <string name="filter_snoozed">Snoozed</string>
<string name="dismiss">Dismiss</string> <string name="dismiss">Dismiss</string>
<string name="hint_customize_edit_title">Too much information?</string>
<string name="hint_customize_edit_body">You can customize this screen by rearranging or removing fields</string>
</resources> </resources>

Loading…
Cancel
Save