mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
donations: at the top of Settings, and a popup on Home after the third drive that worked, then at most every three months
This commit is contained in:
1 parent
71397a0777
commit
3042e01e75
4 files changed
+124
-4
No files matched your search
@@ -228,6 +228,7 @@ object SessionReport {
|
|||||||
}
|
}
|
||||||
if (outcome == "connected") runCatching {
|
if (outcome == "connected") runCatching {
|
||||||
app.seb3thehacker.gearslip.stats.WorkedPrompt.onDriveWorked(context, headUnitInfo, protocolVersion, carScreen, carDpi)
|
app.seb3thehacker.gearslip.stats.WorkedPrompt.onDriveWorked(context, headUnitInfo, protocolVersion, carScreen, carDpi)
|
||||||
|
app.seb3thehacker.gearslip.ui.DonatePrompt.onDriveWorked(context)
|
||||||
}
|
}
|
||||||
runCatching { app.seb3thehacker.gearslip.update.UpdateChecker.onDriveEnded(context) }
|
runCatching { app.seb3thehacker.gearslip.update.UpdateChecker.onDriveEnded(context) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package app.seb3thehacker.gearslip.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import app.seb3thehacker.gearslip.GearslipLog
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The donation ask on Home, after a drive that worked. A driver sees it first after their third
|
||||||
|
* such drive, so Gearslip has earned it, and then at most once every three months. Tapping either
|
||||||
|
* donate button ends it for good.
|
||||||
|
*/
|
||||||
|
object DonatePrompt {
|
||||||
|
|
||||||
|
private const val PREFS = "donate_prompt"
|
||||||
|
private const val KEY_DRIVES = "drives"
|
||||||
|
private const val KEY_LAST = "last_shown"
|
||||||
|
private const val KEY_DONATED = "donated"
|
||||||
|
private const val KEY_WAITING = "waiting"
|
||||||
|
private const val FIRST_AFTER = 3
|
||||||
|
private const val GAP_MS = 90L * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
private var loaded = false
|
||||||
|
private val flow = MutableStateFlow(false)
|
||||||
|
|
||||||
|
/** True while the ask is waiting for the driver to open Home. */
|
||||||
|
fun pending(context: Context): StateFlow<Boolean> {
|
||||||
|
if (!loaded) {
|
||||||
|
loaded = true
|
||||||
|
flow.value = prefs(context).getBoolean(KEY_WAITING, false)
|
||||||
|
}
|
||||||
|
return flow
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A drive that reached the car's screen ended cleanly. Asks if it's time. */
|
||||||
|
fun onDriveWorked(context: Context) {
|
||||||
|
val p = prefs(context)
|
||||||
|
if (p.getBoolean(KEY_DONATED, false)) return
|
||||||
|
val drives = p.getInt(KEY_DRIVES, 0) + 1
|
||||||
|
val due = drives >= FIRST_AFTER && System.currentTimeMillis() - p.getLong(KEY_LAST, 0L) > GAP_MS
|
||||||
|
p.edit().putInt(KEY_DRIVES, drives).apply {
|
||||||
|
if (due) putBoolean(KEY_WAITING, true)
|
||||||
|
}.apply()
|
||||||
|
if (due) {
|
||||||
|
loaded = true
|
||||||
|
flow.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The driver closed the ask; [donated] if they picked a way to give. */
|
||||||
|
fun answer(context: Context, donated: Boolean) {
|
||||||
|
prefs(context).edit()
|
||||||
|
.putBoolean(KEY_WAITING, false)
|
||||||
|
.putLong(KEY_LAST, System.currentTimeMillis())
|
||||||
|
.apply { if (donated) putBoolean(KEY_DONATED, true) }
|
||||||
|
.apply()
|
||||||
|
loaded = true
|
||||||
|
flow.value = false
|
||||||
|
GearslipLog.i(if (donated) "donate prompt: opened a donation link" else "donate prompt: not now")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shows the donation ask over Home when one is waiting and the car isn't connected. */
|
||||||
|
@Composable
|
||||||
|
internal fun DonateDialog(live: Boolean) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val waiting by remember { DonatePrompt.pending(context) }.collectAsStateWithLifecycle()
|
||||||
|
if (!waiting || live) return
|
||||||
|
fun give(url: String) {
|
||||||
|
DonatePrompt.answer(context, donated = true)
|
||||||
|
GearslipLinks.open(context, url)
|
||||||
|
}
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { DonatePrompt.answer(context, donated = false) },
|
||||||
|
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
|
||||||
|
title = { Text("Glad that drive worked") },
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
"Gearslip is free, with no ads and no tracking. If it gets you on the road, a " +
|
||||||
|
"donation keeps it going.",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Button(onClick = { give(GearslipLinks.SPONSORS) }, Modifier.fillMaxWidth()) {
|
||||||
|
Text("Donate on GitHub")
|
||||||
|
}
|
||||||
|
OutlinedButton(onClick = { give(GearslipLinks.LIBERAPAY) }, Modifier.fillMaxWidth()) {
|
||||||
|
Text("Donate on Liberapay")
|
||||||
|
}
|
||||||
|
TextButton(onClick = { DonatePrompt.answer(context, donated = false) }, Modifier.fillMaxWidth()) {
|
||||||
|
Text("Not now")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -144,6 +144,8 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
// Sharing just went on from the "it worked" ask: a little thanks.
|
// Sharing just went on from the "it worked" ask: a little thanks.
|
||||||
if (celebrate) ConfettiFall(onDone = WorkedPrompt::celebrated)
|
if (celebrate) ConfettiFall(onDone = WorkedPrompt::celebrated)
|
||||||
|
// The usage notes ask comes first; the donation ask waits until that's answered.
|
||||||
|
if (workedCar == null) DonateDialog(live)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ fun SettingsScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(0.dp))
|
Spacer(Modifier.height(0.dp))
|
||||||
|
SettingsSection(
|
||||||
|
"Support Gearslip",
|
||||||
|
footer = "Gearslip is free, with no ads and no tracking. Donations keep it going.",
|
||||||
|
) { DonateRows() }
|
||||||
ConnectionSection()
|
ConnectionSection()
|
||||||
HelpSection(onOpenHelp, onOpenCarPreview, onOpenLogs)
|
HelpSection(onOpenHelp, onOpenCarPreview, onOpenLogs)
|
||||||
CarAudioSection()
|
CarAudioSection()
|
||||||
@@ -131,10 +135,6 @@ fun SettingsScreen(
|
|||||||
) { UsageStatsRows() }
|
) { UsageStatsRows() }
|
||||||
SettingsSection("Updates") { UpdateRow() }
|
SettingsSection("Updates") { UpdateRow() }
|
||||||
CertificateSection()
|
CertificateSection()
|
||||||
SettingsSection(
|
|
||||||
"Support Gearslip",
|
|
||||||
footer = "Gearslip is free, with no ads and no tracking. Donations keep it going.",
|
|
||||||
) { DonateRows() }
|
|
||||||
AboutSection(onOpenWhatsNew, onReplayTutorial, onReplayIntro)
|
AboutSection(onOpenWhatsNew, onReplayTutorial, onReplayIntro)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user