Initial LCDPet v1.0 release candidate

This commit is contained in:
Kris
2026-07-05 14:03:13 -04:00
commit ca54c8c965
47 changed files with 5943 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+57
View File
@@ -0,0 +1,57 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "today.notfeelingit.lcdpet"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
applicationId = "today.notfeelingit.lcdpet"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
optimization {
enable = false
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation("androidx.datastore:datastore-preferences:1.2.1")
testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
}
@@ -0,0 +1,24 @@
package today.notfeelingit.lcdpet
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("today.notfeelingit.lcdpet", appContext.packageName)
}
}
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LCDPet">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.LCDPet"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
+28
View File
@@ -0,0 +1,28 @@
LCDPet Audio Credits
Weather Channel music:
“Calm Piano Jazz Loop” by ispeakwaves
Source: Freesound
Sound page: https://freesound.org/people/ispeakwaves/sounds/570316/
Original file: 570316__ispeakwaves__calm-piano-jazz-loop.wav
App file: weather_channel_jazz.ogg
License: Creative Commons Attribution 4.0
Use: Watch TV Channel 1 Weather loop
No Signal static:
“TV_Light_Static.aif” by crcavol
Source: Freesound
Sound page: https://freesound.org/people/crcavol/sounds/154674/
Original file: 154674__crcavol__tv_light_static.aiff
App file: tv_static_light.ogg
License: Creative Commons 0 / CC0
Use: Watch TV Channel 2 No Signal static loop
TV power-off sound:
“Tv 100Hz off.wav” by KnightRider1
Source: Freesound
Sound page: https://freesound.org/people/KnightRider1/sounds/90682/
App file: tv_off.ogg
License: Creative Commons 0 / CC0
Use: Watch TV CRT power-off sound
Edited for length
@@ -0,0 +1,557 @@
package today.notfeelingit.lcdpet
import android.media.MediaPlayer
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.graphics.Shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
@Composable
fun TouchGrassActivityScreen(
lcdDark: Color,
onGrassTap: () -> Unit
) {
var grassFingerColumn by remember { mutableIntStateOf(-1) }
var grassWaveTick by remember { mutableIntStateOf(0) }
var grassWidthPx by remember { mutableIntStateOf(1) }
val grassColumns = 33
fun updateGrassFinger(xPosition: Float) {
val safeWidth = maxOf(grassWidthPx, 1)
grassFingerColumn = ((xPosition / safeWidth.toFloat()) * grassColumns)
.toInt()
.coerceIn(0, grassColumns - 1)
grassWaveTick += 1
onGrassTap()
}
Column(
modifier = Modifier
.fillMaxWidth()
.border(2.dp, lcdDark)
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
text = fluidGrassText(
fingerColumn = grassFingerColumn,
waveTick = grassWaveTick,
columns = grassColumns
),
fontFamily = FontFamily.Monospace,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
grassWidthPx = coordinates.size.width
}
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
updateGrassFinger(offset.x)
},
onDrag = { change, _ ->
updateGrassFinger(change.position.x)
},
onDragEnd = {
grassFingerColumn = -1
},
onDragCancel = {
grassFingerColumn = -1
}
)
}
)
Text(
text = "Slide through the grass.",
color = lcdDark,
textAlign = TextAlign.Center
)
Text(
text = "Pet is taking time alone.",
color = lcdDark,
textAlign = TextAlign.Center
)
}
}
@Composable
fun WatchTvActivityScreen(
lcdDark: Color,
selectedChannel: TvChannel,
weatherLabel: String,
onChannelSelected: (TvChannel) -> Unit
) {
var tvFrame by remember { mutableIntStateOf(0) }
var tvPowerState by remember { mutableIntStateOf(1) }
var tvPowerOffSoundRequest by remember { mutableIntStateOf(0) }
// 1 = on, 2 = CRT shutting off, 0 = off
val context = LocalContext.current
LaunchedEffect(selectedChannel, tvPowerState) {
tvFrame = 0
while (true) {
delay(if (tvPowerState == 2) 120L else 450L)
tvFrame += 1
}
}
LaunchedEffect(tvPowerState) {
if (tvPowerState == 2) {
delay(420L)
tvPowerState = 0
}
}
LaunchedEffect(tvPowerOffSoundRequest) {
if (tvPowerOffSoundRequest > 0) {
val powerOffPlayer = MediaPlayer.create(context, R.raw.tv_off)
try {
powerOffPlayer?.apply {
isLooping = false
setVolume(0.75f, 0.75f)
start()
}
while (powerOffPlayer?.isPlaying == true) {
delay(50L)
}
} finally {
powerOffPlayer?.release()
}
}
}
DisposableEffect(selectedChannel, tvPowerState) {
var player: MediaPlayer? = null
if (tvPowerState == 1) {
val soundResource = when (selectedChannel) {
TvChannel.Weather -> R.raw.weather_channel_jazz
TvChannel.NoSignal -> R.raw.tv_static_light
}
val channelVolume = when (selectedChannel) {
TvChannel.Weather -> 0.75f
TvChannel.NoSignal -> 0.60f
}
player = MediaPlayer.create(context, soundResource)?.apply {
isLooping = true
setVolume(channelVolume, channelVolume)
start()
}
}
onDispose {
player?.stop()
player?.release()
player = null
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.border(2.dp, lcdDark)
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(170.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Column(
modifier = Modifier.width(48.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
TvChannelButton(
text = "1",
selected = tvPowerState == 1 && selectedChannel == TvChannel.Weather,
lcdDark = lcdDark,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
onClick = {
if (tvPowerState == 1) {
onChannelSelected(TvChannel.Weather)
}
}
)
TvChannelButton(
text = "2",
selected = tvPowerState == 1 && selectedChannel == TvChannel.NoSignal,
lcdDark = lcdDark,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
onClick = {
if (tvPowerState == 1) {
onChannelSelected(TvChannel.NoSignal)
}
}
)
TvPowerButton(
powerOn = tvPowerState == 1,
lcdDark = lcdDark,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
onClick = {
if (tvPowerState == 1) {
tvPowerOffSoundRequest += 1
tvPowerState = 2
} else {
tvPowerState = 1
}
}
)
}
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.border(2.dp, lcdDark)
.background(if (tvPowerState == 1) Color.Transparent else Color.Black)
.padding(horizontal = 10.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = if (tvPowerState == 1) {
tvPreviewText(
channel = selectedChannel,
frame = tvFrame,
weatherLabel = weatherLabel
)
} else {
tvPowerOffPreviewText(
frame = tvFrame,
shuttingDown = tvPowerState == 2
)
},
fontFamily = FontFamily.Monospace,
textAlign = if (tvPowerState == 1) TextAlign.Start else TextAlign.Center,
color = if (tvPowerState == 1) lcdDark else Color.White,
modifier = Modifier.fillMaxWidth(),
softWrap = !(tvPowerState == 1 && selectedChannel == TvChannel.NoSignal)
)
}
}
Text(
text = "Pet is outside having alone time.",
color = lcdDark,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun TvChannelButton(
text: String,
selected: Boolean,
lcdDark: Color,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Box(
modifier = modifier
.border(2.dp, lcdDark)
.clickable { onClick() }
.padding(vertical = 6.dp),
contentAlignment = Alignment.Center
) {
Text(
text = if (selected) "[$text]" else text,
fontWeight = FontWeight.Bold,
color = lcdDark,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun TvPowerButton(
powerOn: Boolean,
lcdDark: Color,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val textGlow = if (powerOn) {
Shadow(
color = Color.White.copy(alpha = 0.65f),
offset = Offset.Zero,
blurRadius = 4f
)
} else {
null
}
Box(
modifier = modifier
.background(Color.Black)
.border(2.dp, lcdDark)
.clickable { onClick() }
.padding(vertical = 2.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Θ",
fontFamily = FontFamily.Monospace,
fontSize = 16.sp,
lineHeight = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Center,
style = TextStyle(
shadow = textGlow
)
)
}
}
private fun fluidGrassText(
fingerColumn: Int,
waveTick: Int,
columns: Int
): AnnotatedString {
val rows = 7
val grassColor = Color(0xFF2E7D32)
return buildAnnotatedString {
for (row in 0 until rows) {
for (column in 0 until columns) {
val distance = if (fingerColumn < 0) {
99
} else {
kotlin.math.abs(column - fingerColumn)
}
val phase = (row + column + waveTick) % 4
val blade = when {
distance == 0 -> if (phase % 2 == 0) '/' else '\\'
distance == 1 -> if (phase % 2 == 0) '/' else '|'
distance == 2 -> if (phase % 2 == 0) '|' else '\\'
else -> '|'
}
withStyle(SpanStyle(color = grassColor)) {
append(blade)
}
}
if (row != rows - 1) {
append("\n")
}
}
}
}
private fun tvPreviewText(
channel: TvChannel,
frame: Int,
weatherLabel: String
): String {
return when (channel) {
TvChannel.Weather -> weatherPreviewText(weatherLabel)
TvChannel.NoSignal -> noSignalPreviewText(
channelNumber = 2,
frame = frame
)
}
}
private fun weatherPreviewText(weatherLabel: String): String {
val normalizedWeather = weatherLabel.lowercase()
val conditionText = when {
normalizedWeather.contains("rain") -> "RAINING"
normalizedWeather.contains("cloud") || normalizedWeather.contains("overcast") -> "CLOUDY"
else -> "SUNNY"
}
val iconLines = when (conditionText) {
"RAINING" -> listOf(
" .--. ",
" (____) ",
" ' ' ' "
)
"CLOUDY" -> listOf(
" .--. ",
" (____) ",
" "
)
else -> listOf(
" \\ | / ",
" \\|/ ",
" -- o --"
)
}
return listOf(
"${iconLines[0]} $conditionText",
"${iconLines[1]} TEMP: 72",
"${iconLines[2]} UNIT: C/F"
).joinToString("\n")
}
private fun tvPowerOffPreviewText(
frame: Int,
shuttingDown: Boolean
): String {
if (!shuttingDown) {
return listOf(
"",
"",
"",
"",
""
).joinToString("\n")
}
return when (frame) {
0 -> listOf(
"",
"--------------------------------",
"================================",
"--------------------------------",
""
).joinToString("\n")
1 -> listOf(
"",
"",
" ------------",
"",
""
).joinToString("\n")
2 -> listOf(
"",
"",
" --*--",
"",
""
).joinToString("\n")
else -> listOf(
"",
"",
"",
"",
""
).joinToString("\n")
}
}
private fun noSignalPreviewText(
channelNumber: Int,
frame: Int
): String {
val width = 34
val staticLine = if (frame % 2 == 0) {
"|".repeat(width)
} else {
":".repeat(width)
}
val secondStaticLine = staticLine
return listOf(
"CH $channelNumber",
"",
" NO SIGNAL",
staticLine,
secondStaticLine
).joinToString("\n")
}
private fun tvTextLine(
width: Int,
vararg parts: Pair<Int, String>,
fill: Char = ' '
): String {
return tvTextLine(
width = width,
fill = fill,
parts = parts
)
}
private fun tvTextLine(
width: Int,
fill: Char,
parts: Array<out Pair<Int, String>>
): String {
val chars = CharArray(width) { fill }
parts.forEach { part ->
val start = part.first
val value = part.second
value.forEachIndexed { index, char ->
val target = start + index
if (target in chars.indices && char != ' ') {
chars[target] = char
}
}
}
return String(chars)
}
@@ -0,0 +1,12 @@
package today.notfeelingit.lcdpet
enum class DowntimeMode {
None,
TouchGrass,
WatchTv
}
enum class TvChannel {
Weather,
NoSignal
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,172 @@
package today.notfeelingit.lcdpet
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
val Context.lcdPetDataStore by preferencesDataStore(name = "lcdpet_save")
data class LCDPetRecordSave(
val bond: Int = 0,
val petAgeDays: Int = 0,
val careScore: Int = 0,
val neglectScore: Int = 0,
val hunger: Int = 5,
val health: Int = 10,
val happiness: Int = 5,
val energy: Int = 5,
val dirtiness: Int = 0,
val potty: Int = 0,
val boredom: Int = 0,
val location: String = "Inside",
val weather: String = "Sunny",
val status: String = "Nervous",
val weakCause: String = "",
val pottyStain: String = "",
val happyOutsideTrip: Boolean = false,
val isTradingInPet: Boolean = false,
val isFindingNewPet: Boolean = false,
val tradeInProgress: Int = 0,
val currentWant: String = "",
val petName: String = "",
val isSleeping: Boolean = false,
val sleepMode: String = "",
val roughWakeAlert: Boolean = false,
val autoSleepStartedAtMs: Long = 0L,
val careJournalRaw: String = "",
val lastSavedAtMs: Long = System.currentTimeMillis()
)
private object LCDPetSaveKeys {
val BOND = intPreferencesKey("bond")
val PET_AGE_DAYS = intPreferencesKey("pet_age_days")
val CARE_SCORE = intPreferencesKey("care_score")
val NEGLECT_SCORE = intPreferencesKey("neglect_score")
val HUNGER = intPreferencesKey("hunger")
val HEALTH = intPreferencesKey("health")
val HAPPINESS = intPreferencesKey("happiness")
val ENERGY = intPreferencesKey("energy")
val DIRTINESS = intPreferencesKey("dirtiness")
val POTTY = intPreferencesKey("potty")
val BOREDOM = intPreferencesKey("boredom")
val LOCATION = stringPreferencesKey("location")
val WEATHER = stringPreferencesKey("weather")
val STATUS = stringPreferencesKey("status")
val WEAK_CAUSE = stringPreferencesKey("weak_cause")
val POTTY_STAIN = stringPreferencesKey("potty_stain")
val HAPPY_OUTSIDE_TRIP = booleanPreferencesKey("happy_outside_trip")
val IS_TRADING_IN_PET = booleanPreferencesKey("is_trading_in_pet")
val IS_FINDING_NEW_PET = booleanPreferencesKey("is_finding_new_pet")
val TRADE_IN_PROGRESS = intPreferencesKey("trade_in_progress")
val CURRENT_WANT = stringPreferencesKey("current_want")
val PET_NAME = stringPreferencesKey("pet_name")
val IS_SLEEPING = booleanPreferencesKey("is_sleeping")
val SLEEP_MODE = stringPreferencesKey("sleep_mode")
val ROUGH_WAKE_ALERT = booleanPreferencesKey("rough_wake_alert")
val AUTO_SLEEP_STARTED_AT_MS = longPreferencesKey("auto_sleep_started_at_ms")
val CARE_JOURNAL = stringPreferencesKey("care_journal")
val LAST_SAVED_AT_MS = longPreferencesKey("last_saved_at_ms")
}
val Context.lcdPetRecordSaveFlow: Flow<LCDPetRecordSave>
get() = lcdPetDataStore.data.map { preferences ->
LCDPetRecordSave(
bond = preferences[LCDPetSaveKeys.BOND] ?: 0,
petAgeDays = preferences[LCDPetSaveKeys.PET_AGE_DAYS] ?: 0,
careScore = preferences[LCDPetSaveKeys.CARE_SCORE] ?: 0,
neglectScore = preferences[LCDPetSaveKeys.NEGLECT_SCORE] ?: 0,
hunger = preferences[LCDPetSaveKeys.HUNGER] ?: 5,
health = preferences[LCDPetSaveKeys.HEALTH] ?: 10,
happiness = preferences[LCDPetSaveKeys.HAPPINESS] ?: 5,
energy = preferences[LCDPetSaveKeys.ENERGY] ?: 5,
dirtiness = preferences[LCDPetSaveKeys.DIRTINESS] ?: 0,
potty = preferences[LCDPetSaveKeys.POTTY] ?: 0,
boredom = preferences[LCDPetSaveKeys.BOREDOM] ?: 0,
location = preferences[LCDPetSaveKeys.LOCATION] ?: "Inside",
weather = preferences[LCDPetSaveKeys.WEATHER] ?: "Sunny",
status = preferences[LCDPetSaveKeys.STATUS] ?: "Nervous",
weakCause = preferences[LCDPetSaveKeys.WEAK_CAUSE] ?: "",
pottyStain = preferences[LCDPetSaveKeys.POTTY_STAIN] ?: "",
happyOutsideTrip = preferences[LCDPetSaveKeys.HAPPY_OUTSIDE_TRIP] ?: false,
isTradingInPet = preferences[LCDPetSaveKeys.IS_TRADING_IN_PET] ?: false,
isFindingNewPet = preferences[LCDPetSaveKeys.IS_FINDING_NEW_PET] ?: false,
tradeInProgress = preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] ?: 0,
currentWant = preferences[LCDPetSaveKeys.CURRENT_WANT] ?: "",
petName = preferences[LCDPetSaveKeys.PET_NAME] ?: "",
isSleeping = preferences[LCDPetSaveKeys.IS_SLEEPING] ?: false,
sleepMode = preferences[LCDPetSaveKeys.SLEEP_MODE] ?: "",
roughWakeAlert = preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] ?: false,
autoSleepStartedAtMs = preferences[LCDPetSaveKeys.AUTO_SLEEP_STARTED_AT_MS] ?: 0L,
careJournalRaw = preferences[LCDPetSaveKeys.CARE_JOURNAL] ?: "",
lastSavedAtMs = preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] ?: System.currentTimeMillis()
)
}
suspend fun Context.saveLCDPetStats(
bond: Int,
petAgeDays: Int,
careScore: Int,
neglectScore: Int,
hunger: Int,
health: Int,
happiness: Int,
energy: Int,
dirtiness: Int,
potty: Int,
boredom: Int,
location: String,
weather: String,
status: String,
weakCause: String,
pottyStain: String,
happyOutsideTrip: Boolean,
isTradingInPet: Boolean,
isFindingNewPet: Boolean,
tradeInProgress: Int,
currentWant: String,
petName: String,
isSleeping: Boolean,
sleepMode: String,
roughWakeAlert: Boolean,
autoSleepStartedAtMs: Long,
careJournalRaw: String? = null
) {
lcdPetDataStore.edit { preferences ->
preferences[LCDPetSaveKeys.BOND] = bond
preferences[LCDPetSaveKeys.PET_AGE_DAYS] = petAgeDays
preferences[LCDPetSaveKeys.CARE_SCORE] = careScore
preferences[LCDPetSaveKeys.NEGLECT_SCORE] = neglectScore
preferences[LCDPetSaveKeys.HUNGER] = hunger
preferences[LCDPetSaveKeys.HEALTH] = health
preferences[LCDPetSaveKeys.HAPPINESS] = happiness
preferences[LCDPetSaveKeys.ENERGY] = energy
preferences[LCDPetSaveKeys.DIRTINESS] = dirtiness
preferences[LCDPetSaveKeys.POTTY] = potty
preferences[LCDPetSaveKeys.BOREDOM] = boredom
preferences[LCDPetSaveKeys.LOCATION] = location
preferences[LCDPetSaveKeys.WEATHER] = weather
preferences[LCDPetSaveKeys.STATUS] = status
preferences[LCDPetSaveKeys.WEAK_CAUSE] = weakCause
preferences[LCDPetSaveKeys.POTTY_STAIN] = pottyStain
preferences[LCDPetSaveKeys.HAPPY_OUTSIDE_TRIP] = happyOutsideTrip
preferences[LCDPetSaveKeys.IS_TRADING_IN_PET] = isTradingInPet
preferences[LCDPetSaveKeys.IS_FINDING_NEW_PET] = isFindingNewPet
preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] = tradeInProgress
preferences[LCDPetSaveKeys.CURRENT_WANT] = currentWant
preferences[LCDPetSaveKeys.PET_NAME] = petName
preferences[LCDPetSaveKeys.IS_SLEEPING] = isSleeping
preferences[LCDPetSaveKeys.SLEEP_MODE] = sleepMode
preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] = roughWakeAlert
preferences[LCDPetSaveKeys.AUTO_SLEEP_STARTED_AT_MS] = autoSleepStartedAtMs
if (careJournalRaw != null) {
preferences[LCDPetSaveKeys.CARE_JOURNAL] = careJournalRaw
}
preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] = System.currentTimeMillis()
}
}
@@ -0,0 +1,11 @@
package today.notfeelingit.lcdpet.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@@ -0,0 +1,58 @@
package today.notfeelingit.lcdpet.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
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.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun LCDPetTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package today.notfeelingit.lcdpet.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
+12
View File
@@ -0,0 +1,12 @@
# Add project specific R8 rules here.
# AGP will combine all keep rule files in src/main/keepRules to pass to R8
#
# For more details, see
# https://d.android.com/r/tools/r8/keep-rules
# 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 *;
#}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">LCDPet</string>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.LCDPet" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package today.notfeelingit.lcdpet
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}