feat: main screen implemented

This commit is contained in:
MaximOksiuta
2025-11-22 04:53:03 +03:00
parent 82e5066950
commit 09ff18cb02
36 changed files with 812 additions and 644 deletions
+6
View File
@@ -19,3 +19,9 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
# Keep annotation definitions
-keep class org.koin.core.annotation.** { *; }
# Keep classes annotated with Koin annotations
-keep @org.koin.core.annotation.* class * { *; }
@@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "bf664fe902e116c42af432814d63d6a7", "identityHash": "3e896e9a3d3b2f61149f8c0fde7e5964",
"entities": [ "entities": [
{ {
"tableName": "users", "tableName": "users",
@@ -52,11 +52,69 @@
"id" "id"
] ]
} }
},
{
"tableName": "resumes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `experience_type` TEXT NOT NULL, `about_me` TEXT NOT NULL, `key_skills` TEXT NOT NULL, `position` TEXT NOT NULL, `from_salary` INTEGER, `to_salary` INTEGER, `recommended_skills` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "experienceType",
"columnName": "experience_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "aboutMe",
"columnName": "about_me",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "keySkills",
"columnName": "key_skills",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fromSalary",
"columnName": "from_salary",
"affinity": "INTEGER"
},
{
"fieldPath": "toSalary",
"columnName": "to_salary",
"affinity": "INTEGER"
},
{
"fieldPath": "recommendedSkills",
"columnName": "recommended_skills",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
} }
], ],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e896e9a3d3b2f61149f8c0fde7e5964')"
] ]
} }
} }
@@ -28,10 +28,8 @@ class App : Application() {
androidContext(this@App) androidContext(this@App)
analytics() analytics()
modules( modules(
listOf(
AppModules().module AppModules().module
) )
)
} }
FirebaseApp.initializeApp(this@App) FirebaseApp.initializeApp(this@App)
} }
@@ -1,6 +1,7 @@
package com.prodhack.moscow2025.common.di package com.prodhack.moscow2025.common.di
import com.prodhack.moscow2025.data.data_providers.local_db.DatabaseProvider import com.prodhack.moscow2025.data.data_providers.local_db.DatabaseProvider
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module import org.koin.core.annotation.Module
/** /**
@@ -1,7 +1,11 @@
package com.prodhack.moscow2025.common.di package com.prodhack.moscow2025.common.di
import android.content.Context
import androidx.room.Room
import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase
import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
@Module @Module
@ComponentScan("com.prodhack.moscow2025.presentation") @ComponentScan("com.prodhack.moscow2025.presentation")
@@ -13,4 +17,13 @@ class DomainModule
@Module @Module
@ComponentScan("com.prodhack.moscow2025.data") @ComponentScan("com.prodhack.moscow2025.data")
class DataModule class DataModule{
@Single
fun provideDatabase(context: Context): AppDatabase =
Room.databaseBuilder(
context,
AppDatabase::class.java,
"t_tasks.db"
).fallbackToDestructiveMigration()
.build()
}
@@ -1,6 +0,0 @@
package com.prodhack.moscow2025.data.base
interface BaseEntity {
val id: Number
}
@@ -8,7 +8,7 @@ import androidx.room.RoomDatabase
import androidx.room.withTransaction import androidx.room.withTransaction
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
class BaseRemoteMediator<DBEntity : BaseEntity>( class BaseRemoteMediator<DBEntity : Any>(
private val db: RoomDatabase, private val db: RoomDatabase,
private val dao: BasePaginationDAO<DBEntity>, private val dao: BasePaginationDAO<DBEntity>,
private val makeRequest: suspend (page: Long, pageCount: Int) -> Result<List<DBEntity>> private val makeRequest: suspend (page: Long, pageCount: Int) -> Result<List<DBEntity>>
@@ -26,17 +26,12 @@ class BaseRemoteMediator<DBEntity : BaseEntity>(
) )
LoadType.APPEND -> { LoadType.APPEND -> {
val lastItem = state.lastItemOrNull() state.pages.size + 1
if (lastItem == null) {
1
} else {
(lastItem.id.toLong() / state.config.pageSize) + 1
}
} }
} }
val result = makeRequest( val result = makeRequest(
loadKey, (loadKey.toLong() - 1) * state.config.pageSize,
state.config.pageSize state.config.pageSize
) )
@@ -46,8 +41,7 @@ class BaseRemoteMediator<DBEntity : BaseEntity>(
if (loadType == LoadType.REFRESH) { if (loadType == LoadType.REFRESH) {
dao.clearAll() dao.clearAll()
} }
val beerEntities = data dao.upsertAll(data)
dao.upsertAll(beerEntities)
} }
MediatorResult.Success( MediatorResult.Success(
endOfPaginationReached = data.size < state.config.pageSize endOfPaginationReached = data.size < state.config.pageSize
@@ -141,7 +141,7 @@ abstract class BaseRepository {
} }
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
protected fun <Value : BaseEntity> paginatedRequest( protected fun <Value : Any> paginatedRequest(
pageSize: Int = 10, pageSize: Int = 10,
prefetchDistance: Int = pageSize, prefetchDistance: Int = pageSize,
enablePlaceholders: Boolean = true, enablePlaceholders: Boolean = true,
@@ -149,7 +149,7 @@ abstract class BaseRepository {
maxSize: Int = Int.MAX_VALUE, maxSize: Int = Int.MAX_VALUE,
jumpThreshold: Int = Int.MIN_VALUE, jumpThreshold: Int = Int.MIN_VALUE,
dbDao: BasePaginationDAO<Value>, dbDao: BasePaginationDAO<Value>,
makeRequest: suspend (page: Long, pageSize: Int) -> Result<List<Value>> makeRequest: suspend (offset: Long, pageSize: Int) -> Result<List<Value>>
): Flow<PagingData<Value>> { ): Flow<PagingData<Value>> {
assertDBSpecify() assertDBSpecify()
@@ -1,5 +0,0 @@
package com.prodhack.moscow2025.data.base
interface DBMappableDTO <T> {
fun mapToDB(): T
}
@@ -1,5 +0,0 @@
package com.prodhack.moscow2025.data.base
interface DomainMappableDTO <T> {
fun mapToDomain(): T
}
@@ -3,11 +3,13 @@ package com.prodhack.moscow2025.data.data_providers.local_db
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase 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.CleanUpDao
import com.prodhack.moscow2025.data.data_providers.local_db.dao.ResumeDao
import com.prodhack.moscow2025.data.data_providers.local_db.dao.UserDao import com.prodhack.moscow2025.data.data_providers.local_db.dao.UserDao
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity
@Database( @Database(
entities = [UserEntity::class], entities = [UserEntity::class, ResumeEntity::class],
version = 1, version = 1,
exportSchema = true exportSchema = true
) )
@@ -15,4 +17,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao abstract fun userDao(): UserDao
abstract fun cleanUpDao(): CleanUpDao abstract fun cleanUpDao(): CleanUpDao
abstract fun resumeDao(): ResumeDao
} }
@@ -8,12 +8,5 @@ import org.koin.core.annotation.Single
@Module @Module
class DatabaseProvider { class DatabaseProvider {
@Single
fun provideDatabase(context: Context): AppDatabase =
Room.databaseBuilder(
context,
AppDatabase::class.java,
"t_tasks.db"
).fallbackToDestructiveMigration()
.build()
} }
@@ -0,0 +1,21 @@
package com.prodhack.moscow2025.data.data_providers.local_db.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.prodhack.moscow2025.data.base.BasePaginationDAO
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
@Dao
interface ResumeDao: BasePaginationDAO<ResumeEntity> {
@Query("DELETE FROM resumes")
override suspend fun clearAll()
@Upsert
override suspend fun upsertAll(data: List<ResumeEntity>)
@Query("SELECT * FROM resumes")
override fun getPaginatedData(): PagingSource<Int, ResumeEntity>
}
@@ -0,0 +1,37 @@
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.ExperienceType
import com.prodhack.moscow2025.domain.models.ResumeModel
import kotlin.math.exp
@Entity(tableName = "resumes")
data class ResumeEntity(
@PrimaryKey(autoGenerate = false)
val id: String,
@ColumnInfo("experience_type")
val experienceType: String,
@ColumnInfo("about_me")
val aboutMe: String,
@ColumnInfo("key_skills")
val keySkills: String,
val position: String,
@ColumnInfo("from_salary")
val fromSalary: Int?,
@ColumnInfo("to_salary")
val toSalary: Int?,
@ColumnInfo("recommended_skills")
val recommendedSkills: String
) {
fun mapToDomain(): ResumeModel = ResumeModel(
id = id,
position = position,
about = aboutMe,
experienceType = ExperienceType.valueOf(experienceType),
skills = keySkills.split("|"),
prediction = Pair(fromSalary, toSalary),
recommendedSkills = recommendedSkills.split("|")
)
}
@@ -7,33 +7,6 @@ import com.prodhack.moscow2025.domain.models.User
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable 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 @Serializable
data class UserLoginRequest( data class UserLoginRequest(
@@ -58,28 +31,3 @@ data class TokenResponse(
@SerialName("access_token") @SerialName("access_token")
val token: String 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
)
}
@@ -0,0 +1,8 @@
package com.prodhack.moscow2025.data.dto
import kotlinx.serialization.Serializable
@Serializable
data class ErrorNetworkDTO(
val detail: String
)
@@ -0,0 +1,85 @@
package com.prodhack.moscow2025.data.dto
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
import com.prodhack.moscow2025.domain.models.ExperienceType
import com.prodhack.moscow2025.domain.models.ResumeModel
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
enum class ExperienceTypeDTO {
@SerialName("noExperience")
NoExperience,
@SerialName("lessThan1")
LessThan1,
@SerialName("between1And3")
Between1And3,
@SerialName("between3And6")
Between3And6,
@SerialName("moreThan6")
MoreThan6;
fun mapToDomain(): ExperienceType = when (this) {
NoExperience -> ExperienceType.NoExperience
LessThan1 -> ExperienceType.LessThan1
Between1And3 -> ExperienceType.Between1And3
Between3And6 -> ExperienceType.Between3And6
MoreThan6 -> ExperienceType.MoreThan6
}
}
@Serializable
data class ResumeDTO(
val id: String,
@SerialName("experience_type")
val experienceType: ExperienceTypeDTO,
@SerialName("about_me")
val aboutMe: String,
@SerialName("key_skills")
val keySkills: List<String>,
val position: String,
val prediction: PredictionDTO
) {
fun mapToDomain(): ResumeModel = ResumeModel(
id = id,
about = aboutMe,
skills = keySkills,
position = position,
experienceType = experienceType.mapToDomain(),
prediction = Pair(
prediction.fromSalary.toIntOrNull(),
prediction.toSalary.toIntOrNull()
),
recommendedSkills = prediction.recommendedSkills
)
fun mapToDB(): ResumeEntity = ResumeEntity(
id = id,
aboutMe = aboutMe,
keySkills = keySkills.joinToString("|"),
position = position,
fromSalary = prediction.fromSalary.toIntOrNull(),
toSalary = prediction.toSalary.toIntOrNull(),
recommendedSkills = prediction.recommendedSkills.joinToString("|"),
experienceType = experienceType.mapToDomain().name
)
}
@Serializable
data class PredictionDTO(
@SerialName("from_salary")
val fromSalary: String,
@SerialName("to_salary")
val toSalary: String,
@SerialName("recommended_skills")
val recommendedSkills: List<String>
)
@Serializable
data class ResumeListDTO(
val resumes: List<ResumeDTO>
)
@@ -0,0 +1,54 @@
package com.prodhack.moscow2025.data.dto
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 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 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
)
}
@@ -0,0 +1,40 @@
package com.prodhack.moscow2025.data.repImplementations
import androidx.paging.map
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.dto.ResumeListDTO
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository
import com.prodhack.moscow2025.domain.models.ResumeModel
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
import io.ktor.client.request.url
import io.ktor.http.HttpMethod
import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single
@Single
class ResumeRepositoryImpl(
ktorClient: ApiKtorClient,
override val db: AppDatabase
) : ResumeRepository, BaseRepository() {
override val defaultKtorClient = ktorClient.client
private val resumeDao = db.resumeDao()
override fun loadResumeList(): RemotePagingWrapper<ResumeModel> = paginatedRequest(
pageSize = 20,
dbDao = resumeDao,
makeRequest = { offset, pageSize ->
networkRequest<ResumeListDTO> {
method = HttpMethod.Get
url {
url("/resume/list")
parameters.append("limit", pageSize.toString())
parameters.append("offset", offset.toString())
}
}.map { it -> it.resumes.map { it.mapToDB() } }
}
).map { it -> it.map { it.mapToDomain() } }
}
@@ -0,0 +1,8 @@
package com.prodhack.moscow2025.domain.interfaces.resumes
import com.prodhack.moscow2025.domain.models.ResumeModel
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
interface ResumeRepository {
fun loadResumeList(): RemotePagingWrapper<ResumeModel>
}
@@ -0,0 +1,19 @@
package com.prodhack.moscow2025.domain.models
data class ResumeModel(
val id: String,
val position: String,
val about: String,
val skills: List<String>,
val experienceType: ExperienceType,
val prediction: Pair<Int?, Int?>,
val recommendedSkills: List<String>
)
enum class ExperienceType {
NoExperience,
LessThan1,
Between1And3,
Between3And6,
MoreThan6
}
@@ -1,5 +1,6 @@
package com.prodhack.moscow2025.domain.usecase.auth package com.prodhack.moscow2025.domain.usecase.auth
import android.util.Log
import com.prodhack.moscow2025.domain.interfaces.AuthRepository import com.prodhack.moscow2025.domain.interfaces.AuthRepository
import com.prodhack.moscow2025.domain.interfaces.UserRepository import com.prodhack.moscow2025.domain.interfaces.UserRepository
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
@@ -16,14 +17,22 @@ class CheckSessionUseCase(
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
private val userRepository: UserRepository private val userRepository: UserRepository
) { ) {
private companion object {
const val TAG = "CheckSessionUseCase"
}
/** /**
* return session state with * @return session state in enum format [SessionState]
*/ */
suspend operator fun invoke(): SessionState = suspend operator fun invoke(): SessionState =
if (authRepository.fetchLoginState().firstOrNull() == true) { if (authRepository.fetchLoginState().firstOrNull() == true) {
Log.d(TAG, "user authorized, requesting profile")
if (userRepository.fetchProfile().getOrNull()?.firstName.isNullOrBlank()) { if (userRepository.fetchProfile().getOrNull()?.firstName.isNullOrBlank()) {
Log.d(TAG, "user authorized, first name is blank -> need fill profile")
SessionState.NotFilledProfile SessionState.NotFilledProfile
} else { } else {
Log.d(TAG, "user authorized, first name is filled -> user already fill profile")
SessionState.FilledAndAuthorized SessionState.FilledAndAuthorized
} }
} else { } else {
@@ -0,0 +1,40 @@
package com.prodhack.moscow2025.domain.usecase.resumes
import androidx.paging.PagingData
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository
import com.prodhack.moscow2025.domain.models.ExperienceType
import com.prodhack.moscow2025.domain.models.ResumeModel
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
import kotlinx.coroutines.flow.flow
import org.koin.core.annotation.Single
@Single
class LoadResumeListUseCase(private val resumeRepository: ResumeRepository) {
// operator fun invoke(): RemotePagingWrapper<ResumeModel> = resumeRepository.loadResumeList()
// Mocked data
operator fun invoke(): RemotePagingWrapper<ResumeModel> = flow {
emit(
PagingData.from(
listOf(
ResumeModel(
id = "iajxioasdkmcaolsd,c",
position = "Android разработчик",
about = "Ну оооочень крутой андроид разраб, с огромным количеством опыта. " +
"И нет это я не про себя, это просто какие-то данные," +
" чтобы проверить, что это чудовище работает",
skills = listOf(
"Android SDK",
"Kotlin",
"Room",
"Ktor"
),
experienceType = ExperienceType.Between3And6,
prediction = Pair(200000, 230000),
recommendedSkills = listOf("KMP")
)
)
)
)
}
}
@@ -26,6 +26,10 @@ import kotlin.getValue
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private companion object {
const val TAG = "MainActivity"
}
private val checkSessionUseCase: CheckSessionUseCase by inject() private val checkSessionUseCase: CheckSessionUseCase by inject()
private val sessionDestinationState = MutableStateFlow<AppDestination?>(null) private val sessionDestinationState = MutableStateFlow<AppDestination?>(null)
@@ -42,8 +46,11 @@ class MainActivity : ComponentActivity() {
runBlocking { runBlocking {
val sessionState = try { val sessionState = try {
checkSessionUseCase() checkSessionUseCase().also {
Log.d(TAG, "SessionState received $it")
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Exception in session state getting process", e)
SessionState.NotAuthorized SessionState.NotAuthorized
} }
sessionDestinationState.value = sessionDestinationState.value =
@@ -67,7 +74,6 @@ class MainActivity : ComponentActivity() {
.addOnCompleteListener { task -> .addOnCompleteListener { task ->
if (task.isSuccessful) { if (task.isSuccessful) {
val token = task.result val token = task.result
Log.d("TOKEN", token)
} }
} }
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -28,17 +29,17 @@ fun TopLogo(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Image( Icon(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
painter = painterResource(R.drawable.ic_launcher_foreground), painter = painterResource(R.drawable.app_logo),
contentDescription = "App logo" contentDescription = "App logo"
) )
Spacer(modifier = Modifier.width(Paddings.medium)) Spacer(modifier = Modifier.width(Paddings.medium))
Text( Text(
text = stringResource(R.string.app_name), text = stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleMedium,
fontSize = 48.sp fontSize = 24.sp
) )
} }
} }
@@ -0,0 +1,17 @@
package com.prodhack.moscow2025.presentation.dataModels
import com.prodhack.moscow2025.domain.models.ResumeModel
data class UIResumeBaseInfo(
val id: String,
val positionName: String,
val salary: String
)
fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo(
id = id,
positionName = position,
salary = prediction.first?.let { from ->
prediction.second?.let { to -> "$from-$to" } ?: from.toString()
} ?: prediction.second?.toString() ?: "Ошибка"
)
@@ -28,7 +28,7 @@ fun TTasksApp(
context: Context, context: Context,
sessionDestination: AppDestination? = null sessionDestination: AppDestination? = null
) { ) {
MoscowHackatonTemplateTheme() { MoscowHackatonTemplateTheme {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val bottomBarState = remember { mutableStateOf<Int?>(null) } val bottomBarState = remember { mutableStateOf<Int?>(null) }
@@ -144,11 +144,10 @@ fun ErrorCollectorScope.FillProfileScreen(
style = typography.titleLarge, style = typography.titleLarge,
fontSize = 31.sp fontSize = 31.sp
) )
Image( Icon(
painter = painterResource(R.drawable.ic_launcher_foreground), painter = painterResource(R.drawable.app_logo),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(140.dp), modifier = Modifier.size(140.dp)
contentScale = ContentScale.Crop
) )
} }
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(20.dp))
@@ -16,6 +16,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
@@ -127,8 +128,8 @@ fun ErrorCollectorScope.LoginScreen(
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Image( Icon(
painter = painterResource(R.drawable.ic_launcher_foreground), painter = painterResource(R.drawable.app_logo),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.size(200.dp) .size(200.dp)
@@ -1,9 +1,39 @@
package com.prodhack.moscow2025.presentation.screens.main package com.prodhack.moscow2025.presentation.screens.main
import android.widget.Toast
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems
import com.prodhack.moscow2025.R
import com.prodhack.moscow2025.presentation.components.standart.BigButton
import com.prodhack.moscow2025.presentation.components.standart.TTFloatingActionButton
import com.prodhack.moscow2025.presentation.components.standart.TopLogo
import com.prodhack.moscow2025.presentation.dataModels.UIResumeBaseInfo
import com.prodhack.moscow2025.presentation.theme.Paddings
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -14,269 +44,138 @@ fun ErrorCollectorScope.MainScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: MainScreenViewModel = koinViewModel() viewModel: MainScreenViewModel = koinViewModel()
) { ) {
Text("Main screen will be here soon") val typography = MaterialTheme.typography
// val openCalendarModal = remember { mutableStateOf(false) } val colorScheme = MaterialTheme.colorScheme
// val openTaskAddSheet = remember { mutableStateOf(false) } val shapes = MaterialTheme.shapes
// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
// val tasks = viewModel.taskList.collectAsLazyPagingItems() Box {
// Column(
// val selectedTask = remember { mutableStateOf<UITaskModel?>(null) } modifier = modifier
// .fillMaxSize()
// Box( .padding(horizontal = 20.dp),
// modifier = modifier horizontalAlignment = Alignment.CenterHorizontally
// .fillMaxSize() ) {
// .padding(horizontal = Paddings.large), TopLogo()
// contentAlignment = Alignment.BottomCenter Spacer(modifier = Modifier.height(Paddings.medium))
// ) { Text(
// Column( text = "Ваши резюме",
// modifier = Modifier.fillMaxSize(), style = typography.titleLarge,
// horizontalAlignment = Alignment.CenterHorizontally fontSize = 32.sp,
// ) { color = colorScheme.onBackground
// Spacer(modifier = Modifier.height(Paddings.large)) )
// TopLogo()
// Spacer(modifier = Modifier.height(Paddings.large)) Spacer(modifier = Modifier.height(Paddings.large))
//
// MainScreenFilters(viewModel = viewModel) { val items = viewModel.resumeList.collectAsLazyPagingItems()
// openCalendarModal.value = true
// } if (items.itemCount == 0 && items.loadState.append.endOfPaginationReached) {
// Text(
// Spacer(modifier = Modifier.height(Paddings.large)) text = "Здесь пока ничего нет",
// style = typography.labelLarge,
// viewModel.topicList.FoldUIStateWithGlobalCallbacks { topics -> textAlign = TextAlign.Center,
// BubbledCategoryFilters( fontSize = 24.sp,
// categories = topics, color = colorScheme.onBackground
// selectedItemId = viewModel.selectedTopicId.value ?: -1 )
// ) { categoryId ->
// viewModel.selectTopic(categoryId) BigButton(onClick = {
// } TODO()
// } }, buttonText = "Создать резюме", isLoading = false)
// Spacer(modifier = Modifier.height(Paddings.large)) } else if (items.loadState.hasError) {
// Text(
// if (tasks.loadState.hasError) { modifier = Modifier
// Text( .fillMaxWidth()
// "Упс, кажется что-то пошло не так :(\nНо мы уже со всем разбираемся!", .background(colorScheme.error, shape = shapes.small)
// style = Typography.titleMedium, .padding(Paddings.medium),
// textAlign = TextAlign.Center, text = "Кажется что-то пошло не так, но мы уже чиним 🛠️",
// fontSize = 18.sp, style = typography.labelLarge,
// color = MaterialTheme.colorScheme.error textAlign = TextAlign.Center,
// ) fontSize = 24.sp,
// } else if (tasks.loadState.append.endOfPaginationReached && tasks.itemCount == 0) { color = colorScheme.onError
// Spacer(modifier = Modifier.weight(1f)) )
// } else {
// Text( LazyColumn(
// "Сделал дело - гуляй смело\nСамое время запланировать новую поездку", horizontalAlignment = Alignment.CenterHorizontally,
// style = Typography.titleMedium, verticalArrangement = Arrangement.spacedBy(
// textAlign = TextAlign.Center, Paddings.medium
// fontSize = 18.sp, )
// color = MaterialTheme.colorScheme.onBackground ) {
// ) items(items.itemCount) {
// Spacer(modifier = Modifier.height(Paddings.large)) val resume = items[it]
// BigButton(buttonText = "Начать", onClick = { resume?.let {
// ResumeShortInfoCard(info = it) {
// }, isLoading = false)
// }
// Spacer(modifier = Modifier.weight(3f)) }
// }
// } else {
// LazyColumn( item {
// verticalArrangement = Arrangement.spacedBy(Paddings.small), if (items.loadState.append.endOfPaginationReached.not()) {
// horizontalAlignment = Alignment.CenterHorizontally CircularProgressIndicator()
// ) { }
// items(tasks.itemCount) { it -> }
// val task = tasks[it] }
// task?.let { }
// TaskCard( }
// onClick = {
// selectedTask.value = it val context = LocalContext.current
// }, TTFloatingActionButton(
// taskInfo = it, modifier = Modifier
// isArchiveWaiting = it.id in viewModel.archiveWaitingTasksIds.value .align(Alignment.BottomCenter)
// ) { .padding(bottom = Paddings.medium),
// viewModel.toggleTaskAsDone( onClick = {
// tripId = it.tripId, Toast.makeText(context, "Will be soon...", Toast.LENGTH_SHORT).show()
// taskId = it.id, },
// currState = it.archived text = "Добавить резюме"
// ) )
// tasks.refresh() }
// } }
// }
// } @Composable
// fun ResumeShortInfoCard(
// item { modifier: Modifier = Modifier,
// if (!tasks.loadState.append.endOfPaginationReached) { info: UIResumeBaseInfo,
// CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) onClick: () -> Unit
// } ) {
// } val typography = MaterialTheme.typography
// } Card(
// } modifier = modifier.fillMaxWidth(),
// } shape = MaterialTheme.shapes.small,
// onClick = onClick
// TTFloatingActionButton( ) {
// modifier = Modifier Row(
// .align(Alignment.BottomCenter) modifier = Modifier
// .padding(bottom = Paddings.medium), .fillMaxWidth()
// onClick = { .padding(Paddings.medium),
// openTaskAddSheet.value = true verticalAlignment = Alignment.CenterVertically,
// }, horizontalArrangement = Arrangement.SpaceBetween
// text = "Добавить задачу" ) {
// ) Column(
// } verticalArrangement = Arrangement.spacedBy(Paddings.small)
// ) {
// Text(info.positionName, style = typography.labelLarge, fontSize = 20.sp)
// AnimatedVisibility(openCalendarModal.value) { Row {
// DateRangePickerModal({ Text(
// Log.d("DatePicker", it.toString()) "Ожидаемая ЗП: ",
// if (it.first != null && it.second != null) { style = typography.labelLarge,
// viewModel.setDate(Pair(it.first!!, it.second!!)) fontSize = 18.sp
// openCalendarModal.value = false )
// } Text(
// }) { "${info.salary}",
// openCalendarModal.value = false style = typography.titleMedium,
// } color = MaterialTheme.colorScheme.primary,
// } fontSize = 18.sp
// )
// if (openTaskAddSheet.value) { }
// AddTaskBottomSheet(
// sheetState = sheetState, }
// onDismiss = {
// openTaskAddSheet.value = false Icon(
// } modifier = Modifier.size(24.dp),
// ) painter = painterResource(R.drawable.ic_arr_details),
// } contentDescription = "Open details"
// )
// 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
// }
// }
// }
} }
@@ -1,143 +1,17 @@
package com.prodhack.moscow2025.presentation.screens.main package com.prodhack.moscow2025.presentation.screens.main
import androidx.paging.map
import com.prodhack.moscow2025.domain.usecase.resumes.LoadResumeListUseCase
import com.prodhack.moscow2025.presentation.dataModels.UIResumeBaseInfo
import com.prodhack.moscow2025.presentation.dataModels.mapToBaseUIInfo
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
import kotlinx.coroutines.flow.map
import org.koin.android.annotation.KoinViewModel import org.koin.android.annotation.KoinViewModel
@KoinViewModel @KoinViewModel
class MainScreenViewModel( class MainScreenViewModel(
// private val loadTasksUseCase: LoadTasksUseCase, loadResumeListUseCase: LoadResumeListUseCase
// private val loadTasksTopicsListUseCase: LoadTasksTopicListUseCase,
// private val setFinishedStateToTaskUseCase: SetFinishedStateToTaskUseCase,
// private val changeDeadlineUseCase: ChangeDeadlineUseCase
) : BaseViewModel() { ) : BaseViewModel() {
val resumeList = loadResumeListUseCase().map { it -> it.map { it.mapToBaseUIInfo() } }
// 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 <dateString>, 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<Long, Long>) {
// 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<Int?>(null)
//
// val topicList = MutableUIStateFlow<List<UITaskTopicModel>>()
//
// 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<Long, Job>()
//
// 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()
// }
} }
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
@@ -111,8 +112,8 @@ fun ErrorCollectorScope.RegisterScreen(
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( Icon(
painter = painterResource(R.drawable.ic_launcher_foreground), painter = painterResource(R.drawable.app_logo),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.size(200.dp) .size(200.dp)
@@ -3,6 +3,7 @@ package com.prodhack.moscow2025.presentation.theme
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
@@ -149,6 +150,10 @@ fun MoscowHackatonTemplateTheme(
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, typography = Typography,
shapes = Shapes(
extraSmall = com.prodhack.moscow2025.presentation.theme.Shapes.verySmallRoundedBox,
small = com.prodhack.moscow2025.presentation.theme.Shapes.smallRoundedBox
),
content = content content = content
) )
} }
+36
View File
@@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M435.9,733L178.4,733C150.7,733 145.8,739.9 145.8,713.2L145.7,168.1C145.7,162.1 143.7,142.8 148.5,139C153.2,138.6 160.2,138.9 165.2,138.9L717.9,139C748.3,139 739.6,146.3 739.6,179L739.9,425.6C740,437.5 727.3,435.8 727.2,425.8L727.1,150.8L157.8,150.8L157.8,387.6L157.8,721L534,721C544.7,721.1 545.9,732.9 536.4,733C527.3,733.1 518.2,733 509,733L447.9,733C448.3,737.7 447.9,746.5 447.9,751.6L447.8,843.5C447.8,848.5 451.7,875.6 437.8,863.9C434.2,860.8 435.9,817.9 435.9,811.9L435.9,733Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M305.7,247.4C296.3,247.6 243.5,248.4 238.9,246.4C233,243.9 235.6,237.2 241.5,235.6C248.5,235.5 307.8,233.1 310,239C311.6,243.4 310,246.2 305.7,247.4Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M444.3,453.6C430,455.6 435.9,420.7 435.9,409L435.9,269.7C435.9,262.2 433.5,240 438.6,235.6C448.2,234.6 447.7,240.8 447.8,248.3C448,263.8 447.9,279.4 447.9,295L447.9,426.9C447.9,434.5 450.1,449.1 444.3,453.6Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M578.5,260.8C579.7,253.2 577,239.6 584.6,235.6C596,235 592.6,252.7 592.5,260.8C637.5,267.4 636.2,323.5 621.6,311.1C616.3,307 623.4,289.8 602.8,276.8C560.7,249.5 518.5,334.1 591.6,338C633.7,340.2 650.7,415 591,429C590.6,436.5 592.9,448.2 588.8,453.6C575,458.3 578.4,437.1 578.5,429C524.8,412.5 542.7,362.7 552.2,382.3C561.9,439.1 621.9,417.3 617.9,380C613.8,341.8 576.6,356.3 556.4,340.4C526.7,317 539.6,264.8 578.5,260.8Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M366.7,327L288,327C272.9,327 257.8,327.1 242.7,326.8C233.5,326.6 233.6,316.8 241.5,315.1L328.5,315.1C341.4,315.1 354.9,314.5 367.6,315.6C372.3,316 373.9,324.3 366.7,327Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M368.5,402.8C365.7,403.2 357.9,402.9 354.5,402.9L274,402.9C266.5,402.9 223.6,408.2 239,391C241.5,390.5 249.4,390.9 252.4,390.9L362.1,390.9C370.6,391.7 377.1,396.6 368.5,402.8Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M735.8,639.9C690.6,642 652.2,607.1 650,561.9C647.8,516.7 682.5,478.3 727.7,475.9C773.1,473.6 811.7,508.5 814,553.9C816.2,599.3 781.1,637.8 735.8,639.9ZM722.8,488.8C684.8,493.8 657.9,528.6 662.7,566.7C667.5,604.7 702.2,631.8 740.3,627.2C778.7,622.6 806,587.6 801.1,549.2C796.3,510.9 761.2,483.8 722.8,488.8Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M417.5,543.9C410.6,544.3 400.4,543.9 393.3,543.9L278.2,543.8C274.5,543.8 214.4,548.1 237.8,531.6C241.2,531.2 250.6,531.6 254.6,531.6L408.4,531.7C411.5,531.9 415.6,531.6 418.6,532.5C424,534.3 421.6,542 417.5,543.9Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M490.7,622.9C483.2,623.3 472.8,622.9 465.2,622.9L298.3,622.9C289.2,622.9 241.4,624 236.6,622.2C231.2,620.2 232.8,612.3 237.8,610.7L468.1,610.7C474.9,610.7 483.9,610.5 490.8,611.3C497.2,612.8 495.8,620.4 490.7,622.9Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M846.8,865L618.9,865C582.7,865 586.5,871.1 586.5,835.6L586.5,735.1C586.5,705.6 584.3,678.1 619.9,671.4C620.4,671.4 620.8,671.4 621.3,671.4L824.8,671.4C866.1,671.4 879.5,678.8 879.5,722.6L879.5,849.1C879.5,871.9 870.2,865.1 846.8,865ZM624.3,683.5C598,686.4 599,702.8 599,723.4L599,852.7L649.6,852.7L649.6,797.6C649.6,791.9 647.3,769.9 651.8,766.3C669.7,752 661.9,844.6 661.9,852.7L804.5,852.7L804.5,817.6C804.5,803 804,786.4 805.1,771.8C805.5,765.5 816.5,766.1 816.8,774C817,782.9 816.9,791.7 816.9,800.6L816.9,852.7L867.5,852.7L867.5,723.8C867.5,686 858.6,683.4 823.1,683.5L624.3,683.5Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M8.512,4.43C8.663,4.301 8.859,4.237 9.058,4.252C9.256,4.268 9.44,4.361 9.569,4.512L15.569,11.512C15.686,11.648 15.75,11.821 15.75,12C15.75,12.179 15.686,12.352 15.569,12.488L9.569,19.488C9.438,19.632 9.255,19.718 9.061,19.73C8.867,19.742 8.676,19.677 8.528,19.551C8.38,19.424 8.287,19.245 8.269,19.051C8.251,18.857 8.309,18.664 8.431,18.512L14.012,12L8.431,5.488C8.302,5.337 8.237,5.141 8.252,4.943C8.267,4.745 8.36,4.561 8.511,4.431"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<resources> <resources>
<string name="app_name">MoscowHackatonTemplate</string> <string name="app_name">Rekomenci fluon</string>
</resources> </resources>