dev #1

Merged
rayc-root merged 4 commits from dev into main 2024-03-04 17:35:25 +08:00
13 changed files with 462 additions and 45 deletions

View File

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="gradleHome" value="/usr/local/Cellar/gradle/8.4/libexec" />
<option name="gradleJvm" value="17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View File

@ -0,0 +1,32 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -1,6 +1,7 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("io.objectbox")
}
android {
@ -67,5 +68,17 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
// google fonts 感觉有些字体用不了
implementation("androidx.compose.ui:ui-text-google-fonts:1.6.1")
// implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")
// implementation("androidx.compose.runtime:runtime-livedata:1.7.0")
// implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
// view model
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
implementation("com.google.accompanist:accompanist-systemuicontroller:0.27.0")
}

View File

@ -3,12 +3,12 @@
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".WeightTrackApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/weight"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WeightTrack"
tools:targetApi="31">

View File

@ -1,30 +1,51 @@
package com.eacenic.weighttrack
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
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.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DatePicker
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@ -32,18 +53,27 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.eacenic.weighttrack.ui.theme.WeightTrackTheme
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WeightTrackTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
@ -55,77 +85,276 @@ class MainActivity : ComponentActivity() {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WeightTrackLayout() {
@RequiresApi(Build.VERSION_CODES.O)
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
val scope = rememberCoroutineScope()
@SuppressLint("StateFlowValueCalledInComposition")
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun WeightTrackLayout(
weightTrackViewModel: WeightTrackViewModel = viewModel()
) {
val systemUIController = rememberSystemUiController()
LaunchedEffect(Unit) {
systemUIController.setStatusBarColor(Color(0xFF424242))
}
val weightRecords by weightTrackViewModel.historyData.collectAsState()
val newestRecord by weightTrackViewModel.newestRecord.collectAsState()
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
var bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var curRecord by rememberSaveable { mutableStateOf<WeightRecord?>(null) }
weightTrackViewModel.loadWeightRecords()
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.Gray
color = Color.White
) {
Column {
Row {
CurrentWeight()
}
Button(onClick = {
Column(
modifier = Modifier.padding(horizontal = 20.dp)
) {
CurrentWeight(newestRecord, Modifier.padding(top = 20.dp))
LazyColumn(
modifier = Modifier.fillMaxHeight(0.9f)
) {
this.items(weightRecords) { it ->
// Text(text = "${it.weight}")
WeightRecordItem(
weightRecord = it,
onClickEdit = { record ->
curRecord = record
openBottomSheet = true
}) {
Text(text = "添加")
}
)
}
}
EditButton(onClick = { openBottomSheet = true })
}
if (openBottomSheet) {
ModalBottomSheet(
onDismissRequest = { openBottomSheet = false },
sheetState = bottomSheetState
) {
Row {
Button(
onClick = {
scope.launch { bottomSheetState.hide() }.invokeOnCompletion {
if (!bottomSheetState.isVisible) {
BottomSheetEditor(
onDismiss = {
openBottomSheet = false
curRecord = null
},
curRecord = curRecord
)
}
}
}
) {
Text(text = "关闭")
}
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun WeightRecordItem(
weightRecord: WeightRecord,
onClickEdit: (curRecord: WeightRecord) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${weightRecord.weight}",
fontFamily = doodleShadowFontFamily,
fontSize = 36.sp,
modifier = Modifier.weight(1f)
)
Text(
text = dateTimeFormatter.format(weightRecord.date),
fontFamily = doodleShadowFontFamily,
fontSize = 18.sp,
modifier = Modifier.padding(end = 20.dp)
)
IconButton(
onClick = { onClickEdit(weightRecord) },
modifier = Modifier.padding(horizontal = 5.dp)
) {
Icon(Icons.Filled.Edit, contentDescription = "edit")
}
}
}
@Composable
fun CurrentWeight() {
fun CurrentWeight(
curRecord: WeightRecord? = null,
modifier: Modifier = Modifier
) {
Row(
modifier = Modifier
modifier = modifier
.wrapContentHeight()
.padding(20.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.fillMaxWidth(0.9f)
.fillMaxWidth()
.height(120.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.Cyan)
.background(Color(0xFF424242))
) {
Text(
text = "62.6",
text = "${curRecord?.weight ?: "No Data"}",
modifier = Modifier.align(Alignment.Center),
fontSize = 80.sp,
color = Color.White,
fontFamily = doodleShadowFontFamily
)
}
}
}
@Composable
fun EditButton(
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Button(
modifier = Modifier
.height(48.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(5.dp),
colors = ButtonDefaults.buttonColors(Color(0xFF424242)),
onClick = onClick,
) {
Text(
text = "ADD",
color = Color.White,
fontSize = 36.sp,
fontFamily = doodleShadowFontFamily
)
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomSheetEditor(
onDismiss: () -> Unit,
weightTrackViewModel: WeightTrackViewModel = viewModel(),
curRecord: WeightRecord? = null
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var text by remember { mutableStateOf("${curRecord?.weight ?: ""}") }
var clickable = text.isNotEmpty()
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = curRecord?.date?.toInstant(ZoneOffset.UTC)?.toEpochMilli()
)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = bottomSheetState
) {
Column(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
OutlinedTextField(
value = text,
modifier = Modifier.fillMaxWidth(),
onValueChange = {
val newVal = it.toFloatOrNull()
if ((it.isNotEmpty() && newVal == null) || (newVal != null && newVal > 1000f)) {
Toast.makeText(
context,
"只能输入数字且要求数字小于1000",
Toast.LENGTH_SHORT
).show()
return@OutlinedTextField
}
if (it.length > 6) {
Toast.makeText(
context,
"数字长度不能超过6位",
Toast.LENGTH_SHORT
).show()
return@OutlinedTextField
}
text = it
clickable = text.isNotEmpty()
},
)
}
Spacer(
modifier = Modifier
.height(20.dp)
.fillMaxWidth()
)
Row(
modifier = Modifier.fillMaxWidth()
) {
DatePicker(
state = datePickerState,
modifier = Modifier.padding(16.dp),
title = { Text(text = "称重日期") },
)
}
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = {
scope.launch { bottomSheetState.hide() }.invokeOnCompletion {
if (!bottomSheetState.isVisible) {
onDismiss()
}
}
}
) {
Text(text = "CANCEL", fontSize = 18.sp, fontFamily = doodleShadowFontFamily)
}
Spacer(modifier = Modifier.size(60.dp))
Button(
onClick = {
val newDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(datePickerState.selectedDateMillis!!),
ZoneId.systemDefault()
)
val newRecord = curRecord?.copy(weight = text.toFloat(), date = newDate)
?: WeightRecord(weight = text.toFloat(), date = newDate)
weightTrackViewModel.addWeightRecord(newRecord)
text = ""
onDismiss()
},
enabled = clickable
) {
Text(text = "CONFIRM", fontSize = 18.sp, fontFamily = doodleShadowFontFamily)
}
}
Spacer(modifier = Modifier.size(40.dp))
}
}
}

View File

@ -0,0 +1,35 @@
package com.eacenic.weighttrack
import android.os.Build
import androidx.annotation.RequiresApi
import io.objectbox.annotation.Convert
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import io.objectbox.converter.PropertyConverter
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Entity
data class WeightRecord @RequiresApi(Build.VERSION_CODES.O) constructor(
@Id
var id: Long = 0,
var weight: Float,
@Convert(converter = WeightRecordConverter::class, dbType = String::class)
var date: LocalDateTime? = null,
var isDeleted: Boolean = false
)
@RequiresApi(Build.VERSION_CODES.O)
val ymdHmsFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
class WeightRecordConverter: PropertyConverter<LocalDateTime?, String?> {
@RequiresApi(Build.VERSION_CODES.O)
override fun convertToEntityProperty(databaseValue: String?): LocalDateTime {
return LocalDateTime.parse(databaseValue, ymdHmsFormatter)
}
@RequiresApi(Build.VERSION_CODES.O)
override fun convertToDatabaseValue(entityProperty: LocalDateTime?): String {
return entityProperty?.format(ymdHmsFormatter) ?: ""
}
}

View File

@ -0,0 +1,12 @@
package com.eacenic.weighttrack
import android.app.Application
class WeightTrackApp: Application() {
override fun onCreate() {
super.onCreate()
WeightTrackObjectBox.init(this)
}
}

View File

@ -0,0 +1,17 @@
package com.eacenic.weighttrack
import android.content.Context
import io.objectbox.BoxStore
object WeightTrackObjectBox {
lateinit var store: BoxStore
private set
fun init(context: Context) {
store = MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
}
}

View File

@ -0,0 +1,43 @@
package com.eacenic.weighttrack
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.lifecycle.ViewModel
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.time.LocalDateTime
const val TAG = "WeightTrackViewModel"
class WeightTrackViewModel: ViewModel() {
@RequiresApi(Build.VERSION_CODES.O)
private var _historyData = MutableStateFlow<List<WeightRecord>>(mutableListOf<WeightRecord>())
@RequiresApi(Build.VERSION_CODES.O)
val historyData: StateFlow<List<WeightRecord>> = _historyData
private var _curRecord = MutableStateFlow<WeightRecord?>(null)
val newestRecord = _curRecord
@RequiresApi(Build.VERSION_CODES.O)
fun addWeightRecord(newRecord: WeightRecord) {
saveWeightRecord(newRecord)
loadWeightRecords()
}
private val weightBox = WeightTrackObjectBox.store.boxFor(WeightRecord::class)
private fun saveWeightRecord(weightRecord: WeightRecord) {
weightBox.put(weightRecord)
}
@RequiresApi(Build.VERSION_CODES.O)
fun loadWeightRecords() {
val storedData = weightBox.all
storedData.sortByDescending { it.date }
_historyData.value = storedData
if (storedData.isNotEmpty()) {
_curRecord.value = storedData[0]
}
}
}

View File

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="297dp"
android:height="210dp"
android:viewportWidth="297"
android:viewportHeight="210">
<path
android:pathData="M21.15,51.45L21.15,51.45A24.41,14.3 90,0 1,35.44 75.85L35.44,133.96A24.41,14.3 90,0 1,21.15 158.37L21.15,158.37A24.41,14.3 90,0 1,6.85 133.96L6.85,75.85A24.41,14.3 90,0 1,21.15 51.45z"
android:strokeWidth="0.59"
android:fillColor="#000000"/>
<path
android:pathData="M115.64,88.33l63.41,0l0,33.15l-63.41,0z"
android:strokeWidth="0.49"
android:fillColor="#000000"/>
<path
android:pathData="m76.68,15.39c-22.04,0 -39.78,18.23 -39.78,40.87l0,97.3c0,22.64 17.74,40.87 39.78,40.87 22.04,0 39.78,-18.23 39.78,-40.87L116.46,56.26c0,-22.64 -17.74,-40.87 -39.78,-40.87zM76.68,52.69c7.35,0 13.26,10.63 13.26,23.84l0,56.76c0,13.21 -5.92,23.84 -13.26,23.84 -7.35,0 -13.26,-10.63 -13.26,-23.84L63.42,76.53c0,-13.21 5.91,-23.84 13.26,-23.84z"
android:strokeWidth="1.28"
android:fillColor="#666666"/>
<path
android:pathData="m218.41,15.39c-22.04,0 -39.78,18.23 -39.78,40.87l0,97.3c0,22.64 17.74,40.87 39.78,40.87 22.04,0 39.78,-18.23 39.78,-40.87L258.2,56.26c0,-22.64 -17.74,-40.87 -39.78,-40.87zM218.41,52.69c7.35,0 13.26,10.63 13.26,23.84l0,56.76c0,13.21 -5.92,23.84 -13.26,23.84 -7.35,0 -13.26,-10.63 -13.26,-23.84L205.15,76.53c0,-13.21 5.91,-23.84 13.26,-23.84z"
android:strokeWidth="1.28"
android:fillColor="#666666"/>
<path
android:pathData="M274.77,51.45L274.77,51.45A24.41,14.3 90,0 1,289.07 75.85L289.07,133.96A24.41,14.3 90,0 1,274.77 158.37L274.77,158.37A24.41,14.3 90,0 1,260.48 133.96L260.48,75.85A24.41,14.3 90,0 1,274.77 51.45z"
android:strokeWidth="0.59"
android:fillColor="#000000"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -1,4 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("io.objectbox:objectbox-gradle-plugin:3.8.0")
}
}
plugins {
id("com.android.application") version "8.2.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false