commit 0ce8ca012886073e2a2a90dcf59535d5abf1968f Author: Andrew K. Choi Date: Tue Jan 13 15:58:31 2026 +0900 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000..7ef04e2 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..01bcd69 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml new file mode 100644 index 0000000..8ec256a --- /dev/null +++ b/.idea/render.experimental.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.kotlin/errors/errors-1768261907650.log b/.kotlin/errors/errors-1768261907650.log new file mode 100644 index 0000000..0d8594a --- /dev/null +++ b/.kotlin/errors/errors-1768261907650.log @@ -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) + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..a6d92a7 --- /dev/null +++ b/app/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/elva/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/elva/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d0d642e --- /dev/null +++ b/app/src/androidTest/java/com/example/elva/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..587bafa --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/UserInfo.kt b/app/src/main/java/UserInfo.kt new file mode 100644 index 0000000..eecfcf2 --- /dev/null +++ b/app/src/main/java/UserInfo.kt @@ -0,0 +1,3 @@ +// ФАЙЛ УДАЛЕН - используйте AuthModels.kt +// Этот класс теперь находится в AuthModels.kt + diff --git a/app/src/main/java/com/example/elva/ElvaApp.kt b/app/src/main/java/com/example/elva/ElvaApp.kt new file mode 100644 index 0000000..7f6aa93 --- /dev/null +++ b/app/src/main/java/com/example/elva/ElvaApp.kt @@ -0,0 +1,8 @@ +package com.example.elva + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class ElvaApp : Application() + diff --git a/app/src/main/java/com/example/elva/MainActivity.kt b/app/src/main/java/com/example/elva/MainActivity.kt new file mode 100644 index 0000000..557e9d2 --- /dev/null +++ b/app/src/main/java/com/example/elva/MainActivity.kt @@ -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(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(R.id.nav_cycle)?.setOnClickListener { + navController.navigate(R.id.nav_cycle) + } + + findViewById(R.id.nav_health)?.setOnClickListener { + navController.navigate(R.id.dashboardFragment) + } + + findViewById(R.id.nav_sos)?.setOnClickListener { + navController.navigate(R.id.nav_sos) + } + + findViewById(R.id.nav_chat)?.setOnClickListener { + // TODO: Navigate to Chat fragment + } + + findViewById(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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/elva/data/api/ApiConfig.kt b/app/src/main/java/com/example/elva/data/api/ApiConfig.kt new file mode 100644 index 0000000..d795a9c --- /dev/null +++ b/app/src/main/java/com/example/elva/data/api/ApiConfig.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/api/AuthApiService.kt b/app/src/main/java/com/example/elva/data/api/AuthApiService.kt new file mode 100644 index 0000000..215016c --- /dev/null +++ b/app/src/main/java/com/example/elva/data/api/AuthApiService.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/api/ProfileApiService.kt b/app/src/main/java/com/example/elva/data/api/ProfileApiService.kt new file mode 100644 index 0000000..450b13d --- /dev/null +++ b/app/src/main/java/com/example/elva/data/api/ProfileApiService.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/api/UserApiService.kt b/app/src/main/java/com/example/elva/data/api/UserApiService.kt new file mode 100644 index 0000000..f00dfba --- /dev/null +++ b/app/src/main/java/com/example/elva/data/api/UserApiService.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/local/HealthDataStore.kt b/app/src/main/java/com/example/elva/data/local/HealthDataStore.kt new file mode 100644 index 0000000..e191166 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/local/HealthDataStore.kt @@ -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) { + val json = gson.toJson(periods) + prefs.edit().putString("periods", json).apply() + } + + fun getPeriods(): List { + val json = prefs.getString("periods", null) ?: return emptyList() + val type = object : TypeToken>() {}.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) { + val json = gson.toJson(intakes) + prefs.edit().putString("water_intakes", json).apply() + } + + fun getWaterIntakes(): List { + val json = prefs.getString("water_intakes", null) ?: return emptyList() + val type = object : TypeToken>() {}.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) { + val json = gson.toJson(entries) + prefs.edit().putString("calorie_entries", json).apply() + } + + fun getCalorieEntries(): List { + val json = prefs.getString("calorie_entries", null) ?: return emptyList() + val type = object : TypeToken>() {}.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) { + val json = gson.toJson(entries) + prefs.edit().putString("weight_entries", json).apply() + } + + fun getWeightEntries(): List { + val json = prefs.getString("weight_entries", null) ?: return emptyList() + val type = object : TypeToken>() {}.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) { + val json = gson.toJson(entries) + prefs.edit().putString("sleep_entries", json).apply() + } + + fun getSleepEntries(): List { + val json = prefs.getString("sleep_entries", null) ?: return emptyList() + val type = object : TypeToken>() {}.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) { + val json = gson.toJson(entries) + prefs.edit().putString("journal_entries", json).apply() + } + + fun getJournalEntries(): List { + val json = prefs.getString("journal_entries", null) ?: return emptyList() + val type = object : TypeToken>() {}.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 } + } +} + diff --git a/app/src/main/java/com/example/elva/data/local/SessionManager.kt b/app/src/main/java/com/example/elva/data/local/SessionManager.kt new file mode 100644 index 0000000..5569d77 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/local/SessionManager.kt @@ -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 = 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" + } +} + diff --git a/app/src/main/java/com/example/elva/data/models/auth/AuthModels.kt b/app/src/main/java/com/example/elva/data/models/auth/AuthModels.kt new file mode 100644 index 0000000..cb0ab55 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/models/auth/AuthModels.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/data/models/dashboard/DashboardSummary.kt b/app/src/main/java/com/example/elva/data/models/dashboard/DashboardSummary.kt new file mode 100644 index 0000000..d123f48 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/models/dashboard/DashboardSummary.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/data/models/health/FlowIntensity.kt b/app/src/main/java/com/example/elva/data/models/health/FlowIntensity.kt new file mode 100644 index 0000000..7a30df7 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/models/health/FlowIntensity.kt @@ -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 + } + } + } +} + diff --git a/app/src/main/java/com/example/elva/data/models/health/HealthMetrics.kt b/app/src/main/java/com/example/elva/data/models/health/HealthMetrics.kt new file mode 100644 index 0000000..6002a70 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/models/health/HealthMetrics.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/models/health/HealthModels.kt b/app/src/main/java/com/example/elva/data/models/health/HealthModels.kt new file mode 100644 index 0000000..65fdc24 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/models/health/HealthModels.kt @@ -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 = 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 // кг +) + diff --git a/app/src/main/java/com/example/elva/data/models/health/Period.kt b/app/src/main/java/com/example/elva/data/models/health/Period.kt new file mode 100644 index 0000000..de96849 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/models/health/Period.kt @@ -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 = 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? +) + diff --git a/app/src/main/java/com/example/elva/data/models/profile/ProfileModels.kt b/app/src/main/java/com/example/elva/data/models/profile/ProfileModels.kt new file mode 100644 index 0000000..fd79703 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/models/profile/ProfileModels.kt @@ -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 +) + + diff --git a/app/src/main/java/com/example/elva/data/models/user/UserProfile.kt b/app/src/main/java/com/example/elva/data/models/user/UserProfile.kt new file mode 100644 index 0000000..4cacd7c --- /dev/null +++ b/app/src/main/java/com/example/elva/data/models/user/UserProfile.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/data/remote/ApiConfig.kt b/app/src/main/java/com/example/elva/data/remote/ApiConfig.kt new file mode 100644 index 0000000..a2975cb --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/ApiConfig.kt @@ -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" +} + diff --git a/app/src/main/java/com/example/elva/data/remote/CalendarApi.kt b/app/src/main/java/com/example/elva/data/remote/CalendarApi.kt new file mode 100644 index 0000000..8e3fc23 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/CalendarApi.kt @@ -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 + + @POST("/api/v1/calendar/entries") + suspend fun addEntry( + @Header("Authorization") token: String, + @Body request: CreateCalendarEntryRequest + ): CalendarEntryDto +} + diff --git a/app/src/main/java/com/example/elva/data/remote/ContactsApi.kt b/app/src/main/java/com/example/elva/data/remote/ContactsApi.kt new file mode 100644 index 0000000..1bf191e --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/ContactsApi.kt @@ -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 + + @POST("/api/v1/users/me/emergency-contacts") + suspend fun addContact( + @Header("Authorization") token: String, + @Body request: CreateContactRequest + ): ContactDto +} + diff --git a/app/src/main/java/com/example/elva/data/remote/EmergencyApi.kt b/app/src/main/java/com/example/elva/data/remote/EmergencyApi.kt new file mode 100644 index 0000000..cf85cf1 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/EmergencyApi.kt @@ -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 + + @GET("/api/v1/alerts/responses") + suspend fun getResponses( + @Header("Authorization") token: String + ): List +} + diff --git a/app/src/main/java/com/example/elva/data/remote/LocationApi.kt b/app/src/main/java/com/example/elva/data/remote/LocationApi.kt new file mode 100644 index 0000000..1ba5b6c --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/LocationApi.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/remote/NotificationApi.kt b/app/src/main/java/com/example/elva/data/remote/NotificationApi.kt new file mode 100644 index 0000000..5cca964 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/NotificationApi.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/remote/NutritionApi.kt b/app/src/main/java/com/example/elva/data/remote/NutritionApi.kt new file mode 100644 index 0000000..35574ff --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/NutritionApi.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/remote/api/AuthApiService.kt b/app/src/main/java/com/example/elva/data/remote/api/AuthApiService.kt new file mode 100644 index 0000000..a9e488b --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/api/AuthApiService.kt @@ -0,0 +1,2 @@ +// DEPRECATED: Этот файл удален. Используйте com.example.elva.data.api.AuthApiService + diff --git a/app/src/main/java/com/example/elva/data/remote/api/EmergencyApiService.kt b/app/src/main/java/com/example/elva/data/remote/api/EmergencyApiService.kt new file mode 100644 index 0000000..0d0444c --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/api/EmergencyApiService.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/remote/api/ProfileApiService.kt b/app/src/main/java/com/example/elva/data/remote/api/ProfileApiService.kt new file mode 100644 index 0000000..aeeca83 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/api/ProfileApiService.kt @@ -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 +} + diff --git a/app/src/main/java/com/example/elva/data/remote/model/CalendarModels.kt b/app/src/main/java/com/example/elva/data/remote/model/CalendarModels.kt new file mode 100644 index 0000000..b74c0e8 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/model/CalendarModels.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/data/remote/model/ContactsModels.kt b/app/src/main/java/com/example/elva/data/remote/model/ContactsModels.kt new file mode 100644 index 0000000..c737f04 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/model/ContactsModels.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/data/remote/model/EmergencyModels.kt b/app/src/main/java/com/example/elva/data/remote/model/EmergencyModels.kt new file mode 100644 index 0000000..8615434 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/model/EmergencyModels.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/data/remote/model/LocationModels.kt b/app/src/main/java/com/example/elva/data/remote/model/LocationModels.kt new file mode 100644 index 0000000..c1f5940 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/model/LocationModels.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/data/remote/model/NutritionModels.kt b/app/src/main/java/com/example/elva/data/remote/model/NutritionModels.kt new file mode 100644 index 0000000..55a0953 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/model/NutritionModels.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/data/remote/model/auth/AuthModels.kt b/app/src/main/java/com/example/elva/data/remote/model/auth/AuthModels.kt new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/com/example/elva/data/remote/model/emergency/EmergencyModels.kt b/app/src/main/java/com/example/elva/data/remote/model/emergency/EmergencyModels.kt new file mode 100644 index 0000000..88d785a --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/model/emergency/EmergencyModels.kt @@ -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, + 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" +} + diff --git a/app/src/main/java/com/example/elva/data/remote/model/profile/ProfileModels.kt b/app/src/main/java/com/example/elva/data/remote/model/profile/ProfileModels.kt new file mode 100644 index 0000000..b116097 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/remote/model/profile/ProfileModels.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/data/repository/AuthRepository.kt b/app/src/main/java/com/example/elva/data/repository/AuthRepository.kt new file mode 100644 index 0000000..414206a --- /dev/null +++ b/app/src/main/java/com/example/elva/data/repository/AuthRepository.kt @@ -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 { + 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 { + 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() + } +} + diff --git a/app/src/main/java/com/example/elva/data/repository/DashboardRepository.kt b/app/src/main/java/com/example/elva/data/repository/DashboardRepository.kt new file mode 100644 index 0000000..7bcbbc7 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/repository/DashboardRepository.kt @@ -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 + ) + } +} + diff --git a/app/src/main/java/com/example/elva/data/repository/HealthRepository.kt b/app/src/main/java/com/example/elva/data/repository/HealthRepository.kt new file mode 100644 index 0000000..d23f8a5 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/repository/HealthRepository.kt @@ -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 = 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() + 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 = 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 = dataStore.getCalorieEntries() + + fun getCalorieEntriesForDate(date: LocalDate): List { + 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 = 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 = 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 = dataStore.getJournalEntries() + + fun getJournalEntriesForDate(date: LocalDate): List { + return getJournalEntries().filter { it.date == date } + } + + fun addJournalEntry(entry: HealthJournalEntry) = dataStore.addJournalEntry(entry) + + fun deleteJournalEntry(entryId: Long) = dataStore.deleteJournalEntry(entryId) +} + diff --git a/app/src/main/java/com/example/elva/data/repository/ProfileRepository.kt b/app/src/main/java/com/example/elva/data/repository/ProfileRepository.kt new file mode 100644 index 0000000..0a0efeb --- /dev/null +++ b/app/src/main/java/com/example/elva/data/repository/ProfileRepository.kt @@ -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 { + return try { + val profile = profileApi.getProfile() + Result.success(profile) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun updateProfile(request: ProfileUpdateRequest): Result { + return try { + val updatedProfile = profileApi.updateProfile(request) + Result.success(updatedProfile) + } catch (e: Exception) { + Result.failure(e) + } + } +} + diff --git a/app/src/main/java/com/example/elva/data/repository/UserRepository.kt b/app/src/main/java/com/example/elva/data/repository/UserRepository.kt new file mode 100644 index 0000000..d278f35 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/repository/UserRepository.kt @@ -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 { + 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) + } + } +} + diff --git a/app/src/main/java/com/example/elva/data/websocket/EmergencyWebSocketManager.kt b/app/src/main/java/com/example/elva/data/websocket/EmergencyWebSocketManager.kt new file mode 100644 index 0000000..45c78f5 --- /dev/null +++ b/app/src/main/java/com/example/elva/data/websocket/EmergencyWebSocketManager.kt @@ -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() + 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(text) + + when (message.type) { + "connection_established" -> { + notifyConnectionEstablished(message.userId ?: -1) + } + "emergency_alert" -> { + val alert = json.decodeFromString(text) + notifyNewEmergencyAlert(alert) + } + "alert_update" -> { + val update = json.decodeFromString(text) + notifyAlertUpdate(update) + } + "alert_response" -> { + val response = json.decodeFromString(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() + } +} + diff --git a/app/src/main/java/com/example/elva/data/websocket/WebSocketEventListener.kt b/app/src/main/java/com/example/elva/data/websocket/WebSocketEventListener.kt new file mode 100644 index 0000000..8ff4c5b --- /dev/null +++ b/app/src/main/java/com/example/elva/data/websocket/WebSocketEventListener.kt @@ -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) +} + diff --git a/app/src/main/java/com/example/elva/di/AppModule.kt b/app/src/main/java/com/example/elva/di/AppModule.kt new file mode 100644 index 0000000..aaee103 --- /dev/null +++ b/app/src/main/java/com/example/elva/di/AppModule.kt @@ -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) +} + diff --git a/app/src/main/java/com/example/elva/di/NetworkModule.kt b/app/src/main/java/com/example/elva/di/NetworkModule.kt new file mode 100644 index 0000000..a82d6b4 --- /dev/null +++ b/app/src/main/java/com/example/elva/di/NetworkModule.kt @@ -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) + } +} + diff --git a/app/src/main/java/com/example/elva/ui/alerts/AlertsFragment.kt b/app/src/main/java/com/example/elva/ui/alerts/AlertsFragment.kt new file mode 100644 index 0000000..b5fdc33 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/alerts/AlertsFragment.kt @@ -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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/alerts/AlertsViewModel.kt b/app/src/main/java/com/example/elva/ui/alerts/AlertsViewModel.kt new file mode 100644 index 0000000..eea5bd5 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/alerts/AlertsViewModel.kt @@ -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 = _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 = emptyList() +) + diff --git a/app/src/main/java/com/example/elva/ui/auth/LoginFragment.kt b/app/src/main/java/com/example/elva/ui/auth/LoginFragment.kt new file mode 100644 index 0000000..de7408a --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/auth/LoginFragment.kt @@ -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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/auth/LoginViewModel.kt b/app/src/main/java/com/example/elva/ui/auth/LoginViewModel.kt new file mode 100644 index 0000000..ea64cf9 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/auth/LoginViewModel.kt @@ -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.Idle) + val uiState: StateFlow = _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() +} + diff --git a/app/src/main/java/com/example/elva/ui/auth/RegisterDialogFragment.kt b/app/src/main/java/com/example/elva/ui/auth/RegisterDialogFragment.kt new file mode 100644 index 0000000..3e6490a --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/auth/RegisterDialogFragment.kt @@ -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 + } +} diff --git a/app/src/main/java/com/example/elva/ui/auth/RegisterViewModel.kt b/app/src/main/java/com/example/elva/ui/auth/RegisterViewModel.kt new file mode 100644 index 0000000..7eafc7a --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/auth/RegisterViewModel.kt @@ -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.Idle) + val uiState: StateFlow = _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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/calendar/CalendarAdapter.kt b/app/src/main/java/com/example/elva/ui/calendar/CalendarAdapter.kt new file mode 100644 index 0000000..741f83c --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/calendar/CalendarAdapter.kt @@ -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(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() { + override fun areItemsTheSame(oldItem: CalendarDay, newItem: CalendarDay): Boolean { + return oldItem.date == newItem.date + } + + override fun areContentsTheSame(oldItem: CalendarDay, newItem: CalendarDay): Boolean { + return oldItem == newItem + } + } +} + diff --git a/app/src/main/java/com/example/elva/ui/calendar/CalendarFragment.kt b/app/src/main/java/com/example/elva/ui/calendar/CalendarFragment.kt new file mode 100644 index 0000000..09bd82c --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/calendar/CalendarFragment.kt @@ -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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/calendar/CalendarViewModel.kt b/app/src/main/java/com/example/elva/ui/calendar/CalendarViewModel.kt new file mode 100644 index 0000000..75dfd51 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/calendar/CalendarViewModel.kt @@ -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 = _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 = 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 +) + diff --git a/app/src/main/java/com/example/elva/ui/calendar/EditPeriodDialogFragment.kt b/app/src/main/java/com/example/elva/ui/calendar/EditPeriodDialogFragment.kt new file mode 100644 index 0000000..168350e --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/calendar/EditPeriodDialogFragment.kt @@ -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() + } +} + diff --git a/app/src/main/java/com/example/elva/ui/calendar/PeriodCalendarFragment.kt b/app/src/main/java/com/example/elva/ui/calendar/PeriodCalendarFragment.kt new file mode 100644 index 0000000..e14be80 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/calendar/PeriodCalendarFragment.kt @@ -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 { + val days = mutableListOf() + 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 +} + diff --git a/app/src/main/java/com/example/elva/ui/calendar/PeriodCalendarViewModel.kt b/app/src/main/java/com/example/elva/ui/calendar/PeriodCalendarViewModel.kt new file mode 100644 index 0000000..d1aad9a --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/calendar/PeriodCalendarViewModel.kt @@ -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 = _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 = emptyList(), + val cycleInfo: CycleInfo = CycleInfo( + lastPeriodStart = null, + nextPeriodPrediction = null, + ovulationPrediction = null + ), + val selectedDate: LocalDate? = null, + val isLoading: Boolean = true, + val error: String? = null +) + diff --git a/app/src/main/java/com/example/elva/ui/contacts/ContactsFragment.kt b/app/src/main/java/com/example/elva/ui/contacts/ContactsFragment.kt new file mode 100644 index 0000000..f831e21 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/contacts/ContactsFragment.kt @@ -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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/contacts/ContactsViewModel.kt b/app/src/main/java/com/example/elva/ui/contacts/ContactsViewModel.kt new file mode 100644 index 0000000..509b452 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/contacts/ContactsViewModel.kt @@ -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 = _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 = emptyList() +) diff --git a/app/src/main/java/com/example/elva/ui/cycle/CycleData.kt b/app/src/main/java/com/example/elva/ui/cycle/CycleData.kt new file mode 100644 index 0000000..0772efd --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/cycle/CycleData.kt @@ -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() +) + diff --git a/app/src/main/java/com/example/elva/ui/cycle/CycleFragment.kt b/app/src/main/java/com/example/elva/ui/cycle/CycleFragment.kt new file mode 100644 index 0000000..1bfa853 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/cycle/CycleFragment.kt @@ -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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/cycle/CycleViewModel.kt b/app/src/main/java/com/example/elva/ui/cycle/CycleViewModel.kt new file mode 100644 index 0000000..6eea229 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/cycle/CycleViewModel.kt @@ -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.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) + } +} + diff --git a/app/src/main/java/com/example/elva/ui/dashboard/CalendarAdapter.kt b/app/src/main/java/com/example/elva/ui/dashboard/CalendarAdapter.kt new file mode 100644 index 0000000..2d05e09 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/dashboard/CalendarAdapter.kt @@ -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(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() { + override fun areItemsTheSame(oldItem: CalendarDay, newItem: CalendarDay): Boolean { + return oldItem.day == newItem.day + } + + override fun areContentsTheSame(oldItem: CalendarDay, newItem: CalendarDay): Boolean { + return oldItem == newItem + } + } +} + diff --git a/app/src/main/java/com/example/elva/ui/dashboard/DashboardFragment.kt b/app/src/main/java/com/example/elva/ui/dashboard/DashboardFragment.kt new file mode 100644 index 0000000..7ecaecc --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/dashboard/DashboardFragment.kt @@ -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(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(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(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 + } +} + + diff --git a/app/src/main/java/com/example/elva/ui/dashboard/DashboardFragmentNew.kt b/app/src/main/java/com/example/elva/ui/dashboard/DashboardFragmentNew.kt new file mode 100644 index 0000000..ae6fbd1 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/dashboard/DashboardFragmentNew.kt @@ -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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/com/example/elva/ui/dashboard/DashboardViewModel.kt new file mode 100644 index 0000000..f4414bf --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/dashboard/DashboardViewModel.kt @@ -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(null) + val dashboardSummary: StateFlow = _dashboardSummary + + private val _isLoading = MutableStateFlow(true) + val isLoading: StateFlow = _isLoading + + private val _error = MutableStateFlow(null) + val error: StateFlow = _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() + } +} + + + diff --git a/app/src/main/java/com/example/elva/ui/dashboard/HealthViewModel.kt b/app/src/main/java/com/example/elva/ui/dashboard/HealthViewModel.kt new file mode 100644 index 0000000..7229762 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/dashboard/HealthViewModel.kt @@ -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 = _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 +) + diff --git a/app/src/main/java/com/example/elva/ui/health/CaloriesInputDialog.kt b/app/src/main/java/com/example/elva/ui/health/CaloriesInputDialog.kt new file mode 100644 index 0000000..f0380fc --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/health/CaloriesInputDialog.kt @@ -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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/health/HealthDashboardFragment.kt b/app/src/main/java/com/example/elva/ui/health/HealthDashboardFragment.kt new file mode 100644 index 0000000..89acda0 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/health/HealthDashboardFragment.kt @@ -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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/health/HealthDashboardState.kt b/app/src/main/java/com/example/elva/ui/health/HealthDashboardState.kt new file mode 100644 index 0000000..282c981 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/health/HealthDashboardState.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/ui/health/HealthDashboardViewModel.kt b/app/src/main/java/com/example/elva/ui/health/HealthDashboardViewModel.kt new file mode 100644 index 0000000..4bc3743 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/health/HealthDashboardViewModel.kt @@ -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 = _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 { + 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() { + // Навигация к календарю будет обработана во фрагменте + } +} + diff --git a/app/src/main/java/com/example/elva/ui/health/HealthFragment.kt b/app/src/main/java/com/example/elva/ui/health/HealthFragment.kt new file mode 100644 index 0000000..90476e7 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/health/HealthFragment.kt @@ -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 + } +} + diff --git a/app/src/main/java/com/example/elva/ui/health/HealthMetric.kt b/app/src/main/java/com/example/elva/ui/health/HealthMetric.kt new file mode 100644 index 0000000..e515cfc --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/health/HealthMetric.kt @@ -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 +) + diff --git a/app/src/main/java/com/example/elva/ui/health/HealthRepository.kt b/app/src/main/java/com/example/elva/ui/health/HealthRepository.kt new file mode 100644 index 0000000..0c02ba6 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/health/HealthRepository.kt @@ -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 { + val json = prefs.getString("calorie_entries", "[]") ?: "[]" + val type = object : TypeToken>() {}.type + return gson.fromJson(json, type) + } + + private fun saveCalorieEntries(entries: List) { + 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 { + val json = prefs.getString("water_entries", "[]") ?: "[]" + val type = object : TypeToken>() {}.type + return gson.fromJson(json, type) + } + + private fun saveWaterEntries(entries: List) { + 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 { + val json = prefs.getString("weight_entries", "[]") ?: "[]" + val type = object : TypeToken>() {}.type + return gson.fromJson(json, type) + } + + private fun saveWeightEntries(entries: List) { + 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 { + val json = prefs.getString("sleep_entries", "[]") ?: "[]" + val type = object : TypeToken>() {}.type + return gson.fromJson(json, type) + } + + private fun saveSleepEntries(entries: List) { + 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 { + val json = prefs.getString("health_notes", "[]") ?: "[]" + val type = object : TypeToken>() {}.type + return gson.fromJson(json, type) + } + + private fun saveNotes(notes: List) { + 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 { + val json = prefs.getString("periods", "[]") ?: "[]" + val type = object : TypeToken>() {}.type + return gson.fromJson(json, type) + } + + private fun savePeriods(periods: List) { + 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) + } +} + diff --git a/app/src/main/java/com/example/elva/ui/health/HealthViewModel.kt b/app/src/main/java/com/example/elva/ui/health/HealthViewModel.kt new file mode 100644 index 0000000..93500ab --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/health/HealthViewModel.kt @@ -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.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _selectedDate = MutableStateFlow(LocalDate.now()) + val selectedDate: StateFlow = _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() +} + diff --git a/app/src/main/java/com/example/elva/ui/health/InputDialogs.kt b/app/src/main/java/com/example/elva/ui/health/InputDialogs.kt new file mode 100644 index 0000000..fa15d35 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/health/InputDialogs.kt @@ -0,0 +1,110 @@ +package com.example.elva.ui.health + +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.example.elva.R +import com.example.elva.databinding.DialogCaloriesInputBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +class WaterInputDialog( + 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.water) + .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 + } +} + +class WeightInputDialog( + private val currentValue: Float?, + private val goal: Float?, + private val onSave: (weight: Float, goal: Float?) -> Unit +) : DialogFragment() { + + private var _binding: DialogCaloriesInputBinding? = null + private val binding get() = _binding!! + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + _binding = DialogCaloriesInputBinding.inflate(layoutInflater) + + currentValue?.let { binding.etConsumed.setText(String.format("%.1f", it)) } + goal?.let { binding.etGoal.setText(String.format("%.1f", it)) } + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.weight) + .setView(binding.root) + .setPositiveButton(R.string.save) { _, _ -> + val weight = binding.etConsumed.text.toString().toFloatOrNull() ?: 0f + val newGoal = binding.etGoal.text.toString().toFloatOrNull() + onSave(weight, newGoal) + } + .setNegativeButton(R.string.cancel, null) + .create() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + +class SleepInputDialog( + private val currentValue: Float, + private val goal: Float, + private val quality: SleepQuality, + private val onSave: (hours: Float, goal: Float, quality: SleepQuality) -> Unit +) : DialogFragment() { + + private var _binding: DialogCaloriesInputBinding? = null + private val binding get() = _binding!! + private var selectedQuality = quality + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + _binding = DialogCaloriesInputBinding.inflate(layoutInflater) + + binding.etConsumed.setText(String.format("%.1f", currentValue)) + binding.etGoal.setText(String.format("%.1f", goal)) + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.sleep) + .setView(binding.root) + .setPositiveButton(R.string.save) { _, _ -> + val hours = binding.etConsumed.text.toString().toFloatOrNull() ?: 0f + val newGoal = binding.etGoal.text.toString().toFloatOrNull() ?: goal + onSave(hours, newGoal, selectedQuality) + } + .setNegativeButton(R.string.cancel, null) + .create() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + diff --git a/app/src/main/java/com/example/elva/ui/location/LocationFragment.kt b/app/src/main/java/com/example/elva/ui/location/LocationFragment.kt new file mode 100644 index 0000000..2013a5e --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/location/LocationFragment.kt @@ -0,0 +1,39 @@ +package com.example.elva.ui.location + +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.FragmentLocationBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class LocationFragment : Fragment() { + + private var _binding: FragmentLocationBinding? = null + private val binding get() = _binding!! + private val viewModel: LocationViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentLocationBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.locationList.layoutManager = LinearLayoutManager(requireContext()) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + diff --git a/app/src/main/java/com/example/elva/ui/location/LocationViewModel.kt b/app/src/main/java/com/example/elva/ui/location/LocationViewModel.kt new file mode 100644 index 0000000..fd672ec --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/location/LocationViewModel.kt @@ -0,0 +1,8 @@ +package com.example.elva.ui.location + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class LocationViewModel @Inject constructor() : ViewModel() diff --git a/app/src/main/java/com/example/elva/ui/nutrition/NutritionFragment.kt b/app/src/main/java/com/example/elva/ui/nutrition/NutritionFragment.kt new file mode 100644 index 0000000..98962bb --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/nutrition/NutritionFragment.kt @@ -0,0 +1,40 @@ +package com.example.elva.ui.nutrition + +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.FragmentNutritionBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class NutritionFragment : Fragment() { + + private var _binding: FragmentNutritionBinding? = null + private val binding get() = _binding!! + private val viewModel: NutritionViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentNutritionBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.addNutrition.setOnClickListener { + viewModel.addNutrition(binding.nutritionDish.text.toString()) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + diff --git a/app/src/main/java/com/example/elva/ui/nutrition/NutritionViewModel.kt b/app/src/main/java/com/example/elva/ui/nutrition/NutritionViewModel.kt new file mode 100644 index 0000000..33dc5b3 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/nutrition/NutritionViewModel.kt @@ -0,0 +1,16 @@ +package com.example.elva.ui.nutrition + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +@HiltViewModel +class NutritionViewModel @Inject constructor() : ViewModel() { + fun addNutrition(dish: String) { + viewModelScope.launch { + // TODO call repository + } + } +} diff --git a/app/src/main/java/com/example/elva/ui/onboarding/OnboardingAdapter.kt b/app/src/main/java/com/example/elva/ui/onboarding/OnboardingAdapter.kt new file mode 100644 index 0000000..a9fb497 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/onboarding/OnboardingAdapter.kt @@ -0,0 +1,34 @@ +package com.example.elva.ui.onboarding + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.example.elva.R + +class OnboardingAdapter : RecyclerView.Adapter() { + + private val layouts = listOf( + R.layout.onboarding_page_1, + R.layout.onboarding_page_2, + R.layout.onboarding_page_3, + R.layout.onboarding_page_4, + R.layout.onboarding_page_5 + ) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OnboardingViewHolder { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + return OnboardingViewHolder(view) + } + + override fun onBindViewHolder(holder: OnboardingViewHolder, position: Int) { + // Layouts are already pre-designed, no binding needed + } + + override fun getItemCount(): Int = layouts.size + + override fun getItemViewType(position: Int): Int = layouts[position] + + class OnboardingViewHolder(view: View) : RecyclerView.ViewHolder(view) +} + diff --git a/app/src/main/java/com/example/elva/ui/onboarding/OnboardingFragment.kt b/app/src/main/java/com/example/elva/ui/onboarding/OnboardingFragment.kt new file mode 100644 index 0000000..002f5de --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/onboarding/OnboardingFragment.kt @@ -0,0 +1,86 @@ +package com.example.elva.ui.onboarding + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.viewpager2.widget.ViewPager2 +import com.example.elva.R +import com.example.elva.databinding.FragmentOnboardingBinding + +class OnboardingFragment : Fragment() { + private var _binding: FragmentOnboardingBinding? = null + private val binding get() = _binding!! + + private lateinit var onboardingAdapter: OnboardingAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentOnboardingBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupViewPager() + setupButtons() + } + + private fun setupViewPager() { + onboardingAdapter = OnboardingAdapter() + binding.viewPager.adapter = onboardingAdapter + + // Listen to page changes + binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + updateButtons(position) + } + }) + } + + private fun setupButtons() { + binding.tvSkip.setOnClickListener { + navigateToLogin() + } + + binding.btnNext.setOnClickListener { + val currentItem = binding.viewPager.currentItem + if (currentItem < onboardingAdapter.itemCount - 1) { + binding.viewPager.currentItem = currentItem + 1 + } else { + navigateToLogin() + } + } + } + + private fun updateButtons(position: Int) { + // Update next button text + binding.btnNext.text = if (position == onboardingAdapter.itemCount - 1) { + getString(R.string.get_started) + } else { + getString(R.string.next) + } + } + + private fun navigateToLogin() { + // Mark onboarding as completed + requireActivity().getSharedPreferences("elva_prefs", android.content.Context.MODE_PRIVATE) + .edit() + .putBoolean("onboarding_completed", true) + .apply() + + findNavController().navigate(R.id.action_onboardingFragment_to_loginFragment) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + diff --git a/app/src/main/java/com/example/elva/ui/onboarding/OnboardingPageFragment.kt b/app/src/main/java/com/example/elva/ui/onboarding/OnboardingPageFragment.kt new file mode 100644 index 0000000..f640049 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/onboarding/OnboardingPageFragment.kt @@ -0,0 +1,76 @@ +package com.example.elva.ui.onboarding + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.example.elva.databinding.FragmentOnboardingPageBinding + +class OnboardingPageFragment : Fragment() { + + private var _binding: FragmentOnboardingPageBinding? = null + private val binding get() = _binding!! + + private var pagePosition: Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + pagePosition = it.getInt(ARG_POSITION) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentOnboardingPageBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Устанавливаем контент в зависимости от позиции + when (pagePosition) { + 0 -> { + binding.textTitle.text = getString(com.example.elva.R.string.onboarding_title_1) + binding.textDescription.text = getString(com.example.elva.R.string.onboarding_description_1) + } + 1 -> { + binding.textTitle.text = getString(com.example.elva.R.string.onboarding_title_2) + binding.textDescription.text = getString(com.example.elva.R.string.onboarding_description_2) + } + 2 -> { + binding.textTitle.text = getString(com.example.elva.R.string.onboarding_title_3) + binding.textDescription.text = getString(com.example.elva.R.string.onboarding_description_3) + } + 3 -> { + binding.textTitle.text = getString(com.example.elva.R.string.onboarding_title_4) + binding.textDescription.text = getString(com.example.elva.R.string.onboarding_description_4) + } + 4 -> { + binding.textTitle.text = getString(com.example.elva.R.string.onboarding_title_5) + binding.textDescription.text = getString(com.example.elva.R.string.onboarding_description_5) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + private const val ARG_POSITION = "position" + + fun newInstance(position: Int) = OnboardingPageFragment().apply { + arguments = Bundle().apply { + putInt(ARG_POSITION, position) + } + } + } +} + diff --git a/app/src/main/java/com/example/elva/ui/onboarding/OnboardingPagerAdapter.kt b/app/src/main/java/com/example/elva/ui/onboarding/OnboardingPagerAdapter.kt new file mode 100644 index 0000000..577a066 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/onboarding/OnboardingPagerAdapter.kt @@ -0,0 +1,14 @@ +package com.example.elva.ui.onboarding + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter + +class OnboardingPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { + + override fun getItemCount(): Int = 5 + + override fun createFragment(position: Int): Fragment { + return OnboardingPageFragment.newInstance(position) + } +} + diff --git a/app/src/main/java/com/example/elva/ui/profile/ProfileFragment.kt b/app/src/main/java/com/example/elva/ui/profile/ProfileFragment.kt new file mode 100644 index 0000000..9dbe15d --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/profile/ProfileFragment.kt @@ -0,0 +1,120 @@ +package com.example.elva.ui.profile + +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.data.models.user.UserProfile +import com.example.elva.databinding.FragmentProfileBinding +import com.example.elva.util.AuthManager +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class ProfileFragment : Fragment() { + + private var _binding: FragmentProfileBinding? = null + private val binding get() = _binding!! + + private val viewModel: ProfileViewModel by viewModels() + + @Inject + lateinit var authManager: AuthManager + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfileBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + observeState() + } + + private fun setupUi() { + binding.editProfileBtn.setOnClickListener { + Toast.makeText(context, "Редактирование профиля будет доступно скоро", Toast.LENGTH_SHORT).show() + } + + binding.logoutBtn.setOnClickListener { + logout() + } + } + + private fun logout() { + lifecycleScope.launch { + authManager.logout() + findNavController().navigate(R.id.action_profileFragment_to_loginFragment) + } + } + + private fun observeState() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { state -> + when (state) { + is ProfileUiState.Loading -> { + showLoading(true) + } + is ProfileUiState.Success -> { + showLoading(false) + displayProfile(state.profile) + } + is ProfileUiState.Error -> { + showLoading(false) + Toast.makeText(context, state.message, Toast.LENGTH_LONG).show() + } + } + } + } + } + } + + private fun showLoading(isLoading: Boolean) { + binding.progressBar.isVisible = isLoading + binding.profileContent.isVisible = !isLoading + } + + private fun displayProfile(profile: UserProfile) { + with(binding) { + usernameText.text = profile.username ?: "Не указано" + emailText.text = profile.email + + val fullName = when { + !profile.fullName.isNullOrBlank() -> profile.fullName + !profile.firstName.isNullOrBlank() || !profile.lastName.isNullOrBlank() -> { + "${profile.firstName ?: ""} ${profile.lastName ?: ""}".trim() + } + else -> "Не указано" + } + fullNameText.text = fullName + + phoneText.text = profile.phoneNumber ?: profile.phone ?: "Не указано" + + locationSharingText.text = if (profile.locationSharingEnabled) "✅ Включено" else "❌ Выключено" + emergencyNotificationsText.text = if (profile.emergencyNotificationsEnabled) "✅ Включено" else "❌ Выключено" + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + diff --git a/app/src/main/java/com/example/elva/ui/profile/ProfileViewModel.kt b/app/src/main/java/com/example/elva/ui/profile/ProfileViewModel.kt new file mode 100644 index 0000000..ec69144 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/profile/ProfileViewModel.kt @@ -0,0 +1,48 @@ +package com.example.elva.ui.profile + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.elva.data.models.user.UserProfile +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 + +sealed class ProfileUiState { + object Loading : ProfileUiState() + data class Success(val profile: UserProfile) : ProfileUiState() + data class Error(val message: String) : ProfileUiState() +} + +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val userRepository: UserRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(ProfileUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadProfile() + } + + fun loadProfile() { + _uiState.value = ProfileUiState.Loading + + viewModelScope.launch { + val result = userRepository.getCurrentUser() + + result.onSuccess { profile -> + _uiState.value = ProfileUiState.Success(profile) + }.onFailure { error -> + _uiState.value = ProfileUiState.Error( + error.message ?: "Ошибка загрузки профиля" + ) + } + } + } +} + diff --git a/app/src/main/java/com/example/elva/ui/reflow/ReflowFragment.kt b/app/src/main/java/com/example/elva/ui/reflow/ReflowFragment.kt new file mode 100644 index 0000000..f13c118 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/reflow/ReflowFragment.kt @@ -0,0 +1,42 @@ +package com.example.elva.ui.reflow + +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.lifecycle.ViewModelProvider +import com.example.elva.databinding.FragmentReflowBinding + +class ReflowFragment : Fragment() { + + private var _binding: FragmentReflowBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val reflowViewModel = + ViewModelProvider(this).get(ReflowViewModel::class.java) + + _binding = FragmentReflowBinding.inflate(inflater, container, false) + val root: View = binding.root + + val textView: TextView = binding.textReflow + reflowViewModel.text.observe(viewLifecycleOwner) { + textView.text = it + } + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/elva/ui/reflow/ReflowViewModel.kt b/app/src/main/java/com/example/elva/ui/reflow/ReflowViewModel.kt new file mode 100644 index 0000000..02c552b --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/reflow/ReflowViewModel.kt @@ -0,0 +1,13 @@ +package com.example.elva.ui.reflow + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class ReflowViewModel : ViewModel() { + + private val _text = MutableLiveData().apply { + value = "This is reflow Fragment" + } + val text: LiveData = _text +} \ No newline at end of file diff --git a/app/src/main/java/com/example/elva/ui/responses/ResponsesFragment.kt b/app/src/main/java/com/example/elva/ui/responses/ResponsesFragment.kt new file mode 100644 index 0000000..eb9c65b --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/responses/ResponsesFragment.kt @@ -0,0 +1,39 @@ +package com.example.elva.ui.responses + +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.FragmentResponsesBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ResponsesFragment : Fragment() { + + private var _binding: FragmentResponsesBinding? = null + private val binding get() = _binding!! + private val viewModel: ResponsesViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentResponsesBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.responsesList.layoutManager = LinearLayoutManager(requireContext()) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + diff --git a/app/src/main/java/com/example/elva/ui/responses/ResponsesViewModel.kt b/app/src/main/java/com/example/elva/ui/responses/ResponsesViewModel.kt new file mode 100644 index 0000000..9c8e1f2 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/responses/ResponsesViewModel.kt @@ -0,0 +1,26 @@ +package com.example.elva.ui.responses + +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 ResponsesViewModel @Inject constructor() : ViewModel() { + + private val _state = MutableStateFlow(ResponsesState()) + val state: StateFlow = _state + + fun loadResponses() { + viewModelScope.launch { + _state.value = ResponsesState(items = listOf("Ответ на алерт")) + } + } +} + +data class ResponsesState( + val items: List = emptyList() +) diff --git a/app/src/main/java/com/example/elva/ui/safety/SafetyFragment.kt b/app/src/main/java/com/example/elva/ui/safety/SafetyFragment.kt new file mode 100644 index 0000000..f18b88f --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/safety/SafetyFragment.kt @@ -0,0 +1,39 @@ +package com.example.elva.ui.safety + +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.FragmentSafetyBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SafetyFragment : Fragment() { + + private var _binding: FragmentSafetyBinding? = null + private val binding get() = _binding!! + private val viewModel: SafetyViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSafetyBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.safetyList.layoutManager = LinearLayoutManager(requireContext()) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + diff --git a/app/src/main/java/com/example/elva/ui/safety/SafetyViewModel.kt b/app/src/main/java/com/example/elva/ui/safety/SafetyViewModel.kt new file mode 100644 index 0000000..9c260d1 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/safety/SafetyViewModel.kt @@ -0,0 +1,8 @@ +package com.example.elva.ui.safety + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class SafetyViewModel @Inject constructor() : ViewModel() diff --git a/app/src/main/java/com/example/elva/ui/settings/SettingsFragment.kt b/app/src/main/java/com/example/elva/ui/settings/SettingsFragment.kt new file mode 100644 index 0000000..b9a56bc --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/settings/SettingsFragment.kt @@ -0,0 +1,42 @@ +package com.example.elva.ui.settings + +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.lifecycle.ViewModelProvider +import com.example.elva.databinding.FragmentSettingsBinding + +class SettingsFragment : Fragment() { + + private var _binding: FragmentSettingsBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val settingsViewModel = + ViewModelProvider(this).get(SettingsViewModel::class.java) + + _binding = FragmentSettingsBinding.inflate(inflater, container, false) + val root: View = binding.root + + val textView: TextView = binding.textSettings + settingsViewModel.text.observe(viewLifecycleOwner) { + textView.text = it + } + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/elva/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/example/elva/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000..6aa9ea2 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/settings/SettingsViewModel.kt @@ -0,0 +1,13 @@ +package com.example.elva.ui.settings + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class SettingsViewModel : ViewModel() { + + private val _text = MutableLiveData().apply { + value = "This is settings Fragment" + } + val text: LiveData = _text +} \ No newline at end of file diff --git a/app/src/main/java/com/example/elva/ui/slideshow/SlideshowFragment.kt b/app/src/main/java/com/example/elva/ui/slideshow/SlideshowFragment.kt new file mode 100644 index 0000000..0745359 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/slideshow/SlideshowFragment.kt @@ -0,0 +1,42 @@ +package com.example.elva.ui.slideshow + +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.lifecycle.ViewModelProvider +import com.example.elva.databinding.FragmentSlideshowBinding + +class SlideshowFragment : Fragment() { + + private var _binding: FragmentSlideshowBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val slideshowViewModel = + ViewModelProvider(this).get(SlideshowViewModel::class.java) + + _binding = FragmentSlideshowBinding.inflate(inflater, container, false) + val root: View = binding.root + + val textView: TextView = binding.textSlideshow + slideshowViewModel.text.observe(viewLifecycleOwner) { + textView.text = it + } + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/elva/ui/slideshow/SlideshowViewModel.kt b/app/src/main/java/com/example/elva/ui/slideshow/SlideshowViewModel.kt new file mode 100644 index 0000000..ecacbd8 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/slideshow/SlideshowViewModel.kt @@ -0,0 +1,13 @@ +package com.example.elva.ui.slideshow + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class SlideshowViewModel : ViewModel() { + + private val _text = MutableLiveData().apply { + value = "This is slideshow Fragment" + } + val text: LiveData = _text +} \ No newline at end of file diff --git a/app/src/main/java/com/example/elva/ui/sos/SosFragment.kt b/app/src/main/java/com/example/elva/ui/sos/SosFragment.kt new file mode 100644 index 0000000..78a706e --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/sos/SosFragment.kt @@ -0,0 +1,55 @@ +package com.example.elva.ui.sos + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.example.elva.databinding.FragmentSosBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SosFragment : Fragment() { + + private var _binding: FragmentSosBinding? = null + private val binding get() = _binding!! + private val viewModel: SosViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSosBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val adapter = ArrayAdapter.createFromResource( + requireContext(), + com.example.elva.R.array.sos_types, + android.R.layout.simple_spinner_item + ) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.sosType.adapter = adapter + + binding.sosSubmit.setOnClickListener { + viewModel.sendAlert( + type = binding.sosType.selectedItem.toString(), + latitude = binding.sosLat.text.toString(), + longitude = binding.sosLon.text.toString(), + address = binding.sosAddress.text.toString(), + message = binding.sosMessage.text.toString() + ) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + diff --git a/app/src/main/java/com/example/elva/ui/sos/SosViewModel.kt b/app/src/main/java/com/example/elva/ui/sos/SosViewModel.kt new file mode 100644 index 0000000..e67a44b --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/sos/SosViewModel.kt @@ -0,0 +1,32 @@ +package com.example.elva.ui.sos + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.elva.data.remote.model.CreateAlertRequest +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +@HiltViewModel +class SosViewModel @Inject constructor() : ViewModel() { + + fun sendAlert( + type: String, + latitude: String, + longitude: String, + address: String, + message: String + ) { + viewModelScope.launch { + // TODO connect to EmergencyRepository + val request = CreateAlertRequest( + alertType = type, + latitude = latitude.toDoubleOrNull() ?: 0.0, + longitude = longitude.toDoubleOrNull() ?: 0.0, + address = address, + message = message + ) + } + } +} + diff --git a/app/src/main/java/com/example/elva/ui/transform/TransformFragment.kt b/app/src/main/java/com/example/elva/ui/transform/TransformFragment.kt new file mode 100644 index 0000000..394cdef --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/transform/TransformFragment.kt @@ -0,0 +1,104 @@ +package com.example.elva.ui.transform + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +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.FragmentTransformBinding +import com.example.elva.databinding.ItemTransformBinding + +/** + * Fragment that demonstrates a responsive layout pattern where the format of the content + * transforms depending on the size of the screen. Specifically this Fragment shows items in + * the [RecyclerView] using LinearLayoutManager in a small screen + * and shows items using GridLayoutManager in a large screen. + */ +class TransformFragment : Fragment() { + + private var _binding: FragmentTransformBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val transformViewModel = ViewModelProvider(this).get(TransformViewModel::class.java) + _binding = FragmentTransformBinding.inflate(inflater, container, false) + val root: View = binding.root + + val recyclerView = binding.recyclerviewTransform + val adapter = TransformAdapter() + recyclerView.adapter = adapter + transformViewModel.texts.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + class TransformAdapter : + ListAdapter(object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: String, newItem: String): Boolean = + oldItem == newItem + + override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = + oldItem == newItem + }) { + + private val drawables = listOf( + R.drawable.avatar_1, + R.drawable.avatar_2, + R.drawable.avatar_3, + R.drawable.avatar_4, + R.drawable.avatar_5, + R.drawable.avatar_6, + R.drawable.avatar_7, + R.drawable.avatar_8, + R.drawable.avatar_9, + R.drawable.avatar_10, + R.drawable.avatar_11, + R.drawable.avatar_12, + R.drawable.avatar_13, + R.drawable.avatar_14, + R.drawable.avatar_15, + R.drawable.avatar_16, + ) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransformViewHolder { + val binding = ItemTransformBinding.inflate(LayoutInflater.from(parent.context)) + return TransformViewHolder(binding) + } + + override fun onBindViewHolder(holder: TransformViewHolder, position: Int) { + holder.textView.text = getItem(position) + holder.imageView.setImageDrawable( + ResourcesCompat.getDrawable(holder.imageView.resources, drawables[position], null) + ) + } + } + + class TransformViewHolder(binding: ItemTransformBinding) : + RecyclerView.ViewHolder(binding.root) { + + val imageView: ImageView = binding.imageViewItemTransform + val textView: TextView = binding.textViewItemTransform + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/elva/ui/transform/TransformViewModel.kt b/app/src/main/java/com/example/elva/ui/transform/TransformViewModel.kt new file mode 100644 index 0000000..78af617 --- /dev/null +++ b/app/src/main/java/com/example/elva/ui/transform/TransformViewModel.kt @@ -0,0 +1,16 @@ +package com.example.elva.ui.transform + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class TransformViewModel : ViewModel() { + + private val _texts = MutableLiveData>().apply { + value = (1..16).mapIndexed { _, i -> + "This is item # $i" + } + } + + val texts: LiveData> = _texts +} \ No newline at end of file diff --git a/app/src/main/java/com/example/elva/util/AuthManager.kt b/app/src/main/java/com/example/elva/util/AuthManager.kt new file mode 100644 index 0000000..a72dfcf --- /dev/null +++ b/app/src/main/java/com/example/elva/util/AuthManager.kt @@ -0,0 +1,152 @@ +package com.example.elva.util + +import android.content.Context +import android.util.Base64 +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import dagger.hilt.android.qualifiers.ApplicationContext +import org.json.JSONObject +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AuthManager @Inject constructor( + @ApplicationContext private val context: Context +) { + + private val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + private val prefs = EncryptedSharedPreferences.create( + context, + "auth_prefs", + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + companion object { + private const val KEY_ACCESS_TOKEN = "access_token" + private const val KEY_USER_ID = "user_id" + private const val KEY_USER_EMAIL = "user_email" + private const val KEY_USER_USERNAME = "user_username" + private const val KEY_USER_UUID = "user_uuid" + } + + fun saveAuthData( + token: String, + userId: Int, + email: String, + username: String, + uuid: String = "" + ) { + android.util.Log.d("AuthManager", "saveAuthData called with: token=${token.take(20)}..., userId=$userId, email=$email, username=$username, uuid=$uuid") + try { + val success = prefs.edit() + .putString(KEY_ACCESS_TOKEN, token) + .putInt(KEY_USER_ID, userId) + .putString(KEY_USER_EMAIL, email) + .putString(KEY_USER_USERNAME, username) + .putString(KEY_USER_UUID, uuid) + .commit() // Используем commit() для синхронного сохранения + + android.util.Log.d("AuthManager", "SharedPreferences commit result: $success") + + // Проверяем что данные сохранились + val savedToken = prefs.getString(KEY_ACCESS_TOKEN, null) + val savedUserId = prefs.getInt(KEY_USER_ID, -1) + val savedEmail = prefs.getString(KEY_USER_EMAIL, null) + val savedUsername = prefs.getString(KEY_USER_USERNAME, null) + + android.util.Log.d("AuthManager", "Verification after save:") + android.util.Log.d("AuthManager", " - token: ${if(savedToken != null) "exists(${savedToken.take(20)}...)" else "null"}") + android.util.Log.d("AuthManager", " - userId: $savedUserId") + android.util.Log.d("AuthManager", " - email: $savedEmail") + android.util.Log.d("AuthManager", " - username: $savedUsername") + + if (savedToken != null && savedToken == token) { + android.util.Log.d("AuthManager", "saveAuthData: Data saved and verified successfully") + } else { + android.util.Log.e("AuthManager", "saveAuthData: WARNING - Token verification failed!") + android.util.Log.e("AuthManager", " Expected: ${token.take(20)}...") + android.util.Log.e("AuthManager", " Got: ${savedToken?.take(20) ?: "null"}") + } + } catch (e: Exception) { + android.util.Log.e("AuthManager", "Error saving auth data", e) + } + } + + // Вспомогательные методы для сохранения данных по отдельности + fun saveAuthToken(token: String) { + prefs.edit() + .putString(KEY_ACCESS_TOKEN, token) + .apply() + } + + fun saveUserId(userId: Int) { + prefs.edit() + .putInt(KEY_USER_ID, userId) + .apply() + } + + fun saveUserEmail(email: String) { + prefs.edit() + .putString(KEY_USER_EMAIL, email) + .apply() + } + + fun saveUserUsername(username: String) { + prefs.edit() + .putString(KEY_USER_USERNAME, username) + .apply() + } + + fun saveUserUuid(uuid: String) { + prefs.edit() + .putString(KEY_USER_UUID, uuid) + .apply() + } + + fun getAccessToken(): String? = prefs.getString(KEY_ACCESS_TOKEN, null) + + fun getUserId(): Int = prefs.getInt(KEY_USER_ID, -1) + + fun getUserEmail(): String? = prefs.getString(KEY_USER_EMAIL, null) + + fun getUserUsername(): String? = prefs.getString(KEY_USER_USERNAME, null) + + fun getUserUuid(): String? = prefs.getString(KEY_USER_UUID, null) + + fun isLoggedIn(): Boolean { + val token = getAccessToken() + val expired = isTokenExpired() + val result = token != null && !expired + android.util.Log.d("AuthManager", "isLoggedIn: token=${if(token != null) "exists(${token.take(20)}...)" else "null"}, expired=$expired, result=$result") + if (token != null) { + android.util.Log.d("AuthManager", "Token details: userId=${getUserId()}, email=${getUserEmail()}, username=${getUserUsername()}") + } + return result + } + + fun isTokenExpired(): Boolean { + val token = getAccessToken() ?: return true + return try { + val parts = token.split(".") + if (parts.size != 3) return true + + val payload = String(Base64.decode(parts[1], Base64.URL_SAFE)) + val json = JSONObject(payload) + val exp = json.getLong("exp") + + exp < System.currentTimeMillis() / 1000 + } catch (e: Exception) { + true + } + } + + fun logout() { + prefs.edit().clear().apply() + } +} + diff --git a/app/src/main/res/anim/no_change.xml b/app/src/main/res/anim/no_change.xml new file mode 100644 index 0000000..b4e7c99 --- /dev/null +++ b/app/src/main/res/anim/no_change.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/anim/slide_down_to_bottom.xml b/app/src/main/res/anim/slide_down_to_bottom.xml new file mode 100644 index 0000000..24ee9d3 --- /dev/null +++ b/app/src/main/res/anim/slide_down_to_bottom.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/src/main/res/anim/slide_in_bottom.xml b/app/src/main/res/anim/slide_in_bottom.xml new file mode 100644 index 0000000..5e6e1a4 --- /dev/null +++ b/app/src/main/res/anim/slide_in_bottom.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 0000000..1f09f9c --- /dev/null +++ b/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..5c29641 --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/anim/slide_out_bottom.xml b/app/src/main/res/anim/slide_out_bottom.xml new file mode 100644 index 0000000..e745127 --- /dev/null +++ b/app/src/main/res/anim/slide_out_bottom.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..0f20138 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000..7f10938 --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/anim/slide_up_from_bottom.xml b/app/src/main/res/anim/slide_up_from_bottom.xml new file mode 100644 index 0000000..28aeff7 --- /dev/null +++ b/app/src/main/res/anim/slide_up_from_bottom.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_1.xml b/app/src/main/res/drawable/avatar_1.xml new file mode 100644 index 0000000..e7ad1c6 --- /dev/null +++ b/app/src/main/res/drawable/avatar_1.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_10.xml b/app/src/main/res/drawable/avatar_10.xml new file mode 100644 index 0000000..cda140f --- /dev/null +++ b/app/src/main/res/drawable/avatar_10.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_11.xml b/app/src/main/res/drawable/avatar_11.xml new file mode 100644 index 0000000..8658f1a --- /dev/null +++ b/app/src/main/res/drawable/avatar_11.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_12.xml b/app/src/main/res/drawable/avatar_12.xml new file mode 100644 index 0000000..28b3196 --- /dev/null +++ b/app/src/main/res/drawable/avatar_12.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_13.xml b/app/src/main/res/drawable/avatar_13.xml new file mode 100644 index 0000000..7883220 --- /dev/null +++ b/app/src/main/res/drawable/avatar_13.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_14.xml b/app/src/main/res/drawable/avatar_14.xml new file mode 100644 index 0000000..4edeecb --- /dev/null +++ b/app/src/main/res/drawable/avatar_14.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_15.xml b/app/src/main/res/drawable/avatar_15.xml new file mode 100644 index 0000000..03b220d --- /dev/null +++ b/app/src/main/res/drawable/avatar_15.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_16.xml b/app/src/main/res/drawable/avatar_16.xml new file mode 100644 index 0000000..a359c02 --- /dev/null +++ b/app/src/main/res/drawable/avatar_16.xml @@ -0,0 +1,37 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_2.xml b/app/src/main/res/drawable/avatar_2.xml new file mode 100644 index 0000000..615cc96 --- /dev/null +++ b/app/src/main/res/drawable/avatar_2.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_3.xml b/app/src/main/res/drawable/avatar_3.xml new file mode 100644 index 0000000..f3e1c56 --- /dev/null +++ b/app/src/main/res/drawable/avatar_3.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_4.xml b/app/src/main/res/drawable/avatar_4.xml new file mode 100644 index 0000000..d4dd501 --- /dev/null +++ b/app/src/main/res/drawable/avatar_4.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_5.xml b/app/src/main/res/drawable/avatar_5.xml new file mode 100644 index 0000000..9b9522d --- /dev/null +++ b/app/src/main/res/drawable/avatar_5.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_6.xml b/app/src/main/res/drawable/avatar_6.xml new file mode 100644 index 0000000..068ee90 --- /dev/null +++ b/app/src/main/res/drawable/avatar_6.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_7.xml b/app/src/main/res/drawable/avatar_7.xml new file mode 100644 index 0000000..13b1154 --- /dev/null +++ b/app/src/main/res/drawable/avatar_7.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_8.xml b/app/src/main/res/drawable/avatar_8.xml new file mode 100644 index 0000000..b63105f --- /dev/null +++ b/app/src/main/res/drawable/avatar_8.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/avatar_9.xml b/app/src/main/res/drawable/avatar_9.xml new file mode 100644 index 0000000..dddf848 --- /dev/null +++ b/app/src/main/res/drawable/avatar_9.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_button_primary.xml b/app/src/main/res/drawable/bg_button_primary.xml new file mode 100644 index 0000000..4453144 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_primary.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_button_secondary.xml b/app/src/main/res/drawable/bg_button_secondary.xml new file mode 100644 index 0000000..eeb67be --- /dev/null +++ b/app/src/main/res/drawable/bg_button_secondary.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_day_ovulation.xml b/app/src/main/res/drawable/bg_calendar_day_ovulation.xml new file mode 100644 index 0000000..bbb9e48 --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_day_ovulation.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_day_period.xml b/app/src/main/res/drawable/bg_calendar_day_period.xml new file mode 100644 index 0000000..5009ca9 --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_day_period.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_day_predicted.xml b/app/src/main/res/drawable/bg_calendar_day_predicted.xml new file mode 100644 index 0000000..05dc53f --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_day_predicted.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_day_today.xml b/app/src/main/res/drawable/bg_calendar_day_today.xml new file mode 100644 index 0000000..0320823 --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_day_today.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_fertile.xml b/app/src/main/res/drawable/bg_calendar_fertile.xml new file mode 100644 index 0000000..da37018 --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_fertile.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_ovulation.xml b/app/src/main/res/drawable/bg_calendar_ovulation.xml new file mode 100644 index 0000000..2a58ff2 --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_ovulation.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_period.xml b/app/src/main/res/drawable/bg_calendar_period.xml new file mode 100644 index 0000000..f45c5fd --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_period.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_predicted.xml b/app/src/main/res/drawable/bg_calendar_predicted.xml new file mode 100644 index 0000000..76235b7 --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_predicted.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_selected.xml b/app/src/main/res/drawable/bg_calendar_selected.xml new file mode 100644 index 0000000..d958491 --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_selected.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_today.xml b/app/src/main/res/drawable/bg_calendar_today.xml new file mode 100644 index 0000000..d958491 --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_today.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_card.xml b/app/src/main/res/drawable/bg_card.xml new file mode 100644 index 0000000..6363495 --- /dev/null +++ b/app/src/main/res/drawable/bg_card.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/app/src/main/res/drawable/bg_gradient_auth.xml b/app/src/main/res/drawable/bg_gradient_auth.xml new file mode 100644 index 0000000..5dc29ee --- /dev/null +++ b/app/src/main/res/drawable/bg_gradient_auth.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_gradient_intro.xml b/app/src/main/res/drawable/bg_gradient_intro.xml new file mode 100644 index 0000000..d8e6050 --- /dev/null +++ b/app/src/main/res/drawable/bg_gradient_intro.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_gradient_main.xml b/app/src/main/res/drawable/bg_gradient_main.xml new file mode 100644 index 0000000..b8cd1f4 --- /dev/null +++ b/app/src/main/res/drawable/bg_gradient_main.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_input_field.xml b/app/src/main/res/drawable/bg_input_field.xml new file mode 100644 index 0000000..bc0a5dc --- /dev/null +++ b/app/src/main/res/drawable/bg_input_field.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/bg_input_field_focused.xml b/app/src/main/res/drawable/bg_input_field_focused.xml new file mode 100644 index 0000000..1e853d6 --- /dev/null +++ b/app/src/main/res/drawable/bg_input_field_focused.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/bg_input_selector.xml b/app/src/main/res/drawable/bg_input_selector.xml new file mode 100644 index 0000000..e14ff32 --- /dev/null +++ b/app/src/main/res/drawable/bg_input_selector.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_kcal_badge.xml b/app/src/main/res/drawable/bg_kcal_badge.xml new file mode 100644 index 0000000..b5ae6b0 --- /dev/null +++ b/app/src/main/res/drawable/bg_kcal_badge.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_login_card.xml b/app/src/main/res/drawable/bg_login_card.xml new file mode 100644 index 0000000..8178bd8 --- /dev/null +++ b/app/src/main/res/drawable/bg_login_card.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_register_card.xml b/app/src/main/res/drawable/bg_register_card.xml new file mode 100644 index 0000000..8178bd8 --- /dev/null +++ b/app/src/main/res/drawable/bg_register_card.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_sos_button.xml b/app/src/main/res/drawable/bg_sos_button.xml new file mode 100644 index 0000000..929a2db --- /dev/null +++ b/app/src/main/res/drawable/bg_sos_button.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/cycle_circle_background.xml b/app/src/main/res/drawable/cycle_circle_background.xml new file mode 100644 index 0000000..66a1c97 --- /dev/null +++ b/app/src/main/res/drawable/cycle_circle_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/cycle_progress_arc.xml b/app/src/main/res/drawable/cycle_progress_arc.xml new file mode 100644 index 0000000..bbafe82 --- /dev/null +++ b/app/src/main/res/drawable/cycle_progress_arc.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/day_circle_current.xml b/app/src/main/res/drawable/day_circle_current.xml new file mode 100644 index 0000000..f6a323b --- /dev/null +++ b/app/src/main/res/drawable/day_circle_current.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/day_circle_default.xml b/app/src/main/res/drawable/day_circle_default.xml new file mode 100644 index 0000000..82383ad --- /dev/null +++ b/app/src/main/res/drawable/day_circle_default.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/day_circle_fertile.xml b/app/src/main/res/drawable/day_circle_fertile.xml new file mode 100644 index 0000000..82383ad --- /dev/null +++ b/app/src/main/res/drawable/day_circle_fertile.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/day_circle_ovulation.xml b/app/src/main/res/drawable/day_circle_ovulation.xml new file mode 100644 index 0000000..e6e171e --- /dev/null +++ b/app/src/main/res/drawable/day_circle_ovulation.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/day_circle_period.xml b/app/src/main/res/drawable/day_circle_period.xml new file mode 100644 index 0000000..5009ca9 --- /dev/null +++ b/app/src/main/res/drawable/day_circle_period.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/day_circle_today.xml b/app/src/main/res/drawable/day_circle_today.xml new file mode 100644 index 0000000..5009ca9 --- /dev/null +++ b/app/src/main/res/drawable/day_circle_today.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/dialog_background.xml b/app/src/main/res/drawable/dialog_background.xml new file mode 100644 index 0000000..aba2b8d --- /dev/null +++ b/app/src/main/res/drawable/dialog_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/dot_selected.xml b/app/src/main/res/drawable/dot_selected.xml new file mode 100644 index 0000000..ae13a9c --- /dev/null +++ b/app/src/main/res/drawable/dot_selected.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/app/src/main/res/drawable/dot_unselected.xml b/app/src/main/res/drawable/dot_unselected.xml new file mode 100644 index 0000000..77a137e --- /dev/null +++ b/app/src/main/res/drawable/dot_unselected.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/elva_logo.xml b/app/src/main/res/drawable/elva_logo.xml new file mode 100644 index 0000000..b172b93 --- /dev/null +++ b/app/src/main/res/drawable/elva_logo.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/gradient_background.xml b/app/src/main/res/drawable/gradient_background.xml new file mode 100644 index 0000000..8a86be8 --- /dev/null +++ b/app/src/main/res/drawable/gradient_background.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..50fd170 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_app_logo.xml b/app/src/main/res/drawable/ic_app_logo.xml new file mode 100644 index 0000000..6e94b41 --- /dev/null +++ b/app/src/main/res/drawable/ic_app_logo.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..fb7d311 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000..459e606 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_calendar.xml b/app/src/main/res/drawable/ic_calendar.xml new file mode 100644 index 0000000..faa3c21 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_calories.xml b/app/src/main/res/drawable/ic_calories.xml new file mode 100644 index 0000000..d7e8b65 --- /dev/null +++ b/app/src/main/res/drawable/ic_calories.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_camera_black_24dp.xml b/app/src/main/res/drawable/ic_camera_black_24dp.xml new file mode 100644 index 0000000..634fe92 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera_black_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_chart.xml b/app/src/main/res/drawable/ic_chart.xml new file mode 100644 index 0000000..f5f3b32 --- /dev/null +++ b/app/src/main/res/drawable/ic_chart.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_chat.xml b/app/src/main/res/drawable/ic_chat.xml new file mode 100644 index 0000000..851366e --- /dev/null +++ b/app/src/main/res/drawable/ic_chat.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_chat_new.xml b/app/src/main/res/drawable/ic_chat_new.xml new file mode 100644 index 0000000..2760f1a --- /dev/null +++ b/app/src/main/res/drawable/ic_chat_new.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_chevron_left.xml b/app/src/main/res/drawable/ic_chevron_left.xml new file mode 100644 index 0000000..9c8c5c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_left.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml new file mode 100644 index 0000000..e826351 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cycle.xml b/app/src/main/res/drawable/ic_cycle.xml new file mode 100644 index 0000000..92960dc --- /dev/null +++ b/app/src/main/res/drawable/ic_cycle.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_food.xml b/app/src/main/res/drawable/ic_food.xml new file mode 100644 index 0000000..2b555d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_food.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_gallery_black_24dp.xml b/app/src/main/res/drawable/ic_gallery_black_24dp.xml new file mode 100644 index 0000000..03c7709 --- /dev/null +++ b/app/src/main/res/drawable/ic_gallery_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_google.xml b/app/src/main/res/drawable/ic_google.xml new file mode 100644 index 0000000..999cde1 --- /dev/null +++ b/app/src/main/res/drawable/ic_google.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_health.xml b/app/src/main/res/drawable/ic_health.xml new file mode 100644 index 0000000..75ea12e --- /dev/null +++ b/app/src/main/res/drawable/ic_health.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_heart_drop.xml b/app/src/main/res/drawable/ic_heart_drop.xml new file mode 100644 index 0000000..02bfa97 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_drop.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_heart_filled.xml b/app/src/main/res/drawable/ic_heart_filled.xml new file mode 100644 index 0000000..abda8af --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_filled.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..f86816a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..9f585ea --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_mood_blood.xml b/app/src/main/res/drawable/ic_mood_blood.xml new file mode 100644 index 0000000..bd21391 --- /dev/null +++ b/app/src/main/res/drawable/ic_mood_blood.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mood_happy.xml b/app/src/main/res/drawable/ic_mood_happy.xml new file mode 100644 index 0000000..1adcb77 --- /dev/null +++ b/app/src/main/res/drawable/ic_mood_happy.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mood_smile.xml b/app/src/main/res/drawable/ic_mood_smile.xml new file mode 100644 index 0000000..e0cc9dd --- /dev/null +++ b/app/src/main/res/drawable/ic_mood_smile.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_mood_tracker.xml b/app/src/main/res/drawable/ic_mood_tracker.xml new file mode 100644 index 0000000..844c60d --- /dev/null +++ b/app/src/main/res/drawable/ic_mood_tracker.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_nav_cycle.xml b/app/src/main/res/drawable/ic_nav_cycle.xml new file mode 100644 index 0000000..73a5ac3 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_cycle.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_nutrition.xml b/app/src/main/res/drawable/ic_nutrition.xml new file mode 100644 index 0000000..1d483b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_nutrition.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_profile.xml b/app/src/main/res/drawable/ic_profile.xml new file mode 100644 index 0000000..1e9fff0 --- /dev/null +++ b/app/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_settings_black_24dp.xml b/app/src/main/res/drawable/ic_settings_black_24dp.xml new file mode 100644 index 0000000..a7c7678 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_black_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sleep.xml b/app/src/main/res/drawable/ic_sleep.xml new file mode 100644 index 0000000..56d544f --- /dev/null +++ b/app/src/main/res/drawable/ic_sleep.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_slideshow_black_24dp.xml b/app/src/main/res/drawable/ic_slideshow_black_24dp.xml new file mode 100644 index 0000000..5e9e163 --- /dev/null +++ b/app/src/main/res/drawable/ic_slideshow_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sos_icon.xml b/app/src/main/res/drawable/ic_sos_icon.xml new file mode 100644 index 0000000..8f5de69 --- /dev/null +++ b/app/src/main/res/drawable/ic_sos_icon.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_sos_white.xml b/app/src/main/res/drawable/ic_sos_white.xml new file mode 100644 index 0000000..e940d10 --- /dev/null +++ b/app/src/main/res/drawable/ic_sos_white.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_tip.xml b/app/src/main/res/drawable/ic_tip.xml new file mode 100644 index 0000000..6c6b7d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_tip.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_water.xml b/app/src/main/res/drawable/ic_water.xml new file mode 100644 index 0000000..9213d82 --- /dev/null +++ b/app/src/main/res/drawable/ic_water.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_weight.xml b/app/src/main/res/drawable/ic_weight.xml new file mode 100644 index 0000000..b5dd2ad --- /dev/null +++ b/app/src/main/res/drawable/ic_weight.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/legend_fertile.xml b/app/src/main/res/drawable/legend_fertile.xml new file mode 100644 index 0000000..82383ad --- /dev/null +++ b/app/src/main/res/drawable/legend_fertile.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/legend_ovulation.xml b/app/src/main/res/drawable/legend_ovulation.xml new file mode 100644 index 0000000..e6e171e --- /dev/null +++ b/app/src/main/res/drawable/legend_ovulation.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/legend_period.xml b/app/src/main/res/drawable/legend_period.xml new file mode 100644 index 0000000..5009ca9 --- /dev/null +++ b/app/src/main/res/drawable/legend_period.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/onboarding_1.xml b/app/src/main/res/drawable/onboarding_1.xml new file mode 100644 index 0000000..3ddc47e --- /dev/null +++ b/app/src/main/res/drawable/onboarding_1.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/period_circle_background.xml b/app/src/main/res/drawable/period_circle_background.xml new file mode 100644 index 0000000..5009ca9 --- /dev/null +++ b/app/src/main/res/drawable/period_circle_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/progress_bar.xml b/app/src/main/res/drawable/progress_bar.xml new file mode 100644 index 0000000..8ea2284 --- /dev/null +++ b/app/src/main/res/drawable/progress_bar.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/progress_rounded.xml b/app/src/main/res/drawable/progress_rounded.xml new file mode 100644 index 0000000..fae3659 --- /dev/null +++ b/app/src/main/res/drawable/progress_rounded.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/side_nav_bar.xml b/app/src/main/res/drawable/side_nav_bar.xml new file mode 100644 index 0000000..6d81870 --- /dev/null +++ b/app/src/main/res/drawable/side_nav_bar.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_selected.xml b/app/src/main/res/drawable/tab_selected.xml new file mode 100644 index 0000000..7dc3ebf --- /dev/null +++ b/app/src/main/res/drawable/tab_selected.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/tab_selector.xml b/app/src/main/res/drawable/tab_selector.xml new file mode 100644 index 0000000..9876886 --- /dev/null +++ b/app/src/main/res/drawable/tab_selector.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tab_unselected.xml b/app/src/main/res/drawable/tab_unselected.xml new file mode 100644 index 0000000..e6511e6 --- /dev/null +++ b/app/src/main/res/drawable/tab_unselected.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout-w1240dp/activity_main.xml b/app/src/main/res/layout-w1240dp/activity_main.xml new file mode 100644 index 0000000..62d4507 --- /dev/null +++ b/app/src/main/res/layout-w1240dp/activity_main.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/layout-w1240dp/app_bar_main.xml b/app/src/main/res/layout-w1240dp/app_bar_main.xml new file mode 100644 index 0000000..b42ece1 --- /dev/null +++ b/app/src/main/res/layout-w1240dp/app_bar_main.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/app/src/main/res/layout-w1240dp/content_main.xml b/app/src/main/res/layout-w1240dp/content_main.xml new file mode 100644 index 0000000..4a865a2 --- /dev/null +++ b/app/src/main/res/layout-w1240dp/content_main.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp/activity_main.xml b/app/src/main/res/layout-w600dp/activity_main.xml new file mode 100644 index 0000000..54d27d3 --- /dev/null +++ b/app/src/main/res/layout-w600dp/activity_main.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/layout-w600dp/app_bar_main.xml b/app/src/main/res/layout-w600dp/app_bar_main.xml new file mode 100644 index 0000000..b42ece1 --- /dev/null +++ b/app/src/main/res/layout-w600dp/app_bar_main.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/app/src/main/res/layout-w600dp/content_main.xml b/app/src/main/res/layout-w600dp/content_main.xml new file mode 100644 index 0000000..438772e --- /dev/null +++ b/app/src/main/res/layout-w600dp/content_main.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp/fragment_transform.xml b/app/src/main/res/layout-w600dp/fragment_transform.xml new file mode 100644 index 0000000..a091107 --- /dev/null +++ b/app/src/main/res/layout-w600dp/fragment_transform.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp/item_transform.xml b/app/src/main/res/layout-w600dp/item_transform.xml new file mode 100644 index 0000000..0d7b771 --- /dev/null +++ b/app/src/main/res/layout-w600dp/item_transform.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..1647395 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml new file mode 100644 index 0000000..b42ece1 --- /dev/null +++ b/app/src/main/res/layout/app_bar_main.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/app/src/main/res/layout/bottom_navigation_bar.xml b/app/src/main/res/layout/bottom_navigation_bar.xml new file mode 100644 index 0000000..9e6e7da --- /dev/null +++ b/app/src/main/res/layout/bottom_navigation_bar.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..29efa85 --- /dev/null +++ b/app/src/main/res/layout/content_main.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_add_water.xml b/app/src/main/res/layout/dialog_add_water.xml new file mode 100644 index 0000000..c6ed7c1 --- /dev/null +++ b/app/src/main/res/layout/dialog_add_water.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_calories_input.xml b/app/src/main/res/layout/dialog_calories_input.xml new file mode 100644 index 0000000..504f9ab --- /dev/null +++ b/app/src/main/res/layout/dialog_calories_input.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_edit_period.xml b/app/src/main/res/layout/dialog_edit_period.xml new file mode 100644 index 0000000..a458039 --- /dev/null +++ b/app/src/main/res/layout/dialog_edit_period.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_input.xml b/app/src/main/res/layout/dialog_input.xml new file mode 100644 index 0000000..0e3fc1b --- /dev/null +++ b/app/src/main/res/layout/dialog_input.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_register.xml b/app/src/main/res/layout/dialog_register.xml new file mode 100644 index 0000000..68851d8 --- /dev/null +++ b/app/src/main/res/layout/dialog_register.xml @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +