You've already forked RekomenciMobile
feat: main screen implemented
This commit is contained in:
Vendored
+6
@@ -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 * { *; }
|
||||||
+60
-2
@@ -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,9 +28,7 @@ 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
|
||||||
|
|||||||
@@ -19,157 +19,157 @@ import kotlin.time.Duration
|
|||||||
|
|
||||||
abstract class BaseRepository {
|
abstract class BaseRepository {
|
||||||
|
|
||||||
// Caching module ==============================================================================
|
// Caching module ==============================================================================
|
||||||
private val internalCacheStorage = mutableMapOf<String, CacheEntry<*>>()
|
private val internalCacheStorage = mutableMapOf<String, CacheEntry<*>>()
|
||||||
|
|
||||||
private data class CacheEntry<T>(
|
private data class CacheEntry<T>(
|
||||||
val value: T,
|
val value: T,
|
||||||
val expirationTime: Long
|
val expirationTime: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
fun <T> putCache(cacheConfiguration: Pair<String, Duration>, value: T) {
|
fun <T> putCache(cacheConfiguration: Pair<String, Duration>, value: T) {
|
||||||
internalCacheStorage[cacheConfiguration.first] =
|
internalCacheStorage[cacheConfiguration.first] =
|
||||||
CacheEntry(value, cacheConfiguration.second.inWholeSeconds)
|
CacheEntry(value, cacheConfiguration.second.inWholeSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T> getCache(key: String): T? {
|
fun <T> getCache(key: String): T? {
|
||||||
val entry = internalCacheStorage[key] ?: return null
|
val entry = internalCacheStorage[key] ?: return null
|
||||||
if (entry.expirationTime < System.currentTimeMillis()) {
|
if (entry.expirationTime < System.currentTimeMillis()) {
|
||||||
internalCacheStorage.remove(key)
|
internalCacheStorage.remove(key)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return entry.value as T
|
return entry.value as T
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base data sources ===========================================================================
|
// Base data sources ===========================================================================
|
||||||
|
|
||||||
protected open val defaultKtorClient: HttpClient? = null
|
protected open val defaultKtorClient: HttpClient? = null
|
||||||
protected open val db: RoomDatabase? = null
|
protected open val db: RoomDatabase? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "BaseRepository"
|
private const val TAG = "BaseRepository"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal methods ============================================================================
|
// Internal methods ============================================================================
|
||||||
|
|
||||||
private fun assertKtorClientSpecify() {
|
private fun assertKtorClientSpecify() {
|
||||||
if (defaultKtorClient == null) {
|
if (defaultKtorClient == null) {
|
||||||
Log.e(TAG, "You must specify ktor client for make network requests")
|
Log.e(TAG, "You must specify ktor client for make network requests")
|
||||||
throw IllegalStateException("You must specify ktor client for make network requests")
|
throw IllegalStateException("You must specify ktor client for make network requests")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertDBSpecify() {
|
private fun assertDBSpecify() {
|
||||||
if (db == null) {
|
if (db == null) {
|
||||||
throw IllegalStateException("You must specify db for use pagination/cashing")
|
throw IllegalStateException("You must specify db for use pagination/cashing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// And methods for use :) ======================================================================
|
// And methods for use :) ======================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a network request using the provided Ktor client and request builder block.
|
* Makes a network request using the provided Ktor client and request builder block.
|
||||||
*
|
*
|
||||||
* This function handles the common boilerplate for making a network request,
|
* This function handles the common boilerplate for making a network request,
|
||||||
* including error handling and converting exceptions to a domain-specific `NetworkError`.
|
* 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 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`.
|
* @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.
|
* 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.
|
* @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.
|
* @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.
|
* @throws IllegalStateException if `ktorClient` is null and `defaultKtorClient` is also null.
|
||||||
*/
|
*/
|
||||||
internal suspend inline fun <reified T> networkRequest(
|
internal suspend inline fun <reified T> networkRequest(
|
||||||
ktorClient: HttpClient? = this.defaultKtorClient,
|
ktorClient: HttpClient? = this.defaultKtorClient,
|
||||||
cacheConfiguration: Pair<String, Duration>? = null,
|
cacheConfiguration: Pair<String, Duration>? = null,
|
||||||
block: HttpRequestBuilder.() -> Unit
|
block: HttpRequestBuilder.() -> Unit
|
||||||
): Result<T> {
|
): Result<T> {
|
||||||
Log.d(TAG, "Network request! Asserting ktor client specify")
|
Log.d(TAG, "Network request! Asserting ktor client specify")
|
||||||
assertKtorClientSpecify()
|
assertKtorClientSpecify()
|
||||||
Log.d(TAG, "ktor client is specified - continue network request")
|
Log.d(TAG, "ktor client is specified - continue network request")
|
||||||
return try {
|
return try {
|
||||||
Log.d(TAG, "Start request!")
|
Log.d(TAG, "Start request!")
|
||||||
val response = ktorClient!!.request(block = block)
|
val response = ktorClient!!.request(block = block)
|
||||||
Log.d(TAG, "Request was made without exceptions")
|
Log.d(TAG, "Request was made without exceptions")
|
||||||
|
|
||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
Result.success(
|
Result.success(
|
||||||
value = response
|
value = response
|
||||||
).map {
|
).map {
|
||||||
it.body<T>()
|
it.body<T>()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val firstCodeNum = response.status.value / 100
|
val firstCodeNum = response.status.value / 100
|
||||||
val detail = (response.body() as? ErrorNetworkDTO)?.detail ?: "Unknown"
|
val detail = (response.body() as? ErrorNetworkDTO)?.detail ?: "Unknown"
|
||||||
Result.failure(
|
Result.failure(
|
||||||
when (firstCodeNum) {
|
when (firstCodeNum) {
|
||||||
4 -> NetworkError.InputError(detail)
|
4 -> NetworkError.InputError(detail)
|
||||||
else -> NetworkError.Unexpected(detail)
|
else -> NetworkError.Unexpected(detail)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Exception in request process! $e")
|
Log.e(TAG, "Exception in request process! $e")
|
||||||
Result.failure(
|
Result.failure(
|
||||||
exception = e.convertToNetworkError()
|
exception = e.convertToNetworkError()
|
||||||
)
|
)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
Log.v(TAG, "Network request was successful")
|
Log.v(TAG, "Network request was successful")
|
||||||
if (cacheConfiguration != null) {
|
if (cacheConfiguration != null) {
|
||||||
putCache(cacheConfiguration, it)
|
putCache(cacheConfiguration, it)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Log.e(TAG, "Network request has error! $it")
|
Log.e(TAG, "Network request has error! $it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal suspend inline fun <reified T> internalCachedRequest(
|
internal suspend inline fun <reified T> internalCachedRequest(
|
||||||
ktorClient: HttpClient? = this.defaultKtorClient,
|
ktorClient: HttpClient? = this.defaultKtorClient,
|
||||||
cacheConfiguration: Pair<String, Duration>,
|
cacheConfiguration: Pair<String, Duration>,
|
||||||
block: HttpRequestBuilder.() -> Unit
|
block: HttpRequestBuilder.() -> Unit
|
||||||
): Result<T> {
|
): Result<T> {
|
||||||
val cachedResult = getCache<T>(cacheConfiguration.first)
|
val cachedResult = getCache<T>(cacheConfiguration.first)
|
||||||
|
|
||||||
return if (cachedResult != null) {
|
return if (cachedResult != null) {
|
||||||
Result.success(cachedResult)
|
Result.success(cachedResult)
|
||||||
} else {
|
} else {
|
||||||
networkRequest<T>(ktorClient, cacheConfiguration, block)
|
networkRequest<T>(ktorClient, cacheConfiguration, block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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,
|
||||||
initialLoadSize: Int = pageSize * 3,
|
initialLoadSize: Int = pageSize * 3,
|
||||||
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()
|
||||||
|
|
||||||
return Pager(
|
return Pager(
|
||||||
config = PagingConfig(
|
config = PagingConfig(
|
||||||
pageSize,
|
pageSize,
|
||||||
prefetchDistance,
|
prefetchDistance,
|
||||||
enablePlaceholders,
|
enablePlaceholders,
|
||||||
initialLoadSize,
|
initialLoadSize,
|
||||||
maxSize,
|
maxSize,
|
||||||
jumpThreshold
|
jumpThreshold
|
||||||
),
|
),
|
||||||
remoteMediator = BaseRemoteMediator(
|
remoteMediator = BaseRemoteMediator(
|
||||||
db = db!!,
|
db = db!!,
|
||||||
dao = dbDao,
|
dao = dbDao,
|
||||||
makeRequest = makeRequest
|
makeRequest = makeRequest
|
||||||
),
|
),
|
||||||
pagingSourceFactory = {
|
pagingSourceFactory = {
|
||||||
dbDao.getPaginatedData()
|
dbDao.getPaginatedData()
|
||||||
}
|
}
|
||||||
).flow
|
).flow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
+4
-1
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-8
@@ -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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -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>
|
||||||
|
}
|
||||||
+37
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
+40
@@ -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() } }
|
||||||
|
}
|
||||||
+8
@@ -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
|
||||||
|
}
|
||||||
+10
-1
@@ -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 {
|
||||||
|
|||||||
+40
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -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) }
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -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)
|
||||||
|
|||||||
+162
-263
@@ -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
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-133
@@ -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()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">MoscowHackatonTemplate</string>
|
<string name="app_name">Rekomenci fluon</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user