commit d710525123097a47c1de88a30526d30e081a818c
Author: MaximOksiuta <63787095+MaximOksiuta@users.noreply.github.com>
Date: Fri Nov 21 13:19:14 2025 +0300
Initial with template
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..a0d2490
--- /dev/null
+++ b/.idea/appInsightsSettings.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
\ 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/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ 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/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..7061a0d
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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/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/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..91bc919
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,201 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.jetbrainsKotlinAndroid)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.googleKsp)
+ alias(libs.plugins.room)
+ alias(libs.plugins.serialization)
+ alias(libs.plugins.secrets.gradle.plugin)
+ alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.google.services.gmc)
+ alias(libs.plugins.firebase.crashlytics)
+ alias(libs.plugins.kotzilla)
+}
+
+android {
+ namespace = "com.prodhack.moscow2025"
+ compileSdk {
+ version = release(36)
+ }
+
+ defaultConfig {
+ applicationId = "com.prodhack.moscow2025"
+ minSdk = 29
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ room {
+ schemaDirectory("$projectDir/schemas")
+ }
+
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ viewBinding = true
+ }
+
+ ksp {
+ arg("KOIN_CONFIG_CHECK", "true")
+ arg("KOIN_DEFAULT_MODULE", "false")
+ arg("KOIN_USE_COMPOSE_VIEWMODEL", "true")
+ }
+ composeOptions.kotlinCompilerExtensionVersion = "1.5.6"
+ packagingOptions.resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}")
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_21)
+ freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.kotzilla.sdk)
+ // Base libraries ------------------------------------------------------------------------------
+
+ implementation(libs.core.ktx)
+ implementation(platform(libs.kotlin.bom))
+ implementation(libs.lifecycle.runtime.ktx)
+ implementation(libs.activity.compose)
+ implementation(platform(libs.compose.bom))
+ implementation(libs.ui)
+ implementation(libs.ui.graphics)
+ implementation(libs.ui.tooling.preview)
+ implementation(libs.compose.animation.graphics)
+ implementation(libs.material.icons.extended)
+ implementation(libs.androidx.foundation)
+ androidTestImplementation(platform(libs.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.ui.tooling)
+ debugImplementation(libs.ui.test.manifest)
+
+ // Material
+ implementation(libs.material3)
+
+ // Navigation
+ implementation(libs.navigation.compose)
+
+ // Data store
+ implementation(libs.androidx.datastore.preferences)
+
+ // TESTING -------------------------------------------------------------------------------------
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.ext.junit)
+ androidTestImplementation(libs.espresso.core)
+
+ // Koin testing
+ testImplementation(libs.koin.test)
+
+ // IMAGES --------------------------------------------------------------------------------------
+
+ // Coil
+ implementation(libs.coil.compose)
+ implementation(libs.coil.svg)
+ implementation(libs.coil.gif)
+
+ // VIEW ELEMENTS -------------------------------------------------------------------------------
+
+ // Lottie animation for start
+ implementation(libs.lottie.compose)
+
+ // Fonts
+ implementation(libs.ui.text.google.fonts)
+
+ // Constraint layout
+ implementation(libs.constraintlayout.compose)
+
+ // NETWORK -------------------------------------------------------------------------------------
+
+ // gson
+ implementation(libs.gson)
+
+
+ // Ktor
+ implementation(libs.ktor.serialization.kotlinx.json)
+ implementation(libs.ktor.client.logging)
+ implementation(libs.ktor.client.core)
+ implementation(libs.io.ktor.ktor.client.cio)
+ implementation(libs.ktor.client.websockets)
+ implementation(libs.ktor.client.negotiation)
+ implementation(libs.ktor.client.auth)
+
+ // Paging
+ implementation(libs.androidx.paging.runtime.ktx)
+ implementation(libs.androidx.paging.compose)
+
+ // GOOGLE AND FACEBOOK SERVICES ----------------------------------------------------------------
+
+ // Cloud messaging
+ implementation(libs.play.services.gcm)
+ implementation(libs.google.firebase.analytics)
+ implementation(libs.firebase.messaging)
+
+ // Import the BoM for the Firebase platform
+ implementation(platform(libs.firebase.bom))
+
+ // Guava
+ implementation(libs.guava)
+
+ // OTHER SERVICES ------------------------------------------------------------------------------
+
+ // CameraX
+ implementation(libs.camera.core)
+ implementation(libs.camera.camera2)
+ implementation(libs.androidx.camera.lifecycle)
+ implementation(libs.androidx.camera.video)
+ implementation(libs.androidx.camera.view)
+ implementation(libs.androidx.camera.extensions)
+
+ // Koin
+ implementation(libs.koin.core)
+ implementation(libs.koin.android)
+ implementation(libs.koin.annotations)
+ implementation(libs.koin.androidx.compose)
+ implementation(libs.koin.androidx.compose.navigation)
+ ksp(libs.koin.ksp.compiler)
+ ksp(libs.koin.ksp.bom)
+
+ // Viewmodel
+ implementation(libs.androidx.savedstate.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
+ implementation(libs.androidx.lifecycle.service)
+ implementation(libs.lifecycle.runtime.ktx)
+
+ // Room
+ implementation(libs.androidx.room.runtime)
+ ksp(libs.androidx.room.compiler)
+ implementation(libs.androidx.room.ktx)
+ implementation(libs.androidx.room.paging)
+
+ // Permission manager
+ implementation(libs.accompanist.permissions)
+
+ // System UI controller
+ implementation(libs.accompanist.systemuicontroller)
+
+ implementation(libs.androidx.core.splashscreen)
+
+ // END -------------------------------------------------------------------------------------
+}
\ No newline at end of file
diff --git a/app/google-services.json b/app/google-services.json
new file mode 100644
index 0000000..182ebf8
--- /dev/null
+++ b/app/google-services.json
@@ -0,0 +1,29 @@
+{
+ "project_info": {
+ "project_number": "846499996834",
+ "project_id": "prodmoscow2025",
+ "storage_bucket": "prodmoscow2025.firebasestorage.app"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:846499996834:android:3f1b450bd2c804dff523fb",
+ "android_client_info": {
+ "package_name": "com.prodhack.moscow2025"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyDFkGaR9ME9drbAw2pAeSDd2QX8vY5H8d8"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/app/kotzilla.json b/app/kotzilla.json
new file mode 100644
index 0000000..7273d9b
--- /dev/null
+++ b/app/kotzilla.json
@@ -0,0 +1,11 @@
+{
+ "sdkVersion": "1.3.1",
+ "keys": [
+ {
+ "appId": "603ad697-3a85-4f69-89db-d0322cbb6059",
+ "applicationPackageName": "com.prodhack.moscow2025",
+ "keyId": "019aa5e2-2c8b-76f5-aa95-104934100116",
+ "apiKey": "ktz-sdk-rwsABf6YmCK4P70l7drtjOaWEj8s9WRu17VulWyrfcM"
+ }
+ ]
+}
\ 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/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json b/app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json
new file mode 100644
index 0000000..4877ef0
--- /dev/null
+++ b/app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json
@@ -0,0 +1,62 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "bf664fe902e116c42af432814d63d6a7",
+ "entities": [
+ {
+ "tableName": "users",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `email` TEXT NOT NULL, `first_name` TEXT, `last_name` TEXT, `display_name` TEXT, `avatar_url` TEXT, `phone` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "firstName",
+ "columnName": "first_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastName",
+ "columnName": "last_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "display_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "avatarUrl",
+ "columnName": "avatar_url",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "phone",
+ "columnName": "phone",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf664fe902e116c42af432814d63d6a7')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/prodhack/moscow2025/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/prodhack/moscow2025/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..f5450d7
--- /dev/null
+++ b/app/src/androidTest/java/com/prodhack/moscow2025/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.prodhack.moscow2025
+
+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.prodhack.moscow2025", 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..b9f1966
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/kotzilla.key b/app/src/main/assets/kotzilla.key
new file mode 100644
index 0000000..53027ae
--- /dev/null
+++ b/app/src/main/assets/kotzilla.key
@@ -0,0 +1 @@
+Omt0ei1zZGstcndzQUJmNlltQ0s0UDcwbDdkcnRqT2FXRWo4czlXUnUxN1Z1bFd5cmZjTQ==
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/FirebaseMessagingService.kt b/app/src/main/java/com/prodhack/moscow2025/FirebaseMessagingService.kt
new file mode 100644
index 0000000..d3672b0
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/FirebaseMessagingService.kt
@@ -0,0 +1,81 @@
+package com.prodhack.moscow2025
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Intent
+import android.util.Log
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.core.app.NotificationCompat
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import com.prodhack.moscow2025.presentation.MainActivity
+
+class FirebaseMessagingService : FirebaseMessagingService() {
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ }
+
+ override fun onMessageReceived(message: RemoteMessage) {
+
+ val title = message.data["title"]
+ val text = message.data["body"]
+ Log.e(
+ "fcm",
+ "title=$title" +
+ "\ntext=$text"
+ )
+
+ title?.let { it1 -> text?.let { it2 -> sendNotification(it1, it2) } }
+ }
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ Log.e("fcm", "token=$token")
+ }
+
+ private fun createNotificationChannel() {
+ val channel = NotificationChannel(
+ "default_channel_id",
+ "MainAppNotifications",
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ description = "Channel for main motifications"
+ enableLights(true)
+ lightColor = Color.Red.toArgb()
+ enableVibration(true)
+ vibrationPattern = longArrayOf(0, 500, 200, 500)
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+ }
+
+ val notificationManager =
+ getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ private fun sendNotification(title: String?, messageBody: String?) {
+ val intent = Intent(this, MainActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ this, 0, intent,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val notificationBuilder = NotificationCompat.Builder(this, "default_channel_id")
+ .setSmallIcon(R.drawable.ic_launcher_background) // замените на свою иконку
+ .setContentTitle(title ?: "Уведомление")
+ .setContentText(messageBody)
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent)
+
+ val notificationManager =
+ getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.notify(0, notificationBuilder.build())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/common/App.kt b/app/src/main/java/com/prodhack/moscow2025/common/App.kt
new file mode 100644
index 0000000..b8c58b6
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/common/App.kt
@@ -0,0 +1,68 @@
+package com.prodhack.moscow2025.common
+
+import android.app.Application
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import android.util.Log
+import com.google.firebase.FirebaseApp
+import com.prodhack.moscow2025.common.di.AppModule
+import com.prodhack.moscow2025.common.di.DataModule
+import com.prodhack.moscow2025.common.di.DomainModule
+import com.prodhack.moscow2025.data.data_providers.local_db.DatabaseProvider
+import io.kotzilla.sdk.analytics.koin.analytics
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.context.startKoin
+import org.koin.ksp.generated.module
+
+
+class App : Application() {
+
+ companion object {
+ lateinit var instance: Application
+ lateinit var version: String
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ instance = this
+ version = getAppVersion()
+ startKoin {
+ androidContext(this@App)
+ analytics()
+ modules(
+ listOf(
+ AppModule().module,
+ DataModule().module,
+ DomainModule().module,
+ DatabaseProvider().module
+ )
+ )
+ }
+ FirebaseApp.initializeApp(this@App)
+ }
+
+ private fun getAppVersion(): String {
+ var pInfo: PackageInfo? = null
+ try {
+ val pm = packageManager
+ if (pm != null) {
+ pInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
+ } else {
+ pm.getPackageInfo(packageName, 0)
+ }
+ }
+ } catch (e: Exception) {
+ Log.d("App", "method: getAppVersion - error: $e")
+ }
+ if (pInfo == null) {
+ pInfo = PackageInfo()
+ pInfo.versionName = "0.0.0"
+ pInfo.longVersionCode = 0
+ }
+ var version = pInfo.versionName + "."
+ version += pInfo.longVersionCode
+ return version
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/common/Constants.kt b/app/src/main/java/com/prodhack/moscow2025/common/Constants.kt
new file mode 100644
index 0000000..d66fabc
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/common/Constants.kt
@@ -0,0 +1,5 @@
+package com.prodhack.moscow2025.common
+
+object Constants {
+ const val BASE_API_URL = "https://hackaton.paas.itqdev.xyz/"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt b/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt
new file mode 100644
index 0000000..2c55559
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt
@@ -0,0 +1,16 @@
+package com.prodhack.moscow2025.common.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("com.prodhack.moscow2025.presentation")
+class AppModule
+
+@Module
+@ComponentScan("com.prodhack.moscow2025.domain")
+class DomainModule
+
+@Module
+@ComponentScan("com.prodhack.moscow2025.data")
+class DataModule
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt
new file mode 100644
index 0000000..17dcbd9
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt
@@ -0,0 +1,6 @@
+package com.prodhack.moscow2025.data.base
+
+
+interface BaseEntity {
+ val id: Number
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BasePaginationDAO.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BasePaginationDAO.kt
new file mode 100644
index 0000000..30ab752
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BasePaginationDAO.kt
@@ -0,0 +1,10 @@
+package com.prodhack.moscow2025.data.base
+
+import androidx.paging.PagingSource
+
+interface BasePaginationDAO {
+ suspend fun clearAll()
+ suspend fun upsertAll(data: List)
+
+ fun getPaginatedData(): PagingSource
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt
new file mode 100644
index 0000000..ddf62da
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt
@@ -0,0 +1,62 @@
+package com.prodhack.moscow2025.data.base
+
+import androidx.paging.ExperimentalPagingApi
+import androidx.paging.LoadType
+import androidx.paging.PagingState
+import androidx.paging.RemoteMediator
+import androidx.room.RoomDatabase
+import androidx.room.withTransaction
+
+@OptIn(ExperimentalPagingApi::class)
+class BaseRemoteMediator(
+ private val db: RoomDatabase,
+ private val dao: BasePaginationDAO,
+ private val makeRequest: suspend (page: Long, pageCount: Int) -> Result>
+) : RemoteMediator() {
+
+ override suspend fun load(
+ loadType: LoadType,
+ state: PagingState
+ ): MediatorResult {
+ return try {
+ val loadKey = when (loadType) {
+ LoadType.REFRESH -> 1
+ LoadType.PREPEND -> return MediatorResult.Success(
+ endOfPaginationReached = true
+ )
+
+ LoadType.APPEND -> {
+ val lastItem = state.lastItemOrNull()
+ if (lastItem == null) {
+ 1
+ } else {
+ (lastItem.id.toLong() / state.config.pageSize) + 1
+ }
+ }
+ }
+
+ val result = makeRequest(
+ loadKey,
+ state.config.pageSize
+ )
+
+ if (result.isSuccess) {
+ val data = result.getOrNull()!!
+ db.withTransaction {
+ if (loadType == LoadType.REFRESH) {
+ dao.clearAll()
+ }
+ val beerEntities = data
+ dao.upsertAll(beerEntities)
+ }
+ MediatorResult.Success(
+ endOfPaginationReached = data.size < state.config.pageSize
+ )
+ } else {
+ MediatorResult.Error(result.exceptionOrNull()!!)
+ }
+ } catch (e: Exception) {
+ MediatorResult.Error(e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt
new file mode 100644
index 0000000..21f0771
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt
@@ -0,0 +1,175 @@
+package com.prodhack.moscow2025.data.base
+
+import android.util.Log
+import androidx.paging.ExperimentalPagingApi
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.room.RoomDatabase
+import com.prodhack.moscow2025.data.dto.ErrorNetworkDTO
+import com.prodhack.moscow2025.domain.utils.NetworkError
+import com.prodhack.moscow2025.domain.utils.convertToNetworkError
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.HttpRequestBuilder
+import io.ktor.client.request.request
+import io.ktor.http.isSuccess
+import kotlinx.coroutines.flow.Flow
+import kotlin.time.Duration
+
+abstract class BaseRepository {
+
+ // Caching module ==============================================================================
+ private val internalCacheStorage = mutableMapOf>()
+
+ private data class CacheEntry(
+ val value: T,
+ val expirationTime: Long
+ )
+
+ fun putCache(cacheConfiguration: Pair, value: T) {
+ internalCacheStorage[cacheConfiguration.first] =
+ CacheEntry(value, cacheConfiguration.second.inWholeSeconds)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun getCache(key: String): T? {
+ val entry = internalCacheStorage[key] ?: return null
+ if (entry.expirationTime < System.currentTimeMillis()) {
+ internalCacheStorage.remove(key)
+ return null
+ }
+ return entry.value as T
+ }
+
+ // Base data sources ===========================================================================
+
+ protected open val defaultKtorClient: HttpClient? = null
+ protected open val db: RoomDatabase? = null
+
+ companion object {
+ private const val TAG = "BaseRepository"
+ }
+
+ // Internal methods ============================================================================
+
+ private fun assertKtorClientSpecify() {
+ if (defaultKtorClient == null) {
+ Log.e(TAG, "You must specify ktor client for make network requests")
+ throw IllegalStateException("You must specify ktor client for make network requests")
+ }
+ }
+
+ private fun assertDBSpecify() {
+ if (db == null) {
+ throw IllegalStateException("You must specify db for use pagination/cashing")
+ }
+ }
+
+ // And methods for use :) ======================================================================
+
+ /**
+ * Makes a network request using the provided Ktor client and request builder block.
+ *
+ * This function handles the common boilerplate for making a network request,
+ * including error handling and converting exceptions to a domain-specific `NetworkError`.
+ *
+ * @param T The expected successful response type. This type must be deserializable by Ktor.
+ * @param ktorClient The [HttpClient] to use for the request. Defaults to `this.defaultKtorClient`.
+ * An [IllegalStateException] will be thrown if no client is provided and `defaultKtorClient` is null.
+ * @param block A lambda function that configures the [HttpRequestBuilder] for the request.
+ * @return A [Result] object containing either the successful response of type [T] or a [NetworkError] if the request fails.
+ * @throws IllegalStateException if `ktorClient` is null and `defaultKtorClient` is also null.
+ */
+ internal suspend inline fun networkRequest(
+ ktorClient: HttpClient? = this.defaultKtorClient,
+ cacheConfiguration: Pair? = null,
+ block: HttpRequestBuilder.() -> Unit
+ ): Result {
+ Log.d(TAG, "Network request! Asserting ktor client specify")
+ assertKtorClientSpecify()
+ Log.d(TAG, "ktor client is specified - continue network request")
+ return try {
+ Log.d(TAG, "Start request!")
+ val response = ktorClient!!.request(block = block)
+ Log.d(TAG, "Request was made without exceptions")
+
+ if (response.status.isSuccess()) {
+ Result.success(
+ value = response
+ ).map {
+ it.body()
+ }
+ } else {
+ val firstCodeNum = response.status.value / 100
+ val detail = (response.body() as? ErrorNetworkDTO)?.detail ?: "Unknown"
+ Result.failure(
+ when (firstCodeNum) {
+ 4 -> NetworkError.InputError(detail)
+ else -> NetworkError.Unexpected(detail)
+ }
+ )
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception in request process! $e")
+ Result.failure(
+ exception = e.convertToNetworkError()
+ )
+ }.onSuccess {
+ Log.v(TAG, "Network request was successful")
+ if (cacheConfiguration != null) {
+ putCache(cacheConfiguration, it)
+ }
+ }.onFailure {
+ Log.e(TAG, "Network request has error! $it")
+ }
+ }
+
+
+ internal suspend inline fun internalCachedRequest(
+ ktorClient: HttpClient? = this.defaultKtorClient,
+ cacheConfiguration: Pair,
+ block: HttpRequestBuilder.() -> Unit
+ ): Result {
+ val cachedResult = getCache(cacheConfiguration.first)
+
+ return if (cachedResult != null) {
+ Result.success(cachedResult)
+ } else {
+ networkRequest(ktorClient, cacheConfiguration, block)
+ }
+ }
+
+ @OptIn(ExperimentalPagingApi::class)
+ protected fun paginatedRequest(
+ pageSize: Int = 10,
+ prefetchDistance: Int = pageSize,
+ enablePlaceholders: Boolean = true,
+ initialLoadSize: Int = pageSize * 3,
+ maxSize: Int = Int.MAX_VALUE,
+ jumpThreshold: Int = Int.MIN_VALUE,
+ dbDao: BasePaginationDAO,
+ makeRequest: suspend (page: Long, pageSize: Int) -> Result>
+ ): Flow> {
+ assertDBSpecify()
+
+ return Pager(
+ config = PagingConfig(
+ pageSize,
+ prefetchDistance,
+ enablePlaceholders,
+ initialLoadSize,
+ maxSize,
+ jumpThreshold
+ ),
+ remoteMediator = BaseRemoteMediator(
+ db = db!!,
+ dao = dbDao,
+ makeRequest = makeRequest
+ ),
+ pagingSourceFactory = {
+ dbDao.getPaginatedData()
+ }
+ ).flow
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt
new file mode 100644
index 0000000..60c8262
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt
@@ -0,0 +1,5 @@
+package com.prodhack.moscow2025.data.base
+
+interface DBMappableDTO {
+ fun mapToDB(): T
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt
new file mode 100644
index 0000000..204b7d3
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt
@@ -0,0 +1,5 @@
+package com.prodhack.moscow2025.data.base
+
+interface DomainMappableDTO {
+ fun mapToDomain(): T
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/GalleryPagingSource.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/GalleryPagingSource.kt
new file mode 100644
index 0000000..bcbbb4d
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/GalleryPagingSource.kt
@@ -0,0 +1,60 @@
+package com.prodhack.moscow2025.data.data_providers
+
+import android.content.ContentResolver
+import android.provider.MediaStore
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+
+class GalleryPagingSource(
+ private val contentResolver: ContentResolver
+) : PagingSource() {
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ return try {
+ val page = params.key ?: 0
+ val pageSize = params.loadSize
+
+ val projection = arrayOf(
+ MediaStore.Images.Media._ID,
+ MediaStore.Images.Media.DATE_ADDED
+ )
+
+ val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
+
+ val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ val cursor = contentResolver.query(
+ uri,
+ projection,
+ null,
+ null,
+ "$sortOrder LIMIT $pageSize OFFSET ${page * pageSize}"
+ )
+
+ val images = mutableListOf()
+ cursor?.use {
+ val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
+ while (it.moveToNext()) {
+ images.add(it.getLong(idColumn))
+ }
+ }
+
+ val nextKey = if (images.size < pageSize) null else page + 1
+
+ LoadResult.Page(
+ data = images,
+ prevKey = if (page == 0) null else page - 1,
+ nextKey = nextKey
+ )
+
+ } catch (e: Exception) {
+ LoadResult.Error(e)
+ }
+ }
+
+ override fun getRefreshKey(state: PagingState): Int? {
+ return state.anchorPosition?.let { anchorPosition ->
+ state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
+ ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt
new file mode 100644
index 0000000..3071978
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt
@@ -0,0 +1,79 @@
+package com.prodhack.moscow2025.data.data_providers.api
+
+import com.prodhack.moscow2025.common.Constants
+import com.prodhack.moscow2025.data.data_providers.localInfo.AuthorizationDataStore
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.plugins.HttpRequestRetry
+import io.ktor.client.plugins.auth.Auth
+import io.ktor.client.plugins.auth.providers.BearerTokens
+import io.ktor.client.plugins.auth.providers.bearer
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.defaultRequest
+import io.ktor.client.plugins.logging.ANDROID
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logger
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.Json
+import org.koin.core.annotation.Single
+
+
+// Configuration Ktor client for request to API
+@Single
+class ApiKtorClient(authorizationDataStore: AuthorizationDataStore) {
+
+ val client = HttpClient(OkHttp) {
+ install(Logging) {
+ logger = Logger.ANDROID
+ level = LogLevel.ALL
+ }
+ install(HttpRequestRetry) {
+ retryOnServerErrors(maxRetries = 3)
+ exponentialDelay()
+ }
+ install(ContentNegotiation) {
+ json(Json {
+ prettyPrint = true
+ isLenient = true
+ ignoreUnknownKeys = true
+ })
+ }
+ defaultRequest {
+ url(Constants.BASE_API_URL)
+ }
+ install(Auth) {
+ bearer {
+ sendWithoutRequest { request ->
+ val segments = request.url.pathSegments
+
+ val endpointsWithoutAuth = listOf(
+ "sign_in",
+ "sign_up"
+ )
+
+ endpointsWithoutAuth.any { segments.contains(it) }.not()
+ }
+ loadTokens {
+ return@loadTokens authorizationDataStore.token.first()
+ .toBearerTokens()
+ }
+ refreshTokens {
+ CoroutineScope(Dispatchers.IO).launch {
+ authorizationDataStore.clearToken()
+ }
+
+ return@refreshTokens null
+ }
+ }
+ }
+ }
+
+ private fun String.toBearerTokens(): BearerTokens {
+ return BearerTokens(this, null)
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/utils/TimestampFormatter.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/utils/TimestampFormatter.kt
new file mode 100644
index 0000000..b2aa9d3
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/utils/TimestampFormatter.kt
@@ -0,0 +1,12 @@
+package com.prodhack.moscow2025.data.data_providers.api.utils
+
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+
+internal fun String.parseToTimestamp(): Long? {
+ val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ dateFormatter.timeZone = TimeZone.getTimeZone("UTC")
+
+ return dateFormatter.parse(this)?.time
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/localInfo/AuthorizationDataStore.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/localInfo/AuthorizationDataStore.kt
new file mode 100644
index 0000000..0994d90
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/localInfo/AuthorizationDataStore.kt
@@ -0,0 +1,55 @@
+package com.prodhack.moscow2025.data.data_providers.localInfo
+
+import android.content.Context
+import android.util.Log
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.map
+import kotlinx.io.IOException
+import org.koin.core.annotation.Single
+
+
+@Single
+class AuthorizationDataStore(
+ context: Context
+) {
+ private val Context.dataStore by preferencesDataStore(
+ name = "authTokens"
+ )
+
+ private val dataStore = context.dataStore
+
+ private companion object {
+ const val TAG = "AuthorizationDataStore"
+ val ACCESS_TOKEN = stringPreferencesKey("accessToken")
+ }
+
+ suspend fun saveToken(accessToken: String) {
+ dataStore.edit { preferences ->
+ preferences[ACCESS_TOKEN] = accessToken
+ }
+ }
+
+ suspend fun clearToken() {
+ dataStore.edit { preferences ->
+ preferences[ACCESS_TOKEN] = ""
+ }
+ }
+
+ val token
+ get() = dataStore.data
+ .catch {
+ Log.e(TAG, "Error reading preferences.", it)
+ if (it is IOException) {
+ Log.e(TAG, "return empty prefs")
+ emit(emptyPreferences())
+ } else {
+ throw it
+ }
+ }.map { preferences ->
+ preferences[ACCESS_TOKEN] ?: ""
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt
new file mode 100644
index 0000000..db2fb77
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt
@@ -0,0 +1,18 @@
+package com.prodhack.moscow2025.data.data_providers.local_db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import com.prodhack.moscow2025.data.data_providers.local_db.dao.CleanUpDao
+import com.prodhack.moscow2025.data.data_providers.local_db.dao.UserDao
+import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity
+
+@Database(
+ entities = [UserEntity::class],
+ version = 1,
+ exportSchema = true
+)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun userDao(): UserDao
+
+ abstract fun cleanUpDao(): CleanUpDao
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt
new file mode 100644
index 0000000..6b1976b
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt
@@ -0,0 +1,19 @@
+package com.prodhack.moscow2025.data.data_providers.local_db
+
+import android.content.Context
+import androidx.room.Room
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
+
+@Module
+class DatabaseProvider {
+
+ @Single
+ fun provideDatabase(context: Context): AppDatabase =
+ Room.databaseBuilder(
+ context,
+ AppDatabase::class.java,
+ "t_tasks.db"
+ ).fallbackToDestructiveMigration()
+ .build()
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/CleanUpDao.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/CleanUpDao.kt
new file mode 100644
index 0000000..efa0e44
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/CleanUpDao.kt
@@ -0,0 +1,17 @@
+package com.prodhack.moscow2025.data.data_providers.local_db.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Transaction
+
+@Dao
+interface CleanUpDao {
+ @Query("DELETE FROM users")
+ suspend fun cleanUpUsers()
+
+ @Transaction
+ suspend fun cleanUp() {
+ cleanUpUsers()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/UserDao.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/UserDao.kt
new file mode 100644
index 0000000..8c2c6b0
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/UserDao.kt
@@ -0,0 +1,24 @@
+package com.prodhack.moscow2025.data.data_providers.local_db.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface UserDao {
+
+ @Query("SELECT * FROM users LIMIT 1")
+ fun observeUser(): Flow
+
+ @Query("SELECT * FROM users LIMIT 1")
+ suspend fun getUser(): UserEntity?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(user: UserEntity)
+
+ @Query("DELETE FROM users")
+ suspend fun clear()
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/UserEntity.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/UserEntity.kt
new file mode 100644
index 0000000..3e149cd
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/UserEntity.kt
@@ -0,0 +1,44 @@
+package com.prodhack.moscow2025.data.data_providers.local_db.entities
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.prodhack.moscow2025.domain.models.User
+
+@Entity(tableName = "users")
+data class UserEntity(
+ @PrimaryKey(autoGenerate = false)
+ val id: String,
+ val email: String,
+ @ColumnInfo(name = "first_name")
+ val firstName: String?,
+ @ColumnInfo(name = "last_name")
+ val lastName: String?,
+ @ColumnInfo(name = "display_name")
+ val displayName: String?,
+ @ColumnInfo(name = "avatar_url")
+ val avatarUrl: String?,
+ val phone: String?
+) {
+ fun mapToDomain(): User {
+ return User(
+ id = id,
+ firstName = firstName,
+ lastName = lastName,
+ displayName = displayName,
+ email = email,
+ avatarUrl = avatarUrl,
+ phone = phone
+ )
+ }
+}
+
+fun User.mapToDB(): UserEntity = UserEntity(
+ id = id,
+ firstName = firstName,
+ lastName = lastName,
+ displayName = displayName,
+ phone = phone,
+ email = email,
+ avatarUrl = avatarUrl
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt b/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt
new file mode 100644
index 0000000..1870dce
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt
@@ -0,0 +1,85 @@
+package com.prodhack.moscow2025.data.dto
+
+import com.prodhack.moscow2025.domain.models.LoginData
+import com.prodhack.moscow2025.domain.models.RegisterData
+import com.prodhack.moscow2025.domain.models.UpdateUserData
+import com.prodhack.moscow2025.domain.models.User
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ErrorNetworkDTO(
+ val detail: String
+)
+
+@Serializable
+data class UserPatchRequest(
+ val email: String?,
+ @SerialName("display_name")
+ val displayName: String? = null,
+ @SerialName("first_name")
+ val firstName: String? = null,
+ @SerialName("last_name")
+ val lastName: String? = null,
+ @SerialName("avatar_url")
+ val avatarUrl: String? = null,
+ val phone: String? = null,
+)
+
+fun UpdateUserData.mapToData(): UserPatchRequest = UserPatchRequest(
+ email = email,
+ displayName = displayName,
+ firstName = firstName,
+ lastName = lastName,
+ avatarUrl = avatarUrl,
+ phone = phone
+)
+
+@Serializable
+data class UserLoginRequest(
+ val email: String,
+ val password: String
+)
+
+fun LoginData.mapToData(): UserLoginRequest = UserLoginRequest(email, password)
+
+
+@Serializable
+data class UserRegisterRequest(
+ val email: String,
+ val password: String
+)
+
+fun RegisterData.mapToData(): UserRegisterRequest = UserRegisterRequest(email, password)
+
+
+@Serializable
+data class TokenResponse(
+ @SerialName("access_token")
+ val token: String
+)
+
+@Serializable
+data class UserResponse(
+ val id: String,
+ val email: String,
+ @SerialName("display_name")
+ val displayName: String? = null,
+ @SerialName("first_name")
+ val firstName: String? = null,
+ @SerialName("last_name")
+ val lastName: String? = null,
+ @SerialName("avatar_url")
+ val avatarUrl: String? = null,
+ val phone: String? = null,
+) {
+ fun mapToDomain(): User = User(
+ id = id,
+ email = email,
+ displayName = displayName,
+ firstName = firstName,
+ lastName = lastName,
+ avatarUrl = avatarUrl,
+ phone = phone
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt
new file mode 100644
index 0000000..a3cabe7
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt
@@ -0,0 +1,60 @@
+package com.prodhack.moscow2025.data.repImplementations
+
+import com.prodhack.moscow2025.data.base.BaseRepository
+import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient
+import com.prodhack.moscow2025.data.data_providers.localInfo.AuthorizationDataStore
+import com.prodhack.moscow2025.data.dto.TokenResponse
+import com.prodhack.moscow2025.data.dto.mapToData
+import com.prodhack.moscow2025.domain.interfaces.AuthRepository
+import com.prodhack.moscow2025.domain.models.LoginData
+import com.prodhack.moscow2025.domain.models.RegisterData
+import io.ktor.client.request.setBody
+import io.ktor.client.request.url
+import io.ktor.http.ContentType
+import io.ktor.http.HttpMethod
+import io.ktor.http.contentType
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import org.koin.core.annotation.Single
+
+@Single
+class AuthRepositoryImpl(
+ ktorClient: ApiKtorClient,
+ private val authorizationDataStore: AuthorizationDataStore
+) : AuthRepository, BaseRepository() {
+
+ override val defaultKtorClient = ktorClient.client
+
+ override fun fetchLoginState(): Flow =
+ authorizationDataStore.token.map { it.isNotBlank() }
+
+ override suspend fun signUpRequest(request: RegisterData): Result =
+ networkRequest {
+ url {
+ method = HttpMethod.Post
+ url("/auth/sign_up/email")
+ setBody(request.mapToData())
+ contentType(ContentType.Application.Json)
+ }
+ }.map {
+ authorizationDataStore.saveToken(it.token)
+ "Success"
+ }
+
+ override suspend fun signInRequest(request: LoginData): Result =
+ networkRequest {
+ url {
+ method = HttpMethod.Post
+ url("/auth/sign_up/email")
+ setBody(request.mapToData())
+ contentType(ContentType.Application.Json)
+ }
+ }.map {
+ authorizationDataStore.saveToken(it.token)
+ "Success"
+ }
+
+ override suspend fun clearLoginData() {
+ authorizationDataStore.clearToken()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/GalleryRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/GalleryRepositoryImpl.kt
new file mode 100644
index 0000000..27c7df8
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/GalleryRepositoryImpl.kt
@@ -0,0 +1,23 @@
+package com.prodhack.moscow2025.data.repImplementations
+
+import android.app.Application
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import com.prodhack.moscow2025.data.data_providers.GalleryPagingSource
+import com.prodhack.moscow2025.domain.interfaces.GalleryRepository
+import kotlinx.coroutines.flow.Flow
+import org.koin.core.annotation.Single
+
+@Single
+class GalleryRepositoryImpl(private val application: Application) : GalleryRepository {
+ override fun getImagesIds(): Flow> = Pager(
+ config = PagingConfig(
+ pageSize = 50,
+ enablePlaceholders = false
+ ),
+ pagingSourceFactory = {
+ GalleryPagingSource(application.contentResolver)
+ }
+ ).flow
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/UserRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/UserRepositoryImpl.kt
new file mode 100644
index 0000000..f1d274f
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/UserRepositoryImpl.kt
@@ -0,0 +1,75 @@
+package com.prodhack.moscow2025.data.repImplementations
+
+import com.prodhack.moscow2025.data.base.BaseRepository
+import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient
+import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase
+import com.prodhack.moscow2025.data.data_providers.local_db.entities.mapToDB
+import com.prodhack.moscow2025.data.dto.UserResponse
+import com.prodhack.moscow2025.data.dto.mapToData
+import com.prodhack.moscow2025.domain.interfaces.UserRepository
+import com.prodhack.moscow2025.domain.models.UpdateUserData
+import com.prodhack.moscow2025.domain.models.User
+import io.ktor.client.request.setBody
+import io.ktor.client.request.url
+import io.ktor.http.ContentType
+import io.ktor.http.HttpMethod
+import io.ktor.http.contentType
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import org.koin.core.annotation.Single
+
+@Single
+class UserRepositoryImpl(
+ ktorClient: ApiKtorClient,
+ override val db: AppDatabase
+) : UserRepository, BaseRepository() {
+
+ override val defaultKtorClient = ktorClient.client
+ private val userDao = db.userDao()
+
+ override fun observeUser(): Flow {
+ CoroutineScope(Dispatchers.IO).launch {
+ fetchProfile()
+ }
+ return userDao.observeUser().map {
+ it?.mapToDomain()
+ }
+ }
+
+ private suspend fun writeProfileToDB(data: User) {
+ userDao.upsert(data.mapToDB())
+ }
+
+ override suspend fun fetchProfile(): Result = networkRequest {
+ url {
+ method = HttpMethod.Get
+ url("/profile")
+ }
+ }.map {
+ it.mapToDomain().also {
+ writeProfileToDB(it)
+ }
+ }
+
+ override suspend fun updateProfile(request: UpdateUserData): Result {
+ return networkRequest {
+ url {
+ method = HttpMethod.Patch
+ url("/profile")
+ setBody(request.mapToData())
+ contentType(ContentType.Application.Json)
+ }
+ }.map {
+ it.mapToDomain().also {
+ writeProfileToDB(it)
+ }
+ }
+ }
+
+ override suspend fun clearLocalUserData() {
+ userDao.clear()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/AuthRepository.kt b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/AuthRepository.kt
new file mode 100644
index 0000000..6f377a2
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/AuthRepository.kt
@@ -0,0 +1,15 @@
+package com.prodhack.moscow2025.domain.interfaces
+
+import com.prodhack.moscow2025.domain.models.LoginData
+import com.prodhack.moscow2025.domain.models.RegisterData
+import kotlinx.coroutines.flow.Flow
+
+interface AuthRepository {
+ fun fetchLoginState(): Flow
+
+ suspend fun signUpRequest(request: RegisterData): Result
+
+ suspend fun signInRequest(request: LoginData): Result
+
+ suspend fun clearLoginData()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/GalleryRepository.kt b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/GalleryRepository.kt
new file mode 100644
index 0000000..fa17126
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/GalleryRepository.kt
@@ -0,0 +1,8 @@
+package com.prodhack.moscow2025.domain.interfaces
+
+import androidx.paging.PagingData
+import kotlinx.coroutines.flow.Flow
+
+interface GalleryRepository {
+ fun getImagesIds(): Flow>
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/UserRepository.kt b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/UserRepository.kt
new file mode 100644
index 0000000..a01ff12
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/UserRepository.kt
@@ -0,0 +1,15 @@
+package com.prodhack.moscow2025.domain.interfaces
+
+import com.prodhack.moscow2025.domain.models.UpdateUserData
+import com.prodhack.moscow2025.domain.models.User
+import kotlinx.coroutines.flow.Flow
+
+interface UserRepository {
+ fun observeUser(): Flow
+
+ suspend fun fetchProfile(): Result
+
+ suspend fun updateProfile(request: UpdateUserData): Result
+
+ suspend fun clearLocalUserData()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt b/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt
new file mode 100644
index 0000000..0eef4f8
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt
@@ -0,0 +1,11 @@
+package com.prodhack.moscow2025.domain.models
+
+data class RegisterData(
+ val email: String,
+ val password: String
+)
+
+data class LoginData(
+ val email: String,
+ val password: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/models/User.kt b/app/src/main/java/com/prodhack/moscow2025/domain/models/User.kt
new file mode 100644
index 0000000..7a95845
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/models/User.kt
@@ -0,0 +1,20 @@
+package com.prodhack.moscow2025.domain.models
+
+data class User(
+ val id: String,
+ val email: String,
+ val displayName: String?,
+ val firstName: String?,
+ val lastName: String?,
+ val avatarUrl: String?,
+ val phone: String?
+)
+
+data class UpdateUserData(
+ val email: String? = null,
+ val displayName: String? = null,
+ val firstName: String? = null,
+ val lastName: String? = null,
+ val avatarUrl: String? = null,
+ val phone: String? = null
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt
new file mode 100644
index 0000000..371becd
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt
@@ -0,0 +1,15 @@
+package com.prodhack.moscow2025.domain.usecase.auth
+
+import com.prodhack.moscow2025.domain.interfaces.AuthRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.firstOrNull
+import org.koin.core.annotation.Single
+
+@Single
+class CheckSessionUseCase(
+ private val authRepository: AuthRepository
+) {
+ operator suspend fun invoke(): Boolean {
+ return authRepository.fetchLoginState().firstOrNull() == true
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/GetUserUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/GetUserUseCase.kt
new file mode 100644
index 0000000..f5d98ee
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/GetUserUseCase.kt
@@ -0,0 +1,12 @@
+package com.prodhack.moscow2025.domain.usecase.auth
+
+import com.prodhack.moscow2025.domain.models.User
+import com.prodhack.moscow2025.domain.interfaces.UserRepository
+import org.koin.core.annotation.Single
+
+@Single
+class GetUserUseCase(
+ private val userRepository: UserRepository
+) {
+ suspend operator fun invoke(): Result = userRepository.fetchProfile()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LogOutUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LogOutUseCase.kt
new file mode 100644
index 0000000..4371f82
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LogOutUseCase.kt
@@ -0,0 +1,16 @@
+package com.prodhack.moscow2025.domain.usecase.auth
+
+import com.prodhack.moscow2025.domain.interfaces.AuthRepository
+import com.prodhack.moscow2025.domain.interfaces.UserRepository
+import org.koin.core.annotation.Single
+
+@Single
+class LogOutUseCase(
+ private val authRepository: AuthRepository,
+ private val userRepository: UserRepository
+) {
+ suspend operator fun invoke() {
+ authRepository.clearLoginData()
+ userRepository.clearLocalUserData()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LoginUserUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LoginUserUseCase.kt
new file mode 100644
index 0000000..34a97df
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LoginUserUseCase.kt
@@ -0,0 +1,14 @@
+package com.prodhack.moscow2025.domain.usecase.auth
+
+import com.prodhack.moscow2025.domain.models.LoginData
+import com.prodhack.moscow2025.domain.interfaces.AuthRepository
+import org.koin.core.annotation.Single
+
+@Single
+class LoginUserUseCase(
+ private val authRepository: AuthRepository
+) {
+ suspend operator fun invoke(data: LoginData): Result {
+ return authRepository.signInRequest(data)
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/RegisterUserUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/RegisterUserUseCase.kt
new file mode 100644
index 0000000..3a0e301
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/RegisterUserUseCase.kt
@@ -0,0 +1,14 @@
+package com.prodhack.moscow2025.domain.usecase.auth
+
+import com.prodhack.moscow2025.domain.models.RegisterData
+import com.prodhack.moscow2025.domain.interfaces.AuthRepository
+import org.koin.core.annotation.Single
+
+@Single
+class RegisterUserUseCase(
+ private val authRepository: AuthRepository
+) {
+ suspend operator fun invoke(data: RegisterData): Result {
+ return authRepository.signUpRequest(data)
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/UpdateUserUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/UpdateUserUseCase.kt
new file mode 100644
index 0000000..a1b9d1c
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/UpdateUserUseCase.kt
@@ -0,0 +1,15 @@
+package com.prodhack.moscow2025.domain.usecase.auth
+
+import com.prodhack.moscow2025.domain.models.UpdateUserData
+import com.prodhack.moscow2025.domain.models.User
+import com.prodhack.moscow2025.domain.interfaces.UserRepository
+import org.koin.core.annotation.Single
+
+@Single
+class UpdateUserUseCase(
+ private val userRepository: UserRepository
+) {
+ suspend operator fun invoke(data: UpdateUserData): Result {
+ return userRepository.updateProfile(data)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt
new file mode 100644
index 0000000..d176516
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt
@@ -0,0 +1,102 @@
+package com.prodhack.moscow2025.domain.usecase.auth
+
+import android.util.Patterns
+import org.koin.core.annotation.Single
+
+enum class AuthField {
+ FirstName,
+ SecondName,
+ Email,
+ Password,
+ ConfirmPassword,
+ Phone
+}
+
+
+data class ValidationResult(
+ val errors: Map = emptyMap()
+) {
+ val isValid: Boolean
+ get() = errors.isEmpty()
+}
+
+@Single
+class ValidateAuthFieldsUseCase {
+
+ fun validateFillProfile(
+ displayName: String,
+ firstName: String,
+ lastName: String,
+ phone: String
+ ): ValidationResult {
+ val errors = buildMap {
+ if (displayName.isBlank()) put(AuthField.FirstName, "Введите никнейм")
+ if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
+ if (lastName.isBlank()) put(AuthField.SecondName, "Введите фамилию")
+ if (!isPhoneValid(phone)) put(AuthField.Phone, "Некорректный номер телефона")
+ }
+ return ValidationResult(errors)
+ }
+
+ fun validateSignUp(
+ email: String,
+ password: String,
+ confirmPassword: String
+ ): ValidationResult {
+ val errors = buildMap {
+ if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
+ validatePassword(password)?.let { put(AuthField.Password, it) }
+ if (confirmPassword.isBlank()) put(AuthField.ConfirmPassword, "Повторите пароль")
+
+ if (password != confirmPassword) {
+ put(AuthField.ConfirmPassword, "Пароли не совпадают")
+ }
+ }
+ return ValidationResult(errors)
+ }
+
+ fun validatePassword(password: String): String? {
+ if (password.length < 8) {
+ return "Пароль должен быть не менее 8 символов"
+ }
+ if (!password.any { it.isUpperCase() }) {
+ return "Пароль должен содержать хотя бы одну заглавную букву"
+ }
+ if (!password.any { it.isDigit() }) {
+ return "Пароль должен содержать хотя бы одну цифру"
+ }
+ if (!password.any { !it.isLetterOrDigit() }) {
+ return "Пароль должен содержать хотя бы один специальный символ"
+ }
+ return null
+ }
+
+
+ fun validateLogin(
+ email: String,
+ password: String
+ ): ValidationResult {
+ val errors = buildMap {
+ if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
+ validatePassword(password)?.let { put(AuthField.Password, it) }
+ }
+ return ValidationResult(errors)
+ }
+
+ fun validateProfile(
+ firstName: String,
+ secondName: String,
+ ): ValidationResult {
+ val errors = buildMap {
+ if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
+ if (secondName.isBlank()) put(AuthField.SecondName, "Введите фамилию")
+ }
+ return ValidationResult(errors)
+ }
+
+ private fun isEmailValid(email: String): Boolean =
+ email.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches()
+
+ private fun isPhoneValid(phone: String): Boolean =
+ phone.isNotBlank() && Patterns.PHONE.matcher(phone).matches()
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/utils/NetworkError.kt b/app/src/main/java/com/prodhack/moscow2025/domain/utils/NetworkError.kt
new file mode 100644
index 0000000..7bf5034
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/utils/NetworkError.kt
@@ -0,0 +1,38 @@
+package com.prodhack.moscow2025.domain.utils
+
+import com.prodhack.moscow2025.domain.utils.NetworkError.Connection
+import com.prodhack.moscow2025.domain.utils.NetworkError.InputError
+import com.prodhack.moscow2025.domain.utils.NetworkError.Unexpected
+import io.ktor.client.plugins.ClientRequestException
+import io.ktor.client.plugins.RedirectResponseException
+import io.ktor.client.plugins.ServerResponseException
+
+/**
+ * Network error wrapper class
+ */
+sealed class NetworkError : Throwable() {
+
+ /**
+ * Network connection error
+ */
+ class Connection() : NetworkError()
+
+ /**
+ * Unexpected error for example HTTP code - 500 or exception when mapping data
+ */
+ class Unexpected(val error: String) : NetworkError()
+
+ /**
+ * User input error - 400 codes
+ */
+ class InputError(val error: String) : NetworkError()
+}
+
+fun Throwable.convertToNetworkError() =
+ when (this) {
+ is NetworkError -> this
+ is RedirectResponseException -> Unexpected(error = message)
+ is ClientRequestException -> InputError(error = message)
+ is ServerResponseException -> Unexpected(error = message)
+ else -> Connection()
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/utils/TypeAliases.kt b/app/src/main/java/com/prodhack/moscow2025/domain/utils/TypeAliases.kt
new file mode 100644
index 0000000..a7da9b0
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/domain/utils/TypeAliases.kt
@@ -0,0 +1,21 @@
+package com.prodhack.moscow2025.domain.utils
+
+import androidx.paging.PagingData
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Simple wrapper for convenience of network requests in repositories
+ *
+ * @see Flow
+ * @see Result
+ * @see NetworkError
+ */
+internal typealias RemoteWrapper = Flow>
+
+/**
+ * Simple wrapper for convenience of network paging requests in repositories
+ *
+ * @see Flow
+ * @see PagingData
+ */
+internal typealias RemotePagingWrapper = Flow>
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt
new file mode 100644
index 0000000..65a1e1b
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt
@@ -0,0 +1,133 @@
+package com.prodhack.moscow2025.presentation
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.core.content.ContextCompat
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.WindowCompat
+import com.google.firebase.messaging.FirebaseMessaging
+import com.prodhack.moscow2025.domain.usecase.auth.CheckSessionUseCase
+import com.prodhack.moscow2025.presentation.navigation.AppDestination
+import com.prodhack.moscow2025.presentation.navigation.TTasksApp
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.runBlocking
+import org.koin.android.ext.android.inject
+import kotlin.getValue
+
+class MainActivity : ComponentActivity() {
+
+ private val checkSessionUseCase: CheckSessionUseCase by inject()
+
+ private val sessionDestinationState = MutableStateFlow(null)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ val splashScreen = installSplashScreen()
+ var stateLoaded = false
+ splashScreen.setKeepOnScreenCondition {
+ stateLoaded.not()
+ }
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ runBlocking {
+ val isAuthorized = try {
+ checkSessionUseCase()
+ } catch (e: Exception) {
+ false
+ }
+ sessionDestinationState.value =
+ if (isAuthorized) AppDestination.Main else AppDestination.Login
+
+ stateLoaded = true
+ }
+
+ setContent {
+ val sessionDestination by sessionDestinationState.collectAsState()
+ TTasksApp(sessionDestination = sessionDestination, context = this)
+ LaunchedEffect(Unit) {
+ requestPermissions(
+ arrayOf(Manifest.permission.ACCESS_NOTIFICATION_POLICY), 123
+ )
+ FirebaseMessaging.getInstance().token
+ .addOnCompleteListener { task ->
+ if (task.isSuccessful) {
+ val token = task.result
+ Log.d("TOKEN", token)
+ }
+ }
+
+ checkAndRequestNotificationPermission()
+ }
+ }
+ }
+
+ private fun checkAndRequestNotificationPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ when {
+ ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED -> {
+ // Разрешение уже есть, получаем токен
+ getFCMToken()
+ }
+
+ else -> {
+ // Запрашиваем разрешение
+ requestPermissions(
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ 123
+ )
+ }
+ }
+ } else {
+ // Для версий ниже Android 13 разрешение не требуется
+ getFCMToken()
+ }
+ }
+
+ private fun getFCMToken() {
+ FirebaseMessaging.getInstance().token
+ .addOnCompleteListener { task ->
+ if (task.isSuccessful) {
+ val token = task.result
+ Log.d("TOKEN", token)
+ } else {
+ Log.e("TOKEN", "Failed to get token", task.exception)
+ }
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray,
+ deviceId: Int
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == 123) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ getFCMToken()
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/BottomNavigation.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/BottomNavigation.kt
new file mode 100644
index 0000000..6c4a7b7
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/BottomNavigation.kt
@@ -0,0 +1,138 @@
+package com.prodhack.moscow2025.presentation.components
+
+import android.util.Log
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.prodhack.moscow2025.R
+import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
+import com.prodhack.moscow2025.presentation.theme.Paddings
+import com.prodhack.moscow2025.presentation.theme.Shapes
+import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
+
+@Composable
+fun TBottomNavigation(modifier: Modifier = Modifier, selectedPage: Int, onSelect: (Int) -> Unit) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ .padding(vertical = Paddings.small),
+ contentAlignment = Alignment.Center
+ ) {
+
+ val firstIconPos = remember { mutableFloatStateOf(0f) }
+ val secondIconPos = remember { mutableFloatStateOf(0f) }
+ val thirdIconPos = remember { mutableFloatStateOf(0f) }
+
+ val indicatorOffset =
+ with(LocalDensity.current) {
+ when (selectedPage) {
+ 0 -> firstIconPos.floatValue - secondIconPos.floatValue
+ 1 -> 0f
+ 2 -> thirdIconPos.floatValue - secondIconPos.floatValue
+ else -> null
+ }?.toDp()
+ }
+ AnimatedVisibility(indicatorOffset != null) {
+ indicatorOffset?.let {
+ Box(
+ modifier = Modifier
+ .size(85.dp, 45.dp)
+ .offset(x = animateDpAsState(it).value)
+ .background(
+ MaterialTheme.colorScheme.primary,
+ shape = Shapes.smallRoundedBox
+ )
+ )
+ }
+ }
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
+ Icon(
+ modifier = Modifier
+ .size(30.dp)
+ .onGloballyPositioned {
+ it.parentCoordinates?.positionInParent()?.let {
+ firstIconPos.floatValue = it.x
+ }
+ }
+ .noRippleClickable {
+ onSelect(0)
+ },
+ painter = painterResource(R.drawable.ic_trips),
+ tint = animateColorAsState(if (selectedPage == 0) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant).value,
+ contentDescription = "open trips list screen"
+ )
+
+ Icon(
+ modifier = Modifier
+ .size(30.dp)
+ .onGloballyPositioned {
+ it.parentCoordinates?.positionInParent()?.let {
+ secondIconPos.floatValue = it.x
+ }
+ }
+ .noRippleClickable {
+ onSelect(1)
+ },
+ painter = painterResource(R.drawable.ic_home),
+ tint = animateColorAsState(if (selectedPage == 1) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant).value,
+ contentDescription = "open tasks screen"
+ )
+
+ Icon(
+ modifier = Modifier
+ .size(30.dp)
+ .onGloballyPositioned {
+ it.parentCoordinates?.positionInParent()?.let {
+ thirdIconPos.floatValue = it.x
+ }
+ }
+ .noRippleClickable {
+ onSelect(2)
+ },
+ painter = painterResource(R.drawable.ic_profile),
+ tint = animateColorAsState(if (selectedPage == 2) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant).value,
+ contentDescription = "open tasks screen"
+ )
+ }
+
+ }
+}
+
+@Preview
+@Composable
+fun TBottomNavigationPreview() {
+ MoscowHackatonTemplateTheme {
+ Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Bottom) {
+ val page = remember { mutableIntStateOf(0) }
+ TBottomNavigation(selectedPage = page.intValue) {
+ Log.d("click", it.toString())
+ page.intValue = it
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTBigButton.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTBigButton.kt
new file mode 100644
index 0000000..d2283c5
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTBigButton.kt
@@ -0,0 +1,84 @@
+package com.prodhack.moscow2025.presentation.components.standart
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.prodhack.moscow2025.presentation.theme.Shapes
+
+@Composable
+fun BigButton(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+ buttonText: String,
+ isLoading: Boolean
+) {
+ val colorScheme = MaterialTheme.colorScheme
+ val typography = MaterialTheme.typography
+ Button(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(60.dp),
+ shape = Shapes.smallRoundedBox,
+ onClick = onClick,
+ enabled = !isLoading,
+ colors = ButtonColors(
+ containerColor = colorScheme.onPrimary,
+ contentColor = colorScheme.primary,
+ disabledContainerColor = colorScheme.onPrimary,
+ disabledContentColor = colorScheme.primary
+ )
+ ){
+ if (isLoading) {
+ CircularProgressIndicator()
+ } else {
+ Text(
+ text = buttonText,
+ style = typography.labelMedium,
+ fontSize = 24.sp,
+ )
+ }
+ }
+}
+
+@Composable
+fun MediumButton(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+ buttonText: String,
+ isLoading: Boolean
+) {
+ val colorScheme = MaterialTheme.colorScheme
+ val typography = MaterialTheme.typography
+ Button(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(40.dp),
+ shape = Shapes.smallRoundedBox,
+ onClick = onClick,
+ enabled = !isLoading,
+ colors = ButtonColors(
+ containerColor = colorScheme.primary,
+ contentColor = colorScheme.onPrimary,
+ disabledContainerColor = colorScheme.primary,
+ disabledContentColor = colorScheme.onPrimary
+ )
+ ){
+ if (isLoading) {
+ CircularProgressIndicator()
+ } else {
+ Text(
+ text = buttonText,
+ style = typography.labelMedium,
+ fontSize = 16.sp,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTCheckBox.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTCheckBox.kt
new file mode 100644
index 0000000..4804db0
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTCheckBox.kt
@@ -0,0 +1,38 @@
+package com.prodhack.moscow2025.presentation.components.standart
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.prodhack.moscow2025.R
+import com.prodhack.moscow2025.presentation.theme.Shapes
+
+@Composable
+fun TCheckBox(modifier: Modifier = Modifier, checked: Boolean, color: Color) {
+ Box(
+ modifier = modifier
+ .background(Color.Transparent)
+ .border(width = 1.dp, color = color, shape = Shapes.verySmallRoundedBox)
+ ) {
+ AnimatedContent(checked) {
+ if (it) {
+ Icon(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(2.dp),
+ painter = painterResource(R.drawable.ic_checkmark),
+ tint = color,
+ contentDescription = "checkmark"
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTFloatingActionButton.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTFloatingActionButton.kt
new file mode 100644
index 0000000..1c26fa0
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTFloatingActionButton.kt
@@ -0,0 +1,53 @@
+package com.prodhack.moscow2025.presentation.components.standart
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.prodhack.moscow2025.R
+
+@Composable
+fun TTFloatingActionButton(
+ modifier: Modifier,
+ onClick: () -> Unit,
+ text: String
+) {
+ val colorScheme = MaterialTheme.colorScheme
+ val typography = MaterialTheme.typography
+
+ ExtendedFloatingActionButton(
+ modifier = modifier,
+ onClick = {
+ onClick()
+ },
+ shape = RoundedCornerShape(10.dp),
+ containerColor = colorScheme.tertiaryContainer,
+ contentColor = colorScheme.onTertiaryContainer,
+ elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 5.dp)
+ ) {
+ Row {
+ Text(
+ text = text,
+ style = typography.titleMedium,
+ fontSize = 16.sp
+ )
+ Spacer(Modifier.width(10.dp))
+ Icon(
+ painter = painterResource(R.drawable.add_square_outline),
+ contentDescription = null,
+ modifier = Modifier.size(22.dp)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTNamedTextField.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTNamedTextField.kt
new file mode 100644
index 0000000..4cf3bdd
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTNamedTextField.kt
@@ -0,0 +1,47 @@
+package com.prodhack.moscow2025.presentation.components.standart
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun TTNamedTextField(
+ name: String,
+ value: String,
+ onValueChange: (String) -> Unit,
+ error: String? = null,
+ singleLine: Boolean = true,
+ keyboardOptions: KeyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Next
+ ),
+ onDone: (() -> Unit)? = null
+) {
+ Column {
+ Text(
+ text = name,
+ style = typography.labelLarge,
+ fontSize = 14.sp,
+ color = Color.White
+ )
+ Spacer(Modifier.height(5.dp))
+ TTTextField(
+ value = value,
+ onValueChange = onValueChange,
+ error = error,
+ singleLine = singleLine,
+ keyboardOptions = keyboardOptions,
+ onDone = onDone
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTPasswordField.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTPasswordField.kt
new file mode 100644
index 0000000..49100f4
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTPasswordField.kt
@@ -0,0 +1,127 @@
+package com.prodhack.moscow2025.presentation.components.standart
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun TTPasswordField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ label: String,
+ error: String? = null,
+ keyboardOptions: KeyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Next
+ ),
+ onDone: (() -> Unit)? = null
+) {
+ val colorScheme = MaterialTheme.colorScheme
+ val typography = MaterialTheme.typography
+ var isVisible by remember { mutableStateOf(false) }
+
+ Box(
+ Modifier.height(70.dp)
+ ) {
+ Box(
+ Modifier
+ .fillMaxWidth().height(56.dp)
+ .offset(x = 5.dp)
+ .background(
+ color = Color.White,
+ shape = RoundedCornerShape(15.dp)
+ )
+ )
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth().offset(y = 5.dp),
+ value = value,
+ onValueChange = onValueChange,
+ textStyle = typography.labelLarge,
+ placeholder = {
+ Text(
+ label,
+ style = typography.labelLarge,
+ fontSize = 14.sp
+ )
+ },
+ isError = error != null,
+ supportingText = {
+ if (error != null) {
+ Text(
+ text = error,
+ style = typography.labelLarge,
+ fontSize = 12.sp
+ )
+ }
+ },
+ keyboardOptions = keyboardOptions,
+ keyboardActions = KeyboardActions(
+ onDone = {
+ onDone?.invoke()
+ }
+ ),
+ singleLine = true,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedContainerColor = colorScheme.primary,
+ unfocusedContainerColor = colorScheme.primary,
+ errorContainerColor = colorScheme.error,
+
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent,
+ errorBorderColor = Color.Transparent,
+
+ focusedPlaceholderColor = colorScheme.onPrimary,
+ unfocusedPlaceholderColor = colorScheme.onPrimary,
+ errorPlaceholderColor = colorScheme.onError,
+
+ focusedTextColor = colorScheme.onPrimary,
+ unfocusedTextColor = colorScheme.onPrimary,
+ errorTextColor = colorScheme.onError,
+
+ cursorColor = colorScheme.onPrimary,
+ errorCursorColor = colorScheme.onError
+ ),
+ shape = RoundedCornerShape(15.dp),
+ visualTransformation = if (isVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ trailingIcon = {
+ val icon = if (isVisible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility
+ IconButton(onClick = { isVisible = !isVisible }) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp),
+ tint = colorScheme.onPrimary
+ )
+ }
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt
new file mode 100644
index 0000000..fbacc62
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt
@@ -0,0 +1,402 @@
+package com.prodhack.moscow2025.presentation.components.standart
+
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuAnchorType
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.prodhack.moscow2025.R
+import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class)
+@Composable
+fun TTTextField(
+ modifier: Modifier = Modifier,
+ onClick: (() -> Unit)? = null,
+ value: String,
+ onValueChange: (String) -> Unit,
+ readOnly: Boolean = false,
+ label: String = "",
+ error: String? = null,
+ singleLine: Boolean = true,
+ maxLines: Int = 1,
+ keyboardOptions: KeyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Next
+ ),
+ onDone: (() -> Unit)? = null,
+ trailingIcon: @Composable () -> Unit = {}
+) {
+ val typography = MaterialTheme.typography
+ val colorScheme = MaterialTheme.colorScheme
+
+ Box(
+ Modifier.height(70.dp),
+ ) {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ .offset(x = 5.dp)
+ .background(
+ color = Color.White,
+ shape = RoundedCornerShape(15.dp)
+ )
+ )
+ OutlinedTextField(
+ modifier = modifier
+ .fillMaxWidth()
+ .offset(y = 5.dp),
+ value = value,
+ readOnly = readOnly,
+ onValueChange = onValueChange,
+ textStyle = typography.labelLarge,
+ placeholder = {
+ Text(
+ label,
+ style = typography.labelLarge,
+ fontSize = 14.sp,
+ )
+ },
+ isError = error != null,
+ supportingText = {
+ if (error != null) {
+ Spacer(Modifier.height(5.dp))
+ Text(
+ text = error,
+ style = typography.labelLarge,
+ fontSize = 12.sp
+ )
+ }
+ },
+ keyboardOptions = keyboardOptions,
+ keyboardActions = KeyboardActions(
+ onDone = {
+ onDone?.invoke()
+ }
+ ),
+ singleLine = singleLine,
+ maxLines = maxLines,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedContainerColor = colorScheme.primary,
+ unfocusedContainerColor = colorScheme.primary,
+ errorContainerColor = colorScheme.error,
+
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent,
+ errorBorderColor = Color.Transparent,
+
+ focusedPlaceholderColor = colorScheme.onPrimary,
+ unfocusedPlaceholderColor = colorScheme.onPrimary,
+ errorPlaceholderColor = colorScheme.onError,
+
+ focusedTextColor = colorScheme.onPrimary,
+ unfocusedTextColor = colorScheme.onPrimary,
+ errorTextColor = colorScheme.onError,
+
+ cursorColor = colorScheme.onPrimary,
+ errorCursorColor = colorScheme.onError
+ ),
+ shape = RoundedCornerShape(15.dp),
+ trailingIcon = trailingIcon
+ )
+
+ if (readOnly && onClick != null) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .noRippleClickable(onClick)
+ )
+ }
+ }
+}
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TTTextFieldWithDropdown(
+ modifier: Modifier = Modifier,
+ value: String,
+ onValueChange: (String) -> Unit = {},
+ readOnly: Boolean = true,
+ label: String,
+ error: String? = null,
+ singleLine: Boolean = true,
+ maxLines: Int = 1,
+ keyboardOptions: KeyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Next
+ ),
+ dropdownItems: List = emptyList(),
+ onDropdownItemSelected: (T) -> Unit = {},
+ dropDownItem: @Composable (T) -> Unit,
+ trailingIcon: @Composable (Boolean) -> Unit = {
+ Icon(
+ modifier = Modifier
+ .size(24.dp)
+ .rotate(animateFloatAsState(if (it) 180f else 0f).value),
+ painter = painterResource(R.drawable.ic_arr_dropdown),
+ tint = MaterialTheme.colorScheme.onPrimary,
+ contentDescription = null
+ )
+ }
+) {
+ val typography = MaterialTheme.typography
+ val colorScheme = MaterialTheme.colorScheme
+
+ var expanded by remember { mutableStateOf(false) }
+
+ Box(
+ modifier.height(70.dp),
+ ) {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ .offset(x = 5.dp)
+ .background(
+ color = Color.White,
+ shape = RoundedCornerShape(15.dp)
+ )
+ )
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = !expanded },
+ modifier = Modifier.offset(y = 5.dp)
+ ) {
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable),
+ value = value,
+ readOnly = readOnly,
+ onValueChange = onValueChange,
+ textStyle = typography.labelLarge,
+ placeholder = {
+ Text(
+ label,
+ style = typography.labelLarge,
+ fontSize = 14.sp,
+ )
+ },
+ isError = error != null,
+ supportingText = {
+ if (error != null) {
+ Spacer(Modifier.height(5.dp))
+ Text(
+ text = error,
+ style = typography.labelLarge,
+ fontSize = 12.sp
+ )
+ }
+ },
+ keyboardOptions = keyboardOptions,
+ singleLine = singleLine,
+ maxLines = maxLines,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedContainerColor = colorScheme.primary,
+ unfocusedContainerColor = colorScheme.primary,
+ errorContainerColor = colorScheme.error,
+
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent,
+ errorBorderColor = Color.Transparent,
+
+ focusedPlaceholderColor = colorScheme.onPrimary,
+ unfocusedPlaceholderColor = colorScheme.onPrimary,
+ errorPlaceholderColor = colorScheme.onError,
+
+ focusedTextColor = colorScheme.onPrimary,
+ unfocusedTextColor = colorScheme.onPrimary,
+ errorTextColor = colorScheme.onError,
+
+ cursorColor = colorScheme.onPrimary,
+ errorCursorColor = colorScheme.onError
+ ),
+ shape = RoundedCornerShape(15.dp),
+ trailingIcon = {
+ trailingIcon(expanded)
+ }
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ modifier = Modifier.exposedDropdownSize()
+ ) {
+ if (dropdownItems.isEmpty()) {
+ DropdownMenuItem(
+ text = {
+ Text("Здесь пока ничего нет", style = typography.titleMedium)
+ },
+ onClick = {
+ expanded = false
+ }
+ )
+ }
+ dropdownItems.forEach { item ->
+ DropdownMenuItem(
+ text = {
+ dropDownItem(item)
+ },
+ onClick = {
+ onDropdownItemSelected(item)
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TTTextFieldWithSearch(
+ modifier: Modifier = Modifier,
+ value: String,
+ onValueChange: (String) -> Unit = {},
+ readOnly: Boolean = true,
+ label: String,
+ error: String? = null,
+ singleLine: Boolean = true,
+ maxLines: Int = 1,
+ keyboardOptions: KeyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Next
+ ),
+ dropdownItems: List = emptyList(),
+ onDropdownItemSelected: (T) -> Unit = {},
+ dropDownItem: @Composable (T) -> Unit,
+ trailingIcon: @Composable (Boolean) -> Unit = {}
+) {
+ val typography = MaterialTheme.typography
+ val colorScheme = MaterialTheme.colorScheme
+
+ var expanded by remember { mutableStateOf(false) }
+
+ Box(
+ modifier.height(70.dp),
+ ) {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ .offset(x = 5.dp)
+ .background(
+ color = Color.White,
+ shape = RoundedCornerShape(15.dp)
+ )
+ )
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = !expanded },
+ modifier = Modifier.offset(y = 5.dp)
+ ) {
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable),
+ value = value,
+ readOnly = readOnly,
+ onValueChange = onValueChange,
+ textStyle = typography.labelLarge,
+ placeholder = {
+ Text(
+ label,
+ style = typography.labelLarge,
+ fontSize = 14.sp,
+ )
+ },
+ isError = error != null,
+ supportingText = {
+ if (error != null) {
+ Spacer(Modifier.height(5.dp))
+ Text(
+ text = error,
+ style = typography.labelLarge,
+ fontSize = 12.sp
+ )
+ }
+ },
+ keyboardOptions = keyboardOptions,
+ singleLine = singleLine,
+ maxLines = maxLines,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedContainerColor = colorScheme.primary,
+ unfocusedContainerColor = colorScheme.primary,
+ errorContainerColor = colorScheme.error,
+
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent,
+ errorBorderColor = Color.Transparent,
+
+ focusedPlaceholderColor = colorScheme.onPrimary,
+ unfocusedPlaceholderColor = colorScheme.onPrimary,
+ errorPlaceholderColor = colorScheme.onError,
+
+ focusedTextColor = colorScheme.onPrimary,
+ unfocusedTextColor = colorScheme.onPrimary,
+ errorTextColor = colorScheme.onError,
+
+ cursorColor = colorScheme.onPrimary,
+ errorCursorColor = colorScheme.onError
+ ),
+ shape = RoundedCornerShape(15.dp),
+ trailingIcon = {
+ trailingIcon(expanded)
+ }
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ modifier = Modifier.exposedDropdownSize()
+ ) {
+ dropdownItems.forEach { item ->
+ DropdownMenuItem(
+ text = {
+ dropDownItem(item)
+ },
+ onClick = {
+ onDropdownItemSelected(item)
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt
new file mode 100644
index 0000000..d1c6b29
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt
@@ -0,0 +1,44 @@
+package com.prodhack.moscow2025.presentation.components.standart
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.prodhack.moscow2025.R
+import com.prodhack.moscow2025.presentation.theme.Paddings
+
+@Composable
+fun TopLogo(
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ modifier = Modifier.size(100.dp),
+ painter = painterResource(R.drawable.ic_launcher_foreground),
+ contentDescription = "App logo"
+ )
+
+ Spacer(modifier = Modifier.width(Paddings.medium))
+ Text(
+ text = stringResource(R.string.app_name),
+ style = MaterialTheme.typography.titleLarge,
+ fontSize = 48.sp
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt
new file mode 100644
index 0000000..cd12537
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt
@@ -0,0 +1,18 @@
+package com.prodhack.moscow2025.presentation.navigation
+
+/**
+ * Centralized list of application destinations.
+ *
+ * Keeping the routes in one place helps to avoid
+ * string duplication and makes refactoring safer.
+ */
+sealed class AppDestination(val route: String) {
+ data object Login : AppDestination("app/login")
+ data object Register : AppDestination("app/register")
+
+ data object Main : AppDestination("app/main")
+
+
+ data object Profile : AppDestination("app/profile")
+
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt
new file mode 100644
index 0000000..1bf1391
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt
@@ -0,0 +1,99 @@
+package com.prodhack.moscow2025.presentation.navigation
+
+import android.content.Context
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.navigation.compose.currentBackStackEntryAsState
+import com.prodhack.moscow2025.presentation.components.TBottomNavigation
+import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
+
+@Composable
+fun TTasksApp(
+ appState: TTasksAppState = rememberTTasksAppState(),
+ context: Context,
+ sessionDestination: AppDestination? = null
+) {
+ MoscowHackatonTemplateTheme() {
+ val snackbarHostState = remember { SnackbarHostState() }
+ val bottomBarState = remember { mutableStateOf(null) }
+
+ when (appState.navController.currentBackStackEntryAsState().value?.destination?.route) {
+ AppDestination.Login.route -> {
+ bottomBarState.value = null
+ }
+
+ AppDestination.Register.route -> {
+ bottomBarState.value = null
+ }
+
+ AppDestination.Main.route -> {
+ bottomBarState.value = 1
+ }
+
+ AppDestination.Profile.route -> {
+ bottomBarState.value = 2
+ }
+ }
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ snackbar = { data ->
+ Snackbar(
+ snackbarData = data,
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer,
+ shape = MaterialTheme.shapes.medium
+ )
+ }
+ )
+ },
+ bottomBar = {
+ bottomBarState.value?.let { bbState ->
+ TBottomNavigation(
+ modifier = Modifier
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ .windowInsetsPadding(WindowInsets.navigationBars),
+ selectedPage = bbState
+ ) { newPage ->
+ when (newPage) {
+ 0 -> {
+ TODO()
+ }
+
+ 1 -> {
+ appState.navController.navigate(AppDestination.Main.route)
+ }
+
+ 2 -> {
+ appState.navController.navigate(AppDestination.Profile.route)
+ }
+ }
+ }
+ }
+ },
+ ) { padding ->
+ TTasksNavHost(
+ navController = appState.navController,
+ modifier = Modifier.padding(padding),
+ sessionDestination = sessionDestination,
+ snackbarHostState = snackbarHostState,
+ context = context
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksAppState.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksAppState.kt
new file mode 100644
index 0000000..2e62727
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksAppState.kt
@@ -0,0 +1,40 @@
+package com.prodhack.moscow2025.presentation.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.navigation.NavDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.NavOptionsBuilder
+import androidx.navigation.compose.rememberNavController
+import kotlinx.coroutines.CoroutineScope
+
+@Stable
+class TTasksAppState(
+ val navController: NavHostController,
+ val coroutineScope: CoroutineScope
+) {
+ val currentDestination: NavDestination?
+ get() = navController.currentDestination
+
+ fun navigateTo(
+ destination: AppDestination,
+ builder: NavOptionsBuilder.() -> Unit = {}
+ ) {
+ navController.navigate(destination.route, builder)
+ }
+
+ fun navigateBack(): Boolean = navController.popBackStack()
+}
+
+@Composable
+fun rememberTTasksAppState(
+ navController: NavHostController = rememberNavController(),
+ coroutineScope: CoroutineScope = rememberCoroutineScope()
+): TTasksAppState = remember(navController, coroutineScope) {
+ TTasksAppState(
+ navController = navController,
+ coroutineScope = coroutineScope
+ )
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt
new file mode 100644
index 0000000..9f75f4a
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt
@@ -0,0 +1,80 @@
+package com.prodhack.moscow2025.presentation.navigation
+
+import android.content.Context
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import com.prodhack.moscow2025.presentation.screens.main.MainScreen
+import com.prodhack.moscow2025.domain.utils.NetworkError
+import com.prodhack.moscow2025.presentation.screens.login.LoginScreen
+import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen
+import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks
+import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
+import org.koin.compose.viewmodel.koinActivityViewModel
+
+@Composable
+fun TTasksNavHost(
+ navController: NavHostController,
+ modifier: Modifier = Modifier,
+ sessionDestination: AppDestination? = null,
+ context: Context,
+ snackbarHostState: SnackbarHostState
+) {
+ val startDestination = sessionDestination?.route ?: AppDestination.Login.route
+
+ ErrorCollectorScope(context, navController, object : ErrorCallbacks {
+ override fun processConnectionError(networkError: NetworkError.Connection) {
+
+ }
+
+ override fun processUnexpectedError(networkError: NetworkError.Unexpected) {
+
+ }
+
+ }) {
+ NavHost(
+ navController = navController,
+ startDestination = startDestination,
+ modifier = modifier
+ ) {
+ composable(AppDestination.Login.route) {
+ LoginScreen(
+ snackbarHostState = snackbarHostState,
+ onRegisterClick = {
+ navController.navigate(AppDestination.Register.route)
+ },
+ onSuccess = {
+ navController.navigate(AppDestination.Main.route) {
+ popUpTo(AppDestination.Login.route) {
+ inclusive = true
+ }
+ }
+ }
+ )
+ }
+
+ composable(AppDestination.Register.route) {
+ RegisterScreen(
+ snackbarHostState = snackbarHostState,
+ onLoginClick = {
+ navController.popBackStack()
+ },
+ onSuccess = {
+ navController.navigate(AppDestination.Main.route) {
+ popUpTo(AppDestination.Register.route) {
+ inclusive = true
+ }
+ }
+ }
+ )
+ }
+
+ composable(AppDestination.Main.route) {
+ MainScreen()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt
new file mode 100644
index 0000000..fee1d41
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt
@@ -0,0 +1,9 @@
+package com.prodhack.moscow2025.presentation.screens.fillProfile
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+
+@Composable
+fun FillProfileScreen() {
+ Text("Fill profile will be here soon :)")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt
new file mode 100644
index 0000000..df97251
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt
@@ -0,0 +1,185 @@
+package com.prodhack.moscow2025.presentation.screens.fillProfile
+
+import android.content.ContentUris
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.net.Uri
+import android.provider.MediaStore
+import androidx.lifecycle.viewModelScope
+import androidx.paging.map
+import coil.ImageLoader
+import coil.request.ImageRequest
+import com.prodhack.moscow2025.domain.interfaces.GalleryRepository
+import com.prodhack.moscow2025.domain.models.UpdateUserData
+import com.prodhack.moscow2025.domain.usecase.auth.AuthField
+import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase
+import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
+import com.prodhack.moscow2025.presentation.utils.UIState
+import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
+import com.prodhack.moscow2025.presentation.utils.toByteArray
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+data class FillProfileFormState(
+ val displayName: String = "",
+ val firstName: String = "",
+ val lastName: String = "",
+ val phone: String = "",
+ val avatar: ByteArray? = null,
+ val errors: Map = emptyMap()
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as FillProfileFormState
+
+ if (displayName != other.displayName) return false
+ if (firstName != other.firstName) return false
+ if (lastName != other.lastName) return false
+ if (phone != other.phone) return false
+ if (!avatar.contentEquals(other.avatar)) return false
+ if (errors != other.errors) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = displayName.hashCode()
+ result = 31 * result + firstName.hashCode()
+ result = 31 * result + lastName.hashCode()
+ result = 31 * result + phone.hashCode()
+ result = 31 * result + (avatar?.contentHashCode() ?: 0)
+ result = 31 * result + errors.hashCode()
+ return result
+ }
+}
+
+class FillProfileViewModel(
+ private val updateUserUseCase: UpdateUserUseCase,
+ private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase,
+ private val galleryRepository: GalleryRepository
+) : BaseViewModel() {
+ private val _formStateFillProfile = MutableStateFlow(FillProfileFormState())
+ val formStateSignUp: StateFlow = _formStateFillProfile
+
+
+ private val _profileFillState = MutableUIStateFlow()
+ val profileFillState: StateFlow> = _profileFillState
+
+
+ fun onDisplayNameChange(value: String) {
+ _formStateFillProfile.update {
+ it.copy(
+ displayName = value,
+ errors = it.errors - AuthField.Email
+ )
+ }
+ }
+
+ fun onFirstNameChange(value: String) {
+ _formStateFillProfile.update {
+ it.copy(
+ firstName = value,
+ errors = it.errors - AuthField.Email
+ )
+ }
+ }
+
+ fun onLastNameChange(value: String) {
+ _formStateFillProfile.update {
+ it.copy(
+ lastName = value,
+ errors = it.errors - AuthField.Email
+ )
+ }
+ }
+
+ fun onPhoneChange(value: String) {
+ _formStateFillProfile.update {
+ it.copy(
+ phone = value,
+ errors = it.errors - AuthField.Email
+ )
+ }
+ }
+
+
+ val galleryItems = galleryRepository.getImagesIds().map {
+ it.map { id ->
+ ContentUris.withAppendedId(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ id
+ )
+ }
+ }
+
+ fun post(context: Context) {
+ viewModelScope.launch {
+ post(
+ (ImageLoader(context).execute(
+ ImageRequest.Builder(context)
+ .data(currentPhoto).build()
+ ).drawable as BitmapDrawable).bitmap
+ )
+ }
+ }
+
+ fun post(bitmap: Bitmap) {
+ viewModelScope.launch {
+ _formStateFillProfile.update {
+ it.copy(
+ avatar = bitmap.toByteArray()
+ )
+ }
+ }
+ }
+
+ fun clearAvatar() {
+ viewModelScope.launch {
+ _formStateFillProfile.update {
+ it.copy(
+ avatar = null
+ )
+ }
+ }
+ }
+
+ var currentPhoto: Uri? = null
+
+ fun selectImage(photo: Uri) {
+ currentPhoto = photo
+ }
+
+ fun submit() {
+ viewModelScope.launch {
+ val validation = validateAuthFieldsUseCase.validateFillProfile(
+ displayName = _formStateFillProfile.value.displayName,
+ firstName = _formStateFillProfile.value.firstName,
+ lastName = _formStateFillProfile.value.lastName,
+ phone = _formStateFillProfile.value.phone
+ )
+
+ if (!validation.isValid) {
+ _formStateFillProfile.update { it.copy(errors = validation.errors) }
+ return@launch
+ }
+
+ _profileFillState.emit(UIState.Loading())
+
+ val result = updateUserUseCase(
+ UpdateUserData(
+ displayName = _formStateFillProfile.value.displayName,
+ firstName = _formStateFillProfile.value.firstName,
+ lastName = _formStateFillProfile.value.lastName,
+ phone = _formStateFillProfile.value.phone
+ )
+ )
+ result.map { it.id }.collectRequest(_profileFillState)
+ }
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt
new file mode 100644
index 0000000..e7f1dc1
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt
@@ -0,0 +1,206 @@
+package com.prodhack.moscow2025.presentation.screens.login
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import com.prodhack.moscow2025.R
+import com.prodhack.moscow2025.domain.usecase.auth.AuthField
+import com.prodhack.moscow2025.presentation.components.standart.BigButton
+import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField
+import com.prodhack.moscow2025.presentation.components.standart.TTTextField
+import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
+import com.prodhack.moscow2025.presentation.utils.UIState
+import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun ErrorCollectorScope.LoginScreen(
+ modifier: Modifier = Modifier,
+ snackbarHostState: SnackbarHostState,
+ onRegisterClick: () -> Unit,
+ onSuccess: () -> Unit,
+ viewModel: LoginViewModel = koinViewModel()
+) {
+
+ val showDialog = remember { mutableStateOf(false) }
+
+ val testCreds = listOf(
+ Pair("user1@mail.ru", "qQW!!!.rty3nqc18123"),
+ Pair("user2@mail.ru", "qQW!!!.rty3nqc18123"),
+ Pair("user3@mail.ru", "qQW!!!.rty3nqc18123"),
+ Pair("user4@mail.ru", "qQW!!!.rty3nqc18123"),
+ Pair("user5@mail.ru", "qQW!!!.rty3nqc18123")
+ )
+
+ val typography = MaterialTheme.typography
+ val colorScheme = MaterialTheme.colorScheme
+
+ val formState by viewModel.formState.collectAsState()
+
+ var errorText by remember { mutableStateOf("") }
+
+ val authState by viewModel.authState.collectAsStateWithCallbacks(
+ onInputError = {
+ errorText = it.error
+ },
+ onConnectionError = {
+ errorText = "Нет подключения к сети"
+ },
+ onUnexpectedError = {
+ errorText = it.error
+ },
+ onLoading = {
+ errorText = ""
+ },
+ onSuccess = {
+ errorText = ""
+ }
+ )
+
+ LaunchedEffect(authState) {
+ if (authState is UIState.Success) {
+ onSuccess()
+ }
+ }
+
+ LaunchedEffect(errorText) {
+ if (errorText.isNotEmpty()) {
+ snackbarHostState.showSnackbar(
+ message = "Ошибка: $errorText",
+ duration = SnackbarDuration.Short
+ )
+ }
+ }
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .imePadding()
+ .systemBarsPadding(),
+ contentAlignment = Alignment.BottomStart
+ ) {
+ Image(
+ painter = painterResource(R.drawable.lottie),
+ contentDescription = null,
+ modifier = Modifier
+ .width(130.dp),
+ contentScale = ContentScale.Crop
+ )
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 30.dp)
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_launcher_foreground),
+ contentDescription = null,
+ modifier = Modifier
+ .size(250.dp)
+ .noRippleClickable {
+ showDialog.value = true
+ }
+ )
+ Spacer(Modifier.height(10.dp))
+ Text(
+ text = "Вход",
+ style = MaterialTheme.typography.titleLarge,
+ fontSize = 40.sp
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ TTTextField(
+ value = formState.email,
+ onValueChange = viewModel::onEmailChange,
+ label = "Ваш email",
+ error = formState.errors[AuthField.Email]
+ )
+ Spacer(Modifier.height(12.dp))
+ TTPasswordField(
+ value = formState.password,
+ onValueChange = viewModel::onPasswordChange,
+ label = "Пароль",
+ error = formState.errors[AuthField.Password],
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Done
+ ),
+ onDone = viewModel::submit
+ )
+ Spacer(modifier = Modifier.height(40.dp))
+ BigButton(
+ onClick = viewModel::submit,
+ modifier = Modifier.fillMaxWidth(),
+ buttonText = "Войти",
+ isLoading = authState is UIState.Loading
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ TextButton(
+ onClick = onRegisterClick,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = "Зарегистрироваться",
+ style = typography.labelMedium,
+ color = colorScheme.onBackground,
+ fontSize = 24.sp
+ )
+ }
+ Spacer(Modifier.height(80.dp))
+ }
+ if (showDialog.value) {
+ Dialog(
+ onDismissRequest = {
+ showDialog.value = false
+ }
+ ) {
+ Column {
+ testCreds.forEach {
+ Button(onClick = {
+ viewModel.onEmailChange(it.first)
+ viewModel.onPasswordChange(it.second)
+ viewModel.submit()
+ }) {
+ Text(it.first)
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt
new file mode 100644
index 0000000..a75cd2c
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt
@@ -0,0 +1,64 @@
+package com.prodhack.moscow2025.presentation.screens.login
+
+import androidx.lifecycle.viewModelScope
+import com.prodhack.moscow2025.domain.models.LoginData
+import com.prodhack.moscow2025.domain.usecase.auth.AuthField
+import com.prodhack.moscow2025.domain.usecase.auth.LoginUserUseCase
+import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
+import com.prodhack.moscow2025.presentation.utils.UIState
+import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.koin.android.annotation.KoinViewModel
+
+data class LoginFormState(
+ val email: String = "",
+ val password: String = "",
+ val errors: Map = emptyMap()
+)
+
+@KoinViewModel
+class LoginViewModel(
+ private val loginUserUseCase: LoginUserUseCase,
+ private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase
+) : BaseViewModel() {
+
+ private val _formState = MutableStateFlow(LoginFormState())
+ val formState: StateFlow = _formState
+
+ private val _authState = MutableUIStateFlow()
+ val authState: StateFlow> = _authState
+
+ fun onEmailChange(value: String) {
+ _formState.update { it.copy(email = value, errors = it.errors - AuthField.Email) }
+ }
+
+ fun onPasswordChange(value: String) {
+ _formState.update { it.copy(password = value, errors = it.errors - AuthField.Password) }
+ }
+
+ fun submit() {
+ viewModelScope.launch {
+ val validation = validateAuthFieldsUseCase.validateLogin(
+ email = _formState.value.email,
+ password = _formState.value.password
+ )
+ if (!validation.isValid) {
+ _formState.update { it.copy(errors = validation.errors) }
+ return@launch
+ }
+
+ _authState.emit(UIState.Loading())
+
+ val result = loginUserUseCase(
+ LoginData(
+ email = _formState.value.email,
+ password = _formState.value.password
+ )
+ )
+ result.collectRequest(_authState)
+ }
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt
new file mode 100644
index 0000000..e7f84d3
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt
@@ -0,0 +1,282 @@
+package com.prodhack.moscow2025.presentation.screens.main
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
+import org.koin.androidx.compose.koinViewModel
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ErrorCollectorScope.MainScreen(
+ modifier: Modifier = Modifier,
+ viewModel: MainScreenViewModel = koinViewModel()
+) {
+ Text("Main screen will be here soon")
+// val openCalendarModal = remember { mutableStateOf(false) }
+// val openTaskAddSheet = remember { mutableStateOf(false) }
+// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+// val tasks = viewModel.taskList.collectAsLazyPagingItems()
+//
+// val selectedTask = remember { mutableStateOf(null) }
+//
+// Box(
+// modifier = modifier
+// .fillMaxSize()
+// .padding(horizontal = Paddings.large),
+// contentAlignment = Alignment.BottomCenter
+// ) {
+// Column(
+// modifier = Modifier.fillMaxSize(),
+// horizontalAlignment = Alignment.CenterHorizontally
+// ) {
+// Spacer(modifier = Modifier.height(Paddings.large))
+// TopLogo()
+// Spacer(modifier = Modifier.height(Paddings.large))
+//
+// MainScreenFilters(viewModel = viewModel) {
+// openCalendarModal.value = true
+// }
+//
+// Spacer(modifier = Modifier.height(Paddings.large))
+//
+// viewModel.topicList.FoldUIStateWithGlobalCallbacks { topics ->
+// BubbledCategoryFilters(
+// categories = topics,
+// selectedItemId = viewModel.selectedTopicId.value ?: -1
+// ) { categoryId ->
+// viewModel.selectTopic(categoryId)
+// }
+// }
+// Spacer(modifier = Modifier.height(Paddings.large))
+//
+// if (tasks.loadState.hasError) {
+// Text(
+// "Упс, кажется что-то пошло не так :(\nНо мы уже со всем разбираемся!",
+// style = Typography.titleMedium,
+// textAlign = TextAlign.Center,
+// fontSize = 18.sp,
+// color = MaterialTheme.colorScheme.error
+// )
+// } else if (tasks.loadState.append.endOfPaginationReached && tasks.itemCount == 0) {
+// Spacer(modifier = Modifier.weight(1f))
+//
+// Text(
+// "Сделал дело - гуляй смело\nСамое время запланировать новую поездку",
+// style = Typography.titleMedium,
+// textAlign = TextAlign.Center,
+// fontSize = 18.sp,
+// color = MaterialTheme.colorScheme.onBackground
+// )
+// Spacer(modifier = Modifier.height(Paddings.large))
+// BigButton(buttonText = "Начать", onClick = {
+//
+// }, isLoading = false)
+//
+// Spacer(modifier = Modifier.weight(3f))
+//
+// } else {
+// LazyColumn(
+// verticalArrangement = Arrangement.spacedBy(Paddings.small),
+// horizontalAlignment = Alignment.CenterHorizontally
+// ) {
+// items(tasks.itemCount) { it ->
+// val task = tasks[it]
+// task?.let {
+// TaskCard(
+// onClick = {
+// selectedTask.value = it
+// },
+// taskInfo = it,
+// isArchiveWaiting = it.id in viewModel.archiveWaitingTasksIds.value
+// ) {
+// viewModel.toggleTaskAsDone(
+// tripId = it.tripId,
+// taskId = it.id,
+// currState = it.archived
+// )
+// tasks.refresh()
+// }
+// }
+// }
+//
+// item {
+// if (!tasks.loadState.append.endOfPaginationReached) {
+// CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
+// }
+// }
+// }
+// }
+// }
+//
+// TTFloatingActionButton(
+// modifier = Modifier
+// .align(Alignment.BottomCenter)
+// .padding(bottom = Paddings.medium),
+// onClick = {
+// openTaskAddSheet.value = true
+// },
+// text = "Добавить задачу"
+// )
+// }
+//
+//
+// AnimatedVisibility(openCalendarModal.value) {
+// DateRangePickerModal({
+// Log.d("DatePicker", it.toString())
+// if (it.first != null && it.second != null) {
+// viewModel.setDate(Pair(it.first!!, it.second!!))
+// openCalendarModal.value = false
+// }
+// }) {
+// openCalendarModal.value = false
+// }
+// }
+//
+// if (openTaskAddSheet.value) {
+// AddTaskBottomSheet(
+// sheetState = sheetState,
+// onDismiss = {
+// openTaskAddSheet.value = false
+// }
+// )
+// }
+//
+// val cs = MaterialTheme.colorScheme
+//
+// val viewSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+//
+// if (selectedTask.value != null) {
+//
+// val openCalendarModal2 = remember { mutableStateOf(false) }
+//
+// ModalBottomSheet(
+// onDismissRequest = {
+// selectedTask.value = null
+// },
+// sheetState = viewSheetState,
+// dragHandle = {},
+// shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp)
+// ) {
+// Column(
+// modifier = Modifier
+// .padding(horizontal = 24.dp, vertical = 16.dp)
+// .verticalScroll(rememberScrollState()),
+// horizontalAlignment = Alignment.CenterHorizontally
+// ) {
+// Text(
+// text = "Просмотр задачи",
+// color = cs.onSurface,
+// style = Typography.titleMedium,
+// fontSize = 22.sp,
+// textAlign = TextAlign.Center,
+// modifier = Modifier
+// .fillMaxWidth()
+// .padding(bottom = 24.dp, top = 8.dp)
+// )
+//
+// Spacer(modifier = Modifier.height(Paddings.medium))
+//
+// Text(
+// text = selectedTask.value!!.name,
+// color = cs.onSurface,
+// style = Typography.titleMedium,
+// fontSize = 20.sp,
+// textAlign = TextAlign.Center,
+// modifier = Modifier
+// .fillMaxWidth()
+// .padding(bottom = 24.dp, top = 8.dp)
+// )
+//
+// Spacer(modifier = Modifier.height(Paddings.medium))
+//
+//
+// Text(
+// text = "Что нужно сделать",
+// color = cs.onSurface,
+// style = Typography.titleMedium,
+// fontSize = 18.sp,
+// modifier = Modifier
+// .fillMaxWidth()
+// .padding(bottom = 24.dp, top = 8.dp)
+// )
+//
+// Spacer(modifier = Modifier.height(Paddings.small))
+//
+// Text(
+// text = selectedTask.value!!.whatNeedToDo,
+// color = cs.onSurface,
+// style = Typography.labelLarge,
+// fontSize = 16.sp,
+// modifier = Modifier
+// .fillMaxWidth()
+// .padding(bottom = 24.dp, top = 8.dp)
+// )
+//
+// Spacer(modifier = Modifier.height(Paddings.medium))
+//
+// Text(
+// text = "Для чего",
+// color = cs.onSurface,
+// style = Typography.titleMedium,
+// fontSize = 18.sp,
+// modifier = Modifier
+// .fillMaxWidth()
+// .padding(bottom = 24.dp, top = 8.dp)
+// )
+//
+// Spacer(modifier = Modifier.height(Paddings.small))
+//
+// Text(
+// text = selectedTask.value!!.reason,
+// color = cs.onSurface,
+// style = Typography.labelLarge,
+// fontSize = 16.sp,
+// modifier = Modifier
+// .fillMaxWidth()
+// .padding(bottom = 24.dp, top = 8.dp)
+// )
+//
+// Spacer(modifier = Modifier.height(Paddings.large))
+//
+// TTTextField(
+// onClick = {
+// openCalendarModal2.value = true
+// },
+// value = timestampToDateWithYear(selectedTask.value!!.deadline),
+// readOnly = true,
+// onValueChange = {},
+// label = "Дедлайн",
+// trailingIcon = {
+// Icon(
+// modifier = Modifier
+// .size(24.dp),
+// painter = painterResource(
+// R.drawable.ic_calendar
+// ),
+// tint = MaterialTheme.colorScheme.onPrimary,
+// contentDescription = null
+// )
+// }
+// )
+// }
+// }
+//
+// AnimatedVisibility(openCalendarModal2.value) {
+// DatePickerModal({
+// Log.d("DatePicker", it.toString())
+// it?.let { date ->
+// viewModel.changeTaskDeadline(selectedTask.value, date)
+// selectedTask.value = null
+// openCalendarModal.value = false
+// }
+// }) {
+// openCalendarModal.value = false
+// }
+// }
+// }
+}
+
+
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt
new file mode 100644
index 0000000..adbc27e
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt
@@ -0,0 +1,143 @@
+package com.prodhack.moscow2025.presentation.screens.main
+
+import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
+import org.koin.android.annotation.KoinViewModel
+
+
+@KoinViewModel
+class MainScreenViewModel(
+// private val loadTasksUseCase: LoadTasksUseCase,
+// private val loadTasksTopicsListUseCase: LoadTasksTopicListUseCase,
+// private val setFinishedStateToTaskUseCase: SetFinishedStateToTaskUseCase,
+// private val changeDeadlineUseCase: ChangeDeadlineUseCase
+) : BaseViewModel() {
+
+// var userChanged = false
+//
+// // Date filter
+// private val defaultDateFilterState =
+// getStartOfTodayTimestamp().let { Pair(it, it + 86400000) }
+//
+//
+// private val dateState =
+// mutableStateOf(defaultDateFilterState)
+//
+// val dateString = derivedStateOf {
+// Log.d(
+// "MainScreenViewModel",
+// "deriving state , defaultDateFilterState - $defaultDateFilterState"
+// )
+// when (dateState.value.first) {
+// defaultDateFilterState.first -> "Сегодня"
+// defaultDateFilterState.second -> "Завтра"
+// else -> timestampToDate(dateState.value.first)
+// } + "-" +
+// when (dateState.value.second) {
+// defaultDateFilterState.first -> "Сегодня"
+// defaultDateFilterState.second -> "Завтра"
+// else -> timestampToDate(dateState.value.second)
+// }
+// }
+//
+// fun setDate(dates: Pair) {
+// userChanged = true
+// dateState.value =
+// Pair(
+// convertGMTToSystemTimezone(dates.first),
+// convertGMTToSystemTimezone(dates.second)
+// )
+//
+// Log.d("MainScreenViewModel", "updated dates ${dateState.value}")
+// }
+//
+// // Other
+// val onlyMyTasksState = mutableStateOf(true)
+//
+// val showFinished = mutableStateOf(false)
+//
+// // Topic filters
+//
+// val selectedTopicId = mutableStateOf(null)
+//
+// val topicList = MutableUIStateFlow>()
+//
+// fun loadTopicList() {
+// loadTasksTopicsListUseCase().map { it -> it.map { it -> it.map { it.mapToUI() } } }
+// .collectRequest(topicList)
+// }
+//
+// fun selectTopic(id: Int) {
+// if (selectedTopicId.value == id) {
+// selectedTopicId.value = null
+// } else {
+// selectedTopicId.value = id
+// }
+// }
+//
+// // Tasks
+// @OptIn(ExperimentalCoroutinesApi::class)
+// val taskList = snapshotFlow {
+// val dates = dateState.value
+// TaskFilters(
+// dateStart = dates.first,
+// dateEnd = dates.second,
+// topicId = selectedTopicId.value,
+// onlySelf = onlyMyTasksState.value,
+// showArchived = showFinished.value
+// )
+// }.flatMapLatest {
+// loadTasksUseCase(it)
+// }.map { it -> it.map { it.mapToUI() } }
+//
+// private val archiveWaitingTaskJobs = mutableStateMapOf()
+//
+// val archiveWaitingTasksIds = derivedStateOf { archiveWaitingTaskJobs.keys }
+//
+// fun toggleTaskAsDone(tripId: Long, taskId: Long, currState: Boolean) {
+// if (currState) {
+// viewModelScope.launch {
+// setFinishedStateToTaskUseCase(
+// tripId = tripId,
+// taskId = taskId,
+// finishedState = false
+// )
+// }
+// } else {
+// if (taskId in archiveWaitingTasksIds.value) {
+// archiveWaitingTaskJobs[taskId]?.let { job ->
+// if (!job.isCompleted) {
+// job.cancel()
+// }
+// }
+// archiveWaitingTaskJobs.remove(taskId)
+// } else {
+// archiveWaitingTaskJobs[taskId] = viewModelScope.launch {
+// delay(1000)
+// setFinishedStateToTaskUseCase(
+// tripId = tripId,
+// taskId = taskId,
+// finishedState = true
+// )
+// }.also {
+// it.start()
+// }
+// }
+// }
+// }
+//
+// fun update() {
+// loadTopicList()
+// }
+//
+// fun changeTaskDeadline(value: UITaskModel?, date: Long) {
+// viewModelScope.launch {
+// value?.let {
+// changeDeadlineUseCase(value.tripId, value.id, date)
+// }
+// }
+// }
+//
+// init {
+// update()
+// }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt
new file mode 100644
index 0000000..1bf160a
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt
@@ -0,0 +1,182 @@
+package com.prodhack.moscow2025.presentation.screens.register
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.prodhack.moscow2025.R
+import com.prodhack.moscow2025.domain.usecase.auth.AuthField
+import com.prodhack.moscow2025.domain.utils.NetworkError
+import com.prodhack.moscow2025.presentation.components.standart.BigButton
+import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField
+import com.prodhack.moscow2025.presentation.components.standart.TTTextField
+import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
+import com.prodhack.moscow2025.presentation.utils.UIState
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun ErrorCollectorScope.RegisterScreen(
+ modifier: Modifier = Modifier,
+ snackbarHostState: SnackbarHostState,
+ onLoginClick: () -> Unit,
+ onSuccess: () -> Unit,
+ viewModel: RegisterViewModel = koinViewModel()
+) {
+ val typography = MaterialTheme.typography
+ val colorScheme = MaterialTheme.colorScheme
+
+ val formState by viewModel.formStateSignUp.collectAsState()
+ var errorText by remember { mutableStateOf("") }
+ val registerState by viewModel.registerState.collectAsStateWithCallbacks(
+ onInputError = {
+ errorText = it.error
+ },
+ onConnectionError = {
+ errorText = "Нет подключения к сети"
+ },
+ onUnexpectedError = {
+ errorText = it.error
+ },
+ onLoading = {
+ errorText = ""
+ },
+ onSuccess = {
+ errorText = ""
+ }
+ )
+
+ LaunchedEffect(registerState) {
+ if (registerState is UIState.Success) {
+ onSuccess()
+ }
+ }
+
+ LaunchedEffect(errorText) {
+ if (errorText.isNotEmpty()) {
+ snackbarHostState.showSnackbar(
+ message = "Ошибка: $errorText",
+ duration = SnackbarDuration.Short
+ )
+ }
+ }
+
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .imePadding()
+ .systemBarsPadding(),
+ contentAlignment = Alignment.BottomStart
+ ) {
+ Image(
+ painter = painterResource(R.drawable.lottie),
+ contentDescription = null,
+ modifier = Modifier.width(130.dp),
+ contentScale = ContentScale.Crop
+ )
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(start = 30.dp, end = 30.dp)
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "Давайте\nзнакомиться!",
+ style = typography.titleLarge,
+ fontSize = 31.sp
+ )
+ Image(
+ painter = painterResource(R.drawable.ic_launcher_foreground),
+ contentDescription = null,
+ modifier = Modifier.size(140.dp),
+ contentScale = ContentScale.Crop
+ )
+ }
+ Spacer(Modifier.height(20.dp))
+ TTTextField(
+ value = formState.email,
+ onValueChange = viewModel::onEmailChange,
+ label = "Ваш email",
+ error = formState.errors[AuthField.Email]
+ )
+ Spacer(Modifier.height(12.dp))
+ TTPasswordField(
+ value = formState.password,
+ onValueChange = viewModel::onPasswordChange,
+ label = "Пароль",
+ error = formState.errors[AuthField.Password]
+ )
+ Spacer(Modifier.height(12.dp))
+ TTPasswordField(
+ value = formState.confirmPassword,
+ onValueChange = viewModel::onConfirmPasswordChange,
+ label = "Повторите пароль",
+ error = formState.errors[AuthField.ConfirmPassword],
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Done
+ ),
+ onDone = viewModel::submit
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ BigButton(
+ onClick = viewModel::submit,
+ modifier = Modifier.fillMaxWidth(),
+ buttonText = "Зарегистрироваться",
+ isLoading = registerState is UIState.Loading
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ TextButton(
+ onClick = onLoginClick,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = "Уже есть аккаунт?",
+ style = typography.labelMedium,
+ color = colorScheme.onBackground,
+ fontSize = 24.sp
+ )
+ }
+ Spacer(Modifier.height(80.dp))
+ }
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt
new file mode 100644
index 0000000..6f78027
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt
@@ -0,0 +1,110 @@
+package com.prodhack.moscow2025.presentation.screens.register
+
+import androidx.lifecycle.viewModelScope
+import com.prodhack.moscow2025.domain.models.RegisterData
+import com.prodhack.moscow2025.domain.usecase.auth.AuthField
+import com.prodhack.moscow2025.domain.usecase.auth.RegisterUserUseCase
+import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
+import com.prodhack.moscow2025.presentation.utils.UIState
+import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.koin.android.annotation.KoinViewModel
+
+data class RegisterFormState(
+ val email: String = "",
+ val password: String = "",
+ val confirmPassword: String = "",
+ val errors: Map = emptyMap()
+)
+
+@KoinViewModel
+class RegisterViewModel(
+ private val registerUserUseCase: RegisterUserUseCase,
+ private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase
+) : BaseViewModel() {
+
+ private val _formStateSignUp = MutableStateFlow(RegisterFormState())
+ val formStateSignUp: StateFlow = _formStateSignUp
+
+
+ private val _registerState = MutableUIStateFlow()
+ val registerState: StateFlow> = _registerState
+
+
+ fun onEmailChange(value: String) {
+ _formStateSignUp.update { it.copy(email = value, errors = it.errors - AuthField.Email) }
+ }
+
+ fun onPasswordChange(value: String) {
+ _formStateSignUp.update {
+ it.copy(
+ password = value,
+ errors = it.errors - AuthField.Password
+ )
+ }
+ }
+
+ fun onConfirmPasswordChange(value: String) {
+ _formStateSignUp.update {
+ it.copy(
+ confirmPassword = value,
+ errors = it.errors - AuthField.ConfirmPassword
+ )
+ }
+ }
+
+ fun submit() {
+ viewModelScope.launch {
+
+ val validation = validateAuthFieldsUseCase.validateSignUp(
+ email = _formStateSignUp.value.email,
+ password = _formStateSignUp.value.password,
+ confirmPassword = _formStateSignUp.value.confirmPassword
+ )
+
+ if (!validation.isValid) {
+ _formStateSignUp.update { it.copy(errors = validation.errors) }
+ return@launch
+ }
+
+ _registerState.emit(UIState.Loading())
+
+ val result = registerUserUseCase(
+ RegisterData(
+ email = _formStateSignUp.value.email,
+ password = _formStateSignUp.value.password
+ )
+ )
+ result.collectRequest(_registerState)
+
+// val validation = validateAuthFieldsUseCase.validateRegister(
+// firstName = _formStateSignUp.value.firstName,
+// lastName = _formStateSignUp.value.lastName,
+// email = _formStateSignUp.value.email,
+// password = _formStateSignUp.value.password,
+// confirmPassword = _formStateSignUp.value.confirmPassword,
+// phone = _formStateSignUp.value.ph
+// )
+//
+// if (!validation.isValid) {
+// _formStateSignUp.update { it.copy(errors = validation.errors) }
+// return@launch
+// }
+//
+// _registerState.emit(UIState.Loading())
+//
+// val result = registerUserUseCase(
+// RegisterData(
+// firstName = _formStateSignUp.value.firstName,
+// secondName = _formStateSignUp.value.lastName,
+// email = _formStateSignUp.value.email,
+// password = _formStateSignUp.value.password
+// )
+// )
+// result.collectRequest(_registerState)
+ }
+ }
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Color.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Color.kt
new file mode 100644
index 0000000..4dee16c
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Color.kt
@@ -0,0 +1,103 @@
+package com.prodhack.moscow2025.presentation.theme
+
+import androidx.compose.ui.graphics.Color
+
+val WhitePrimary = Color(0xFF1b6b51)
+val WhiteSurfaceTint = Color(0xFF1b6b51)
+val WhiteOnPrimary = Color(0xFFFFFFFF)
+val WhitePrimaryContainer = Color(0xFFa6f2d1)
+val WhiteOnPrimaryContainer = Color(0xFF00513b)
+val WhiteSecondary = Color(0xFF4c6359)
+val WhiteOnSecondary = Color(0xFFFFFFFF)
+val WhiteSecondaryContainer = Color(0xFFcee9db)
+val WhiteOnSecondaryContainer = Color(0xFF354b41)
+val WhiteTertiary = Color(0xFF3e6374)
+val WhiteOnTertiary = Color(0xFFFFFFFF)
+val WhiteTertiaryContainer = Color(0xFFc2e8fd)
+val WhiteOnTertiaryContainer = Color(0xFF264b5c)
+val WhiteError = Color(0xFFba1a1a)
+val WhiteOnError = Color(0xFFFFFFFF)
+val WhiteErrorContainer = Color(0xFFffdad6)
+val WhiteOnErrorContainer = Color(0xFF93000a)
+val WhiteBackground = Color(0xFFf5fbf5)
+val WhiteOnBackground = Color(0xFF171d1a)
+val WhiteSurface = Color(0xFFf5fbf5)
+val WhiteOnSurface = Color(0xFF171d1a)
+val WhiteSurfaceVariant = Color(0xFFdbe5de)
+val WhiteOnSurfaceVariant = Color(0xFF404944)
+val WhiteOutline = Color(0xFF707974)
+val WhiteOutlineVariant = Color(0xFFbfc9c2)
+val WhiteShadow = Color(0xFF000000)
+val WhiteScrim = Color(0xFF000000)
+val WhiteInverseSurface = Color(0xFF2c322e)
+val WhiteInverseOnSurface = Color(0xFFecf2ed)
+val WhiteInversePrimary = Color(0xFF8bd6b6)
+val WhitePrimaryFixed = Color(0xFFa6f2d1)
+val WhiteOnPrimaryFixed = Color(0xFF002116)
+val WhitePrimaryFixedDim = Color(0xFF8bd6b6)
+val WhiteOnPrimaryFixedVariant = Color(0xFF00513b)
+val WhiteSecondaryFixed = Color(0xFFcee9db)
+val WhiteOnSecondaryFixed = Color(0xFF092017)
+val WhiteSecondaryFixedDim = Color(0xFFb3ccbf)
+val WhiteOnSecondaryFixedVariant = Color(0xFF354b41)
+val WhiteTertiaryFixed = Color(0xFFc2e8fd)
+val WhiteOnTertiaryFixed = Color(0xFF001f2a)
+val WhiteTertiaryFixedDim = Color(0xFFa6cce0)
+val WhiteOnTertiaryFixedVariant = Color(0xFF264b5c)
+val WhiteSurfaceDim = Color(0xFFd6dbd6)
+val WhiteSurfaceBright = Color(0xFFf5fbf5)
+val WhiteSurfaceContainerLowest = Color(0xFFFFFFFF)
+val WhiteSurfaceContainerLow = Color(0xFFeff5f0)
+val WhiteSurfaceContainer = Color(0xFFe9efea)
+val WhiteSurfaceContainerHigh = Color(0xFFe4eae4)
+val WhiteSurfaceContainerHighest = Color(0xFFdee4df)
+
+val DarkPrimary = Color(0xFF8bd6b6)
+val DarkSurfaceTint = Color(0xFF8bd6b6)
+val DarkOnPrimary = Color(0xFF003828)
+val DarkPrimaryContainer = Color(0xFF00513b)
+val DarkOnPrimaryContainer = Color(0xFFa6f2d1)
+val DarkSecondary = Color(0xFFb3ccbf)
+val DarkOnSecondary = Color(0xFF1e352b)
+val DarkSecondaryContainer = Color(0xFF354b41)
+val DarkOnSecondaryContainer = Color(0xFFcee9db)
+val DarkTertiary = Color(0xFFa6cce0)
+val DarkOnTertiary = Color(0xFF093544)
+val DarkTertiaryContainer = Color(0xFF264b5c)
+val DarkOnTertiaryContainer = Color(0xFFc2e8fd)
+val DarkError = Color(0xFFffb4ab)
+val DarkOnError = Color(0xFF690005)
+val DarkErrorContainer = Color(0xFF93000a)
+val DarkOnErrorContainer = Color(0xFFffdad6)
+val DarkBackground = Color(0xFF0f1512)
+val DarkOnBackground = Color(0xFFdee4df)
+val DarkSurface = Color(0xFF0f1512)
+val DarkOnSurface = Color(0xFFdee4df)
+val DarkSurfaceVariant = Color(0xFF404944)
+val DarkOnSurfaceVariant = Color(0xFFbfc9c2)
+val DarkOutline = Color(0xFF89938d)
+val DarkOutlineVariant = Color(0xFF404944)
+val DarkShadow = Color(0xFF000000)
+val DarkScrim = Color(0xFF000000)
+val DarkInverseSurface = Color(0xFFdee4df)
+val DarkInverseOnSurface = Color(0xFF2c322e)
+val DarkInversePrimary = Color(0xFF1b6b51)
+val DarkPrimaryFixed = Color(0xFFa6f2d1)
+val DarkOnPrimaryFixed = Color(0xFF002116)
+val DarkPrimaryFixedDim = Color(0xFF8bd6b6)
+val DarkOnPrimaryFixedVariant = Color(0xFF00513b)
+val DarkSecondaryFixed = Color(0xFFcee9db)
+val DarkOnSecondaryFixed = Color(0xFF092017)
+val DarkSecondaryFixedDim = Color(0xFFb3ccbf)
+val DarkOnSecondaryFixedVariant = Color(0xFF354b41)
+val DarkTertiaryFixed = Color(0xFFc2e8fd)
+val DarkOnTertiaryFixed = Color(0xFF001f2a)
+val DarkTertiaryFixedDim = Color(0xFFa6cce0)
+val DarkOnTertiaryFixedVariant = Color(0xFF264b5c)
+val DarkSurfaceDim = Color(0xFF0f1512)
+val DarkSurfaceBright = Color(0xFF343b37)
+val DarkSurfaceContainerLowest = Color(0xFF0a0f0d)
+val DarkSurfaceContainerLow = Color(0xFF171d1a)
+val DarkSurfaceContainer = Color(0xFF1b211e)
+val DarkSurfaceContainerHigh = Color(0xFF252b28)
+val DarkSurfaceContainerHighest = Color(0xFF303633)
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Dim.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Dim.kt
new file mode 100644
index 0000000..dfebcc3
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Dim.kt
@@ -0,0 +1,12 @@
+package com.prodhack.moscow2025.presentation.theme
+
+import androidx.compose.ui.unit.dp
+
+object Paddings {
+ val verySmall = 4.dp
+
+ val small = 8.dp
+
+ val medium = 12.dp
+ val large = 20.dp
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Shapes.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Shapes.kt
new file mode 100644
index 0000000..6563358
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Shapes.kt
@@ -0,0 +1,11 @@
+package com.prodhack.moscow2025.presentation.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.ui.unit.dp
+
+object Shapes{
+ val verySmallRoundedBox = RoundedCornerShape(Paddings.verySmall)
+
+ val smallRoundedBox = RoundedCornerShape(10.dp)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt
new file mode 100644
index 0000000..09fb7de
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt
@@ -0,0 +1,154 @@
+package com.prodhack.moscow2025.presentation.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+// Light color scheme
+private val LightColorScheme = lightColorScheme(
+ primary = WhitePrimary,
+ onPrimary = WhiteOnPrimary,
+ primaryContainer = WhitePrimaryContainer,
+ onPrimaryContainer = WhiteOnPrimaryContainer,
+ inversePrimary = WhiteInversePrimary,
+
+ secondary = WhiteSecondary,
+ onSecondary = WhiteOnSecondary,
+ secondaryContainer = WhiteSecondaryContainer,
+ onSecondaryContainer = WhiteOnSecondaryContainer,
+
+ tertiary = WhiteTertiary,
+ onTertiary = WhiteOnTertiary,
+ tertiaryContainer = WhiteTertiaryContainer,
+ onTertiaryContainer = WhiteOnTertiaryContainer,
+
+ error = WhiteError,
+ onError = WhiteOnError,
+ errorContainer = WhiteErrorContainer,
+ onErrorContainer = WhiteOnErrorContainer,
+
+ background = WhiteBackground,
+ onBackground = WhiteOnBackground,
+ surface = WhiteSurface,
+ onSurface = WhiteOnSurface,
+ surfaceVariant = WhiteSurfaceVariant,
+ onSurfaceVariant = WhiteOnSurfaceVariant,
+ inverseSurface = WhiteInverseSurface,
+ inverseOnSurface = WhiteInverseOnSurface,
+
+ outline = WhiteOutline,
+ outlineVariant = WhiteOutlineVariant,
+
+ scrim = WhiteScrim,
+ surfaceTint = WhiteSurfaceTint,
+
+ // Fixed colors
+ primaryFixed = WhitePrimaryFixed,
+ onPrimaryFixed = WhiteOnPrimaryFixed,
+ primaryFixedDim = WhitePrimaryFixedDim,
+ onPrimaryFixedVariant = WhiteOnPrimaryFixedVariant,
+
+ secondaryFixed = WhiteSecondaryFixed,
+ onSecondaryFixed = WhiteOnSecondaryFixed,
+ secondaryFixedDim = WhiteSecondaryFixedDim,
+ onSecondaryFixedVariant = WhiteOnSecondaryFixedVariant,
+
+ tertiaryFixed = WhiteTertiaryFixed,
+ onTertiaryFixed = WhiteOnTertiaryFixed,
+ tertiaryFixedDim = WhiteTertiaryFixedDim,
+ onTertiaryFixedVariant = WhiteOnTertiaryFixedVariant,
+
+ surfaceDim = WhiteSurfaceDim,
+ surfaceBright = WhiteSurfaceBright,
+ surfaceContainerLowest = WhiteSurfaceContainerLowest,
+ surfaceContainerLow = WhiteSurfaceContainerLow,
+ surfaceContainer = WhiteSurfaceContainer,
+ surfaceContainerHigh = WhiteSurfaceContainerHigh,
+ surfaceContainerHighest = WhiteSurfaceContainerHighest
+)
+
+// Dark color scheme
+private val DarkColorScheme = darkColorScheme(
+ primary = DarkPrimary,
+ onPrimary = DarkOnPrimary,
+ primaryContainer = DarkPrimaryContainer,
+ onPrimaryContainer = DarkOnPrimaryContainer,
+ inversePrimary = DarkInversePrimary,
+
+ secondary = DarkSecondary,
+ onSecondary = DarkOnSecondary,
+ secondaryContainer = DarkSecondaryContainer,
+ onSecondaryContainer = DarkOnSecondaryContainer,
+
+ tertiary = DarkTertiary,
+ onTertiary = DarkOnTertiary,
+ tertiaryContainer = DarkTertiaryContainer,
+ onTertiaryContainer = DarkOnTertiaryContainer,
+
+ error = DarkError,
+ onError = DarkOnError,
+ errorContainer = DarkErrorContainer,
+ onErrorContainer = DarkOnErrorContainer,
+
+ background = DarkBackground,
+ onBackground = DarkOnBackground,
+ surface = DarkSurface,
+ onSurface = DarkOnSurface,
+ surfaceVariant = DarkSurfaceVariant,
+ onSurfaceVariant = DarkOnSurfaceVariant,
+ inverseSurface = DarkInverseSurface,
+ inverseOnSurface = DarkInverseOnSurface,
+
+ outline = DarkOutline,
+ outlineVariant = DarkOutlineVariant,
+
+ scrim = DarkScrim,
+ surfaceTint = DarkSurfaceTint,
+
+ // Fixed colors
+ primaryFixed = DarkPrimaryFixed,
+ onPrimaryFixed = DarkOnPrimaryFixed,
+ primaryFixedDim = DarkPrimaryFixedDim,
+ onPrimaryFixedVariant = DarkOnPrimaryFixedVariant,
+
+ secondaryFixed = DarkSecondaryFixed,
+ onSecondaryFixed = DarkOnSecondaryFixed,
+ secondaryFixedDim = DarkSecondaryFixedDim,
+ onSecondaryFixedVariant = DarkOnSecondaryFixedVariant,
+
+ tertiaryFixed = DarkTertiaryFixed,
+ onTertiaryFixed = DarkOnTertiaryFixed,
+ tertiaryFixedDim = DarkTertiaryFixedDim,
+ onTertiaryFixedVariant = DarkOnTertiaryFixedVariant,
+
+ surfaceDim = DarkSurfaceDim,
+ surfaceBright = DarkSurfaceBright,
+ surfaceContainerLowest = DarkSurfaceContainerLowest,
+ surfaceContainerLow = DarkSurfaceContainerLow,
+ surfaceContainer = DarkSurfaceContainer,
+ surfaceContainerHigh = DarkSurfaceContainerHigh,
+ surfaceContainerHighest = DarkSurfaceContainerHighest
+)
+
+@Composable
+fun MoscowHackatonTemplateTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Type.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Type.kt
new file mode 100644
index 0000000..719ffca
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Type.kt
@@ -0,0 +1,40 @@
+package com.prodhack.moscow2025.presentation.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import com.prodhack.moscow2025.R
+
+val TinkoffSansFamily = FontFamily(
+ Font(
+ R.font.tinkoff_sans_bold,
+ FontWeight.Bold
+ ),
+ Font(
+ R.font.tinkoff_sans_regular,
+ FontWeight.Normal
+ ),
+ Font(
+ R.font.tinkoff_sans_medium,
+ FontWeight.Medium
+ )
+)
+
+val Typography = Typography(
+ titleLarge = TextStyle(
+ fontFamily = TinkoffSansFamily,
+ fontWeight = FontWeight.Bold
+ ),
+ titleMedium = TextStyle(
+ fontFamily = TinkoffSansFamily,
+ fontWeight = FontWeight.Medium
+ ),
+ labelLarge = TextStyle(
+ fontFamily = TinkoffSansFamily,
+ fontWeight = FontWeight.Normal
+ )
+
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/SetUtils.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/SetUtils.kt
new file mode 100644
index 0000000..f4d6e9a
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/SetUtils.kt
@@ -0,0 +1,9 @@
+package com.prodhack.moscow2025.presentation.utils
+
+fun MutableSet.toggleItem(item: T) {
+ if (item in this) {
+ remove(item)
+ } else {
+ add(item)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/StringUtils.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/StringUtils.kt
new file mode 100644
index 0000000..741b9de
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/StringUtils.kt
@@ -0,0 +1,3 @@
+package com.prodhack.moscow2025.presentation.utils
+
+fun String?.notNullOrBlank() = this != null && this.isNotBlank()
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/TimeUtils.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/TimeUtils.kt
new file mode 100644
index 0000000..1e6806a
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/TimeUtils.kt
@@ -0,0 +1,55 @@
+package com.prodhack.moscow2025.presentation.utils
+
+import java.text.SimpleDateFormat
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+import java.time.temporal.ChronoUnit
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+fun daysUntilTimestampZoned(targetTimestamp: Long, zoneId: ZoneId = ZoneId.systemDefault()): Int {
+ val now = Instant.now().atZone(zoneId)
+ val targetTime = Instant.ofEpochMilli(targetTimestamp).atZone(zoneId)
+
+ return ChronoUnit.DAYS.between(now, targetTime).toInt()
+}
+
+fun getStartOfDayTimestamp(date: Date): Long {
+ val localDate = date.toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate()
+ return localDate.atStartOfDay(ZoneId.systemDefault())
+ .toInstant()
+ .toEpochMilli()
+}
+
+fun getStartOfTodayTimestamp(): Long {
+ val today = LocalDate.now()
+ return today.atStartOfDay(ZoneId.systemDefault())
+ .toInstant()
+ .toEpochMilli()
+}
+
+fun timestampToDate(timestamp: Long, timeZone: TimeZone = TimeZone.getDefault()): String {
+ val date = Date(timestamp)
+ val formatter = SimpleDateFormat("dd.MM", Locale.getDefault())
+ formatter.timeZone = timeZone
+ return formatter.format(date)
+}
+
+fun timestampToDateWithYear(timestamp: Long, timeZone: TimeZone = TimeZone.getDefault()): String {
+ val date = Date(timestamp)
+ val formatter = SimpleDateFormat("dd.MM.YYYY", Locale.getDefault())
+ formatter.timeZone = timeZone
+ return formatter.format(date)
+}
+
+fun convertGMTToSystemTimezone(gmtTimestamp: Long): Long {
+ return getStartOfDayTimestamp(Date(gmtTimestamp))
+}
+
+fun timestampToIso(timestamp: Long): String {
+ return Instant.ofEpochMilli(timestamp).toString()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt
new file mode 100644
index 0000000..0f56b58
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt
@@ -0,0 +1,226 @@
+package com.prodhack.moscow2025.presentation.utils
+
+import android.content.Context
+import android.util.Log
+import android.widget.Toast
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.compose.rememberNavController
+import com.prodhack.moscow2025.domain.utils.NetworkError
+import com.prodhack.moscow2025.domain.utils.convertToNetworkError
+import com.prodhack.moscow2025.presentation.utils.ui.placeholders.ErrorPlaceholder
+import com.prodhack.moscow2025.presentation.utils.ui.placeholders.LoadingPlaceholder
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+sealed class UIState {
+ class Idle : UIState()
+ class Loading : UIState()
+ class Error(val error: NetworkError) : UIState()
+ class Success(val data: T) : UIState()
+
+ fun map(mapper: (T) -> S): UIState {
+ return when (this) {
+ is Idle -> Idle()
+ is Loading -> Loading()
+ is Error -> Error(this.error)
+ is Success -> Success(mapper(this.data))
+ }
+ }
+
+ fun getOrNull(): T? = if (this is Success) {
+ data
+ } else {
+ null
+ }
+
+ val isSuccess: Boolean
+ get() = this is Success
+}
+
+interface ErrorCallbacks {
+ fun processConnectionError(networkError: NetworkError.Connection)
+ fun processUnexpectedError(networkError: NetworkError.Unexpected)
+}
+
+open class ErrorCollectorScope(
+ private val context: Context,
+ val navController: NavController,
+ private val errorCallbacks: ErrorCallbacks
+) {
+ companion object {
+ private const val TAG = "ErrorCollectorScope"
+ }
+
+ @Composable
+ fun Flow>.collectAsStateWithCallbacks(
+ onInputError: ((NetworkError.InputError) -> Unit) = {
+ Toast.makeText(context, "Something went wrong", Toast.LENGTH_SHORT)
+ .show()
+ },
+ onUnexpectedError: ((NetworkError.Unexpected) -> Unit) = {},
+ onConnectionError: ((NetworkError.Connection) -> Unit) = {},
+ onLoading: (() -> Unit) = {},
+ onSuccess: (T) -> Unit = {}
+ ): State> = this.onEach {
+ when (it) {
+ is UIState.Loading -> {
+ onLoading()
+ }
+
+ is UIState.Error -> {
+ Log.e(TAG, "collected error ${it.error}")
+ when (it.error) {
+ is NetworkError.Connection -> {
+ errorCallbacks.processConnectionError(it.error)
+ onConnectionError.invoke(it.error)
+ }
+
+ is NetworkError.Unexpected -> {
+ errorCallbacks.processUnexpectedError(it.error)
+ onUnexpectedError.invoke(it.error)
+ }
+ is NetworkError.InputError -> onInputError.invoke(it.error)
+ }
+ }
+
+ is UIState.Success -> {
+ onSuccess.invoke(it.data)
+ }
+
+ else -> {}
+ }
+ }.collectAsState(UIState.Idle())
+
+
+ @Composable
+ fun Flow>.collectAsValueStateWithCallbacks(
+ onInputError: ((NetworkError.InputError) -> Unit) = {
+ Toast.makeText(context, "Something went wrong", Toast.LENGTH_SHORT)
+ .show()
+ },
+ onLoading: (() -> Unit) = {},
+ onSuccess: (T) -> Unit = {}
+ ): State = this.map {
+ when (it) {
+ is UIState.Loading -> {
+ onLoading()
+ null
+ }
+
+ is UIState.Error -> {
+ Log.e(TAG, "collected error ${it.error}")
+ when (it.error) {
+ is NetworkError.Connection -> errorCallbacks.processConnectionError(it.error)
+ is NetworkError.Unexpected -> errorCallbacks.processUnexpectedError(it.error)
+ is NetworkError.InputError -> onInputError.invoke(it.error)
+ }
+ null
+ }
+
+ is UIState.Success -> {
+ onSuccess.invoke(it.data)
+ it.data
+ }
+
+ else -> {
+ null
+ }
+ }
+ }.collectAsState(null)
+
+ @Composable
+ fun Flow>.FoldUIStateWithGlobalCallbacks(
+ modifier: Modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 20.dp),
+ onIdle: @Composable () -> Unit = {},
+ onError: @Composable (NetworkError) -> Unit = { ErrorPlaceholder(modifier = modifier) { navController?.popBackStack() } },
+ onLoading: @Composable () -> Unit = { LoadingPlaceholder(modifier = modifier) },
+ onSuccess: @Composable (T) -> Unit
+ ) {
+ val state = this.onEach {
+ if (it is UIState.Error) {
+ Log.e(TAG, "collected error ${it.error}")
+ when (it.error) {
+ is NetworkError.Connection -> errorCallbacks.processConnectionError(it.error)
+ is NetworkError.Unexpected -> errorCallbacks.processUnexpectedError(it.error)
+ else -> {}
+ }
+ }
+ }.collectAsState(initial = UIState.Idle()).value
+
+ when (state) {
+ is UIState.Idle -> {
+ onIdle()
+ }
+
+ is UIState.Error -> {
+ onError(state.error.convertToNetworkError())
+ }
+
+ is UIState.Loading -> {
+ onLoading()
+ }
+
+ is UIState.Success -> {
+ onSuccess(state.data)
+ }
+ }
+ }
+
+ @Composable
+ fun UIState.FoldUIStateWithGlobalCallbacks(
+ modifier: Modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 20.dp),
+ onIdle: @Composable () -> Unit = {},
+ onError: @Composable (NetworkError) -> Unit = { ErrorPlaceholder(modifier = modifier) { navController?.popBackStack() } },
+ onLoading: @Composable () -> Unit = { LoadingPlaceholder(modifier = modifier) },
+ onSuccess: @Composable (T) -> Unit
+ ) {
+ if (this is UIState.Error) {
+ Log.e(TAG, "collected error ${this.error}")
+ when (error) {
+ is NetworkError.Connection -> errorCallbacks.processConnectionError(error)
+ is NetworkError.Unexpected -> errorCallbacks.processUnexpectedError(error)
+ else -> {}
+ }
+ }
+
+ when (this) {
+ is UIState.Idle -> {
+ onIdle()
+ }
+
+ is UIState.Error -> {
+ onError(error.convertToNetworkError())
+ }
+
+ is UIState.Loading -> {
+ onLoading()
+ }
+
+ is UIState.Success -> {
+ onSuccess.invoke(data)
+ }
+ }
+ }
+}
+
+@Composable
+fun ErrorCollectorScope(
+ context: Context,
+ navController: NavController? = null,
+ errorCallbacks: ErrorCallbacks,
+ content: @Composable ErrorCollectorScope.() -> Unit
+) {
+ ErrorCollectorScope(context, navController ?: rememberNavController(), errorCallbacks).content()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/base/BaseViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/base/BaseViewModel.kt
new file mode 100644
index 0000000..fdbf6d9
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/base/BaseViewModel.kt
@@ -0,0 +1,78 @@
+package com.prodhack.moscow2025.presentation.utils.base
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import androidx.paging.map
+import com.prodhack.moscow2025.presentation.utils.UIState
+import com.prodhack.moscow2025.domain.utils.convertToNetworkError
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/**
+ * Base class for all [ViewModel]s
+ */
+abstract class BaseViewModel : ViewModel() {
+
+ /**
+ * Creates [MutableStateFlow] with [UIState] and the given initial value [UIState.Idle]
+ */
+ @Suppress("FunctionName")
+ protected fun MutableUIStateFlow(defaultValue: T? = null) =
+ MutableStateFlow>(defaultValue?.let { UIState.Success(it) } ?: UIState.Idle())
+
+ /**
+ * Reset [MutableUIStateFlow] to [UIState.Idle]
+ */
+ protected fun MutableStateFlow>.reset() {
+ value = UIState.Idle()
+ }
+
+ /**
+ * Collect network request
+ *
+ * @return [UIState] depending request result
+ */
+ protected fun Flow>.collectRequest(
+ state: MutableStateFlow>,
+ ) {
+ viewModelScope.launch {
+ state.value = UIState.Loading()
+ this@collectRequest.collect {
+ state.value = if (it.isSuccess) {
+ UIState.Success(it.getOrNull()!!)
+ } else {
+ UIState.Error(it.exceptionOrNull()!!.convertToNetworkError())
+ }
+ }
+ }
+ }
+
+ /**
+ * Collect network request
+ *
+ * @return [UIState] depending request result
+ */
+ protected fun Result.collectRequest(
+ state: MutableStateFlow>
+ ) {
+ state.value = UIState.Loading()
+ state.value = if (isSuccess) {
+ UIState.Success(getOrNull()!!)
+ } else {
+ UIState.Error(exceptionOrNull()!!.convertToNetworkError())
+ }
+ }
+
+
+ /**
+ * Collect paging request
+ */
+ protected fun Flow>.collectPagingRequest(
+ mappedData: suspend (T) -> S
+ ) = map { it.map { data -> mappedData(data) } }.cachedIn(viewModelScope)
+
+}
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/imageToByteArray.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/imageToByteArray.kt
new file mode 100644
index 0000000..8652d62
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/imageToByteArray.kt
@@ -0,0 +1,14 @@
+package com.prodhack.moscow2025.presentation.utils
+
+import android.graphics.Bitmap
+import java.io.ByteArrayOutputStream
+
+fun Bitmap.toByteArray(): ByteArray {
+ val stream = ByteArrayOutputStream() // Create a ByteArrayOutputStream
+ compress(
+ Bitmap.CompressFormat.JPEG,
+ 100,
+ stream
+ ) // Compress Bitmap to PNG with 100% quality
+ return stream.toByteArray() // Convert stream to byte array
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/CalendarModal.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/CalendarModal.kt
new file mode 100644
index 0000000..d76e76d
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/CalendarModal.kt
@@ -0,0 +1,88 @@
+package com.prodhack.moscow2025.presentation.utils.ui
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DatePicker
+import androidx.compose.material3.DatePickerDialog
+import androidx.compose.material3.DateRangePicker
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberDatePickerState
+import androidx.compose.material3.rememberDateRangePickerState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun DateRangePickerModal(
+ onDateRangeSelected: (Pair) -> Unit,
+ onDismiss: () -> Unit
+) {
+ val dateRangePickerState = rememberDateRangePickerState()
+
+ DatePickerDialog(
+ onDismissRequest = onDismiss,
+ confirmButton = {
+ TextButton(
+ onClick = {
+ onDateRangeSelected(
+ Pair(
+ dateRangePickerState.selectedStartDateMillis,
+ dateRangePickerState.selectedEndDateMillis
+ )
+ )
+ onDismiss()
+ }
+ ) {
+ Text("OK")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text("Cancel")
+ }
+ }
+ ) {
+ DateRangePicker(
+ state = dateRangePickerState,
+ title = {
+ Text(
+ text = "Select date range"
+ )
+ },
+ showModeToggle = false,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(500.dp)
+ .padding(16.dp)
+ )
+ }
+}
+
+@Composable
+fun DatePickerModal(
+ onDateSelected: (Long?) -> Unit,
+ onDismiss: () -> Unit
+) {
+ val datePickerState = rememberDatePickerState()
+
+ DatePickerDialog(
+ onDismissRequest = onDismiss,
+ confirmButton = {
+ TextButton(onClick = {
+ onDateSelected(datePickerState.selectedDateMillis)
+ onDismiss()
+ }) {
+ Text("OK")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text("Cancel")
+ }
+ }
+ ) {
+ DatePicker(state = datePickerState)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/ColoredClickable.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/ColoredClickable.kt
new file mode 100644
index 0000000..ef25098
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/ColoredClickable.kt
@@ -0,0 +1,42 @@
+package com.prodhack.moscow2025.presentation.utils.ui
+
+import androidx.compose.foundation.LocalIndication
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.ripple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.debugInspectorInfo
+
+fun Modifier.clickable(
+ rippleColor: Color? = null,
+ onClick: () -> Unit
+) = composed(
+ inspectorInfo = debugInspectorInfo {
+ name = "clickable"
+ properties["rippleColor"] = rippleColor
+ properties["onClick"] = onClick
+ }
+) {
+ this.clickable(
+ onClick = onClick,
+ indication = rippleColor?.let {
+ ripple(
+ color = it
+ )
+ } ?: LocalIndication.current,
+ interactionSource = remember { MutableInteractionSource() }
+ )
+}
+
+@Composable
+fun Modifier.noRippleClickable(
+ onClick: () -> Unit
+) = this.clickable(
+ onClick = onClick,
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/ErrorPlaceHolder.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/ErrorPlaceHolder.kt
new file mode 100644
index 0000000..e3a7555
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/ErrorPlaceHolder.kt
@@ -0,0 +1,41 @@
+package com.prodhack.moscow2025.presentation.utils.ui.placeholders
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
+
+
+data class ErrorTexts(
+ val title: String = "Error",
+ val mainText: String = "Oh mio dio! \n" +
+ "Sembra che qualcosa non va",
+ val description: String = "Lavoreremo per sistemare le cose, ti chiediamo tornare più tardi."
+)
+
+@Composable
+fun ErrorPlaceholder(
+ modifier: Modifier = Modifier,
+ showTop: Boolean = false,
+ small: Boolean = false,
+ showButton: Boolean = true,
+ errorTexts: ErrorTexts = ErrorTexts(),
+ actionText: String = "Ok",
+ onAction: () -> Unit
+) {
+ Text("Error")
+}
+
+
+@Preview
+@Composable
+fun ErrorPlaceHolderPreview() {
+ MoscowHackatonTemplateTheme {
+ Scaffold {
+ ErrorPlaceholder(modifier = Modifier.padding(it), showTop = true) { }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/LoadingPlaceholder.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/LoadingPlaceholder.kt
new file mode 100644
index 0000000..a253804
--- /dev/null
+++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/LoadingPlaceholder.kt
@@ -0,0 +1,32 @@
+package com.prodhack.moscow2025.presentation.utils.ui.placeholders
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
+
+@Composable
+fun LoadingPlaceholder(
+ modifier: Modifier = Modifier,
+ text: String = "Già quasi scaricato, per favore aspetta un po"
+) {
+ Text(modifier = modifier, text = text)
+}
+
+@Preview
+@Composable
+private fun LoadingPlaceholderPreview() {
+ Scaffold { paddingValues ->
+ MoscowHackatonTemplateTheme() {
+ LoadingPlaceholder(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(paddingValues)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/add_square_outline.xml b/app/src/main/res/drawable/add_square_outline.xml
new file mode 100644
index 0000000..0604309
--- /dev/null
+++ b/app/src/main/res/drawable/add_square_outline.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_arr_dropdown.xml b/app/src/main/res/drawable/ic_arr_dropdown.xml
new file mode 100644
index 0000000..0c01396
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arr_dropdown.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..ec87b7a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_calendar.xml
@@ -0,0 +1,13 @@
+
+
+
+
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..330aeae
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chart.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_checkmark.xml b/app/src/main/res/drawable/ic_checkmark.xml
new file mode 100644
index 0000000..e583e14
--- /dev/null
+++ b/app/src/main/res/drawable/ic_checkmark.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_documents.xml b/app/src/main/res/drawable/ic_documents.xml
new file mode 100644
index 0000000..a40950b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_documents.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_flag_filled.xml b/app/src/main/res/drawable/ic_flag_filled.xml
new file mode 100644
index 0000000..2489881
--- /dev/null
+++ b/app/src/main/res/drawable/ic_flag_filled.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_flag_unfilled.xml b/app/src/main/res/drawable/ic_flag_unfilled.xml
new file mode 100644
index 0000000..daa4f77
--- /dev/null
+++ b/app/src/main/res/drawable/ic_flag_unfilled.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_group.xml b/app/src/main/res/drawable/ic_group.xml
new file mode 100644
index 0000000..bb9d073
--- /dev/null
+++ b/app/src/main/res/drawable/ic_group.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml
new file mode 100644
index 0000000..b8ae911
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home.xml
@@ -0,0 +1,13 @@
+
+
+
+
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..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_magnifer.xml b/app/src/main/res/drawable/ic_magnifer.xml
new file mode 100644
index 0000000..38a28a4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_magnifer.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml
new file mode 100644
index 0000000..c88b884
--- /dev/null
+++ b/app/src/main/res/drawable/ic_minus.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..a71432d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml
new file mode 100644
index 0000000..800e533
--- /dev/null
+++ b/app/src/main/res/drawable/ic_plus.xml
@@ -0,0 +1,13 @@
+
+
+
+
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..45abdfd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_trips.xml b/app/src/main/res/drawable/ic_trips.xml
new file mode 100644
index 0000000..cadeea0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_trips.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/logout_icon.xml b/app/src/main/res/drawable/logout_icon.xml
new file mode 100644
index 0000000..46590a0
--- /dev/null
+++ b/app/src/main/res/drawable/logout_icon.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/lottie.png b/app/src/main/res/drawable/lottie.png
new file mode 100644
index 0000000..ba1559f
Binary files /dev/null and b/app/src/main/res/drawable/lottie.png differ
diff --git a/app/src/main/res/font/tinkoff_sans_bold.ttf b/app/src/main/res/font/tinkoff_sans_bold.ttf
new file mode 100644
index 0000000..e8eb528
Binary files /dev/null and b/app/src/main/res/font/tinkoff_sans_bold.ttf differ
diff --git a/app/src/main/res/font/tinkoff_sans_medium.ttf b/app/src/main/res/font/tinkoff_sans_medium.ttf
new file mode 100644
index 0000000..d62120c
Binary files /dev/null and b/app/src/main/res/font/tinkoff_sans_medium.ttf differ
diff --git a/app/src/main/res/font/tinkoff_sans_regular.ttf b/app/src/main/res/font/tinkoff_sans_regular.ttf
new file mode 100644
index 0000000..13963b9
Binary files /dev/null and b/app/src/main/res/font/tinkoff_sans_regular.ttf differ
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..74d295c
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ MoscowHackatonTemplate
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..bf97c8c
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/prodhack/moscow2025/ExampleUnitTest.kt b/app/src/test/java/com/prodhack/moscow2025/ExampleUnitTest.kt
new file mode 100644
index 0000000..f31b7db
--- /dev/null
+++ b/app/src/test/java/com/prodhack/moscow2025/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.prodhack.moscow2025
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..155fa48
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,20 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.google.services.gmc) apply false
+ alias(libs.plugins.androidApplication) apply false
+ alias(libs.plugins.jetbrainsKotlinAndroid) apply false
+ alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.googleKsp) apply false
+ alias(libs.plugins.room) apply false
+ alias(libs.plugins.serialization) apply false
+ alias(libs.plugins.kotzilla) apply false
+}
+
+buildscript {
+ dependencies {
+ classpath(libs.secrets.gradle.plugin)
+ classpath(libs.build.tools)
+ classpath(libs.google.services.gmc)
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..47517b0
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,133 @@
+[versions]
+accompanistPermissions = "0.37.3"
+accompanistSystemuicontroller = "0.36.0"
+activityCompose = "1.11.0"
+cameraxVersion = "1.5.1"
+coil = "2.7.0"
+composeBom = "2025.11.00"
+constraintlayoutCompose = "1.1.1"
+coreKtx = "1.17.0"
+coreSplashscreen = "1.2.0"
+datastorePreferences = "1.1.7"
+firebaseBom = "34.5.0"
+gson = "2.13.2"
+guava = "33.5.0-jre"
+junit = "4.13.2"
+espressoCore = "3.7.0"
+junitVersion = "1.3.0"
+koinVersion = "4.1.1"
+koinAnnotation = "2.3.1"
+kotlinBom = "2.0.21"
+ktorVersion = "3.3.2"
+lifecycleRuntimeKtx = "2.9.4"
+lottieCompose = "6.7.1"
+material3 = "1.4.0"
+navigationCompose = "2.9.6"
+pagingRuntime = "3.3.6"
+pagingCompose = "3.3.6"
+playServicesGcm = "17.0.0"
+room = "2.8.3"
+savedstateKtx = "1.4.0"
+secretsGradlePlugin = "2.0.1"
+uiTextGoogleFonts = "1.9.4"
+# Plugins versions
+ksp = "2.2.21-2.0.4"
+agp = "8.13.1"
+kotlin = "2.2.21"
+androidLibrary = "8.13.1"
+googleServicesGMC = "4.4.4"
+crashlytics = "3.0.6"
+foundation = "1.9.4"
+kotzilla = "1.4.0"
+
+
+[libraries]
+accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
+accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
+activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
+androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraxVersion" }
+androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraxVersion" }
+androidx-camera-video = { module = "androidx.camera:camera-video", version.ref = "cameraxVersion" }
+androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraxVersion" }
+androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
+androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
+androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycleRuntimeKtx" }
+androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingCompose" }
+androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "pagingRuntime" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
+androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
+androidx-savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "savedstateKtx" }
+camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraxVersion" }
+camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraxVersion" }
+coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
+coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" }
+coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
+compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
+constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" }
+core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
+espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
+ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
+firebase-messaging = { module = "com.google.firebase:firebase-messaging" }
+google-firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
+gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
+guava = { module = "com.google.guava:guava", version.ref = "guava" }
+io-ktor-ktor-client-cio = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorVersion" }
+junit = { module = "junit:junit", version.ref = "junit" }
+
+kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlinBom" }
+
+# Kotzilla
+koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinVersion" }
+koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinVersion" }
+koin-androidx-compose-navigation = { module = "io.insert-koin:koin-androidx-compose-navigation", version.ref = "koinVersion" }
+
+koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinVersion" }
+koin-test = { module = "io.insert-koin:koin-test", version.ref = "koinVersion" }
+koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koinAnnotation" }
+koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koinAnnotation" }
+koin-ksp-bom = { module = "io.insert-koin:koin-annotations-bom", version.ref = "koinAnnotation"}
+
+ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktorVersion" }
+ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVersion" }
+ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktorVersion" }
+ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktorVersion" }
+ktor-client-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorVersion" }
+ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorVersion" }
+lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieCompose" }
+navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
+material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
+play-services-gcm = { module = "com.google.android.gms:play-services-gcm", version.ref = "playServicesGcm" }
+secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }
+build-tools = { module = "com.android.tools.build:gradle", version.ref = "agp" }
+google-services-gmc = { module = "com.google.gms:google-services", version.ref = "googleServicesGMC" }
+ui = { module = "androidx.compose.ui:ui" }
+ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
+ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
+ui-text-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version.ref = "uiTextGoogleFonts" }
+ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics" }
+androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
+kotzilla-sdk = { group = "io.kotzilla", name = "kotzilla-sdk", version.ref = "kotzilla" }
+
+
+[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+googleKsp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+room = { id = "androidx.room", version.ref = "room" }
+secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" }
+kotlin-parcelize = { id = "kotlin-parcelize" }
+google-services-gmc = { id = "com.google.gms.google-services", version.ref = "googleServicesGMC" }
+android-library = { id = "com.android.library", version.ref = "androidLibrary" }
+firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "crashlytics" }
+kotzilla = { id = "io.kotzilla.kotzilla-plugin", version.ref = "kotzilla" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..39f8bc9
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+#Thu Nov 20 19:44:23 MSK 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..ef07e01
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..db3a6ac
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..f1e7296
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,28 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven(url = "https://jitpack.io")
+ maven(
+ url = "https://maven.google.com"
+ )
+ }
+}
+
+rootProject.name = "MoscowHackatonTemplate"
+include(":app")
+
\ No newline at end of file