diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 152d7b8..f1e45b6 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -15,8 +15,8 @@ android {
applicationId = "today.notfeelingit.lcdpet"
minSdk = 26
targetSdk = 36
- versionCode = 1
- versionName = "1.0"
+ versionCode = 2
+ versionName = "1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8c25918..bd18db4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
Unit,
- onRelease: () -> Unit
+ onRelease: () -> Unit,
+ modifier: Modifier = Modifier,
+ fillWidthFraction: Float = 0.86f
) {
Box(
- modifier = Modifier
- .fillMaxWidth(0.86f)
+ modifier = modifier
+ .fillMaxWidth(fillWidthFraction)
.border(2.dp, if (enabled) lcdDark else lcdFaded)
.pointerInput(enabled) {
detectTapGestures(
@@ -324,15 +335,15 @@ fun LCDHoldSubButton(
@Composable
fun LCDSubMenuFrame(
lcdDark: Color,
- content: @Composable () -> Unit
+ content: @Composable RowScope.() -> Unit
) {
- Column(
+ Row(
modifier = Modifier
.fillMaxWidth()
.border(2.dp, lcdDark)
- .padding(vertical = 8.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(6.dp)
+ .padding(6.dp),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalAlignment = Alignment.CenterVertically
) {
content()
}
@@ -341,7 +352,8 @@ fun LCDSubMenuFrame(
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val lcdBackground = Color(0xFFC4C7B0)
- val lcdDark = Color(0xFF2A2A2A)
+ val lcdDark = Color(0xFF343434)
+
val sickGreen = Color(0xFF2F7D32)
val lcdFaded = lcdDark.copy(alpha = 0.30f)
@@ -353,6 +365,22 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
onResult = {}
)
+ var microphonePermissionGranted by remember {
+ mutableStateOf(
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.RECORD_AUDIO
+ ) == PackageManager.PERMISSION_GRANTED
+ )
+ }
+
+ val microphonePermissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission(),
+ onResult = { granted ->
+ microphonePermissionGranted = granted
+ }
+ )
+
LaunchedEffect(Unit) {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
@@ -371,6 +399,14 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
"Credits unavailable."
}
}
+
+ val howToText = remember(context) {
+ runCatching {
+ context.assets.open("how_to.txt").bufferedReader().use { it.readText() }
+ }.getOrElse {
+ "HOW TO content could not be loaded."
+ }
+ }
val newPetWeatherOptions = listOf("Sunny", "Cloudy", "Rain", "Snow")
var location by remember { mutableStateOf("Inside") }
var weather by remember { mutableStateOf(newPetWeatherOptions.random()) }
@@ -417,17 +453,22 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
var isTradingInPet by remember { mutableStateOf(false) }
var isFindingNewPet by remember { mutableStateOf(false) }
var tradeInProgress by remember { mutableIntStateOf(0) }
+ var tradeInStartedAtMs by remember { mutableStateOf(0L) }
+ var tradeInRemainingMs by remember { mutableStateOf(0L) }
var tradeInConfirm by remember { mutableStateOf(false) }
var isBathing by remember { mutableStateOf(false) }
var isTalking by remember { mutableStateOf(false) }
+ var microphoneRecorder by remember { mutableStateOf(null) }
+ var microphoneLevel by remember { mutableIntStateOf(0) }
var bathBondAtStart by remember { mutableIntStateOf(0) }
var bathWantMatched by remember { mutableStateOf(false) }
var careMenuOpen by remember { mutableStateOf(false) }
var socializeMenuOpen by remember { mutableStateOf(false) }
var statsMenuOpen by remember { mutableStateOf(false) }
var careJournalOpen by remember { mutableStateOf(false) }
- var creditsOpen by remember { mutableStateOf(false) }
- var creditsCloseButtonCorner by remember { mutableStateOf("BottomRight") }
+ var infoPanelOpen by remember { mutableStateOf(false) }
+ var infoPanelType by remember { mutableStateOf("Credits") }
+ var infoPanelButtonCorner by remember { mutableStateOf("BottomRight") }
var careJournalTab by remember { mutableStateOf("All") }
var testWantMenuOpen by remember { mutableStateOf(false) }
var testBondMenuOpen by remember { mutableStateOf(false) }
@@ -498,7 +539,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
val offlineOutsideEnergyDrainTickMs = roughSleepEnergyTickMs
val roughWakeAlertMs = if (testingMode) 4_000L else 30_000L
- val tradeInWaitMs = if (testingMode) 5_000L else 24L * hourMs
+ val tradeInWaitMs = if (testingMode) 5_000L else 2L * hourMs
val findingNewPetMs = if (testingMode) 2_000L else 5_000L
val tradeInProgressBlocks = 10
@@ -651,6 +692,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
isTradingInPet = save.isTradingInPet
isFindingNewPet = save.isFindingNewPet
tradeInProgress = save.tradeInProgress.coerceIn(0, tradeInProgressBlocks)
+ tradeInStartedAtMs = save.tradeInStartedAtMs
currentWant = save.currentWant
petName = save.petName
pendingPetName = save.petName
@@ -744,6 +786,19 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
val tradeInProgressBar = "▓".repeat(tradeInProgress) + "▒".repeat(tradeInProgressBlocks - tradeInProgress)
+ fun tradeCountdownText(remainingMs: Long): String {
+ val totalSeconds = ((remainingMs + 999L) / 1_000L).coerceAtLeast(0L)
+ val hours = totalSeconds / 3_600L
+ val minutes = (totalSeconds % 3_600L) / 60L
+ val seconds = totalSeconds % 60L
+
+ return when {
+ hours > 0L -> "${hours}h ${minutes}m"
+ minutes > 0L -> "${minutes}m ${seconds}s"
+ else -> "${seconds}s"
+ }
+ }
+
fun addCareJournalEntry(
category: String,
action: String,
@@ -890,9 +945,20 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
isTradingInPet -> "Trading pet..."
// Consequence alerts: show the cause when sickness came from a missed need.
- weakCause == "Starving" && status in listOf("Sick", "Weak") -> "$displayPetName is hungry and sick!"
- weakCause == "Overfed" && status in listOf("Sick", "Weak") -> "$displayPetName is stuffed and sick!"
- weakCause == "Dirty" && status in listOf("Sick", "Weak") -> "$displayPetName needs a bath and is sick!"
+ weakCause == "Starving" &&
+ hunger >= 9 &&
+ status in listOf("Sick", "Weak") ->
+ "$displayPetName is hungry and sick!"
+
+ weakCause == "Overfed" &&
+ hunger <= 1 &&
+ status in listOf("Sick", "Weak") ->
+ "$displayPetName is stuffed and sick!"
+
+ weakCause == "Dirty" &&
+ dirtiness >= 10 &&
+ status in listOf("Sick", "Weak") ->
+ "$displayPetName needs a bath and is sick!"
// Need warnings.
energy <= 0 && !isSleeping -> "$displayPetName needs a nap!"
@@ -948,9 +1014,12 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
framePropAnimation == "Bath" && framePropDisplay == "≈≈≈" -> {
framePropDisplay.forEachIndexed { index, character ->
+ val isActive = index == framePropStyle % 3
+
withStyle(
SpanStyle(
- fontWeight = if (index == framePropStyle % 3) {
+ color = if (isActive) lcdDark else lcdFaded,
+ fontWeight = if (isActive) {
FontWeight.Bold
} else {
FontWeight.Normal
@@ -1269,13 +1338,89 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
}
}
+ @Suppress("MissingPermission")
fun beginTalk() {
- if (!isTradingInPet && !isFindingNewPet && !isSleeping && !isBathing && framePropAnimation.isEmpty()) {
+ if (
+ microphonePermissionGranted &&
+ !isTradingInPet &&
+ !isFindingNewPet &&
+ !isSleeping &&
+ !isBathing &&
+ framePropAnimation.isEmpty()
+ ) {
isTalking = true
activityMood = if (bond >= 100) "Happy" else "Calm"
framePropAnimation = ""
framePropStyle = 0
frameProp = "¢==-"
+ microphoneLevel = 0
+
+ val sampleRate = 16_000
+ val minimumBufferSize = AudioRecord.getMinBufferSize(
+ sampleRate,
+ AudioFormat.CHANNEL_IN_MONO,
+ AudioFormat.ENCODING_PCM_16BIT
+ )
+ val bufferSize = minimumBufferSize.coerceAtLeast(2_048)
+
+ val recorder = runCatching {
+ AudioRecord(
+ MediaRecorder.AudioSource.MIC,
+ sampleRate,
+ AudioFormat.CHANNEL_IN_MONO,
+ AudioFormat.ENCODING_PCM_16BIT,
+ bufferSize
+ )
+ }.getOrNull()
+
+ if (recorder == null || recorder.state != AudioRecord.STATE_INITIALIZED) {
+ recorder?.release()
+ return
+ }
+
+ microphoneRecorder = recorder
+ recorder.startRecording()
+
+ saveScope.launch {
+ withContext(Dispatchers.IO) {
+ val audioBuffer = ShortArray(bufferSize)
+
+ while (recorder.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
+ val samplesRead = recorder.read(
+ audioBuffer,
+ 0,
+ audioBuffer.size
+ )
+
+ if (samplesRead > 0) {
+ var peak = 0
+
+ for (index in 0 until samplesRead) {
+ peak = maxOf(peak, abs(audioBuffer[index].toInt()))
+ }
+
+ val level = when {
+ peak >= 12_000 -> 3
+ peak >= 5_000 -> 2
+ peak >= 1_500 -> 1
+ else -> 0
+ }
+
+ withContext(Dispatchers.Main) {
+ if (isTalking && microphoneRecorder === recorder) {
+ microphoneLevel = level
+ frameProp = when (level) {
+ 3 -> "¢==-)))"
+ 2 -> "¢==-))"
+ 1 -> "¢==-)"
+ else -> "¢==-"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
}
@@ -1283,10 +1428,19 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
if (isTalking) {
isTalking = false
- if (frameProp == "¢==-") {
- frameProp = ""
+ microphoneRecorder?.let { recorder ->
+ runCatching {
+ if (recorder.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
+ recorder.stop()
+ }
+ }
+ recorder.release()
}
+ microphoneRecorder = null
+ microphoneLevel = 0
+ frameProp = ""
+
val wantMatched = completeWant("Talk")
if (!wantMatched) {
@@ -1413,7 +1567,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
currentWant = ""
currentWantStartedAtMs = 0L
careJournalOpen = false
- creditsOpen = false
+ infoPanelOpen = false
careJournalTab = "All"
careJournalEntries = emptyList()
petName = ""
@@ -1429,6 +1583,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
isTradingInPet = false
isFindingNewPet = false
tradeInProgress = 0
+ tradeInStartedAtMs = 0L
}
fun beginTradeInPet() {
@@ -1447,6 +1602,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
autoSleepStartedAtMs = 0L
tradeInConfirm = false
tradeInProgress = 0
+ tradeInStartedAtMs = System.currentTimeMillis()
isTradingInPet = true
// Force-save trade start immediately so closing the app mid-trade resumes correctly.
@@ -1472,6 +1628,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
isTradingInPet = true,
isFindingNewPet = false,
tradeInProgress = 0,
+ tradeInStartedAtMs = tradeInStartedAtMs,
currentWant = currentWant,
petName = petName,
petTemplateId = petTemplateId,
@@ -1490,6 +1647,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
isTradingInPet = false
tradeInConfirm = false
tradeInProgress = 0
+ tradeInStartedAtMs = 0L
// Force-save trade cancel immediately.
saveScope.launch {
@@ -1514,6 +1672,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
isTradingInPet = false,
isFindingNewPet = false,
tradeInProgress = 0,
+ tradeInStartedAtMs = 0L,
currentWant = currentWant,
petName = petName,
petTemplateId = petTemplateId,
@@ -1729,6 +1888,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
isTradingInPet = isTradingInPet,
isFindingNewPet = isFindingNewPet,
tradeInProgress = tradeInProgress,
+ tradeInStartedAtMs = tradeInStartedAtMs,
currentWant = currentWant,
petName = petName,
petTemplateId = petTemplateId,
@@ -1741,23 +1901,48 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
}
}
- LaunchedEffect(isTradingInPet) {
- if (isTradingInPet) {
- val tradeInStepMs = (tradeInWaitMs / tradeInProgressBlocks).coerceAtLeast(1L)
- val startingProgress = tradeInProgress.coerceIn(0, tradeInProgressBlocks)
+ LaunchedEffect(isTradingInPet, saveLoaded, tradeInStartedAtMs) {
+ if (isTradingInPet && saveLoaded) {
+ if (tradeInStartedAtMs <= 0L) {
+ val restoredProgress = tradeInProgress.coerceIn(0, tradeInProgressBlocks)
+ val restoredElapsedMs =
+ (tradeInWaitMs * restoredProgress) / tradeInProgressBlocks
- for (step in startingProgress until tradeInProgressBlocks) {
- delay(tradeInStepMs)
- tradeInProgress = step + 1
+ tradeInStartedAtMs =
+ System.currentTimeMillis() - restoredElapsedMs
}
- isTradingInPet = false
- tradeInConfirm = false
- statsMenuOpen = false
- isFindingNewPet = true
+ while (isTradingInPet) {
+ val elapsedMs =
+ (System.currentTimeMillis() - tradeInStartedAtMs)
+ .coerceAtLeast(0L)
- // Force-save finding-new-pet state immediately.
- context.saveLCDPetStats(
+ tradeInRemainingMs =
+ (tradeInWaitMs - elapsedMs).coerceAtLeast(0L)
+
+ tradeInProgress =
+ ((elapsedMs * tradeInProgressBlocks) / tradeInWaitMs)
+ .toInt()
+ .coerceIn(0, tradeInProgressBlocks)
+
+ if (tradeInRemainingMs <= 0L) {
+ break
+ }
+
+ delay(minOf(1_000L, tradeInRemainingMs))
+ }
+
+ if (isTradingInPet) {
+ tradeInProgress = tradeInProgressBlocks
+ tradeInRemainingMs = 0L
+ tradeInStartedAtMs = 0L
+ isTradingInPet = false
+ tradeInConfirm = false
+ statsMenuOpen = false
+ isFindingNewPet = true
+
+ // Force-save finding-new-pet state immediately.
+ context.saveLCDPetStats(
bond = bond,
petAgeDays = petAgeDays,
careScore = careScore,
@@ -1778,6 +1963,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
isTradingInPet = false,
isFindingNewPet = true,
tradeInProgress = tradeInProgressBlocks,
+ tradeInStartedAtMs = 0L,
currentWant = currentWant,
petName = petName,
petTemplateId = petTemplateId,
@@ -1786,7 +1972,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
roughWakeAlert = roughWakeAlert,
autoSleepStartedAtMs = autoSleepStartedAtMs,
careJournalRaw = encodeCareJournalEntries(careJournalEntries)
- )
+ )
+ }
}
}
@@ -2389,6 +2576,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
isTradingInPet = isTradingInPet,
isFindingNewPet = isFindingNewPet,
tradeInProgress = tradeInProgress,
+ tradeInStartedAtMs = tradeInStartedAtMs,
currentWant = currentWant,
petName = petName,
petTemplateId = petTemplateId,
@@ -2465,12 +2653,13 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
}
val screenScrollState = rememberScrollState()
- Box(
- modifier = modifier
- .fillMaxSize()
- .background(lcdBackground)
- ) {
- Column(
+ CompositionLocalProvider(LocalContentColor provides lcdDark) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .background(lcdBackground)
+ ) {
+ Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(screenScrollState)
@@ -2499,7 +2688,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
if (statsMenuOpen) {
careMenuOpen = false
socializeMenuOpen = false
- creditsOpen = false
+ infoPanelOpen = false
} else {
careJournalOpen = false
testWantMenuOpen = false
@@ -2523,19 +2712,16 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Credits / Licenses",
- fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.clickable {
- creditsOpen = !creditsOpen
-
- if (creditsOpen) {
- statsMenuOpen = false
- careJournalOpen = false
- testWantMenuOpen = false
- testBondMenuOpen = false
- careMenuOpen = false
- socializeMenuOpen = false
- }
+ infoPanelType = "Credits"
+ infoPanelOpen = true
+ statsMenuOpen = false
+ careJournalOpen = false
+ testWantMenuOpen = false
+ testBondMenuOpen = false
+ careMenuOpen = false
+ socializeMenuOpen = false
}
)
}
@@ -2704,7 +2890,9 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Bond",
- modifier = Modifier.width(72.dp)
+ modifier = Modifier.width(92.dp),
+ maxLines = 1,
+ softWrap = false
)
Text(text = "$bondPercent% $bondRelationship")
}
@@ -2712,7 +2900,9 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Age",
- modifier = Modifier.width(72.dp)
+ modifier = Modifier.width(92.dp),
+ maxLines = 1,
+ softWrap = false
)
Text(text = petAgeText)
}
@@ -2722,7 +2912,9 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Care",
- modifier = Modifier.width(72.dp)
+ modifier = Modifier.width(92.dp),
+ maxLines = 1,
+ softWrap = false
)
Text(text = "$careScore")
}
@@ -2730,7 +2922,9 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Neglect",
- modifier = Modifier.width(72.dp)
+ modifier = Modifier.width(92.dp),
+ maxLines = 1,
+ softWrap = false
)
Text(text = "$neglectScore")
}
@@ -2757,6 +2951,19 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
)
if (isTradingInPet) {
+ Text(
+ text = "You’ll need to wait before a new pet can be found.",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Text(
+ text = "New pet search begins in ${tradeCountdownText(tradeInRemainingMs)}",
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
Text(
text = "[$tradeInProgressBar]",
fontWeight = FontWeight.Bold,
@@ -2764,6 +2971,12 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
modifier = Modifier.fillMaxWidth()
)
+ Text(
+ text = "You can still change your mind.",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
LCDSubButton(
text = "CHANGE MIND",
enabled = true,
@@ -2870,7 +3083,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
if (testBondMenuOpen) {
testWantMenuOpen = false
careJournalOpen = false
- creditsOpen = false
+ infoPanelOpen = false
}
}
)
@@ -2920,7 +3133,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
if (testWantMenuOpen) {
testBondMenuOpen = false
careJournalOpen = false
- creditsOpen = false
+ infoPanelOpen = false
}
}
)
@@ -3094,7 +3307,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
onClick = {
careJournalOpen = !careJournalOpen
if (careJournalOpen) {
- creditsOpen = false
+ infoPanelOpen = false
testWantMenuOpen = false
testBondMenuOpen = false
}
@@ -3143,26 +3356,53 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
else -> careJournalEntries
}
- Row {
- Text(
- text = "Action",
- fontWeight = FontWeight.Bold,
- modifier = Modifier.width(86.dp)
- )
- Text(
- text = "Met",
- fontWeight = FontWeight.Bold,
- modifier = Modifier.width(36.dp)
- )
- Text(
- text = "Resp",
- fontWeight = FontWeight.Bold,
- modifier = Modifier.width(48.dp)
- )
- Text(
- text = "Date",
- fontWeight = FontWeight.Bold
- )
+ when (careJournalTab) {
+ "Needs" -> {
+ Row {
+ Text(
+ text = "Need",
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.width(150.dp)
+ )
+ Text(
+ text = "When",
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+
+ "Wants" -> {
+ Row {
+ Text(
+ text = "Want",
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.weight(1.4f),
+ maxLines = 1,
+ softWrap = false
+ )
+ Text(
+ text = "Resp.",
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.weight(0.8f),
+ maxLines = 1,
+ softWrap = false
+ )
+ Text(
+ text = "When",
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.weight(0.9f),
+ maxLines = 1,
+ softWrap = false
+ )
+ }
+ }
+
+ else -> {
+ Text(
+ text = "All entries",
+ fontWeight = FontWeight.Bold
+ )
+ }
}
if (visibleJournalEntries.isEmpty()) {
@@ -3173,24 +3413,68 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
)
} else {
visibleJournalEntries.take(8).forEach { entry ->
- Row {
- Text(
- text = entry.action.take(10),
- modifier = Modifier.width(86.dp)
- )
- Text(
- text = if (entry.met) "+" else "-",
- modifier = Modifier.width(36.dp)
- )
- Text(
- text = journalResponseText(entry.responseMs),
- modifier = Modifier.width(48.dp)
- )
- Text(text = journalDateText(entry.happenedAtMs))
+ val resultMark = if (entry.met) "✓" else "✕"
+ val actionText = "$resultMark ${entry.action}"
+
+ when (careJournalTab) {
+ "Needs" -> {
+ Row {
+ Text(
+ text = actionText,
+ modifier = Modifier.width(150.dp),
+ maxLines = 1,
+ softWrap = false
+ )
+ Text(text = journalDateText(entry.happenedAtMs))
+ }
+ }
+
+ "Wants" -> {
+ Row {
+ Text(
+ text = actionText,
+ modifier = Modifier.weight(1.4f),
+ maxLines = 1,
+ softWrap = false
+ )
+ Text(
+ text = entry.responseMs?.let {
+ journalResponseText(it)
+ } ?: "—",
+ modifier = Modifier.weight(0.8f),
+ maxLines = 1,
+ softWrap = false
+ )
+ Text(
+ text = journalDateText(entry.happenedAtMs),
+ modifier = Modifier.weight(0.9f),
+ maxLines = 1,
+ softWrap = false
+ )
+ }
+ }
+
+ else -> {
+ val responsePart = if (
+ entry.category == "Want" &&
+ entry.responseMs != null
+ ) {
+ " · ${journalResponseText(entry.responseMs)}"
+ } else {
+ ""
+ }
+
+ Text(
+ text = "${entry.category} $actionText$responsePart · ${journalDateText(entry.happenedAtMs)}",
+ maxLines = 1,
+ softWrap = false
+ )
+ }
}
}
}
}
+
}
}
@@ -3199,7 +3483,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Box(
modifier = Modifier
.fillMaxWidth()
- .height(235.dp)
+ .heightIn(min = 235.dp)
.padding(top = 10.dp, bottom = 10.dp)
.border(2.dp, lcdDark),
contentAlignment = Alignment.Center
@@ -3303,7 +3587,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
}
}
- if (creditsOpen) {
+ if (infoPanelOpen) {
androidx.compose.ui.window.Popup(
alignment = Alignment.TopCenter
) {
@@ -3323,54 +3607,96 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
- text = "CREDITS / LICENSES",
+ text = if (infoPanelType == "HowTo") {
+ "HOW TO"
+ } else {
+ "CREDITS / LICENSES"
+ },
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
- creditsText.lines().forEach { creditLine ->
- if (creditLine.startsWith("Sound page: ")) {
- val creditUrl = creditLine.removePrefix("Sound page: ").trim()
-
+ if (infoPanelType == "HowTo") {
+ howToText.lines().forEach { howToLine ->
Text(
- text = "Sound page:",
- fontSize = 11.sp,
- lineHeight = 13.sp,
- fontFamily = FontFamily.Monospace
- )
-
- Text(
- text = creditUrl,
+ text = howToLine,
fontSize = 11.sp,
lineHeight = 13.sp,
fontFamily = FontFamily.Monospace,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.clickable {
- context.startActivity(
- Intent(
- Intent.ACTION_VIEW,
- Uri.parse(creditUrl)
- )
- )
+ fontWeight = if (
+ howToLine.isNotBlank() &&
+ howToLine == howToLine.uppercase()
+ ) {
+ FontWeight.Bold
+ } else {
+ FontWeight.Normal
}
)
- } else {
- Text(
- text = creditLine,
- fontSize = 11.sp,
- lineHeight = 13.sp,
- fontFamily = FontFamily.Monospace
- )
+ }
+ } else {
+ creditsText.lines().forEach { creditLine ->
+ if (creditLine.startsWith("Sound page: ")) {
+ val creditUrl = creditLine.removePrefix("Sound page: ").trim()
+
+ Text(
+ text = "Sound page:",
+ fontSize = 11.sp,
+ lineHeight = 13.sp,
+ fontFamily = FontFamily.Monospace
+ )
+
+ Text(
+ text = creditUrl,
+ fontSize = 11.sp,
+ lineHeight = 13.sp,
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.clickable {
+ context.startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(creditUrl)
+ )
+ )
+ }
+ )
+ } else if (
+ creditLine.startsWith("https://") ||
+ creditLine.startsWith("http://")
+ ) {
+ Text(
+ text = creditLine,
+ fontSize = 11.sp,
+ lineHeight = 13.sp,
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.clickable {
+ context.startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(creditLine)
+ )
+ )
+ }
+ )
+ } else {
+ Text(
+ text = creditLine,
+ fontSize = 11.sp,
+ lineHeight = 13.sp,
+ fontFamily = FontFamily.Monospace
+ )
+ }
}
}
}
- val creditsCloseOnLeft = creditsCloseButtonCorner in listOf("BottomLeft", "TopLeft")
+ val infoPanelControlsOnLeft = infoPanelButtonCorner in listOf("BottomLeft", "TopLeft")
Row(
modifier = Modifier.align(
- when (creditsCloseButtonCorner) {
+ when (infoPanelButtonCorner) {
"BottomLeft" -> Alignment.BottomStart
"TopLeft" -> Alignment.TopStart
"TopRight" -> Alignment.TopEnd
@@ -3379,7 +3705,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
- if (creditsCloseOnLeft) {
+ if (infoPanelControlsOnLeft) {
Text(
text = "CLOSE",
fontWeight = FontWeight.Bold,
@@ -3387,7 +3713,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
.background(lcdBackground)
.border(1.dp, lcdDark)
.clickable {
- creditsOpen = false
+ infoPanelOpen = false
}
.padding(horizontal = 10.dp, vertical = 6.dp)
)
@@ -3399,7 +3725,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
.background(lcdBackground)
.border(1.dp, lcdDark)
.clickable {
- creditsCloseButtonCorner = when (creditsCloseButtonCorner) {
+ infoPanelButtonCorner = when (infoPanelButtonCorner) {
"BottomRight" -> "BottomLeft"
"BottomLeft" -> "TopLeft"
"TopLeft" -> "TopRight"
@@ -3416,7 +3742,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
.background(lcdBackground)
.border(1.dp, lcdDark)
.clickable {
- creditsCloseButtonCorner = when (creditsCloseButtonCorner) {
+ infoPanelButtonCorner = when (infoPanelButtonCorner) {
"BottomRight" -> "BottomLeft"
"BottomLeft" -> "TopLeft"
"TopLeft" -> "TopRight"
@@ -3433,7 +3759,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
.background(lcdBackground)
.border(1.dp, lcdDark)
.clickable {
- creditsOpen = false
+ infoPanelOpen = false
}
.padding(horizontal = 10.dp, vertical = 6.dp)
)
@@ -3451,8 +3777,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Happiness",
- modifier = Modifier.width(92.dp),
- textAlign = TextAlign.End
+ modifier = Modifier.width(112.dp),
+ textAlign = TextAlign.End,
+ maxLines = 1,
+ softWrap = false
)
Text(text = " $happinessBar")
}
@@ -3460,8 +3788,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Energy",
- modifier = Modifier.width(92.dp),
- textAlign = TextAlign.End
+ modifier = Modifier.width(112.dp),
+ textAlign = TextAlign.End,
+ maxLines = 1,
+ softWrap = false
)
Text(text = " $energyBar")
}
@@ -3469,8 +3799,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Hunger",
- modifier = Modifier.width(92.dp),
- textAlign = TextAlign.End
+ modifier = Modifier.width(112.dp),
+ textAlign = TextAlign.End,
+ maxLines = 1,
+ softWrap = false
)
Text(text = " $hungerBar")
}
@@ -3478,8 +3810,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Dirtiness",
- modifier = Modifier.width(92.dp),
- textAlign = TextAlign.End
+ modifier = Modifier.width(112.dp),
+ textAlign = TextAlign.End,
+ maxLines = 1,
+ softWrap = false
)
Text(text = " $dirtinessBar")
}
@@ -3487,8 +3821,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Potty",
- modifier = Modifier.width(92.dp),
- textAlign = TextAlign.End
+ modifier = Modifier.width(112.dp),
+ textAlign = TextAlign.End,
+ maxLines = 1,
+ softWrap = false
)
Text(text = " $pottyBar")
}
@@ -3496,8 +3832,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
Row {
Text(
text = "Boredom",
- modifier = Modifier.width(92.dp),
- textAlign = TextAlign.End
+ modifier = Modifier.width(112.dp),
+ textAlign = TextAlign.End,
+ maxLines = 1,
+ softWrap = false
)
Text(text = " $boredomBar")
}
@@ -3558,6 +3896,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
LCDSubMenuFrame(lcdDark = lcdDark) {
LCDSubButton(
text = "BATHE",
+ modifier = Modifier.weight(1f),
+ fillWidthFraction = 1f,
enabled = canBathe,
lcdDark = lcdDark,
lcdFaded = lcdFaded,
@@ -3568,6 +3908,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
LCDSubButton(
text = "FEED",
+ modifier = Modifier.weight(1f),
+ fillWidthFraction = 1f,
enabled = canFeed,
lcdDark = lcdDark,
lcdFaded = lcdFaded,
@@ -3578,6 +3920,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
LCDSubButton(
text = "NAP",
+ modifier = Modifier.weight(1f),
+ fillWidthFraction = 1f,
enabled = canNap,
lcdDark = lcdDark,
lcdFaded = lcdFaded,
@@ -3608,6 +3952,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
LCDSubMenuFrame(lcdDark = lcdDark) {
LCDSubButton(
text = "PET",
+ modifier = Modifier.weight(1f),
+ fillWidthFraction = 1f,
enabled = canPet,
lcdDark = lcdDark,
lcdFaded = lcdFaded,
@@ -3618,6 +3964,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
LCDSubButton(
text = "PLAY",
+ modifier = Modifier.weight(1f),
+ fillWidthFraction = 1f,
enabled = canPlay,
lcdDark = lcdDark,
lcdFaded = lcdFaded,
@@ -3628,11 +3976,19 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
LCDHoldSubButton(
text = "TALK",
+ modifier = Modifier.weight(1f),
+ fillWidthFraction = 1f,
enabled = canTalk,
lcdDark = lcdDark,
lcdFaded = lcdFaded,
onPress = {
- beginTalk()
+ if (microphonePermissionGranted) {
+ beginTalk()
+ } else {
+ microphonePermissionLauncher.launch(
+ Manifest.permission.RECORD_AUDIO
+ )
+ }
},
onRelease = {
endTalk()
@@ -3661,6 +4017,28 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
}
)
}
+
+ if (!careMenuOpen && !socializeMenuOpen) {
+ Text(
+ text = "How To",
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 28.dp, bottom = 4.dp)
+ .clickable {
+ infoPanelType = "HowTo"
+ infoPanelOpen = true
+ statsMenuOpen = false
+ careJournalOpen = false
+ testWantMenuOpen = false
+ testBondMenuOpen = false
+ careMenuOpen = false
+ socializeMenuOpen = false
+ }
+ )
+ }
+ }
}
}
}
@@ -3673,4 +4051,4 @@ fun GreetingPreview() {
LCDPetTheme {
Greeting("LCDPet")
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt b/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt
index 29ba206..29a6baf 100644
--- a/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt
+++ b/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt
@@ -33,6 +33,7 @@ data class LCDPetRecordSave(
val isTradingInPet: Boolean = false,
val isFindingNewPet: Boolean = false,
val tradeInProgress: Int = 0,
+ val tradeInStartedAtMs: Long = 0L,
val currentWant: String = "",
val petName: String = "",
val petTemplateId: String = "classic",
@@ -65,6 +66,7 @@ private object LCDPetSaveKeys {
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 TRADE_IN_STARTED_AT_MS = longPreferencesKey("trade_in_started_at_ms")
val CURRENT_WANT = stringPreferencesKey("current_want")
val PET_NAME = stringPreferencesKey("pet_name")
val PET_TEMPLATE_ID = stringPreferencesKey("pet_template_id")
@@ -99,6 +101,7 @@ val Context.lcdPetRecordSaveFlow: Flow
isTradingInPet = preferences[LCDPetSaveKeys.IS_TRADING_IN_PET] ?: false,
isFindingNewPet = preferences[LCDPetSaveKeys.IS_FINDING_NEW_PET] ?: false,
tradeInProgress = preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] ?: 0,
+ tradeInStartedAtMs = preferences[LCDPetSaveKeys.TRADE_IN_STARTED_AT_MS] ?: 0L,
currentWant = preferences[LCDPetSaveKeys.CURRENT_WANT] ?: "",
petName = preferences[LCDPetSaveKeys.PET_NAME] ?: "",
petTemplateId = preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] ?: "classic",
@@ -132,6 +135,7 @@ suspend fun Context.saveLCDPetStats(
isTradingInPet: Boolean,
isFindingNewPet: Boolean,
tradeInProgress: Int,
+ tradeInStartedAtMs: Long = 0L,
currentWant: String,
petName: String,
petTemplateId: String = "classic",
@@ -162,6 +166,7 @@ suspend fun Context.saveLCDPetStats(
preferences[LCDPetSaveKeys.IS_TRADING_IN_PET] = isTradingInPet
preferences[LCDPetSaveKeys.IS_FINDING_NEW_PET] = isFindingNewPet
preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] = tradeInProgress
+ preferences[LCDPetSaveKeys.TRADE_IN_STARTED_AT_MS] = tradeInStartedAtMs
preferences[LCDPetSaveKeys.CURRENT_WANT] = currentWant
preferences[LCDPetSaveKeys.PET_NAME] = petName
preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] = petTemplateId
diff --git a/app/src/main/java/today/notfeelingit/lcdpet/ui/theme/Theme.kt b/app/src/main/java/today/notfeelingit/lcdpet/ui/theme/Theme.kt
index 4dc1c5c..fec268b 100644
--- a/app/src/main/java/today/notfeelingit/lcdpet/ui/theme/Theme.kt
+++ b/app/src/main/java/today/notfeelingit/lcdpet/ui/theme/Theme.kt
@@ -1,21 +1,8 @@
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,
@@ -35,23 +22,10 @@ private val LightColorScheme = lightColorScheme(
@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,
+ colorScheme = LightColorScheme,
typography = Typography,
content = content
)