Merge pull request #101 from nsh07/play-paywall
Add in-app purchase in the Google Play version of the app
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.billing
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* Google Play implementation of BillingManager
|
||||
*/
|
||||
class FossBillingManager : BillingManager {
|
||||
override val isPlus = MutableStateFlow(true).asStateFlow()
|
||||
override val isLoaded = MutableStateFlow(true).asStateFlow()
|
||||
}
|
||||
|
||||
object BillingManagerProvider {
|
||||
val manager: BillingManager = FossBillingManager()
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.billing
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||
|
||||
@Composable
|
||||
fun TomatoPlusPaywallDialog(
|
||||
isPlus: Boolean,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
BackHandler(enabled = true, onDismiss)
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(colorScheme.surface)
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
painterResource(R.drawable.bmc),
|
||||
null,
|
||||
tint = colorScheme.onSurface
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
stringResource(R.string.tomato_foss),
|
||||
style = typography.headlineSmall,
|
||||
fontFamily = robotoFlexTopBar,
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
stringResource(R.string.tomato_foss_desc, "BuyMeACoffee"),
|
||||
textAlign = TextAlign.Center,
|
||||
color = colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 24.dp)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Button(onClick = { uriHandler.openUri("https://coff.ee/nsh07") }) {
|
||||
Text("Buy Me A Coffee")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.billing
|
||||
|
||||
import android.content.Context
|
||||
|
||||
fun initializePurchases(context: Context) {}
|
||||
@@ -30,12 +30,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.nsh07.pomodoro.ui.AppScreen
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
||||
import org.nsh07.pomodoro.utils.toColor
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private val timerViewModel: TimerViewModel by viewModels(factoryProducer = { TimerViewModel.Factory })
|
||||
private val settingsViewModel: SettingsViewModel by viewModels(factoryProducer = { SettingsViewModel.Factory })
|
||||
|
||||
private val appContainer by lazy {
|
||||
@@ -62,6 +60,20 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
val seed = preferencesState.colorScheme.toColor()
|
||||
|
||||
val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle()
|
||||
val isPurchaseStateLoaded by settingsViewModel.isPurchaseStateLoaded.collectAsStateWithLifecycle()
|
||||
val isSettingsLoaded by settingsViewModel.isSettingsLoaded.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(isPurchaseStateLoaded, isPlus, isSettingsLoaded) {
|
||||
if (isPurchaseStateLoaded && isSettingsLoaded) {
|
||||
if (!isPlus) {
|
||||
settingsViewModel.resetPaywalledSettings()
|
||||
} else {
|
||||
settingsViewModel.reloadSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TomatoTheme(
|
||||
darkTheme = darkTheme,
|
||||
seedColor = seed,
|
||||
@@ -73,7 +85,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
AppScreen(
|
||||
timerViewModel = timerViewModel,
|
||||
isPlus = isPlus,
|
||||
isAODEnabled = preferencesState.aodEnabled,
|
||||
setTimerFrequency = {
|
||||
appContainer.appTimerRepository.timerFrequency = it
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import org.nsh07.pomodoro.billing.initializePurchases
|
||||
import org.nsh07.pomodoro.data.AppContainer
|
||||
import org.nsh07.pomodoro.data.DefaultAppContainer
|
||||
|
||||
@@ -12,6 +30,8 @@ class TomatoApplication : Application() {
|
||||
super.onCreate()
|
||||
container = DefaultAppContainer(this)
|
||||
|
||||
initializePurchases(this)
|
||||
|
||||
val notificationChannel = NotificationChannel(
|
||||
"timer",
|
||||
getString(R.string.timer_progress),
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.billing
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface BillingManager {
|
||||
val isPlus: StateFlow<Boolean>
|
||||
val isLoaded: StateFlow<Boolean>
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.billing.BillingManager
|
||||
import org.nsh07.pomodoro.billing.BillingManagerProvider
|
||||
import org.nsh07.pomodoro.service.addTimerActions
|
||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||
import org.nsh07.pomodoro.utils.millisecondsToStr
|
||||
@@ -34,6 +36,7 @@ interface AppContainer {
|
||||
val appPreferenceRepository: AppPreferenceRepository
|
||||
val appStatRepository: AppStatRepository
|
||||
val appTimerRepository: AppTimerRepository
|
||||
val billingManager: BillingManager
|
||||
val notificationManager: NotificationManagerCompat
|
||||
val notificationManagerService: NotificationManager
|
||||
val notificationBuilder: NotificationCompat.Builder
|
||||
@@ -54,6 +57,8 @@ class DefaultAppContainer(context: Context) : AppContainer {
|
||||
|
||||
override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() }
|
||||
|
||||
override val billingManager: BillingManager by lazy { BillingManagerProvider.manager }
|
||||
|
||||
override val notificationManager: NotificationManagerCompat by lazy {
|
||||
NotificationManagerCompat.from(context)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
@@ -41,8 +43,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
@@ -54,6 +58,7 @@ import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
import org.nsh07.pomodoro.billing.TomatoPlusPaywallDialog
|
||||
import org.nsh07.pomodoro.service.TimerService
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
||||
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
||||
@@ -65,10 +70,11 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AppScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
|
||||
isAODEnabled: Boolean,
|
||||
setTimerFrequency: (Float) -> Unit
|
||||
isPlus: Boolean,
|
||||
setTimerFrequency: (Float) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -91,6 +97,7 @@ fun AppScreen(
|
||||
}
|
||||
}
|
||||
|
||||
var showPaywall by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
@@ -157,6 +164,7 @@ fun AppScreen(
|
||||
entry<Screen.Timer> {
|
||||
TimerScreen(
|
||||
timerState = uiState,
|
||||
isPlus = isPlus,
|
||||
progress = { progress },
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
@@ -218,6 +226,7 @@ fun AppScreen(
|
||||
|
||||
entry<Screen.Settings.Main> {
|
||||
SettingsScreenRoot(
|
||||
setShowPaywall = { showPaywall = it },
|
||||
modifier = modifier.padding(
|
||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||
@@ -240,4 +249,12 @@ fun AppScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
showPaywall,
|
||||
enter = slideInVertically { it },
|
||||
exit = slideOutVertically { it }
|
||||
) {
|
||||
TomatoPlusPaywallDialog(isPlus = isPlus) { showPaywall = false }
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.nsh07.pomodoro.ui.settingsScreen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -67,6 +68,7 @@ import org.nsh07.pomodoro.service.TimerService
|
||||
import org.nsh07.pomodoro.ui.Screen
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusPromo
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings
|
||||
@@ -80,6 +82,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreenRoot(
|
||||
setShowPaywall: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
|
||||
) {
|
||||
@@ -102,6 +105,7 @@ fun SettingsScreenRoot(
|
||||
viewModel.longBreakTimeTextFieldState
|
||||
}
|
||||
|
||||
val isPlus by viewModel.isPlus.collectAsStateWithLifecycle()
|
||||
val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
|
||||
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
|
||||
val dndEnabled by viewModel.dndEnabled.collectAsStateWithLifecycle(false)
|
||||
@@ -119,6 +123,7 @@ fun SettingsScreenRoot(
|
||||
}
|
||||
|
||||
SettingsScreen(
|
||||
isPlus = isPlus,
|
||||
preferencesState = preferencesState,
|
||||
backStack = backStack,
|
||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||
@@ -143,13 +148,16 @@ fun SettingsScreenRoot(
|
||||
},
|
||||
onThemeChange = viewModel::saveTheme,
|
||||
onColorSchemeChange = viewModel::saveColorScheme,
|
||||
setShowPaywall = setShowPaywall,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("LocalContextGetResourceValueCall")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
private fun SettingsScreen(
|
||||
isPlus: Boolean,
|
||||
preferencesState: PreferencesState,
|
||||
backStack: SnapshotStateList<Screen.Settings>,
|
||||
focusTimeInputFieldState: TextFieldState,
|
||||
@@ -168,6 +176,7 @@ private fun SettingsScreen(
|
||||
onAlarmSoundChanged: (Uri?) -> Unit,
|
||||
onThemeChange: (String) -> Unit,
|
||||
onColorSchemeChange: (Color) -> Unit,
|
||||
setShowPaywall: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -217,10 +226,20 @@ private fun SettingsScreen(
|
||||
) {
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
|
||||
item { AboutCard() }
|
||||
if (!isPlus) item {
|
||||
PlusPromo(isPlus, setShowPaywall)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
}
|
||||
|
||||
item { AboutCard(isPlus) }
|
||||
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
|
||||
if (isPlus) item {
|
||||
PlusPromo(isPlus, setShowPaywall)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
}
|
||||
|
||||
itemsIndexed(settingsScreens) { index, item ->
|
||||
ClickableListItem(
|
||||
leadingContent = {
|
||||
@@ -266,14 +285,17 @@ private fun SettingsScreen(
|
||||
entry<Screen.Settings.Appearance> {
|
||||
AppearanceSettings(
|
||||
preferencesState = preferencesState,
|
||||
isPlus = isPlus,
|
||||
onBlackThemeChange = onBlackThemeChange,
|
||||
onThemeChange = onThemeChange,
|
||||
onColorSchemeChange = onColorSchemeChange,
|
||||
setShowPaywall = setShowPaywall,
|
||||
onBack = backStack::removeLastOrNull
|
||||
)
|
||||
}
|
||||
entry<Screen.Settings.Timer> {
|
||||
TimerSettings(
|
||||
isPlus = isPlus,
|
||||
aodEnabled = preferencesState.aodEnabled,
|
||||
dndEnabled = dndEnabled,
|
||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||
@@ -282,7 +304,8 @@ private fun SettingsScreen(
|
||||
sessionsSliderState = sessionsSliderState,
|
||||
onAodEnabledChange = onAodEnabledChange,
|
||||
onDndEnabledChange = onDndEnabledChange,
|
||||
onBack = backStack::removeLastOrNull
|
||||
setShowPaywall = setShowPaywall,
|
||||
onBack = backStack::removeLastOrNull,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,10 @@ import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||
|
||||
// Taken from https://github.com/shub39/Grit/blob/master/app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
|
||||
@Composable
|
||||
fun AboutCard(modifier: Modifier = Modifier) {
|
||||
fun AboutCard(
|
||||
isPlus: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -77,7 +80,8 @@ fun AboutCard(modifier: Modifier = Modifier) {
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.app_name),
|
||||
if (!isPlus) stringResource(R.string.app_name)
|
||||
else stringResource(R.string.app_name_plus),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontFamily = robotoFlexTopBar
|
||||
)
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
package org.nsh07.pomodoro.ui.settingsScreen.components
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -38,6 +40,7 @@ import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -56,6 +59,7 @@ fun ColorSchemePickerListItem(
|
||||
color: Color,
|
||||
items: Int,
|
||||
index: Int,
|
||||
isPlus: Boolean,
|
||||
onColorChange: (Color) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -65,6 +69,7 @@ fun ColorSchemePickerListItem(
|
||||
Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e),
|
||||
Color.White
|
||||
)
|
||||
val zeroCorner = remember { CornerSize(0) }
|
||||
|
||||
Column(
|
||||
modifier
|
||||
@@ -76,45 +81,49 @@ fun ColorSchemePickerListItem(
|
||||
}
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painterResource(R.drawable.colors),
|
||||
null
|
||||
)
|
||||
},
|
||||
headlineContent = { Text("Dynamic color") },
|
||||
supportingContent = { Text("Adapt theme colors from your wallpaper") },
|
||||
trailingContent = {
|
||||
val checked = color == colorSchemes.last()
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
if (it) onColorChange(colorSchemes.last())
|
||||
else onColorChange(colorSchemes.first())
|
||||
},
|
||||
thumbContent = {
|
||||
if (checked) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.check),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.clear),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = switchColors
|
||||
)
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(middleListItemShape)
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painterResource(R.drawable.colors),
|
||||
null
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(R.string.dynamic_color)) },
|
||||
supportingContent = { Text(stringResource(R.string.dynamic_color_desc)) },
|
||||
trailingContent = {
|
||||
val checked = color == colorSchemes.last()
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
if (it) onColorChange(colorSchemes.last())
|
||||
else onColorChange(colorSchemes.first())
|
||||
},
|
||||
enabled = isPlus,
|
||||
thumbContent = {
|
||||
if (checked) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.check),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.clear),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = switchColors
|
||||
)
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(middleListItemShape)
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
}
|
||||
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
@@ -131,24 +140,31 @@ fun ColorSchemePickerListItem(
|
||||
)
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(middleListItemShape)
|
||||
modifier = Modifier.clip(
|
||||
RoundedCornerShape(
|
||||
topStart = middleListItemShape.topStart,
|
||||
topEnd = middleListItemShape.topEnd,
|
||||
zeroCorner,
|
||||
zeroCorner
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 48.dp),
|
||||
userScrollEnabled = isPlus,
|
||||
modifier = Modifier
|
||||
.background(listItemColors.containerColor)
|
||||
.padding(bottom = 8.dp)
|
||||
) {
|
||||
LazyRow(contentPadding = PaddingValues(horizontal = 48.dp)) {
|
||||
items(colorSchemes.dropLast(1)) {
|
||||
ColorPickerButton(
|
||||
it,
|
||||
it == color,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
) {
|
||||
onColorChange(it)
|
||||
}
|
||||
items(colorSchemes.dropLast(1)) {
|
||||
ColorPickerButton(
|
||||
color = it,
|
||||
isSelected = it == color,
|
||||
enabled = isPlus,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
) {
|
||||
onColorChange(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,12 +176,17 @@ fun ColorSchemePickerListItem(
|
||||
fun ColorPickerButton(
|
||||
color: Color,
|
||||
isSelected: Boolean,
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
IconButton(
|
||||
shapes = IconButtonDefaults.shapes(),
|
||||
colors = IconButtonDefaults.iconButtonColors(containerColor = color),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
containerColor = color,
|
||||
disabledContainerColor = color.copy(0.3f)
|
||||
),
|
||||
enabled = enabled,
|
||||
modifier = modifier.size(48.dp),
|
||||
onClick = onClick
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.ui.settingsScreen.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun PlusDivider(
|
||||
setShowPaywall: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = modifier.padding(vertical = 14.dp)) {
|
||||
HorizontalDivider(modifier = Modifier.clip(CircleShape), thickness = 4.dp)
|
||||
Button(
|
||||
onClick = { setShowPaywall(true) },
|
||||
modifier = Modifier
|
||||
.background(colorScheme.surfaceContainer)
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Text("Customize further with Tomato+", style = typography.titleSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.ui.settingsScreen.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||
|
||||
@Composable
|
||||
fun PlusPromo(
|
||||
isPlus: Boolean,
|
||||
setShowPaywall: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val container = if (isPlus) colorScheme.surfaceBright else colorScheme.primary
|
||||
val onContainer = if (isPlus) colorScheme.onSurface else colorScheme.onPrimary
|
||||
val onContainerVariant = if (isPlus) colorScheme.onSurfaceVariant else colorScheme.onPrimary
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.background(container)
|
||||
.padding(16.dp)
|
||||
.clickable { setShowPaywall(true) }
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.tomato_logo_notification),
|
||||
null,
|
||||
tint = onContainerVariant,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
if (!isPlus) stringResource(R.string.get_plus)
|
||||
else stringResource(R.string.app_name_plus),
|
||||
style = typography.titleLarge,
|
||||
fontFamily = robotoFlexTopBar,
|
||||
color = onContainer
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Icon(
|
||||
painterResource(R.drawable.arrow_forward_big),
|
||||
null,
|
||||
tint = onContainerVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||
|
||||
@@ -69,11 +70,13 @@ fun ThemePickerListItem(
|
||||
Column(
|
||||
modifier
|
||||
.clip(
|
||||
when (index) {
|
||||
0 -> topListItemShape
|
||||
items - 1 -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
},
|
||||
if (items > 1)
|
||||
when (index) {
|
||||
0 -> topListItemShape
|
||||
items - 1 -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
}
|
||||
else cardShape,
|
||||
),
|
||||
) {
|
||||
ListItem(
|
||||
|
||||
@@ -48,6 +48,7 @@ import androidx.compose.ui.unit.dp
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||
@@ -55,16 +56,18 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||
import org.nsh07.pomodoro.utils.toColor
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AppearanceSettings(
|
||||
preferencesState: PreferencesState,
|
||||
isPlus: Boolean,
|
||||
onBlackThemeChange: (Boolean) -> Unit,
|
||||
onThemeChange: (String) -> Unit,
|
||||
onColorSchemeChange: (Color) -> Unit,
|
||||
setShowPaywall: (Boolean) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -100,22 +103,26 @@ fun AppearanceSettings(
|
||||
item {
|
||||
Spacer(Modifier.height(14.dp))
|
||||
}
|
||||
item {
|
||||
ColorSchemePickerListItem(
|
||||
color = preferencesState.colorScheme.toColor(),
|
||||
items = 3,
|
||||
index = 0,
|
||||
onColorChange = onColorSchemeChange
|
||||
)
|
||||
}
|
||||
item {
|
||||
ThemePickerListItem(
|
||||
theme = preferencesState.theme,
|
||||
onThemeChange = onThemeChange,
|
||||
items = if (isPlus) 3 else 1,
|
||||
index = 0
|
||||
)
|
||||
}
|
||||
|
||||
if (!isPlus) {
|
||||
item { PlusDivider(setShowPaywall) }
|
||||
}
|
||||
|
||||
item {
|
||||
ColorSchemePickerListItem(
|
||||
color = preferencesState.colorScheme.toColor(),
|
||||
items = 3,
|
||||
index = 1,
|
||||
modifier = Modifier
|
||||
.clip(middleListItemShape)
|
||||
index = if (isPlus) 1 else 0,
|
||||
isPlus = isPlus,
|
||||
onColorChange = onColorSchemeChange,
|
||||
)
|
||||
}
|
||||
item {
|
||||
@@ -136,6 +143,7 @@ fun AppearanceSettings(
|
||||
Switch(
|
||||
checked = item.checked,
|
||||
onCheckedChange = { item.onClick(it) },
|
||||
enabled = isPlus,
|
||||
thumbContent = {
|
||||
if (item.checked) {
|
||||
Icon(
|
||||
@@ -168,11 +176,15 @@ fun AppearanceSettings(
|
||||
@Composable
|
||||
fun AppearanceSettingsPreview() {
|
||||
val preferencesState = PreferencesState()
|
||||
AppearanceSettings(
|
||||
preferencesState = preferencesState,
|
||||
onBlackThemeChange = {},
|
||||
onThemeChange = {},
|
||||
onColorSchemeChange = {},
|
||||
onBack = {}
|
||||
)
|
||||
TomatoTheme {
|
||||
AppearanceSettings(
|
||||
preferencesState = preferencesState,
|
||||
isPlus = false,
|
||||
onBlackThemeChange = {},
|
||||
onThemeChange = {},
|
||||
onColorSchemeChange = {},
|
||||
setShowPaywall = {},
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ import androidx.compose.ui.unit.dp
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
|
||||
@@ -88,6 +89,7 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun TimerSettings(
|
||||
isPlus: Boolean,
|
||||
aodEnabled: Boolean,
|
||||
dndEnabled: Boolean,
|
||||
focusTimeInputFieldState: TextFieldState,
|
||||
@@ -97,7 +99,8 @@ fun TimerSettings(
|
||||
onAodEnabledChange: (Boolean) -> Unit,
|
||||
onDndEnabledChange: (Boolean) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
setShowPaywall: (Boolean) -> Unit
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||
val context = LocalContext.current
|
||||
@@ -260,7 +263,8 @@ fun TimerSettings(
|
||||
)
|
||||
}
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
itemsIndexed(switchItems) { index, item ->
|
||||
|
||||
itemsIndexed(if (isPlus) switchItems else switchItems.take(1)) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
@@ -295,15 +299,60 @@ fun TimerSettings(
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(
|
||||
when (index) {
|
||||
if (isPlus) when (index) {
|
||||
0 -> topListItemShape
|
||||
switchItems.size - 1 -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
}
|
||||
else cardShape
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (!isPlus) {
|
||||
item {
|
||||
PlusDivider(setShowPaywall)
|
||||
}
|
||||
itemsIndexed(switchItems.drop(1)) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painterResource(item.icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(item.label)) },
|
||||
supportingContent = { Text(stringResource(item.description)) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = item.checked,
|
||||
onCheckedChange = { item.onClick(it) },
|
||||
enabled = isPlus,
|
||||
thumbContent = {
|
||||
if (item.checked) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.check),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.clear),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = switchColors
|
||||
)
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(cardShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
@@ -352,14 +401,16 @@ private fun TimerSettingsPreview() {
|
||||
steps = 6
|
||||
)
|
||||
TimerSettings(
|
||||
isPlus = false,
|
||||
aodEnabled = true,
|
||||
dndEnabled = false,
|
||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||
sessionsSliderState = sessionsSliderState,
|
||||
aodEnabled = true,
|
||||
dndEnabled = false,
|
||||
onBack = {},
|
||||
onAodEnabledChange = {},
|
||||
onDndEnabledChange = {}
|
||||
onDndEnabledChange = {},
|
||||
setShowPaywall = {},
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
@@ -40,17 +40,25 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.nsh07.pomodoro.TomatoApplication
|
||||
import org.nsh07.pomodoro.billing.BillingManager
|
||||
import org.nsh07.pomodoro.data.AppPreferenceRepository
|
||||
import org.nsh07.pomodoro.data.TimerRepository
|
||||
import org.nsh07.pomodoro.ui.Screen
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
|
||||
class SettingsViewModel(
|
||||
private val billingManager: BillingManager,
|
||||
private val preferenceRepository: AppPreferenceRepository,
|
||||
private val timerRepository: TimerRepository,
|
||||
) : ViewModel() {
|
||||
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
|
||||
|
||||
val isPlus = billingManager.isPlus
|
||||
val isPurchaseStateLoaded = billingManager.isLoaded
|
||||
|
||||
private val _isSettingsLoaded = MutableStateFlow(false)
|
||||
val isSettingsLoaded = _isSettingsLoaded.asStateFlow()
|
||||
|
||||
private val _preferencesState = MutableStateFlow(PreferencesState())
|
||||
val preferencesState = _preferencesState.asStateFlow()
|
||||
|
||||
@@ -90,23 +98,8 @@ class SettingsViewModel(
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val theme = preferenceRepository.getStringPreference("theme")
|
||||
?: preferenceRepository.saveStringPreference("theme", "auto")
|
||||
val colorScheme = preferenceRepository.getStringPreference("color_scheme")
|
||||
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
|
||||
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
|
||||
?: preferenceRepository.saveBooleanPreference("black_theme", false)
|
||||
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
|
||||
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
|
||||
|
||||
_preferencesState.update { currentState ->
|
||||
currentState.copy(
|
||||
theme = theme,
|
||||
colorScheme = colorScheme,
|
||||
blackTheme = blackTheme,
|
||||
aodEnabled = aodEnabled
|
||||
)
|
||||
}
|
||||
reloadSettings()
|
||||
_isSettingsLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,14 +224,46 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun resetPaywalledSettings() {
|
||||
_preferencesState.update { currentState ->
|
||||
currentState.copy(
|
||||
aodEnabled = false,
|
||||
blackTheme = false,
|
||||
colorScheme = Color.White.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reloadSettings() {
|
||||
val theme = preferenceRepository.getStringPreference("theme")
|
||||
?: preferenceRepository.saveStringPreference("theme", "auto")
|
||||
val colorScheme = preferenceRepository.getStringPreference("color_scheme")
|
||||
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
|
||||
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
|
||||
?: preferenceRepository.saveBooleanPreference("black_theme", false)
|
||||
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
|
||||
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
|
||||
|
||||
_preferencesState.update { currentState ->
|
||||
currentState.copy(
|
||||
theme = theme,
|
||||
colorScheme = colorScheme,
|
||||
blackTheme = blackTheme,
|
||||
aodEnabled = aodEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
val application = (this[APPLICATION_KEY] as TomatoApplication)
|
||||
val appPreferenceRepository = application.container.appPreferenceRepository
|
||||
val appTimerRepository = application.container.appTimerRepository
|
||||
val appBillingManager = application.container.billingManager
|
||||
|
||||
SettingsViewModel(
|
||||
billingManager = appBillingManager,
|
||||
preferenceRepository = appPreferenceRepository,
|
||||
timerRepository = appTimerRepository,
|
||||
)
|
||||
|
||||
@@ -107,6 +107,7 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||
@Composable
|
||||
fun SharedTransitionScope.TimerScreen(
|
||||
timerState: TimerState,
|
||||
isPlus: Boolean,
|
||||
progress: () -> Float,
|
||||
onAction: (TimerAction) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -159,7 +160,8 @@ fun SharedTransitionScope.TimerScreen(
|
||||
when (it) {
|
||||
TimerMode.BRAND ->
|
||||
Text(
|
||||
stringResource(R.string.app_name),
|
||||
if (!isPlus) stringResource(R.string.app_name)
|
||||
else stringResource(R.string.app_name_plus),
|
||||
style = TextStyle(
|
||||
fontFamily = robotoFlexTopBar,
|
||||
fontSize = 32.sp,
|
||||
@@ -552,6 +554,7 @@ fun TimerScreenPreview() {
|
||||
SharedTransitionLayout {
|
||||
TimerScreen(
|
||||
timerState,
|
||||
isPlus = true,
|
||||
{ 0.3f },
|
||||
{}
|
||||
)
|
||||
|
||||
@@ -81,4 +81,10 @@
|
||||
<string name="sound">Sound</string>
|
||||
<string name="dnd">Do Not Disturb</string>
|
||||
<string name="dnd_desc">Turn on DND when running a Focus timer</string>
|
||||
<string name="app_name_plus">Tomato+</string>
|
||||
<string name="get_plus">Get Tomato+</string>
|
||||
<string name="dynamic_color">Dynamic color</string>
|
||||
<string name="dynamic_color_desc">Adapt theme colors from your wallpaper</string>
|
||||
<string name="tomato_foss">Tomato FOSS</string>
|
||||
<string name="tomato_foss_desc">All features are unlocked in this version. If my app made a difference in your life, please consider supporting me by donating on %1$s.</string>
|
||||
</resources>
|
||||
21
app/src/play/AndroidManifest.xml
Normal file
21
app/src/play/AndroidManifest.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2025 Nishant Mishra
|
||||
~
|
||||
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
~
|
||||
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
~ General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
~ License, or (at your option) any later version.
|
||||
~
|
||||
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
~ Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with Tomato.
|
||||
~ If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.billing
|
||||
|
||||
import android.util.Log
|
||||
import com.revenuecat.purchases.Purchases
|
||||
import com.revenuecat.purchases.getCustomerInfoWith
|
||||
import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
private const val ENTITLEMENT_ID = "plus"
|
||||
|
||||
/**
|
||||
* Google Play implementation of BillingManager
|
||||
*/
|
||||
class PlayBillingManager : BillingManager {
|
||||
private val _isPlus = MutableStateFlow(false)
|
||||
override val isPlus = _isPlus.asStateFlow()
|
||||
|
||||
private val _isLoaded = MutableStateFlow(false)
|
||||
override val isLoaded = _isLoaded.asStateFlow()
|
||||
|
||||
private val purchases by lazy { Purchases.sharedInstance }
|
||||
|
||||
init {
|
||||
purchases.updatedCustomerInfoListener =
|
||||
UpdatedCustomerInfoListener { customerInfo ->
|
||||
_isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true
|
||||
}
|
||||
|
||||
// Fetch initial customer info
|
||||
purchases.getCustomerInfoWith(
|
||||
onSuccess = { customerInfo ->
|
||||
_isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true
|
||||
_isLoaded.value = true
|
||||
},
|
||||
onError = { error ->
|
||||
Log.e("GooglePlayPaywallManager", "Error fetching customer info: $error")
|
||||
_isLoaded.value = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object BillingManagerProvider {
|
||||
val manager: BillingManager = PlayBillingManager()
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.billing
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.revenuecat.purchases.ui.revenuecatui.Paywall
|
||||
import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
|
||||
import com.revenuecat.purchases.ui.revenuecatui.customercenter.CustomerCenter
|
||||
import org.nsh07.pomodoro.R
|
||||
|
||||
@Composable
|
||||
fun TomatoPlusPaywallDialog(
|
||||
isPlus: Boolean,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val paywallOptions = remember {
|
||||
PaywallOptions.Builder(dismissRequest = onDismiss).build()
|
||||
}
|
||||
|
||||
Scaffold { innerPadding ->
|
||||
if (!isPlus) {
|
||||
Paywall(paywallOptions)
|
||||
|
||||
FilledTonalIconButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.arrow_back),
|
||||
null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CustomerCenter(onDismiss = onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.billing
|
||||
|
||||
import android.content.Context
|
||||
import com.revenuecat.purchases.Purchases
|
||||
import com.revenuecat.purchases.PurchasesConfiguration
|
||||
|
||||
fun initializePurchases(context: Context) {
|
||||
Purchases.configure(
|
||||
PurchasesConfiguration
|
||||
.Builder(context, "goog_jBpRIBjTYvhKYluCqkPXSHbuFbX")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user