kotlin poezdec

This commit is contained in:
2025-11-30 20:26:32 +06:00
parent 01cf2c1439
commit 523c373a04
10 changed files with 764 additions and 87 deletions

View File

@@ -0,0 +1,76 @@
kotlin version: 2.0.21
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing C:/Users/Admin/AndroidStudioProjects/NTO-2025-Android-TeamTask/app/src/main/java/ru/myitschool/work/App.kt:7:5: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
... 33 more

View File

@@ -0,0 +1,76 @@
kotlin version: 2.0.21
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing C:/Users/Admin/AndroidStudioProjects/NTO-2025-Android-TeamTask/app/src/main/java/ru/myitschool/work/App.kt:7:5: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
... 33 more

View File

@@ -1,9 +1,3 @@
/*plugins {
composeCompiler
kotlinAndroid
kotlinSerialization version Version.Kotlin.language
androidApplication
}*/
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.data.repo
import android.content.Context
import androidx.datastore.preferences.preferencesDataStore
// DataStore для хранения auth-кода
val Context.authDataStore by preferencesDataStore(name = "auth_data")

View File

@@ -1,6 +1,9 @@
package ru.myitschool.work.data.repo package ru.myitschool.work.data.repo
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import ru.myitschool.work.App import ru.myitschool.work.App
import ru.myitschool.work.data.source.NetworkDataSource import ru.myitschool.work.data.source.NetworkDataSource
@@ -10,23 +13,17 @@ object AuthRepository {
private val KEY_CODE = stringPreferencesKey("auth_code") private val KEY_CODE = stringPreferencesKey("auth_code")
private var codeCache: String? = null private var codeCache: String? = null
// suspend fun checkAndSave(text: String): Result<Boolean> { // Проверка кода на сервере и сохранение при успехе
// return NetworkDataSource.checkAuth(text).onSuccess { success -> suspend fun checkAndSave(text: String): Result<Boolean> {
// if (success) { return NetworkDataSource.checkAuth(text).onSuccess { success ->
// codeCache = text if (success) {
// } codeCache = text
// } dataStore.edit { prefs ->
// } prefs[KEY_CODE] = text
suspend fun checkAndSave(text: String): Result<Boolean> { }
return NetworkDataSource.checkAuth(text).onSuccess { success ->
if (success) {
codeCache = text
dataStore.edit { prefs ->
prefs[KEY_CODE] = text
} }
} }
} }
}
/** Сохранённый код (из кэша или DataStore) */ /** Сохранённый код (из кэша или DataStore) */
suspend fun getSavedCode(): String? { suspend fun getSavedCode(): String? {
@@ -47,6 +44,4 @@ suspend fun checkAndSave(text: String): Result<Boolean> {
prefs.clear() prefs.clear()
} }
} }
}
}

View File

@@ -19,6 +19,11 @@ import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.source.dto.UserDto import ru.myitschool.work.data.source.dto.UserDto
object NetworkDataSource { object NetworkDataSource {
/** Поставь false, когда поднимешь настоящий бэкенд */
private const val USE_STUB = true
// Реальный клиент Ktor (для будущего)
private val client by lazy { private val client by lazy {
HttpClient(CIO) { HttpClient(CIO) {
install(ContentNegotiation) { install(ContentNegotiation) {
@@ -34,36 +39,78 @@ object NetworkDataSource {
} }
} }
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) { // ----------------- Заглушечные данные -----------------
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL)) private val stubUser = UserDto(
when (response.status) { name = "Тестовый пользователь",
HttpStatusCode.OK -> true booking = listOf(
else -> error(response.bodyAsText()) UserDto.BookingDto(
room = "Опенспейс 1",
time = "2025-01-05"
),
UserDto.BookingDto(
room = "Опенспейс 2",
time = "2025-01-06"
),
)
)
private val stubAvailableBookings: List<UserDto.BookingDto> = listOf(
UserDto.BookingDto(room = "Опенспейс 1", time = "2025-01-10"),
UserDto.BookingDto(room = "Опенспейс 1", time = "2025-01-11"),
UserDto.BookingDto(room = "Опенспейс 2", time = "2025-01-10"),
UserDto.BookingDto(room = "Опенспейс 3", time = "2025-01-12"),
)
// ----------------- Публичные методы -----------------
suspend fun checkAuth(code: String): Result<Boolean> {
if (USE_STUB) {
// Примитивная проверка заглушки: 4 символа → ок
return Result.success(code.length == 4)
}
return withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) {
HttpStatusCode.OK -> true
else -> error(response.bodyAsText())
}
} }
} }
} }
suspend fun getUserInfo(code: String): Result<UserDto> {
if (USE_STUB) {
return Result.success(stubUser)
}
return withContext(Dispatchers.IO) {
suspend fun getUserInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) { runCatching {
runCatching { val response = client.get(getUrl(code, Constants.INFO_URL))
val response = client.get(getUrl(code, Constants.INFO_URL)) when (response.status) {
when (response.status) { HttpStatusCode.OK -> response.body<UserDto>()
HttpStatusCode.OK -> response.body<UserDto>() else -> error(response.bodyAsText())
else -> error(response.bodyAsText()) }
} }
} }
} }
suspend fun getAvailableBookings( suspend fun getAvailableBookings(
code: String code: String
): Result<List<UserDto.BookingDto>> = withContext(Dispatchers.IO) { ): Result<List<UserDto.BookingDto>> {
runCatching { if (USE_STUB) {
val response = client.get(getUrl(code, Constants.BOOKING_URL)) return Result.success(stubAvailableBookings)
when (response.status) { }
HttpStatusCode.OK -> response.body<List<UserDto.BookingDto>>()
else -> error(response.bodyAsText()) return withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.BOOKING_URL))
when (response.status) {
HttpStatusCode.OK -> response.body<List<UserDto.BookingDto>>()
else -> error(response.bodyAsText())
}
} }
} }
} }
@@ -72,24 +119,32 @@ object NetworkDataSource {
code: String, code: String,
room: String, room: String,
time: String time: String
): Result<Unit> = withContext(Dispatchers.IO) { ): Result<Unit> {
runCatching { if (USE_STUB) {
val response = client.post(getUrl(code, Constants.BOOK_URL)) { // Типа всё хорошо
setBody( return Result.success(Unit)
MultiPartFormDataContent( }
formData {
append("room", room) return withContext(Dispatchers.IO) {
append("time", time) runCatching {
} val response = client.post(getUrl(code, Constants.BOOK_URL)) {
setBody(
MultiPartFormDataContent(
formData {
append("room", room)
append("time", time)
}
)
) )
) }
} when (response.status) {
when (response.status) { HttpStatusCode.OK -> Unit
HttpStatusCode.OK -> Unit else -> error(response.bodyAsText())
else -> error(response.bodyAsText()) }
} }
} }
} }
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" private fun getUrl(code: String, targetUrl: String) =
} "${Constants.HOST}/api/$code$targetUrl"
}

View File

@@ -2,10 +2,7 @@ package ru.myitschool.work.ui.screen
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Box
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.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@@ -15,6 +12,8 @@ import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthScreen import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.main.MainScreen
@Composable @Composable
fun AppNavHost( fun AppNavHost(
@@ -32,18 +31,10 @@ fun AppNavHost(
AuthScreen(navController = navController) AuthScreen(navController = navController)
} }
composable<MainScreenDestination> { composable<MainScreenDestination> {
Box( MainScreen(navController = navController)
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
} }
composable<BookScreenDestination> { composable<BookScreenDestination> {
Box( BookScreen(navController = navController)
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
} }
} }
} }

View File

@@ -16,9 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -41,7 +38,10 @@ fun AuthScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.actionFlow.collect { viewModel.actionFlow.collect {
navController.navigate(MainScreenDestination) navController.navigate(MainScreenDestination) {
// очищаем backstack до экрана авторизации
popUpTo(MainScreenDestination) { inclusive = false }
}
} }
} }
@@ -60,6 +60,7 @@ fun AuthScreen(
when (val currentState = state) { when (val currentState = state) {
is AuthState.Data -> Content(viewModel, currentState) is AuthState.Data -> Content(viewModel, currentState)
is AuthState.Loading -> { is AuthState.Loading -> {
Spacer(modifier = Modifier.size(16.dp))
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(64.dp) modifier = Modifier.size(64.dp)
) )
@@ -73,25 +74,41 @@ private fun Content(
viewModel: AuthViewModel, viewModel: AuthViewModel,
state: AuthState.Data state: AuthState.Data
) { ) {
var inputText by remember { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
if (state.isErrorVisible) {
Text(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Auth.ERROR),
text = "Неверный код или ошибка сервера",
color = Color.Red,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.size(8.dp))
}
TextField( TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), modifier = Modifier
value = inputText, .testTag(TestIds.Auth.CODE_INPUT)
onValueChange = { .fillMaxWidth(),
inputText = it value = state.code,
viewModel.onIntent(AuthIntent.TextInput(it)) onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) },
},
label = { Text(stringResource(R.string.auth_label)) } label = { Text(stringResource(R.string.auth_label)) }
) )
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
Button( Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), modifier = Modifier
.testTag(TestIds.Auth.SIGN_BUTTON)
.fillMaxWidth(),
onClick = { onClick = {
viewModel.onIntent(AuthIntent.Send(inputText)) viewModel.onIntent(AuthIntent.Send(state.code))
}, },
enabled = true enabled = state.isButtonEnabled
) { ) {
Text(stringResource(R.string.auth_sign_in)) Text(stringResource(R.string.auth_sign_in))
} }
} }

View File

@@ -0,0 +1,268 @@
package ru.myitschool.work.ui.screen.book
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.core.TestIds
@Composable
fun BookScreen(
navController: NavController,
viewModel: BookViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.onIntent(BookIntent.Load)
viewModel.actionFlow.collect { action ->
when (action) {
BookViewModel.Action.CloseWithSuccess -> {
navController.popBackStack()
}
}
}
}
when (val current = state) {
is BookState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is BookState.Error -> {
BookErrorContent(
message = current.message,
onBack = { navController.popBackStack() },
onRefresh = { viewModel.onIntent(BookIntent.Refresh) }
)
}
is BookState.Empty -> {
BookEmptyContent(
onBack = { navController.popBackStack() },
onRefresh = { viewModel.onIntent(BookIntent.Refresh) }
)
}
is BookState.Data -> {
BookDataContent(
state = current,
onBack = { navController.popBackStack() },
onRefresh = { viewModel.onIntent(BookIntent.Refresh) },
onSelectDate = { viewModel.onIntent(BookIntent.SelectDate(it)) },
onSelectPlace = { viewModel.onIntent(BookIntent.SelectPlace(it)) },
onBook = { viewModel.onIntent(BookIntent.Book) }
)
}
}
}
@Composable
private fun BookErrorContent(
message: String,
onBack: () -> Unit,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.testTag(TestIds.Book.ERROR),
text = message,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
Text("Назад")
}
}
}
@Composable
private fun BookEmptyContent(
onBack: () -> Unit,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.testTag(TestIds.Book.EMPTY),
text = "Всё забронировано"
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
Text("Назад")
}
}
}
@Composable
private fun BookDataContent(
state: BookState.Data,
onBack: () -> Unit,
onRefresh: () -> Unit,
onSelectDate: (Int) -> Unit,
onSelectPlace: (Int) -> Unit,
onBook: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
Text("Назад")
}
Button(
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
}
Spacer(modifier = Modifier.size(16.dp))
// Вкладки с датами
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(state.dates) { index, dateLabel ->
Surface(
modifier = Modifier
.testTag(TestIds.Book.getIdDateItemByPosition(index)),
tonalElevation = if (index == state.selectedDateIndex) 4.dp else 0.dp
) {
Text(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 8.dp)
.testTag(TestIds.Book.ITEM_DATE),
text = dateLabel,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
Spacer(modifier = Modifier.size(16.dp))
// Список мест
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(state.places, key = { _, item -> item.id }) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Book.getIdPlaceItemByPosition(index))
.selectable(
selected = item.isSelected,
onClick = { onSelectPlace(item.id) }
)
) {
Row(
modifier = Modifier
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_TEXT),
text = "${item.roomName}${item.time}",
style = MaterialTheme.typography.bodyMedium
)
}
RadioButton(
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR),
selected = item.isSelected,
onClick = { onSelectPlace(item.id) }
)
}
}
}
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Book.BOOK_BUTTON),
onClick = onBook,
enabled = state.places.any { it.isSelected }
) {
Text("Забронировать")
}
}
}

View File

@@ -0,0 +1,198 @@
package ru.myitschool.work.ui.screen.main
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination
@Composable
fun MainScreen(
navController: NavController,
viewModel: MainViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.onIntent(MainIntent.Load)
viewModel.actionFlow.collect { action ->
when (action) {
MainViewModel.Action.NavigateToAuth -> {
navController.navigate(AuthScreenDestination) {
popUpTo(AuthScreenDestination) { inclusive = true }
}
}
MainViewModel.Action.NavigateToBooking -> {
navController.navigate(BookScreenDestination)
}
}
}
}
when (val current = state) {
is MainState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is MainState.Error -> {
MainErrorContent(
message = current.message,
onRefresh = { viewModel.onIntent(MainIntent.Refresh) }
)
}
is MainState.Data -> {
MainDataContent(
state = current,
onRefresh = { viewModel.onIntent(MainIntent.Refresh) },
onLogout = { viewModel.onIntent(MainIntent.Logout) },
onAddBooking = { viewModel.onIntent(MainIntent.AddBooking) }
)
}
}
}
@Composable
private fun MainErrorContent(
message: String,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.testTag(TestIds.Main.ERROR),
text = message,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
}
}
@Composable
private fun MainDataContent(
state: MainState.Data,
onRefresh: () -> Unit,
onLogout: () -> Unit,
onAddBooking: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(64.dp)
.testTag(TestIds.Main.PROFILE_IMAGE),
contentAlignment = Alignment.Center
) {
Text("🙂")
}
Spacer(modifier = Modifier.size(16.dp))
Text(
modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME),
text = state.name,
style = MaterialTheme.typography.titleMedium
)
}
Spacer(modifier = Modifier.size(16.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON),
onClick = onLogout
) {
Text("Выход")
}
Button(
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
Button(
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON),
onClick = onAddBooking
) {
Text("Бронь")
}
}
Spacer(modifier = Modifier.size(16.dp))
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(state.bookings, key = { _, item -> item.id }) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Main.getIdItemByPosition(index))
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Text(
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
text = item.dateLabel,
style = MaterialTheme.typography.bodyMedium
)
Text(
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
text = item.roomName,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
}
}