From fe566ab88b3634af5a5193ef30c8586383a8cf46 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 13 Jul 2026 13:14:26 -0400 Subject: [PATCH] Prepare LCDPet 1.1 update --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 1 + app/src/main/assets/audio_credits.txt | 15 +- app/src/main/assets/how_to.txt | 223 ++++++ .../today/notfeelingit/lcdpet/MainActivity.kt | 666 ++++++++++++++---- .../today/notfeelingit/lcdpet/PetStorage.kt | 5 + .../notfeelingit/lcdpet/ui/theme/Theme.kt | 28 +- 7 files changed, 768 insertions(+), 174 deletions(-) create mode 100644 app/src/main/assets/how_to.txt 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 )