.
================================================
FILE: README.md
================================================
Vanilla
Vanilla (formerly CuteCalc) is a cute and elegant calculator app for Android !
---
👀 Overview
- Material 3 Expressive design!
- Very lightweight! (~5Mb app size)
- No permissions needed!
- Very fast and feature-rich!
---
🤔 Why ?
I am 15 y/o and have been into computers ever since I was ~10, growing up, I always thought about how software could do anything someone could dream of, so I started learning multiple languages until stepping upon Kotlin. Since then, I've learnt and started to build Android apps, and CuteCalc is my first project upon, I hope, alot more.
---
💬 Contact Me
[Discord server](https://discord.gg/c6aCu4yjbu)
[Email](sosauce_dev@protonmail.com)
---
❤️ Support
If you wish to support me, you can see how to do so on [my website](https://sosauce.github.io/support/)
---
⚠️ Copyright
Copyright (c)2026 sosauce
This program 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.
This program 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.
The above copyright notice, this permission notice, and its license shall be included in all copies or substantial portions of the Software.
You can find a copy of the GNU General Public License v3 [here](https://www.gnu.org/licenses/)
---
#### You can find the SHA-256 [here](https://sosauce.github.io/projects/)
================================================
FILE: app/.gitignore
================================================
/build
/release
================================================
FILE: app/build.gradle.kts
================================================
import com.android.build.api.variant.VariantOutputConfiguration
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.ksp)
}
androidComponents {
onVariants(selector().withBuildType("release")) { variant ->
val mainOutput = variant.outputs.single { it.outputType == VariantOutputConfiguration.OutputType.SINGLE }
@Suppress("UnstableApiUsage")
mainOutput.outputFileName = "Vanilla_${mainOutput.versionName.get()}.apk"
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
android {
namespace = "com.sosauce.vanilla"
compileSdk = 37
defaultConfig {
applicationId = "com.sosauce.cutecalc"
minSdk = 23
targetSdk = 37
versionCode = 50002
versionName = "4.0.2"
ndk {
//noinspection ChromeOsAbiSupport
abiFilters += arrayOf("arm64-v8a", "armeabi-v7a")
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
compose = true
aidl = false
shaders = false
buildConfig = false
resValues = false
viewBinding = false
}
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.material3)
implementation(libs.androidx.ui)
implementation(libs.androidx.datastore.preferences)
implementation(libs.keval)
implementation(libs.androidx.room.ktx)
implementation(libs.squircle.shape)
ksp(libs.androidx.room.compiler)
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/com/sosauce/vanilla/MainActivity.kt
================================================
package com.sosauce.vanilla
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.sosauce.vanilla.data.datastore.rememberAppTheme
import com.sosauce.vanilla.data.datastore.rememberShowOnLockScreen
import com.sosauce.vanilla.ui.navigation.Nav
import com.sosauce.vanilla.ui.theme.CuteCalcTheme
import com.sosauce.vanilla.utils.CuteTheme
import com.sosauce.vanilla.utils.showOnLockScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen()
enableEdgeToEdge()
setContent {
val isSystemInDarkTheme = isSystemInDarkTheme()
val theme by rememberAppTheme()
val showOnLockScreen by rememberShowOnLockScreen()
showOnLockScreen(showOnLockScreen)
CuteCalcTheme {
WindowCompat
.getInsetsController(window, window.decorView)
.apply {
val isLight =
if (theme == CuteTheme.SYSTEM) !isSystemInDarkTheme else theme == CuteTheme.LIGHT
isAppearanceLightStatusBars = isLight
isAppearanceLightNavigationBars = isLight
}
Nav()
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/data/actions/CalcAction.kt
================================================
package com.sosauce.vanilla.data.actions
sealed interface CalcAction {
data object GetResult : CalcAction
data object ResetField : CalcAction
data object Backspace : CalcAction
data class AddToField(
val char: Char
) : CalcAction
data class AddExpressionToField(
val expression: String
) : CalcAction
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/data/calculator/Evaluator.kt
================================================
package com.sosauce.vanilla.data.calculator
import com.notkamui.keval.Keval
import com.notkamui.keval.KevalInvalidArgumentException
import com.notkamui.keval.KevalInvalidExpressionException
import com.notkamui.keval.KevalZeroDivisionException
import java.math.RoundingMode
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.sqrt
class NegativeSquareRootException : RuntimeException("Be for real 3:<")
class ValueTooLargeException : RuntimeException("Value too large")
object Evaluator {
private val KEVAL = Keval.create {
binaryOperator {
symbol = Tokens.ADD
precedence = 2
isLeftAssociative = true
implementation = { a, b -> a + b }
}
unaryOperator {
symbol = Tokens.ADD
isPrefix = true
implementation = { it }
}
binaryOperator {
symbol = Tokens.SUBTRACT
precedence = 2
isLeftAssociative = true
implementation = { a, b -> a - b }
}
unaryOperator {
symbol = Tokens.SUBTRACT
isPrefix = true
implementation = { -it }
}
binaryOperator {
symbol = Tokens.MULTIPLY
precedence = 3
isLeftAssociative = true
implementation = { a, b -> a * b }
}
binaryOperator {
symbol = Tokens.DIVIDE
precedence = 3
isLeftAssociative = true
implementation = { a, b ->
if (b == 0.0) throw KevalZeroDivisionException()
a / b
}
}
binaryOperator {
symbol = Tokens.POWER
precedence = 4
isLeftAssociative = false
implementation = { a, b -> a.pow(b) }
}
unaryOperator {
symbol = Tokens.FACTORIAL
isPrefix = false
implementation = {
if (it < 0) throw KevalInvalidArgumentException("Factorial of a negative number")
if (floor(it) != it) throw KevalInvalidArgumentException("Factorial of a non-integer")
(1..it.toInt()).fold(1.0) { acc, i -> acc * i }
}
}
unaryOperator {
symbol = Tokens.SQUARE_ROOT
isPrefix = true
implementation =
{ arg -> if (arg < 0) throw NegativeSquareRootException() else sqrt(arg) }
}
unaryOperator {
symbol = Tokens.MODULO
isPrefix = false
implementation = { arg -> arg / 100 }
}
constant {
name = "PI"
value = Math.PI
}
}
private var prevResult: String = ""
@JvmStatic
fun eval(
formula: String,
precision: Int
): String = try {
val result = KEVAL
.eval(formula.replace(Tokens.PI.toString(), "PI").handleRelativePercentage())
val formattedResult = if (result > Double.MAX_VALUE) {
throw ValueTooLargeException()
} else {
result
.toBigDecimal()
.setScale(precision, RoundingMode.HALF_UP)
.stripTrailingZeros()
.toPlainString()
}
prevResult = formattedResult
formattedResult
} catch (e: KevalInvalidExpressionException) {
prevResult
} catch (e: Exception) {
e.message ?: "Undetermined error"
}
// We don't call "handleRelativePercentage" here to avoid recursive call
@JvmStatic
private fun evalParenthesis(formula: String): String {
val result = KEVAL.eval(formula)
return if (result > Double.MAX_VALUE) {
throw ValueTooLargeException()
} else {
result.toBigDecimal().stripTrailingZeros().toPlainString()
}
}
private fun String.handleRelativePercentage(): String {
return relativePercentageRegex.replace(this.processParenthesisExpression()) { match ->
val firstOperand = match.groupValues[1].toDouble()
val operator = match.groupValues[2]
val percentage = match.groupValues[3].toDouble()
when (operator) {
"+" -> "$firstOperand + ($firstOperand * $percentage / 100)"
"-" -> "$firstOperand - ($firstOperand * $percentage / 100)"
"*" -> "$firstOperand * ($percentage / 100)"
else -> "$firstOperand"
}
}
}
private fun String.processParenthesisExpression(): String {
var expression = this
parenthesisRegex.findAll(this).forEach { matchResult ->
val calculated = evalParenthesis(matchResult.value)
val replaceWith = if (this.contains("%")) calculated else "($calculated)"
expression = expression.replace(matchResult.value, replaceWith)
}
return expression
}
private val parenthesisRegex = Regex("""\(([^()]+)\)""")
private val relativePercentageRegex = Regex("""(\d+(?:\.\d+)?)\s*([+\-*])\s*(\d+(?:\.\d+)?)%""")
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/data/calculator/Tokens.kt
================================================
package com.sosauce.vanilla.data.calculator
object Tokens {
const val ONE = '1'
const val TWO = '2'
const val THREE = '3'
const val FOUR = '4'
const val FIVE = '5'
const val SIX = '6'
const val SEVEN = '7'
const val EIGHT = '8'
const val NINE = '9'
const val ZERO = '0'
const val FACTORIAL = '!'
const val SQUARE_ROOT = '√'
const val PI = 'π'
const val MODULO = '%'
const val OPEN_PARENTHESIS = '('
const val CLOSED_PARENTHESIS = ')'
const val POWER = '^'
const val DIVIDE = '/'
const val ADD = '+'
const val SUBTRACT = '-'
const val MULTIPLY = '×'
const val DECIMAL = '.'
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/data/datastore/DataStore.kt
================================================
package com.sosauce.vanilla.data.datastore
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.sosauce.vanilla.utils.CuteTheme
val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
data object PreferencesKeys {
val THEME = stringPreferencesKey("theme")
val BUTTON_VIBRATION_ENABLED = booleanPreferencesKey("button_vibration_enabled")
val DECIMAL_FORMATTING = booleanPreferencesKey("decimal_formatting")
val ENABLE_HISTORY = booleanPreferencesKey("enable_history")
val HISTORY_MAX_ITEMS = longPreferencesKey("HISTORY_MAX_ITEMS")
val SAVE_ERRORS_TO_HISTORY = booleanPreferencesKey("SAVE_ERRORS_TO_HISTORY")
val USE_BUTTONS_ANIMATIONS = booleanPreferencesKey("use_buttons_animation")
val USE_SYSTEM_FONT = booleanPreferencesKey("use_system_font")
val SHOW_CLEAR_BUTTON = booleanPreferencesKey("show_clear_button")
val DECIMAL_PRECISION = intPreferencesKey("DECIMAL_PRECISION")
val SHOW_ON_LOCKSCREEN = booleanPreferencesKey("SHOW_ON_LOCKSCREEN")
val HISTORY_NEWEST_FIRST = booleanPreferencesKey("HISTORY_NEWEST_FIRST")
val COLORED_OPERATORS = booleanPreferencesKey("COLORED_OPERATORS")
val SWAP_ZERO_AND_DECIMAL = booleanPreferencesKey("SWAP_ZERO_AND_DECIMAL")
}
@Composable
fun rememberVibration() =
rememberPreference(
key = PreferencesKeys.BUTTON_VIBRATION_ENABLED,
defaultValue = false
)
@Composable
fun rememberAppTheme() =
rememberPreference(
key = PreferencesKeys.THEME,
defaultValue = CuteTheme.SYSTEM
)
@Composable
fun rememberDecimal() =
rememberPreference(
key = PreferencesKeys.DECIMAL_FORMATTING,
defaultValue = false
)
@Composable
fun rememberUseHistory() =
rememberPreference(
key = PreferencesKeys.ENABLE_HISTORY,
defaultValue = true
)
@Composable
fun rememberUseButtonsAnimation() =
rememberPreference(
key = PreferencesKeys.USE_BUTTONS_ANIMATIONS,
defaultValue = true
)
@Composable
fun rememberUseSystemFont() =
rememberPreference(
key = PreferencesKeys.USE_SYSTEM_FONT,
defaultValue = false
)
@Composable
fun rememberShowClearButton() =
rememberPreference(
key = PreferencesKeys.SHOW_CLEAR_BUTTON,
defaultValue = true
)
@Composable
fun rememberHistoryMaxItems() =
rememberPreference(
key = PreferencesKeys.HISTORY_MAX_ITEMS,
defaultValue = Long.MAX_VALUE
)
@Composable
fun rememberSaveErrorsToHistory() =
rememberPreference(
key = PreferencesKeys.SAVE_ERRORS_TO_HISTORY,
defaultValue = false
)
@Composable
fun rememberDecimalPrecision() =
rememberPreference(
key = PreferencesKeys.DECIMAL_PRECISION,
defaultValue = 100
)
@Composable
fun rememberShowOnLockScreen() =
rememberPreference(
key = PreferencesKeys.SHOW_ON_LOCKSCREEN,
defaultValue = false
)
@Composable
fun rememberHistoryNewestFirst() =
rememberPreference(
key = PreferencesKeys.HISTORY_NEWEST_FIRST,
defaultValue = true
)
@Composable
fun rememberColoredOperators() =
rememberPreference(
key = PreferencesKeys.COLORED_OPERATORS,
defaultValue = true
)
@Composable
fun rememberSwapZeroAndDecimal() =
rememberPreference(
key = PreferencesKeys.SWAP_ZERO_AND_DECIMAL,
defaultValue = false
)
fun getDecimalPrecision(context: Context) = getPreference(
key = PreferencesKeys.DECIMAL_PRECISION,
defaultValue = 1000,
context = context
)
================================================
FILE: app/src/main/java/com/sosauce/vanilla/data/datastore/SettingsExt.kt
================================================
package com.sosauce.vanilla.data.datastore
import android.content.Context
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@Composable
fun rememberPreference(
key: Preferences.Key,
defaultValue: T,
): MutableState {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val state by remember {
context.dataStore.data
.map { it[key] ?: defaultValue }
}.collectAsStateWithLifecycle(initialValue = defaultValue)
return remember(state) {
object : MutableState {
override var value: T
get() = state
set(value) {
coroutineScope.launch {
context.dataStore.edit {
it[key] = value
}
}
}
override fun component1() = value
override fun component2(): (T) -> Unit = { value = it }
}
}
}
fun getPreference(
key: Preferences.Key,
defaultValue: T,
context: Context
): Flow =
context.dataStore.data
.map { preference ->
preference[key] ?: defaultValue
}
@Composable
fun rememberIsLandscape(): Boolean {
val config = LocalConfiguration.current
return remember(config.orientation) {
config.orientation == Configuration.ORIENTATION_LANDSCAPE
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/data/sysui/QSTile.kt
================================================
package com.sosauce.vanilla.data.sysui
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import com.sosauce.vanilla.MainActivity
@RequiresApi(Build.VERSION_CODES.N)
class QSTile : TileService() {
override fun onClick() {
super.onClick()
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent)
} else {
val newIntent = Intent(intent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(newIntent)
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/domain/model/Calculation.kt
================================================
package com.sosauce.vanilla.domain.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Calculation(
val operation: String,
val result: String,
@PrimaryKey(autoGenerate = true)
val id: Int = 0
)
================================================
FILE: app/src/main/java/com/sosauce/vanilla/domain/repository/HistoryDao.kt
================================================
package com.sosauce.vanilla.domain.repository
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import com.sosauce.vanilla.domain.model.Calculation
import kotlinx.coroutines.flow.Flow
@Dao
interface HistoryDao {
@Insert
suspend fun insertCalculation(calculation: Calculation)
@Delete
suspend fun deleteCalculation(calculation: Calculation)
@Query("DELETE FROM calculation")
suspend fun deleteAllCalculations()
@Query("SELECT * FROM calculation ORDER BY id ASC")
fun getAllCalculations(): Flow>
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/domain/repository/HistoryDatabase.kt
================================================
package com.sosauce.vanilla.domain.repository
import androidx.room.Database
import androidx.room.RoomDatabase
import com.sosauce.vanilla.domain.model.Calculation
@Database(
entities = [Calculation::class],
version = 1
)
abstract class HistoryDatabase : RoomDatabase() {
abstract val dao: HistoryDao
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/domain/repository/HistoryEvents.kt
================================================
package com.sosauce.vanilla.domain.repository
import com.sosauce.vanilla.domain.model.Calculation
sealed interface HistoryEvents {
data class DeleteCalculation(val calculation: Calculation) : HistoryEvents
data object DeleteAllCalculation : HistoryEvents
data class AddCalculation(
val operation: String,
val result: String,
val maxHistoryItems: Long,
val saveErrors: Boolean
) : HistoryEvents
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/domain/repository/HistoryState.kt
================================================
package com.sosauce.vanilla.domain.repository
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.sosauce.vanilla.domain.model.Calculation
data class HistoryState(
val calculations: List = emptyList(),
val operation: MutableState = mutableStateOf(""),
val result: MutableState = mutableStateOf("")
)
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/navigation/Navigation.kt
================================================
package com.sosauce.vanilla.ui.navigation
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.sosauce.vanilla.data.actions.CalcAction
import com.sosauce.vanilla.data.datastore.rememberIsLandscape
import com.sosauce.vanilla.ui.screens.calculator.CalculatorScreen
import com.sosauce.vanilla.ui.screens.calculator.CalculatorScreenLandscape
import com.sosauce.vanilla.ui.screens.calculator.CalculatorViewModel
import com.sosauce.vanilla.ui.screens.history.HistoryScreen
import com.sosauce.vanilla.ui.screens.history.HistoryViewModel
import com.sosauce.vanilla.ui.screens.settings.SettingsScreen
import com.sosauce.vanilla.utils.CalculatorViewModelFactory
import com.sosauce.vanilla.utils.HistoryViewModelFactory
import com.sosauce.vanilla.utils.bouncySpec
import com.sosauce.vanilla.utils.navigationBouncySpec
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
fun Nav() {
val activity = LocalActivity.current!!
val isLandscape = rememberIsLandscape()
val viewModel =
viewModel(factory = CalculatorViewModelFactory(activity.application))
val historyViewModel =
viewModel(factory = HistoryViewModelFactory(activity.application))
var screenToDisplay by rememberSaveable { mutableStateOf(Screens.MAIN) }
val windowInfo = LocalWindowInfo.current
// Mimic back behavior from navigation
BackHandler {
if (screenToDisplay != Screens.MAIN) {
screenToDisplay = Screens.MAIN
} else {
activity.moveTaskToBack(true)
}
}
AnimatedContent(
targetState = screenToDisplay,
transitionSpec = { slideInHorizontally(navigationBouncySpec) { -it } + fadeIn() togetherWith fadeOut() },
modifier = Modifier.background(MaterialTheme.colorScheme.background)
) { screen ->
when (screen) {
Screens.MAIN -> {
// survive config changes without needing a saver
val yTranslation = retain { Animatable(0f) }
val scope = rememberCoroutineScope()
Box(
modifier = Modifier.clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)),
) {
val calculations by historyViewModel.allCalculations.collectAsStateWithLifecycle()
HistoryScreen(
calculations = calculations,
onEvents = historyViewModel::onEvent,
onPutBackToField = { expression ->
viewModel.handleAction(CalcAction.AddExpressionToField(expression))
},
onGotoMain = {
scope.launch {
yTranslation.animateTo(0f, bouncySpec())
}
}
)
if (isLandscape) {
CalculatorScreenLandscape(
modifier = Modifier
.graphicsLayer {
translationY = yTranslation.value
},
viewModel = viewModel,
historyViewModel = historyViewModel,
onNavigate = { screenToDisplay = it },
onGotoHistory = {
scope.launch {
yTranslation.animateTo(windowInfo.containerSize.height.toFloat(), bouncySpec())
}
}
)
} else {
CalculatorScreen(
modifier = Modifier
.graphicsLayer {
translationY = yTranslation.value
},
viewModel = viewModel,
onNavigate = { screenToDisplay = it },
historyViewModel = historyViewModel,
onUpdateDragAmount = { dragAmount ->
val value = (yTranslation.value + dragAmount).coerceAtLeast(0f) // always keep the value positive or else it's a shithole to manage
scope.launch {
yTranslation.snapTo(value)
}
},
onDragStopped = {
if (yTranslation.value.roundToInt() >= windowInfo.containerSize.height / 2) {
yTranslation.animateTo(windowInfo.containerSize.height.toFloat(), bouncySpec())
} else {
yTranslation.animateTo(0f, bouncySpec())
}
}
)
}
}
}
Screens.SETTINGS -> {
SettingsScreen(
onNavigate = { screenToDisplay = it }
)
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/navigation/Screens.kt
================================================
package com.sosauce.vanilla.ui.navigation
enum class Screens {
MAIN,
SETTINGS
}
enum class SettingsScreen {
SETTINGS,
LOOK_AND_FEEL,
HISTORY,
FORMATTING,
MISC
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/calculator/CalculatorScreen.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
package com.sosauce.vanilla.ui.screens.calculator
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
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 androidx.compose.ui.util.fastForEach
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.actions.CalcAction
import com.sosauce.vanilla.data.calculator.Tokens
import com.sosauce.vanilla.data.datastore.rememberHistoryMaxItems
import com.sosauce.vanilla.data.datastore.rememberSaveErrorsToHistory
import com.sosauce.vanilla.data.datastore.rememberShowClearButton
import com.sosauce.vanilla.data.datastore.rememberSwapZeroAndDecimal
import com.sosauce.vanilla.data.datastore.rememberUseHistory
import com.sosauce.vanilla.domain.repository.HistoryEvents
import com.sosauce.vanilla.ui.navigation.Screens
import com.sosauce.vanilla.ui.screens.calculator.components.ButtonType
import com.sosauce.vanilla.ui.screens.calculator.components.CalcButton
import com.sosauce.vanilla.ui.screens.calculator.components.CalculationDisplay
import com.sosauce.vanilla.ui.screens.calculator.components.CuteButton
import com.sosauce.vanilla.ui.screens.history.HistoryViewModel
import com.sosauce.vanilla.utils.BACKSPACE
import com.sosauce.vanilla.utils.PARENTHESES
import com.sosauce.vanilla.utils.whichParenthesis
import kotlinx.coroutines.CoroutineScope
import java.text.DecimalFormatSymbols
@Composable
fun CalculatorScreen(
modifier: Modifier = Modifier,
viewModel: CalculatorViewModel,
historyViewModel: HistoryViewModel,
onNavigate: (Screens) -> Unit,
onUpdateDragAmount: (Float) -> Unit,
onDragStopped: suspend CoroutineScope.(Float) -> Unit
) {
val localeDecimalChar =
remember { DecimalFormatSymbols.getInstance().decimalSeparator.toString() }
val showClearButton by rememberShowClearButton()
val saveErrorsToHistory by rememberSaveErrorsToHistory()
val maxItemsToHistory by rememberHistoryMaxItems()
val saveToHistory by rememberUseHistory()
val swapZeroAndDecimal by rememberSwapZeroAndDecimal()
val row1 = listOf(
CalcButton(
text = "!",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.FACTORIAL)) },
rectangle = true,
type = ButtonType.SPECIAL
),
CalcButton(
text = "%",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.MODULO)) },
rectangle = true,
type = ButtonType.SPECIAL
),
CalcButton(
text = "√",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.SQUARE_ROOT)) },
rectangle = true,
type = ButtonType.SPECIAL
),
CalcButton(
text = "π",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.PI)) },
rectangle = true,
type = ButtonType.SPECIAL
)
)
val row2 = listOf(
if (showClearButton) {
CalcButton(
text = "C",
onClick = { viewModel.handleAction(CalcAction.ResetField) },
type = ButtonType.ACTION
)
} else {
CalcButton(
text = "(",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.OPEN_PARENTHESIS)) },
type = ButtonType.OPERATOR
)
},
if (showClearButton) {
CalcButton(
text = PARENTHESES,
onClick = {
viewModel.handleAction(
CalcAction.AddToField(
viewModel.textFieldState.text.toString().whichParenthesis()
)
)
},
type = ButtonType.OPERATOR
)
} else {
CalcButton(
text = ")",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.CLOSED_PARENTHESIS)) },
type = ButtonType.OPERATOR
)
},
CalcButton(
text = "^",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.POWER)) },
type = ButtonType.OPERATOR
),
CalcButton(
text = "/",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.DIVIDE)) },
type = ButtonType.OPERATOR
)
)
val row3 = listOf(
CalcButton(
text = "7",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.SEVEN)) },
type = ButtonType.OTHER
),
CalcButton(
text = "8",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.EIGHT)) },
type = ButtonType.OTHER
),
CalcButton(
text = "9",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.NINE)) },
type = ButtonType.OTHER
),
CalcButton(
text = "×",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.MULTIPLY)) },
type = ButtonType.OPERATOR
)
)
val row4 = listOf(
CalcButton(
text = "4",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.FOUR)) },
type = ButtonType.OTHER
),
CalcButton(
text = "5",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.FIVE)) },
type = ButtonType.OTHER
),
CalcButton(
text = "6",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.SIX)) },
type = ButtonType.OTHER
),
CalcButton(
text = "-",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.SUBTRACT)) },
type = ButtonType.OPERATOR
)
)
val row5 = listOf(
CalcButton(
text = "1",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.ONE)) },
type = ButtonType.OTHER
),
CalcButton(
text = "2",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.TWO)) },
type = ButtonType.OTHER
),
CalcButton(
text = "3",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.THREE)) },
type = ButtonType.OTHER
),
CalcButton(
text = "+",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.ADD)) },
type = ButtonType.OPERATOR
)
)
val row6 = listOf(
if (!swapZeroAndDecimal) {
CalcButton(
text = "0",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.ZERO)) },
type = ButtonType.OTHER
)
} else {
CalcButton(
text = localeDecimalChar,
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.DECIMAL)) },
type = ButtonType.OTHER
)
},
if (swapZeroAndDecimal) {
CalcButton(
text = "0",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.ZERO)) },
type = ButtonType.OTHER
)
} else {
CalcButton(
text = localeDecimalChar,
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.DECIMAL)) },
type = ButtonType.OTHER
)
},
CalcButton(
text = BACKSPACE,
onClick = { viewModel.handleAction(CalcAction.Backspace) },
onLongClick = { viewModel.handleAction(CalcAction.ResetField) },
type = ButtonType.OTHER
),
CalcButton(
text = "=",
onClick = {
val operation = viewModel.textFieldState.text.toString()
viewModel.handleAction(CalcAction.GetResult)
val result = viewModel.evaluatedCalculation
if (saveToHistory && operation != result) {
historyViewModel.onEvent(
HistoryEvents.AddCalculation(
operation = operation,
result = result,
maxHistoryItems = maxItemsToHistory,
saveErrors = saveErrorsToHistory
)
)
}
},
type = ButtonType.ACTION
)
)
val dragState = rememberDraggableState { dragAmount ->
onUpdateDragAmount(dragAmount)
}
Scaffold(
modifier = modifier,
topBar = {
CenterAlignedTopAppBar(
modifier = Modifier.clip(RoundedCornerShape(topStart = 50.dp, topEnd = 50.dp)),
title = {
BottomSheetDefaults.DragHandle(
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.draggable(
state = dragState,
orientation = Orientation.Vertical,
onDragStopped = onDragStopped
)
)
},
actions = {
// IconButton(
// onClick = {},
// shapes = IconButtonDefaults.shapes()
// ) {
// Icon(
// painter = painterResource(R.drawable.history_rounded),
// contentDescription = stringResource(R.string.history),
// tint = MaterialTheme.colorScheme.onBackground
// )
// }
IconButton(
onClick = { onNavigate(Screens.SETTINGS) },
shapes = IconButtonDefaults.shapes()
) {
Icon(
painter = painterResource(R.drawable.settings_filled),
contentDescription = stringResource(R.string.settings)
)
}
}
)
}
) { pv ->
Column(
modifier = Modifier
.padding(horizontal = 10.dp)
.fillMaxSize()
.padding(pv),
verticalArrangement = Arrangement.Bottom
) {
CalculationDisplay(
modifier = Modifier.weight(1f),
viewModel = viewModel,
onNavigate = onNavigate
)
Spacer(Modifier.height(5.dp))
Column(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(9.dp),
) {
val rows = listOf(row1, row2, row3, row4, row5, row6)
rows.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(9.dp)
) {
row.fastForEach { button ->
key(button.text) {
CuteButton(
modifier = Modifier.weight(1f),
text = button.text,
onClick = button.onClick,
onLongClick = button.onLongClick,
rectangle = button.rectangle,
buttonType = button.type
)
}
}
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/calculator/CalculatorScreenLandscape.kt
================================================
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.calculator
import android.annotation.SuppressLint
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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.actions.CalcAction
import com.sosauce.vanilla.data.calculator.Tokens
import com.sosauce.vanilla.data.datastore.rememberHistoryMaxItems
import com.sosauce.vanilla.data.datastore.rememberSaveErrorsToHistory
import com.sosauce.vanilla.data.datastore.rememberShowClearButton
import com.sosauce.vanilla.data.datastore.rememberUseHistory
import com.sosauce.vanilla.domain.repository.HistoryEvents
import com.sosauce.vanilla.ui.navigation.Screens
import com.sosauce.vanilla.ui.screens.calculator.components.ButtonType
import com.sosauce.vanilla.ui.screens.calculator.components.CalcButton
import com.sosauce.vanilla.ui.screens.calculator.components.CalculationDisplay
import com.sosauce.vanilla.ui.screens.calculator.components.CuteButton
import com.sosauce.vanilla.ui.screens.history.HistoryViewModel
import com.sosauce.vanilla.utils.BACKSPACE
import com.sosauce.vanilla.utils.PARENTHESES
import com.sosauce.vanilla.utils.whichParenthesis
import java.text.DecimalFormatSymbols
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun CalculatorScreenLandscape(
modifier: Modifier = Modifier,
viewModel: CalculatorViewModel,
historyViewModel: HistoryViewModel,
onNavigate: (Screens) -> Unit,
onGotoHistory: () -> Unit
) {
val showClearButton by rememberShowClearButton()
val localeDecimalChar =
remember { DecimalFormatSymbols.getInstance().decimalSeparator.toString() }
val saveErrorsToHistory by rememberSaveErrorsToHistory()
val maxItemsToHistory by rememberHistoryMaxItems()
val saveToHistory by rememberUseHistory()
val row1 = listOf(
CalcButton(
text = "√",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.SQUARE_ROOT)) },
type = ButtonType.OTHER
),
CalcButton(
text = "π",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.PI)) },
type = ButtonType.OTHER
),
CalcButton(
text = "9",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.NINE)) },
type = ButtonType.OTHER
),
CalcButton(
text = "8",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.EIGHT)) },
type = ButtonType.OTHER
),
CalcButton(
text = "7",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.SEVEN)) },
type = ButtonType.OTHER
),
CalcButton(
text = "^",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.POWER)) },
type = ButtonType.OPERATOR
),
if (showClearButton) {
CalcButton(
text = PARENTHESES,
onClick = {
viewModel.handleAction(
CalcAction.AddToField(
viewModel.textFieldState.text.whichParenthesis()
)
)
},
type = ButtonType.OPERATOR
)
} else {
CalcButton(
text = "(",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.OPEN_PARENTHESIS)) },
type = ButtonType.OPERATOR
)
},
if (showClearButton) {
CalcButton(
text = "C",
onClick = { viewModel.handleAction(CalcAction.ResetField) },
type = ButtonType.ACTION
)
} else {
CalcButton(
text = ")",
onClick = {
viewModel.handleAction(
CalcAction.AddToField(Tokens.CLOSED_PARENTHESIS)
)
},
type = ButtonType.OPERATOR
)
}
)
val row2 = listOf(
CalcButton(
text = "%",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.MODULO)) },
type = ButtonType.OTHER
),
CalcButton(
text = "3",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.THREE)) },
type = ButtonType.OTHER
),
CalcButton(
text = "4",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.FOUR)) },
type = ButtonType.OTHER
),
CalcButton(
text = "5",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.FIVE)) },
type = ButtonType.OTHER
),
CalcButton(
text = "6",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.SIX)) },
type = ButtonType.OTHER
),
CalcButton(
text = "+",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.ADD)) },
type = ButtonType.OPERATOR
),
CalcButton(
text = "-",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.SUBTRACT)) },
type = ButtonType.OPERATOR
),
CalcButton(
text = BACKSPACE,
onClick = { viewModel.handleAction(CalcAction.Backspace) },
onLongClick = { viewModel.handleAction(CalcAction.ResetField) },
type = ButtonType.ACTION
)
)
val row3 = listOf(
CalcButton(
text = "!",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.FACTORIAL)) },
type = ButtonType.OTHER
),
CalcButton(
text = "2",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.TWO)) },
type = ButtonType.OTHER
),
CalcButton(
text = "1",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.ONE)) },
type = ButtonType.OTHER
),
CalcButton(
text = "0",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.ZERO)) },
type = ButtonType.OTHER
),
CalcButton(
text = localeDecimalChar,
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.DECIMAL)) },
type = ButtonType.OTHER
),
CalcButton(
text = "×",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.MULTIPLY)) },
type = ButtonType.OPERATOR
),
CalcButton(
text = "/",
onClick = { viewModel.handleAction(CalcAction.AddToField(Tokens.DIVIDE)) },
type = ButtonType.OPERATOR
),
CalcButton(
text = "=",
onClick = {
val operation = viewModel.textFieldState.text.toString()
viewModel.handleAction(CalcAction.GetResult)
val result = viewModel.evaluatedCalculation
if (saveToHistory && operation != result) {
historyViewModel.onEvent(
HistoryEvents.AddCalculation(
operation = operation,
result = result,
maxHistoryItems = maxItemsToHistory,
saveErrors = saveErrorsToHistory
)
)
}
},
type = ButtonType.ACTION
)
)
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets.safeDrawing
) { pv ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(pv),
contentAlignment = Alignment.BottomCenter
) {
Column(
modifier = Modifier
.align(Alignment.TopStart)
.zIndex(1f) //history doesnt click otherwise ?
) {
IconButton(
onClick = { onNavigate(Screens.SETTINGS) },
shapes = IconButtonDefaults.shapes()
) {
Icon(
painter = painterResource(R.drawable.settings_filled),
contentDescription = stringResource(R.string.settings)
)
}
IconButton(
onClick = onGotoHistory,
shapes = IconButtonDefaults.shapes()
) {
Icon(
painter = painterResource(R.drawable.history_rounded),
contentDescription = stringResource(R.string.history)
)
}
}
Column {
CalculationDisplay(
viewModel = viewModel,
onNavigate = onNavigate
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(9.dp),
) {
val rows = listOf(row1, row2, row3)
rows.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(9.dp)
) {
row.fastForEach { button ->
CuteButton(
text = button.text,
onClick = button.onClick,
onLongClick = button.onLongClick,
rectangle = true,
buttonType = button.type
)
}
}
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/calculator/CalculatorViewModel.kt
================================================
package com.sosauce.vanilla.ui.screens.calculator
import android.app.Application
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.sosauce.vanilla.data.actions.CalcAction
import com.sosauce.vanilla.data.calculator.Evaluator
import com.sosauce.vanilla.data.datastore.getDecimalPrecision
import com.sosauce.vanilla.utils.backspace
import com.sosauce.vanilla.utils.insertText
import com.sosauce.vanilla.utils.isErrorMessage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class CalculatorViewModel(
private val application: Application
) : AndroidViewModel(application) {
val textFieldState = TextFieldState()
var evaluatedCalculation by mutableStateOf("")
private set
private val _previewShowErrors = MutableStateFlow(false)
val previewShowErrors = _previewShowErrors.asStateFlow()
init {
viewModelScope.launch {
snapshotFlow { textFieldState.text.toString() }
.collectLatest { text ->
val decimalPrecision =
getDecimalPrecision(application.applicationContext).first()
evaluatedCalculation = if (textFieldState.text.isEmpty()) {
// there's currently a bug that will keep the preview to the last result even if this is empty, my head hurts too much to search a real fix atm
""
} else {
Evaluator.eval(text, decimalPrecision)
}
}
}
}
fun handleAction(action: CalcAction) {
_previewShowErrors.update { false }
when (action) {
is CalcAction.GetResult -> {
if (evaluatedCalculation.isErrorMessage()) {
_previewShowErrors.update { true }
} else {
textFieldState.setTextAndPlaceCursorAtEnd(evaluatedCalculation)
}
}
is CalcAction.AddToField -> textFieldState.insertText(action.char)
is CalcAction.ResetField -> textFieldState.clearText()
is CalcAction.Backspace -> textFieldState.backspace()
is CalcAction.AddExpressionToField -> textFieldState.setTextAndPlaceCursorAtEnd(action.expression)
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/calculator/components/CalcButton.kt
================================================
package com.sosauce.vanilla.ui.screens.calculator.components
data class CalcButton(
val text: String,
val onClick: () -> Unit,
val onLongClick: (() -> Unit)? = null,
val type: ButtonType = ButtonType.OTHER,
val rectangle: Boolean = false
)
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/calculator/components/CalculationDisplay.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.calculator.components
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.OutputTransformation
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sosauce.vanilla.data.calculator.Tokens
import com.sosauce.vanilla.data.datastore.rememberColoredOperators
import com.sosauce.vanilla.data.datastore.rememberDecimal
import com.sosauce.vanilla.data.datastore.rememberUseSystemFont
import com.sosauce.vanilla.ui.navigation.Screens
import com.sosauce.vanilla.ui.screens.calculator.CalculatorViewModel
import com.sosauce.vanilla.ui.theme.nunitoFontFamily
import com.sosauce.vanilla.utils.formatNumber
import com.sosauce.vanilla.utils.isErrorMessage
@Composable
fun CalculationDisplay(
modifier: Modifier = Modifier,
viewModel: CalculatorViewModel,
onNavigate: (Screens) -> Unit
) {
val useSystemFont by rememberUseSystemFont()
val shouldFormat by rememberDecimal()
val scrollState = rememberScrollState()
val previewScrollState = rememberScrollState()
val previewCanShowErrors by viewModel.previewShowErrors.collectAsStateWithLifecycle()
val coloredOperators by rememberColoredOperators()
LaunchedEffect(viewModel.textFieldState.text) {
scrollState.animateScrollTo(scrollState.maxValue)
previewScrollState.animateScrollTo(previewScrollState.maxValue)
}
Column(
modifier = modifier.padding(5.dp),
verticalArrangement = Arrangement.Bottom
) {
Text(
text = viewModel.evaluatedCalculation
.formatNumber(shouldFormat)
.takeIf { !it.isErrorMessage() || previewCanShowErrors } ?: "",
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(previewScrollState),
style = MaterialTheme.typography.displayMediumEmphasized.copy(
textAlign = TextAlign.End,
fontWeight = FontWeight.ExtraBold,
color = if (!viewModel.evaluatedCalculation.isErrorMessage()) {
MaterialTheme.colorScheme.tertiary
} else MaterialTheme.colorScheme.error
)
)
DisableSoftKeyboard {
BasicTextField(
state = viewModel.textFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
textStyle = MaterialTheme.typography.displayMediumEmphasized.copy(
textAlign = TextAlign.End,
color = MaterialTheme.colorScheme.onSurface,
fontFamily = if (!useSystemFont) nunitoFontFamily else null,
fontWeight = FontWeight.ExtraBold
),
modifier = Modifier.fillMaxWidth(),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
scrollState = scrollState,
outputTransformation = CalculatorOutputTransform(
format = shouldFormat,
coloredOperators = coloredOperators,
operatorColor = MaterialTheme.colorScheme.primary
)
)
}
}
}
class CalculatorOutputTransform(
private val format: Boolean,
private val coloredOperators: Boolean,
private val operatorColor: Color
) : OutputTransformation {
override fun TextFieldBuffer.transformOutput() {
if (format) {
val expression = originalText.toString()
if (expression.isEmpty()) return
var shift = 0
NUMBERS_REGEX.findAll(expression).forEach { match ->
val start = match.range.first + shift
val end = match.range.last + 1 + shift
val number = match.value
val formatted = number.formatNumber(true)
replace(start, end, formatted)
shift += formatted.length - number.length
}
}
if (coloredOperators) {
val operators =
setOf(Tokens.ADD, Tokens.SUBTRACT, Tokens.MULTIPLY, Tokens.DIVIDE, Tokens.POWER)
asCharSequence().forEachIndexed { index, char ->
if (char in operators) {
addStyle(SpanStyle(color = operatorColor), index, index + 1)
}
}
}
}
companion object {
val NUMBERS_REGEX = "[\\d.]+".toRegex()
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/calculator/components/CuteButton.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.calculator.components
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.datastore.rememberIsLandscape
import com.sosauce.vanilla.data.datastore.rememberUseButtonsAnimation
import com.sosauce.vanilla.data.datastore.rememberVibration
import com.sosauce.vanilla.utils.BACKSPACE
import com.sosauce.vanilla.utils.PARENTHESES
@Composable
fun CuteButton(
modifier: Modifier = Modifier,
text: String,
buttonType: ButtonType = ButtonType.OTHER,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
rectangle: Boolean
) {
val haptic = LocalHapticFeedback.current
val shouldVibrate by rememberVibration()
val useButtonsAnimation by rememberUseButtonsAnimation()
val isPressed by interactionSource.collectIsPressedAsState()
val cornerRadius by animateIntAsState(
targetValue = if (isPressed && useButtonsAnimation) 24 else 50
)
val isLandscape = rememberIsLandscape()
val backgroundColor = when (buttonType) {
ButtonType.OPERATOR -> MaterialTheme.colorScheme.primary
ButtonType.OTHER -> MaterialTheme.colorScheme.surfaceContainer
ButtonType.ACTION -> MaterialTheme.colorScheme.tertiary
ButtonType.SPECIAL -> Color.Transparent
}
Box(
modifier = modifier
.semantics { role = Role.Button }
.clip(RoundedCornerShape(cornerRadius))
.combinedClickable(
interactionSource = interactionSource,
indication = ripple(),
onClick = {
onClick()
if (shouldVibrate) haptic.performHapticFeedback(HapticFeedbackType.Confirm)
},
onLongClick = onLongClick
)
.defaultMinSize(
minWidth = ButtonDefaults.MinWidth,
minHeight = ButtonDefaults.MinHeight
)
.background(backgroundColor)
.let {
if (!isLandscape && !rectangle) it.aspectRatio(1f) else it
},
contentAlignment = Alignment.Center
) {
when (text) {
BACKSPACE -> {
Icon(
painter = painterResource(R.drawable.backspace_filled),
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.contentColorFor(backgroundColor),
modifier = Modifier.size(45.dp)
)
}
PARENTHESES -> {
Icon(
painter = painterResource(R.drawable.parentheses),
contentDescription = null,
tint = MaterialTheme.colorScheme.contentColorFor(backgroundColor),
modifier = Modifier.size(45.dp)
)
}
else -> {
Text(
text = text,
color = contentColorFor(backgroundColor),
style = MaterialTheme.typography.displaySmallEmphasized
)
}
}
}
}
enum class ButtonType {
OPERATOR,
SPECIAL,
ACTION,
OTHER
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/calculator/components/DisableSoftKeyboard.kt
================================================
package com.sosauce.vanilla.ui.screens.calculator.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.InterceptPlatformTextInput
import kotlinx.coroutines.awaitCancellation
// https://stackoverflow.com/a/78720287
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DisableSoftKeyboard(
content: @Composable () -> Unit
) {
InterceptPlatformTextInput(
interceptor = { _, _ ->
awaitCancellation()
},
content = content
)
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/history/HistoryScreen.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.history
import android.content.ClipData
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.DropdownMenuPopup
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.datastore.rememberColoredOperators
import com.sosauce.vanilla.data.datastore.rememberDecimal
import com.sosauce.vanilla.data.datastore.rememberHistoryNewestFirst
import com.sosauce.vanilla.data.datastore.rememberUseHistory
import com.sosauce.vanilla.domain.model.Calculation
import com.sosauce.vanilla.domain.repository.HistoryEvents
import com.sosauce.vanilla.ui.screens.history.components.DeletionConfirmationDialog
import com.sosauce.vanilla.ui.screens.history.components.HistoryActionButtons
import com.sosauce.vanilla.ui.shared_components.AnimatedFab
import com.sosauce.vanilla.utils.formatExpression
import com.sosauce.vanilla.utils.formatNumber
import com.sosauce.vanilla.utils.isErrorMessage
import com.sosauce.vanilla.utils.isOperator
import com.sosauce.vanilla.utils.sort
@Composable
fun HistoryScreen(
calculations: List,
onEvents: (HistoryEvents) -> Unit,
onPutBackToField: (String) -> Unit,
onGotoMain: () -> Unit
) {
val lazyState = rememberLazyListState()
var isHistoryEnable by rememberUseHistory()
val newestFirst by rememberHistoryNewestFirst()
var showDeleteConfirmation by remember { mutableStateOf(false) }
if (showDeleteConfirmation) {
DeletionConfirmationDialog(
onDismissRequest = { showDeleteConfirmation = false },
onDelete = { onEvents(HistoryEvents.DeleteAllCalculation) }
)
}
Scaffold(
bottomBar = {
Row(
modifier = Modifier
.padding(horizontal = 15.dp)
.fillMaxWidth()
.navigationBarsPadding(),
horizontalArrangement = Arrangement.SpaceBetween
) {
AnimatedFab(
onClick = onGotoMain,
icon = R.drawable.arrow_up,
containerColor = MaterialTheme.colorScheme.surfaceContainer
)
HistoryActionButtons { showDeleteConfirmation = true }
}
}
) { pv ->
if (!isHistoryEnable) {
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(R.string.history_not_enabled))
Spacer(Modifier.height(10.dp))
Button(
onClick = { isHistoryEnable = !isHistoryEnable },
shapes = ButtonDefaults.shapes()
) {
Text(stringResource(R.string.enable_history))
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = pv,
state = lazyState
) {
if (calculations.isEmpty()) {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(R.drawable.history_rounded),
contentDescription = null,
modifier = Modifier.size(70.dp)
)
Spacer(Modifier.height(10.dp))
Text(
text = stringResource(R.string.no_calc_found),
style = MaterialTheme.typography.headlineMediumEmphasized,
fontWeight = FontWeight.Black
)
Text(
text = stringResource(R.string.calc_empty),
style = MaterialTheme.typography.bodyMediumEmphasized,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
itemsIndexed(
items = calculations.sort(newestFirst),
key = { _, item -> item.id }
) { index, item ->
CalculationItem(
calculation = item,
onEvents = onEvents,
onPutBackToField = onPutBackToField,
topDp = if (index == 0) 24.dp else 4.dp,
bottomDp = if (index == calculations.lastIndex) 24.dp else 4.dp,
modifier = Modifier.animateItem()
)
}
}
}
}
}
}
@Composable
private fun CalculationItem(
calculation: Calculation,
onEvents: (HistoryEvents) -> Unit,
onPutBackToField: (String) -> Unit,
topDp: Dp,
bottomDp: Dp,
modifier: Modifier = Modifier
) {
val clipboardManager = LocalClipboard.current
val localContentColor = LocalContentColor.current
val shouldFormat by rememberDecimal()
val coloredOperators by rememberColoredOperators()
var actionsExpanded by remember { mutableStateOf(false) }
val actions = listOf(
HistoryAction(
onClick = { onPutBackToField(calculation.operation) },
icon = R.drawable.undo,
text = R.string.put_field
),
HistoryAction(
onClick = {
clipboardManager.nativeClipboard.setPrimaryClip(
ClipData.newPlainText(
"",
"${calculation.operation} = ${calculation.result}"
)
)
},
icon = R.drawable.copy,
text = R.string.copy
),
HistoryAction(
onClick = { onEvents(HistoryEvents.DeleteCalculation(calculation)) },
icon = R.drawable.delete,
text = R.string.delete,
tint = MaterialTheme.colorScheme.error
)
)
Card(
onClick = { onPutBackToField(calculation.operation) },
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 2.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer
),
shape = RoundedCornerShape(
topStart = topDp,
topEnd = topDp,
bottomEnd = bottomDp,
bottomStart = bottomDp
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(15.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.weight(1f),
horizontalAlignment = Alignment.Start
) {
Text(
text = buildAnnotatedString {
calculation.operation.formatExpression(shouldFormat).forEach { char ->
if (coloredOperators && char.isOperator()) {
withStyle(SpanStyle(MaterialTheme.colorScheme.primary)) {
append(char)
}
} else append(char)
}
},
style = MaterialTheme.typography.titleLargeEmphasized,
modifier = Modifier.basicMarquee()
)
Text(
text = calculation.result.formatNumber(shouldFormat),
style = MaterialTheme.typography.titleLargeEmphasized.copy(
color = if (calculation.result.isErrorMessage()) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.tertiary
),
modifier = Modifier.basicMarquee()
)
}
IconButton(
onClick = { actionsExpanded = true },
shapes = IconButtonDefaults.shapes()
) {
Icon(
painter = painterResource(R.drawable.more_vert),
contentDescription = stringResource(R.string.more_actions)
)
DropdownMenuPopup(
expanded = actionsExpanded,
onDismissRequest = { actionsExpanded = false }
) {
DropdownMenuGroup(
shapes = MenuDefaults.groupShapes()
) {
actions.fastForEachIndexed { index, action ->
DropdownMenuItem(
onClick = {
action.onClick()
actionsExpanded = false
},
text = {
Text(
text = stringResource(action.text),
color = action.tint ?: localContentColor
)
},
leadingIcon = {
Icon(
painter = painterResource(action.icon),
contentDescription = null,
tint = action.tint ?: localContentColor
)
},
shape = when (index) {
0 -> MenuDefaults.leadingItemShape
actions.lastIndex -> MenuDefaults.trailingItemShape
else -> MenuDefaults.middleItemShape
}
)
}
}
}
}
}
}
}
private data class HistoryAction(
val onClick: () -> Unit,
val icon: Int,
val text: Int,
val tint: Color? = null
)
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/history/HistoryViewModel.kt
================================================
package com.sosauce.vanilla.ui.screens.history
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sosauce.vanilla.domain.model.Calculation
import com.sosauce.vanilla.domain.repository.HistoryDao
import com.sosauce.vanilla.domain.repository.HistoryEvents
import com.sosauce.vanilla.utils.isErrorMessage
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class HistoryViewModel(
private val dao: HistoryDao,
) : ViewModel() {
val allCalculations = dao.getAllCalculations()
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
emptyList()
)
fun onEvent(event: HistoryEvents) {
when (event) {
is HistoryEvents.AddCalculation -> {
val calculation = Calculation(
operation = event.operation,
result = event.result
)
if (event.saveErrors || !event.result.isErrorMessage()) {
viewModelScope.launch {
if (allCalculations.value.size.toLong() == event.maxHistoryItems) {
dao.deleteCalculation(allCalculations.value.first())
}
dao.insertCalculation(calculation)
}
} else {
return
}
}
is HistoryEvents.DeleteCalculation -> {
viewModelScope.launch { dao.deleteCalculation(event.calculation) }
}
is HistoryEvents.DeleteAllCalculation -> {
viewModelScope.launch { dao.deleteAllCalculations() }
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/history/components/DeletionConfirmationDialog.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.history.components
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.sosauce.vanilla.R
@Composable
fun DeletionConfirmationDialog(
onDismissRequest: () -> Unit,
onDelete: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(stringResource(R.string.clear_history)) },
text = { Text(stringResource(R.string.cant_be_undone)) },
confirmButton = {
TextButton(
onClick = {
onDelete()
onDismissRequest()
},
shapes = ButtonDefaults.shapes()
) {
Text(stringResource(R.string.delete))
}
},
dismissButton = {
TextButton(
onClick = onDismissRequest,
shapes = ButtonDefaults.shapes()
) {
Text(stringResource(R.string.cancel))
}
},
icon = {
Icon(
painter = painterResource(R.drawable.delete),
contentDescription = null
)
}
)
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/history/components/HistoryActionButtons.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.history.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.DropdownMenuPopup
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.datastore.rememberHistoryNewestFirst
@Composable
fun HistoryActionButtons(
modifier: Modifier = Modifier,
onDeleteHistory: () -> Unit
) {
var dropDownExpanded by remember { mutableStateOf(false) }
var newestFirst by rememberHistoryNewestFirst()
SmallFloatingActionButton(
onClick = {},
modifier = modifier,
shape = RoundedCornerShape(14.dp),
containerColor = MaterialTheme.colorScheme.surfaceContainer
) {
Row {
IconButton(
onClick = { dropDownExpanded = true }
) {
AnimatedContent(
targetState = !dropDownExpanded
) {
Icon(
painter = if (it) painterResource(R.drawable.sort_rounded) else painterResource(
R.drawable.close
),
contentDescription = stringResource(R.string.sort)
)
}
}
IconButton(
onClick = onDeleteHistory
) {
Icon(
painter = painterResource(R.drawable.trash_rounded),
contentDescription = stringResource(R.string.delete),
tint = MaterialTheme.colorScheme.error
)
}
DropdownMenuPopup(
expanded = dropDownExpanded,
onDismissRequest = { dropDownExpanded = false }
) {
DropdownMenuGroup(
shapes = MenuDefaults.groupShapes()
) {
DropdownMenuItem(
selected = newestFirst,
onClick = { newestFirst = true },
text = { Text(stringResource(R.string.newest_first)) },
trailingIcon = {
if (newestFirst) {
Icon(
painter = painterResource(R.drawable.check),
contentDescription = null
)
}
},
shapes = MenuDefaults.itemShapes()
)
DropdownMenuItem(
selected = !newestFirst,
onClick = { newestFirst = false },
text = { Text(stringResource(R.string.oldest_first)) },
trailingIcon = {
if (!newestFirst) {
Icon(
painter = painterResource(R.drawable.check),
contentDescription = null
)
}
},
shapes = MenuDefaults.itemShapes()
)
}
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/SettingsFormatting.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.datastore.rememberDecimal
import com.sosauce.vanilla.data.datastore.rememberDecimalPrecision
import com.sosauce.vanilla.ui.screens.settings.components.SettingsDropdownMenu
import com.sosauce.vanilla.ui.screens.settings.components.SettingsSwitch
import com.sosauce.vanilla.ui.screens.settings.components.SettingsWithTitle
import com.sosauce.vanilla.ui.shared_components.AnimatedFab
import com.sosauce.vanilla.utils.formatNumber
import com.sosauce.vanilla.utils.selfAlignHorizontally
@Composable
fun SettingsFormatting() {
var shouldFormat by rememberDecimal()
var decimalPrecision by rememberDecimalPrecision()
val decimalPrecisionOptions = MutableList(16) { it }.apply { add(1000) }
Column {
SettingsWithTitle(
title = R.string.formatting
) {
SettingsSwitch(
checked = shouldFormat,
onCheckedChange = { shouldFormat = !shouldFormat },
topDp = 24.dp,
bottomDp = 4.dp,
text = R.string.decimal_formatting
)
SettingsDropdownMenu(
value = decimalPrecision.toLong(),
topDp = 4.dp,
bottomDp = 24.dp,
text = R.string.decimal_precision,
optionalDescription = R.string.decimal_precision_desc
) {
decimalPrecisionOptions.fastForEachIndexed { index, number ->
val selected = number == decimalPrecision
DropdownMenuItem(
onClick = { decimalPrecision = number },
selected = selected,
text = { Text(number.toString().formatNumber(shouldFormat)) },
shapes = MenuDefaults.itemShape(index, decimalPrecisionOptions.count()),
trailingIcon = {
if (selected) {
Icon(
painter = painterResource(R.drawable.check),
contentDescription = null
)
}
}
)
}
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/SettingsHistory.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.datastore.rememberHistoryMaxItems
import com.sosauce.vanilla.data.datastore.rememberSaveErrorsToHistory
import com.sosauce.vanilla.data.datastore.rememberUseHistory
import com.sosauce.vanilla.ui.screens.settings.components.SettingsDropdownMenu
import com.sosauce.vanilla.ui.screens.settings.components.SettingsSwitch
import com.sosauce.vanilla.ui.screens.settings.components.SettingsWithTitle
import com.sosauce.vanilla.ui.shared_components.AnimatedFab
import com.sosauce.vanilla.utils.selfAlignHorizontally
@Composable
fun SettingsHistory() {
var useHistory by rememberUseHistory()
var historyMaxItems by rememberHistoryMaxItems()
var saveErrorsToHistory by rememberSaveErrorsToHistory()
val historyItemsChoice = listOf(
10,
20,
50,
100,
200,
500,
1000,
10000,
Long.MAX_VALUE
)
Column {
SettingsWithTitle(
title = R.string.history
) {
SettingsSwitch(
checked = useHistory,
onCheckedChange = { useHistory = !useHistory },
topDp = 24.dp,
bottomDp = 4.dp,
text = R.string.enable_history
)
SettingsSwitch(
checked = saveErrorsToHistory,
onCheckedChange = { saveErrorsToHistory = !saveErrorsToHistory },
topDp = 4.dp,
bottomDp = 4.dp,
text = R.string.save_errors
)
SettingsDropdownMenu(
value = historyMaxItems,
topDp = 4.dp,
bottomDp = 24.dp,
text = R.string.max_history_items
) {
historyItemsChoice.fastForEachIndexed { index, number ->
val selected = number == historyMaxItems
DropdownMenuItem(
onClick = { historyMaxItems = number },
selected = selected,
text = { Text(if (number == Long.MAX_VALUE) stringResource(R.string.no_limit) else number.toString()) },
trailingIcon = {
if (selected) {
Icon(
painter = painterResource(R.drawable.check),
contentDescription = null
)
}
},
shapes = MenuDefaults.itemShape(index, historyItemsChoice.count())
)
}
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/SettingsLookAndFeel.kt
================================================
package com.sosauce.vanilla.ui.screens.settings
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.datastore.rememberAppTheme
import com.sosauce.vanilla.data.datastore.rememberColoredOperators
import com.sosauce.vanilla.data.datastore.rememberShowClearButton
import com.sosauce.vanilla.data.datastore.rememberSwapZeroAndDecimal
import com.sosauce.vanilla.data.datastore.rememberUseButtonsAnimation
import com.sosauce.vanilla.data.datastore.rememberUseSystemFont
import com.sosauce.vanilla.data.datastore.rememberVibration
import com.sosauce.vanilla.ui.screens.settings.components.FontSelector
import com.sosauce.vanilla.ui.screens.settings.components.LazyRowWithScrollButton
import com.sosauce.vanilla.ui.screens.settings.components.SettingsSwitch
import com.sosauce.vanilla.ui.screens.settings.components.SettingsWithTitle
import com.sosauce.vanilla.ui.screens.settings.components.ThemeItem
import com.sosauce.vanilla.ui.screens.settings.components.ThemeSelector
import com.sosauce.vanilla.ui.shared_components.AnimatedFab
import com.sosauce.vanilla.ui.theme.nunitoFontFamily
import com.sosauce.vanilla.utils.CuteTheme
import com.sosauce.vanilla.utils.anyDarkColorScheme
import com.sosauce.vanilla.utils.anyLightColorScheme
import com.sosauce.vanilla.utils.selfAlignHorizontally
@Composable
fun SettingsLookAndFeel() {
var theme by rememberAppTheme()
var useSystemFont by rememberUseSystemFont()
var useButtonsAnimation by rememberUseButtonsAnimation()
var useHapticFeedback by rememberVibration()
var showClearButton by rememberShowClearButton()
var coloredOperators by rememberColoredOperators()
var swapZeroAndDecimal by rememberSwapZeroAndDecimal()
val themeItems = listOf(
ThemeItem(
onClick = { theme = CuteTheme.SYSTEM },
backgroundColor = if (isSystemInDarkTheme()) anyDarkColorScheme().background else anyLightColorScheme().background,
text = stringResource(R.string.follow_sys),
isSelected = theme == CuteTheme.SYSTEM,
iconAndTint = Pair(
painterResource(R.drawable.system_theme),
if (isSystemInDarkTheme()) anyDarkColorScheme().onBackground else anyLightColorScheme().onBackground
)
),
ThemeItem(
onClick = { theme = CuteTheme.DARK },
backgroundColor = anyDarkColorScheme().background,
text = stringResource(R.string.dark_mode),
isSelected = theme == CuteTheme.DARK,
iconAndTint = Pair(
painterResource(R.drawable.dark_mode),
anyDarkColorScheme().onBackground
)
),
ThemeItem(
onClick = { theme = CuteTheme.LIGHT },
backgroundColor = anyLightColorScheme().background,
text = stringResource(R.string.light_mode),
isSelected = theme == CuteTheme.LIGHT,
iconAndTint = Pair(
painterResource(R.drawable.light_mode),
anyLightColorScheme().onBackground
)
),
ThemeItem(
onClick = { theme = CuteTheme.AMOLED },
backgroundColor = Color.Black,
text = stringResource(R.string.amoled_mode),
isSelected = theme == CuteTheme.AMOLED,
iconAndTint = Pair(painterResource(R.drawable.amoled), Color.White)
)
)
val fontItems = listOf(
FontItem(
onClick = { useSystemFont = false },
fontStyle = FontStyle.DEFAULT,
borderColor = if (!useSystemFont) MaterialTheme.colorScheme.primary else Color.Transparent,
text = {
Text(
text = "Tt",
fontFamily = nunitoFontFamily
)
},
),
FontItem(
onClick = { useSystemFont = true },
fontStyle = FontStyle.SYSTEM,
borderColor = if (useSystemFont) MaterialTheme.colorScheme.primary else Color.Transparent,
text = {
Text(
text = "Tt",
fontFamily = FontFamily.Default
)
}
)
)
Column {
SettingsWithTitle(
title = R.string.theme
) {
Card(
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 2.dp)
) {
LazyRowWithScrollButton(
items = themeItems
) { item ->
ThemeSelector(
onClick = item.onClick,
backgroundColor = item.backgroundColor,
text = item.text,
isThemeSelected = item.isSelected,
icon = {
Icon(
painter = item.iconAndTint.first,
contentDescription = null,
tint = item.iconAndTint.second,
)
}
)
}
}
}
SettingsWithTitle(
title = R.string.font
) {
Card(
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 2.dp)
) {
LazyRowWithScrollButton(
items = fontItems
) { item ->
FontSelector(
item
)
}
}
}
SettingsWithTitle(
title = R.string.ui
) {
SettingsSwitch(
checked = useButtonsAnimation,
onCheckedChange = { useButtonsAnimation = !useButtonsAnimation },
topDp = 24.dp,
bottomDp = 4.dp,
text = R.string.buttons_anim
)
SettingsSwitch(
checked = coloredOperators,
onCheckedChange = { coloredOperators = !coloredOperators },
topDp = 4.dp,
bottomDp = 4.dp,
text = R.string.colored_operatos
)
SettingsSwitch(
checked = swapZeroAndDecimal,
onCheckedChange = { swapZeroAndDecimal = !swapZeroAndDecimal },
topDp = 4.dp,
bottomDp = 4.dp,
text = R.string.swap_zero_and_decimal
)
SettingsSwitch(
checked = useHapticFeedback,
onCheckedChange = { useHapticFeedback = !useHapticFeedback },
topDp = 4.dp,
bottomDp = 4.dp,
text = R.string.haptic_feedback
)
SettingsSwitch(
checked = showClearButton,
onCheckedChange = { showClearButton = !showClearButton },
topDp = 4.dp,
bottomDp = 24.dp,
text = R.string.show_clear_button,
optionalDescription = R.string.clear_button_desc
)
}
}
}
@Immutable
data class FontItem(
val onClick: () -> Unit,
val fontStyle: FontStyle,
val borderColor: Color,
val text: @Composable () -> Unit
)
enum class FontStyle {
DEFAULT,
SYSTEM
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/SettingsMisc.kt
================================================
package com.sosauce.vanilla.ui.screens.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.datastore.rememberShowOnLockScreen
import com.sosauce.vanilla.ui.screens.settings.components.SettingsSwitch
import com.sosauce.vanilla.ui.screens.settings.components.SettingsWithTitle
import com.sosauce.vanilla.ui.shared_components.AnimatedFab
import com.sosauce.vanilla.utils.selfAlignHorizontally
@Composable
fun SettingsMisc() {
var showOnLockScreen by rememberShowOnLockScreen()
Column {
SettingsWithTitle(
title = R.string.misc
) {
SettingsSwitch(
checked = showOnLockScreen,
onCheckedChange = { showOnLockScreen = !showOnLockScreen },
topDp = 24.dp,
bottomDp = 24.dp,
text = R.string.show_ls,
optionalDescription = R.string.show_ls_desc
)
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/SettingsScreen.kt
================================================
@file:OptIn(ExperimentalUuidApi::class)
package com.sosauce.vanilla.ui.screens.settings
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
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.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.sosauce.vanilla.R
import com.sosauce.vanilla.ui.navigation.Screens
import com.sosauce.vanilla.ui.navigation.SettingsScreen
import com.sosauce.vanilla.ui.screens.settings.components.AboutCard
import com.sosauce.vanilla.ui.screens.settings.components.SettingsCategoryCard
import com.sosauce.vanilla.ui.shared_components.AnimatedFab
import com.sosauce.vanilla.utils.bouncySpec
import com.sosauce.vanilla.utils.navigationBouncySpec
import com.sosauce.vanilla.utils.selfAlignHorizontally
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Composable
fun SettingsScreen(
onNavigate: (Screens) -> Unit
) {
var screenToDisplay by rememberSaveable { mutableStateOf(SettingsScreen.SETTINGS) }
// Mimic back behavior from navigation
BackHandler {
if (screenToDisplay != SettingsScreen.SETTINGS) {
screenToDisplay = SettingsScreen.SETTINGS
} else {
onNavigate(Screens.MAIN)
}
}
Scaffold(
bottomBar = {
AnimatedFab(
onClick = {
if (screenToDisplay == SettingsScreen.SETTINGS) {
onNavigate(Screens.MAIN)
} else {
screenToDisplay = SettingsScreen.SETTINGS
}
},
modifier = Modifier
.padding(start = 15.dp)
.navigationBarsPadding()
.selfAlignHorizontally(Alignment.Start),
icon = R.drawable.back_arrow,
containerColor = MaterialTheme.colorScheme.surfaceContainer
)
}
) { paddingValues ->
AnimatedContent(
targetState = screenToDisplay,
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(paddingValues)
.background(MaterialTheme.colorScheme.background),
transitionSpec = { slideInHorizontally(navigationBouncySpec) { -it } + fadeIn() togetherWith fadeOut() }
) { screen ->
when (screen) {
SettingsScreen.SETTINGS -> {
SettingsPage(
onNavigateSettings = { screenToDisplay = it }
)
}
SettingsScreen.LOOK_AND_FEEL -> { SettingsLookAndFeel() }
SettingsScreen.HISTORY -> { SettingsHistory() }
SettingsScreen.FORMATTING -> { SettingsFormatting() }
SettingsScreen.MISC -> { SettingsMisc() }
}
}
}
}
@Composable
private fun SettingsPage(
onNavigateSettings: (SettingsScreen) -> Unit
) {
val settingsCategories = listOf(
SettingsCategory(
name = R.string.look_and_feel,
description = R.string.look_and_feel_desc,
icon = R.drawable.palette,
onNavigate = { onNavigateSettings(SettingsScreen.LOOK_AND_FEEL) }
),
SettingsCategory(
name = R.string.history,
description = R.string.history_desc,
icon = R.drawable.history_rounded,
onNavigate = { onNavigateSettings(SettingsScreen.HISTORY) }
),
SettingsCategory(
name = R.string.formatting,
description = R.string.formatting_desc,
icon = R.drawable.formatting,
onNavigate = { onNavigateSettings(SettingsScreen.FORMATTING) }
),
SettingsCategory(
name = R.string.misc,
description = R.string.misc_desc,
icon = R.drawable.more_horiz,
onNavigate = { onNavigateSettings(SettingsScreen.MISC) }
)
)
Column {
AboutCard()
Spacer(Modifier.height(20.dp))
settingsCategories.fastForEachIndexed { index, category ->
SettingsCategoryCard(
icon = category.icon,
name = category.name,
description = category.description,
topDp = if (index == 0) 24.dp else 4.dp,
bottomDp = if (index == settingsCategories.lastIndex) 24.dp else 4.dp,
onNavigate = category.onNavigate
)
}
}
}
@Immutable
private data class SettingsCategory(
val id: String = Uuid.random().toString(),
val name: Int,
val description: Int,
val icon: Int,
val onNavigate: () -> Unit
)
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/components/AboutCard.kt
================================================
package com.sosauce.vanilla.ui.screens.settings.components
import androidx.compose.foundation.background
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.sosauce.vanilla.R
import com.sosauce.vanilla.utils.GITHUB_RELEASES
import com.sosauce.vanilla.utils.SUPPORT_PAGE
import com.sosauce.vanilla.utils.appVersion
import sv.lib.squircleshape.CornerSmoothing
import sv.lib.squircleshape.SquircleShape
@Composable
fun AboutCard() {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
Card(
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 2.dp),
shape = RoundedCornerShape(24.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(100.dp)
.padding(15.dp)
.background(
shape = SquircleShape(smoothing = CornerSmoothing.Full),
color = Color(0xFFf4a7bd)
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(R.drawable.calculator),
contentDescription = null,
modifier = Modifier.size(50.dp),
tint = Color(0xFFfdd9dc)
)
}
Column {
Text("Vanilla")
Text(
text = "${stringResource(id = R.string.version)} ${context.appVersion}",
style = MaterialTheme.typography.bodyMediumEmphasized.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
)
}
Spacer(Modifier.weight(1f))
Row(
verticalAlignment = Alignment.Top,
modifier = Modifier.padding(end = 15.dp)
) {
FilledIconButton(
onClick = { uriHandler.openUri(GITHUB_RELEASES) },
shapes = IconButtonDefaults.shapes()
) {
Icon(
painter = painterResource(R.drawable.github),
contentDescription = null
)
}
FilledIconButton(
onClick = { uriHandler.openUri(SUPPORT_PAGE) },
shapes = IconButtonDefaults.shapes()
) {
Icon(
painter = painterResource(R.drawable.favorite_filled),
contentDescription = null
)
}
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/components/FontSelector.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.settings.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialShapes
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.toShape
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.stringResource
import androidx.compose.ui.unit.dp
import com.sosauce.vanilla.R
import com.sosauce.vanilla.ui.screens.settings.FontStyle
@Composable
fun FontSelector(
item: com.sosauce.vanilla.ui.screens.settings.FontItem
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(10.dp)
.height(100.dp)
.clip(RoundedCornerShape(12.dp))
.clickable { item.onClick() }
) {
Box(
modifier = Modifier
.padding(10.dp)
.size(50.dp)
.clip(MaterialShapes.Cookie9Sided.toShape())
.border(
width = 2.dp,
color = item.borderColor,
shape = MaterialShapes.Cookie9Sided.toShape()
)
.background(MaterialTheme.colorScheme.surfaceContainerHighest),
contentAlignment = Alignment.Center
) { item.text() }
Spacer(Modifier.weight(1f))
Text(
text = if (item.fontStyle == FontStyle.SYSTEM) stringResource(R.string.system) else "Default"
)
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/components/LazyRowWithScrollButton.kt
================================================
package com.sosauce.vanilla.ui.screens.settings.components
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import com.sosauce.vanilla.R
import kotlinx.coroutines.launch
@Composable
fun LazyRowWithScrollButton(
items: List,
content: @Composable (T) -> Unit
) {
val state = rememberLazyListState()
val scope = rememberCoroutineScope()
Box {
LazyRow(
state = state
) {
items(
items = items
) { type ->
content(type)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = state.canScrollForward,
modifier = Modifier.align(Alignment.CenterEnd),
enter = slideInHorizontally { it },
exit = slideOutHorizontally { it }
) {
IconButton(
onClick = {
scope.launch {
state.animateScrollToItem(items.lastIndex)
}
}
) {
Icon(
painter = painterResource(R.drawable.arrow_right),
contentDescription = null
)
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/components/SettingsCategoryCard.kt
================================================
package com.sosauce.vanilla.ui.screens.settings.components
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun SettingsCategoryCard(
icon: Int,
name: Int,
description: Int,
topDp: Dp,
bottomDp: Dp,
onNavigate: () -> Unit
) {
Card(
onClick = onNavigate,
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 2.dp),
shape = RoundedCornerShape(
topStart = topDp,
topEnd = topDp,
bottomStart = bottomDp,
bottomEnd = bottomDp
)
) {
Row(
modifier = Modifier
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(icon),
contentDescription = null
)
Spacer(Modifier.width(15.dp))
Column {
Text(stringResource(name))
Text(
text = stringResource(description),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/components/SettingsSwitch.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.settings.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuPopup
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sosauce.vanilla.R
@Composable
fun SettingsSwitch(
checked: Boolean,
onCheckedChange: () -> Unit,
topDp: Dp,
bottomDp: Dp,
text: Int,
optionalDescription: Int? = null
) {
Card(
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 2.dp),
shape = RoundedCornerShape(
topStart = topDp,
topEnd = topDp,
bottomStart = bottomDp,
bottomEnd = bottomDp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(15.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(1f)
) {
Column {
Text(stringResource(text))
optionalDescription?.let {
Text(
text = stringResource(it),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp
)
}
}
}
Switch(
checked = checked,
onCheckedChange = { onCheckedChange() },
colors = SwitchDefaults.colors(
uncheckedBorderColor = Color.Transparent
)
)
}
}
}
@Composable
fun SettingsDropdownMenu(
value: Long,
topDp: Dp,
bottomDp: Dp,
text: Int,
optionalDescription: Int? = null,
dropdownContent: @Composable (ColumnScope.() -> Unit)
) {
var expanded by remember { mutableStateOf(false) }
Card(
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 2.dp),
shape = RoundedCornerShape(
topStart = topDp,
topEnd = topDp,
bottomStart = bottomDp,
bottomEnd = bottomDp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(15.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(1f)
) {
Column {
Text(stringResource(text))
optionalDescription?.let {
Text(
text = stringResource(it),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp
)
}
}
}
TextButton(
onClick = { expanded = true }
) {
AnimatedContent(
targetState = value
) {
Text(
text = if (it == Long.MAX_VALUE) stringResource(R.string.no_limit) else it.toString(),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 15.sp
)
}
DropdownMenuPopup(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuGroup(
shapes = MenuDefaults.groupShapes()
) { dropdownContent() }
}
}
}
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/components/SettingsWithTitle.kt
================================================
package com.sosauce.vanilla.ui.screens.settings.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@Composable
fun SettingsWithTitle(
title: Int,
content: @Composable () -> Unit
) {
Column {
Text(
text = stringResource(id = title),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 34.dp, vertical = 8.dp)
)
content()
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/screens/settings/components/ThemeSelector.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.screens.settings.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialShapes
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
@Composable
fun ThemeSelector(
onClick: () -> Unit,
backgroundColor: Color,
icon: @Composable () -> Unit,
text: String,
isThemeSelected: Boolean
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(10.dp)
.height(100.dp)
.clip(RoundedCornerShape(12.dp))
.clickable { onClick() }
) {
Box(
modifier = Modifier
.padding(10.dp)
.size(50.dp)
.clip(MaterialShapes.Cookie9Sided.toShape())
.border(
width = 2.dp,
color = if (isThemeSelected) MaterialTheme.colorScheme.secondary else Color.Transparent,
shape = MaterialShapes.Cookie9Sided.toShape()
)
.background(backgroundColor),
contentAlignment = Alignment.Center
) {
icon()
}
Spacer(Modifier.weight(1f))
Text(text)
}
}
@Immutable
data class ThemeItem(
val onClick: () -> Unit,
val backgroundColor: Color,
val text: String,
val isSelected: Boolean,
val iconAndTint: Pair
)
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/shared_components/AnimatedFab.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.shared_components
import androidx.annotation.DrawableRes
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialShapes
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.toPath
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.graphics.shapes.Morph
import com.sosauce.vanilla.utils.bouncySpec
private data class FabAnimation(
val rotation: Float,
val scale: Float,
val shape: Shape
)
@Composable
private fun rememberFabAnimations(isPressed: Boolean): FabAnimation {
val morph = remember {
Morph(
MaterialShapes.Cookie9Sided,
MaterialShapes.Circle
)
}
val animatedScale by animateFloatAsState(
targetValue = if (isPressed) 0.8f else 1f,
label = "scale",
animationSpec = bouncySpec()
)
val animatedRotation by animateFloatAsState(
targetValue = if (isPressed) 180f else 0f,
label = "rotation",
animationSpec = bouncySpec()
)
val animatedProgress by animateFloatAsState(
targetValue = if (isPressed) 1f else 0f,
label = "progress",
animationSpec = bouncySpec()
)
val shape = remember(morph, animatedProgress) {
MorphPolygonShape(morph, animatedProgress)
}
return FabAnimation(
rotation = animatedRotation,
scale = animatedScale,
shape = shape
)
}
@Composable
fun AnimatedFab(
onClick: () -> Unit,
@DrawableRes icon: Int,
modifier: Modifier = Modifier,
minSize: Dp = 56.dp,
containerColor: Color = FloatingActionButtonDefaults.containerColor
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val fabAnimation = rememberFabAnimations(isPressed)
Box(
modifier = modifier
.scale(fabAnimation.scale)
.defaultMinSize(minWidth = minSize, minHeight = minSize)
.clip(fabAnimation.shape)
.background(containerColor)
.clickable(interactionSource = interactionSource, indication = null, onClick = onClick)
) {
Icon(
painter = painterResource(icon),
contentDescription = null,
tint = contentColorFor(containerColor),
modifier = Modifier
.align(Alignment.Center)
.rotate(fabAnimation.rotation)
)
}
}
class MorphPolygonShape(
private val morph: Morph,
private val percentage: Float
) : Shape {
private val matrix = Matrix()
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
matrix.scale(size.width, size.height)
val path = morph.toPath(progress = percentage)
path.transform(matrix)
return Outline.Generic(path)
}
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/ui/theme/Theme.kt
================================================
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package com.sosauce.vanilla.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import com.sosauce.vanilla.R
import com.sosauce.vanilla.data.datastore.rememberAppTheme
import com.sosauce.vanilla.data.datastore.rememberUseSystemFont
import com.sosauce.vanilla.utils.CuteTheme
import com.sosauce.vanilla.utils.anyDarkColorScheme
import com.sosauce.vanilla.utils.anyLightColorScheme
@Composable
fun CuteCalcTheme(
content: @Composable () -> Unit
) {
val isSystemInDarkTheme = isSystemInDarkTheme()
val appTheme by rememberAppTheme()
val useSystemFont by rememberUseSystemFont()
val colorScheme = when (appTheme) {
CuteTheme.AMOLED -> anyDarkColorScheme().copy(
surface = Color.Black,
inverseSurface = Color.White,
background = Color.Black,
)
CuteTheme.SYSTEM -> if (isSystemInDarkTheme) anyDarkColorScheme() else anyLightColorScheme()
CuteTheme.DARK -> anyDarkColorScheme()
CuteTheme.LIGHT -> anyLightColorScheme()
else -> anyDarkColorScheme()
}
MaterialExpressiveTheme(
colorScheme = colorScheme,
content = content,
typography = if (useSystemFont) null else NunitoTypography
)
}
val nunitoFontFamily = FontFamily(
Font(R.font.nunito_extrabold, FontWeight.ExtraBold, FontStyle.Normal)
)
val NunitoTypography = Typography().run {
copy(
displayLarge = displayLarge.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
displayMedium = displayMedium.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
displaySmall = displaySmall.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
headlineLarge = headlineLarge.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
headlineMedium = headlineMedium.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
headlineSmall = headlineSmall.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
titleLarge = titleLarge.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
titleMedium = titleMedium.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
titleSmall = titleSmall.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
bodyLarge = bodyLarge.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
bodyMedium = bodyMedium.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
bodySmall = bodySmall.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
labelLarge = labelLarge.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
labelMedium = labelMedium.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
labelSmall = labelSmall.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
displayLargeEmphasized = displayLargeEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
displayMediumEmphasized = displayMediumEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
displaySmallEmphasized = displaySmallEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
headlineLargeEmphasized = headlineLargeEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
headlineMediumEmphasized = headlineMediumEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
headlineSmallEmphasized = headlineSmallEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
titleLargeEmphasized = titleLargeEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
titleMediumEmphasized = titleMediumEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
titleSmallEmphasized = titleSmallEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
bodyLargeEmphasized = bodyLargeEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
bodyMediumEmphasized = bodyMediumEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
bodySmallEmphasized = bodySmallEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
labelLargeEmphasized = labelLargeEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
labelMediumEmphasized = labelMediumEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
),
labelSmallEmphasized = labelSmallEmphasized.copy(
fontFamily = nunitoFontFamily,
fontWeight = FontWeight.ExtraBold
)
)
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/utils/Constants.kt
================================================
package com.sosauce.vanilla.utils
const val CUTE_MUSIC = "com.sosauce.cutemusic"
const val GITHUB_RELEASES = "https://github.com/sosauce/CuteCalc/releases"
const val SUPPORT_PAGE = "https://sosauce.github.io/support/"
const val BACKSPACE = "backspace"
const val PARENTHESES = "parentheses"
object CuteTheme {
const val SYSTEM = "SYSTEM"
const val DARK = "DARK"
const val LIGHT = "LIGHT"
const val AMOLED = "AMOLED"
}
================================================
FILE: app/src/main/java/com/sosauce/vanilla/utils/Extensions.kt
================================================
package com.sosauce.vanilla.utils
import android.app.Activity
import android.content.Context
import android.os.Build
import android.view.WindowManager
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.delete
import androidx.compose.foundation.text.input.insert
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntOffset
import com.sosauce.vanilla.data.calculator.Tokens
import com.sosauce.vanilla.domain.model.Calculation
import java.text.DecimalFormatSymbols
fun Modifier.thenIf(
condition: Boolean,
modifier: Modifier.() -> Modifier
): Modifier {
return if (condition) {
this.then(modifier())
} else this
}
fun List.sort(
newestFirst: Boolean
): List {
return if (newestFirst) {
this.sortedByDescending { it.id }
} else {
this
}
}
@Composable
fun anyLightColorScheme(): ColorScheme {
val context = LocalContext.current
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicLightColorScheme(context)
} else {
lightColorScheme()
}
}
@Composable
fun anyDarkColorScheme(): ColorScheme {
val context = LocalContext.current
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicDarkColorScheme(context)
} else {
darkColorScheme()
}
}
fun TextFieldState.insertText(char: Char) {
val expression = this.text
val cursorPosition = selection.start
val charInfrontCursor = expression.getOrNull(cursorPosition - 1) ?: ' '
val charBehindCursor = expression.getOrNull(cursorPosition) ?: ' '
when (char) {
Tokens.ZERO, Tokens.ONE, Tokens.TWO, Tokens.THREE, Tokens.FOUR,
Tokens.FIVE, Tokens.SIX, Tokens.SEVEN, Tokens.EIGHT, Tokens.NINE,
Tokens.PI, Tokens.OPEN_PARENTHESIS, Tokens.CLOSED_PARENTHESIS,
Tokens.SQUARE_ROOT, Tokens.MODULO, Tokens.FACTORIAL -> {
edit { insert(cursorPosition, char.toString()) }
}
Tokens.DECIMAL -> {
val toInsert = if (!charInfrontCursor.isDigit()) {
"${Tokens.ZERO}${Tokens.DECIMAL}"
} else Tokens.DECIMAL.toString()
edit { insert(cursorPosition, toInsert) }
}
Tokens.SUBTRACT -> {
if (charInfrontCursor.isOperator()) {
edit { insert(cursorPosition, "${Tokens.OPEN_PARENTHESIS}${Tokens.SUBTRACT}") }
} else if (charBehindCursor.isOperator()) {
edit {
replace(cursorPosition, cursorPosition + 1, char.toString())
}
} else {
edit { insert(cursorPosition, char.toString()) }
}
}
Tokens.ADD, Tokens.DIVIDE, Tokens.MULTIPLY, Tokens.POWER -> {
if (charInfrontCursor.isOperator()) {
edit { replace(cursorPosition - 1, cursorPosition, char.toString()) }
} else if (charBehindCursor.isOperator()) {
edit {
replace(cursorPosition, cursorPosition + 1, char.toString())
}
} else {
edit { insert(cursorPosition, char.toString()) }
}
}
}
}
fun TextFieldState.backspace() {
val cursorPosition = selection.start
if (selection.collapsed && cursorPosition > 0) {
edit {
delete(cursorPosition - 1, cursorPosition)
}
}
}
fun Char.isOperator(): Boolean {
val operators =
listOf(Tokens.ADD, Tokens.SUBTRACT, Tokens.DIVIDE, Tokens.MULTIPLY, Tokens.POWER)
return this in operators
}
fun String.isErrorMessage(): Boolean {
return any { char -> char.isLetter() }
}
fun CharSequence.whichParenthesis(): Char {
return if (count { it == Tokens.OPEN_PARENTHESIS } > count { it == Tokens.CLOSED_PARENTHESIS }) {
Tokens.CLOSED_PARENTHESIS
} else {
Tokens.OPEN_PARENTHESIS
}
}
/**
* Formats a number not an expression !!
*/
fun String.formatNumber(shouldFormat: Boolean): String {
val number = this
val localSymbols = DecimalFormatSymbols.getInstance()
if (number.any { it.isLetter() } || !shouldFormat) return number
val integer = number.takeWhile { it != '.' }
val decimal = number.removePrefix(integer).replace('.', localSymbols.decimalSeparator)
// 1234
val formattedInteger = integer
.reversed() // 4321
.chunked(3) // [432, 1]
.joinToString(localSymbols.groupingSeparator.toString()) // 432,1
.reversed() // 1,234
return "${formattedInteger}${decimal}"
}
fun String.formatExpression(shouldFormat: Boolean): String {
if (!shouldFormat) return this
var expression = this
val numberRegex = Regex("[\\d.]+")
numberRegex.findAll(expression).forEach { result ->
expression = expression.replace(result.value, result.value.formatNumber(true))
}
return expression
}
fun Activity.showOnLockScreen(show: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(show)
} else {
if (show) {
window.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
)
}
}
}
fun Modifier.selfAlignHorizontally(align: Alignment.Horizontal = Alignment.CenterHorizontally): Modifier {
return this.then(
Modifier
.fillMaxWidth()
.wrapContentWidth(align)
)
}
fun bouncySpec() = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
val navigationBouncySpec = spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow)
val Context.appVersion
get() = packageManager.getPackageInfo(packageName, 0).versionName
================================================
FILE: app/src/main/java/com/sosauce/vanilla/utils/ViewModelFactories.kt
================================================
package com.sosauce.vanilla.utils
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.room.Room
import com.sosauce.vanilla.domain.repository.HistoryDatabase
import com.sosauce.vanilla.ui.screens.calculator.CalculatorViewModel
import com.sosauce.vanilla.ui.screens.history.HistoryViewModel
class HistoryViewModelFactory(val application: Application) : ViewModelProvider.Factory {
private val historyDb by lazy {
Room.databaseBuilder(
context = application,
klass = HistoryDatabase::class.java,
name = "history.db"
).build()
}
override fun create(modelClass: Class): T {
return HistoryViewModel(historyDb.dao) as T
}
}
class CalculatorViewModelFactory(val application: Application) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return CalculatorViewModel(application) as T
}
}
================================================
FILE: app/src/main/res/drawable/amoled.xml
================================================
================================================
FILE: app/src/main/res/drawable/arrow_right.xml
================================================
================================================
FILE: app/src/main/res/drawable/arrow_up.xml
================================================
================================================
FILE: app/src/main/res/drawable/back_arrow.xml
================================================
================================================
FILE: app/src/main/res/drawable/backspace_filled.xml
================================================
================================================
FILE: app/src/main/res/drawable/backspace_rounded.xml
================================================
================================================
FILE: app/src/main/res/drawable/calculator.xml
================================================
================================================
FILE: app/src/main/res/drawable/check.xml
================================================
================================================
FILE: app/src/main/res/drawable/close.xml
================================================
================================================
FILE: app/src/main/res/drawable/copy.xml
================================================
================================================
FILE: app/src/main/res/drawable/dark_mode.xml
================================================
================================================
FILE: app/src/main/res/drawable/delete.xml
================================================
================================================
FILE: app/src/main/res/drawable/favorite_filled.xml
================================================
================================================
FILE: app/src/main/res/drawable/formatting.xml
================================================
================================================
FILE: app/src/main/res/drawable/github.xml
================================================
================================================
FILE: app/src/main/res/drawable/history_rounded.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_launcher_foreground.xml
================================================
================================================
FILE: app/src/main/res/drawable/icon_splash.xml
================================================
================================================
FILE: app/src/main/res/drawable/light_mode.xml
================================================
================================================
FILE: app/src/main/res/drawable/more_horiz.xml
================================================
================================================
FILE: app/src/main/res/drawable/more_vert.xml
================================================
================================================
FILE: app/src/main/res/drawable/palette.xml
================================================
================================================
FILE: app/src/main/res/drawable/parentheses.xml
================================================
================================================
FILE: app/src/main/res/drawable/settings_filled.xml
================================================
================================================
FILE: app/src/main/res/drawable/sort_rounded.xml
================================================
================================================
FILE: app/src/main/res/drawable/system_theme.xml
================================================
================================================
FILE: app/src/main/res/drawable/trash_rounded.xml
================================================
================================================
FILE: app/src/main/res/drawable/undo.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#201A1A
#f0d2ce
================================================
FILE: app/src/main/res/values/ic_launcher_background.xml
================================================
#F5A6BD
================================================
FILE: app/src/main/res/values/strings.xml
================================================
Theme
Settings
Dark
Light
Amoled
Version
Check updates
Vanilla by sosauce
System
Font
System
Buttons animation
History
Use history
It looks like history isn\'t enabled !
Enable history
Misc
Haptic feedback
Ascending
Descending
Decimal formatting
Show clear button
You can still long press the backspace button to clear the input field.
Back
Sort
Delete
Backspace
Put back in input field
Copy to clipboard
More actions
Look and feel
Why not calculate in style ?
Default
UI
Remember everything !
Max saved items in history
No limit
Other settings that didn\'t deserve their own page.
Formatting
Show back button
Clear history
Are you sure you want to do that ? This action can\'t be undone !
Cancel
Save errors to history
Support
Decimal precision
Adjust the number of decimal places.
Make numbers look great !
Show app on lock screen
The app will still be usable on the lock screen. This can be useful for example, in stores.
Newest first
Oldest first
No calculation found !
Start calculating already !
Colored operators
Swap zero & decimal
================================================
FILE: app/src/main/res/values/themes.xml
================================================
================================================
FILE: app/src/main/res/values-es/strings.xml
================================================
Tema
Ajustes
Modo Oscuro
Modo Claro
Modo Amoled
Versión
Buscar actualizaciones
Vanilla por sosauce
Seguir sistema
Fuente
Sistema
Animación de botones
Historial
Usar historial
¡Parece que el historial no está habilitado!
Habilitar historial
Varios
Respuesta háptica
Ascendente
Descendente
Formato decimal
Mostrar botón de borrar
Aún puedes mantener presionado el botón de retroceso para borrar el campo de entrada.
Atrás
Ordenar
Eliminar
Retroceso
Poner de nuevo en el campo de entrada
Copiar al portapapeles
Más acciones
Apariencia y estilo
¿Por qué no calcular con estilo?
Predeterminada
IU
¡Recordarlo todo!
Máximo de elementos en el historial
Sin límite
Otros ajustes que no merecían su propia página.
Formato
Mostrar botón de retroceso
Limpiar historial
¿Realmente quieres hacer eso? ¡Esta acción no se puede deshacer!
Cancelar
Guardar errores en el historial
Apóyame
Precisión decimal
Ajusta el número de decimales.
¡Haz que los números se vean geniales!
Mostrar app en pantalla de bloqueo
La app seguirá siendo utilizable en la pantalla de bloqueo. Esto puede ser útil, por ejemplo, en tiendas.
Primero los más recientes
Primero los más antiguos
¡No se encontraron cálculos!
¡Empieza a calcular de una vez!
Operadores coloreados
================================================
FILE: app/src/main/res/values-fr-rFR/strings.xml
================================================
Thème
Paramètres
Mode sombre
Clair
Mode amoled
Version
Mettre à jour
Vanilla par sosauce
Suivre le système
Police d\'écriture
Système
Animation des buttons
Historique
Utiliser l\'historique
Il semblerait que l\'historique ne soit pas activé !
Activé l\'historique
Divers
Vibrations
Ascendant
Descendant
Format décimal
Afficher le button effacer
Vous pouvez toujours rester appuyer sur le button d\'effacement arrière pour effacer le champs de calcul.
Retour
Trier
Supprimer
Retour arrière
Remettre dans le champ
Copier dans le presse-papiers
Plus d\'action
Apparence
Pourquoi pas calculer en style ?
Défaut
UI
Souvenez-vous de tout !
Éléments max sauvegarder dans l\'historique
Pas de limite
Autre paramètres qui ne méritaient pas leurs pages
Formattage
Montrer le button arrière
Vider l\'historique
Êtes-vous sûr de vouloir faire ceci ?
Annuler
Sauvegarder les erreurs dans l\'historique
Supporter
Précision décimale
Ajuster le nombre de chiffres après la décimale
Rendez les nombres beaux !
Montrer l\'app sur l\'écran de vérouillage
L\'app sera utilisable sur l\'écran de vérouillage.
================================================
FILE: app/src/main/res/values-v31/colors.xml
================================================
@android:color/system_neutral2_900
@android:color/system_accent1_100
================================================
FILE: app/src/main/res/values-zh-rCN/strings.xml
================================================
主题
设置
深色
浅色
Amoled
版本
检查更新
Vanilla by sosauce
系统
字体
系统
按钮动画
历史记录
使用历史记录
看起来没有启用历史记录!
启用历史记录
杂项
触觉反馈
上升
下降
小数格式
显示清除按钮
你仍然可以长按退格键来清空输入字段。
返回
排序
删除
退格
Put back in input field
复制到剪贴板
更多操作
外观和感觉
为什么不美观地计算呢?
默认
UI
记住一切!
历史记录中能保存的最大项目
无限
其他不值得单独页面的设置。
格式化
显示返回按钮
清除历史记录
你确定要这样做吗?此操作无法撤销!
取消
将错误保存到历史记录
支持
小数精度
调整小数位数。
让数字看起来更出色!
在锁屏上显示应用
该应用在锁屏状态下仍然可用。例如,这在商店里可能会很有用。
最新的在前
最早的在前
未找到计算!
快开始计算吧!
================================================
FILE: build.gradle.kts
================================================
plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ksp) apply false
}
================================================
FILE: fastlane/metadata/android/de/full_description.txt
================================================
Vanilla ist eine kleine, schnelle, Open-Source Android Taschenrechner-App im Material 3 Design, die keine Berechtigungen braucht.
================================================
FILE: fastlane/metadata/android/de/short_description.txt
================================================
Eine simple, kleine, Open-Source Taschenrechner-App
================================================
FILE: fastlane/metadata/android/en-US/full_description.txt
================================================
Vanilla is a cute an elegant calculator app for Android!
================================================
FILE: fastlane/metadata/android/en-US/short_description.txt
================================================
A cute and elegant calculator app for Android
================================================
FILE: font_licence.txt
================================================
Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
================================================
FILE: gradle/libs.versions.toml
================================================
[versions]
agp = "9.2.1"
composeBom = "2026.05.00"
coreSplashscreen = "1.2.0"
datastorePreferences = "1.2.1"
keval = "1.1.1"
kotlin = "2.3.21"
ksp = "2.3.4"
lifecycleViewmodelCompose = "2.10.0"
roomCompiler = "2.8.4"
roomKtx = "2.8.4"
activityCompose = "1.13.0"
material3 = "1.5.0-alpha19"
squircleShape = "5.2.1"
[libraries]
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
androidx-ui = { module = "androidx.compose.ui:ui" }
keval = { module = "com.notkamui.libs:keval", version.ref = "keval" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
squircle-shape = { module = "com.stoyanvuchev:squircle-shape", version.ref = "squircleShape" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Fri Dec 15 16:55:56 CET 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: settings.gradle.kts
================================================
@file:Suppress("UnstableApiUsage")
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
rootProject.name = "Vanilla"
include(":app")