大法师

This commit is contained in:
coco
2026-07-17 15:37:35 +08:00
parent 5a58127ee5
commit 0b5ad29d15
571 changed files with 5751 additions and 545 deletions
@@ -78,7 +78,9 @@ fun PinkOVApp() {
AppDestinations.HOME -> HomePage(
onScanClick = { showScanPage = true }
)
AppDestinations.RECORDS -> RecordsPage()
AppDestinations.RECORDS -> RecordsPage(
onScanClick = { showScanPage = true }
)
AppDestinations.MINE -> MinePage()
}
}
@@ -15,7 +15,7 @@ import com.pinkbear.pinkov.data.entity.ScanRecord
@Database(
entities = [ScanRecord::class, PeriodSettings::class, DailyRecord::class],
version = 2,
version = 4,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
@@ -56,6 +56,78 @@ abstract class AppDatabase : RoomDatabase() {
}
}
private val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
// Recreate daily_records to replace follicle_size/follicle_time with
// the 4 new follicle_left_a/b / follicle_right_a/b columns.
db.execSQL("""
CREATE TABLE IF NOT EXISTS `daily_records_new` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`date` INTEGER NOT NULL,
`is_period_start` INTEGER NOT NULL DEFAULT 0,
`had_sex` INTEGER NOT NULL DEFAULT 0,
`mood` TEXT,
`discharge` TEXT,
`note` TEXT,
`temperature` REAL,
`temperature_time` INTEGER,
`sex_method` TEXT,
`sex_time` INTEGER,
`symptoms` TEXT,
`supplements` TEXT,
`weight` REAL,
`weight_time` INTEGER,
`habits` TEXT,
`stool_type` TEXT,
`plan` TEXT,
`follicle_left_a` INTEGER,
`follicle_left_b` INTEGER,
`follicle_right_a` INTEGER,
`follicle_right_b` INTEGER
)
""".trimIndent())
// Copy existing data (old follicle_size/follicle_time are dropped)
db.execSQL("""
INSERT INTO `daily_records_new` (
`id`, `date`, `is_period_start`, `had_sex`, `mood`, `discharge`, `note`,
`temperature`, `temperature_time`, `sex_method`, `sex_time`,
`symptoms`, `supplements`, `weight`, `weight_time`, `habits`,
`stool_type`, `plan`
) SELECT
`id`, `date`, `is_period_start`, `had_sex`, `mood`, `discharge`, `note`,
`temperature`, `temperature_time`, `sex_method`, `sex_time`,
`symptoms`, `supplements`, `weight`, `weight_time`, `habits`,
`stool_type`, `plan`
FROM `daily_records`
""".trimIndent())
db.execSQL("DROP TABLE `daily_records`")
db.execSQL("ALTER TABLE `daily_records_new` RENAME TO `daily_records`")
db.execSQL(
"CREATE UNIQUE INDEX IF NOT EXISTS `index_daily_records_date` ON `daily_records` (`date`)"
)
}
}
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `temperature` REAL DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `temperature_time` INTEGER DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `sex_method` TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `sex_time` INTEGER DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `follicle_size` REAL DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `follicle_time` INTEGER DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `symptoms` TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `supplements` TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `weight` REAL DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `weight_time` INTEGER DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `habits` TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `stool_type` TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE `daily_records` ADD COLUMN `plan` TEXT DEFAULT NULL")
}
}
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
@@ -63,7 +135,7 @@ abstract class AppDatabase : RoomDatabase() {
AppDatabase::class.java,
"pinkov_database"
)
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.build()
.also { INSTANCE = it }
}
@@ -24,19 +24,91 @@ data class DailyRecord(
@ColumnInfo(name = "is_period_start")
val isPeriodStart: Boolean = false,
// ---- 基础体温 ----
/** Basal body temperature in Celsius. Null = not recorded. */
@ColumnInfo(name = "temperature")
val temperature: Float? = null,
/** Time of temperature measurement (epoch millis). */
@ColumnInfo(name = "temperature_time")
val temperatureTime: Long? = null,
// ---- 爱爱 ----
/** User recorded sexual activity on this date. */
@ColumnInfo(name = "had_sex")
val hadSex: Boolean = false,
/** Mood tag (e.g. 焦虑, 期待, 低落, 放松). Null = not recorded. */
@ColumnInfo(name = "mood")
val mood: String? = null,
/** Sex method (e.g. 无措施, 避孕套, 体外排精…). Null = not recorded. */
@ColumnInfo(name = "sex_method")
val sexMethod: String? = null,
/** Cervical mucus / discharge description. Null = not recorded. */
/** Time of sexual activity (epoch millis). */
@ColumnInfo(name = "sex_time")
val sexTime: Long? = null,
// ---- 白带 ----
/** Cervical mucus / discharge type. Null = not recorded. */
@ColumnInfo(name = "discharge")
val discharge: String? = null,
/** Free-text notes for this date. */
// ---- 卵泡监测 ----
/** Left follicle width in mm. Null = not recorded. */
@ColumnInfo(name = "follicle_left_a")
val follicleLeftA: Int? = null,
/** Left follicle height in mm. Null = not recorded. */
@ColumnInfo(name = "follicle_left_b")
val follicleLeftB: Int? = null,
/** Right follicle width in mm. Null = not recorded. */
@ColumnInfo(name = "follicle_right_a")
val follicleRightA: Int? = null,
/** Right follicle height in mm. Null = not recorded. */
@ColumnInfo(name = "follicle_right_b")
val follicleRightB: Int? = null,
// ---- 症状 ----
/** Symptom tags, comma-separated. Null = not recorded. */
@ColumnInfo(name = "symptoms")
val symptoms: String? = null,
// ---- 心情 ----
/** Mood tag (e.g. 超开心, 焦虑, 低落, 放松). Null = not recorded. */
@ColumnInfo(name = "mood")
val mood: String? = null,
// ---- 营养补充 ----
/** Supplement tags, comma-separated. Null = not recorded. */
@ColumnInfo(name = "supplements")
val supplements: String? = null,
// ---- 体重 ----
/** Body weight in kg. Null = not recorded. */
@ColumnInfo(name = "weight")
val weight: Float? = null,
/** Time of weight measurement (epoch millis). */
@ColumnInfo(name = "weight_time")
val weightTime: Long? = null,
// ---- 日记 ----
/** Free-text diary entry for this date. */
@ColumnInfo(name = "note")
val note: String? = null,
// ---- 好习惯 ----
/** Habit tags, comma-separated. Null = not recorded. */
@ColumnInfo(name = "habits")
val habits: String? = null,
// ---- 便便 ----
/** Stool type (e.g. 正常, 稀便, 干便…). Null = not recorded. */
@ColumnInfo(name = "stool_type")
val stoolType: String? = null,
// ---- 计划 ----
/** Plan text for this date. Null = not recorded. */
@ColumnInfo(name = "plan")
val plan: String? = null,
)
@@ -1,42 +1,84 @@
package com.pinkbear.pinkov.ui.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.width
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.pinkbear.pinkov.R
import com.pinkbear.pinkov.data.entity.DailyRecord
import com.pinkbear.pinkov.ui.component.checkin.DiarySheet
import com.pinkbear.pinkov.ui.component.checkin.DischargeSheet
import com.pinkbear.pinkov.ui.component.checkin.FollicleSheet
import com.pinkbear.pinkov.ui.component.checkin.HabitSheet
import com.pinkbear.pinkov.ui.component.checkin.LoveSheet
import com.pinkbear.pinkov.ui.component.checkin.MoodSheet
import com.pinkbear.pinkov.ui.component.checkin.PlanSheet
import com.pinkbear.pinkov.ui.component.checkin.StoolSheet
import com.pinkbear.pinkov.ui.component.checkin.SupplementSheet
import com.pinkbear.pinkov.ui.component.checkin.SymptomSheet
import com.pinkbear.pinkov.ui.component.checkin.TemperatureSheet
import com.pinkbear.pinkov.ui.component.checkin.WeightSheet
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.Locale
/**
* Which check-in bottom sheet is currently open.
*/
private enum class CheckInSheet {
TEMPERATURE, LOVE, DISCHARGE, FOLLICLE,
SYMPTOM, MOOD, SUPPLEMENT, WEIGHT,
DIARY, HABIT, STOOL, PLAN
}
/**
* Bottom panel showing check-in items for the selected calendar date.
*
* When no date is selected, shows a hint. When a date is selected,
* shows the "月经来了" switch and (in future) more check-in items.
*/
@Composable
fun CheckInPanel(
selectedDate: LocalDate?,
record: DailyRecord?,
onPeriodStartChanged: (LocalDate, Boolean) -> Unit,
onScanClick: () -> Unit,
onTemperatureSave: (LocalDate, Float, Long) -> Unit,
onLoveSave: (LocalDate, String, Long) -> Unit,
onDischargeSave: (LocalDate, String) -> Unit,
onFollicleSave: (LocalDate, Int, Int, Int, Int) -> Unit,
onSymptomSave: (LocalDate, String) -> Unit,
onMoodSave: (LocalDate, String) -> Unit,
onSupplementSave: (LocalDate, String) -> Unit,
onWeightSave: (LocalDate, Float, Long) -> Unit,
onDiarySave: (LocalDate, String) -> Unit,
onHabitSave: (LocalDate, String) -> Unit,
onStoolSave: (LocalDate, String) -> Unit,
onPlanSave: (LocalDate, String) -> Unit,
modifier: Modifier = Modifier,
) {
val formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日", Locale.CHINESE)
var openSheet by remember { mutableStateOf<CheckInSheet?>(null) }
Column(
modifier = modifier
@@ -69,25 +111,233 @@ fun CheckInPanel(
onPeriodStartChanged(selectedDate, checked)
},
)
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant,
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// ---- Placeholder items (future) ----
PlaceholderCheckInRow(label = "爱爱")
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant,
// ---- 13 check-in entries ----
// 1. 排卵试纸 → navigate to scan
CheckInEntryRow(
iconRes = R.drawable.record_icon_pailuan,
label = "排卵试纸",
valueText = null, // never shows saved value, always navigates
onClick = onScanClick,
)
PlaceholderCheckInRow(label = "心情")
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant,
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 2. 基础体温
CheckInEntryRow(
iconRes = R.drawable.record_icon_tiwen_no,
label = "基础体温",
valueText = record?.temperature?.let { "${it}°C" },
onClick = { openSheet = CheckInSheet.TEMPERATURE },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 3. 爱爱
CheckInEntryRow(
iconRes = R.drawable.record_icon_aiai,
label = "爱爱",
valueText = record?.sexMethod,
onClick = { openSheet = CheckInSheet.LOVE },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 4. 白带
CheckInEntryRow(
iconRes = R.drawable.record_icon_baidai,
label = "白带",
valueText = record?.discharge,
onClick = { openSheet = CheckInSheet.DISCHARGE },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 5. 卵泡监测
CheckInEntryRow(
iconRes = R.drawable.record_icon_pailuan,
label = "卵泡监测",
valueText = buildFollicleDisplay(record),
onClick = { openSheet = CheckInSheet.FOLLICLE },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 6. 症状
CheckInEntryRow(
iconRes = R.drawable.record_icon_ill,
label = "症状",
valueText = record?.symptoms,
onClick = { openSheet = CheckInSheet.SYMPTOM },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 7. 心情
CheckInEntryRow(
iconRes = R.drawable.record_icon_xinqing,
label = "心情",
valueText = record?.mood,
onClick = { openSheet = CheckInSheet.MOOD },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 8. 营养补充
CheckInEntryRow(
iconRes = R.drawable.record_icon_help,
label = "营养补充",
valueText = record?.supplements,
onClick = { openSheet = CheckInSheet.SUPPLEMENT },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 9. 体重
CheckInEntryRow(
iconRes = R.drawable.record_icon_tizhong_no,
label = "体重",
valueText = record?.weight?.let { "${it}kg" },
onClick = { openSheet = CheckInSheet.WEIGHT },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 10. 日记
CheckInEntryRow(
iconRes = R.drawable.record_icon_riji,
label = "日记",
valueText = record?.note,
onClick = { openSheet = CheckInSheet.DIARY },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 11. 好习惯
CheckInEntryRow(
iconRes = R.drawable.record_icon_habite,
label = "好习惯",
valueText = record?.habits,
onClick = { openSheet = CheckInSheet.HABIT },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 12. 便便
CheckInEntryRow(
iconRes = R.drawable.record_icon_sleep,
label = "便便",
valueText = record?.stoolType,
onClick = { openSheet = CheckInSheet.STOOL },
)
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
// 13. 计划
CheckInEntryRow(
iconRes = R.drawable.record_icon_tool,
label = "计划",
valueText = record?.plan,
onClick = { openSheet = CheckInSheet.PLAN },
)
PlaceholderCheckInRow(label = "白带")
}
}
// ---- Bottom Sheets ----
when (openSheet) {
CheckInSheet.TEMPERATURE -> TemperatureSheet(
initialTemp = record?.temperature,
initialTime = record?.temperatureTime,
onSave = { temp, time ->
selectedDate?.let { onTemperatureSave(it, temp, time) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.LOVE -> LoveSheet(
initialMethod = record?.sexMethod,
initialTime = record?.sexTime,
onSave = { method, time ->
selectedDate?.let { onLoveSave(it, method, time) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.DISCHARGE -> DischargeSheet(
initialType = record?.discharge,
onSave = { type ->
selectedDate?.let { onDischargeSave(it, type) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.FOLLICLE -> FollicleSheet(
initialLeftA = record?.follicleLeftA,
initialLeftB = record?.follicleLeftB,
initialRightA = record?.follicleRightA,
initialRightB = record?.follicleRightB,
onSave = { leftA, leftB, rightA, rightB ->
selectedDate?.let { onFollicleSave(it, leftA, leftB, rightA, rightB) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.SYMPTOM -> SymptomSheet(
initialSymptoms = record?.symptoms,
onSave = { symptoms ->
selectedDate?.let { onSymptomSave(it, symptoms) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.MOOD -> MoodSheet(
initialMood = record?.mood,
onSave = { mood ->
selectedDate?.let { onMoodSave(it, mood) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.SUPPLEMENT -> SupplementSheet(
initialSupplements = record?.supplements,
onSave = { supplements ->
selectedDate?.let { onSupplementSave(it, supplements) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.WEIGHT -> WeightSheet(
initialWeight = record?.weight,
initialTime = record?.weightTime,
onSave = { weight, time ->
selectedDate?.let { onWeightSave(it, weight, time) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.DIARY -> DiarySheet(
initialNote = record?.note,
onSave = { note ->
selectedDate?.let { onDiarySave(it, note) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.HABIT -> HabitSheet(
initialHabits = record?.habits,
onSave = { habits ->
selectedDate?.let { onHabitSave(it, habits) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.STOOL -> StoolSheet(
initialType = record?.stoolType,
onSave = { type ->
selectedDate?.let { onStoolSave(it, type) }
openSheet = null
},
onDismiss = { openSheet = null },
)
CheckInSheet.PLAN -> PlanSheet(
initialPlan = record?.plan,
onSave = { plan ->
selectedDate?.let { onPlanSave(it, plan) }
openSheet = null
},
onDismiss = { openSheet = null },
)
null -> { /* No sheet open */ }
}
}
@Composable
@@ -112,24 +362,71 @@ private fun CheckInSwitchRow(
}
}
/**
* A single check-in entry row: icon | label | value/arrow.
*/
@Composable
private fun PlaceholderCheckInRow(label: String) {
private fun CheckInEntryRow(
iconRes: Int,
label: String,
valueText: String?,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(iconRes),
contentDescription = label,
modifier = Modifier.size(22.dp),
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = label,
fontSize = 15.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f),
)
Text(
text = "",
fontSize = 15.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
if (valueText != null) {
Text(
text = valueText,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
)
Spacer(modifier = Modifier.width(4.dp))
}
Icon(
painter = painterResource(R.drawable.record_icon_right_arrow),
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
/**
* Build display text for follicle data matching Meiyou's format:
* "左12×10mm", "右15×13mm", or "左12×10 右15×13mm".
*/
private fun buildFollicleDisplay(record: DailyRecord?): String? {
if (record == null) return null
val left = record.follicleLeftA?.takeIf { it > 0 }
val leftB = record.follicleLeftB?.takeIf { it > 0 }
val right = record.follicleRightA?.takeIf { it > 0 }
val rightB = record.follicleRightB?.takeIf { it > 0 }
val leftStr = if (left != null && leftB != null) "${left}×${leftB}mm" else null
val rightStr = if (right != null && rightB != null) "${right}×${rightB}mm" else null
return when {
leftStr != null && rightStr != null -> "$leftStr $rightStr"
leftStr != null -> leftStr
rightStr != null -> rightStr
else -> null
}
}
@@ -0,0 +1,28 @@
package com.pinkbear.pinkov.ui.component.checkin
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
/**
* Generic bottom sheet wrapper for check-in forms.
*
* Provides a consistent [ModalBottomSheet] with skip-scrim-area behavior.
* Content is rendered via [content] slot.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CheckInBottomSheet(
onDismiss: () -> Unit,
content: @Composable ColumnScope.() -> Unit,
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
content = content,
)
}
@@ -0,0 +1,39 @@
package com.pinkbear.pinkov.ui.component.checkin
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun DiarySheet(
initialNote: String?,
onSave: (String) -> Unit,
onDismiss: () -> Unit,
) {
var note by remember { mutableStateOf(initialNote ?: "") }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("日记", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = note,
onValueChange = { note = it },
label = { Text("写点什么呢…") },
modifier = Modifier.fillMaxWidth().height(160.dp),
maxLines = 8,
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { onSave(note) },
modifier = Modifier.fillMaxWidth(),
enabled = note.isNotBlank(),
) { Text("保存") }
Spacer(modifier = Modifier.height(8.dp))
}
}
@@ -0,0 +1,178 @@
package com.pinkbear.pinkov.ui.component.checkin
import com.pinkbear.pinkov.R
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* One of the 5 leukorrhea / cervical-mucus types, matching Meiyou's data model.
*/
private data class DischargeOption(
val code: Int,
val iconRes: Int,
val name: String,
val description: String,
)
/** The 5 discharge types, kept in Meiyou's V2 dialog order. */
private val DISCHARGE_OPTIONS = listOf(
DischargeOption(3, R.drawable.icon_leukorrhea_ganzao, "干燥", "内裤干爽,分泌物量少,无水分"),
DischargeOption(2, R.drawable.icon_leukorrhea_nianchou, "粘稠", "分泌物稠厚,触感像胶状物"),
DischargeOption(4, R.drawable.icon_leukorrhea_xichou, "稀糊状", "含水量偏多偏黄色,触感像乳液"),
DischargeOption(5, R.drawable.icon_leukorrhea_shuizhuang, "水状", "呈白色或透明,触感似水滴"),
DischargeOption(1, R.drawable.icon_leukorrhea_danqing, "蛋清状", "呈透明状,两指间可拉丝"),
)
/** Pink accent used for the selected ring — matches Meiyou's #ff94b8. */
private val LeukorrheaPink = Color(0xFFFF94B8)
/**
* Bottom sheet for recording cervical mucus / discharge type.
*
* Mimics Meiyou's [LeukorrheaAbBottomDialog] (V2): 5 icons in a horizontal row,
* each with a circular pink border when selected plus a check-mark overlay.
* Tap a selected item to deselect it (back to "unrecorded").
*/
@Composable
fun DischargeSheet(
initialType: String?,
onSave: (String) -> Unit,
onDismiss: () -> Unit,
) {
// Resolve the initial type name → code for pre-selection
val initialCode = remember(initialType) {
DISCHARGE_OPTIONS.firstOrNull { it.name == initialType }?.code
}
var selectedCode by remember { mutableStateOf(initialCode) }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("白带", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(20.dp))
// ---- 5-icon horizontal row ----
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
DISCHARGE_OPTIONS.forEach { option ->
val isSelected = selectedCode == option.code
DischargeIconItem(
option = option,
selected = isSelected,
onClick = {
selectedCode = if (isSelected) null else option.code
},
)
}
}
// ---- Description for the selected type ----
val selectedDesc = DISCHARGE_OPTIONS.firstOrNull { it.code == selectedCode }?.description
if (selectedDesc != null) {
Spacer(modifier = Modifier.height(16.dp))
Surface(
shape = RoundedCornerShape(8.dp),
color = LeukorrheaPink.copy(alpha = 0.08f),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = selectedDesc,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(12.dp),
)
}
}
Spacer(modifier = Modifier.height(20.dp))
// ---- Save button ----
Button(
onClick = {
val name = DISCHARGE_OPTIONS.firstOrNull { it.code == selectedCode }?.name ?: ""
onSave(name)
},
modifier = Modifier.fillMaxWidth(),
enabled = selectedCode != null,
) {
Text("保存")
}
Spacer(modifier = Modifier.height(8.dp))
}
}
/**
* Single discharge icon cell: circle background (pink ring when selected),
* icon image, check overlay at bottom-right when selected, name label below.
*/
@Composable
private fun DischargeIconItem(
option: DischargeOption,
selected: Boolean,
onClick: () -> Unit,
) {
val ringWidth = if (selected) 2.dp else 0.dp
val ringColor = if (selected) LeukorrheaPink else Color.Transparent
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clickable { onClick() }
.padding(4.dp),
) {
// Circular icon with optional pink ring + check overlay
Box(
modifier = Modifier
.size(52.dp)
.border(ringWidth, ringColor, CircleShape)
.padding(4.dp),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(option.iconRes),
contentDescription = option.name,
modifier = Modifier.size(44.dp),
contentScale = ContentScale.Fit,
)
// Check-mark overlay at bottom-right when selected
if (selected) {
Image(
painter = painterResource(R.drawable.icon_leukorrhea_choose),
contentDescription = "已选中",
modifier = Modifier
.size(20.dp)
.align(Alignment.BottomEnd),
contentScale = ContentScale.Fit,
)
}
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = option.name,
fontSize = 12.sp,
textAlign = TextAlign.Center,
color = if (selected) LeukorrheaPink else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
)
}
}
@@ -0,0 +1,166 @@
package com.pinkbear.pinkov.ui.component.checkin
import com.pinkbear.pinkov.R
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* Bottom sheet for recording follicle monitoring data, matching Meiyou's
* [FollicleNumKeyboardDialog] design:
* - Two rows: left ovary (左侧卵泡大小) and right ovary (右侧卵泡大小)
* - Each row: width (A) × height (B) in mm, integer input, max 2 digits
* - No time field (Meiyou does not record time for follicle measurements)
*/
@Composable
fun FollicleSheet(
initialLeftA: Int?,
initialLeftB: Int?,
initialRightA: Int?,
initialRightB: Int?,
onSave: (Int, Int, Int, Int) -> Unit,
onDismiss: () -> Unit,
) {
var leftA by remember { mutableStateOf(initialLeftA?.toString() ?: "") }
var leftB by remember { mutableStateOf(initialLeftB?.toString() ?: "") }
var rightA by remember { mutableStateOf(initialRightA?.toString() ?: "") }
var rightB by remember { mutableStateOf(initialRightB?.toString() ?: "") }
val hasAnyData = leftA.isNotBlank() || leftB.isNotBlank() ||
rightA.isNotBlank() || rightB.isNotBlank()
CheckInBottomSheet(onDismiss = onDismiss) {
Text("卵泡监测", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(12.dp))
Text(
"建议记录最大卵泡",
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(20.dp))
// ---- Left ovary row ----
FollicleSizeRow(
label = "左侧卵泡大小",
valueA = leftA,
valueB = leftB,
onValueAChange = { leftA = it.take(2) },
onValueBChange = { leftB = it.take(2) },
)
Spacer(modifier = Modifier.height(12.dp))
// ---- Right ovary row ----
FollicleSizeRow(
label = "右侧卵泡大小",
valueA = rightA,
valueB = rightB,
onValueAChange = { rightA = it.take(2) },
onValueBChange = { rightB = it.take(2) },
)
Spacer(modifier = Modifier.height(24.dp))
// ---- Save button ----
Button(
onClick = {
val la = leftA.toIntOrNull() ?: 0
val lb = leftB.toIntOrNull() ?: 0
val ra = rightA.toIntOrNull() ?: 0
val rb = rightB.toIntOrNull() ?: 0
onSave(la, lb, ra, rb)
},
modifier = Modifier.fillMaxWidth(),
enabled = hasAnyData,
) {
Text("保存")
}
Spacer(modifier = Modifier.height(8.dp))
}
}
/**
* A single follicle size input row: label + [EditText A] + × + [EditText B] + mm.
* Matches Meiyou's `dialog_layout_follicle_pink.xml` row layout.
*/
@Composable
private fun FollicleSizeRow(
label: String,
valueA: String,
valueB: String,
onValueAChange: (String) -> Unit,
onValueBChange: (String) -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
// Label
Text(
text = label,
fontSize = 17.sp,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.width(8.dp))
// Width input (A)
OutlinedTextField(
value = valueA,
onValueChange = onValueAChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
modifier = Modifier.width(68.dp).height(56.dp),
shape = RoundedCornerShape(8.dp),
textStyle = MaterialTheme.typography.bodyLarge.copy(
fontSize = 16.sp,
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
),
)
Spacer(modifier = Modifier.width(8.dp))
// Multiply sign
Image(
painter = painterResource(R.drawable.icon_multiply),
contentDescription = "",
modifier = Modifier.size(12.dp),
)
Spacer(modifier = Modifier.width(8.dp))
// Height input (B)
OutlinedTextField(
value = valueB,
onValueChange = onValueBChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
modifier = Modifier.width(68.dp).height(56.dp),
shape = RoundedCornerShape(8.dp),
textStyle = MaterialTheme.typography.bodyLarge.copy(
fontSize = 16.sp,
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
),
)
Spacer(modifier = Modifier.width(8.dp))
// Unit
Text(
text = "mm",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
@@ -0,0 +1,57 @@
package com.pinkbear.pinkov.ui.component.checkin
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val HABIT_OPTIONS = listOf(
"早睡", "运动", "泡脚", "喝豆浆", "不熬夜",
"冥想", "瑜伽", "散步", "阅读", "喝水2L",
"不吃辣", "戒咖啡", "吃早餐", "按时吃饭", "其他",
)
@Composable
fun HabitSheet(
initialHabits: String?,
onSave: (String) -> Unit,
onDismiss: () -> Unit,
) {
val initialSet = remember(initialHabits) {
initialHabits?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }?.toSet() ?: emptySet()
}
var selected by remember { mutableStateOf(initialSet) }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("好习惯", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
Column(modifier = Modifier.fillMaxWidth()) {
var rowItems = mutableListOf<String>()
HABIT_OPTIONS.forEach { item ->
rowItems.add(item)
if (rowItems.size == 3) {
CheckInTagRow(rowItems, selected) { tag ->
selected = if (tag in selected) selected - tag else selected + tag
}
rowItems = mutableListOf()
}
}
if (rowItems.isNotEmpty()) {
CheckInTagRow(rowItems, selected) { tag ->
selected = if (tag in selected) selected - tag else selected + tag
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { onSave(selected.joinToString(",")) },
modifier = Modifier.fillMaxWidth(),
) { Text("保存") }
Spacer(modifier = Modifier.height(8.dp))
}
}
@@ -0,0 +1,190 @@
package com.pinkbear.pinkov.ui.component.checkin
import com.pinkbear.pinkov.R
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.text.SimpleDateFormat
import java.util.*
/**
* Love method option: icon resource + Chinese label.
*/
private data class LoveOption(
val iconRes: Int,
val label: String,
)
/** Map of love icon resources to Chinese labels. */
private val LOVE_OPTIONS = listOf(
LoveOption(R.drawable.icon_love_wcs, "无措施"),
LoveOption(R.drawable.icon_love_byt, "避孕套"),
LoveOption(R.drawable.icon_love_twpj, "体外排精"),
LoveOption(R.drawable.icon_love_wsj, "未射精"),
LoveOption(R.drawable.icon_love_jyh, "紧急避孕药"),
LoveOption(R.drawable.icon_love_cxby, "短效避孕药"),
LoveOption(R.drawable.icon_love_dxby, "长效避孕药"),
LoveOption(R.drawable.icon_love_jjbyy, "节育环"),
LoveOption(R.drawable.icon_love_qtyy, "其他措施"),
LoveOption(R.drawable.icon_love_ziwei, "自慰"),
)
/**
* Bottom sheet for recording sexual activity method and time.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoveSheet(
initialMethod: String?,
initialTime: Long?,
onSave: (String, Long) -> Unit,
onDismiss: () -> Unit,
) {
val now = remember { System.currentTimeMillis() }
var selectedMethod by remember { mutableStateOf(initialMethod) }
var time by remember { mutableStateOf(initialTime ?: now) }
var showTimePicker by remember { mutableStateOf(false) }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("同房方式", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
// 2 rows × 5 columns grid of love method icons
LazyVerticalGrid(
columns = GridCells.Fixed(5),
modifier = Modifier.fillMaxWidth().height(200.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
items(LOVE_OPTIONS.size) { index ->
val option = LOVE_OPTIONS[index]
val selected = selectedMethod == option.label
LoveMethodItem(
option = option,
selected = selected,
onClick = { selectedMethod = option.label },
)
}
}
Spacer(modifier = Modifier.height(12.dp))
// Time row
val timeFmt = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("同房时间", fontSize = 14.sp)
TextButton(onClick = { showTimePicker = true }) {
Text(timeFmt.format(Date(time)), fontSize = 15.sp)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Save button
Button(
onClick = {
selectedMethod?.let { onSave(it, time) }
},
modifier = Modifier.fillMaxWidth(),
enabled = selectedMethod != null,
) {
Text("保存")
}
Spacer(modifier = Modifier.height(8.dp))
}
// Time picker dialog
if (showTimePicker) {
val tpState = rememberTimePickerState(
initialHour = (time % 86400000 / 3600000).toInt(),
initialMinute = (time % 3600000 / 60000).toInt(),
is24Hour = true,
)
AlertDialog(
onDismissRequest = { showTimePicker = false },
title = { Text("选择同房时间") },
text = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
TimePicker(state = tpState)
}
},
confirmButton = {
TextButton(onClick = {
val cal = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, tpState.hour)
set(Calendar.MINUTE, tpState.minute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
time = cal.timeInMillis
showTimePicker = false
}) { Text("确定") }
},
dismissButton = {
TextButton(onClick = { showTimePicker = false }) { Text("取消") }
},
)
}
}
@Composable
private fun LoveMethodItem(
option: LoveOption,
selected: Boolean,
onClick: () -> Unit,
) {
val borderColor = if (selected) MaterialTheme.colorScheme.primary else Color.Transparent
val bgColor = if (selected)
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
else
Color.Transparent
Column(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.border(2.dp, borderColor, RoundedCornerShape(8.dp))
.background(bgColor)
.clickable { onClick() }
.padding(6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(option.iconRes),
contentDescription = option.label,
modifier = Modifier.size(40.dp),
contentScale = ContentScale.Fit,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = option.label,
fontSize = 10.sp,
textAlign = TextAlign.Center,
maxLines = 1,
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@@ -0,0 +1,112 @@
package com.pinkbear.pinkov.ui.component.checkin
import com.pinkbear.pinkov.R
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* Mood option with icon resource and label.
*/
private data class MoodOption(val iconRes: Int, val label: String)
private val MOOD_OPTIONS = listOf(
MoodOption(R.drawable.icon_feeling_1_chaokaixin, "超开心"),
MoodOption(R.drawable.icon_feeling_2_tingkaixin, "挺开心"),
MoodOption(R.drawable.icon_feeling_3_yiban, "一般"),
MoodOption(R.drawable.icon_feeling_4_bukaixin, "不开心"),
MoodOption(R.drawable.icon_feeling_5_haoshangxin, "好伤心"),
MoodOption(R.drawable.icon_feeling_6_xingfen, "兴奋"),
MoodOption(R.drawable.icon_feeling_7_jingxi, "惊喜"),
MoodOption(R.drawable.icon_feeling_8_manzu, "满足"),
MoodOption(R.drawable.icon_feeling_9_xindong, "心动"),
MoodOption(R.drawable.icon_feeling_10_zixin, "自信"),
MoodOption(R.drawable.icon_feeling_11_fangsong, "放松"),
MoodOption(R.drawable.icon_feeling_12_pingjing, "平静"),
MoodOption(R.drawable.icon_feeling_13_fanzao, "烦躁"),
MoodOption(R.drawable.icon_feeling_14_yinu, "易怒"),
MoodOption(R.drawable.icon_feeling_15_shengqi, "生气"),
MoodOption(R.drawable.icon_feeling_16_jiaolv, "焦虑"),
MoodOption(R.drawable.icon_feeling_17_neihao, "内耗"),
MoodOption(R.drawable.icon_feeling_18_yali, "压力"),
MoodOption(R.drawable.icon_feeling_19_haipa, "害怕"),
MoodOption(R.drawable.icon_feeling_20_lengmo, "冷漠"),
)
/**
* Bottom sheet for selecting mood (single-select grid).
*/
@Composable
fun MoodSheet(
initialMood: String?,
onSave: (String) -> Unit,
onDismiss: () -> Unit,
) {
var selected by remember { mutableStateOf(initialMood) }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("心情", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
LazyVerticalGrid(
columns = GridCells.Fixed(5),
modifier = Modifier.fillMaxWidth().height(280.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
items(MOOD_OPTIONS.size) { index ->
val option = MOOD_OPTIONS[index]
val isSelected = selected == option.label
Column(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.border(2.dp, if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent, RoundedCornerShape(8.dp))
.background(if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) else Color.Transparent)
.clickable { selected = option.label }
.padding(6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(option.iconRes),
contentDescription = option.label,
modifier = Modifier.size(36.dp),
contentScale = ContentScale.Fit,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = option.label,
fontSize = 10.sp,
textAlign = TextAlign.Center,
maxLines = 1,
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { selected?.let { onSave(it) } },
modifier = Modifier.fillMaxWidth(),
enabled = selected != null,
) { Text("保存") }
Spacer(modifier = Modifier.height(8.dp))
}
}
@@ -0,0 +1,39 @@
package com.pinkbear.pinkov.ui.component.checkin
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun PlanSheet(
initialPlan: String?,
onSave: (String) -> Unit,
onDismiss: () -> Unit,
) {
var plan by remember { mutableStateOf(initialPlan ?: "") }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("计划", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = plan,
onValueChange = { plan = it },
label = { Text("今天有什么计划…") },
modifier = Modifier.fillMaxWidth().height(120.dp),
maxLines = 6,
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { onSave(plan) },
modifier = Modifier.fillMaxWidth(),
enabled = plan.isNotBlank(),
) { Text("保存") }
Spacer(modifier = Modifier.height(8.dp))
}
}
@@ -0,0 +1,50 @@
package com.pinkbear.pinkov.ui.component.checkin
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun StoolSheet(
initialType: String?,
onSave: (String) -> Unit,
onDismiss: () -> Unit,
) {
val options = listOf("正常", "稀便", "干便", "便秘", "腹泻", "黑便", "血便", "其他")
var selected by remember { mutableStateOf(initialType) }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("便便", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
Column(modifier = Modifier.fillMaxWidth()) {
options.forEach { label ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selected = label }
.padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(selected = selected == label, onClick = { selected = label })
Spacer(modifier = Modifier.width(8.dp))
Text(label, fontSize = 15.sp)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { selected?.let { onSave(it) } },
modifier = Modifier.fillMaxWidth(),
enabled = selected != null,
) { Text("保存") }
Spacer(modifier = Modifier.height(8.dp))
}
}
@@ -0,0 +1,96 @@
package com.pinkbear.pinkov.ui.component.checkin
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val SUPPLEMENT_OPTIONS = listOf(
"叶酸", "维生素D", "维生素E", "钙片", "铁剂",
"DHA", "辅酶Q10", "维生素C", "维生素B族", "",
"益生菌", "蔓越莓", "月见草油", "圣洁莓", "其他",
)
@Composable
fun SupplementSheet(
initialSupplements: String?,
onSave: (String) -> Unit,
onDismiss: () -> Unit,
) {
val initialSet = remember(initialSupplements) {
initialSupplements?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }?.toSet() ?: emptySet()
}
var selected by remember { mutableStateOf(initialSet) }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("营养补充", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
Column(modifier = Modifier.fillMaxWidth()) {
var rowItems = mutableListOf<String>()
SUPPLEMENT_OPTIONS.forEach { item ->
rowItems.add(item)
if (rowItems.size == 3) {
CheckInTagRow(rowItems, selected) { tag ->
selected = if (tag in selected) selected - tag else selected + tag
}
rowItems = mutableListOf()
}
}
if (rowItems.isNotEmpty()) {
CheckInTagRow(rowItems, selected) { tag ->
selected = if (tag in selected) selected - tag else selected + tag
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { onSave(selected.joinToString(",")) },
modifier = Modifier.fillMaxWidth(),
) { Text("保存") }
Spacer(modifier = Modifier.height(8.dp))
}
}
@Composable
internal fun CheckInTagRow(
items: List<String>,
selected: Set<String>,
onToggle: (String) -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items.forEach { label ->
val isSelected = label in selected
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(8.dp))
.background(
if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant
)
.clickable { onToggle(label) }
.padding(horizontal = 12.dp, vertical = 10.dp),
) {
Text(
text = label,
fontSize = 13.sp,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
@@ -0,0 +1,254 @@
package com.pinkbear.pinkov.ui.component.checkin
import com.pinkbear.pinkov.R
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* A single symptom option with icon resource and Chinese name.
* @param id Numeric ID matching Meiyou's all_symptom_data.json.
*/
private data class SymptomOption(
val id: Int,
val iconRes: Int,
val name: String,
)
// ---- Section 1: 身体症状 (pregnancy_prepare mode) ----
private val BODY_SYMPTOMS = listOf(
SymptomOption(192, R.drawable.icon_zhengzhuang_192_meiyou, "没有症状"),
SymptomOption(11, R.drawable.icon_zhengzhuang_011_yaosuan, "腰酸"),
SymptomOption(10, R.drawable.icon_zhengzhuang_010_futong, "腹痛"),
SymptomOption(12, R.drawable.icon_zhengzhuang_012_xiaofuzhuizhang, "小腹坠胀"),
SymptomOption(4, R.drawable.icon_zhengzhuang_004_rufangzhangtong, "乳房胀痛"),
SymptomOption(1, R.drawable.icon_zhengzhuang_001_shentisuantong, "身体酸痛"),
SymptomOption(0, R.drawable.icon_zhengzhuang_000_toutong, "头痛"),
SymptomOption(3, R.drawable.icon_zhengzhuang_003_xuanyun, "眩晕"),
SymptomOption(7, R.drawable.icon_zhengzhuang_007_shimian, "失眠"),
SymptomOption(5, R.drawable.icon_zhengzhuang_005_fenci, "粉刺"),
SymptomOption(32, R.drawable.icon_zhengzhuang_032_pifuganzao, "皮肤干燥"),
SymptomOption(6, R.drawable.icon_zhengzhuang_006_shiyubuzhen, "食欲不振"),
SymptomOption(117, R.drawable.icon_zhengzhuang_117_shiyuwangsheng, "食欲旺盛"),
SymptomOption(2, R.drawable.icon_zhengzhuang_002_fuxie, "腹泻"),
SymptomOption(14, R.drawable.icon_zhengzhuang_014_bianmi, "便秘"),
SymptomOption(22, R.drawable.icon_zhengzhuang_022_pibei, "疲惫"),
)
// ---- Section 2: 阴道分泌物 (pregnancy_prepare mode) ----
private val DISCHARGE_SYMPTOMS = listOf(
SymptomOption(96, R.drawable.icon_zhengzhuang_096_hesefenmiwu, "褐色分泌物"),
SymptomOption(33, R.drawable.icon_zhengzhuang_033_chuxue, "出血"),
SymptomOption(127, R.drawable.icon_zhengzhuang_127_youxuekuai, "有血块"),
SymptomOption(25, R.drawable.icon_zhengzhuang_025_baidaizengduo, "白带增多"),
)
/** Width for each icon cell: (screen - padding - gaps) / 4. Approx for bottom sheet. */
private val CELL_WIDTH = 72.dp
/**
* Bottom sheet for recording symptoms — three sections matching the Meiyou app design:
* 1. 身体症状 (body symptoms) — 4-column icon grid
* 2. 阴道分泌物 (vaginal discharge) — 4-column icon grid
* 3. 手工输入更多症状 — free-text input for custom symptoms
*/
@Composable
fun SymptomSheet(
initialSymptoms: String?,
onSave: (String) -> Unit,
onDismiss: () -> Unit,
) {
// Split saved symptoms into known names and custom text.
val knownNames = BODY_SYMPTOMS.map { it.name } + DISCHARGE_SYMPTOMS.map { it.name }
val initialParts = remember(initialSymptoms) {
initialSymptoms
?.split(",")
?.map { it.trim() }
?.filter { it.isNotEmpty() }
?: emptyList()
}
val initialSelected = remember(initialParts) {
initialParts.filter { it in knownNames }.toSet()
}
val initialCustom = remember(initialParts) {
initialParts.filter { it !in knownNames }.joinToString("")
}
var selected by remember { mutableStateOf(initialSelected) }
var customText by remember { mutableStateOf(initialCustom) }
CheckInBottomSheet(onDismiss = onDismiss) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
Text("症状", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
// ===== Section 1: 身体症状 =====
SectionTitle("身体症状")
Spacer(modifier = Modifier.height(8.dp))
SymptomIconGrid(
options = BODY_SYMPTOMS,
selectedNames = selected,
onToggle = { name ->
selected = if (name in selected) selected - name else selected + name
},
)
Spacer(modifier = Modifier.height(20.dp))
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(16.dp))
// ===== Section 2: 阴道分泌物 =====
SectionTitle("阴道分泌物")
Spacer(modifier = Modifier.height(8.dp))
SymptomIconGrid(
options = DISCHARGE_SYMPTOMS,
selectedNames = selected,
onToggle = { name ->
selected = if (name in selected) selected - name else selected + name
},
)
Spacer(modifier = Modifier.height(20.dp))
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(16.dp))
// ===== Section 3: 手工输入更多症状 =====
SectionTitle("手工输入更多症状")
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = customText,
onValueChange = { customText = it },
placeholder = { Text("输入其他症状,用逗号或分号分隔", fontSize = 13.sp) },
modifier = Modifier
.fillMaxWidth()
.height(100.dp),
maxLines = 4,
)
Spacer(modifier = Modifier.height(20.dp))
// ===== Save button =====
Button(
onClick = {
val parts = selected.toList() + customText
.split(",", "", ";", "")
.map { it.trim() }
.filter { it.isNotEmpty() }
onSave(parts.joinToString(","))
},
modifier = Modifier.fillMaxWidth(),
) {
Text("保存")
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Composable
private fun SectionTitle(title: String) {
Text(
text = title,
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface,
)
}
/**
* A 4-column grid of symptom icons.
*/
@Composable
private fun SymptomIconGrid(
options: List<SymptomOption>,
selectedNames: Set<String>,
onToggle: (String) -> Unit,
) {
// Chunk into rows of 4
val rows = options.chunked(4)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
rows.forEach { rowItems ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
rowItems.forEach { option ->
SymptomIconItem(
option = option,
selected = option.name in selectedNames,
onClick = { onToggle(option.name) },
modifier = Modifier.weight(1f),
)
}
// Fill remaining slots with empty space to keep alignment
repeat(4 - rowItems.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
@Composable
private fun SymptomIconItem(
option: SymptomOption,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val borderColor = if (selected) MaterialTheme.colorScheme.primary else Color.Transparent
val bgColor = if (selected)
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
else
Color.Transparent
Column(
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.border(2.dp, borderColor, RoundedCornerShape(8.dp))
.background(bgColor)
.clickable { onClick() }
.padding(vertical = 8.dp, horizontal = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(option.iconRes),
contentDescription = option.name,
modifier = Modifier.size(36.dp),
contentScale = ContentScale.Fit,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = option.name,
fontSize = 10.sp,
textAlign = TextAlign.Center,
maxLines = 1,
color = if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@@ -0,0 +1,114 @@
package com.pinkbear.pinkov.ui.component.checkin
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.text.SimpleDateFormat
import java.util.*
/**
* Bottom sheet for recording basal body temperature.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TemperatureSheet(
initialTemp: Float?,
initialTime: Long?,
onSave: (Float, Long) -> Unit,
onDismiss: () -> Unit,
) {
val now = remember { System.currentTimeMillis() }
var temp by remember { mutableStateOf(initialTemp?.toString() ?: "") }
var time by remember { mutableStateOf(initialTime ?: now) }
var showTimePicker by remember { mutableStateOf(false) }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("基础体温", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
// Temperature input
OutlinedTextField(
value = temp,
onValueChange = { temp = it },
label = { Text("体温") },
suffix = { Text("°C") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
// Time display + picker button
val timeFmt = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("测温时间", fontSize = 14.sp)
TextButton(onClick = { showTimePicker = true }) {
Text(timeFmt.format(Date(time)), fontSize = 15.sp)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Save button
Button(
onClick = {
val t = temp.toFloatOrNull() ?: return@Button
onSave(t, time)
},
modifier = Modifier.fillMaxWidth(),
enabled = temp.isNotBlank(),
) {
Text("保存")
}
Spacer(modifier = Modifier.height(8.dp))
}
// Time picker dialog
if (showTimePicker) {
val tpState = rememberTimePickerState(
initialHour = (time % 86400000 / 3600000).toInt(),
initialMinute = (time % 3600000 / 60000).toInt(),
is24Hour = true,
)
AlertDialog(
onDismissRequest = { showTimePicker = false },
title = { Text("选择测温时间") },
text = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
TimePicker(state = tpState)
}
},
confirmButton = {
TextButton(onClick = {
val cal = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, tpState.hour)
set(Calendar.MINUTE, tpState.minute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
time = cal.timeInMillis
showTimePicker = false
}) { Text("确定") }
},
dismissButton = {
TextButton(onClick = { showTimePicker = false }) { Text("取消") }
},
)
}
}
@@ -0,0 +1,90 @@
package com.pinkbear.pinkov.ui.component.checkin
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WeightSheet(
initialWeight: Float?,
initialTime: Long?,
onSave: (Float, Long) -> Unit,
onDismiss: () -> Unit,
) {
val now = remember { System.currentTimeMillis() }
var weightStr by remember { mutableStateOf(initialWeight?.toString() ?: "") }
var time by remember { mutableStateOf(initialTime ?: now) }
var showTimePicker by remember { mutableStateOf(false) }
CheckInBottomSheet(onDismiss = onDismiss) {
Text("体重", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = weightStr,
onValueChange = { weightStr = it },
label = { Text("体重") },
suffix = { Text("kg") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
val timeFmt = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("测量时间", fontSize = 14.sp)
TextButton(onClick = { showTimePicker = true }) {
Text(timeFmt.format(Date(time)), fontSize = 15.sp)
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { weightStr.toFloatOrNull()?.let { onSave(it, time) } },
modifier = Modifier.fillMaxWidth(),
enabled = weightStr.isNotBlank(),
) { Text("保存") }
Spacer(modifier = Modifier.height(8.dp))
}
if (showTimePicker) {
val tpState = rememberTimePickerState(
initialHour = (time % 86400000 / 3600000).toInt(),
initialMinute = (time % 3600000 / 60000).toInt(),
is24Hour = true,
)
AlertDialog(
onDismissRequest = { showTimePicker = false },
title = { Text("选择测量时间") },
text = { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { TimePicker(state = tpState) } },
confirmButton = {
TextButton(onClick = {
val cal = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, tpState.hour)
set(Calendar.MINUTE, tpState.minute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
time = cal.timeInMillis
showTimePicker = false
}) { Text("确定") }
},
dismissButton = { TextButton(onClick = { showTimePicker = false }) { Text("取消") } },
)
}
}
@@ -118,10 +118,13 @@ fun HomePage(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.03f),
Color.Black.copy(alpha = 0.08f),
Color.Black.copy(alpha = 0.14f),
)
Color.Black.copy(alpha = 0.08f),
Color.Black.copy(alpha = 0.03f),
)
)
)
)
@@ -133,7 +136,7 @@ fun HomePage(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(MaterialTheme.colorScheme.surfaceVariant),
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center
) {
Text(
@@ -147,7 +150,7 @@ fun HomePage(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(MaterialTheme.colorScheme.surfaceVariant)
.background(MaterialTheme.colorScheme.surface)
) {
item {
Spacer(modifier = Modifier.height(12.dp))
@@ -199,7 +202,7 @@ private fun RecordHeaderRow() {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant)
.background(MaterialTheme.colorScheme.surface)
.padding(vertical = 8.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
@@ -264,7 +267,7 @@ private fun RecordRow(
"阴性" -> Color(0xFF4CAF50)
"阳性" -> Color(0xFFFF9800)
"强阳" -> Color(0xFFF44336)
else -> Color.White
else -> MaterialTheme.colorScheme.surface
}
Card(
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Favorite
@@ -82,65 +83,75 @@ fun MinePage(modifier: Modifier = Modifier) {
}
}
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant
)
// HorizontalDivider(
// thickness = 1.dp,
// color = MaterialTheme.colorScheme.outlineVariant
// )
// ---- Bottom ~80%: Menu items ----
// ---- Menu items ----
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(top = 16.dp)
) {
MenuItemRow(
icon = Icons.Default.Info,
title = "关于我们",
onClick = { /* TODO: navigate to about page */ }
)
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant
)
// Group 1: 关于我们, 版本更新, 经期设置
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column {
MenuItemRow(
icon = Icons.Default.Info,
title = "关于我们",
onClick = { /* TODO: navigate to about page */ }
)
MenuItemRow(
icon = Icons.Default.Refresh,
title = "版本更新",
onClick = { /* TODO: check for updates */ }
)
MenuItemRow(
icon = Icons.Default.Favorite,
title = "经期设置",
onClick = { showPeriodSettings = true }
)
}
}
MenuItemRow(
icon = Icons.Default.Refresh,
title = "版本更新",
onClick = { /* TODO: check for updates */ }
)
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant
)
Spacer(modifier = Modifier.height(10.dp))
// ---- 经期设置 (above 设置) ----
MenuItemRow(
icon = Icons.Default.Favorite,
title = "经期设置",
onClick = { showPeriodSettings = true }
)
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant
)
MenuItemRow(
icon = Icons.Default.Settings,
title = "设置",
onClick = { /* TODO: navigate to settings page */ }
)
// Group 2: 设置
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
MenuItemRow(
icon = Icons.Default.Settings,
title = "设置",
onClick = { /* TODO: navigate to settings page */ }
)
}
// Push version text to bottom
Spacer(modifier = Modifier.weight(1f))
Text(
text = "v1.0.0",
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp)
)
// Text(
// text = "v1.0.0",
// color = MaterialTheme.colorScheme.onSurfaceVariant,
// fontSize = 12.sp,
// modifier = Modifier
// .align(Alignment.CenterHorizontally)
// .padding(bottom = 16.dp)
// )
}
}
}
@@ -178,7 +189,7 @@ private fun MenuItemRow(
imageVector = icon,
contentDescription = title,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(16.dp))
@@ -11,7 +11,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -45,6 +47,7 @@ import java.time.LocalDate
@Composable
fun RecordsPage(
modifier: Modifier = Modifier,
onScanClick: () -> Unit = {},
recordsViewModel: RecordsViewModel = viewModel(),
) {
val calendarState = rememberSelectableCalendarState(
@@ -77,8 +80,9 @@ fun RecordsPage(
Scaffold(modifier = modifier) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.fillMaxWidth()
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) {
// Calendar section
SelectableCalendar(
@@ -114,6 +118,43 @@ fun RecordsPage(
onPeriodStartChanged = { date, isStart ->
recordsViewModel.setPeriodStart(date, isStart)
},
onScanClick = onScanClick,
onTemperatureSave = { date, temp, time ->
recordsViewModel.updateTemperature(date, temp, time)
},
onLoveSave = { date, method, time ->
recordsViewModel.updateSexMethod(date, method, time)
},
onDischargeSave = { date, type ->
recordsViewModel.updateDischarge(date, type)
},
onFollicleSave = { date, leftA, leftB, rightA, rightB ->
recordsViewModel.updateFollicle(date, leftA, leftB, rightA, rightB)
},
onSymptomSave = { date, symptoms ->
recordsViewModel.updateSymptoms(date, symptoms)
},
onMoodSave = { date, mood ->
recordsViewModel.updateMood(date, mood)
},
onSupplementSave = { date, supplements ->
recordsViewModel.updateSupplements(date, supplements)
},
onWeightSave = { date, weight, time ->
recordsViewModel.updateWeight(date, weight, time)
},
onDiarySave = { date, note ->
recordsViewModel.updateDiary(date, note)
},
onHabitSave = { date, habits ->
recordsViewModel.updateHabits(date, habits)
},
onStoolSave = { date, type ->
recordsViewModel.updateStool(date, type)
},
onPlanSave = { date, plan ->
recordsViewModel.updatePlan(date, plan)
},
)
}
}
@@ -64,6 +64,134 @@ class RecordsViewModel(application: Application) : AndroidViewModel(application)
}
}
// ---- Check-in update methods ----
/** Update basal body temperature for a date. */
fun updateTemperature(date: LocalDate, temp: Float, time: Long) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(temperature = temp, temperatureTime = time)
?: DailyRecord(date = date.toEpochDay(), temperature = temp, temperatureTime = time)
recordDao.upsert(record)
}
}
/** Update sex method and time for a date. */
fun updateSexMethod(date: LocalDate, method: String, time: Long) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(hadSex = true, sexMethod = method, sexTime = time)
?: DailyRecord(date = date.toEpochDay(), hadSex = true, sexMethod = method, sexTime = time)
recordDao.upsert(record)
}
}
/** Update discharge type for a date. */
fun updateDischarge(date: LocalDate, type: String) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(discharge = type)
?: DailyRecord(date = date.toEpochDay(), discharge = type)
recordDao.upsert(record)
}
}
/** Update follicle monitoring for a date (left/right ovary, width×height in mm). */
fun updateFollicle(date: LocalDate, leftA: Int, leftB: Int, rightA: Int, rightB: Int) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(
follicleLeftA = leftA, follicleLeftB = leftB,
follicleRightA = rightA, follicleRightB = rightB,
) ?: DailyRecord(
date = date.toEpochDay(),
follicleLeftA = leftA, follicleLeftB = leftB,
follicleRightA = rightA, follicleRightB = rightB,
)
recordDao.upsert(record)
}
}
/** Update symptoms for a date (comma-separated). */
fun updateSymptoms(date: LocalDate, symptoms: String) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(symptoms = symptoms)
?: DailyRecord(date = date.toEpochDay(), symptoms = symptoms)
recordDao.upsert(record)
}
}
/** Update mood for a date. */
fun updateMood(date: LocalDate, mood: String) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(mood = mood)
?: DailyRecord(date = date.toEpochDay(), mood = mood)
recordDao.upsert(record)
}
}
/** Update supplements for a date (comma-separated). */
fun updateSupplements(date: LocalDate, supplements: String) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(supplements = supplements)
?: DailyRecord(date = date.toEpochDay(), supplements = supplements)
recordDao.upsert(record)
}
}
/** Update weight for a date. */
fun updateWeight(date: LocalDate, weight: Float, time: Long) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(weight = weight, weightTime = time)
?: DailyRecord(date = date.toEpochDay(), weight = weight, weightTime = time)
recordDao.upsert(record)
}
}
/** Update diary note for a date. */
fun updateDiary(date: LocalDate, note: String) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(note = note)
?: DailyRecord(date = date.toEpochDay(), note = note)
recordDao.upsert(record)
}
}
/** Update habits for a date (comma-separated). */
fun updateHabits(date: LocalDate, habits: String) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(habits = habits)
?: DailyRecord(date = date.toEpochDay(), habits = habits)
recordDao.upsert(record)
}
}
/** Update stool type for a date. */
fun updateStool(date: LocalDate, type: String) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(stoolType = type)
?: DailyRecord(date = date.toEpochDay(), stoolType = type)
recordDao.upsert(record)
}
}
/** Update plan text for a date. */
fun updatePlan(date: LocalDate, plan: String) {
viewModelScope.launch(Dispatchers.IO) {
val existing = recordDao.getRecordOnce(date.toEpochDay())
val record = existing?.copy(plan = plan)
?: DailyRecord(date = date.toEpochDay(), plan = plan)
recordDao.upsert(record)
}
}
/** Remove all period-start flags within the same month as [date], except [date] itself. */
private suspend fun clearPeriodStartsInMonth(date: LocalDate) {
val monthStart = date.withDayOfMonth(1)
@@ -1,170 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<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: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" />
<!-- Radial pink gradient matching theme primary: #FF8BBC → #FFC1D4 -->
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="54"
android:centerY="54"
android:gradientRadius="76"
android:type="radial">
<item
android:color="#FFFFC1D4"
android:offset="0.0" />
<item
android:color="#FFFF8BBC"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
</vector>
@@ -1,30 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<!-- Left ear -->
<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>
android:pathData="M24,32 A10,10 0 1,1 44,32 A10,10 0 1,1 24,32 Z" />
<!-- Right ear -->
<path
android:fillColor="#FFFFFF"
android:pathData="M64,32 A10,10 0 1,1 84,32 A10,10 0 1,1 64,32 Z" />
<!-- Left inner ear -->
<path
android:fillColor="#FFD0DD"
android:pathData="M28.5,32 A5.5,5.5 0 1,1 39.5,32 A5.5,5.5 0 1,1 28.5,32 Z" />
<!-- Right inner ear -->
<path
android:fillColor="#FFD0DD"
android:pathData="M68.5,32 A5.5,5.5 0 1,1 79.5,32 A5.5,5.5 0 1,1 68.5,32 Z" />
<!-- Face (round, white) -->
<path
android:fillColor="#FFFFFF"
android:pathData="M32,50 A22,22 0 1,1 76,50 A22,22 0 1,1 32,50 Z" />
<!-- Left eye -->
<path
android:fillColor="#FF6B8A"
android:pathData="M41,43 A3,3 0 1,1 47,43 A3,3 0 1,1 41,43 Z" />
<!-- Right eye -->
<path
android:fillColor="#FF6B8A"
android:pathData="M61,43 A3,3 0 1,1 67,43 A3,3 0 1,1 61,43 Z" />
<!-- Eye highlights (tiny white dots for cuteness) -->
<path
android:fillColor="#FFFFFF"
android:pathData="M42.5,42 A1.2,1.2 0 1,1 44.9,42 A1.2,1.2 0 1,1 42.5,42 Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M62.5,42 A1.2,1.2 0 1,1 64.9,42 A1.2,1.2 0 1,1 62.5,42 Z" />
<!-- Nose (small pink triangle) -->
<path
android:fillColor="#FF8BBC"
android:pathData="M51,49 L57,49 L54,53.5 Z" />
<!-- Mouth (cute curved lines) -->
<path
android:fillColor="#00000000"
android:strokeColor="#FF8BBC"
android:strokeWidth="1.2"
android:pathData="M49,55.5 Q51.5,59 54,55.5 Q56.5,59 59,55.5" />
<!-- Left cheek blush -->
<path
android:fillColor="#FFD0DD"
android:fillAlpha="0.5"
android:pathData="M35,52 A4.5,3 0 1,1 44,52 A4.5,3 0 1,1 35,52 Z" />
<!-- Right cheek blush -->
<path
android:fillColor="#FFD0DD"
android:fillAlpha="0.5"
android:pathData="M64,52 A4.5,3 0 1,1 73,52 A4.5,3 0 1,1 64,52 Z" />
<!-- Heart badge on chest (好孕 accent) -->
<path
android:fillColor="#FF8BBC"
android:pathData="M54,65 C54,65 48,60 48,57 C48,54.5 50,53 52.5,53 C53.8,53 54,54 54,54 C54,54 54.2,53 55.5,53 C58,53 60,54.5 60,57 C60,60 54,65 54,65 Z" />
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Some files were not shown because too many files have changed in this diff Show More