main fucntions
This commit is contained in:
123
.idea/codeStyles/Project.xml
generated
Normal file
123
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
|||||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
|||||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/10.json
Normal file
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/10.json
Normal file
File diff suppressed because it is too large
Load Diff
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/11.json
Normal file
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/11.json
Normal file
File diff suppressed because it is too large
Load Diff
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/3.json
Normal file
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/3.json
Normal file
File diff suppressed because it is too large
Load Diff
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/4.json
Normal file
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/4.json
Normal file
File diff suppressed because it is too large
Load Diff
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/5.json
Normal file
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/5.json
Normal file
File diff suppressed because it is too large
Load Diff
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/7.json
Normal file
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/7.json
Normal file
File diff suppressed because it is too large
Load Diff
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/8.json
Normal file
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/8.json
Normal file
File diff suppressed because it is too large
Load Diff
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/9.json
Normal file
1602
app/schemas/kr.smartsoltech.wellshe.data.AppDatabase/9.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ import androidx.room.TypeConverter
|
|||||||
ExerciseFormulaVar::class,
|
ExerciseFormulaVar::class,
|
||||||
CatalogVersion::class
|
CatalogVersion::class
|
||||||
],
|
],
|
||||||
version = 2,
|
version = 11,
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
@TypeConverters(LocalDateConverter::class, InstantConverter::class, StringListConverter::class)
|
@TypeConverters(LocalDateConverter::class, InstantConverter::class, StringListConverter::class)
|
||||||
|
|||||||
@@ -96,3 +96,681 @@ val MIGRATION_1_2 = object : Migration(1, 2) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция базы данных с версии 2 на версию 3.
|
||||||
|
* Исправляет проблему с типом данных столбца date в таблице water_logs.
|
||||||
|
*/
|
||||||
|
val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Создаем временную таблицу с правильными типами данных
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `water_logs_new` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`date` INTEGER NOT NULL,
|
||||||
|
`amount` INTEGER NOT NULL,
|
||||||
|
`timestamp` INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Копируем данные из старой таблицы в новую, преобразуя дату из TEXT в INTEGER
|
||||||
|
// Для этого используем SQLite функцию strftime для преобразования строковой даты в timestamp
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO water_logs_new (id, date, amount, timestamp)
|
||||||
|
SELECT id,
|
||||||
|
CASE
|
||||||
|
WHEN date IS NOT NULL THEN strftime('%s', date) * 1000
|
||||||
|
ELSE strftime('%s', 'now') * 1000
|
||||||
|
END as date_int,
|
||||||
|
amount,
|
||||||
|
timestamp
|
||||||
|
FROM water_logs
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Удаляем старую таблицу
|
||||||
|
database.execSQL("DROP TABLE water_logs")
|
||||||
|
|
||||||
|
// Переименовываем новую таблицу в старое имя
|
||||||
|
database.execSQL("ALTER TABLE water_logs_new RENAME TO water_logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция базы данных с версии 3 на версию 4.
|
||||||
|
* Исправляет проблему с типом данных столбца date в таблице sleep_logs.
|
||||||
|
*/
|
||||||
|
val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Создаем временную таблицу с правильными типами данных
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `sleep_logs_new` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`date` INTEGER NOT NULL,
|
||||||
|
`bedTime` TEXT NOT NULL,
|
||||||
|
`wakeTime` TEXT NOT NULL,
|
||||||
|
`duration` REAL NOT NULL,
|
||||||
|
`quality` TEXT NOT NULL,
|
||||||
|
`notes` TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Копируем данные из старой таблицы в новую, преобразуя дату из TEXT в INTEGER
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO sleep_logs_new (id, date, bedTime, wakeTime, duration, quality, notes)
|
||||||
|
SELECT id,
|
||||||
|
CASE
|
||||||
|
WHEN date IS NOT NULL THEN strftime('%s', date) * 1000
|
||||||
|
ELSE strftime('%s', 'now') * 1000
|
||||||
|
END as date_int,
|
||||||
|
bedTime,
|
||||||
|
wakeTime,
|
||||||
|
duration,
|
||||||
|
quality,
|
||||||
|
notes
|
||||||
|
FROM sleep_logs
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Удаляем старую таблицу
|
||||||
|
database.execSQL("DROP TABLE sleep_logs")
|
||||||
|
|
||||||
|
// Переименовываем новую таблицу в старое имя
|
||||||
|
database.execSQL("ALTER TABLE sleep_logs_new RENAME TO sleep_logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция базы данных с версии 4 на версию 5.
|
||||||
|
* Исправляет проблему с типом данных столбца date в таблице workouts.
|
||||||
|
*/
|
||||||
|
val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Создаем временную таблицу с правильными типами данных
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `workouts_new` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`date` INTEGER NOT NULL,
|
||||||
|
`type` TEXT NOT NULL,
|
||||||
|
`name` TEXT NOT NULL,
|
||||||
|
`duration` INTEGER NOT NULL,
|
||||||
|
`caloriesBurned` INTEGER NOT NULL,
|
||||||
|
`intensity` TEXT NOT NULL,
|
||||||
|
`notes` TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Копируем данные из старой таблицы в новую, преобразуя дату из TEXT в INTEGER
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO workouts_new (id, date, type, name, duration, caloriesBurned, intensity, notes)
|
||||||
|
SELECT id,
|
||||||
|
CASE
|
||||||
|
WHEN date IS NOT NULL THEN strftime('%s', date) * 1000
|
||||||
|
ELSE strftime('%s', 'now') * 1000
|
||||||
|
END as date_int,
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
duration,
|
||||||
|
caloriesBurned,
|
||||||
|
intensity,
|
||||||
|
notes
|
||||||
|
FROM workouts
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Удаляем старую таблицу
|
||||||
|
database.execSQL("DROP TABLE workouts")
|
||||||
|
|
||||||
|
// Переименовываем новую таблицу в старое имя
|
||||||
|
database.execSQL("ALTER TABLE workouts_new RENAME TO workouts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция базы данных с версии 5 на версию 6.
|
||||||
|
* Исправляет проблему с типом данных столбца date в таблице calories.
|
||||||
|
*/
|
||||||
|
val MIGRATION_5_6 = object : Migration(5, 6) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Создаем временную таблицу с правильными типами данных
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `calories_new` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`date` INTEGER NOT NULL,
|
||||||
|
`consumed` INTEGER NOT NULL,
|
||||||
|
`burned` INTEGER NOT NULL,
|
||||||
|
`target` INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Копируем данные из старой таблицы в новую, преобразуя дату из TEXT в INTEGER
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO calories_new (id, date, consumed, burned, target)
|
||||||
|
SELECT id,
|
||||||
|
CASE
|
||||||
|
WHEN date IS NOT NULL THEN strftime('%s', date) * 1000
|
||||||
|
ELSE strftime('%s', 'now') * 1000
|
||||||
|
END as date_int,
|
||||||
|
consumed,
|
||||||
|
burned,
|
||||||
|
target
|
||||||
|
FROM calories
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Удаляем старую таблицу
|
||||||
|
database.execSQL("DROP TABLE calories")
|
||||||
|
|
||||||
|
// Переименовываем новую таблицу в старое имя
|
||||||
|
database.execSQL("ALTER TABLE calories_new RENAME TO calories")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция базы данных с версии 6 на версию 7.
|
||||||
|
* Исправляет проблему с типом данных столбца date во всех оставшихся таблицах.
|
||||||
|
*/
|
||||||
|
val MIGRATION_6_7 = object : Migration(6, 7) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Пропускаем таблицу steps, так как она будет обработана в MIGRATION_7_8
|
||||||
|
// Начинаем с проверки существования таблицы health_records и наличия в ней поля date с типом TEXT
|
||||||
|
try {
|
||||||
|
val cursor = database.query("PRAGMA table_info(health_records)")
|
||||||
|
var hasDateColumn = false
|
||||||
|
var isTextType = false
|
||||||
|
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
val columnName = cursor.getString(cursor.getColumnIndex("name"))
|
||||||
|
val columnType = cursor.getString(cursor.getColumnIndex("type"))
|
||||||
|
|
||||||
|
if (columnName == "date" && columnType.equals("TEXT", ignoreCase = true)) {
|
||||||
|
hasDateColumn = true
|
||||||
|
isTextType = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if (hasDateColumn && isTextType) {
|
||||||
|
// Создаем временную таблицу с правильными типами данных
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `health_records_new` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`date` INTEGER NOT NULL,
|
||||||
|
`type` TEXT NOT NULL,
|
||||||
|
`value` REAL NOT NULL,
|
||||||
|
`notes` TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Копируем данные из старой таблицы в новую, преобразуя дату из TEXT в INTEGER
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO health_records_new (id, date, type, value, notes)
|
||||||
|
SELECT id,
|
||||||
|
CASE
|
||||||
|
WHEN date IS NOT NULL THEN strftime('%s', date) * 1000
|
||||||
|
ELSE strftime('%s', 'now') * 1000
|
||||||
|
END as date_int,
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
notes
|
||||||
|
FROM health_records
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Удаляем старую таблицу
|
||||||
|
database.execSQL("DROP TABLE health_records")
|
||||||
|
|
||||||
|
// Переименовываем новую таблицу в старое имя
|
||||||
|
database.execSQL("ALTER TABLE health_records_new RENAME TO health_records")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Если таблица не существует или возникла другая ошибка, просто продолжаем
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем другие таблицы, которые могут содержать поля даты с типом TEXT
|
||||||
|
// Список таблиц для проверки
|
||||||
|
val tablesToCheck = listOf(
|
||||||
|
"cycle_periods",
|
||||||
|
"user_profiles",
|
||||||
|
"weight_logs",
|
||||||
|
"beverage_logs",
|
||||||
|
"workout_sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
for (table in tablesToCheck) {
|
||||||
|
try {
|
||||||
|
val cursor = database.query("PRAGMA table_info($table)")
|
||||||
|
val dateColumns = mutableListOf<String>()
|
||||||
|
val columns = mutableListOf<Pair<String, String>>()
|
||||||
|
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
val columnNameIndex = cursor.getColumnIndex("name")
|
||||||
|
val columnTypeIndex = cursor.getColumnIndex("type")
|
||||||
|
|
||||||
|
if (columnNameIndex >= 0 && columnTypeIndex >= 0) {
|
||||||
|
val columnName = cursor.getString(columnNameIndex)
|
||||||
|
val columnType = cursor.getString(columnTypeIndex)
|
||||||
|
|
||||||
|
columns.add(columnName to columnType)
|
||||||
|
|
||||||
|
// Проверяем, есть ли в названии колонки слово "date" и тип TEXT
|
||||||
|
if (columnName.contains("date", ignoreCase = true) &&
|
||||||
|
columnType.equals("TEXT", ignoreCase = true)) {
|
||||||
|
dateColumns.add(columnName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
// Если найдены столбцы с датами типа TEXT
|
||||||
|
if (dateColumns.isNotEmpty()) {
|
||||||
|
// Создаем имя для временной таблицы
|
||||||
|
val newTableName = "${table}_new"
|
||||||
|
|
||||||
|
// Создаем SQL для создания новой таблицы с правильными типами
|
||||||
|
val createTableSQL = StringBuilder()
|
||||||
|
createTableSQL.append("CREATE TABLE IF NOT EXISTS `$newTableName` (")
|
||||||
|
|
||||||
|
val columnDefinitions = columns.map { (name, type) ->
|
||||||
|
// Для столбцов с датой меняем тип на INTEGER
|
||||||
|
if (dateColumns.contains(name)) {
|
||||||
|
"`$name` INTEGER" + if (name == "id") " PRIMARY KEY AUTOINCREMENT NOT NULL" else " NOT NULL"
|
||||||
|
} else {
|
||||||
|
"`$name` $type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTableSQL.append(columnDefinitions.joinToString(", "))
|
||||||
|
createTableSQL.append(")")
|
||||||
|
|
||||||
|
// Выполняем создание новой таблицы
|
||||||
|
database.execSQL(createTableSQL.toString())
|
||||||
|
|
||||||
|
// Создаем SQL для копирования данных
|
||||||
|
val insertSQL = StringBuilder()
|
||||||
|
insertSQL.append("INSERT INTO `$newTableName` SELECT ")
|
||||||
|
|
||||||
|
val columnSelects = columns.map { (name, _) ->
|
||||||
|
if (dateColumns.contains(name)) {
|
||||||
|
"CASE WHEN $name IS NOT NULL THEN strftime('%s', $name) * 1000 ELSE strftime('%s', 'now') * 1000 END as $name"
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertSQL.append(columnSelects.joinToString(", "))
|
||||||
|
insertSQL.append(" FROM `$table`")
|
||||||
|
|
||||||
|
// Выполняем копирование данных
|
||||||
|
database.execSQL(insertSQL.toString())
|
||||||
|
|
||||||
|
// Удаляем старую таблицу и переименовываем новую
|
||||||
|
database.execSQL("DROP TABLE `$table`")
|
||||||
|
database.execSQL("ALTER TABLE `$newTableName` RENAME TO `$table`")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Если таблица не существует или возникла другая ошибка, просто продолжаем
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция базы данных с версии 7 на версию 8.
|
||||||
|
* Исправляет проблему с типом данных столбца date в таблице steps.
|
||||||
|
*/
|
||||||
|
val MIGRATION_7_8 = object : Migration(7, 8) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Создаем временную таблицу с правильными типами данных
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `steps_new` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`date` INTEGER NOT NULL,
|
||||||
|
`steps` INTEGER NOT NULL,
|
||||||
|
`distance` REAL NOT NULL,
|
||||||
|
`caloriesBurned` INTEGER NOT NULL,
|
||||||
|
`target` INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Копируем данные из старой таблицы в новую, преобразуя дату из TEXT в INTEGER
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO steps_new (id, date, steps, distance, caloriesBurned, target)
|
||||||
|
SELECT id,
|
||||||
|
CASE
|
||||||
|
WHEN date IS NOT NULL THEN strftime('%s', date) * 1000
|
||||||
|
ELSE strftime('%s', 'now') * 1000
|
||||||
|
END as date_int,
|
||||||
|
steps,
|
||||||
|
distance,
|
||||||
|
caloriesBurned,
|
||||||
|
target
|
||||||
|
FROM steps
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Удаляем старую таблицу
|
||||||
|
database.execSQL("DROP TABLE steps")
|
||||||
|
|
||||||
|
// Переименовываем новую таблицу в старое имя
|
||||||
|
database.execSQL("ALTER TABLE steps_new RENAME TO steps")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция базы данных с версии 8 на версию 9.
|
||||||
|
* Исправляет проблему с типом данных столбца lastPeriodDate в таблице user_profile.
|
||||||
|
*/
|
||||||
|
val MIGRATION_8_9 = object : Migration(8, 9) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Создаем временную таблицу с правильными типами данных
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_profile_new` (
|
||||||
|
`id` INTEGER PRIMARY KEY NOT NULL,
|
||||||
|
`name` TEXT NOT NULL,
|
||||||
|
`email` TEXT NOT NULL,
|
||||||
|
`age` INTEGER NOT NULL,
|
||||||
|
`height` INTEGER NOT NULL,
|
||||||
|
`weight` REAL NOT NULL,
|
||||||
|
`targetWeight` REAL NOT NULL,
|
||||||
|
`activityLevel` TEXT NOT NULL,
|
||||||
|
`dailyWaterGoal` INTEGER NOT NULL,
|
||||||
|
`dailyCalorieGoal` INTEGER NOT NULL,
|
||||||
|
`dailyStepsGoal` INTEGER NOT NULL,
|
||||||
|
`cycleLength` INTEGER NOT NULL,
|
||||||
|
`periodLength` INTEGER NOT NULL,
|
||||||
|
`lastPeriodDate` INTEGER,
|
||||||
|
`profileImagePath` TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Копируем данные из старой таблицы в новую, преобразуя lastPeriodDate из TEXT в INTEGER
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO user_profile_new (
|
||||||
|
id, name, email, age, height, weight, targetWeight, activityLevel,
|
||||||
|
dailyWaterGoal, dailyCalorieGoal, dailyStepsGoal, cycleLength,
|
||||||
|
periodLength, lastPeriodDate, profileImagePath
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id, name, email, age, height, weight, targetWeight, activityLevel,
|
||||||
|
dailyWaterGoal, dailyCalorieGoal, dailyStepsGoal, cycleLength,
|
||||||
|
periodLength,
|
||||||
|
CASE
|
||||||
|
WHEN lastPeriodDate IS NOT NULL THEN strftime('%s', lastPeriodDate) * 1000
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
profileImagePath
|
||||||
|
FROM user_profile
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Удаляем старую таблицу
|
||||||
|
database.execSQL("DROP TABLE user_profile")
|
||||||
|
|
||||||
|
// Переименовываем новую таблицу в старое имя
|
||||||
|
database.execSQL("ALTER TABLE user_profile_new RENAME TO user_profile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция базы данных с версии 9 на версию 10.
|
||||||
|
* Исправляет проблему с отсутствующей таблицей WorkoutSession.
|
||||||
|
*/
|
||||||
|
val MIGRATION_9_10 = object : Migration(9, 10) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Проверяем, существует ли таблица WorkoutSession
|
||||||
|
try {
|
||||||
|
val cursor = database.query("SELECT name FROM sqlite_master WHERE type='table' AND name='WorkoutSession'")
|
||||||
|
val hasTable = cursor.count > 0
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if (!hasTable) {
|
||||||
|
// Создаем таблицу WorkoutSession если она не существует
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `WorkoutSession` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`startedAt` INTEGER NOT NULL,
|
||||||
|
`endedAt` INTEGER,
|
||||||
|
`exerciseId` INTEGER NOT NULL,
|
||||||
|
`kcalTotal` REAL,
|
||||||
|
`distanceKm` REAL,
|
||||||
|
`notes` TEXT
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Создаем индекс для столбца startedAt
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE INDEX IF NOT EXISTS `index_WorkoutSession_startedAt` ON `WorkoutSession` (`startedAt`)"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Если таблица существует, проверяем наличие необходимых столбцов
|
||||||
|
val columnCursor = database.query("PRAGMA table_info(WorkoutSession)")
|
||||||
|
val columns = mutableListOf<String>()
|
||||||
|
|
||||||
|
while (columnCursor.moveToNext()) {
|
||||||
|
val columnName = columnCursor.getString(columnCursor.getColumnIndex("name"))
|
||||||
|
columns.add(columnName)
|
||||||
|
}
|
||||||
|
columnCursor.close()
|
||||||
|
|
||||||
|
// Если нужных колонок нет, пересоздаем таблицу
|
||||||
|
val requiredColumns = listOf("id", "startedAt", "endedAt", "exerciseId",
|
||||||
|
"kcalTotal", "distanceKm", "notes")
|
||||||
|
if (!columns.containsAll(requiredColumns)) {
|
||||||
|
// Переименовываем старую таблицу
|
||||||
|
database.execSQL("ALTER TABLE `WorkoutSession` RENAME TO `WorkoutSession_old`")
|
||||||
|
|
||||||
|
// Создаем новую таблицу с правильной структурой
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE `WorkoutSession` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`startedAt` INTEGER NOT NULL,
|
||||||
|
`endedAt` INTEGER,
|
||||||
|
`exerciseId` INTEGER NOT NULL,
|
||||||
|
`kcalTotal` REAL,
|
||||||
|
`distanceKm` REAL,
|
||||||
|
`notes` TEXT
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Создаем индекс для столбца startedAt
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE INDEX IF NOT EXISTS `index_WorkoutSession_startedAt` ON `WorkoutSession` (`startedAt`)"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Пытаемся скопировать данные из старой таблицы, если это возможно
|
||||||
|
try {
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO WorkoutSession (id, startedAt, endedAt, exerciseId, kcalTotal, distanceKm, notes)
|
||||||
|
SELECT id, startedAt, endedAt, exerciseId, kcalTotal, distanceKm, notes
|
||||||
|
FROM WorkoutSession_old
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Если копирование не удалось, просто продолжаем без данных
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем старую таблицу
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS `WorkoutSession_old`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// В случае любой ошибки, создаем таблицу заново
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS `WorkoutSession`")
|
||||||
|
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE `WorkoutSession` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`startedAt` INTEGER NOT NULL,
|
||||||
|
`endedAt` INTEGER,
|
||||||
|
`exerciseId` INTEGER NOT NULL,
|
||||||
|
`kcalTotal` REAL,
|
||||||
|
`distanceKm` REAL,
|
||||||
|
`notes` TEXT
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Создаем индекс для столбца startedAt
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE INDEX IF NOT EXISTS `index_WorkoutSession_startedAt` ON `WorkoutSession` (`startedAt`)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Также проверяем наличие таблицы WorkoutSessionParam и создаем её при необходимости
|
||||||
|
try {
|
||||||
|
val cursor = database.query("SELECT name FROM sqlite_master WHERE type='table' AND name='WorkoutSessionParam'")
|
||||||
|
val hasTable = cursor.count > 0
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if (!hasTable) {
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `WorkoutSessionParam` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`sessionId` INTEGER NOT NULL,
|
||||||
|
`key` TEXT NOT NULL,
|
||||||
|
`valueNum` REAL,
|
||||||
|
`valueText` TEXT,
|
||||||
|
`unit` TEXT
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS `WorkoutSessionParam`")
|
||||||
|
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE `WorkoutSessionParam` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`sessionId` INTEGER NOT NULL,
|
||||||
|
`key` TEXT NOT NULL,
|
||||||
|
`valueNum` REAL,
|
||||||
|
`valueText` TEXT,
|
||||||
|
`unit` TEXT
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// И проверяем наличие таблицы WorkoutEvent
|
||||||
|
try {
|
||||||
|
val cursor = database.query("SELECT name FROM sqlite_master WHERE type='table' AND name='WorkoutEvent'")
|
||||||
|
val hasTable = cursor.count > 0
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if (!hasTable) {
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `WorkoutEvent` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`sessionId` INTEGER NOT NULL,
|
||||||
|
`timestamp` INTEGER NOT NULL,
|
||||||
|
`eventType` TEXT NOT NULL,
|
||||||
|
`valueNum` REAL,
|
||||||
|
`valueText` TEXT
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS `WorkoutEvent`")
|
||||||
|
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE `WorkoutEvent` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`sessionId` INTEGER NOT NULL,
|
||||||
|
`timestamp` INTEGER NOT NULL,
|
||||||
|
`eventType` TEXT NOT NULL,
|
||||||
|
`valueNum` REAL,
|
||||||
|
`valueText` TEXT
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Миграция базы данных с версии 10 на версию 11.
|
||||||
|
* Исправляет проблему с несоответствием структуры таблицы WorkoutEvent.
|
||||||
|
*/
|
||||||
|
val MIGRATION_10_11 = object : Migration(10, 11) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Создаем временную таблицу с правильной структурой
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `WorkoutEvent_new` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`sessionId` INTEGER NOT NULL,
|
||||||
|
`ts` INTEGER NOT NULL,
|
||||||
|
`eventType` TEXT NOT NULL,
|
||||||
|
`payloadJson` TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Пытаемся скопировать данные из старой таблицы, преобразовывая структуру
|
||||||
|
try {
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO WorkoutEvent_new (id, sessionId, ts, eventType, payloadJson)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
sessionId,
|
||||||
|
timestamp AS ts,
|
||||||
|
eventType,
|
||||||
|
CASE
|
||||||
|
WHEN valueText IS NOT NULL THEN valueText
|
||||||
|
WHEN valueNum IS NOT NULL THEN json_object('value', valueNum)
|
||||||
|
ELSE '{}'
|
||||||
|
END AS payloadJson
|
||||||
|
FROM WorkoutEvent
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Если копирование не удалось из-за несовместимости данных,
|
||||||
|
// просто создаем пустую таблицу
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем старую таблицу
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS WorkoutEvent")
|
||||||
|
|
||||||
|
// Переименовываем новую таблицу
|
||||||
|
database.execSQL("ALTER TABLE WorkoutEvent_new RENAME TO WorkoutEvent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ class CycleRepository @Inject constructor(
|
|||||||
recalculateForecasts()
|
recalculateForecasts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет настройки цикла
|
||||||
|
*/
|
||||||
|
suspend fun updateSettings(settings: CycleSettingsEntity) {
|
||||||
|
settingsDao.insertOrUpdate(settings)
|
||||||
|
}
|
||||||
|
|
||||||
// История циклов
|
// История циклов
|
||||||
fun getAllHistoryFlow(): Flow<List<CycleHistoryEntity>> = historyDao.getAllFlow()
|
fun getAllHistoryFlow(): Flow<List<CycleHistoryEntity>> = historyDao.getAllFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ import kr.smartsoltech.wellshe.data.repo.DrinkLogger
|
|||||||
import kr.smartsoltech.wellshe.data.repo.WeightRepository
|
import kr.smartsoltech.wellshe.data.repo.WeightRepository
|
||||||
import kr.smartsoltech.wellshe.data.repo.WorkoutService
|
import kr.smartsoltech.wellshe.data.repo.WorkoutService
|
||||||
import kr.smartsoltech.wellshe.data.MIGRATION_1_2
|
import kr.smartsoltech.wellshe.data.MIGRATION_1_2
|
||||||
|
import kr.smartsoltech.wellshe.data.MIGRATION_2_3
|
||||||
|
import kr.smartsoltech.wellshe.data.MIGRATION_3_4
|
||||||
|
import kr.smartsoltech.wellshe.data.MIGRATION_4_5
|
||||||
|
import kr.smartsoltech.wellshe.data.MIGRATION_5_6
|
||||||
|
import kr.smartsoltech.wellshe.data.MIGRATION_6_7
|
||||||
|
import kr.smartsoltech.wellshe.data.MIGRATION_7_8
|
||||||
|
import kr.smartsoltech.wellshe.data.MIGRATION_8_9
|
||||||
|
import kr.smartsoltech.wellshe.data.MIGRATION_9_10
|
||||||
|
import kr.smartsoltech.wellshe.data.MIGRATION_10_11
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -33,7 +42,7 @@ object AppModule {
|
|||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
"well_she_db"
|
"well_she_db"
|
||||||
)
|
)
|
||||||
.addMigrations(MIGRATION_1_2)
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package kr.smartsoltech.wellshe.domain.models
|
||||||
|
|
||||||
|
import kr.smartsoltech.wellshe.ui.cycle.settings.FertileWindowMode
|
||||||
|
import kr.smartsoltech.wellshe.ui.cycle.settings.HormonalContraception
|
||||||
|
import kr.smartsoltech.wellshe.ui.cycle.settings.OvulationMethod
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
// Типы событий изменения настроек для старого интерфейса
|
||||||
|
sealed class BasicSettingChange {
|
||||||
|
data class CycleLengthChanged(val days: Int) : BasicSettingChange()
|
||||||
|
data class CycleVariabilityChanged(val days: Int) : BasicSettingChange()
|
||||||
|
data class PeriodLengthChanged(val days: Int) : BasicSettingChange()
|
||||||
|
data class LutealPhaseChanged(val days: String) : BasicSettingChange() // "auto" или число
|
||||||
|
data class LastPeriodStartChanged(val date: LocalDate) : BasicSettingChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class StatusChange {
|
||||||
|
data class HormonalContraceptionChanged(val type: HormonalContraception) : StatusChange()
|
||||||
|
data class PregnancyStatusChanged(val isPregnant: Boolean) : StatusChange()
|
||||||
|
data class PostpartumStatusChanged(val isPostpartum: Boolean) : StatusChange()
|
||||||
|
data class LactatingStatusChanged(val isLactating: Boolean) : StatusChange()
|
||||||
|
data class PerimenopauseStatusChanged(val perimenopause: Boolean) : StatusChange()
|
||||||
|
|
||||||
|
// Вложенные объекты для удобного создания событий
|
||||||
|
object Pregnant {
|
||||||
|
fun changed(value: Boolean) = PregnancyStatusChanged(value)
|
||||||
|
}
|
||||||
|
object Postpartum {
|
||||||
|
fun changed(value: Boolean) = PostpartumStatusChanged(value)
|
||||||
|
}
|
||||||
|
object Lactating {
|
||||||
|
fun changed(value: Boolean) = LactatingStatusChanged(value)
|
||||||
|
}
|
||||||
|
object Perimenopause {
|
||||||
|
fun changed(value: Boolean) = PerimenopauseStatusChanged(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class HistorySetting {
|
||||||
|
data class HistoryWindowChanged(val cycles: Int) : HistorySetting()
|
||||||
|
data class ExcludeOutliersChanged(val exclude: Boolean) : HistorySetting()
|
||||||
|
|
||||||
|
// Вложенные объекты для более удобного обращения
|
||||||
|
object BaselineCycleLength {
|
||||||
|
fun changed(value: Int) = BasicSettingChange.CycleLengthChanged(value)
|
||||||
|
}
|
||||||
|
object CycleVariability {
|
||||||
|
fun changed(value: Int) = BasicSettingChange.CycleVariabilityChanged(value)
|
||||||
|
}
|
||||||
|
object PeriodLength {
|
||||||
|
fun changed(value: Int) = BasicSettingChange.PeriodLengthChanged(value)
|
||||||
|
}
|
||||||
|
object LutealPhase {
|
||||||
|
fun changed(value: String) = BasicSettingChange.LutealPhaseChanged(value)
|
||||||
|
}
|
||||||
|
object LastPeriodStart {
|
||||||
|
fun changed(value: LocalDate) = BasicSettingChange.LastPeriodStartChanged(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вложенные классы для UI
|
||||||
|
class WindowCycles(val cycles: Int) : HistorySetting()
|
||||||
|
class ExcludeOutliers(val exclude: Boolean) : HistorySetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SensorSetting {
|
||||||
|
data class TemperatureUnitChanged(val unit: TemperatureUnit) : SensorSetting()
|
||||||
|
data class BbtTimeWindowChanged(val timeWindow: String) : SensorSetting()
|
||||||
|
data class TimezoneChanged(val timezone: String) : SensorSetting()
|
||||||
|
|
||||||
|
// Вложенные классы для UI
|
||||||
|
class TempUnit(val unit: TemperatureUnit) : SensorSetting()
|
||||||
|
class BbtTimeWindow(val timeWindow: String) : SensorSetting()
|
||||||
|
class Timezone(val timezone: String) : SensorSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class NotificationSetting {
|
||||||
|
data class PeriodReminderDaysChanged(val days: Int) : NotificationSetting()
|
||||||
|
data class OvulationReminderDaysChanged(val days: Int) : NotificationSetting()
|
||||||
|
data class PmsWindowDaysChanged(val days: Int) : NotificationSetting()
|
||||||
|
data class DeviationAlertDaysChanged(val days: Int) : NotificationSetting()
|
||||||
|
data class FertileWindowModeChanged(val mode: FertileWindowMode) : NotificationSetting()
|
||||||
|
|
||||||
|
// Вложенные классы для UI
|
||||||
|
class PeriodReminder(val days: Int) : NotificationSetting()
|
||||||
|
class OvulationReminder(val days: Int) : NotificationSetting()
|
||||||
|
class PmsWindow(val days: Int) : NotificationSetting()
|
||||||
|
class DeviationAlert(val days: Int) : NotificationSetting()
|
||||||
|
class FertileWindowMode(val mode: kr.smartsoltech.wellshe.domain.models.FertileWindowMode) : NotificationSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функции преобразования для всех типов
|
||||||
|
fun ovulationMethodFromString(value: String): OvulationMethod {
|
||||||
|
return when (value) {
|
||||||
|
"bbt" -> OvulationMethod.BBT
|
||||||
|
"lh_test" -> OvulationMethod.LH_TEST
|
||||||
|
"cervical_mucus" -> OvulationMethod.CERVICAL_MUCUS
|
||||||
|
"medical" -> OvulationMethod.MEDICAL
|
||||||
|
else -> OvulationMethod.AUTO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fertileWindowModeFromString(value: String): FertileWindowMode {
|
||||||
|
return when (value) {
|
||||||
|
"conservative" -> FertileWindowMode.CONSERVATIVE
|
||||||
|
"broad" -> FertileWindowMode.BROAD
|
||||||
|
else -> FertileWindowMode.BALANCED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hormonalContraceptionTypeFromString(value: String): HormonalContraception {
|
||||||
|
return when (value) {
|
||||||
|
"coc" -> HormonalContraception.COC
|
||||||
|
"iud" -> HormonalContraception.IUD
|
||||||
|
"implant" -> HormonalContraception.IMPLANT
|
||||||
|
"other" -> HormonalContraception.OTHER
|
||||||
|
else -> HormonalContraception.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun temperatureUnitFromString(value: String): TemperatureUnit {
|
||||||
|
return when (value.uppercase()) {
|
||||||
|
"F" -> TemperatureUnit.FAHRENHEIT
|
||||||
|
else -> TemperatureUnit.CELSIUS
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Этот файл больше не используется, все классы перенесены в CycleSettingsEvents.kt
|
||||||
|
|
||||||
@@ -3,6 +3,8 @@ package kr.smartsoltech.wellshe.ui.cycle
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -28,7 +30,8 @@ import java.util.Locale
|
|||||||
@Composable
|
@Composable
|
||||||
fun CycleScreen(
|
fun CycleScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: CycleViewModel = hiltViewModel()
|
viewModel: CycleViewModel = hiltViewModel(),
|
||||||
|
onNavigateToSettings: () -> Unit = {} // Добавляем параметр для навигации к настройкам
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
@@ -38,12 +41,26 @@ fun CycleScreen(
|
|||||||
viewModel.loadCycleData()
|
viewModel.loadCycleData()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold { paddingValues ->
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Менструальный цикл") },
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onNavigateToSettings) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Settings,
|
||||||
|
contentDescription = "Настройки цикла"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.padding(16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.verticalScroll(scrollState),
|
.verticalScroll(scrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package kr.smartsoltech.wellshe.ui.cycle.settings
|
package kr.smartsoltech.wellshe.ui.cycle.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -13,7 +11,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import kr.smartsoltech.wellshe.data.entity.CycleHistoryEntity
|
import kr.smartsoltech.wellshe.data.entity.CycleHistoryEntity
|
||||||
import kr.smartsoltech.wellshe.data.entity.CycleSettingsEntity
|
import kr.smartsoltech.wellshe.data.entity.CycleSettingsEntity
|
||||||
import kr.smartsoltech.wellshe.domain.models.*
|
import kr.smartsoltech.wellshe.domain.models.*
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.format.FormatStyle
|
import java.time.format.FormatStyle
|
||||||
|
|
||||||
@@ -55,7 +52,7 @@ fun CycleSettingsContent(
|
|||||||
|
|
||||||
// Секция метода определения овуляции
|
// Секция метода определения овуляции
|
||||||
OvulationMethodSection(
|
OvulationMethodSection(
|
||||||
currentMethod = OvulationMethod.fromString(settings.ovulationMethod),
|
currentMethod = ovulationMethodFromString(settings.ovulationMethod),
|
||||||
allowManualOvulation = settings.allowManualOvulation,
|
allowManualOvulation = settings.allowManualOvulation,
|
||||||
onMethodChanged = onOvulationMethodChanged,
|
onMethodChanged = onOvulationMethodChanged,
|
||||||
onAllowManualChanged = onAllowManualOvulationChanged
|
onAllowManualChanged = onAllowManualOvulationChanged
|
||||||
@@ -77,7 +74,7 @@ fun CycleSettingsContent(
|
|||||||
|
|
||||||
// Секция настроек сенсоров и единиц измерения
|
// Секция настроек сенсоров и единиц измерения
|
||||||
SensorSettingsSection(
|
SensorSettingsSection(
|
||||||
tempUnit = TemperatureUnit.fromString(settings.tempUnit),
|
tempUnit = temperatureUnitFromString(settings.tempUnit),
|
||||||
bbtTimeWindow = settings.bbtTimeWindow,
|
bbtTimeWindow = settings.bbtTimeWindow,
|
||||||
timezone = settings.timezone,
|
timezone = settings.timezone,
|
||||||
validationState = validationState,
|
validationState = validationState,
|
||||||
@@ -92,7 +89,7 @@ fun CycleSettingsContent(
|
|||||||
ovulationReminderDays = settings.ovulationReminderDaysBefore,
|
ovulationReminderDays = settings.ovulationReminderDaysBefore,
|
||||||
pmsWindowDays = settings.pmsWindowDays,
|
pmsWindowDays = settings.pmsWindowDays,
|
||||||
deviationAlertDays = settings.deviationAlertDays,
|
deviationAlertDays = settings.deviationAlertDays,
|
||||||
fertileWindowMode = FertileWindowMode.fromString(settings.fertileWindowMode),
|
fertileWindowMode = toModelFertileWindowMode(fertileWindowModeFromString(settings.fertileWindowMode)),
|
||||||
onSettingChanged = onNotificationSettingChanged
|
onSettingChanged = onNotificationSettingChanged
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,7 +150,7 @@ fun BasicSettingsSection(
|
|||||||
value = settings.baselineCycleLength.toString(),
|
value = settings.baselineCycleLength.toString(),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
try {
|
try {
|
||||||
onSettingChanged(BasicSettingChange.BaselineCycleLength(it.toInt()))
|
onSettingChanged(BasicSettingChange.CycleLengthChanged(it.toInt()))
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
// Игнорируем некорректный ввод
|
// Игнорируем некорректный ввод
|
||||||
}
|
}
|
||||||
@@ -172,7 +169,7 @@ fun BasicSettingsSection(
|
|||||||
value = settings.cycleVariabilityDays.toString(),
|
value = settings.cycleVariabilityDays.toString(),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
try {
|
try {
|
||||||
onSettingChanged(BasicSettingChange.CycleVariability(it.toInt()))
|
onSettingChanged(BasicSettingChange.CycleVariabilityChanged(it.toInt()))
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
// Игнорируем некорректный ввод
|
// Игнорируем некорректный ввод
|
||||||
}
|
}
|
||||||
@@ -191,7 +188,7 @@ fun BasicSettingsSection(
|
|||||||
value = settings.periodLengthDays.toString(),
|
value = settings.periodLengthDays.toString(),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
try {
|
try {
|
||||||
onSettingChanged(BasicSettingChange.PeriodLength(it.toInt()))
|
onSettingChanged(BasicSettingChange.PeriodLengthChanged(it.toInt()))
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
// Игнорируем некорректный ввод
|
// Игнорируем некорректный ввод
|
||||||
}
|
}
|
||||||
@@ -209,7 +206,7 @@ fun BasicSettingsSection(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = settings.lutealPhaseDays,
|
value = settings.lutealPhaseDays,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
onSettingChanged(BasicSettingChange.LutealPhase(it))
|
onSettingChanged(BasicSettingChange.LutealPhaseChanged(it))
|
||||||
},
|
},
|
||||||
label = { Text("Лютеиновая фаза (дни или 'auto')") },
|
label = { Text("Лютеиновая фаза (дни или 'auto')") },
|
||||||
isError = validationState.lutealPhaseError != null,
|
isError = validationState.lutealPhaseError != null,
|
||||||
@@ -253,7 +250,7 @@ fun BasicSettingsSection(
|
|||||||
val date = java.time.Instant.ofEpochMilli(millis)
|
val date = java.time.Instant.ofEpochMilli(millis)
|
||||||
.atZone(java.time.ZoneId.systemDefault())
|
.atZone(java.time.ZoneId.systemDefault())
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
onSettingChanged(BasicSettingChange.LastPeriodStart(date))
|
onSettingChanged(BasicSettingChange.LastPeriodStartChanged(date))
|
||||||
}
|
}
|
||||||
showDatePicker = false
|
showDatePicker = false
|
||||||
}
|
}
|
||||||
@@ -341,6 +338,19 @@ fun OvulationMethodSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Функция-адаптер для преобразования типов между доменной моделью и UI-слоем
|
||||||
|
*/
|
||||||
|
private fun HormonalContraceptionType.toUiModel(): HormonalContraception {
|
||||||
|
return when(this) {
|
||||||
|
HormonalContraceptionType.NONE -> HormonalContraception.NONE
|
||||||
|
HormonalContraceptionType.COC -> HormonalContraception.COC
|
||||||
|
HormonalContraceptionType.IUD -> HormonalContraception.IUD
|
||||||
|
HormonalContraceptionType.IMPLANT -> HormonalContraception.IMPLANT
|
||||||
|
HormonalContraceptionType.OTHER -> HormonalContraception.OTHER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Секция статусов, влияющих на точность прогнозов
|
* Секция статусов, влияющих на точность прогнозов
|
||||||
*/
|
*/
|
||||||
@@ -376,7 +386,7 @@ fun StatusSection(
|
|||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = hormonalContraception == type,
|
selected = hormonalContraception == type,
|
||||||
onClick = { onStatusChanged(StatusChange.HormonalContraception(type)) }
|
onClick = { onStatusChanged(StatusChange.HormonalContraceptionChanged(type.toUiModel())) }
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -395,16 +405,16 @@ fun StatusSection(
|
|||||||
// Другие статусы (чекбоксы)
|
// Другие статусы (чекбоксы)
|
||||||
val statusItems = listOf(
|
val statusItems = listOf(
|
||||||
Triple("Беременность", isPregnant) { value: Boolean ->
|
Triple("Беременность", isPregnant) { value: Boolean ->
|
||||||
onStatusChanged(StatusChange.Pregnant(value))
|
onStatusChanged(StatusChange.PregnancyStatusChanged(value))
|
||||||
},
|
},
|
||||||
Triple("Послеродовой период", isPostpartum) { value: Boolean ->
|
Triple("Послеродовой период", isPostpartum) { value: Boolean ->
|
||||||
onStatusChanged(StatusChange.Postpartum(value))
|
onStatusChanged(StatusChange.PostpartumStatusChanged(value))
|
||||||
},
|
},
|
||||||
Triple("Грудное вскармливание", isLactating) { value: Boolean ->
|
Triple("Грудное вскармливание", isLactating) { value: Boolean ->
|
||||||
onStatusChanged(StatusChange.Lactating(value))
|
onStatusChanged(StatusChange.LactatingStatusChanged(value))
|
||||||
},
|
},
|
||||||
Triple("Перименопауза", perimenopause) { value: Boolean ->
|
Triple("Перименопауза", perimenopause) { value: Boolean ->
|
||||||
onStatusChanged(StatusChange.Perimenopause(value))
|
onStatusChanged(StatusChange.PerimenopauseStatusChanged(value))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -535,5 +545,24 @@ fun CycleHistorySection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fertileWindowModeFromString(value: String): kr.smartsoltech.wellshe.ui.cycle.settings.FertileWindowMode {
|
||||||
|
return when (value.lowercase()) {
|
||||||
|
"conservative" -> kr.smartsoltech.wellshe.ui.cycle.settings.FertileWindowMode.CONSERVATIVE
|
||||||
|
"broad" -> kr.smartsoltech.wellshe.ui.cycle.settings.FertileWindowMode.BROAD
|
||||||
|
else -> kr.smartsoltech.wellshe.ui.cycle.settings.FertileWindowMode.BALANCED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Функция-адаптер для преобразования типа FertileWindowMode между UI и domain моделями
|
||||||
|
*/
|
||||||
|
private fun toModelFertileWindowMode(uiMode: kr.smartsoltech.wellshe.ui.cycle.settings.FertileWindowMode): kr.smartsoltech.wellshe.domain.models.FertileWindowMode {
|
||||||
|
return when (uiMode) {
|
||||||
|
kr.smartsoltech.wellshe.ui.cycle.settings.FertileWindowMode.CONSERVATIVE -> kr.smartsoltech.wellshe.domain.models.FertileWindowMode.CONSERVATIVE
|
||||||
|
kr.smartsoltech.wellshe.ui.cycle.settings.FertileWindowMode.BALANCED -> kr.smartsoltech.wellshe.domain.models.FertileWindowMode.BALANCED
|
||||||
|
kr.smartsoltech.wellshe.ui.cycle.settings.FertileWindowMode.BROAD -> kr.smartsoltech.wellshe.domain.models.FertileWindowMode.BROAD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Для полной реализации всех секций потребуется продолжить в новом файле
|
// Для полной реализации всех секций потребуется продолжить в новом файле
|
||||||
// Из-за ограничений размера файла
|
// Из-за ограничений размера файла
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import kr.smartsoltech.wellshe.domain.models.FertileWindowMode
|
import kr.smartsoltech.wellshe.domain.models.FertileWindowMode
|
||||||
import kr.smartsoltech.wellshe.domain.models.TemperatureUnit
|
import kr.smartsoltech.wellshe.domain.models.TemperatureUnit
|
||||||
|
import kr.smartsoltech.wellshe.domain.models.SensorSetting
|
||||||
|
import kr.smartsoltech.wellshe.domain.models.NotificationSetting
|
||||||
|
import kr.smartsoltech.wellshe.domain.models.HistorySetting
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Секция настроек сенсоров и единиц измерения
|
* Секция настроек сенсоров и единиц измерения
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package kr.smartsoltech.wellshe.ui.cycle.settings
|
||||||
|
|
||||||
|
import kr.smartsoltech.wellshe.data.entity.CycleSettingsEntity
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Расширения для преобразования между UI-моделью и моделью базы данных
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Преобразование из Entity в UI-модель
|
||||||
|
fun CycleSettingsEntity.toUiModel(): CycleSettings {
|
||||||
|
return CycleSettings(
|
||||||
|
baselineCycleLength = this.baselineCycleLength,
|
||||||
|
cycleVariabilityDays = this.cycleVariabilityDays,
|
||||||
|
periodLengthDays = this.periodLengthDays,
|
||||||
|
lutealPhaseDays = if (this.lutealPhaseDays == "auto") null else this.lutealPhaseDays.toIntOrNull(),
|
||||||
|
lastPeriodStart = this.lastPeriodStart ?: LocalDate.now().minusDays(28),
|
||||||
|
ovulationMethod = when (this.ovulationMethod) {
|
||||||
|
"auto" -> OvulationMethod.AUTO
|
||||||
|
"bbt" -> OvulationMethod.BBT
|
||||||
|
"lh_test" -> OvulationMethod.LH_TEST
|
||||||
|
"cervical_mucus" -> OvulationMethod.CERVICAL_MUCUS
|
||||||
|
"medical" -> OvulationMethod.MEDICAL
|
||||||
|
else -> OvulationMethod.AUTO
|
||||||
|
},
|
||||||
|
allowManualOvulation = this.allowManualOvulation,
|
||||||
|
hormonalContraception = when (this.hormonalContraception) {
|
||||||
|
"none" -> HormonalContraception.NONE
|
||||||
|
"coc" -> HormonalContraception.COC
|
||||||
|
"iud" -> HormonalContraception.IUD
|
||||||
|
"implant" -> HormonalContraception.IMPLANT
|
||||||
|
"other" -> HormonalContraception.OTHER
|
||||||
|
else -> HormonalContraception.NONE
|
||||||
|
},
|
||||||
|
isPregnant = this.isPregnant,
|
||||||
|
isPostpartum = this.isPostpartum,
|
||||||
|
isLactating = this.isLactating,
|
||||||
|
perimenopause = this.perimenopause,
|
||||||
|
historyWindowCycles = this.historyWindowCycles,
|
||||||
|
excludeOutliers = this.excludeOutliers,
|
||||||
|
tempUnitCelsius = this.tempUnit == "C",
|
||||||
|
// Разделение временного окна на две части
|
||||||
|
bbtWindowStart = this.bbtTimeWindow.split("-").getOrNull(0) ?: "06:00",
|
||||||
|
bbtWindowEnd = this.bbtTimeWindow.split("-").getOrNull(1) ?: "10:00",
|
||||||
|
timezoneIana = this.timezone,
|
||||||
|
periodReminderDaysBefore = this.periodReminderDaysBefore,
|
||||||
|
ovulationReminderDaysBefore = this.ovulationReminderDaysBefore,
|
||||||
|
pmsWindowDays = this.pmsWindowDays,
|
||||||
|
deviationAlertDays = this.deviationAlertDays,
|
||||||
|
fertileWindowMode = when (this.fertileWindowMode) {
|
||||||
|
"conservative" -> FertileWindowMode.CONSERVATIVE
|
||||||
|
"balanced" -> FertileWindowMode.BALANCED
|
||||||
|
"broad" -> FertileWindowMode.BROAD
|
||||||
|
else -> FertileWindowMode.BALANCED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Преобразование из UI-модели в Entity
|
||||||
|
fun CycleSettings.toEntity(): CycleSettingsEntity {
|
||||||
|
return CycleSettingsEntity(
|
||||||
|
id = 1, // Singleton ID
|
||||||
|
baselineCycleLength = this.baselineCycleLength,
|
||||||
|
cycleVariabilityDays = this.cycleVariabilityDays,
|
||||||
|
periodLengthDays = this.periodLengthDays,
|
||||||
|
lutealPhaseDays = this.lutealPhaseDays?.toString() ?: "auto",
|
||||||
|
lastPeriodStart = this.lastPeriodStart,
|
||||||
|
ovulationMethod = when (this.ovulationMethod) {
|
||||||
|
OvulationMethod.AUTO -> "auto"
|
||||||
|
OvulationMethod.BBT -> "bbt"
|
||||||
|
OvulationMethod.LH_TEST -> "lh_test"
|
||||||
|
OvulationMethod.CERVICAL_MUCUS -> "cervical_mucus"
|
||||||
|
OvulationMethod.MEDICAL -> "medical"
|
||||||
|
},
|
||||||
|
allowManualOvulation = this.allowManualOvulation,
|
||||||
|
hormonalContraception = when (this.hormonalContraception) {
|
||||||
|
HormonalContraception.NONE -> "none"
|
||||||
|
HormonalContraception.COC -> "coc"
|
||||||
|
HormonalContraception.IUD -> "iud"
|
||||||
|
HormonalContraception.IMPLANT -> "implant"
|
||||||
|
HormonalContraception.OTHER -> "other"
|
||||||
|
},
|
||||||
|
isPregnant = this.isPregnant,
|
||||||
|
isPostpartum = this.isPostpartum,
|
||||||
|
isLactating = this.isLactating,
|
||||||
|
perimenopause = this.perimenopause,
|
||||||
|
historyWindowCycles = this.historyWindowCycles,
|
||||||
|
excludeOutliers = this.excludeOutliers,
|
||||||
|
tempUnit = if (this.tempUnitCelsius) "C" else "F",
|
||||||
|
bbtTimeWindow = "${this.bbtWindowStart}-${this.bbtWindowEnd}",
|
||||||
|
timezone = this.timezoneIana,
|
||||||
|
periodReminderDaysBefore = this.periodReminderDaysBefore,
|
||||||
|
ovulationReminderDaysBefore = this.ovulationReminderDaysBefore,
|
||||||
|
pmsWindowDays = this.pmsWindowDays,
|
||||||
|
deviationAlertDays = this.deviationAlertDays,
|
||||||
|
fertileWindowMode = when (this.fertileWindowMode) {
|
||||||
|
FertileWindowMode.CONSERVATIVE -> "conservative"
|
||||||
|
FertileWindowMode.BALANCED -> "balanced"
|
||||||
|
FertileWindowMode.BROAD -> "broad"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательные расширения для ViewModel
|
||||||
|
fun OvulationMethod.toStorageString(): String {
|
||||||
|
return when (this) {
|
||||||
|
OvulationMethod.AUTO -> "auto"
|
||||||
|
OvulationMethod.BBT -> "bbt"
|
||||||
|
OvulationMethod.LH_TEST -> "lh_test"
|
||||||
|
OvulationMethod.CERVICAL_MUCUS -> "cervical_mucus"
|
||||||
|
OvulationMethod.MEDICAL -> "medical"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun FertileWindowMode.toStorageString(): String {
|
||||||
|
return when (this) {
|
||||||
|
FertileWindowMode.CONSERVATIVE -> "conservative"
|
||||||
|
FertileWindowMode.BALANCED -> "balanced"
|
||||||
|
FertileWindowMode.BROAD -> "broad"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,189 +2,486 @@ package kr.smartsoltech.wellshe.ui.cycle.settings
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kr.smartsoltech.wellshe.R
|
import java.time.LocalDate
|
||||||
import kr.smartsoltech.wellshe.data.entity.CycleSettingsEntity
|
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
/**
|
// ---------- Модель (минимально достаточная для UI) ----------
|
||||||
* Главный экран настроек цикла
|
enum class OvulationMethod { AUTO, BBT, LH_TEST, CERVICAL_MUCUS, MEDICAL }
|
||||||
*/
|
enum class HormonalContraception { NONE, COC, IUD, IMPLANT, OTHER }
|
||||||
|
enum class FertileWindowMode { CONSERVATIVE, BALANCED, BROAD }
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class CycleSettings(
|
||||||
|
val baselineCycleLength: Int = 28, // 18..60
|
||||||
|
val cycleVariabilityDays: Int = 3, // 0..10
|
||||||
|
val periodLengthDays: Int = 5, // 1..10
|
||||||
|
val lutealPhaseDays: Int? = null, // null => auto (14)
|
||||||
|
val lastPeriodStart: LocalDate = LocalDate.now().minusDays(28),
|
||||||
|
val ovulationMethod: OvulationMethod = OvulationMethod.AUTO,
|
||||||
|
val allowManualOvulation: Boolean = true,
|
||||||
|
|
||||||
|
val hormonalContraception: HormonalContraception = HormonalContraception.NONE,
|
||||||
|
val isPregnant: Boolean = false,
|
||||||
|
val isPostpartum: Boolean = false,
|
||||||
|
val isLactating: Boolean = false,
|
||||||
|
val perimenopause: Boolean = false,
|
||||||
|
|
||||||
|
val historyWindowCycles: Int = 6,
|
||||||
|
val excludeOutliers: Boolean = true,
|
||||||
|
|
||||||
|
val tempUnitCelsius: Boolean = true,
|
||||||
|
val bbtWindowStart: String = "05:00",
|
||||||
|
val bbtWindowEnd: String = "08:00",
|
||||||
|
val timezoneIana: String = "Asia/Seoul",
|
||||||
|
|
||||||
|
val periodReminderDaysBefore: Int = 2,
|
||||||
|
val ovulationReminderDaysBefore: Int = 1,
|
||||||
|
val pmsWindowDays: Int = 3,
|
||||||
|
val deviationAlertDays: Int = 5,
|
||||||
|
val fertileWindowMode: FertileWindowMode = FertileWindowMode.BALANCED
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------- Публичное API экрана ----------
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CycleSettingsScreen(
|
fun CycleSettingsScreen(
|
||||||
viewModel: CycleSettingsViewModel = hiltViewModel(),
|
initial: CycleSettings = CycleSettings(),
|
||||||
onNavigateBack: () -> Unit
|
onClose: () -> Unit,
|
||||||
|
onSave: (CycleSettings) -> Unit
|
||||||
) {
|
) {
|
||||||
val settings by viewModel.settingsState.collectAsStateWithLifecycle()
|
var state by remember { mutableStateOf(initial) }
|
||||||
val validationState by viewModel.validationState.collectAsStateWithLifecycle()
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
val exportImportState by viewModel.exportImportState.collectAsStateWithLifecycle()
|
|
||||||
val cycleHistory by viewModel.cycleHistory.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val (isValid, errorMsg) = remember(state) { validate(state) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
// Отслеживаем события UI из ViewModel
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
viewModel.uiEvents.observeForever { event ->
|
|
||||||
when (event) {
|
|
||||||
is CycleSettingsViewModel.UiEvent.ShowSnackbar -> {
|
|
||||||
coroutineScope.launch {
|
|
||||||
val result = if (event.actionLabel != null && event.action != null) {
|
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
message = event.message,
|
|
||||||
actionLabel = event.actionLabel,
|
|
||||||
duration = SnackbarDuration.Long
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
message = event.message,
|
|
||||||
duration = SnackbarDuration.Short
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вызываем действие отмены, если пользователь нажал на кнопку действия
|
|
||||||
if (result == SnackbarResult.ActionPerformed && event.action != null) {
|
|
||||||
event.action.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Настройки цикла") },
|
title = { Text("Настройки цикла") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onClose) { Icon(Icons.Filled.Close, contentDescription = "Закрыть") }
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.ArrowBack,
|
|
||||||
contentDescription = "Назад"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { viewModel.resetToDefaults() }) {
|
IconButton(
|
||||||
Icon(
|
enabled = isValid,
|
||||||
imageVector = Icons.Default.Refresh,
|
onClick = { onSave(state) }
|
||||||
contentDescription = "Сбросить к рекомендуемым"
|
) { Icon(Icons.Filled.Save, contentDescription = "Сохранить") }
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { inner ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.padding(inner)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
if (settings == null) {
|
// ---- Блок «Базовые параметры цикла» ----
|
||||||
// Показываем загрузку, если настройки еще не получены
|
SectionCard("Базовые параметры") {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
NumberField(
|
||||||
CircularProgressIndicator()
|
label = "Опорная длина цикла (18–60)",
|
||||||
|
value = state.baselineCycleLength,
|
||||||
|
onValueChange = { state = state.copy(baselineCycleLength = it.coerceIn(18, 60)) }
|
||||||
|
)
|
||||||
|
NumberField(
|
||||||
|
label = "Вариабельность (±0–10)",
|
||||||
|
value = state.cycleVariabilityDays,
|
||||||
|
onValueChange = { state = state.copy(cycleVariabilityDays = it.coerceIn(0, 10)) }
|
||||||
|
)
|
||||||
|
NumberField(
|
||||||
|
label = "Длительность менструации (1–10)",
|
||||||
|
value = state.periodLengthDays,
|
||||||
|
onValueChange = { state = state.copy(periodLengthDays = it.coerceIn(1, 10)) }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Лютеиновая фаза (auto / 8..17)
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
val auto = state.lutealPhaseDays == null
|
||||||
|
AssistChip(
|
||||||
|
onClick = { state = state.copy(lutealPhaseDays = null) },
|
||||||
|
label = { Text("Авто") },
|
||||||
|
leadingIcon = {},
|
||||||
|
colors = AssistChipDefaults.assistChipColors()
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
NumberField(
|
||||||
|
label = "Лютеиновая фаза (8–17, вручную)",
|
||||||
|
value = state.lutealPhaseDays ?: 14,
|
||||||
|
enabled = !auto,
|
||||||
|
onValueChange = { state = state.copy(lutealPhaseDays = it.coerceIn(8, 17)) },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = !auto,
|
||||||
|
onCheckedChange = { manual ->
|
||||||
|
state = state.copy(lutealPhaseDays = if (manual) 14 else null)
|
||||||
}
|
}
|
||||||
} else {
|
)
|
||||||
// Основное содержимое экрана настроек
|
}
|
||||||
CycleSettingsContent(
|
|
||||||
settings = settings!!,
|
// Дата начала последней менструации
|
||||||
validationState = validationState,
|
OutlinedButton(onClick = { showDatePicker = true }) {
|
||||||
cycleHistory = cycleHistory,
|
Text("Начало последней менструации: ${state.lastPeriodStart.formatRus()}")
|
||||||
exportImportState = exportImportState,
|
}
|
||||||
onBasicSettingChanged = {
|
}
|
||||||
|
|
||||||
|
// ---- Блок «Метод овуляции / ручной ввод» ----
|
||||||
|
SectionCard("Овуляция") {
|
||||||
|
EnumDropdown(
|
||||||
|
label = "Метод определения",
|
||||||
|
value = state.ovulationMethod,
|
||||||
|
options = OvulationMethod.entries,
|
||||||
|
titleMapper = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is BasicSettingChange.BaselineCycleLength ->
|
OvulationMethod.AUTO -> "Авто"
|
||||||
viewModel.updateBaselineCycleLength(it.value)
|
OvulationMethod.BBT -> "BBT"
|
||||||
is BasicSettingChange.CycleVariability ->
|
OvulationMethod.LH_TEST -> "Тесты LH"
|
||||||
viewModel.updateCycleVariability(it.value)
|
OvulationMethod.CERVICAL_MUCUS -> "Цервикальная слизь"
|
||||||
is BasicSettingChange.PeriodLength ->
|
OvulationMethod.MEDICAL -> "Мед.данные"
|
||||||
viewModel.updatePeriodLength(it.value)
|
|
||||||
is BasicSettingChange.LutealPhase ->
|
|
||||||
viewModel.updateLutealPhase(it.value)
|
|
||||||
is BasicSettingChange.LastPeriodStart ->
|
|
||||||
viewModel.updateLastPeriodStart(it.value)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOvulationMethodChanged = {
|
onChange = { state = state.copy(ovulationMethod = it) }
|
||||||
viewModel.updateOvulationMethod(it)
|
)
|
||||||
},
|
LabeledSwitch(
|
||||||
onAllowManualOvulationChanged = {
|
label = "Разрешить ручной ввод даты овуляции",
|
||||||
viewModel.updateAllowManualOvulation(it)
|
checked = state.allowManualOvulation,
|
||||||
},
|
onCheckedChange = { state = state.copy(allowManualOvulation = it) }
|
||||||
onStatusChanged = {
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Блок «Статусы» ----
|
||||||
|
SectionCard("Статусы") {
|
||||||
|
EnumDropdown(
|
||||||
|
label = "Гормональная контрацепция",
|
||||||
|
value = state.hormonalContraception,
|
||||||
|
options = HormonalContraception.entries,
|
||||||
|
titleMapper = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is StatusChange.HormonalContraception ->
|
HormonalContraception.NONE -> "Нет"
|
||||||
viewModel.updateHormonalContraception(it.value)
|
HormonalContraception.COC -> "КОК"
|
||||||
is StatusChange.Pregnant ->
|
HormonalContraception.IUD -> "ВМС"
|
||||||
viewModel.updatePregnancyStatus(it.value)
|
HormonalContraception.IMPLANT -> "Имплант"
|
||||||
is StatusChange.Postpartum ->
|
HormonalContraception.OTHER -> "Другое"
|
||||||
viewModel.updatePostpartumStatus(it.value)
|
|
||||||
is StatusChange.Lactating ->
|
|
||||||
viewModel.updateLactatingStatus(it.value)
|
|
||||||
is StatusChange.Perimenopause ->
|
|
||||||
viewModel.updatePerimenopauseStatus(it.value)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHistorySettingChanged = {
|
onChange = { state = state.copy(hormonalContraception = it) }
|
||||||
|
)
|
||||||
|
FlowToggles(
|
||||||
|
items = listOf(
|
||||||
|
"Беременность" to state.isPregnant,
|
||||||
|
"Постпартум" to state.isPostpartum,
|
||||||
|
"Лактация" to state.isLactating,
|
||||||
|
"Пременопауза" to state.perimenopause
|
||||||
|
),
|
||||||
|
onToggle = { idx, v ->
|
||||||
|
state = when (idx) {
|
||||||
|
0 -> state.copy(isPregnant = v)
|
||||||
|
1 -> state.copy(isPostpartum = v)
|
||||||
|
2 -> state.copy(isLactating = v)
|
||||||
|
else -> state.copy(perimenopause = v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Блок «История и алгоритм» ----
|
||||||
|
SectionCard("История и алгоритм") {
|
||||||
|
NumberField(
|
||||||
|
label = "Окно истории (циклов, 1–12)",
|
||||||
|
value = state.historyWindowCycles,
|
||||||
|
onValueChange = { state = state.copy(historyWindowCycles = it.coerceIn(1, 12)) }
|
||||||
|
)
|
||||||
|
LabeledSwitch(
|
||||||
|
label = "Исключать аномальные циклы",
|
||||||
|
checked = state.excludeOutliers,
|
||||||
|
onCheckedChange = { state = state.copy(excludeOutliers = it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Блок «Единицы и сенсоры» ----
|
||||||
|
SectionCard("Единицы и сенсоры") {
|
||||||
|
LabeledSwitch(
|
||||||
|
label = "Температура: °C (выкл — °F)",
|
||||||
|
checked = state.tempUnitCelsius,
|
||||||
|
onCheckedChange = { state = state.copy(tempUnitCelsius = it) }
|
||||||
|
)
|
||||||
|
TwoFieldsRow(
|
||||||
|
left = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.bbtWindowStart,
|
||||||
|
onValueChange = { state = state.copy(bbtWindowStart = it) },
|
||||||
|
label = { Text("BBT: окно с") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
right = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.bbtWindowEnd,
|
||||||
|
onValueChange = { state = state.copy(bbtWindowEnd = it) },
|
||||||
|
label = { Text("до") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.timezoneIana,
|
||||||
|
onValueChange = { state = state.copy(timezoneIana = it) },
|
||||||
|
label = { Text("Часовой пояс (IANA)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Блок «Уведомления» ----
|
||||||
|
SectionCard("Уведомления") {
|
||||||
|
NumberField(
|
||||||
|
label = "Напоминание о менструации (дней до)",
|
||||||
|
value = state.periodReminderDaysBefore,
|
||||||
|
onValueChange = { state = state.copy(periodReminderDaysBefore = it.coerceIn(0, 10)) }
|
||||||
|
)
|
||||||
|
NumberField(
|
||||||
|
label = "Напоминание об овуляции (дней до)",
|
||||||
|
value = state.ovulationReminderDaysBefore,
|
||||||
|
onValueChange = { state = state.copy(ovulationReminderDaysBefore = it.coerceIn(0, 10)) }
|
||||||
|
)
|
||||||
|
NumberField(
|
||||||
|
label = "Окно ПМС (дней)",
|
||||||
|
value = state.pmsWindowDays,
|
||||||
|
onValueChange = { state = state.copy(pmsWindowDays = it.coerceIn(0, 7)) }
|
||||||
|
)
|
||||||
|
NumberField(
|
||||||
|
label = "Оповещение об отклонении цикла (дней)",
|
||||||
|
value = state.deviationAlertDays,
|
||||||
|
onValueChange = { state = state.copy(deviationAlertDays = it.coerceIn(1, 14)) }
|
||||||
|
)
|
||||||
|
EnumDropdown(
|
||||||
|
label = "Фертильное окно — режим",
|
||||||
|
value = state.fertileWindowMode,
|
||||||
|
options = FertileWindowMode.entries,
|
||||||
|
titleMapper = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is HistorySetting.WindowCycles ->
|
FertileWindowMode.CONSERVATIVE -> "Консервативный"
|
||||||
viewModel.updateHistoryWindow(it.value)
|
FertileWindowMode.BALANCED -> "Сбалансированный"
|
||||||
is HistorySetting.ExcludeOutliers ->
|
FertileWindowMode.BROAD -> "Широкий"
|
||||||
viewModel.updateExcludeOutliers(it.value)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSensorSettingChanged = {
|
onChange = { state = state.copy(fertileWindowMode = it) }
|
||||||
when (it) {
|
)
|
||||||
is SensorSetting.TempUnit ->
|
|
||||||
viewModel.updateTemperatureUnit(it.value)
|
|
||||||
is SensorSetting.BbtTimeWindow ->
|
|
||||||
viewModel.updateBbtTimeWindow(it.value)
|
|
||||||
is SensorSetting.Timezone ->
|
|
||||||
viewModel.updateTimezone(it.value)
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onNotificationSettingChanged = {
|
if (!isValid && errorMsg != null) {
|
||||||
when (it) {
|
Text(errorMsg, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelMedium)
|
||||||
is NotificationSetting.PeriodReminder ->
|
|
||||||
viewModel.updatePeriodReminderDays(it.value)
|
|
||||||
is NotificationSetting.OvulationReminder ->
|
|
||||||
viewModel.updateOvulationReminderDays(it.value)
|
|
||||||
is NotificationSetting.PmsWindow ->
|
|
||||||
viewModel.updatePmsWindowDays(it.value)
|
|
||||||
is NotificationSetting.DeviationAlert ->
|
|
||||||
viewModel.updateDeviationAlertDays(it.value)
|
|
||||||
is NotificationSetting.FertileWindowMode ->
|
|
||||||
viewModel.updateFertileWindowMode(it.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onClose,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) { Text("Отмена") }
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { onSave(state) },
|
||||||
|
enabled = isValid,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) { Text("Сохранить") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDatePicker) {
|
||||||
|
val statePicker = rememberDatePickerState(initialSelectedDateMillis = state.lastPeriodStart.toEpochMillis())
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showDatePicker = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val millis = statePicker.selectedDateMillis
|
||||||
|
if (millis != null) {
|
||||||
|
state = state.copy(lastPeriodStart = millis.toLocalDate())
|
||||||
|
}
|
||||||
|
showDatePicker = false
|
||||||
|
}) { Text("Готово") }
|
||||||
},
|
},
|
||||||
onCycleAtypicalToggled = { cycleId, atypical ->
|
dismissButton = { TextButton(onClick = { showDatePicker = false }) { Text("Отмена") } }
|
||||||
viewModel.toggleCycleAtypical(cycleId, atypical)
|
) { DatePicker(state = statePicker) }
|
||||||
},
|
}
|
||||||
onExportSettings = {
|
}
|
||||||
viewModel.exportSettingsToJson()
|
|
||||||
},
|
// ---------- Тот же UI как BottomSheet (если нужно модалкой) ----------
|
||||||
onImportSettings = { json ->
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
viewModel.importSettingsFromJson(json)
|
@Composable
|
||||||
},
|
fun CycleSettingsSheet(
|
||||||
onResetExportImportState = {
|
initial: CycleSettings = CycleSettings(),
|
||||||
viewModel.resetExportImportState()
|
onDismiss: () -> Unit,
|
||||||
|
onSave: (CycleSettings) -> Unit
|
||||||
|
) {
|
||||||
|
ModalBottomSheet(onDismissRequest = onDismiss) {
|
||||||
|
CycleSettingsScreen(
|
||||||
|
initial = initial,
|
||||||
|
onClose = onDismiss,
|
||||||
|
onSave = onSave
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Главный экран настроек для интеграции с навигацией и ViewModel
|
||||||
|
@Composable
|
||||||
|
fun CycleSettingsMainScreen(
|
||||||
|
viewModel: CycleSettingsViewModel = hiltViewModel(),
|
||||||
|
onNavigateBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val cycleSettings by viewModel.cycleSettingsFlow.collectAsState(initial = CycleSettings())
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
CycleSettingsScreen(
|
||||||
|
initial = cycleSettings,
|
||||||
|
onClose = onNavigateBack,
|
||||||
|
onSave = { settings ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
viewModel.saveSettings(settings)
|
||||||
|
onNavigateBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Вспомогательные UI-компоненты ----------
|
||||||
|
@Composable
|
||||||
|
private fun SectionCard(
|
||||||
|
title: String,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
Text(title, style = MaterialTheme.typography.titleMedium)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NumberField(
|
||||||
|
label: String,
|
||||||
|
value: Int,
|
||||||
|
onValueChange: (Int) -> Unit,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value.toString(),
|
||||||
|
onValueChange = { s -> s.filter { it.isDigit() }.takeIf { it.isNotEmpty() }?.toInt()?.let(onValueChange) },
|
||||||
|
label = { Text(label) },
|
||||||
|
singleLine = true,
|
||||||
|
enabled = enabled,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LabeledSwitch(
|
||||||
|
label: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(label)
|
||||||
|
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TwoFieldsRow(
|
||||||
|
left: @Composable () -> Unit,
|
||||||
|
right: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) { left() }
|
||||||
|
Box(modifier = Modifier.weight(1f)) { right() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FlowToggles(
|
||||||
|
items: List<Pair<String, Boolean>>,
|
||||||
|
onToggle: (Int, Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
items.forEachIndexed { index, (label, checked) ->
|
||||||
|
LabeledSwitch(
|
||||||
|
label = label,
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = { onToggle(index, it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun <T> EnumDropdown(
|
||||||
|
label: String,
|
||||||
|
value: T,
|
||||||
|
options: List<T>,
|
||||||
|
titleMapper: (T) -> String,
|
||||||
|
onChange: (T) -> Unit
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = titleMapper(value),
|
||||||
|
onValueChange = {},
|
||||||
|
label = { Text(label) },
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
options.forEach { opt ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(titleMapper(opt)) },
|
||||||
|
onClick = {
|
||||||
|
onChange(opt)
|
||||||
|
expanded = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -192,40 +489,38 @@ fun CycleSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ---------- Валидация ----------
|
||||||
* Классы для передачи событий изменения настроек из UI в ViewModel
|
private fun validate(s: CycleSettings): Pair<Boolean, String?> {
|
||||||
*/
|
if (s.baselineCycleLength !in 18..60) return false to "Длина цикла должна быть 18–60 дней"
|
||||||
sealed class BasicSettingChange {
|
if (s.cycleVariabilityDays !in 0..10) return false to "Вариабельность 0–10"
|
||||||
data class BaselineCycleLength(val value: Int) : BasicSettingChange()
|
if (s.periodLengthDays !in 1..10) return false to "Менструация от 1 до 10 дней"
|
||||||
data class CycleVariability(val value: Int) : BasicSettingChange()
|
if (s.lutealPhaseDays != null && s.lutealPhaseDays !in 8..17) return false to "Лютеиновая фаза 8–17 или Авто"
|
||||||
data class PeriodLength(val value: Int) : BasicSettingChange()
|
if (s.historyWindowCycles !in 1..12) return false to "Окно истории 1–12 циклов"
|
||||||
data class LutealPhase(val value: String) : BasicSettingChange()
|
if (s.periodReminderDaysBefore !in 0..10) return false to "Напоминание о менструации 0–10 дней"
|
||||||
data class LastPeriodStart(val value: java.time.LocalDate) : BasicSettingChange()
|
if (s.ovulationReminderDaysBefore !in 0..10) return false to "Напоминание об овуляции 0–10 дней"
|
||||||
|
if (s.pmsWindowDays !in 0..7) return false to "Окно ПМС 0–7 дней"
|
||||||
|
if (s.deviationAlertDays !in 1..14) return false to "Оповещение об отклонении 1–14 дней"
|
||||||
|
return true to null
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class StatusChange {
|
// ---------- Утилиты даты ----------
|
||||||
data class HormonalContraception(val value: kr.smartsoltech.wellshe.domain.models.HormonalContraceptionType) : StatusChange()
|
private fun LocalDate.formatRus(): String = format(DateTimeFormatter.ofPattern("d MMM yyyy"))
|
||||||
data class Pregnant(val value: Boolean) : StatusChange()
|
private fun LocalDate.toEpochMillis(): Long =
|
||||||
data class Postpartum(val value: Boolean) : StatusChange()
|
atStartOfDay(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||||
data class Lactating(val value: Boolean) : StatusChange()
|
|
||||||
data class Perimenopause(val value: Boolean) : StatusChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class HistorySetting {
|
private fun Long.toLocalDate(): LocalDate =
|
||||||
data class WindowCycles(val value: Int) : HistorySetting()
|
java.time.Instant.ofEpochMilli(this).atZone(java.time.ZoneId.systemDefault()).toLocalDate()
|
||||||
data class ExcludeOutliers(val value: Boolean) : HistorySetting()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class SensorSetting {
|
// ---------- Превью ----------
|
||||||
data class TempUnit(val value: kr.smartsoltech.wellshe.domain.models.TemperatureUnit) : SensorSetting()
|
@Preview(showBackground = true, widthDp = 380)
|
||||||
data class BbtTimeWindow(val value: String) : SensorSetting()
|
@Composable
|
||||||
data class Timezone(val value: String) : SensorSetting()
|
private fun CycleSettingsScreenPreview() {
|
||||||
|
val demo = CycleSettings()
|
||||||
|
MaterialTheme {
|
||||||
|
CycleSettingsScreen(
|
||||||
|
initial = demo,
|
||||||
|
onClose = {},
|
||||||
|
onSave = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class NotificationSetting {
|
|
||||||
data class PeriodReminder(val value: Int) : NotificationSetting()
|
|
||||||
data class OvulationReminder(val value: Int) : NotificationSetting()
|
|
||||||
data class PmsWindow(val value: Int) : NotificationSetting()
|
|
||||||
data class DeviationAlert(val value: Int) : NotificationSetting()
|
|
||||||
data class FertileWindowMode(val value: kr.smartsoltech.wellshe.domain.models.FertileWindowMode) : NotificationSetting()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kr.smartsoltech.wellshe.data.entity.CycleHistoryEntity
|
import kr.smartsoltech.wellshe.data.entity.CycleHistoryEntity
|
||||||
import kr.smartsoltech.wellshe.data.entity.CycleSettingsEntity
|
import kr.smartsoltech.wellshe.data.entity.CycleSettingsEntity
|
||||||
import kr.smartsoltech.wellshe.data.repository.CycleRepository
|
import kr.smartsoltech.wellshe.data.repository.CycleRepository
|
||||||
import kr.smartsoltech.wellshe.domain.models.CycleSettings
|
import kr.smartsoltech.wellshe.domain.models.*
|
||||||
import kr.smartsoltech.wellshe.domain.models.FertileWindowMode
|
|
||||||
import kr.smartsoltech.wellshe.domain.models.HormonalContraceptionType
|
|
||||||
import kr.smartsoltech.wellshe.domain.models.OvulationMethod
|
|
||||||
import kr.smartsoltech.wellshe.domain.models.TemperatureUnit
|
|
||||||
import kr.smartsoltech.wellshe.domain.services.CycleSettingsExportService
|
import kr.smartsoltech.wellshe.domain.services.CycleSettingsExportService
|
||||||
import kr.smartsoltech.wellshe.workers.CycleNotificationManager
|
import kr.smartsoltech.wellshe.workers.CycleNotificationManager
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
@@ -39,6 +35,11 @@ class CycleSettingsViewModel @Inject constructor(
|
|||||||
private val _settingsState = MutableStateFlow<CycleSettingsEntity?>(null)
|
private val _settingsState = MutableStateFlow<CycleSettingsEntity?>(null)
|
||||||
val settingsState: StateFlow<CycleSettingsEntity?> = _settingsState.asStateFlow()
|
val settingsState: StateFlow<CycleSettingsEntity?> = _settingsState.asStateFlow()
|
||||||
|
|
||||||
|
// Flow для UI-модели настроек цикла
|
||||||
|
val cycleSettingsFlow = _settingsState.map { entity ->
|
||||||
|
entity?.toUiModel() ?: CycleSettings()
|
||||||
|
}
|
||||||
|
|
||||||
// История циклов
|
// История циклов
|
||||||
private val _cycleHistory = MutableStateFlow<List<CycleHistoryEntity>>(emptyList())
|
private val _cycleHistory = MutableStateFlow<List<CycleHistoryEntity>>(emptyList())
|
||||||
val cycleHistory: StateFlow<List<CycleHistoryEntity>> = _cycleHistory.asStateFlow()
|
val cycleHistory: StateFlow<List<CycleHistoryEntity>> = _cycleHistory.asStateFlow()
|
||||||
@@ -516,6 +517,15 @@ class CycleSettingsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет все настройки цикла
|
||||||
|
*/
|
||||||
|
suspend fun saveSettings(settings: CycleSettings) {
|
||||||
|
val entity = settings.toEntity()
|
||||||
|
cycleRepository.saveSettings(entity)
|
||||||
|
// Уведомления будут запланированы внутри метода saveSettings репозитория через recalculateForecasts
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Модель состояния валидации полей
|
* Модель состояния валидации полей
|
||||||
*/
|
*/
|
||||||
@@ -567,5 +577,7 @@ class CycleSettingsViewModel @Inject constructor(
|
|||||||
val actionLabel: String? = null,
|
val actionLabel: String? = null,
|
||||||
val action: (() -> Unit)? = null
|
val action: (() -> Unit)? = null
|
||||||
) : UiEvent()
|
) : UiEvent()
|
||||||
|
|
||||||
|
object SettingsSaved : UiEvent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,20 @@ fun AppNavGraph(
|
|||||||
startDestination = startDestination
|
startDestination = startDestination
|
||||||
) {
|
) {
|
||||||
composable(BottomNavItem.Cycle.route) {
|
composable(BottomNavItem.Cycle.route) {
|
||||||
CycleScreen()
|
CycleScreen(
|
||||||
|
onNavigateToSettings = {
|
||||||
|
navController.navigate("cycle_settings")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Маршрут к экрану настроек цикла
|
||||||
|
composable("cycle_settings") {
|
||||||
|
kr.smartsoltech.wellshe.ui.cycle.settings.CycleSettingsMainScreen(
|
||||||
|
onNavigateBack = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(BottomNavItem.Body.route) {
|
composable(BottomNavItem.Body.route) {
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ fun BottomNavigation(
|
|||||||
NavigationBar(
|
NavigationBar(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(80.dp), // Минимальная высота Material3
|
.height(64.dp)
|
||||||
|
.imePadding(), // Добавляем отступ для клавиатуры
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
tonalElevation = 8.dp,
|
tonalElevation = 8.dp,
|
||||||
windowInsets = WindowInsets(0.dp) // Убираем стандартные отступы
|
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal) // Учитываем только горизонтальные системные отступы
|
||||||
) {
|
) {
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentDestination = navBackStackEntry?.destination
|
val currentDestination = navBackStackEntry?.destination
|
||||||
@@ -81,7 +82,7 @@ fun BottomNavigation(
|
|||||||
contentDescription = item.title,
|
contentDescription = item.title,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 4.dp)
|
.padding(top = 4.dp)
|
||||||
.size(36.dp)
|
.size(32.dp) // Унифицируем размер иконок
|
||||||
.clip(RoundedCornerShape(6.dp))
|
.clip(RoundedCornerShape(6.dp))
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.padding(4.dp),
|
.padding(4.dp),
|
||||||
@@ -93,7 +94,7 @@ fun BottomNavigation(
|
|||||||
contentDescription = item.title,
|
contentDescription = item.title,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 4.dp)
|
.padding(top = 4.dp)
|
||||||
.size(32.dp),
|
.size(32.dp), // Размер не изменился
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user