init commit

This commit is contained in:
2026-01-13 15:58:31 +09:00
commit 0ce8ca0128
383 changed files with 17236 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

26
.idea/appInsightsSettings.xml generated Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

123
.idea/codeStyles/Project.xml generated Normal file
View 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
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

6
.idea/copilot.data.migration.agent.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.ask.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.edit.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

18
.idea/deploymentTargetSelector.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-11T22:58:32.450358206Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=R3CT80VPBQZ" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/render.experimental.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="showDecorations" value="true" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,69 @@
kotlin version: 2.2.0
error message: java.nio.file.NoSuchFileException: /home/trevor/AndroidStudioProjects/Elva/app/build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/processDebugResources/R.jar
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixFileAttributeViews$Basic.readAttributes(UnixFileAttributeViews.java:55)
at java.base/sun.nio.fs.UnixFileSystemProvider.readAttributes(UnixFileSystemProvider.java:171)
at java.base/sun.nio.fs.LinuxFileSystemProvider.readAttributes(LinuxFileSystemProvider.java:99)
at java.base/java.nio.file.Files.readAttributes(Files.java:1854)
at org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.ZipHandler$1.createAccessor(ZipHandler.java:19)
at org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.ZipHandler$1.createAccessor(ZipHandler.java:15)
at org.jetbrains.kotlin.com.intellij.util.io.FileAccessorCache.createHandle(FileAccessorCache.java:45)
at org.jetbrains.kotlin.com.intellij.util.io.FileAccessorCache.get(FileAccessorCache.java:39)
at org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.ZipHandler.acquireZipHandle(ZipHandler.java:46)
at org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.ZipHandlerBase.contentsToByteArray(ZipHandlerBase.java:85)
at org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.jar.CoreJarVirtualFile.contentsToByteArray(CoreJarVirtualFile.java:101)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCliJavaFileManagerImpl.findClass(KotlinCliJavaFileManagerImpl.kt:148)
at org.jetbrains.kotlin.resolve.jvm.KotlinJavaPsiFacade$CliFinder.findClass(KotlinJavaPsiFacade.java:538)
at org.jetbrains.kotlin.resolve.jvm.KotlinJavaPsiFacade.findClass(KotlinJavaPsiFacade.java:181)
at org.jetbrains.kotlin.load.java.JavaClassFinderImpl.findClass(JavaClassFinderImpl.kt:46)
at org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaPackageScope.classes$lambda$1(LazyJavaPackageScope.kt:80)
at org.jetbrains.kotlin.storage.LockBasedStorageManager$MapBasedMemoizedFunction.invoke(LockBasedStorageManager.java:578)
at org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaPackageScope.findClassifier(LazyJavaPackageScope.kt:145)
at org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaPackageScope.getContributedClassifier(LazyJavaPackageScope.kt:135)
at org.jetbrains.kotlin.load.java.lazy.descriptors.JvmPackageScope.getContributedClassifier(JvmPackageScope.kt:55)
at org.jetbrains.kotlin.resolve.scopes.ChainedMemberScope.getContributedClassifier(ChainedMemberScope.kt:35)
at org.jetbrains.kotlin.resolve.scopes.AbstractScopeAdapter.getContributedClassifier(AbstractScopeAdapter.kt:44)
at org.jetbrains.kotlin.resolve.LazyExplicitImportScope.getContributedClassifier(LazyExplicitImportScope.kt:45)
at org.jetbrains.kotlin.resolve.LazyExplicitImportScope.getContributedDescriptors(LazyExplicitImportScope.kt:71)
at org.jetbrains.kotlin.resolve.scopes.BaseImportingScope.getContributedDescriptors(Scopes.kt:167)
at org.jetbrains.kotlin.resolve.scopes.ResolutionScope$DefaultImpls.getContributedDescriptors$default(ResolutionScope.kt:50)
at org.jetbrains.kotlin.resolve.LazyExplicitImportScope.storeReferencesToDescriptors$frontend(LazyExplicitImportScope.kt:120)
at org.jetbrains.kotlin.resolve.lazy.LazyImportResolverForKtImportDirective.forceResolveImportDirective$lambda$0(LazyImportScope.kt:172)
at org.jetbrains.kotlin.storage.LockBasedStorageManager$MapBasedMemoizedFunction.invoke(LockBasedStorageManager.java:578)
at org.jetbrains.kotlin.storage.LockBasedStorageManager$MapBasedMemoizedFunctionToNotNull.invoke(LockBasedStorageManager.java:681)
at org.jetbrains.kotlin.resolve.lazy.LazyImportResolverForKtImportDirective.forceResolveImport(LazyImportScope.kt:231)
at org.jetbrains.kotlin.resolve.lazy.LazyImportResolverForKtImportDirective.forceResolveNonDefaultImportsTask$lambda$3(LazyImportScope.kt:184)
at org.jetbrains.kotlin.storage.LockBasedStorageManager$LockBasedLazyValue.invoke(LockBasedStorageManager.java:408)
at org.jetbrains.kotlin.storage.LockBasedStorageManager$LockBasedNotNullLazyValue.invoke(LockBasedStorageManager.java:527)
at org.jetbrains.kotlin.resolve.lazy.LazyImportResolverForKtImportDirective.forceResolveNonDefaultImports(LazyImportScope.kt:214)
at org.jetbrains.kotlin.resolve.lazy.FileScopeFactory$FilesScopesBuilder$importResolver$1.forceResolveNonDefaultImports(FileScopeFactory.kt:170)
at org.jetbrains.kotlin.resolve.LazyTopDownAnalyzer.resolveImportsInFile(LazyTopDownAnalyzer.kt:252)
at org.jetbrains.kotlin.kapt.PartialAnalysisHandlerExtension.doAnalysis(PartialAnalysisHandlerExtension.kt:55)
at org.jetbrains.kotlin.kapt.AbstractKaptExtension.doAnalysis(KaptExtension.kt:127)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:345)
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:336)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:170)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:81)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:187)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:44)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:123)
at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:352)
at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:330)
at org.jetbrains.kotlin.cli.common.CLICompiler.execAndOutputXml(CLICompiler.kt:73)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileInProcessImpl(GradleKotlinCompilerWork.kt:388)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.access$compileInProcessImpl(GradleKotlinCompilerWork.kt:85)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork$compileInProcess$future$1.call(GradleKotlinCompilerWork.kt:361)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork$compileInProcess$future$1.call(GradleKotlinCompilerWork.kt:360)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)

0
README.md Normal file
View File

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

100
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,100 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.android.hilt)
}
android {
namespace = "com.example.elva"
compileSdk = 34
defaultConfig {
applicationId = "com.example.elva"
minSdk = 31
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
sourceSets {
getByName("main") {
java.srcDirs("src/main/java", "src/main/kotlin")
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs += listOf(
"-Xjvm-default=all",
"-opt-in=kotlin.RequiresOptIn"
)
languageVersion = "2.0" // Ограничиваем до 2.0 для kapt
}
buildFeatures {
viewBinding = true
dataBinding = true
}
}
kapt {
correctErrorTypes = true
useBuildCache = true
arguments {
arg("dagger.fastInit", "enabled")
arg("dagger.strictMultibindingValidation", "disabled")
}
javacOptions {
option("-source", "17")
option("-target", "17")
}
}
dependencies {
// Используем Kotlin 2.2.0 из libs.versions.toml
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.retrofit)
implementation(libs.retrofit.serialization)
implementation(libs.okhttp.logging)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.security.crypto)
implementation(libs.hilt.android)
implementation(libs.play.services.maps)
implementation(libs.androidx.fragment)
implementation(libs.dots.indicator)
implementation("com.google.code.gson:gson:2.10.1")
kapt(libs.hilt.compiler)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.example.elva
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.elva", appContext.packageName)
}
}

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".ElvaApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Elva"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:resizeableActivity="true"
android:theme="@style/Theme.Elva.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,3 @@
// ФАЙЛ УДАЛЕН - используйте AuthModels.kt
// Этот класс теперь находится в AuthModels.kt

View File

@@ -0,0 +1,8 @@
package com.example.elva
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class ElvaApp : Application()

View File

@@ -0,0 +1,153 @@
package com.example.elva
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import androidx.appcompat.app.AppCompatActivity
import com.example.elva.databinding.ActivityMainBinding
import com.example.elva.util.AuthManager
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
@Inject
lateinit var authManager: AuthManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Скрываем ActionBar по умолчанию (будет показан только для основных экранов)
supportActionBar?.hide()
val navController = (supportFragmentManager
.findFragmentById(R.id.nav_host_fragment_content_main) as NavHostFragment)
.navController
// Определяем стартовый фрагмент
val sharedPref = getSharedPreferences("elva_prefs", MODE_PRIVATE)
val onboardingCompleted = sharedPref.getBoolean("onboarding_completed", false)
val hasToken = authManager.getAccessToken() != null
val isLoggedIn = authManager.isLoggedIn()
android.util.Log.d("MainActivity", "onCreate: onboardingCompleted=$onboardingCompleted")
android.util.Log.d("MainActivity", "onCreate: hasToken=$hasToken")
android.util.Log.d("MainActivity", "onCreate: isLoggedIn=$isLoggedIn")
// Настраиваем граф навигации с правильным стартовым фрагментом
val navGraph = navController.navInflater.inflate(R.navigation.mobile_navigation)
when {
!onboardingCompleted -> {
// Показываем onboarding
android.util.Log.d("MainActivity", "Starting with onboarding")
navGraph.setStartDestination(R.id.onboardingFragment)
}
isLoggedIn -> {
// Пользователь авторизован и токен действителен - показываем dashboard
android.util.Log.d("MainActivity", "User is logged in with valid token, starting with dashboard")
navGraph.setStartDestination(R.id.dashboardFragment)
}
hasToken -> {
// Есть токен, но он истек - показываем экран входа
android.util.Log.d("MainActivity", "Token expired, starting with login")
navGraph.setStartDestination(R.id.loginFragment)
}
else -> {
// Нет токена - показываем экран входа
android.util.Log.d("MainActivity", "No token, starting with login")
navGraph.setStartDestination(R.id.loginFragment)
}
}
navController.graph = navGraph
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.onboardingFragment,
R.id.loginFragment,
R.id.dashboardFragment,
R.id.nav_profile,
R.id.nav_sos,
R.id.nav_alerts,
R.id.nav_contacts,
R.id.nav_calendar,
R.id.nav_nutrition,
R.id.nav_settings,
R.id.nav_location,
R.id.nav_safety
),
binding.drawerLayout
)
binding.navView.setupWithNavController(navController)
// Скрываем навбар, ActionBar и блокируем drawer для onboarding и auth экранов
navController.addOnDestinationChangedListener { _, destination, _ ->
android.util.Log.d("MainActivity", "Navigated to: ${destination.label}")
val bottomNav = findViewById<android.view.View>(R.id.bottom_navigation)
when (destination.id) {
R.id.onboardingFragment,
R.id.loginFragment -> {
// Скрываем навбар и ActionBar для onboarding и auth экранов
bottomNav?.visibility = android.view.View.GONE
supportActionBar?.hide()
binding.drawerLayout.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
}
else -> {
// Показываем навбар но оставляем ActionBar скрытым для всех экранов
bottomNav?.visibility = android.view.View.VISIBLE
supportActionBar?.hide()
binding.drawerLayout.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED)
// Настраиваем кастомный навбар
setupBottomNavigation(navController)
}
}
}
}
private fun setupBottomNavigation(navController: androidx.navigation.NavController) {
findViewById<android.view.View>(R.id.nav_cycle)?.setOnClickListener {
navController.navigate(R.id.nav_cycle)
}
findViewById<android.view.View>(R.id.nav_health)?.setOnClickListener {
navController.navigate(R.id.dashboardFragment)
}
findViewById<android.view.View>(R.id.nav_sos)?.setOnClickListener {
navController.navigate(R.id.nav_sos)
}
findViewById<android.view.View>(R.id.nav_chat)?.setOnClickListener {
// TODO: Navigate to Chat fragment
}
findViewById<android.view.View>(R.id.nav_profile)?.setOnClickListener {
navController.navigate(R.id.nav_profile)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_settings -> {
val navController = findNavController(R.id.nav_host_fragment_content_main)
navController.navigate(R.id.nav_settings)
}
}
return super.onOptionsItemSelected(item)
}
}

View File

@@ -0,0 +1,21 @@
package com.example.elva.data.api
object ApiConfig {
// Для реального устройства используйте IP вашего компьютера
// Для эмулятора используйте 10.0.2.2
const val BASE_HOST = "192.168.0.112" // Замените на IP вашего компьютера
const val BASE_URL = "http://$BASE_HOST:8000"
const val USER_SERVICE_URL = "http://$BASE_HOST:8001"
const val EMERGENCY_SERVICE_URL = "http://$BASE_HOST:8002"
const val LOCATION_SERVICE_URL = "http://$BASE_HOST:8003"
const val CALENDAR_SERVICE_URL = "http://$BASE_HOST:8004"
const val NOTIFICATION_SERVICE_URL = "http://$BASE_HOST:8005"
const val WS_URL = "ws://$BASE_HOST:8002/api/v1/emergency/ws"
const val CONNECT_TIMEOUT = 30L
const val READ_TIMEOUT = 30L
const val WRITE_TIMEOUT = 30L
}

View File

@@ -0,0 +1,17 @@
package com.example.elva.data.api
import com.example.elva.data.models.auth.RegisterRequest
import com.example.elva.data.models.auth.LoginRequest
import com.example.elva.data.models.auth.AuthResponse
import retrofit2.http.Body
import retrofit2.http.POST
interface AuthApiService {
@POST("api/v1/auth/register")
suspend fun register(@Body request: RegisterRequest): AuthResponse
@POST("api/v1/auth/login")
suspend fun login(@Body request: LoginRequest): AuthResponse
}

View File

@@ -0,0 +1,15 @@
package com.example.elva.data.api
import com.example.elva.data.models.profile.ProfileUpdateRequest
import com.example.elva.data.models.profile.UserProfile
import retrofit2.http.*
interface ProfileApiService {
@GET("api/v1/profile")
suspend fun getProfile(): UserProfile
@PUT("api/v1/profile")
suspend fun updateProfile(@Body request: ProfileUpdateRequest): UserProfile
}

View File

@@ -0,0 +1,14 @@
package com.example.elva.data.api
import com.example.elva.data.models.user.UserProfile
import retrofit2.http.GET
import retrofit2.http.Header
interface UserApiService {
@GET("api/v1/users/me")
suspend fun getCurrentUser(
@Header("Authorization") token: String
): UserProfile
}

View File

@@ -0,0 +1,218 @@
package com.example.elva.data.local
import android.content.Context
import android.content.SharedPreferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.time.LocalDate
import java.time.LocalDateTime
import com.example.elva.data.models.health.*
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class HealthDataStore @Inject constructor(
@ApplicationContext context: Context
) {
private val prefs: SharedPreferences = context.getSharedPreferences("health_data", Context.MODE_PRIVATE)
private val gson = Gson()
// Period tracking
fun savePeriods(periods: List<Period>) {
val json = gson.toJson(periods)
prefs.edit().putString("periods", json).apply()
}
fun getPeriods(): List<Period> {
val json = prefs.getString("periods", null) ?: return emptyList()
val type = object : TypeToken<List<Period>>() {}.type
return gson.fromJson(json, type)
}
fun addPeriod(period: Period) {
val periods = getPeriods().toMutableList()
periods.add(period.copy(id = System.currentTimeMillis()))
savePeriods(periods)
}
fun updatePeriod(period: Period) {
val periods = getPeriods().toMutableList()
val index = periods.indexOfFirst { it.id == period.id }
if (index != -1) {
periods[index] = period
savePeriods(periods)
}
}
fun deletePeriod(periodId: Long) {
val periods = getPeriods().filter { it.id != periodId }
savePeriods(periods)
}
// Water intake
fun saveWaterIntakes(intakes: List<WaterIntake>) {
val json = gson.toJson(intakes)
prefs.edit().putString("water_intakes", json).apply()
}
fun getWaterIntakes(): List<WaterIntake> {
val json = prefs.getString("water_intakes", null) ?: return emptyList()
val type = object : TypeToken<List<WaterIntake>>() {}.type
return gson.fromJson(json, type)
}
fun addWater(date: LocalDate, amountMl: Int) {
val intakes = getWaterIntakes().toMutableList()
val existing = intakes.find { it.date == date }
if (existing != null) {
intakes.removeIf { it.date == date }
intakes.add(existing.copy(amountMl = existing.amountMl + amountMl))
} else {
intakes.add(WaterIntake(
id = System.currentTimeMillis(),
date = date,
amountMl = amountMl
))
}
saveWaterIntakes(intakes)
}
fun getWaterGoal(): Int = prefs.getInt("water_goal", 2000)
fun setWaterGoal(goalMl: Int) = prefs.edit().putInt("water_goal", goalMl).apply()
// Calories
fun saveCalorieEntries(entries: List<CalorieEntry>) {
val json = gson.toJson(entries)
prefs.edit().putString("calorie_entries", json).apply()
}
fun getCalorieEntries(): List<CalorieEntry> {
val json = prefs.getString("calorie_entries", null) ?: return emptyList()
val type = object : TypeToken<List<CalorieEntry>>() {}.type
return gson.fromJson(json, type)
}
fun addCalorieEntry(entry: CalorieEntry) {
val entries = getCalorieEntries().toMutableList()
entries.add(entry.copy(id = System.currentTimeMillis()))
saveCalorieEntries(entries)
}
fun deleteCalorieEntry(entryId: Long) {
val entries = getCalorieEntries().filter { it.id != entryId }
saveCalorieEntries(entries)
}
fun getCalorieGoal(): CalorieGoal {
val dailyGoal = prefs.getInt("calorie_goal", 2000)
val protein = prefs.getInt("protein_goal", 0)
val carbs = prefs.getInt("carbs_goal", 0)
val fats = prefs.getInt("fats_goal", 0)
return CalorieGoal(dailyGoal, protein, carbs, fats)
}
fun setCalorieGoal(goal: CalorieGoal) {
prefs.edit()
.putInt("calorie_goal", goal.dailyGoal)
.putInt("protein_goal", goal.protein)
.putInt("carbs_goal", goal.carbs)
.putInt("fats_goal", goal.fats)
.apply()
}
// Weight
fun saveWeightEntries(entries: List<WeightEntry>) {
val json = gson.toJson(entries)
prefs.edit().putString("weight_entries", json).apply()
}
fun getWeightEntries(): List<WeightEntry> {
val json = prefs.getString("weight_entries", null) ?: return emptyList()
val type = object : TypeToken<List<WeightEntry>>() {}.type
return gson.fromJson(json, type)
}
fun addWeightEntry(entry: WeightEntry) {
val entries = getWeightEntries().toMutableList()
entries.add(entry.copy(id = System.currentTimeMillis()))
saveWeightEntries(entries)
}
fun deleteWeightEntry(entryId: Long) {
val entries = getWeightEntries().filter { it.id != entryId }
saveWeightEntries(entries)
}
// Sleep
fun saveSleepEntries(entries: List<SleepEntry>) {
val json = gson.toJson(entries)
prefs.edit().putString("sleep_entries", json).apply()
}
fun getSleepEntries(): List<SleepEntry> {
val json = prefs.getString("sleep_entries", null) ?: return emptyList()
val type = object : TypeToken<List<SleepEntry>>() {}.type
return gson.fromJson(json, type)
}
fun addSleepEntry(entry: SleepEntry) {
val entries = getSleepEntries().toMutableList()
entries.add(entry.copy(id = System.currentTimeMillis()))
saveSleepEntries(entries)
}
fun deleteSleepEntry(entryId: Long) {
val entries = getSleepEntries().filter { it.id != entryId }
saveSleepEntries(entries)
}
fun getSleepGoal(): Float = prefs.getFloat("sleep_goal", 8f)
fun setSleepGoal(hours: Float) = prefs.edit().putFloat("sleep_goal", hours).apply()
// Journal
fun saveJournalEntries(entries: List<HealthJournalEntry>) {
val json = gson.toJson(entries)
prefs.edit().putString("journal_entries", json).apply()
}
fun getJournalEntries(): List<HealthJournalEntry> {
val json = prefs.getString("journal_entries", null) ?: return emptyList()
val type = object : TypeToken<List<HealthJournalEntry>>() {}.type
return gson.fromJson(json, type)
}
fun addJournalEntry(entry: HealthJournalEntry) {
val entries = getJournalEntries().toMutableList()
entries.add(entry.copy(id = System.currentTimeMillis()))
saveJournalEntries(entries)
}
fun deleteJournalEntry(entryId: Long) {
val entries = getJournalEntries().filter { it.id != entryId }
saveJournalEntries(entries)
}
// Helper methods for today's data
fun getTodayWaterIntake(): Int {
val today = LocalDate.now()
return getWaterIntakes().find { it.date == today }?.amountMl ?: 0
}
fun getTodayCalories(): Int {
val today = LocalDate.now()
return getCalorieEntries()
.filter { it.date == today }
.sumOf { it.calories }
}
fun getTodaySleep(): SleepEntry? {
val today = LocalDate.now()
return getSleepEntries().find { it.date == today }
}
fun getLatestWeight(): WeightEntry? {
return getWeightEntries().maxByOrNull { it.date }
}
}

View File

@@ -0,0 +1,35 @@
package com.example.elva.data.local
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore by preferencesDataStore(name = "elva_prefs")
class SessionManager(private val context: Context) {
private val tokenKey = stringPreferencesKey(KEY_TOKEN)
val token: Flow<String?> = context.dataStore.data.map { it[tokenKey] }
suspend fun saveToken(value: String) {
context.dataStore.edit { prefs ->
prefs[tokenKey] = value
}
}
suspend fun clearToken() {
context.dataStore.edit { prefs ->
prefs.remove(tokenKey)
}
}
companion object {
private const val KEY_TOKEN = "auth_token"
}
}

View File

@@ -0,0 +1,60 @@
package com.example.elva.data.models.auth
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Запрос на регистрацию
*/
@Serializable
data class RegisterRequest(
val username: String,
val email: String,
val password: String,
@SerialName("full_name")
val fullName: String? = null,
val phone: String? = null,
@SerialName("date_of_birth")
val dateOfBirth: String? = null,
val bio: String? = null
)
/**
* Запрос на вход
*/
@Serializable
data class LoginRequest(
val email: String? = null,
val username: String? = null,
val password: String
)
/**
* Ответ от сервера при успешной аутентификации
*/
@Serializable
data class AuthResponse(
@SerialName("access_token")
val accessToken: String,
@SerialName("token_type")
val tokenType: String = "bearer"
)
/**
* Информация о пользователе
*/
@Serializable
data class UserInfo(
val id: Int,
val uuid: String? = null,
val username: String,
val email: String,
@SerialName("first_name")
val firstName: String? = null,
@SerialName("last_name")
val lastName: String? = null,
val phone: String? = null,
@SerialName("avatar_url")
val avatarUrl: String? = null
)

View File

@@ -0,0 +1,13 @@
package com.example.elva.data.models.dashboard
data class DashboardSummary(
val cycleDay: Int = 0,
val nextPeriodDays: Int = 0,
val water: Int = 0,
val waterGoal: Int = 2000,
val calories: Int = 0,
val caloriesGoal: Int = 2000,
val sleep: Float = 0f,
val weight: Float = 0f
)

View File

@@ -0,0 +1,19 @@
package com.example.elva.data.models.health
enum class FlowIntensity(val displayName: String) {
LIGHT("Легкая"),
MEDIUM("Средняя"),
HEAVY("Обильная");
companion object {
fun fromString(value: String?): FlowIntensity {
return when (value?.uppercase()) {
"LIGHT" -> LIGHT
"MEDIUM" -> MEDIUM
"HEAVY" -> HEAVY
else -> MEDIUM
}
}
}
}

View File

@@ -0,0 +1,69 @@
package com.example.elva.data.models.health
import java.time.LocalDate
import java.time.LocalDateTime
data class WaterIntake(
val id: Long = 0,
val date: LocalDate,
val amountMl: Int,
val goalMl: Int = 2000
)
data class CalorieEntry(
val id: Long = 0,
val date: LocalDate,
val calories: Int,
val mealType: MealType,
val description: String = "",
val timestamp: LocalDateTime = LocalDateTime.now()
)
enum class MealType {
BREAKFAST, LUNCH, DINNER, SNACK
}
data class CalorieGoal(
val dailyGoal: Int = 2000,
val protein: Int = 0,
val carbs: Int = 0,
val fats: Int = 0
)
data class WeightEntry(
val id: Long = 0,
val date: LocalDate,
val weightKg: Float,
val notes: String = ""
)
data class SleepEntry(
val id: Long = 0,
val date: LocalDate,
val sleepStart: LocalDateTime,
val sleepEnd: LocalDateTime,
val quality: SleepQuality = SleepQuality.GOOD,
val notes: String = ""
) {
val durationHours: Float
get() = java.time.Duration.between(sleepStart, sleepEnd).toMinutes() / 60f
}
enum class SleepQuality {
POOR, FAIR, GOOD, EXCELLENT
}
data class HealthJournalEntry(
val id: Long = 0,
val date: LocalDate,
val timestamp: LocalDateTime = LocalDateTime.now(),
val type: JournalType,
val title: String,
val content: String,
val mood: Int = 5 // 1-10 scale
)
enum class JournalType {
GENERAL, SYMPTOMS, MOOD, EXERCISE, MEDICATION, NOTES
}

View File

@@ -0,0 +1,53 @@
package com.example.elva.data.models.health
import java.time.LocalDate
import java.time.LocalDateTime
// Информация о цикле
data class CycleInfo(
val averageCycleLength: Int = 28,
val averagePeriodLength: Int = 5,
val lastPeriodStart: LocalDate?,
val nextPeriodPrediction: LocalDate?,
val ovulationPrediction: LocalDate?,
val currentCycleDay: Int = 1
)
// Потребление воды - используем из HealthMetrics.kt
// data class WaterIntake уже определён в HealthMetrics.kt
// Калории - используем из HealthMetrics.kt
// data class CalorieEntry уже определён в HealthMetrics.kt
// data class CalorieGoal уже определён в HealthMetrics.kt
// enum class MealType уже определён в HealthMetrics.kt
// Для совместимости с HealthViewModel
data class CalorieIntake(
val date: LocalDate,
val calories: Int,
val goal: Int = 2000,
val meals: List<CalorieEntry> = emptyList()
)
data class WaterIntakeOld(
val date: LocalDate,
val amount: Int, // в мл
val goal: Int = 2000 // в мл
)
// Вес - используем из HealthMetrics.kt
// data class WeightEntry уже определён в HealthMetrics.kt
// Сон - используем из HealthMetrics.kt
// data class SleepEntry уже определён в HealthMetrics.kt
// enum class SleepQuality уже определён в HealthMetrics.kt
// Цели здоровья
data class HealthGoals(
val waterGoal: Int = 2000, // мл
val calorieGoal: Int = 2000, // ккал
val sleepGoal: Float = 8f, // часы
val weightGoal: Float? = null // кг
)

View File

@@ -0,0 +1,30 @@
package com.example.elva.data.models.health
import java.time.LocalDate
data class Period(
val id: Long = 0,
val startDate: LocalDate,
val endDate: LocalDate,
val flow: FlowIntensity = FlowIntensity.MEDIUM,
val symptoms: List<Symptom> = emptyList(),
val notes: String = ""
)
enum class FlowIntensity {
LIGHT, MEDIUM, HEAVY
}
enum class Symptom {
CRAMPS, HEADACHE, MOOD_SWINGS, BLOATING, FATIGUE, BACK_PAIN, NAUSEA
}
data class CycleStats(
val averageCycleLength: Int,
val averagePeriodLength: Int,
val nextPeriodStart: LocalDate?,
val nextOvulation: LocalDate?,
val fertileWindowStart: LocalDate?,
val fertileWindowEnd: LocalDate?
)

View File

@@ -0,0 +1,72 @@
package com.example.elva.data.models.profile
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Профиль пользователя
*/
@Serializable
data class UserProfile(
val id: Int,
val uuid: String,
val username: String,
val email: String,
val phone: String? = null,
@SerialName("first_name")
val firstName: String? = null,
@SerialName("last_name")
val lastName: String? = null,
@SerialName("date_of_birth")
val dateOfBirth: String? = null,
val bio: String? = null,
@SerialName("avatar_url")
val avatarUrl: String? = null,
@SerialName("location_sharing_enabled")
val locationSharingEnabled: Boolean = true,
@SerialName("emergency_notifications_enabled")
val emergencyNotificationsEnabled: Boolean = true,
@SerialName("push_notifications_enabled")
val pushNotificationsEnabled: Boolean = true,
@SerialName("email_verified")
val emailVerified: Boolean = false,
@SerialName("phone_verified")
val phoneVerified: Boolean = false,
@SerialName("is_active")
val isActive: Boolean = true,
@SerialName("created_at")
val createdAt: String,
@SerialName("updated_at")
val updatedAt: String? = null
)
/**
* Запрос на обновление профиля
*/
@Serializable
data class ProfileUpdateRequest(
@SerialName("first_name")
val firstName: String? = null,
@SerialName("last_name")
val lastName: String? = null,
val phone: String? = null,
@SerialName("date_of_birth")
val dateOfBirth: String? = null,
val bio: String? = null,
@SerialName("location_sharing_enabled")
val locationSharingEnabled: Boolean? = null,
@SerialName("emergency_notifications_enabled")
val emergencyNotificationsEnabled: Boolean? = null,
@SerialName("push_notifications_enabled")
val pushNotificationsEnabled: Boolean? = null,
@SerialName("emergency_contact_1_name")
val emergencyContact1Name: String? = null,
@SerialName("emergency_contact_1_phone")
val emergencyContact1Phone: String? = null,
@SerialName("emergency_contact_2_name")
val emergencyContact2Name: String? = null,
@SerialName("emergency_contact_2_phone")
val emergencyContact2Phone: String? = null
)

View File

@@ -0,0 +1,47 @@
package com.example.elva.data.models.user
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserProfile(
val id: Int,
val uuid: String,
val username: String? = null,
val email: String,
val phone: String? = null,
@SerialName("phone_number")
val phoneNumber: String? = null,
@SerialName("first_name")
val firstName: String? = null,
@SerialName("last_name")
val lastName: String? = null,
@SerialName("full_name")
val fullName: String? = null,
@SerialName("date_of_birth")
val dateOfBirth: String? = null,
val bio: String? = null,
@SerialName("avatar_url")
val avatarUrl: String? = null,
@SerialName("emergency_contact_1_name")
val emergencyContact1Name: String? = null,
@SerialName("emergency_contact_1_phone")
val emergencyContact1Phone: String? = null,
@SerialName("emergency_contact_2_name")
val emergencyContact2Name: String? = null,
@SerialName("emergency_contact_2_phone")
val emergencyContact2Phone: String? = null,
@SerialName("location_sharing_enabled")
val locationSharingEnabled: Boolean = true,
@SerialName("emergency_notifications_enabled")
val emergencyNotificationsEnabled: Boolean = true,
@SerialName("push_notifications_enabled")
val pushNotificationsEnabled: Boolean = true,
@SerialName("email_verified")
val emailVerified: Boolean = false,
@SerialName("phone_verified")
val phoneVerified: Boolean = false,
@SerialName("is_active")
val isActive: Boolean = true
)

View File

@@ -0,0 +1,11 @@
package com.example.elva.data.remote
object ApiConfig {
const val USER_SERVICE_BASE_URL = "http://10.0.2.2:8001"
const val EMERGENCY_SERVICE_BASE_URL = "http://10.0.2.2:8002"
const val LOCATION_SERVICE_BASE_URL = "http://10.0.2.2:8003"
const val CALENDAR_SERVICE_BASE_URL = "http://10.0.2.2:8004"
const val NOTIFICATION_SERVICE_BASE_URL = "http://10.0.2.2:8005"
const val NUTRITION_SERVICE_BASE_URL = "http://10.0.2.2:8006"
}

View File

@@ -0,0 +1,20 @@
package com.example.elva.data.remote
import com.example.elva.data.remote.model.CalendarEntryDto
import com.example.elva.data.remote.model.CreateCalendarEntryRequest
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
interface CalendarApi {
@GET("/api/v1/calendar/entries")
suspend fun getEntries(@Header("Authorization") token: String): List<CalendarEntryDto>
@POST("/api/v1/calendar/entries")
suspend fun addEntry(
@Header("Authorization") token: String,
@Body request: CreateCalendarEntryRequest
): CalendarEntryDto
}

View File

@@ -0,0 +1,22 @@
package com.example.elva.data.remote
import com.example.elva.data.remote.model.ContactDto
import com.example.elva.data.remote.model.CreateContactRequest
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
interface ContactsApi {
@GET("/api/v1/users/me/emergency-contacts")
suspend fun getContacts(
@Header("Authorization") token: String
): List<ContactDto>
@POST("/api/v1/users/me/emergency-contacts")
suspend fun addContact(
@Header("Authorization") token: String,
@Body request: CreateContactRequest
): ContactDto
}

View File

@@ -0,0 +1,27 @@
package com.example.elva.data.remote
import com.example.elva.data.remote.model.AlertDto
import com.example.elva.data.remote.model.CreateAlertRequest
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
interface EmergencyApi {
@POST("/api/v1/alert")
suspend fun createAlert(
@Header("Authorization") token: String,
@Body request: CreateAlertRequest
): AlertDto
@GET("/api/v1/alerts/my")
suspend fun getMyAlerts(
@Header("Authorization") token: String
): List<AlertDto>
@GET("/api/v1/alerts/responses")
suspend fun getResponses(
@Header("Authorization") token: String
): List<AlertDto>
}

View File

@@ -0,0 +1,16 @@
package com.example.elva.data.remote
import com.example.elva.data.remote.model.NearbyUserDto
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface LocationApi {
@GET("/api/v1/nearby-users")
suspend fun getNearbyUsers(
@Header("Authorization") token: String,
@Query("latitude") latitude: Double,
@Query("longitude") longitude: Double
): List<NearbyUserDto>
}

View File

@@ -0,0 +1,10 @@
package com.example.elva.data.remote
import retrofit2.http.GET
import retrofit2.http.Header
interface NotificationApi {
@GET("/api/v1/notifications")
suspend fun getNotifications(@Header("Authorization") token: String): List<String>
}

View File

@@ -0,0 +1,22 @@
package com.example.elva.data.remote
import com.example.elva.data.remote.model.NutritionEntryDto
import com.example.elva.data.remote.model.NutritionEntryRequest
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
interface NutritionApi {
@POST("/api/v1/nutrition/entries")
suspend fun addEntry(
@Header("Authorization") token: String,
@Body request: NutritionEntryRequest
): NutritionEntryDto
@GET("/api/v1/nutrition/entries")
suspend fun getEntries(
@Header("Authorization") token: String
): List<NutritionEntryDto>
}

View File

@@ -0,0 +1,2 @@
// DEPRECATED: Этот файл удален. Используйте com.example.elva.data.api.AuthApiService

View File

@@ -0,0 +1,39 @@
package com.example.elva.data.remote.api
import com.example.elva.data.remote.model.emergency.AlertResponse
import com.example.elva.data.remote.model.emergency.AlertResponseItem
import com.example.elva.data.remote.model.emergency.CreateAlertRequest
import com.example.elva.data.remote.model.emergency.NearbyAlertsResponse
import com.example.elva.data.remote.model.emergency.RespondToAlertRequest
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
interface EmergencyApiService {
@POST("api/v1/alert")
suspend fun createAlert(@Body request: CreateAlertRequest): AlertResponse
@POST("api/v1/alert/{alert_id}/respond")
suspend fun respondToAlert(
@Path("alert_id") alertId: Int,
@Body request: RespondToAlertRequest
): AlertResponseItem
@GET("api/v1/alerts/nearby")
suspend fun getNearbyAlerts(
@Query("latitude") latitude: Double,
@Query("longitude") longitude: Double,
@Query("radius_km") radiusKm: Double = 5.0
): NearbyAlertsResponse
@PUT("api/v1/alert/{alert_id}/resolve")
suspend fun resolveAlert(@Path("alert_id") alertId: Int): AlertResponse
@GET("api/v1/alert/{alert_id}")
suspend fun getAlertDetails(@Path("alert_id") alertId: Int): AlertResponse
}

View File

@@ -0,0 +1,17 @@
package com.example.elva.data.remote.api
import com.example.elva.data.remote.model.profile.ProfileUpdateRequest
import com.example.elva.data.remote.model.profile.UserProfile
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
interface ProfileApiService {
@GET("api/v1/profile")
suspend fun getProfile(): UserProfile
@PUT("api/v1/profile")
suspend fun updateProfile(@Body request: ProfileUpdateRequest): UserProfile
}

View File

@@ -0,0 +1,28 @@
package com.example.elva.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateCalendarEntryRequest(
@SerialName("entry_date") val entryDate: String,
@SerialName("entry_type") val entryType: String,
val mood: String? = null,
val notes: String? = null,
@SerialName("period_symptoms") val periodSymptoms: String? = null,
@SerialName("flow_intensity") val flowIntensity: String? = null,
@SerialName("energy_level") val energyLevel: String? = null,
@SerialName("sleep_hours") val sleepHours: String? = null,
val symptoms: String? = null,
val medications: String? = null
)
@Serializable
data class CalendarEntryDto(
val id: String? = null,
@SerialName("entry_date") val entryDate: String? = null,
@SerialName("entry_type") val entryType: String? = null,
val mood: String? = null,
val notes: String? = null
)

View File

@@ -0,0 +1,20 @@
package com.example.elva.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateContactRequest(
val name: String,
@SerialName("phone_number") val phoneNumber: String,
val relation: String? = null
)
@Serializable
data class ContactDto(
val id: String? = null,
val name: String? = null,
@SerialName("phone_number") val phoneNumber: String? = null,
val relation: String? = null
)

View File

@@ -0,0 +1,26 @@
package com.example.elva.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateAlertRequest(
@SerialName("alert_type") val alertType: String,
val latitude: Double,
val longitude: Double,
val address: String,
val message: String,
@SerialName("contact_emergency_services") val contactEmergencyServices: Boolean = true,
@SerialName("notify_emergency_contacts") val notifyEmergencyContacts: Boolean = true
)
@Serializable
data class AlertDto(
val id: String? = null,
@SerialName("alert_type") val alertType: String? = null,
val address: String? = null,
val message: String? = null,
@SerialName("created_at") val createdAt: String? = null,
@SerialName("is_resolved") val isResolved: Boolean? = null
)

View File

@@ -0,0 +1,13 @@
package com.example.elva.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class NearbyUserDto(
val id: String? = null,
@SerialName("full_name") val fullName: String? = null,
val distance: Double? = null,
@SerialName("distance_km") val distanceKm: Double? = null
)

View File

@@ -0,0 +1,23 @@
package com.example.elva.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class NutritionEntryRequest(
@SerialName("food_name") val foodName: String,
val quantity: Int,
val unit: String,
val calories: Int,
val date: String,
val notes: String? = null
)
@Serializable
data class NutritionEntryDto(
val id: String? = null,
@SerialName("food_name") val foodName: String? = null,
val date: String? = null,
val notes: String? = null
)

View File

@@ -0,0 +1,165 @@
package com.example.elva.data.remote.model.emergency
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
// ==================== REST API MODELS ====================
@Serializable
data class CreateAlertRequest(
val latitude: Double,
val longitude: Double,
@SerialName("alert_type")
val alertType: String,
val message: String? = null,
val address: String? = null,
@SerialName("contact_emergency_services")
val contactEmergencyServices: Boolean = false,
@SerialName("notify_emergency_contacts")
val notifyEmergencyContacts: Boolean = true
)
@Serializable
data class AlertResponse(
val id: Int,
@SerialName("user_id")
val userId: Int,
val latitude: Double,
val longitude: Double,
@SerialName("alert_type")
val alertType: String,
val message: String? = null,
val address: String? = null,
val status: String,
@SerialName("created_at")
val createdAt: String,
@SerialName("updated_at")
val updatedAt: String? = null,
@SerialName("resolved_at")
val resolvedAt: String? = null
)
@Serializable
data class RespondToAlertRequest(
@SerialName("response_type")
val responseType: String,
val message: String? = null,
@SerialName("eta_minutes")
val etaMinutes: Int? = null,
@SerialName("location_sharing")
val locationSharing: Boolean = false
)
@Serializable
data class AlertResponseItem(
val id: Int,
@SerialName("alert_id")
val alertId: Int,
@SerialName("responder_id")
val responderId: Int,
@SerialName("response_type")
val responseType: String,
val message: String? = null,
@SerialName("eta_minutes")
val etaMinutes: Int? = null,
@SerialName("created_at")
val createdAt: String
)
@Serializable
data class NearbyAlertsResponse(
val alerts: List<AlertResponse>,
val count: Int
)
// ==================== WEBSOCKET MODELS ====================
@Serializable
data class WebSocketMessage(
val type: String,
@SerialName("user_id")
val userId: Int? = null,
val message: String? = null,
val timestamp: String? = null
)
@Serializable
data class EmergencyAlertWS(
val type: String,
@SerialName("alert_id")
val alertId: Int,
@SerialName("alert_type")
val alertType: String,
val latitude: Double,
val longitude: Double,
val address: String? = null,
val message: String? = null,
@SerialName("user_name")
val userName: String? = null,
@SerialName("user_phone")
val userPhone: String? = null,
@SerialName("created_at")
val createdAt: String,
@SerialName("distance_km")
val distanceKm: Double? = null
)
@Serializable
data class AlertUpdateWS(
val type: String,
@SerialName("alert_id")
val alertId: Int,
val status: String,
@SerialName("responded_users_count")
val respondedUsersCount: Int,
@SerialName("updated_at")
val updatedAt: String
)
@Serializable
data class AlertResponseWS(
val type: String,
@SerialName("alert_id")
val alertId: Int,
@SerialName("responder_id")
val responderId: Int,
@SerialName("responder_name")
val responderName: String? = null,
@SerialName("response_type")
val responseType: String,
val message: String? = null,
@SerialName("eta_minutes")
val etaMinutes: Int? = null,
@SerialName("created_at")
val createdAt: String
)
// ==================== ENUMS ====================
object AlertTypes {
const val GENERAL = "general"
const val MEDICAL = "medical"
const val VIOLENCE = "violence"
const val HARASSMENT = "harassment"
const val UNSAFE_AREA = "unsafe_area"
const val ACCIDENT = "accident"
const val FIRE = "fire"
const val NATURAL_DISASTER = "natural_disaster"
}
object ResponseTypes {
const val HELP_ON_WAY = "help_on_way"
const val CONTACTED_AUTHORITIES = "contacted_authorities"
const val SAFE_NOW = "safe_now"
const val FALSE_ALARM = "false_alarm"
const val INVESTIGATING = "investigating"
const val RESOLVED = "resolved"
}
object AlertStatuses {
const val ACTIVE = "active"
const val PENDING = "pending"
const val RESOLVED = "resolved"
const val CANCELLED = "cancelled"
}

View File

@@ -0,0 +1,57 @@
package com.example.elva.data.remote.model.profile
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserProfile(
val id: Int,
val uuid: String,
val username: String,
val email: String,
val phone: String? = null,
@SerialName("first_name")
val firstName: String? = null,
@SerialName("last_name")
val lastName: String? = null,
@SerialName("date_of_birth")
val dateOfBirth: String? = null,
val bio: String? = null,
@SerialName("avatar_url")
val avatarUrl: String? = null,
@SerialName("location_sharing_enabled")
val locationSharingEnabled: Boolean = true,
@SerialName("emergency_notifications_enabled")
val emergencyNotificationsEnabled: Boolean = true,
@SerialName("push_notifications_enabled")
val pushNotificationsEnabled: Boolean = true,
@SerialName("email_verified")
val emailVerified: Boolean = false,
@SerialName("phone_verified")
val phoneVerified: Boolean = false,
@SerialName("is_active")
val isActive: Boolean = true,
@SerialName("created_at")
val createdAt: String,
@SerialName("updated_at")
val updatedAt: String? = null
)
@Serializable
data class ProfileUpdateRequest(
@SerialName("first_name")
val firstName: String? = null,
@SerialName("last_name")
val lastName: String? = null,
val phone: String? = null,
@SerialName("date_of_birth")
val dateOfBirth: String? = null,
val bio: String? = null,
@SerialName("location_sharing_enabled")
val locationSharingEnabled: Boolean? = null,
@SerialName("emergency_notifications_enabled")
val emergencyNotificationsEnabled: Boolean? = null,
@SerialName("push_notifications_enabled")
val pushNotificationsEnabled: Boolean? = null
)

View File

@@ -0,0 +1,96 @@
package com.example.elva.data.repository
import com.example.elva.data.api.AuthApiService
import com.example.elva.data.models.auth.AuthResponse
import com.example.elva.data.models.auth.LoginRequest
import com.example.elva.data.models.auth.RegisterRequest
import com.example.elva.util.AuthManager
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AuthRepository @Inject constructor(
private val authApi: AuthApiService,
private val authManager: AuthManager
) {
suspend fun register(
username: String,
email: String,
password: String,
fullName: String? = null,
phone: String? = null
): Result<AuthResponse> {
return try {
val request = RegisterRequest(
username = username,
email = email,
password = password,
fullName = fullName,
phone = phone
)
val response = authApi.register(request)
// Сохраняем токен
authManager.saveAuthData(
token = response.accessToken,
userId = 0, // ID будет получен позже через запрос профиля
email = email,
username = username
)
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun login(
email: String? = null,
username: String? = null,
password: String
): Result<AuthResponse> {
return try {
android.util.Log.d("AuthRepository", "login() called with: email=$email, username=$username")
val request = LoginRequest(
email = email,
username = username,
password = password
)
val response = authApi.login(request)
android.util.Log.d("AuthRepository", "login response received: token=${response.accessToken.take(20)}...")
// Сохраняем токен и данные пользователя
val savedEmail = email ?: username ?: ""
val savedUsername = username ?: email ?: ""
android.util.Log.d("AuthRepository", "Saving auth data: email=$savedEmail, username=$savedUsername")
authManager.saveAuthData(
token = response.accessToken,
userId = 0, // ID будет получен позже через запрос профиля
email = savedEmail,
username = savedUsername
)
android.util.Log.d("AuthRepository", "Auth data saved successfully")
Result.success(response)
} catch (e: Exception) {
android.util.Log.e("AuthRepository", "login failed", e)
Result.failure(e)
}
}
fun logout() {
authManager.logout()
}
fun isLoggedIn(): Boolean {
return authManager.isLoggedIn()
}
}

View File

@@ -0,0 +1,32 @@
package com.example.elva.data.repository
import com.example.elva.data.models.dashboard.DashboardSummary
import com.example.elva.data.local.HealthDataStore
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DashboardRepository @Inject constructor(
private val healthDataStore: HealthDataStore
) {
suspend fun getDashboardSummary(): DashboardSummary {
val water = healthDataStore.getTodayWaterIntake()
val waterGoal = healthDataStore.getWaterGoal()
val calories = healthDataStore.getTodayCalories()
val caloriesGoal = healthDataStore.getCaloriesGoal()
val sleepEntry = healthDataStore.getTodaySleep()
val weightEntry = healthDataStore.getLatestWeight()
return DashboardSummary(
cycleDay = 0, // TODO: Calculate from period data
nextPeriodDays = 0, // TODO: Calculate from period data
water = water,
waterGoal = waterGoal,
calories = calories,
caloriesGoal = caloriesGoal,
sleep = sleepEntry?.hours ?: 0f,
weight = weightEntry?.weight ?: 0f
)
}
}

View File

@@ -0,0 +1,189 @@
package com.example.elva.data.repository
import com.example.elva.data.local.HealthDataStore
import com.example.elva.data.models.health.*
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class HealthRepository @Inject constructor(
private val dataStore: HealthDataStore
) {
// Period tracking
fun getPeriods(): List<Period> = dataStore.getPeriods()
fun addPeriod(period: Period) = dataStore.addPeriod(period)
fun updatePeriod(period: Period) = dataStore.updatePeriod(period)
fun deletePeriod(periodId: Long) = dataStore.deletePeriod(periodId)
fun calculateCycleStats(): CycleStats {
val periods = getPeriods().sortedBy { it.startDate }
if (periods.size < 2) {
return CycleStats(
averageCycleLength = 28,
averagePeriodLength = 5,
nextPeriodStart = periods.lastOrNull()?.startDate?.plusDays(28),
nextOvulation = periods.lastOrNull()?.startDate?.plusDays(14),
fertileWindowStart = periods.lastOrNull()?.startDate?.plusDays(12),
fertileWindowEnd = periods.lastOrNull()?.startDate?.plusDays(16)
)
}
// Calculate average cycle length
val cycleLengths = mutableListOf<Int>()
for (i in 0 until periods.size - 1) {
val days = ChronoUnit.DAYS.between(
periods[i].startDate,
periods[i + 1].startDate
).toInt()
cycleLengths.add(days)
}
val avgCycleLength = if (cycleLengths.isNotEmpty()) {
cycleLengths.average().toInt()
} else 28
// Calculate average period length
val periodLengths = periods.map {
ChronoUnit.DAYS.between(it.startDate, it.endDate).toInt() + 1
}
val avgPeriodLength = periodLengths.average().toInt()
// Predict next period
val lastPeriod = periods.last()
val nextPeriodStart = lastPeriod.startDate.plusDays(avgCycleLength.toLong())
// Calculate ovulation (typically 14 days before next period)
val nextOvulation = nextPeriodStart.minusDays(14)
// Fertile window (5 days before ovulation to 1 day after)
val fertileWindowStart = nextOvulation.minusDays(5)
val fertileWindowEnd = nextOvulation.plusDays(1)
return CycleStats(
averageCycleLength = avgCycleLength,
averagePeriodLength = avgPeriodLength,
nextPeriodStart = nextPeriodStart,
nextOvulation = nextOvulation,
fertileWindowStart = fertileWindowStart,
fertileWindowEnd = fertileWindowEnd
)
}
fun isPeriodDay(date: LocalDate): Boolean {
return getPeriods().any { period ->
!date.isBefore(period.startDate) && !date.isAfter(period.endDate)
}
}
fun isFertileDay(date: LocalDate): Boolean {
val stats = calculateCycleStats()
return stats.fertileWindowStart?.let { start ->
stats.fertileWindowEnd?.let { end ->
!date.isBefore(start) && !date.isAfter(end)
}
} ?: false
}
// Water intake
fun getWaterIntakes(): List<WaterIntake> = dataStore.getWaterIntakes()
fun getWaterIntakeForDate(date: LocalDate): WaterIntake? {
return getWaterIntakes().find { it.date == date }
}
fun addWater(date: LocalDate, amountMl: Int) = dataStore.addWater(date, amountMl)
fun getWaterGoal(): Int = dataStore.getWaterGoal()
fun setWaterGoal(goalMl: Int) = dataStore.setWaterGoal(goalMl)
// Calories
fun getCalorieEntries(): List<CalorieEntry> = dataStore.getCalorieEntries()
fun getCalorieEntriesForDate(date: LocalDate): List<CalorieEntry> {
return getCalorieEntries().filter { it.date == date }
}
fun addCalorieEntry(entry: CalorieEntry) = dataStore.addCalorieEntry(entry)
fun deleteCalorieEntry(entryId: Long) = dataStore.deleteCalorieEntry(entryId)
fun getCalorieGoal(): CalorieGoal = dataStore.getCalorieGoal()
fun setCalorieGoal(goal: CalorieGoal) = dataStore.setCalorieGoal(goal)
fun getTotalCaloriesForDate(date: LocalDate): Int {
return getCalorieEntriesForDate(date).sumOf { it.calories }
}
// Weight
fun getWeightEntries(): List<WeightEntry> = dataStore.getWeightEntries()
fun addWeightEntry(entry: WeightEntry) = dataStore.addWeightEntry(entry)
fun deleteWeightEntry(entryId: Long) = dataStore.deleteWeightEntry(entryId)
fun getLatestWeight(): WeightEntry? {
return getWeightEntries().maxByOrNull { it.date }
}
// Sleep
fun getSleepEntries(): List<SleepEntry> = dataStore.getSleepEntries()
fun getSleepEntryForDate(date: LocalDate): SleepEntry? {
return getSleepEntries().find { it.date == date }
}
fun addSleepEntry(entry: SleepEntry) = dataStore.addSleepEntry(entry)
fun deleteSleepEntry(entryId: Long) = dataStore.deleteSleepEntry(entryId)
fun getSleepGoal(): Float = dataStore.getSleepGoal()
fun setSleepGoal(hours: Float) = dataStore.setSleepGoal(hours)
fun getAverageSleepHours(days: Int = 7): Float {
val entries = getSleepEntries()
.sortedByDescending { it.date }
.take(days)
return if (entries.isEmpty()) 0f
else entries.map { it.durationHours }.average().toFloat()
}
// Additional helper methods for ViewModel
fun getCycleInfo(): CycleInfo {
val periods = getPeriods().sortedBy { it.startDate }
val lastPeriod = periods.lastOrNull()
val stats = calculateCycleStats()
return CycleInfo(
averageCycleLength = stats.averageCycleLength,
averagePeriodLength = stats.averagePeriodLength,
lastPeriodStart = lastPeriod?.startDate,
nextPeriodPrediction = stats.nextPeriodStart,
ovulationPrediction = stats.nextOvulation,
currentCycleDay = lastPeriod?.let {
java.time.temporal.ChronoUnit.DAYS.between(it.startDate, LocalDate.now()).toInt() + 1
} ?: 1
)
}
// Journal
fun getJournalEntries(): List<HealthJournalEntry> = dataStore.getJournalEntries()
fun getJournalEntriesForDate(date: LocalDate): List<HealthJournalEntry> {
return getJournalEntries().filter { it.date == date }
}
fun addJournalEntry(entry: HealthJournalEntry) = dataStore.addJournalEntry(entry)
fun deleteJournalEntry(entryId: Long) = dataStore.deleteJournalEntry(entryId)
}

View File

@@ -0,0 +1,32 @@
package com.example.elva.data.repository
import com.example.elva.data.api.ProfileApiService
import com.example.elva.data.models.profile.ProfileUpdateRequest
import com.example.elva.data.models.profile.UserProfile
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ProfileRepository @Inject constructor(
private val profileApi: ProfileApiService
) {
suspend fun getProfile(): Result<UserProfile> {
return try {
val profile = profileApi.getProfile()
Result.success(profile)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun updateProfile(request: ProfileUpdateRequest): Result<UserProfile> {
return try {
val updatedProfile = profileApi.updateProfile(request)
Result.success(updatedProfile)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,40 @@
package com.example.elva.data.repository
import com.example.elva.data.api.UserApiService
import com.example.elva.data.models.user.UserProfile
import com.example.elva.util.AuthManager
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class UserRepository @Inject constructor(
private val userApiService: UserApiService,
private val authManager: AuthManager
) {
suspend fun getCurrentUser(): Result<UserProfile> {
return try {
val token = authManager.getAccessToken()
if (token.isNullOrEmpty()) {
android.util.Log.w("UserRepository", "No auth token found")
return Result.failure(Exception("No auth token found"))
}
android.util.Log.d("UserRepository", "Loading user profile with token: ${token.take(20)}...")
val profile = userApiService.getCurrentUser("Bearer $token")
android.util.Log.d("UserRepository", "Profile loaded: id=${profile.id}, username=${profile.username}")
// Сохраняем полученные данные пользователя
authManager.saveUserId(profile.id)
profile.uuid?.let { authManager.saveUserUuid(it) }
android.util.Log.d("UserRepository", "User data saved to AuthManager")
Result.success(profile)
} catch (e: Exception) {
android.util.Log.e("UserRepository", "Error loading user profile", e)
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,240 @@
package com.example.elva.data.websocket
import com.example.elva.data.remote.model.emergency.AlertResponseWS
import com.example.elva.data.remote.model.emergency.AlertUpdateWS
import com.example.elva.data.remote.model.emergency.EmergencyAlertWS
import com.example.elva.data.remote.model.emergency.WebSocketMessage
import com.example.elva.util.AuthManager
import kotlinx.coroutines.*
import kotlinx.serialization.json.Json
import okhttp3.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EmergencyWebSocketManager @Inject constructor(
private val authManager: AuthManager
) {
private var webSocket: WebSocket? = null
private var isConnected = false
private var reconnectAttempts = 0
private val maxReconnectAttempts = 5
private val listeners = mutableListOf<WebSocketEventListener>()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Замените на ваш IP адрес
private companion object {
private const val WS_HOST = "192.168.0.112"
private const val WS_PORT = 8002
}
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
}
private val client = OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.pingInterval(25, TimeUnit.SECONDS)
.build()
// ==================== ПОДКЛЮЧЕНИЕ ====================
fun connect() {
val token = authManager.getAccessToken()
val userId = authManager.getUserId()
if (token == null || userId == -1) {
notifyError("Authentication required")
return
}
val wsUrl = "ws://$WS_HOST:$WS_PORT/api/v1/emergency/ws/$userId?token=$token"
val request = Request.Builder()
.url(wsUrl)
.build()
webSocket = client.newWebSocket(request, createWebSocketListener())
}
fun disconnect() {
webSocket?.close(1000, "User disconnected")
webSocket = null
isConnected = false
stopPingJob()
}
// ==================== СЛУШАТЕЛЬ WEBSOCKET ====================
private fun createWebSocketListener(): WebSocketListener {
return object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
isConnected = true
reconnectAttempts = 0
startPingJob()
notifyConnectionOpened()
}
override fun onMessage(webSocket: WebSocket, text: String) {
handleIncomingMessage(text)
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
isConnected = false
stopPingJob()
notifyConnectionClosing(code, reason)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
isConnected = false
stopPingJob()
notifyConnectionClosed(code, reason)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
isConnected = false
stopPingJob()
notifyError("Connection failed: ${t.message}")
scheduleReconnect()
}
}
}
// ==================== ОБРАБОТКА СООБЩЕНИЙ ====================
private fun handleIncomingMessage(text: String) {
try {
val message = json.decodeFromString<WebSocketMessage>(text)
when (message.type) {
"connection_established" -> {
notifyConnectionEstablished(message.userId ?: -1)
}
"emergency_alert" -> {
val alert = json.decodeFromString<EmergencyAlertWS>(text)
notifyNewEmergencyAlert(alert)
}
"alert_update" -> {
val update = json.decodeFromString<AlertUpdateWS>(text)
notifyAlertUpdate(update)
}
"alert_response" -> {
val response = json.decodeFromString<AlertResponseWS>(text)
notifyAlertResponse(response)
}
"pong" -> {
// Pong получен, соединение активно
}
}
} catch (e: Exception) {
notifyError("Failed to parse message: ${e.message}")
}
}
// ==================== PING/PONG ====================
private var pingJob: Job? = null
private fun startPingJob() {
pingJob = scope.launch {
while (isConnected) {
delay(30_000) // Ping каждые 30 секунд
sendPing()
}
}
}
private fun stopPingJob() {
pingJob?.cancel()
pingJob = null
}
private fun sendPing() {
if (isConnected) {
webSocket?.send("""{"type": "ping"}""")
}
}
// ==================== ПЕРЕПОДКЛЮЧЕНИЕ ====================
private fun scheduleReconnect() {
if (reconnectAttempts >= maxReconnectAttempts) {
notifyError("Max reconnection attempts reached")
return
}
reconnectAttempts++
val delay = (reconnectAttempts * 2000).toLong() // Экспоненциальная задержка
scope.launch {
delay(delay)
if (!isConnected) {
connect()
}
}
}
// ==================== ОТПРАВКА СООБЩЕНИЙ ====================
fun sendAlertReceived(alertId: Int) {
val message = """{"type": "alert_received", "alert_id": $alertId}"""
webSocket?.send(message)
}
// ==================== СЛУШАТЕЛИ ====================
fun addListener(listener: WebSocketEventListener) {
listeners.add(listener)
}
fun removeListener(listener: WebSocketEventListener) {
listeners.remove(listener)
}
private fun notifyConnectionOpened() {
listeners.forEach { it.onConnectionOpened() }
}
private fun notifyConnectionEstablished(userId: Int) {
listeners.forEach { it.onConnectionEstablished(userId) }
}
private fun notifyNewEmergencyAlert(alert: EmergencyAlertWS) {
listeners.forEach { it.onNewEmergencyAlert(alert) }
}
private fun notifyAlertUpdate(update: AlertUpdateWS) {
listeners.forEach { it.onAlertUpdate(update) }
}
private fun notifyAlertResponse(response: AlertResponseWS) {
listeners.forEach { it.onAlertResponse(response) }
}
private fun notifyConnectionClosing(code: Int, reason: String) {
listeners.forEach { it.onConnectionClosing(code, reason) }
}
private fun notifyConnectionClosed(code: Int, reason: String) {
listeners.forEach { it.onConnectionClosed(code, reason) }
}
private fun notifyError(error: String) {
listeners.forEach { it.onError(error) }
}
// ==================== СОСТОЯНИЕ ====================
fun isConnected(): Boolean = isConnected
fun cleanup() {
disconnect()
scope.cancel()
}
}

View File

@@ -0,0 +1,17 @@
package com.example.elva.data.websocket
import com.example.elva.data.remote.model.emergency.AlertResponseWS
import com.example.elva.data.remote.model.emergency.AlertUpdateWS
import com.example.elva.data.remote.model.emergency.EmergencyAlertWS
interface WebSocketEventListener {
fun onConnectionOpened()
fun onConnectionEstablished(userId: Int)
fun onNewEmergencyAlert(alert: EmergencyAlertWS)
fun onAlertUpdate(update: AlertUpdateWS)
fun onAlertResponse(response: AlertResponseWS)
fun onConnectionClosing(code: Int, reason: String)
fun onConnectionClosed(code: Int, reason: String)
fun onError(error: String)
}

View File

@@ -0,0 +1,21 @@
package com.example.elva.di
import android.content.Context
import com.example.elva.data.local.SessionManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideSessionManager(@ApplicationContext context: Context): SessionManager =
SessionManager(context)
}

View File

@@ -0,0 +1,85 @@
package com.example.elva.di
import com.example.elva.data.api.ApiConfig
import com.example.elva.data.api.AuthApiService
import com.example.elva.data.api.ProfileApiService
import com.example.elva.data.api.UserApiService
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideJson(): Json = Json {
ignoreUnknownKeys = true
isLenient = true
coerceInputValues = true
}
@Provides
@Singleton
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
return HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
}
@Provides
@Singleton
fun provideOkHttpClient(
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.connectTimeout(ApiConfig.CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(ApiConfig.READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(ApiConfig.WRITE_TIMEOUT, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(
okHttpClient: OkHttpClient,
json: Json
): Retrofit {
val contentType = "application/json".toMediaType()
return Retrofit.Builder()
.baseUrl(ApiConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory(contentType))
.build()
}
@Provides
@Singleton
fun provideAuthApiService(retrofit: Retrofit): AuthApiService {
return retrofit.create(AuthApiService::class.java)
}
@Provides
@Singleton
fun provideProfileApiService(retrofit: Retrofit): ProfileApiService {
return retrofit.create(ProfileApiService::class.java)
}
@Provides
@Singleton
fun provideUserApiService(retrofit: Retrofit): UserApiService {
return retrofit.create(UserApiService::class.java)
}
}

View File

@@ -0,0 +1,40 @@
package com.example.elva.ui.alerts
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.elva.databinding.FragmentAlertsBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AlertsFragment : Fragment() {
private var _binding: FragmentAlertsBinding? = null
private val binding get() = _binding!!
private val viewModel: AlertsViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAlertsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.alertsList.layoutManager = LinearLayoutManager(requireContext())
binding.alertsRefresh.setOnClickListener { viewModel.loadAlerts() }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,30 @@
package com.example.elva.ui.alerts
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@HiltViewModel
class AlertsViewModel @Inject constructor() : ViewModel() {
private val _state = MutableStateFlow(AlertsState())
val state: StateFlow<AlertsState> = _state
fun loadAlerts() {
viewModelScope.launch {
_state.value = AlertsState(isLoading = true)
_state.value = AlertsState(alerts = listOf("Тестовый алерт"))
}
}
}
data class AlertsState(
val isLoading: Boolean = false,
val error: String? = null,
val alerts: List<String> = emptyList()
)

View File

@@ -0,0 +1,174 @@
package com.example.elva.ui.auth
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import com.example.elva.R
import com.example.elva.databinding.FragmentLoginBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class LoginFragment : Fragment() {
private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!!
private val viewModel: LoginViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupCardHeight()
setupAnimation()
setupListeners()
observeUiState()
android.util.Log.d("LoginFragment", "LoginFragment loaded, showing login screen")
}
private fun setupCardHeight() {
// Устанавливаем высоту карточки в 60% экрана
view?.post {
val screenHeight = resources.displayMetrics.heightPixels
val cardHeight = (screenHeight * 0.6f).toInt()
val params = binding.loginCard.layoutParams as ViewGroup.MarginLayoutParams
params.height = cardHeight
binding.loginCard.layoutParams = params
}
}
private fun setupAnimation() {
// Анимация появления не нужна, т.к. Navigation Component уже делает это
}
private fun setupListeners() {
binding.apply {
loginButton.setOnClickListener {
val emailOrUsername = usernameInput.text.toString().trim()
val password = passwordInput.text.toString()
viewModel.login(emailOrUsername, password)
}
registerLink.setOnClickListener {
// Навигация к экрану регистрации
findNavController().navigate(R.id.action_loginFragment_to_registerFragment)
}
}
}
private fun observeUiState() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
android.util.Log.d("LoginFragment", "UI State changed: $state")
when (state) {
is LoginUiState.Idle -> {
binding.progressBar.isVisible = false
setInputsEnabled(true)
}
is LoginUiState.Loading -> {
binding.progressBar.isVisible = true
setInputsEnabled(false)
}
is LoginUiState.Success -> {
android.util.Log.d("LoginFragment", "=== LOGIN SUCCESS STATE RECEIVED ===")
binding.progressBar.isVisible = false
setInputsEnabled(false)
// Проверяем статус авторизации
val isLoggedIn = viewModel.checkIfLoggedIn()
android.util.Log.d("LoginFragment", "After login, isLoggedIn = $isLoggedIn")
Toast.makeText(
requireContext(),
"Вход выполнен успешно!",
Toast.LENGTH_SHORT
).show()
// Навигация с небольшой задержкой для отображения Toast
android.util.Log.d("LoginFragment", "Scheduling navigation to dashboard")
view?.postDelayed({
if (isAdded && view != null) {
android.util.Log.d("LoginFragment", "Executing delayed navigation")
navigateToDashboard()
}
}, 300)
}
is LoginUiState.Error -> {
android.util.Log.e("LoginFragment", "Login error: ${state.message}")
binding.progressBar.isVisible = false
setInputsEnabled(true)
Toast.makeText(
requireContext(),
state.message,
Toast.LENGTH_LONG
).show()
viewModel.resetState()
}
}
}
}
}
}
private fun setInputsEnabled(enabled: Boolean) {
binding.apply {
usernameInput.isEnabled = enabled
passwordInput.isEnabled = enabled
loginButton.isEnabled = enabled
registerLink.isEnabled = enabled
}
}
private fun navigateToDashboard() {
android.util.Log.d("LoginFragment", "=== STARTING NAVIGATION TO DASHBOARD ===")
try {
if (!isAdded) {
android.util.Log.e("LoginFragment", "Fragment not added, cannot navigate")
return
}
// Проверяем что NavController доступен
val navController = findNavController()
android.util.Log.d("LoginFragment", "NavController obtained: ${navController.currentDestination?.label}")
// Выполняем навигацию
navController.navigate(R.id.action_login_to_dashboard)
android.util.Log.d("LoginFragment", "=== NAVIGATION TO DASHBOARD COMPLETED ===")
} catch (e: IllegalStateException) {
android.util.Log.e("LoginFragment", "Navigation failed: IllegalStateException", e)
Toast.makeText(requireContext(), "Ошибка перехода на главный экран", Toast.LENGTH_LONG).show()
} catch (e: Exception) {
android.util.Log.e("LoginFragment", "Navigation failed: ${e.javaClass.simpleName}", e)
Toast.makeText(requireContext(), "Ошибка навигации: ${e.message}", Toast.LENGTH_LONG).show()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,96 @@
package com.example.elva.ui.auth
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.elva.data.repository.AuthRepository
import com.example.elva.data.repository.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<LoginUiState>(LoginUiState.Idle)
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
fun login(emailOrUsername: String, password: String) {
android.util.Log.d("LoginViewModel", "login() called with: emailOrUsername=$emailOrUsername")
// Валидация
when {
emailOrUsername.isBlank() -> {
_uiState.value = LoginUiState.Error("Введите email или имя пользователя")
return
}
password.isBlank() -> {
_uiState.value = LoginUiState.Error("Введите пароль")
return
}
}
_uiState.value = LoginUiState.Loading
android.util.Log.d("LoginViewModel", "Starting login process")
viewModelScope.launch {
// Определяем, это email или username
val isEmail = emailOrUsername.contains("@")
android.util.Log.d("LoginViewModel", "isEmail: $isEmail")
val result = if (isEmail) {
authRepository.login(
email = emailOrUsername,
password = password
)
} else {
authRepository.login(
username = emailOrUsername,
password = password
)
}
android.util.Log.d("LoginViewModel", "Login result: success=${result.isSuccess}")
if (result.isSuccess) {
android.util.Log.d("LoginViewModel", "Login successful!")
// Сразу переходим на dashboard, профиль загрузится там
_uiState.value = LoginUiState.Success
} else {
val error = when {
result.exceptionOrNull()?.message?.contains("401") == true ||
result.exceptionOrNull()?.message?.contains("Unauthorized") == true ->
"Неверный email/пароль"
result.exceptionOrNull()?.message?.contains("Unable to resolve host") == true ->
"Не удается подключиться к серверу. Проверьте настройки сети."
else -> result.exceptionOrNull()?.message ?: "Ошибка входа"
}
android.util.Log.e("LoginViewModel", "Login failed: $error", result.exceptionOrNull())
_uiState.value = LoginUiState.Error(error)
}
}
}
fun checkIfLoggedIn(): Boolean {
val isLoggedIn = authRepository.isLoggedIn()
android.util.Log.d("LoginViewModel", "checkIfLoggedIn: $isLoggedIn")
return isLoggedIn
}
fun resetState() {
_uiState.value = LoginUiState.Idle
}
}
sealed class LoginUiState {
object Idle : LoginUiState()
object Loading : LoginUiState()
object Success : LoginUiState()
data class Error(val message: String) : LoginUiState()
}

View File

@@ -0,0 +1,121 @@
package com.example.elva.ui.auth
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import com.example.elva.R
import com.example.elva.databinding.DialogRegisterBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class RegisterDialogFragment : Fragment() {
private var _binding: DialogRegisterBinding? = null
private val binding get() = _binding!!
private val viewModel: RegisterViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogRegisterBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupListeners()
observeUiState()
}
private fun setupListeners() {
binding.apply {
registerButton.setOnClickListener {
val username = usernameInput.text.toString().trim()
val email = emailInput.text.toString().trim()
val password = passwordInput.text.toString()
viewModel.register(
username = username,
email = email,
password = password,
confirmPassword = password, // Используем тот же пароль, валидацию делаем на клиенте
fullName = null,
phone = null
)
}
loginLink.setOnClickListener {
// Вернуться на экран входа
findNavController().navigateUp()
}
}
}
private fun observeUiState() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is RegisterUiState.Idle -> {
binding.progressBar.isVisible = false
setInputsEnabled(true)
}
is RegisterUiState.Loading -> {
binding.progressBar.isVisible = true
setInputsEnabled(false)
}
is RegisterUiState.Success -> {
binding.progressBar.isVisible = false
Toast.makeText(
requireContext(),
"Регистрация успешна!",
Toast.LENGTH_SHORT
).show()
// Переход на главный экран
findNavController().navigate(R.id.action_loginFragment_to_dashboardFragment)
}
is RegisterUiState.Error -> {
binding.progressBar.isVisible = false
setInputsEnabled(true)
Toast.makeText(
requireContext(),
state.message,
Toast.LENGTH_LONG
).show()
viewModel.resetState()
}
}
}
}
}
}
private fun setInputsEnabled(enabled: Boolean) {
binding.apply {
usernameInput.isEnabled = enabled
emailInput.isEnabled = enabled
passwordInput.isEnabled = enabled
registerButton.isEnabled = enabled
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,94 @@
package com.example.elva.ui.auth
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.elva.data.repository.AuthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
sealed class RegisterUiState {
object Idle : RegisterUiState()
object Loading : RegisterUiState()
object Success : RegisterUiState()
data class Error(val message: String) : RegisterUiState()
}
@HiltViewModel
class RegisterViewModel @Inject constructor(
private val authRepository: AuthRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<RegisterUiState>(RegisterUiState.Idle)
val uiState: StateFlow<RegisterUiState> = _uiState.asStateFlow()
fun register(
username: String,
email: String,
password: String,
confirmPassword: String,
fullName: String? = null,
phone: String? = null
) {
// Валидация
when {
username.isBlank() -> {
_uiState.value = RegisterUiState.Error("Введите имя пользователя")
return
}
email.isBlank() -> {
_uiState.value = RegisterUiState.Error("Введите email")
return
}
!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() -> {
_uiState.value = RegisterUiState.Error("Неверный формат email")
return
}
password.length < 8 -> {
_uiState.value = RegisterUiState.Error("Пароль должен содержать минимум 8 символов")
return
}
password != confirmPassword -> {
_uiState.value = RegisterUiState.Error("Пароли не совпадают")
return
}
}
viewModelScope.launch {
_uiState.value = RegisterUiState.Loading
try {
val result = authRepository.register(
username = username,
email = email,
password = password,
fullName = fullName,
phone = phone
)
result.fold(
onSuccess = { response ->
// Данные уже сохранены в AuthRepository
_uiState.value = RegisterUiState.Success
},
onFailure = { error ->
_uiState.value = RegisterUiState.Error(
error.message ?: "Ошибка регистрации"
)
}
)
} catch (e: Exception) {
_uiState.value = RegisterUiState.Error(
e.message ?: "Ошибка регистрации"
)
}
}
}
fun resetState() {
_uiState.value = RegisterUiState.Idle
}
}

View File

@@ -0,0 +1,102 @@
package com.example.elva.ui.calendar
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.elva.R
import com.example.elva.databinding.ItemCalendarDayBinding
import java.time.LocalDate
class CalendarAdapter(
private val onDateClick: (LocalDate) -> Unit
) : ListAdapter<CalendarDay, CalendarAdapter.DayViewHolder>(DayDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DayViewHolder {
val binding = ItemCalendarDayBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return DayViewHolder(binding)
}
override fun onBindViewHolder(holder: DayViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class DayViewHolder(
private val binding: ItemCalendarDayBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(day: CalendarDay) {
when (day.type) {
CalendarDayType.EMPTY -> {
binding.tvDay.text = ""
binding.dayContainer.background = null
binding.dayContainer.setOnClickListener(null)
}
else -> {
binding.tvDay.text = day.date?.dayOfMonth?.toString() ?: ""
// Устанавливаем фон в зависимости от типа дня
val backgroundRes = when (day.type) {
CalendarDayType.PERIOD -> R.drawable.bg_calendar_period
CalendarDayType.PREDICTED -> R.drawable.bg_calendar_predicted
CalendarDayType.FERTILE -> R.drawable.bg_calendar_fertile
CalendarDayType.SELECTED -> R.drawable.bg_calendar_selected
else -> null
}
if (backgroundRes != null) {
binding.dayContainer.background = ContextCompat.getDrawable(
binding.root.context,
backgroundRes
)
binding.tvDay.setTextColor(
ContextCompat.getColor(
binding.root.context,
if (day.type == CalendarDayType.PERIOD)
android.R.color.white
else
R.color.text_primary
)
)
} else {
binding.dayContainer.background = null
binding.tvDay.setTextColor(
ContextCompat.getColor(
binding.root.context,
R.color.text_primary
)
)
}
// Затемняем дни не из текущего месяца
if (day.date?.month != LocalDate.now().month) {
binding.tvDay.alpha = 0.3f
} else {
binding.tvDay.alpha = 1f
}
binding.dayContainer.setOnClickListener {
day.date?.let { onDateClick(it) }
}
}
}
}
}
private class DayDiffCallback : DiffUtil.ItemCallback<CalendarDay>() {
override fun areItemsTheSame(oldItem: CalendarDay, newItem: CalendarDay): Boolean {
return oldItem.date == newItem.date
}
override fun areContentsTheSame(oldItem: CalendarDay, newItem: CalendarDay): Boolean {
return oldItem == newItem
}
}
}

View File

@@ -0,0 +1,39 @@
package com.example.elva.ui.calendar
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.elva.databinding.FragmentCalendarBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class CalendarFragment : Fragment() {
private var _binding: FragmentCalendarBinding? = null
private val binding get() = _binding!!
private val viewModel: CalendarViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCalendarBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// TODO: Implement calendar entry dialog
// binding.addCalendarEntry.setOnClickListener { /* Show dialog */ }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,122 @@
package com.example.elva.ui.calendar
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.elva.data.models.health.Period
import com.example.elva.data.repository.HealthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.YearMonth
import javax.inject.Inject
@HiltViewModel
class CalendarViewModel @Inject constructor(
private val healthRepository: HealthRepository
) : ViewModel() {
private val _state = MutableStateFlow(CalendarState())
val state: StateFlow<CalendarState> = _state.asStateFlow()
init {
loadCalendarData()
}
private fun loadCalendarData() {
viewModelScope.launch {
try {
val cycleInfo = healthRepository.getCycleInfo()
val periods = healthRepository.getPeriods()
_state.value = CalendarState(
currentMonth = YearMonth.now(),
periods = periods,
cycleInfo = cycleInfo,
isLoading = false
)
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message,
isLoading = false
)
}
}
}
fun previousMonth() {
_state.value = _state.value.copy(
currentMonth = _state.value.currentMonth.minusMonths(1)
)
}
fun nextMonth() {
_state.value = _state.value.copy(
currentMonth = _state.value.currentMonth.plusMonths(1)
)
}
fun selectDate(date: LocalDate) {
_state.value = _state.value.copy(selectedDate = date)
}
fun showEditPeriodDialog(startDate: LocalDate? = null) {
_state.value = _state.value.copy(
showEditDialog = true,
editingPeriod = startDate?.let { date ->
// Попытка найти существующий период
_state.value.periods.find {
!date.isBefore(it.startDate) && (it.endDate == null || !date.isAfter(it.endDate))
}
}
)
}
fun dismissEditDialog() {
_state.value = _state.value.copy(
showEditDialog = false,
editingPeriod = null
)
}
fun savePeriod(period: Period) {
viewModelScope.launch {
try {
if (period.id == 0L) {
healthRepository.addPeriod(period)
} else {
healthRepository.updatePeriod(period)
}
dismissEditDialog()
loadCalendarData()
} catch (e: Exception) {
_state.value = _state.value.copy(error = e.message)
}
}
}
fun deletePeriod(periodId: Long) {
viewModelScope.launch {
try {
healthRepository.deletePeriod(periodId)
loadCalendarData()
} catch (e: Exception) {
_state.value = _state.value.copy(error = e.message)
}
}
}
}
data class CalendarState(
val currentMonth: YearMonth = YearMonth.now(),
val selectedDate: LocalDate? = null,
val periods: List<Period> = emptyList(),
val cycleInfo: com.example.elva.data.models.health.CycleInfo? = null,
val showEditDialog: Boolean = false,
val editingPeriod: Period? = null,
val isLoading: Boolean = true,
val error: String? = null
)

View File

@@ -0,0 +1,133 @@
package com.example.elva.ui.calendar
import android.app.DatePickerDialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.example.elva.R
import com.example.elva.data.models.health.FlowIntensity
import com.example.elva.data.models.health.Period
import com.example.elva.databinding.DialogEditPeriodBinding
import java.time.LocalDate
import java.time.format.DateTimeFormatter
class EditPeriodDialogFragment : DialogFragment() {
private var _binding: DialogEditPeriodBinding? = null
private val binding get() = _binding!!
private var startDate: LocalDate = LocalDate.now()
private var endDate: LocalDate? = null
private var selectedFlow: FlowIntensity = FlowIntensity.MEDIUM
private var onSaveListener: ((Period) -> Unit)? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogEditPeriodBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupListeners()
updateUI()
}
private fun setupListeners() {
binding.apply {
// Выбор даты начала
cardStartDate.setOnClickListener {
showDatePicker(startDate) { date ->
startDate = date
updateUI()
}
}
// Выбор даты окончания
cardEndDate.setOnClickListener {
showDatePicker(endDate ?: startDate.plusDays(5)) { date ->
endDate = date
updateUI()
}
}
// Интенсивность
radioGroupFlow.setOnCheckedChangeListener { _, checkedId ->
selectedFlow = when (checkedId) {
R.id.rbLight -> FlowIntensity.LIGHT
R.id.rbMedium -> FlowIntensity.MEDIUM
R.id.rbHeavy -> FlowIntensity.HEAVY
else -> FlowIntensity.MEDIUM
}
}
// Кнопки
btnCancel.setOnClickListener {
dismiss()
}
btnSave.setOnClickListener {
savePeriod()
}
}
}
private fun showDatePicker(currentDate: LocalDate, onDateSelected: (LocalDate) -> Unit) {
val picker = DatePickerDialog(
requireContext(),
{ _, year, month, dayOfMonth ->
onDateSelected(LocalDate.of(year, month + 1, dayOfMonth))
},
currentDate.year,
currentDate.monthValue - 1,
currentDate.dayOfMonth
)
picker.show()
}
private fun updateUI() {
val formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy")
binding.apply {
tvStartDate.text = startDate.format(formatter)
endDate?.let {
tvEndDate.text = it.format(formatter)
} ?: run {
tvEndDate.text = getString(R.string.select_date)
}
}
}
private fun savePeriod() {
val period = Period(
startDate = startDate,
endDate = endDate,
flow = selectedFlow,
notes = binding.etNotes.text?.toString()
)
onSaveListener?.invoke(period)
dismiss()
}
fun setOnSaveListener(listener: (Period) -> Unit) {
onSaveListener = listener
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
fun newInstance() = EditPeriodDialogFragment()
}
}

View File

@@ -0,0 +1,212 @@
package com.example.elva.ui.calendar
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.example.elva.R
import com.example.elva.databinding.FragmentPeriodCalendarBinding
import com.example.elva.data.models.health.Period
import com.google.android.material.datepicker.MaterialDatePicker
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.TextStyle
import java.util.*
@AndroidEntryPoint
class PeriodCalendarFragment : Fragment() {
private var _binding: FragmentPeriodCalendarBinding? = null
private val binding get() = _binding!!
private val viewModel: PeriodCalendarViewModel by viewModels()
private lateinit var calendarAdapter: CalendarAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentPeriodCalendarBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupCalendar()
setupListeners()
observeState()
}
private fun setupCalendar() {
calendarAdapter = CalendarAdapter { date ->
viewModel.onDateSelected(date)
showEditPeriodDialog(date)
}
binding.rvCalendar.apply {
layoutManager = GridLayoutManager(requireContext(), 7)
adapter = calendarAdapter
}
}
private fun setupListeners() {
binding.btnPrevMonth.setOnClickListener {
viewModel.previousMonth()
}
binding.btnNextMonth.setOnClickListener {
viewModel.nextMonth()
}
binding.fabAddPeriod.setOnClickListener {
showAddPeriodDialog()
}
}
private fun observeState() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.state.collect { state ->
updateUI(state)
}
}
}
private fun updateUI(state: PeriodCalendarState) {
// Update month/year header
val formatter = DateTimeFormatter.ofPattern("LLLL yyyy", Locale("ru"))
binding.tvMonthYear.text = state.currentMonth.format(formatter)
.replaceFirstChar { it.uppercase() }
// Update calendar
val days = generateCalendarDays(state)
calendarAdapter.submitList(days)
// Update cycle info
state.cycleInfo.lastPeriodStart?.let { lastStart ->
val daysSince = java.time.temporal.ChronoUnit.DAYS.between(lastStart, LocalDate.now())
binding.tvCycleDay.text = getString(R.string.cycle_day_format, daysSince.toInt() + 1)
} ?: run {
binding.tvCycleDay.text = getString(R.string.no_cycle_data)
}
state.cycleInfo.nextPeriodPrediction?.let { nextPeriod ->
val daysUntil = java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(), nextPeriod)
binding.tvNextPeriod.text = getString(R.string.next_period_format, daysUntil.toInt())
} ?: run {
binding.tvNextPeriod.text = getString(R.string.no_prediction)
}
}
private fun generateCalendarDays(state: PeriodCalendarState): List<CalendarDay> {
val days = mutableListOf<CalendarDay>()
val yearMonth = state.currentMonth
val firstDay = yearMonth.atDay(1)
val lastDay = yearMonth.atEndOfMonth()
// Добавляем пустые ячейки для выравнивания начала месяца
val firstDayOfWeek = firstDay.dayOfWeek.value - 1 // 0 = Monday
repeat(firstDayOfWeek) {
days.add(CalendarDay(null, CalendarDayType.EMPTY))
}
// Добавляем дни месяца
var currentDate = firstDay
while (currentDate <= lastDay) {
val dayType = getDayType(currentDate, state)
days.add(CalendarDay(currentDate, dayType))
currentDate = currentDate.plusDays(1)
}
return days
}
private fun getDayType(date: LocalDate, state: PeriodCalendarState): CalendarDayType {
// Проверяем, является ли день периодом
val isPeriod = state.periods.any { period ->
!date.isBefore(period.startDate) && !date.isAfter(period.endDate)
}
if (isPeriod) return CalendarDayType.PERIOD
// Проверяем, является ли день прогнозируемым периодом
state.cycleInfo.nextPeriodPrediction?.let { nextPeriod ->
if (date >= nextPeriod && date < nextPeriod.plusDays(5)) {
return CalendarDayType.PREDICTED
}
}
// Проверяем фертильное окно
state.cycleInfo.ovulationPrediction?.let { ovulation ->
if (date >= ovulation.minusDays(5) && date <= ovulation.plusDays(1)) {
return CalendarDayType.FERTILE
}
}
return CalendarDayType.NORMAL
}
private fun showAddPeriodDialog() {
val datePicker = MaterialDatePicker.Builder.dateRangePicker()
.setTitleText(R.string.select_period_dates)
.build()
datePicker.addOnPositiveButtonClickListener { selection ->
val startDate = Instant.ofEpochMilli(selection.first)
.atZone(ZoneId.systemDefault())
.toLocalDate()
val endDate = Instant.ofEpochMilli(selection.second)
.atZone(ZoneId.systemDefault())
.toLocalDate()
val period = Period(
id = 0,
startDate = startDate,
endDate = endDate,
flow = FlowIntensity.MEDIUM,
notes = ""
)
viewModel.savePeriod(period)
}
datePicker.show(parentFragmentManager, "date_picker")
}
private fun showEditPeriodDialog(date: LocalDate) {
val dialog = EditPeriodDialog.newInstance(date)
dialog.setOnSaveListener { period ->
viewModel.savePeriod(period)
}
dialog.setOnDeleteListener { periodId ->
viewModel.deletePeriod(periodId)
}
dialog.show(parentFragmentManager, "edit_period")
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
data class CalendarDay(
val date: LocalDate?,
val type: CalendarDayType
)
enum class CalendarDayType {
EMPTY,
NORMAL,
PERIOD,
PREDICTED,
FERTILE,
SELECTED
}

View File

@@ -0,0 +1,106 @@
package com.example.elva.ui.calendar
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.elva.data.models.health.CycleInfo
import com.example.elva.data.models.health.Period
import com.example.elva.data.repository.HealthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.YearMonth
import javax.inject.Inject
@HiltViewModel
class PeriodCalendarViewModel @Inject constructor(
private val healthRepository: HealthRepository
) : ViewModel() {
private val _state = MutableStateFlow(PeriodCalendarState())
val state: StateFlow<PeriodCalendarState> = _state
init {
loadData()
}
fun loadData() {
viewModelScope.launch {
try {
val periods = healthRepository.getPeriods()
val cycleInfo = healthRepository.getCycleInfo()
_state.value = _state.value.copy(
periods = periods,
cycleInfo = cycleInfo,
isLoading = false
)
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message,
isLoading = false
)
}
}
}
fun previousMonth() {
_state.value = _state.value.copy(
currentMonth = _state.value.currentMonth.minusMonths(1)
)
}
fun nextMonth() {
_state.value = _state.value.copy(
currentMonth = _state.value.currentMonth.plusMonths(1)
)
}
fun onDateSelected(date: LocalDate) {
_state.value = _state.value.copy(selectedDate = date)
}
fun clearSelection() {
_state.value = _state.value.copy(selectedDate = null)
}
fun savePeriod(period: Period) {
viewModelScope.launch {
try {
if (period.id == 0L) {
healthRepository.addPeriod(period)
} else {
healthRepository.updatePeriod(period)
}
loadData()
} catch (e: Exception) {
_state.value = _state.value.copy(error = e.message)
}
}
}
fun deletePeriod(periodId: Long) {
viewModelScope.launch {
try {
healthRepository.deletePeriod(periodId)
loadData()
} catch (e: Exception) {
_state.value = _state.value.copy(error = e.message)
}
}
}
}
data class PeriodCalendarState(
val currentMonth: YearMonth = YearMonth.now(),
val periods: List<Period> = emptyList(),
val cycleInfo: CycleInfo = CycleInfo(
lastPeriodStart = null,
nextPeriodPrediction = null,
ovulationPrediction = null
),
val selectedDate: LocalDate? = null,
val isLoading: Boolean = true,
val error: String? = null
)

View File

@@ -0,0 +1,45 @@
package com.example.elva.ui.contacts
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.elva.databinding.FragmentContactsBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ContactsFragment : Fragment() {
private var _binding: FragmentContactsBinding? = null
private val binding get() = _binding!!
private val viewModel: ContactsViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentContactsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.contactsList.layoutManager = LinearLayoutManager(requireContext())
binding.addContactBtn.setOnClickListener {
viewModel.addContact(
binding.contactName.text.toString(),
binding.contactPhone.text.toString()
)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,27 @@
package com.example.elva.ui.contacts
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@HiltViewModel
class ContactsViewModel @Inject constructor() : ViewModel() {
private val _state = MutableStateFlow(ContactsState())
val state: StateFlow<ContactsState> = _state
fun addContact(name: String, phone: String) {
viewModelScope.launch {
val updated = _state.value.contacts + "$name - $phone"
_state.value = ContactsState(contacts = updated)
}
}
}
data class ContactsState(
val contacts: List<String> = emptyList()
)

View File

@@ -0,0 +1,12 @@
package com.example.elva.ui.cycle
import java.util.Date
data class CycleData(
val periodDay: Int = 1,
val cycleDays: Int = 30,
val ovulationInDays: Int = 12,
val nextPeriodInDays: Int = 25,
val startDate: Date = Date()
)

View File

@@ -0,0 +1,135 @@
package com.example.elva.ui.cycle
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.example.elva.R
import com.example.elva.databinding.FragmentCycleBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@AndroidEntryPoint
class CycleFragment : Fragment() {
private var _binding: FragmentCycleBinding? = null
private val binding get() = _binding!!
private val viewModel: CycleViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCycleBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUI()
setupWeekDays()
observeData()
}
private fun setupUI() {
// Установка текущей даты
val dateFormat = SimpleDateFormat("MMMM d", Locale.getDefault())
binding.dateText.text = dateFormat.format(Date())
// Клики
binding.editPeriodButton.setOnClickListener {
// TODO: Открыть диалог редактирования периода
}
binding.addButton.setOnClickListener {
// TODO: Добавить предсказание
}
binding.viewAllButton.setOnClickListener {
// TODO: Показать все циклы
}
}
private fun setupWeekDays() {
// Создаем кружки для дней недели
val calendar = Calendar.getInstance()
calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY)
for (i in 1..7) {
val dayView = createDayView(i, i == 5) // 5 - TODAY
binding.weekNumbersLayout.addView(dayView)
}
}
private fun createDayView(day: Int, isToday: Boolean): View {
val context = requireContext()
val size = (40 * resources.displayMetrics.density).toInt()
val margin = (8 * resources.displayMetrics.density).toInt()
val textView = TextView(context).apply {
text = day.toString()
textSize = 14f
gravity = android.view.Gravity.CENTER
layoutParams = ViewGroup.MarginLayoutParams(size, size).apply {
setMargins(margin / 2, 0, margin / 2, 0)
}
if (isToday) {
// Сегодня - заполненный круг
setBackgroundResource(R.drawable.day_circle_today)
setTextColor(resources.getColor(R.color.white, null))
} else {
// Обычный день - пунктирный круг
setBackgroundResource(R.drawable.day_circle_default)
setTextColor(resources.getColor(R.color.text_secondary, null))
}
}
return textView
}
private fun observeData() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.cycleData.collect { data ->
updateUI(data)
}
}
}
}
private fun updateUI(data: CycleData) {
binding.apply {
// Обновление данных о периоде
periodDay.text = getString(R.string.period_day, data.periodDay)
// Предсказания
ovulationText.text = getString(R.string.ovulation_in_days, data.ovulationInDays)
nextPeriodText.text = getString(R.string.next_period_in_days, data.nextPeriodInDays)
// Информация о цикле
currentCycleText.text = getString(R.string.current_cycle_days, data.cycleDays)
val dateFormat = SimpleDateFormat("MMM d", Locale.getDefault())
startedOnText.text = getString(R.string.started_on, dateFormat.format(data.startDate))
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,35 @@
package com.example.elva.ui.cycle
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@HiltViewModel
class CycleViewModel @Inject constructor() : ViewModel() {
private val _cycleData = MutableStateFlow(CycleData())
val cycleData: StateFlow<CycleData> = _cycleData.asStateFlow()
init {
loadCycleData()
}
private fun loadCycleData() {
// TODO: Загрузка данных из репозитория
// Пока используем данные по умолчанию
_cycleData.value = CycleData(
periodDay = 1,
cycleDays = 30,
ovulationInDays = 12,
nextPeriodInDays = 25
)
}
fun updatePeriod(day: Int) {
_cycleData.value = _cycleData.value.copy(periodDay = day)
}
}

View File

@@ -0,0 +1,89 @@
package com.example.elva.ui.dashboard
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.elva.R
import com.example.elva.databinding.ItemCalendarDayBinding
data class CalendarDay(
val day: Int,
val dayType: DayType,
val isCurrentDay: Boolean = false
)
enum class DayType {
NORMAL,
PERIOD,
OVULATION,
FERTILE,
EMPTY
}
class CalendarAdapter : ListAdapter<CalendarDay, CalendarAdapter.DayViewHolder>(DayDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DayViewHolder {
val binding = ItemCalendarDayBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return DayViewHolder(binding)
}
override fun onBindViewHolder(holder: DayViewHolder, position: Int) {
holder.bind(getItem(position))
}
class DayViewHolder(private val binding: ItemCalendarDayBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(calendarDay: CalendarDay) {
if (calendarDay.dayType == DayType.EMPTY) {
binding.dayText.text = ""
binding.dayContainer.background = null
return
}
binding.dayText.text = calendarDay.day.toString()
// Устанавливаем фон в зависимости от типа дня
val backgroundRes = when (calendarDay.dayType) {
DayType.PERIOD -> R.drawable.day_circle_period
DayType.OVULATION -> R.drawable.day_circle_ovulation
DayType.FERTILE -> R.drawable.day_circle_fertile
else -> if (calendarDay.isCurrentDay) R.drawable.day_circle_current else R.drawable.day_circle_default
}
binding.dayContainer.background = ContextCompat.getDrawable(
binding.root.context,
backgroundRes
)
// Цвет текста для текущего дня
if (calendarDay.isCurrentDay && calendarDay.dayType == DayType.NORMAL) {
binding.dayText.setTextColor(
ContextCompat.getColor(binding.root.context, R.color.primary)
)
} else {
binding.dayText.setTextColor(
ContextCompat.getColor(binding.root.context, R.color.text_primary)
)
}
}
}
class DayDiffCallback : DiffUtil.ItemCallback<CalendarDay>() {
override fun areItemsTheSame(oldItem: CalendarDay, newItem: CalendarDay): Boolean {
return oldItem.day == newItem.day
}
override fun areContentsTheSame(oldItem: CalendarDay, newItem: CalendarDay): Boolean {
return oldItem == newItem
}
}
}

View File

@@ -0,0 +1,201 @@
package com.example.elva.ui.dashboard
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.example.elva.R
import com.example.elva.databinding.FragmentDashboardBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*
@AndroidEntryPoint
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private val viewModel: HealthViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupDate()
setupListeners()
observeState()
}
private fun setupDate() {
val formatter = DateTimeFormatter.ofPattern("d MMMM", Locale("ru"))
val today = LocalDate.now()
binding.tvDate.text = "Сегодня, ${today.format(formatter)}"
}
private fun setupListeners() {
binding.apply {
// Переход к календарю цикла
cardCycle.setOnClickListener {
findNavController().navigate(R.id.action_dashboard_to_periodCalendar)
}
// Добавление воды
cardWater.setOnClickListener {
showWaterDialog()
}
// Добавление калорий
cardCalories.setOnClickListener {
showCaloriesDialog()
}
// Добавление веса
cardWeight.setOnClickListener {
showWeightDialog()
}
// Добавление сна
cardSleep.setOnClickListener {
showSleepDialog()
}
}
}
private fun observeState() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.state.collect { state ->
updateUI(state)
}
}
}
private fun updateUI(state: HealthState) {
binding.apply {
// Информация о цикле
val daysUntil = viewModel.getDaysUntilNextPeriod()
tvCycleInfo.text = if (daysUntil != null) {
"До менструации: $daysUntil ${getDaysWord(daysUntil)}"
} else {
"Нет данных о цикле"
}
// Вода
tvWaterAmount.text = state.waterIntake.amount.toString()
tvWaterGoal.text = "из ${state.waterIntake.goal} мл"
// Калории
tvCaloriesAmount.text = state.calorieIntake.calories.toString()
tvCaloriesGoal.text = "из ${state.calorieIntake.goal} ккал"
// Вес
tvWeightAmount.text = state.latestWeight?.toString() ?: "--"
// Сон
tvSleepAmount.text = state.sleepEntry?.hours?.toString() ?: "--"
}
}
private fun getDaysWord(days: Int): String {
return when {
days % 10 == 1 && days % 100 != 11 -> "день"
days % 10 in 2..4 && (days % 100 < 10 || days % 100 >= 20) -> "дня"
else -> "дней"
}
}
private fun showWaterDialog() {
val amounts = arrayOf("250 мл", "500 мл", "750 мл", "1000 мл")
MaterialAlertDialogBuilder(requireContext())
.setTitle("Добавить воду")
.setItems(amounts) { _, which ->
val amount = when (which) {
0 -> 250
1 -> 500
2 -> 750
else -> 1000
}
viewModel.addWater(amount)
}
.setNegativeButton("Отмена", null)
.show()
}
private fun showCaloriesDialog() {
val view = layoutInflater.inflate(R.layout.dialog_input, null)
val input = view.findViewById<TextInputEditText>(R.id.editInput)
input.hint = "Калории"
MaterialAlertDialogBuilder(requireContext())
.setTitle("Добавить калории")
.setView(view)
.setPositiveButton("Добавить") { _, _ ->
val calories = input.text.toString().toIntOrNull()
if (calories != null && calories > 0) {
viewModel.addCalories(calories)
}
}
.setNegativeButton("Отмена", null)
.show()
}
private fun showWeightDialog() {
val view = layoutInflater.inflate(R.layout.dialog_input, null)
val input = view.findViewById<TextInputEditText>(R.id.editInput)
input.hint = "Вес (кг)"
MaterialAlertDialogBuilder(requireContext())
.setTitle("Записать вес")
.setView(view)
.setPositiveButton("Сохранить") { _, _ ->
val weight = input.text.toString().toFloatOrNull()
if (weight != null && weight > 0) {
viewModel.saveWeight(weight)
}
}
.setNegativeButton("Отмена", null)
.show()
}
private fun showSleepDialog() {
val view = layoutInflater.inflate(R.layout.dialog_input, null)
val input = view.findViewById<TextInputEditText>(R.id.editInput)
input.hint = "Часов сна"
MaterialAlertDialogBuilder(requireContext())
.setTitle("Записать сон")
.setView(view)
.setPositiveButton("Сохранить") { _, _ ->
val hours = input.text.toString().toFloatOrNull()
if (hours != null && hours > 0) {
viewModel.saveSleep(hours, com.example.elva.data.models.health.SleepQuality.NORMAL)
}
}
.setNegativeButton("Отмена", null)
.show()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,147 @@
package com.example.elva.ui.dashboard
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import com.example.elva.R
import com.example.elva.databinding.FragmentDashboardNewBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.time.LocalTime
@AndroidEntryPoint
class DashboardFragmentNew : Fragment() {
private var _binding: FragmentDashboardNewBinding? = null
private val binding get() = _binding!!
private val viewModel: DashboardViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDashboardNewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupGreeting()
setupListeners()
observeState()
// Загружаем данные
viewModel.loadDashboardData()
}
private fun setupGreeting() {
val hour = LocalTime.now().hour
val greeting = when {
hour < 12 -> getString(R.string.good_morning)
hour < 18 -> getString(R.string.good_afternoon)
else -> getString(R.string.good_evening)
}
binding.tvGreeting.text = greeting
}
private fun setupListeners() {
binding.apply {
// Карточка цикла - открыть календарь периодов
cardCycle.setOnClickListener {
findNavController().navigate(R.id.action_dashboard_to_periodCalendar)
}
// Карточка календаря - открыть календарь периодов
cardCalendar.setOnClickListener {
findNavController().navigate(R.id.action_dashboard_to_periodCalendar)
}
// Карточка воды - добавить воду
cardWater.setOnClickListener {
showAddWaterDialog()
}
// Карточка калорий - открыть экран питания
cardCalories.setOnClickListener {
findNavController().navigate(R.id.action_dashboardFragment_to_nav_nutrition)
}
}
}
private fun observeState() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.dashboardSummary.collect { summary ->
updateUI(summary)
}
}
}
}
private fun updateUI(summary: DashboardSummary) {
binding.apply {
// Обновляем день цикла
tvCycleDay.text = getString(R.string.cycle_day_number, summary.cycleDay)
// Обновляем дни до периода
summary.nextPeriodDays?.let { days ->
tvNextPeriod.text = days.toString()
tvNextPeriodLabel.text = when {
days == 0 -> getString(R.string.period_today)
days == 1 -> getString(R.string.period_tomorrow)
days < 0 -> getString(R.string.period_overdue, -days)
else -> resources.getQuantityString(R.plurals.days_until_period, days, days)
}
} ?: run {
tvNextPeriod.text = ""
tvNextPeriodLabel.text = getString(R.string.no_data)
}
// Обновляем воду
val waterProgress = (summary.waterProgress * 100).toInt()
progressWater.progress = waterProgress
tvWaterAmount.text = getString(
R.string.water_progress,
summary.waterAmount,
summary.waterGoal
)
// Обновляем калории
val caloriesProgress = (summary.caloriesProgress * 100).toInt()
progressCalories.progress = caloriesProgress
tvCaloriesAmount.text = getString(
R.string.calories_progress,
summary.calories,
summary.caloriesGoal
)
}
}
private fun showAddWaterDialog() {
val amounts = arrayOf("100 мл", "200 мл", "250 мл", "500 мл")
val amountsValues = arrayOf(100, 200, 250, 500)
androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle(R.string.add_water)
.setItems(amounts) { _, which ->
viewModel.addWater(amountsValues[which])
}
.show()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,64 @@
package com.example.elva.ui.dashboard
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import javax.inject.Inject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import com.example.elva.data.repository.HealthRepository
import com.example.elva.data.repository.DashboardSummary
import java.time.LocalDate
@HiltViewModel
class DashboardViewModel @Inject constructor(
private val healthRepository: HealthRepository
) : ViewModel() {
private val _dashboardSummary = MutableStateFlow<DashboardSummary?>(null)
val dashboardSummary: StateFlow<DashboardSummary?> = _dashboardSummary
private val _isLoading = MutableStateFlow(true)
val isLoading: StateFlow<Boolean> = _isLoading
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error
init {
loadDashboardData()
}
fun loadDashboardData() {
viewModelScope.launch {
try {
_isLoading.value = true
val summary = healthRepository.getDashboardSummary()
_dashboardSummary.value = summary
_error.value = null
} catch (e: Exception) {
_error.value = e.message
} finally {
_isLoading.value = false
}
}
}
fun addWater(amount: Int) {
viewModelScope.launch {
try {
healthRepository.addWater(LocalDate.now(), amount)
loadDashboardData()
} catch (e: Exception) {
_error.value = e.message
}
}
}
fun refresh() {
loadDashboardData()
}
}

View File

@@ -0,0 +1,150 @@
package com.example.elva.ui.dashboard
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.elva.data.models.health.*
import com.example.elva.data.repository.HealthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.time.LocalDate
import javax.inject.Inject
@HiltViewModel
class HealthViewModel @Inject constructor(
private val healthRepository: HealthRepository
) : ViewModel() {
private val _state = MutableStateFlow(HealthState())
val state: StateFlow<HealthState> = _state
init {
loadTodayData()
}
fun loadTodayData() {
val today = LocalDate.now()
viewModelScope.launch {
try {
val waterIntakeData = healthRepository.getWaterIntakeForDate(today)
val waterGoal = healthRepository.getWaterGoal()
val waterIntake = WaterIntakeOld(
date = today,
amount = waterIntakeData?.amountMl ?: 0,
goal = waterGoal
)
val totalCalories = healthRepository.getTotalCaloriesForDate(today)
val calorieGoal = healthRepository.getCalorieGoal()
val calories = CalorieIntake(
date = today,
calories = totalCalories,
goal = calorieGoal.dailyGoal
)
val sleep = healthRepository.getSleepEntry(today)
val weightEntries = healthRepository.getWeightEntries()
val cycleInfo = healthRepository.getCycleInfo()
_state.value = _state.value.copy(
waterIntake = waterIntake,
calorieIntake = calories,
sleepEntry = sleep,
latestWeight = weightEntries.maxByOrNull { it.date }?.weightKg,
cycleInfo = cycleInfo,
isLoading = false
)
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message,
isLoading = false
)
}
}
}
fun addWater(amount: Int) {
viewModelScope.launch {
val today = LocalDate.now()
healthRepository.addWater(today, amount)
val updated = healthRepository.getWaterIntakeForDate(today)
val waterGoal = healthRepository.getWaterGoal()
_state.value = _state.value.copy(
waterIntake = WaterIntakeOld(
date = today,
amount = updated?.amountMl ?: amount,
goal = waterGoal
)
)
}
}
fun addCalories(calories: Int) {
viewModelScope.launch {
val entry = CalorieEntry(
date = LocalDate.now(),
calories = calories,
mealType = MealType.SNACK,
description = "Перекус",
timestamp = java.time.LocalDateTime.now()
)
healthRepository.addCalorieEntry(entry)
val total = healthRepository.getTotalCaloriesForDate(LocalDate.now())
val current = _state.value.calorieIntake
_state.value = _state.value.copy(
calorieIntake = current.copy(calories = total)
)
}
}
fun saveSleep(hours: Float, quality: SleepQuality, note: String? = null) {
viewModelScope.launch {
val now = java.time.LocalDateTime.now()
val entry = SleepEntry(
date = LocalDate.now(),
sleepStart = now.minusHours(hours.toLong()),
sleepEnd = now,
quality = quality,
notes = note ?: ""
)
healthRepository.addSleepEntry(entry)
_state.value = _state.value.copy(sleepEntry = entry)
}
}
fun saveWeight(weightKg: Float, note: String? = null) {
viewModelScope.launch {
val entry = WeightEntry(
date = LocalDate.now(),
weightKg = weightKg,
notes = note ?: ""
)
healthRepository.addWeightEntry(entry)
_state.value = _state.value.copy(latestWeight = weightKg)
}
}
fun getDaysUntilNextPeriod(): Int? {
val nextPeriod = _state.value.cycleInfo.nextPeriodPrediction ?: return null
val today = LocalDate.now()
return if (nextPeriod.isAfter(today)) {
java.time.temporal.ChronoUnit.DAYS.between(today, nextPeriod).toInt()
} else null
}
}
data class HealthState(
val waterIntake: WaterIntakeOld = WaterIntakeOld(date = LocalDate.now(), amount = 0),
val calorieIntake: CalorieIntake = CalorieIntake(date = LocalDate.now(), calories = 0),
val sleepEntry: SleepEntry? = null,
val latestWeight: Float? = null,
val cycleInfo: CycleInfo = CycleInfo(
lastPeriodStart = null,
nextPeriodPrediction = null,
ovulationPrediction = null
),
val isLoading: Boolean = true,
val error: String? = null
)

View File

@@ -0,0 +1,46 @@
package com.example.elva.ui.health
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.example.elva.R
import com.example.elva.databinding.DialogCaloriesInputBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class CaloriesInputDialog(
private val currentValue: Int,
private val goal: Int,
private val onSave: (consumed: Int, goal: Int) -> Unit
) : DialogFragment() {
private var _binding: DialogCaloriesInputBinding? = null
private val binding get() = _binding!!
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogCaloriesInputBinding.inflate(layoutInflater)
// Устанавливаем текущие значения
binding.etConsumed.setText(currentValue.toString())
binding.etGoal.setText(goal.toString())
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.calories)
.setView(binding.root)
.setPositiveButton(R.string.save) { _, _ ->
val consumed = binding.etConsumed.text.toString().toIntOrNull() ?: 0
val newGoal = binding.etGoal.text.toString().toIntOrNull() ?: goal
onSave(consumed, newGoal)
}
.setNegativeButton(R.string.cancel, null)
.create()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,186 @@
package com.example.elva.ui.health
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import com.example.elva.R
import com.example.elva.databinding.FragmentHealthDashboardBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class HealthDashboardFragment : Fragment() {
private var _binding: FragmentHealthDashboardBinding? = null
private val binding get() = _binding!!
private val viewModel: HealthDashboardViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHealthDashboardBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupListeners()
observeState()
}
private fun setupListeners() {
binding.apply {
// Навигация к календарю
calendarIcon.setOnClickListener {
findNavController().navigate(R.id.action_healthDashboard_to_periodCalendar)
}
// Клики на карточки для редактирования
cardCalories.setOnClickListener {
showCaloriesDialog()
}
cardWater.setOnClickListener {
showWaterDialog()
}
cardWeight.setOnClickListener {
showWeightDialog()
}
cardSleep.setOnClickListener {
showSleepDialog()
}
// Журнал
btnJournal.setOnClickListener {
findNavController().navigate(R.id.action_healthDashboard_to_healthJournal)
}
}
}
private fun observeState() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { state ->
updateUI(state)
}
}
}
}
private fun updateUI(state: HealthDashboardState) {
binding.apply {
// Калории
valueCalories.text = state.caloriesConsumed.toString()
goalCalories.text = getString(R.string.calorie_goal_format, state.caloriesGoal)
progressCalories.progress = state.caloriesPercent
// Вода
valueWater.text = state.waterConsumed.toString()
goalWater.text = getString(R.string.water_goal_format, state.waterGoal)
progressWater.progress = state.waterPercent
// Вес
if (state.currentWeight != null) {
valueWeight.text = String.format("%.1f", state.currentWeight)
if (state.weightChange != 0f) {
val sign = if (state.weightChange > 0) "+" else ""
changeWeight.text = getString(
R.string.weight_change_format,
sign,
String.format("%.1f", state.weightChange)
)
changeWeight.visibility = View.VISIBLE
} else {
changeWeight.visibility = View.GONE
}
} else {
valueWeight.text = ""
changeWeight.visibility = View.GONE
}
// Сон
valueSleep.text = String.format("%.1f", state.sleepHours)
goalSleep.text = getString(R.string.sleep_goal_format, state.sleepGoal)
progressSleep.progress = state.sleepPercent
}
}
private fun showCaloriesDialog() {
// Показываем диалог для ввода калорий
CaloriesInputDialog(
currentValue = viewModel.state.value.caloriesConsumed,
goal = viewModel.state.value.caloriesGoal,
onSave = { consumed, goal ->
viewModel.updateCalories(consumed)
if (goal != viewModel.state.value.caloriesGoal) {
viewModel.setCalorieGoal(goal)
}
}
).show(childFragmentManager, "calories_dialog")
}
private fun showWaterDialog() {
WaterInputDialog(
currentValue = viewModel.state.value.waterConsumed,
goal = viewModel.state.value.waterGoal,
onSave = { consumed, goal ->
viewModel.updateWater(consumed)
if (goal != viewModel.state.value.waterGoal) {
viewModel.setWaterGoal(goal)
}
}
).show(childFragmentManager, "water_dialog")
}
private fun showWeightDialog() {
WeightInputDialog(
currentValue = viewModel.state.value.currentWeight,
goal = viewModel.state.value.weightGoal,
onSave = { weight, goal ->
viewModel.updateWeight(weight)
if (goal != null && goal != viewModel.state.value.weightGoal) {
viewModel.setWeightGoal(goal)
}
}
).show(childFragmentManager, "weight_dialog")
}
private fun showSleepDialog() {
SleepInputDialog(
currentValue = viewModel.state.value.sleepHours,
goal = viewModel.state.value.sleepGoal,
quality = viewModel.state.value.sleepQuality,
onSave = { hours, goal, quality ->
viewModel.updateSleep(hours, quality)
if (goal != viewModel.state.value.sleepGoal) {
viewModel.setSleepGoal(goal)
}
}
).show(childFragmentManager, "sleep_dialog")
}
override fun onResume() {
super.onResume()
viewModel.loadHealthData()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,31 @@
package com.example.elva.ui.health
data class HealthDashboardState(
val currentCycleDay: Int = 0,
val nextPeriodDays: Int = 0,
val nextPeriodDate: String = "",
// Калории
val caloriesConsumed: Int = 0,
val caloriesGoal: Int = 2000,
val caloriesPercent: Int = 0,
// Вода
val waterConsumed: Int = 0, // в мл
val waterGoal: Int = 2000, // в мл
val waterPercent: Int = 0,
// Вес
val currentWeight: Float? = null,
val weightGoal: Float? = null,
val weightChange: Float = 0f,
// Сон
val sleepHours: Float = 0f,
val sleepGoal: Float = 8f,
val sleepPercent: Int = 0,
val sleepQuality: SleepQuality = SleepQuality.GOOD,
val isLoading: Boolean = false
)

View File

@@ -0,0 +1,187 @@
package com.example.elva.ui.health
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
@HiltViewModel
class HealthDashboardViewModel @Inject constructor(
private val healthRepository: HealthRepository
) : ViewModel() {
private val _state = MutableStateFlow(HealthDashboardState())
val state: StateFlow<HealthDashboardState> = _state.asStateFlow()
init {
loadHealthData()
}
fun loadHealthData() {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true)
// Загрузка данных периода
val lastPeriod = healthRepository.getLastPeriod()
val (cycleDay, nextPeriodDays, nextPeriodDate) = calculatePeriodInfo(lastPeriod)
// Загрузка калорий
val todayCalories = healthRepository.getTodayCalories()
val calorieGoal = healthRepository.getCalorieGoal()
val caloriesConsumed = todayCalories?.consumed ?: 0
val caloriesPercent = if (calorieGoal > 0)
((caloriesConsumed.toFloat() / calorieGoal) * 100).toInt().coerceIn(0, 100)
else 0
// Загрузка воды
val todayWater = healthRepository.getTodayWater()
val waterGoal = healthRepository.getWaterGoal()
val waterConsumed = todayWater?.consumed ?: 0
val waterPercent = if (waterGoal > 0)
((waterConsumed.toFloat() / waterGoal) * 100).toInt().coerceIn(0, 100)
else 0
// Загрузка веса
val latestWeight = healthRepository.getLatestWeight()
val weightGoal = healthRepository.getWeightGoal()
val weightEntries = healthRepository.getWeightEntries()
val weightChange = if (weightEntries.size >= 2) {
val current = weightEntries.last().weight
val previous = weightEntries[weightEntries.size - 2].weight
current - previous
} else 0f
// Загрузка сна
val lastSleep = healthRepository.getLastNightSleep()
val sleepGoal = healthRepository.getSleepGoal()
val sleepHours = lastSleep?.hours ?: 0f
val sleepPercent = if (sleepGoal > 0)
((sleepHours / sleepGoal) * 100).toInt().coerceIn(0, 100)
else 0
_state.value = HealthDashboardState(
currentCycleDay = cycleDay,
nextPeriodDays = nextPeriodDays,
nextPeriodDate = nextPeriodDate,
caloriesConsumed = caloriesConsumed,
caloriesGoal = calorieGoal,
caloriesPercent = caloriesPercent,
waterConsumed = waterConsumed,
waterGoal = waterGoal,
waterPercent = waterPercent,
currentWeight = latestWeight?.weight,
weightGoal = weightGoal,
weightChange = weightChange,
sleepHours = sleepHours,
sleepGoal = sleepGoal,
sleepPercent = sleepPercent,
sleepQuality = lastSleep?.quality ?: SleepQuality.GOOD,
isLoading = false
)
}
}
fun updateCalories(consumed: Int) {
viewModelScope.launch {
val goal = healthRepository.getCalorieGoal()
healthRepository.saveCalorieEntry(
CalorieEntry(consumed = consumed, goal = goal)
)
loadHealthData()
}
}
fun updateWater(consumed: Int) {
viewModelScope.launch {
val goal = healthRepository.getWaterGoal()
healthRepository.saveWaterEntry(
WaterEntry(consumed = consumed, goal = goal)
)
loadHealthData()
}
}
fun updateWeight(weight: Float) {
viewModelScope.launch {
val goal = healthRepository.getWeightGoal()
healthRepository.saveWeightEntry(
WeightEntry(weight = weight, goal = goal)
)
loadHealthData()
}
}
fun updateSleep(hours: Float, quality: SleepQuality) {
viewModelScope.launch {
val goal = healthRepository.getSleepGoal()
healthRepository.saveSleepEntry(
SleepEntry(hours = hours, goal = goal, quality = quality)
)
loadHealthData()
}
}
fun setCalorieGoal(goal: Int) {
viewModelScope.launch {
healthRepository.setCalorieGoal(goal)
loadHealthData()
}
}
fun setWaterGoal(goal: Int) {
viewModelScope.launch {
healthRepository.setWaterGoal(goal)
loadHealthData()
}
}
fun setWeightGoal(goal: Float) {
viewModelScope.launch {
healthRepository.setWeightGoal(goal)
loadHealthData()
}
}
fun setSleepGoal(goal: Float) {
viewModelScope.launch {
healthRepository.setSleepGoal(goal)
loadHealthData()
}
}
private fun calculatePeriodInfo(lastPeriod: PeriodData?): Triple<Int, Int, String> {
if (lastPeriod == null) {
return Triple(0, 0, "")
}
val calendar = Calendar.getInstance()
val today = calendar.timeInMillis
val startDate = lastPeriod.startDate
// Вычисляем день цикла
val daysDiff = ((today - startDate) / (24 * 60 * 60 * 1000)).toInt()
val cycleDay = (daysDiff % lastPeriod.cycleLength) + 1
// Вычисляем дни до следующего периода
val daysUntilNext = lastPeriod.cycleLength - cycleDay + 1
// Вычисляем дату следующего периода
calendar.timeInMillis = startDate
calendar.add(Calendar.DAY_OF_MONTH, lastPeriod.cycleLength)
val dateFormat = SimpleDateFormat("d MMMM", Locale("ru"))
val nextDateString = dateFormat.format(calendar.time)
return Triple(cycleDay, daysUntilNext, nextDateString)
}
fun openCalendar() {
// Навигация к календарю будет обработана во фрагменте
}
}

View File

@@ -0,0 +1,147 @@
package com.example.elva.ui.health
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.example.elva.R
import com.example.elva.databinding.FragmentHealthBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
@AndroidEntryPoint
class HealthFragment : Fragment() {
private var _binding: FragmentHealthBinding? = null
private val binding get() = _binding!!
private val viewModel: HealthViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHealthBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupClickListeners()
observeUiState()
}
private fun setupClickListeners() {
binding.cardCalendar.setOnClickListener {
// Navigate to calendar fragment
findNavController().navigate(R.id.action_health_to_calendar)
}
binding.cardWater.setOnClickListener {
showAddWaterDialog()
}
binding.cardCalories.setOnClickListener {
showAddCaloriesDialog()
}
binding.cardWeight.setOnClickListener {
showAddWeightDialog()
}
binding.cardSleep.setOnClickListener {
showAddSleepDialog()
}
}
private fun observeUiState() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
when (state) {
is HealthUiState.Loading -> {
// Show loading
}
is HealthUiState.Success -> {
updateUI(state)
}
is HealthUiState.Error -> {
// Show error
}
}
}
}
}
private fun updateUI(state: HealthUiState.Success) {
// Update cycle info
val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale("ru"))
binding.tvNextPeriod.text = state.cycleStats.nextPeriodStart?.format(dateFormatter) ?: "-"
binding.tvCycleLength.text = "${state.cycleStats.averageCycleLength} дней"
// Update water
val waterPercent = if (state.waterGoal > 0) {
(state.waterIntake * 100 / state.waterGoal).coerceIn(0, 100)
} else 0
binding.progressWater.progress = waterPercent
binding.tvWaterProgress.text = "${state.waterIntake}/${state.waterGoal} мл"
// Update calories
val caloriesPercent = if (state.calorieGoal > 0) {
(state.caloriesConsumed * 100 / state.calorieGoal).coerceIn(0, 100)
} else 0
binding.progressCalories.progress = caloriesPercent
binding.tvCaloriesProgress.text = "${state.caloriesConsumed}/${state.calorieGoal} ккал"
// Update weight
binding.tvWeight.text = if (state.currentWeight != null) {
"%.1f кг".format(state.currentWeight)
} else {
"Нет данных"
}
// Update sleep
val sleepPercent = if (state.sleepGoal > 0 && state.todaySleep != null) {
((state.todaySleep / state.sleepGoal) * 100).toInt().coerceIn(0, 100)
} else 0
binding.progressSleep.progress = sleepPercent
binding.tvSleep.text = if (state.todaySleep != null) {
"%.1f ч".format(state.todaySleep)
} else {
"Нет данных"
}
}
private fun showAddWaterDialog() {
// TODO: Implement water dialog
}
private fun showAddCaloriesDialog() {
// TODO: Implement calories dialog
}
private fun showAddWeightDialog() {
// TODO: Implement weight dialog
}
private fun showAddSleepDialog() {
// TODO: Implement sleep dialog
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,90 @@
package com.example.elva.ui.health
import java.util.*
/**
* Общая модель метрики здоровья
*/
sealed class HealthMetric(
open val id: String,
open val timestamp: Long = System.currentTimeMillis()
)
/**
* Метрика калорий
*/
data class CalorieEntry(
override val id: String = UUID.randomUUID().toString(),
val consumed: Int,
val goal: Int,
override val timestamp: Long = System.currentTimeMillis()
) : HealthMetric(id, timestamp)
/**
* Метрика воды
*/
data class WaterEntry(
override val id: String = UUID.randomUUID().toString(),
val consumed: Int, // в мл
val goal: Int, // в мл
override val timestamp: Long = System.currentTimeMillis()
) : HealthMetric(id, timestamp)
/**
* Метрика веса
*/
data class WeightEntry(
override val id: String = UUID.randomUUID().toString(),
val weight: Float, // в кг
val goal: Float?, // целевой вес в кг
override val timestamp: Long = System.currentTimeMillis()
) : HealthMetric(id, timestamp)
/**
* Метрика сна
*/
data class SleepEntry(
override val id: String = UUID.randomUUID().toString(),
val hours: Float,
val goal: Float,
val quality: SleepQuality = SleepQuality.GOOD,
override val timestamp: Long = System.currentTimeMillis()
) : HealthMetric(id, timestamp)
enum class SleepQuality {
EXCELLENT,
GOOD,
FAIR,
POOR
}
/**
* Запись в журнале
*/
data class HealthNote(
val id: String = UUID.randomUUID().toString(),
val title: String,
val content: String,
val category: NoteCategory,
val timestamp: Long = System.currentTimeMillis()
)
enum class NoteCategory {
SYMPTOMS,
MOOD,
EXERCISE,
MEDICATION,
OTHER
}
/**
* Данные периода для календаря
*/
data class PeriodData(
val id: String = UUID.randomUUID().toString(),
val startDate: Long,
val endDate: Long?,
val cycleLength: Int = 28,
val periodLength: Int = 5
)

View File

@@ -0,0 +1,198 @@
package com.example.elva.ui.health
import android.content.Context
import android.content.SharedPreferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* Репозиторий для работы с данными здоровья
*/
@Singleton
class HealthRepository @Inject constructor(
@ApplicationContext private val context: Context
) {
private val prefs: SharedPreferences =
context.getSharedPreferences("health_data", Context.MODE_PRIVATE)
private val gson = Gson()
// Калории
fun saveCalorieEntry(entry: CalorieEntry) {
val entries = getCalorieEntries().toMutableList()
entries.add(entry)
saveCalorieEntries(entries)
}
fun getCalorieEntries(): List<CalorieEntry> {
val json = prefs.getString("calorie_entries", "[]") ?: "[]"
val type = object : TypeToken<List<CalorieEntry>>() {}.type
return gson.fromJson(json, type)
}
private fun saveCalorieEntries(entries: List<CalorieEntry>) {
prefs.edit().putString("calorie_entries", gson.toJson(entries)).apply()
}
fun getTodayCalories(): CalorieEntry? {
return getCalorieEntries()
.filter { isToday(it.timestamp) }
.maxByOrNull { it.timestamp }
}
// Вода
fun saveWaterEntry(entry: WaterEntry) {
val entries = getWaterEntries().toMutableList()
entries.add(entry)
saveWaterEntries(entries)
}
fun getWaterEntries(): List<WaterEntry> {
val json = prefs.getString("water_entries", "[]") ?: "[]"
val type = object : TypeToken<List<WaterEntry>>() {}.type
return gson.fromJson(json, type)
}
private fun saveWaterEntries(entries: List<WaterEntry>) {
prefs.edit().putString("water_entries", gson.toJson(entries)).apply()
}
fun getTodayWater(): WaterEntry? {
return getWaterEntries()
.filter { isToday(it.timestamp) }
.maxByOrNull { it.timestamp }
}
// Вес
fun saveWeightEntry(entry: WeightEntry) {
val entries = getWeightEntries().toMutableList()
entries.add(entry)
saveWeightEntries(entries)
}
fun getWeightEntries(): List<WeightEntry> {
val json = prefs.getString("weight_entries", "[]") ?: "[]"
val type = object : TypeToken<List<WeightEntry>>() {}.type
return gson.fromJson(json, type)
}
private fun saveWeightEntries(entries: List<WeightEntry>) {
prefs.edit().putString("weight_entries", gson.toJson(entries)).apply()
}
fun getLatestWeight(): WeightEntry? {
return getWeightEntries().maxByOrNull { it.timestamp }
}
// Сон
fun saveSleepEntry(entry: SleepEntry) {
val entries = getSleepEntries().toMutableList()
entries.add(entry)
saveSleepEntries(entries)
}
fun getSleepEntries(): List<SleepEntry> {
val json = prefs.getString("sleep_entries", "[]") ?: "[]"
val type = object : TypeToken<List<SleepEntry>>() {}.type
return gson.fromJson(json, type)
}
private fun saveSleepEntries(entries: List<SleepEntry>) {
prefs.edit().putString("sleep_entries", gson.toJson(entries)).apply()
}
fun getLastNightSleep(): SleepEntry? {
return getSleepEntries()
.filter { isYesterday(it.timestamp) || isToday(it.timestamp) }
.maxByOrNull { it.timestamp }
}
// Заметки
fun saveNote(note: HealthNote) {
val notes = getNotes().toMutableList()
notes.add(note)
saveNotes(notes)
}
fun getNotes(): List<HealthNote> {
val json = prefs.getString("health_notes", "[]") ?: "[]"
val type = object : TypeToken<List<HealthNote>>() {}.type
return gson.fromJson(json, type)
}
private fun saveNotes(notes: List<HealthNote>) {
prefs.edit().putString("health_notes", gson.toJson(notes)).apply()
}
fun deleteNote(noteId: String) {
val notes = getNotes().filter { it.id != noteId }
saveNotes(notes)
}
// Периоды
fun savePeriod(period: PeriodData) {
val periods = getPeriods().toMutableList()
periods.add(period)
savePeriods(periods)
}
fun getPeriods(): List<PeriodData> {
val json = prefs.getString("periods", "[]") ?: "[]"
val type = object : TypeToken<List<PeriodData>>() {}.type
return gson.fromJson(json, type)
}
private fun savePeriods(periods: List<PeriodData>) {
prefs.edit().putString("periods", gson.toJson(periods)).apply()
}
fun getLastPeriod(): PeriodData? {
return getPeriods().maxByOrNull { it.startDate }
}
// Цели
fun setCalorieGoal(goal: Int) {
prefs.edit().putInt("calorie_goal", goal).apply()
}
fun getCalorieGoal(): Int = prefs.getInt("calorie_goal", 2000)
fun setWaterGoal(goal: Int) {
prefs.edit().putInt("water_goal", goal).apply()
}
fun getWaterGoal(): Int = prefs.getInt("water_goal", 2000)
fun setWeightGoal(goal: Float) {
prefs.edit().putFloat("weight_goal", goal).apply()
}
fun getWeightGoal(): Float? {
val goal = prefs.getFloat("weight_goal", -1f)
return if (goal > 0) goal else null
}
fun setSleepGoal(goal: Float) {
prefs.edit().putFloat("sleep_goal", goal).apply()
}
fun getSleepGoal(): Float = prefs.getFloat("sleep_goal", 8f)
// Вспомогательные функции
private fun isToday(timestamp: Long): Boolean {
val cal1 = java.util.Calendar.getInstance().apply { timeInMillis = timestamp }
val cal2 = java.util.Calendar.getInstance()
return cal1.get(java.util.Calendar.YEAR) == cal2.get(java.util.Calendar.YEAR) &&
cal1.get(java.util.Calendar.DAY_OF_YEAR) == cal2.get(java.util.Calendar.DAY_OF_YEAR)
}
private fun isYesterday(timestamp: Long): Boolean {
val cal1 = java.util.Calendar.getInstance().apply { timeInMillis = timestamp }
val cal2 = java.util.Calendar.getInstance().apply { add(java.util.Calendar.DAY_OF_YEAR, -1) }
return cal1.get(java.util.Calendar.YEAR) == cal2.get(java.util.Calendar.YEAR) &&
cal1.get(java.util.Calendar.DAY_OF_YEAR) == cal2.get(java.util.Calendar.DAY_OF_YEAR)
}
}

View File

@@ -0,0 +1,169 @@
package com.example.elva.ui.health
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.elva.data.models.health.*
import com.example.elva.data.repository.HealthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.time.LocalDate
import javax.inject.Inject
@HiltViewModel
class HealthViewModel @Inject constructor(
private val healthRepository: HealthRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<HealthUiState>(HealthUiState.Loading)
val uiState: StateFlow<HealthUiState> = _uiState.asStateFlow()
private val _selectedDate = MutableStateFlow(LocalDate.now())
val selectedDate: StateFlow<LocalDate> = _selectedDate.asStateFlow()
init {
loadHealthData()
}
fun loadHealthData() {
viewModelScope.launch {
try {
val today = LocalDate.now()
// Load all data
val cycleStats = healthRepository.calculateCycleStats()
val waterIntake = healthRepository.getWaterIntakeForDate(today)
val waterGoal = healthRepository.getWaterGoal()
val calorieEntries = healthRepository.getCalorieEntriesForDate(today)
val calorieGoal = healthRepository.getCalorieGoal()
val latestWeight = healthRepository.getLatestWeight()
val sleepEntry = healthRepository.getSleepEntryForDate(today)
val sleepGoal = healthRepository.getSleepGoal()
val avgSleep = healthRepository.getAverageSleepHours()
_uiState.value = HealthUiState.Success(
cycleStats = cycleStats,
waterIntake = waterIntake?.amountMl ?: 0,
waterGoal = waterGoal,
caloriesConsumed = calorieEntries.sumOf { it.calories },
calorieGoal = calorieGoal.dailyGoal,
currentWeight = latestWeight?.weightKg,
todaySleep = sleepEntry?.durationHours,
sleepGoal = sleepGoal,
averageSleep = avgSleep
)
} catch (e: Exception) {
_uiState.value = HealthUiState.Error(e.message ?: "Unknown error")
}
}
}
fun setSelectedDate(date: LocalDate) {
_selectedDate.value = date
}
// Water tracking
fun addWater(amountMl: Int) {
viewModelScope.launch {
healthRepository.addWater(LocalDate.now(), amountMl)
loadHealthData()
}
}
fun setWaterGoal(goalMl: Int) {
viewModelScope.launch {
healthRepository.setWaterGoal(goalMl)
loadHealthData()
}
}
// Calorie tracking
fun addCalorieEntry(calories: Int, mealType: MealType, description: String = "") {
viewModelScope.launch {
val entry = CalorieEntry(
date = LocalDate.now(),
calories = calories,
mealType = mealType,
description = description
)
healthRepository.addCalorieEntry(entry)
loadHealthData()
}
}
fun setCalorieGoal(goal: CalorieGoal) {
viewModelScope.launch {
healthRepository.setCalorieGoal(goal)
loadHealthData()
}
}
// Weight tracking
fun addWeightEntry(weightKg: Float, notes: String = "") {
viewModelScope.launch {
val entry = WeightEntry(
date = LocalDate.now(),
weightKg = weightKg,
notes = notes
)
healthRepository.addWeightEntry(entry)
loadHealthData()
}
}
// Sleep tracking
fun addSleepEntry(entry: SleepEntry) {
viewModelScope.launch {
healthRepository.addSleepEntry(entry)
loadHealthData()
}
}
fun setSleepGoal(hours: Float) {
viewModelScope.launch {
healthRepository.setSleepGoal(hours)
loadHealthData()
}
}
// Period tracking
fun addPeriod(period: Period) {
viewModelScope.launch {
healthRepository.addPeriod(period)
loadHealthData()
}
}
fun updatePeriod(period: Period) {
viewModelScope.launch {
healthRepository.updatePeriod(period)
loadHealthData()
}
}
fun deletePeriod(periodId: Long) {
viewModelScope.launch {
healthRepository.deletePeriod(periodId)
loadHealthData()
}
}
}
sealed class HealthUiState {
object Loading : HealthUiState()
data class Success(
val cycleStats: CycleStats,
val waterIntake: Int,
val waterGoal: Int,
val caloriesConsumed: Int,
val calorieGoal: Int,
val currentWeight: Float?,
val todaySleep: Float?,
val sleepGoal: Float,
val averageSleep: Float
) : HealthUiState()
data class Error(val message: String) : HealthUiState()
}

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