Initial LCDPet v1.0 release candidate
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">LCDPet</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.LCDPet" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||