From 523c373a0482e1de627bf787f7cb342d75c63f84 Mon Sep 17 00:00:00 2001 From: Eresperto Date: Sun, 30 Nov 2025 20:26:32 +0600 Subject: [PATCH] kotlin poezdec --- .kotlin/errors/errors-1764509512206.log | 76 +++++ .kotlin/errors/errors-1764509520395.log | 76 +++++ app/build.gradle.kts | 6 - .../work/data/repo/AuthDataStoreExt.kt | 7 + .../work/data/repo/AuthRepository.kt | 29 +- .../work/data/source/NetworkDataSource.kt | 125 +++++--- .../work/ui/screen/NavigationGraph.kt | 19 +- .../work/ui/screen/auth/AuthScreen.kt | 47 ++- .../work/ui/screen/book/BookScreen.kt | 268 ++++++++++++++++++ .../work/ui/screen/main/MainScreen.kt | 198 +++++++++++++ 10 files changed, 764 insertions(+), 87 deletions(-) create mode 100644 .kotlin/errors/errors-1764509512206.log create mode 100644 .kotlin/errors/errors-1764509520395.log create mode 100644 app/src/main/java/ru/myitschool/work/data/repo/AuthDataStoreExt.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt diff --git a/.kotlin/errors/errors-1764509512206.log b/.kotlin/errors/errors-1764509512206.log new file mode 100644 index 0000000..ef56a3f --- /dev/null +++ b/.kotlin/errors/errors-1764509512206.log @@ -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 + + diff --git a/.kotlin/errors/errors-1764509520395.log b/.kotlin/errors/errors-1764509520395.log new file mode 100644 index 0000000..ef56a3f --- /dev/null +++ b/.kotlin/errors/errors-1764509520395.log @@ -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 + + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76293d2..aa10ad0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,9 +1,3 @@ -/*plugins { - composeCompiler - kotlinAndroid - kotlinSerialization version Version.Kotlin.language - androidApplication -}*/ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthDataStoreExt.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthDataStoreExt.kt new file mode 100644 index 0000000..904c1a1 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthDataStoreExt.kt @@ -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") diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index d1dafdd..413749a 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -1,6 +1,9 @@ package ru.myitschool.work.data.repo +import androidx.datastore.preferences.core.edit 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.data.source.NetworkDataSource @@ -10,23 +13,17 @@ object AuthRepository { private val KEY_CODE = stringPreferencesKey("auth_code") private var codeCache: String? = null -// suspend fun checkAndSave(text: String): Result { -// return NetworkDataSource.checkAuth(text).onSuccess { success -> -// if (success) { -// codeCache = text -// } -// } -// } -suspend fun checkAndSave(text: String): Result { - return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text - dataStore.edit { prefs -> - prefs[KEY_CODE] = text + // Проверка кода на сервере и сохранение при успехе + suspend fun checkAndSave(text: String): Result { + return NetworkDataSource.checkAuth(text).onSuccess { success -> + if (success) { + codeCache = text + dataStore.edit { prefs -> + prefs[KEY_CODE] = text + } } } } -} /** Сохранённый код (из кэша или DataStore) */ suspend fun getSavedCode(): String? { @@ -47,6 +44,4 @@ suspend fun checkAndSave(text: String): Result { prefs.clear() } } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 33c3a8e..64eb4a0 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -19,6 +19,11 @@ import ru.myitschool.work.core.Constants import ru.myitschool.work.data.source.dto.UserDto object NetworkDataSource { + + /** Поставь false, когда поднимешь настоящий бэкенд */ + private const val USE_STUB = true + + // Реальный клиент Ktor (для будущего) private val client by lazy { HttpClient(CIO) { install(ContentNegotiation) { @@ -34,36 +39,78 @@ object NetworkDataSource { } } - suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { - return@withContext runCatching { - val response = client.get(getUrl(code, Constants.AUTH_URL)) - when (response.status) { - HttpStatusCode.OK -> true - else -> error(response.bodyAsText()) + // ----------------- Заглушечные данные ----------------- + + private val stubUser = UserDto( + name = "Тестовый пользователь", + booking = listOf( + UserDto.BookingDto( + room = "Опенспейс 1", + time = "2025-01-05" + ), + UserDto.BookingDto( + room = "Опенспейс 2", + time = "2025-01-06" + ), + ) + ) + + private val stubAvailableBookings: List = 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 { + 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 { + if (USE_STUB) { + return Result.success(stubUser) + } - - suspend fun getUserInfo(code: String): Result = withContext(Dispatchers.IO) { - runCatching { - val response = client.get(getUrl(code, Constants.INFO_URL)) - when (response.status) { - HttpStatusCode.OK -> response.body() - else -> error(response.bodyAsText()) + return withContext(Dispatchers.IO) { + runCatching { + val response = client.get(getUrl(code, Constants.INFO_URL)) + when (response.status) { + HttpStatusCode.OK -> response.body() + else -> error(response.bodyAsText()) + } } } } suspend fun getAvailableBookings( code: String - ): Result> = withContext(Dispatchers.IO) { - runCatching { - val response = client.get(getUrl(code, Constants.BOOKING_URL)) - when (response.status) { - HttpStatusCode.OK -> response.body>() - else -> error(response.bodyAsText()) + ): Result> { + if (USE_STUB) { + return Result.success(stubAvailableBookings) + } + + return withContext(Dispatchers.IO) { + runCatching { + val response = client.get(getUrl(code, Constants.BOOKING_URL)) + when (response.status) { + HttpStatusCode.OK -> response.body>() + else -> error(response.bodyAsText()) + } } } } @@ -72,24 +119,32 @@ object NetworkDataSource { code: String, room: String, time: String - ): Result = withContext(Dispatchers.IO) { - runCatching { - val response = client.post(getUrl(code, Constants.BOOK_URL)) { - setBody( - MultiPartFormDataContent( - formData { - append("room", room) - append("time", time) - } + ): Result { + if (USE_STUB) { + // Типа всё хорошо + return Result.success(Unit) + } + + return withContext(Dispatchers.IO) { + runCatching { + val response = client.post(getUrl(code, Constants.BOOK_URL)) { + setBody( + MultiPartFormDataContent( + formData { + append("room", room) + append("time", time) + } + ) ) - ) - } - when (response.status) { - HttpStatusCode.OK -> Unit - else -> error(response.bodyAsText()) + } + when (response.status) { + HttpStatusCode.OK -> Unit + else -> error(response.bodyAsText()) + } } } } - private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" -} \ No newline at end of file + private fun getUrl(code: String, targetUrl: String) = + "${Constants.HOST}/api/$code$targetUrl" +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 01b0f32..923b90a 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -2,10 +2,7 @@ package ru.myitschool.work.ui.screen import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.navigation.NavHostController 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.MainScreenDestination 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 fun AppNavHost( @@ -32,18 +31,10 @@ fun AppNavHost( AuthScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + MainScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + BookScreen(navController = navController) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index f99978e..b54ce5a 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -16,9 +16,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -41,7 +38,10 @@ fun AuthScreen( LaunchedEffect(Unit) { viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) + navController.navigate(MainScreenDestination) { + // очищаем backstack до экрана авторизации + popUpTo(MainScreenDestination) { inclusive = false } + } } } @@ -60,6 +60,7 @@ fun AuthScreen( when (val currentState = state) { is AuthState.Data -> Content(viewModel, currentState) is AuthState.Loading -> { + Spacer(modifier = Modifier.size(16.dp)) CircularProgressIndicator( modifier = Modifier.size(64.dp) ) @@ -73,25 +74,41 @@ private fun Content( viewModel: AuthViewModel, state: AuthState.Data ) { - var inputText by remember { mutableStateOf("") } 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( - modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), - value = inputText, - onValueChange = { - inputText = it - viewModel.onIntent(AuthIntent.TextInput(it)) - }, + modifier = Modifier + .testTag(TestIds.Auth.CODE_INPUT) + .fillMaxWidth(), + value = state.code, + onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) }, label = { Text(stringResource(R.string.auth_label)) } ) + Spacer(modifier = Modifier.size(16.dp)) + Button( - modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), + modifier = Modifier + .testTag(TestIds.Auth.SIGN_BUTTON) + .fillMaxWidth(), onClick = { - viewModel.onIntent(AuthIntent.Send(inputText)) + viewModel.onIntent(AuthIntent.Send(state.code)) }, - enabled = true + enabled = state.isButtonEnabled ) { Text(stringResource(R.string.auth_sign_in)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt new file mode 100644 index 0000000..c109bc8 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -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("Забронировать") + } + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt new file mode 100644 index 0000000..8bdb961 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -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 + ) + } + } + } + } + } +}