Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b40749faa1 | |||
| dbe735e541 | |||
| 40a8428a19 | |||
| 9e603f87e6 | |||
| beb48ab41e | |||
| 04ac941ba4 | |||
| 99c100929a | |||
| ce20de0daa | |||
| 523c373a04 | |||
| 01cf2c1439 | |||
| 371d09995c |
76
.kotlin/errors/errors-1764426177224.log
Normal file
76
.kotlin/errors/errors-1764426177224.log
Normal 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
|
||||||
|
|
||||||
|
|
||||||
76
.kotlin/errors/errors-1764509512206.log
Normal file
76
.kotlin/errors/errors-1764509512206.log
Normal 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
|
||||||
|
|
||||||
|
|
||||||
76
.kotlin/errors/errors-1764509520395.log
Normal file
76
.kotlin/errors/errors-1764509520395.log
Normal 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
|
||||||
|
|
||||||
|
|
||||||
76
.kotlin/errors/errors-1764603791320.log
Normal file
76
.kotlin/errors/errors-1764603791320.log
Normal 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
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# НТО 2025. II отборочный этап. Командные задания — Android
|
# НТО 2025. II отборочный этап. Командные задания — Android
|
||||||
|
|
||||||
## 📖 Предыстория
|
## 📖 Предыстория
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
plugins {
|
plugins {
|
||||||
composeCompiler
|
id("com.android.application")
|
||||||
kotlinAndroid
|
id("org.jetbrains.kotlin.android")
|
||||||
kotlinSerialization version Version.Kotlin.language
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
androidApplication
|
id("org.jetbrains.kotlin.plugin.serialization")
|
||||||
}
|
}
|
||||||
|
|
||||||
val packageName = "ru.myitschool.work"
|
val packageName = "ru.myitschool.work"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = packageName
|
namespace = packageName
|
||||||
compileSdk = Version.Android.Sdk.compile
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = packageName
|
applicationId = packageName
|
||||||
minSdk = Version.Android.Sdk.min
|
minSdk = 28
|
||||||
targetSdk = Version.Android.Sdk.target
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@@ -25,17 +24,17 @@ android {
|
|||||||
buildFeatures.viewBinding = true
|
buildFeatures.viewBinding = true
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = Version.Kotlin.javaSource
|
sourceCompatibility = /*Version.Kotlin.javaSource*/JavaVersion.VERSION_17
|
||||||
targetCompatibility = Version.Kotlin.javaSource
|
targetCompatibility = /*Version.Kotlin.javaSource*/JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = Version.Kotlin.jvmTarget
|
jvmTarget = /*Version.Kotlin.jvmTarget*/"17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
defaultComposeLibrary()
|
/* defaultComposeLibrary()*/
|
||||||
implementation("androidx.datastore:datastore-preferences:1.1.7")
|
implementation("androidx.datastore:datastore-preferences:1.1.7")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
|
||||||
implementation("androidx.navigation:navigation-compose:2.9.6")
|
implementation("androidx.navigation:navigation-compose:2.9.6")
|
||||||
@@ -48,4 +47,34 @@ dependencies {
|
|||||||
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
|
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
|
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
|
|
||||||
|
implementation("androidx.compose.ui:ui")
|
||||||
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
|
implementation("androidx.compose.material3:material3")
|
||||||
|
|
||||||
|
|
||||||
|
implementation("io.ktor:ktor-client-core:$ktor")
|
||||||
|
implementation("io.ktor:ktor-client-cio:$ktor")
|
||||||
|
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
|
||||||
|
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
|
||||||
|
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
|
|
||||||
|
|
||||||
|
implementation("androidx.activity:activity-compose:1.9.3")
|
||||||
|
|
||||||
|
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
||||||
|
|
||||||
|
|
||||||
|
implementation(platform("androidx.compose:compose-bom:2024.09.00"))
|
||||||
|
implementation("androidx.compose.ui:ui")
|
||||||
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
|
implementation("androidx.compose.material3:material3")
|
||||||
|
|
||||||
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
}
|
}
|
||||||
|
|||||||
35
app/src/main/java/ru/myitschool/work/core/DateUtils.kt
Normal file
35
app/src/main/java/ru/myitschool/work/core/DateUtils.kt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package ru.myitschool.work.core
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
object DateUtils {
|
||||||
|
|
||||||
|
private val mainFormatter: DateTimeFormatter =
|
||||||
|
DateTimeFormatter.ofPattern("dd.MM.yyyy")
|
||||||
|
private val bookFormatter: DateTimeFormatter =
|
||||||
|
DateTimeFormatter.ofPattern("dd.MM")
|
||||||
|
|
||||||
|
fun parseDate(raw: String): LocalDate? {
|
||||||
|
return runCatching { LocalDate.parse(raw) }.getOrElse {
|
||||||
|
runCatching { OffsetDateTime.parse(raw).toLocalDate() }.getOrElse {
|
||||||
|
runCatching {
|
||||||
|
LocalDate.parse(raw, DateTimeFormatter.ofPattern("dd.MM.yyyy"))
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формат для главного экрана: dd.MM.yyyy
|
||||||
|
fun formatForMain(raw: String): String {
|
||||||
|
val date = parseDate(raw) ?: return raw
|
||||||
|
return date.format(mainFormatter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формат для экрана бронирования: dd.MM
|
||||||
|
fun formatForBook(raw: String): String {
|
||||||
|
val date = parseDate(raw) ?: return raw
|
||||||
|
return date.format(bookFormatter)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
@@ -1,16 +1,47 @@
|
|||||||
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 kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import ru.myitschool.work.App
|
||||||
import ru.myitschool.work.data.source.NetworkDataSource
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
|
||||||
object AuthRepository {
|
object AuthRepository {
|
||||||
|
|
||||||
|
private val dataStore get() = App.context.authDataStore
|
||||||
|
private val KEY_CODE = stringPreferencesKey("auth_code")
|
||||||
private var codeCache: String? = null
|
private var codeCache: String? = null
|
||||||
|
|
||||||
|
|
||||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||||
if (success) {
|
if (success) {
|
||||||
codeCache = text
|
codeCache = text
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs[KEY_CODE] = text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
suspend fun getSavedCode(): String? {
|
||||||
|
codeCache?.let { return it }
|
||||||
|
|
||||||
|
val code = dataStore.data
|
||||||
|
.map { prefs -> prefs[KEY_CODE] }
|
||||||
|
.first()
|
||||||
|
|
||||||
|
codeCache = code
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun clear() {
|
||||||
|
codeCache = null
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import ru.myitschool.work.core.DateUtils
|
||||||
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
import ru.myitschool.work.data.source.dto.AvailablePlaceDto
|
||||||
|
import ru.myitschool.work.data.source.dto.BookedPlaceDto
|
||||||
|
import ru.myitschool.work.data.source.dto.UserDto
|
||||||
|
import ru.myitschool.work.domain.entities.BookingEntity
|
||||||
|
import ru.myitschool.work.domain.entities.UserEntity
|
||||||
|
|
||||||
|
object UserRepository {
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun getUserInfo(): Result<UserEntity> {
|
||||||
|
val code = AuthRepository.getSavedCode()
|
||||||
|
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||||
|
|
||||||
|
return NetworkDataSource
|
||||||
|
.getUserInfo(code)
|
||||||
|
.map { dto -> dto.toDomainUser() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun getAvailableBookings(): Result<List<BookingEntity>> {
|
||||||
|
val code = AuthRepository.getSavedCode()
|
||||||
|
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||||
|
|
||||||
|
return NetworkDataSource
|
||||||
|
.getAvailableBookings(code)
|
||||||
|
.map { map -> map.toDomainBookings() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun book(date: String, placeId: Int): Result<Unit> {
|
||||||
|
val code = AuthRepository.getSavedCode()
|
||||||
|
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||||
|
|
||||||
|
return NetworkDataSource.book(
|
||||||
|
code = code,
|
||||||
|
date = date,
|
||||||
|
placeId = placeId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun UserDto.toDomainUser(): UserEntity {
|
||||||
|
val bookings = booking
|
||||||
|
.flatMap { (dateString, bookedPlace) ->
|
||||||
|
listOfNotNull(bookedPlace.toDomainBooking(dateString))
|
||||||
|
}
|
||||||
|
.sortedBy { booking ->
|
||||||
|
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserEntity(
|
||||||
|
name = name,
|
||||||
|
photoUrl = photoUrl,
|
||||||
|
bookings = bookings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, List<AvailablePlaceDto>>.toDomainBookings(): List<BookingEntity> {
|
||||||
|
return entries
|
||||||
|
.flatMap { (dateString, places) ->
|
||||||
|
places.mapNotNull { dto ->
|
||||||
|
dto.toDomainBooking(dateString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sortedBy { booking ->
|
||||||
|
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BookedPlaceDto.toDomainBooking(dateString: String): BookingEntity? {
|
||||||
|
val parsedDate = DateUtils.parseDate(dateString) ?: return null
|
||||||
|
return BookingEntity(
|
||||||
|
id = id,
|
||||||
|
roomName = place,
|
||||||
|
time = parsedDate.toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AvailablePlaceDto.toDomainBooking(dateString: String): BookingEntity? {
|
||||||
|
val parsedDate = DateUtils.parseDate(dateString) ?: return null
|
||||||
|
return BookingEntity(
|
||||||
|
id = id,
|
||||||
|
roomName = place,
|
||||||
|
time = parsedDate.toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,32 @@
|
|||||||
package ru.myitschool.work.data.source
|
package ru.myitschool.work.data.source
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.client.utils.EmptyContent.contentType
|
||||||
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.http.contentType
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.myitschool.work.core.Constants
|
import ru.myitschool.work.core.Constants
|
||||||
|
import ru.myitschool.work.data.source.dto.AvailablePlaceDto
|
||||||
|
import ru.myitschool.work.data.source.dto.BookRequestDto
|
||||||
|
import ru.myitschool.work.data.source.dto.BookedPlaceDto
|
||||||
|
import ru.myitschool.work.data.source.dto.UserDto
|
||||||
|
|
||||||
object NetworkDataSource {
|
object NetworkDataSource {
|
||||||
|
|
||||||
|
|
||||||
|
private const val USE_STUB: Boolean = false
|
||||||
|
|
||||||
private val client by lazy {
|
private val client by lazy {
|
||||||
HttpClient(CIO) {
|
HttpClient(CIO) {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
@@ -20,7 +34,7 @@ object NetworkDataSource {
|
|||||||
Json {
|
Json {
|
||||||
isLenient = true
|
isLenient = true
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
explicitNulls = true
|
explicitNulls = false
|
||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -28,15 +42,132 @@ 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
|
photoUrl = null,
|
||||||
else -> error(response.bodyAsText())
|
booking = mapOf(
|
||||||
|
"2025-01-05" to BookedPlaceDto(id = 1, place = "102"),
|
||||||
|
"2025-01-06" to BookedPlaceDto(id = 2, place = "209.13"),
|
||||||
|
"2025-01-09" to BookedPlaceDto(id = 3, place = "Зона 51. 50"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val stubAvailableBookings: Map<String, List<AvailablePlaceDto>> = mapOf(
|
||||||
|
"2025-01-05" to listOf(
|
||||||
|
AvailablePlaceDto(id = 1, place = "102"),
|
||||||
|
AvailablePlaceDto(id = 2, place = "209.13"),
|
||||||
|
),
|
||||||
|
"2025-01-06" to listOf(
|
||||||
|
AvailablePlaceDto(id = 3, place = "Зона 51. 50"),
|
||||||
|
),
|
||||||
|
"2025-01-07" to listOf(
|
||||||
|
AvailablePlaceDto(id = 4, place = "102"),
|
||||||
|
AvailablePlaceDto(id = 5, place = "209.13"),
|
||||||
|
),
|
||||||
|
"2025-01-08" to listOf(
|
||||||
|
AvailablePlaceDto(id = 6, place = "209.13"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----------------- Публичные методы -----------------
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
|
||||||
|
else -> error(response.bodyAsText())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
|
||||||
}
|
suspend fun getUserInfo(code: String): Result<UserDto> {
|
||||||
|
if (USE_STUB) {
|
||||||
|
return Result.success(stubUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val response = client.get(getUrl(code, Constants.INFO_URL))
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.OK -> response.body<UserDto>()
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
|
||||||
|
else -> error(response.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun getAvailableBookings(
|
||||||
|
code: String,
|
||||||
|
): Result<Map<String, List<AvailablePlaceDto>>> {
|
||||||
|
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<Map<String, List<AvailablePlaceDto>>>()
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
|
||||||
|
else -> error(response.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun book(
|
||||||
|
code: String,
|
||||||
|
date: String,
|
||||||
|
placeId: Int,
|
||||||
|
): Result<Unit> {
|
||||||
|
if (USE_STUB) {
|
||||||
|
// В режиме заглушки считаем, что всё прошло успешно
|
||||||
|
return Result.success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(
|
||||||
|
BookRequestDto(
|
||||||
|
date = date,
|
||||||
|
placeId = placeId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.Created,
|
||||||
|
HttpStatusCode.OK -> Unit
|
||||||
|
HttpStatusCode.Conflict -> error("Уже забронировано")
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
|
||||||
|
else -> error(response.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUrl(code: String, targetUrl: String): String =
|
||||||
|
"${Constants.HOST}/api/$code$targetUrl"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package ru.myitschool.work.data.source.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UserDto(
|
||||||
|
@SerialName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerialName("photoUrl")
|
||||||
|
val photoUrl: String? = null,
|
||||||
|
@SerialName("booking")
|
||||||
|
val booking: Map<String, BookedPlaceDto> = emptyMap(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BookedPlaceDto(
|
||||||
|
@SerialName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("place")
|
||||||
|
val place: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AvailablePlaceDto(
|
||||||
|
@SerialName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("place")
|
||||||
|
val place: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BookRequestDto(
|
||||||
|
@SerialName("date")
|
||||||
|
val date: String,
|
||||||
|
@SerialName("placeID")
|
||||||
|
val placeId: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ru.myitschool.work.domain.auth
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
|
||||||
|
class ClearAuthDataUseCase(
|
||||||
|
private val repository: AuthRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke() {
|
||||||
|
repository.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ru.myitschool.work.domain.auth
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
|
||||||
|
class GetSavedAuthCodeUseCase(
|
||||||
|
private val repository: AuthRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(): String? {
|
||||||
|
return repository.getSavedCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package ru.myitschool.work.domain.booking
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.repo.UserRepository
|
||||||
|
|
||||||
|
class BookPlaceUseCase(
|
||||||
|
private val repository: UserRepository,
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(roomName: String, placeId: Int): Result<Unit> {
|
||||||
|
// Нужно получить дату из какого-то источника
|
||||||
|
// Либо изменить логику в ViewModel
|
||||||
|
return repository.book(roomName, placeId) // ← но repository.book ожидает date, а не roomName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//package ru.myitschool.work.domain.booking
|
||||||
|
//
|
||||||
|
//import ru.myitschool.work.data.repo.UserRepository
|
||||||
|
//import ru.myitschool.work.domain.entities.BookingEntity
|
||||||
|
//
|
||||||
|
//class GetAvailableBookingsUseCase(
|
||||||
|
// private val repository: UserRepository
|
||||||
|
//) {
|
||||||
|
// suspend operator fun invoke(): Result<List<BookingEntity>> {
|
||||||
|
// return repository.getAvailableBookings()
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.myitschool.work.domain.entities
|
||||||
|
|
||||||
|
data class BookingEntity(
|
||||||
|
val id: Int,
|
||||||
|
val roomName: String,
|
||||||
|
val time: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.myitschool.work.domain.entities
|
||||||
|
|
||||||
|
data class UserEntity(
|
||||||
|
val name: String,
|
||||||
|
val photoUrl: String?,
|
||||||
|
val bookings: List<BookingEntity>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ru.myitschool.work.domain.booking
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.repo.UserRepository
|
||||||
|
import ru.myitschool.work.domain.entities.BookingEntity
|
||||||
|
|
||||||
|
class GetAvailableBookingsUseCase(
|
||||||
|
private val repository: UserRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(): Result<List<BookingEntity>> {
|
||||||
|
return repository.getAvailableBookings()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ru.myitschool.work.domain.user
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.repo.UserRepository
|
||||||
|
import ru.myitschool.work.domain.entities.UserEntity
|
||||||
|
|
||||||
|
class GetUserInfoUseCase(
|
||||||
|
private val repository: UserRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(): Result<UserEntity> {
|
||||||
|
return repository.getUserInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ class RootActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
actionBar?.hide()
|
||||||
setContent {
|
setContent {
|
||||||
WorkTheme {
|
WorkTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
|
|||||||
@@ -2,48 +2,62 @@ 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.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
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
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
import ru.myitschool.work.domain.auth.GetSavedAuthCodeUseCase
|
||||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
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(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
navController: NavHostController = rememberNavController()
|
navController: NavHostController = rememberNavController()
|
||||||
) {
|
) {
|
||||||
|
// use case для проверки, есть ли сохранённый код
|
||||||
|
val getSavedAuthCodeUseCase = remember {
|
||||||
|
GetSavedAuthCodeUseCase(AuthRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
// асинхронно определяем, с какого экрана стартовать
|
||||||
|
val startDestination by produceState<Any?>(initialValue = null) {
|
||||||
|
val savedCode = getSavedAuthCodeUseCase()
|
||||||
|
value = if (savedCode.isNullOrBlank()) {
|
||||||
|
AuthScreenDestination
|
||||||
|
} else {
|
||||||
|
MainScreenDestination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val destination = startDestination ?: return
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enterTransition = { EnterTransition.None },
|
enterTransition = { EnterTransition.None },
|
||||||
exitTransition = { ExitTransition.None },
|
exitTransition = { ExitTransition.None },
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = AuthScreenDestination,
|
startDestination = destination,
|
||||||
) {
|
) {
|
||||||
composable<AuthScreenDestination> {
|
composable<AuthScreenDestination> {
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ru.myitschool.work.ui.screen.auth
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -9,25 +10,27 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
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
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import io.ktor.websocket.Frame
|
||||||
import ru.myitschool.work.R
|
import ru.myitschool.work.R
|
||||||
import ru.myitschool.work.core.TestIds
|
import ru.myitschool.work.core.TestIds
|
||||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||||
@@ -41,7 +44,10 @@ fun AuthScreen(
|
|||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.actionFlow.collect {
|
viewModel.actionFlow.collect {
|
||||||
navController.navigate(MainScreenDestination)
|
navController.navigate(MainScreenDestination) {
|
||||||
|
// очищаем backstack до экрана авторизации
|
||||||
|
popUpTo(MainScreenDestination) { inclusive = true }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +56,20 @@ fun AuthScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(all = 24.dp),
|
.padding(all = 24.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Top
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.size(48.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.auth_name),
|
||||||
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(48.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.auth_title),
|
text = stringResource(R.string.auth_title),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
@@ -60,6 +78,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(24.dp))
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(64.dp)
|
modifier = Modifier.size(64.dp)
|
||||||
)
|
)
|
||||||
@@ -73,25 +92,54 @@ 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))
|
||||||
TextField(
|
|
||||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
OutlinedTextField(
|
||||||
value = inputText,
|
modifier = Modifier
|
||||||
onValueChange = {
|
.testTag(TestIds.Auth.CODE_INPUT)
|
||||||
inputText = it
|
.fillMaxWidth(),
|
||||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
value = state.code,
|
||||||
},
|
onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) },
|
||||||
label = { Text(stringResource(R.string.auth_label)) }
|
label = { Text(stringResource(R.string.auth_label)) },
|
||||||
|
placeholder = { Text("Введите код сотрудника") }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
|
||||||
Button(
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
|
||||||
onClick = {
|
if (state.isErrorVisible) {
|
||||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
Text(
|
||||||
},
|
modifier = Modifier
|
||||||
enabled = true
|
.fillMaxWidth()
|
||||||
) {
|
.testTag(TestIds.Auth.ERROR),
|
||||||
Text(stringResource(R.string.auth_sign_in))
|
text = "Неверный код или ошибка сервера",
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(TestIds.Auth.SIGN_BUTTON)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onClick = {
|
||||||
|
viewModel.onIntent(AuthIntent.Send(state.code))
|
||||||
|
},
|
||||||
|
enabled = state.isButtonEnabled
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.auth_sign_in))
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_arrow_right),
|
||||||
|
contentDescription = "Войти"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
sealed interface AuthState {
|
sealed interface AuthState {
|
||||||
object Loading: AuthState
|
object Loading : AuthState
|
||||||
object Data: AuthState
|
|
||||||
|
data class Data(
|
||||||
|
val code: String = "",
|
||||||
|
val isButtonEnabled: Boolean = false,
|
||||||
|
val isErrorVisible: Boolean = false,
|
||||||
|
) : AuthState
|
||||||
}
|
}
|
||||||
@@ -12,32 +12,82 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||||
|
import ru.myitschool.work.domain.auth.GetSavedAuthCodeUseCase
|
||||||
|
|
||||||
class AuthViewModel : ViewModel() {
|
class AuthViewModel : ViewModel() {
|
||||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
|
||||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
private val checkAndSaveAuthCodeUseCase by lazy {
|
||||||
|
CheckAndSaveAuthCodeUseCase(AuthRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val getSavedAuthCodeUseCase by lazy {
|
||||||
|
GetSavedAuthCodeUseCase(AuthRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data())
|
||||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|
||||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
val actionFlow: SharedFlow<Unit> = _actionFlow
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
val saved = getSavedAuthCodeUseCase()
|
||||||
|
if (saved != null) {
|
||||||
|
_actionFlow.emit(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onIntent(intent: AuthIntent) {
|
fun onIntent(intent: AuthIntent) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
|
is AuthIntent.TextInput -> {
|
||||||
|
val newCode = intent.text
|
||||||
|
val isValid = isValidCode(newCode)
|
||||||
|
_uiState.update {
|
||||||
|
AuthState.Data(
|
||||||
|
code = newCode,
|
||||||
|
isButtonEnabled = isValid,
|
||||||
|
isErrorVisible = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is AuthIntent.Send -> {
|
is AuthIntent.Send -> {
|
||||||
|
val currentState = _uiState.value
|
||||||
|
if (currentState !is AuthState.Data) return
|
||||||
|
|
||||||
|
val code = currentState.code
|
||||||
|
if (!isValidCode(code)) return
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
_uiState.update { AuthState.Loading }
|
_uiState.update { AuthState.Loading }
|
||||||
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
|
checkAndSaveAuthCodeUseCase.invoke(code).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
|
// успешная авторизация → навигация на главный экран
|
||||||
_actionFlow.emit(Unit)
|
_actionFlow.emit(Unit)
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
error.printStackTrace()
|
error.printStackTrace()
|
||||||
_actionFlow.emit(Unit)
|
_uiState.update {
|
||||||
|
AuthState.Data(
|
||||||
|
code = code,
|
||||||
|
isButtonEnabled = true,
|
||||||
|
isErrorVisible = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AuthIntent.TextInput -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private fun isValidCode(text: String): Boolean {
|
||||||
|
if (text.length != 4) return false
|
||||||
|
return text.all { ch ->
|
||||||
|
ch.isDigit() || (ch in 'a'..'z') || (ch in 'A'..'Z')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
sealed interface BookIntent {
|
||||||
|
data object Load : BookIntent
|
||||||
|
data object Refresh : BookIntent
|
||||||
|
data class SelectDate(val index: Int) : BookIntent
|
||||||
|
data class SelectPlace(val id: Int) : BookIntent
|
||||||
|
data object Book : BookIntent
|
||||||
|
}
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.main_screen_refresh))
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_refresh),
|
||||||
|
contentDescription = "Обновить"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
|
||||||
|
onClick = onBack
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_back),
|
||||||
|
contentDescription = "Назад"
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(text = stringResource(R.string.book_screen_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.main_screen_refresh))
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_refresh),
|
||||||
|
contentDescription = "Обновить"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
|
||||||
|
onClick = onBack
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_back),
|
||||||
|
contentDescription = "Назад"
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(text = stringResource(R.string.book_screen_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_back),
|
||||||
|
contentDescription = "Назад"
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(text = stringResource(R.string.book_screen_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
|
||||||
|
onClick = onRefresh
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.main_screen_refresh))
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_refresh),
|
||||||
|
contentDescription = "Обновить"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
.clickable { onSelectDate(index) }
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
|
tonalElevation = if (index == state.selectedDateIndex) 4.dp else 0.dp,
|
||||||
|
color = if (index == state.selectedDateIndex) {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surface
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
.testTag(TestIds.Book.ITEM_DATE),
|
||||||
|
text = dateLabel,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (index == state.selectedDateIndex) {
|
||||||
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.book_screen_book))
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_arrow_right),
|
||||||
|
contentDescription = "Забронировать"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
data class BookPlaceItem(
|
||||||
|
val id: Int,
|
||||||
|
val roomName: String,
|
||||||
|
val time: String,
|
||||||
|
val isSelected: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface BookState {
|
||||||
|
object Loading : BookState
|
||||||
|
|
||||||
|
data class Data(
|
||||||
|
val dates: List<String>,
|
||||||
|
val selectedDateIndex: Int,
|
||||||
|
val places: List<BookPlaceItem>,
|
||||||
|
) : BookState
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val message: String,
|
||||||
|
) : BookState
|
||||||
|
|
||||||
|
object Empty : BookState
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import java.time.LocalDate
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.myitschool.work.core.DateUtils
|
||||||
|
import ru.myitschool.work.data.repo.UserRepository
|
||||||
|
import ru.myitschool.work.domain.booking.BookPlaceUseCase
|
||||||
|
import ru.myitschool.work.domain.booking.GetAvailableBookingsUseCase
|
||||||
|
import ru.myitschool.work.domain.entities.BookingEntity
|
||||||
|
|
||||||
|
class BookViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val getAvailableBookingsUseCase by lazy {
|
||||||
|
GetAvailableBookingsUseCase(UserRepository)
|
||||||
|
}
|
||||||
|
private val bookPlaceUseCase by lazy {
|
||||||
|
BookPlaceUseCase(UserRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
|
||||||
|
val uiState: StateFlow<BookState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
sealed interface Action {
|
||||||
|
data object CloseWithSuccess : Action
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _actionFlow = MutableSharedFlow<Action>()
|
||||||
|
val actionFlow: SharedFlow<Action> = _actionFlow
|
||||||
|
|
||||||
|
private var allGroups: List<DateGroup> = emptyList()
|
||||||
|
private var selectedDateIndex: Int = 0
|
||||||
|
private var selectedSlotId: Int? = null
|
||||||
|
|
||||||
|
fun onIntent(intent: BookIntent) {
|
||||||
|
when (intent) {
|
||||||
|
BookIntent.Load,
|
||||||
|
BookIntent.Refresh -> {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
is BookIntent.SelectDate -> {
|
||||||
|
selectDate(intent.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
is BookIntent.SelectPlace -> {
|
||||||
|
selectedSlotId = intent.id
|
||||||
|
val current = _uiState.value
|
||||||
|
if (current is BookState.Data) {
|
||||||
|
val updatedPlaces = current.places.map { item ->
|
||||||
|
item.copy(isSelected = item.id == intent.id)
|
||||||
|
}
|
||||||
|
_uiState.value = current.copy(places = updatedPlaces)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BookIntent.Book -> {
|
||||||
|
book()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
_uiState.value = BookState.Loading
|
||||||
|
|
||||||
|
getAvailableBookingsUseCase()
|
||||||
|
.fold(
|
||||||
|
onSuccess = { bookings ->
|
||||||
|
if (bookings.isEmpty()) {
|
||||||
|
_uiState.value = BookState.Empty
|
||||||
|
allGroups = emptyList()
|
||||||
|
selectedSlotId = null
|
||||||
|
selectedDateIndex = 0
|
||||||
|
return@fold
|
||||||
|
}
|
||||||
|
|
||||||
|
allGroups = groupByDate(bookings)
|
||||||
|
|
||||||
|
if (allGroups.isEmpty()) {
|
||||||
|
_uiState.value = BookState.Empty
|
||||||
|
selectedSlotId = null
|
||||||
|
selectedDateIndex = 0
|
||||||
|
return@fold
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedDateIndex = 0
|
||||||
|
selectedSlotId = null
|
||||||
|
val firstGroup = allGroups[0]
|
||||||
|
|
||||||
|
val places = firstGroup.slots.map { slot ->
|
||||||
|
BookPlaceItem(
|
||||||
|
id = slot.id,
|
||||||
|
roomName = slot.roomName,
|
||||||
|
time = slot.time,
|
||||||
|
isSelected = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.value = BookState.Data(
|
||||||
|
dates = allGroups.map { it.label },
|
||||||
|
selectedDateIndex = 0,
|
||||||
|
places = places,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
_uiState.value = BookState.Error(
|
||||||
|
message = error.message ?: "Ошибка загрузки бронирований",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectDate(index: Int) {
|
||||||
|
val groups = allGroups
|
||||||
|
if (index !in groups.indices) return
|
||||||
|
|
||||||
|
selectedDateIndex = index
|
||||||
|
selectedSlotId = null
|
||||||
|
val group = groups[index]
|
||||||
|
val places = group.slots.map { slot ->
|
||||||
|
BookPlaceItem(
|
||||||
|
id = slot.id,
|
||||||
|
roomName = slot.roomName,
|
||||||
|
time = slot.time,
|
||||||
|
isSelected = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val datesLabels = groups.map { it.label }
|
||||||
|
|
||||||
|
val current = _uiState.value
|
||||||
|
_uiState.value = if (current is BookState.Data) {
|
||||||
|
current.copy(
|
||||||
|
dates = datesLabels,
|
||||||
|
selectedDateIndex = index,
|
||||||
|
places = places,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BookState.Data(
|
||||||
|
dates = datesLabels,
|
||||||
|
selectedDateIndex = index,
|
||||||
|
places = places,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun book() {
|
||||||
|
val current = _uiState.value
|
||||||
|
if (current !is BookState.Data) return
|
||||||
|
|
||||||
|
val slotId = selectedSlotId ?: return
|
||||||
|
|
||||||
|
allGroups
|
||||||
|
.flatMap { it.slots }
|
||||||
|
.firstOrNull { it.id == slotId }
|
||||||
|
?: return
|
||||||
|
|
||||||
|
|
||||||
|
val selectedDate = allGroups[selectedDateIndex].date.toString()
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
|
||||||
|
bookPlaceUseCase(selectedDate, slotId)
|
||||||
|
.fold(
|
||||||
|
onSuccess = {
|
||||||
|
_actionFlow.emit(Action.CloseWithSuccess)
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
_uiState.value = BookState.Error(
|
||||||
|
message = error.message ?: "Ошибка бронирования",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class DateGroup(
|
||||||
|
val date: LocalDate,
|
||||||
|
val label: String,
|
||||||
|
val slots: List<BookingEntity>,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun groupByDate(bookings: List<BookingEntity>): List<DateGroup> {
|
||||||
|
val grouped: Map<LocalDate, List<BookingEntity>> =
|
||||||
|
bookings
|
||||||
|
.mapNotNull { booking ->
|
||||||
|
val date = DateUtils.parseDate(booking.time) ?: return@mapNotNull null
|
||||||
|
date to booking
|
||||||
|
}
|
||||||
|
.groupBy({ it.first }, { it.second })
|
||||||
|
|
||||||
|
return grouped.entries
|
||||||
|
.sortedBy { it.key }
|
||||||
|
.map { (date, slots) ->
|
||||||
|
DateGroup(
|
||||||
|
date = date,
|
||||||
|
label = DateUtils.formatForBook(date.toString()),
|
||||||
|
slots = slots,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
sealed interface MainIntent {
|
||||||
|
data object Load : MainIntent
|
||||||
|
data object Refresh : MainIntent
|
||||||
|
data object Logout : MainIntent
|
||||||
|
data object AddBooking : MainIntent
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults.Icon
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import coil3.compose.SubcomposeAsyncImage
|
||||||
|
import org.intellij.lang.annotations.JdkConstants
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
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)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.testTag(TestIds.Main.PROFILE_IMAGE),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
UserAvatar(
|
||||||
|
photoUrl = state.photoUrl,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(TestIds.Main.PROFILE_NAME)
|
||||||
|
.weight(1f),
|
||||||
|
text = state.name,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(TestIds.Main.LOGOUT_BUTTON),
|
||||||
|
onClick = onLogout,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
containerColor = MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text("Выход")
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_logout),
|
||||||
|
contentDescription = "Выйти"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.main_screen_title),
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(20.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)
|
||||||
|
) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.main_screen_date),
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
|
||||||
|
text = item.dateLabel,
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.main_screen_place),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
|
||||||
|
text = item.roomName,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
|
||||||
|
onClick = onRefresh
|
||||||
|
) { Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.main_screen_refresh))
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_refresh),
|
||||||
|
contentDescription = "Обновить"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON),
|
||||||
|
onClick = onAddBooking
|
||||||
|
) { Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.main_screen_new_book))
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_add),
|
||||||
|
contentDescription = "Новая бронь"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Composable
|
||||||
|
private fun UserAvatar(
|
||||||
|
photoUrl: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (photoUrl.isNullOrBlank()) {
|
||||||
|
// Фолбек на старый смайл / иконку
|
||||||
|
Image(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SubcomposeAsyncImage(
|
||||||
|
model = photoUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
loading = {
|
||||||
|
Image(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
error = {
|
||||||
|
Image(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
data class MainBookingItem(
|
||||||
|
val id: Int,
|
||||||
|
val dateLabel: String,
|
||||||
|
val roomName: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface MainState {
|
||||||
|
object Loading : MainState
|
||||||
|
|
||||||
|
data class Data(
|
||||||
|
val name: String,
|
||||||
|
val photoUrl: String?,
|
||||||
|
val bookings: List<MainBookingItem>,
|
||||||
|
) : MainState
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val message: String,
|
||||||
|
) : MainState
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import java.time.LocalDate
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.myitschool.work.core.DateUtils
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
import ru.myitschool.work.data.repo.UserRepository
|
||||||
|
import ru.myitschool.work.domain.auth.ClearAuthDataUseCase
|
||||||
|
import ru.myitschool.work.domain.user.GetUserInfoUseCase
|
||||||
|
|
||||||
|
class MainViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val getUserInfoUseCase by lazy { GetUserInfoUseCase(UserRepository) }
|
||||||
|
private val clearAuthDataUseCase by lazy { ClearAuthDataUseCase(AuthRepository) }
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
|
||||||
|
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
sealed interface Action {
|
||||||
|
data object NavigateToAuth : Action
|
||||||
|
data object NavigateToBooking : Action
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _actionFlow = MutableSharedFlow<Action>()
|
||||||
|
val actionFlow: SharedFlow<Action> = _actionFlow
|
||||||
|
|
||||||
|
fun onIntent(intent: MainIntent) {
|
||||||
|
when (intent) {
|
||||||
|
MainIntent.Load,
|
||||||
|
MainIntent.Refresh -> {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
MainIntent.Logout -> {
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
clearAuthDataUseCase()
|
||||||
|
_actionFlow.emit(Action.NavigateToAuth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MainIntent.AddBooking -> {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_actionFlow.emit(Action.NavigateToBooking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
_uiState.value = MainState.Loading
|
||||||
|
getUserInfoUseCase()
|
||||||
|
.fold(
|
||||||
|
onSuccess = { user ->
|
||||||
|
val bookings = user.bookings
|
||||||
|
.sortedWith(
|
||||||
|
compareBy { booking ->
|
||||||
|
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.mapIndexed { index, booking ->
|
||||||
|
MainBookingItem(
|
||||||
|
id = index,
|
||||||
|
dateLabel = DateUtils.formatForMain(booking.time),
|
||||||
|
roomName = booking.roomName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.value = MainState.Data(
|
||||||
|
name = user.name,
|
||||||
|
photoUrl = user.photoUrl,
|
||||||
|
bookings = bookings,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
_uiState.value = MainState.Error(
|
||||||
|
message = error.message ?: "Ошибка загрузки данных",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,26 +32,26 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
*/
|
*/
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WorkTheme(
|
fun WorkTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
// Dynamic color is available on Android 12+
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = true,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
darkTheme -> DarkColorScheme
|
||||||
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
MaterialTheme(
|
||||||
else -> LightColorScheme
|
colorScheme = colorScheme,
|
||||||
}
|
typography = Typography,
|
||||||
|
content = content
|
||||||
MaterialTheme(
|
)
|
||||||
colorScheme = colorScheme,
|
}
|
||||||
typography = Typography,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
14
app/src/main/res/drawable/ic_add.xml
Normal file
14
app/src/main/res/drawable/ic_add.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M0.5,8.5v-1h15v1Z"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M8.5,15.5h-1V0.5h1Z"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_arrow_right.xml
Normal file
5
app/src/main/res/drawable/ic_arrow_right.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M11.293,4.707l6.293,6.293l-13.586,0l0,2l13.586,0l-6.293,6.293l1.414,1.414l8.707,-8.707l-8.707,-8.707l-1.414,1.414z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_back.xml
Normal file
9
app/src/main/res/drawable/ic_back.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M11.62,3.81l-4.19,4.19l4.19,4.19l-1.53,1.52l-5.71,-5.71l5.71,-5.71l1.53,1.52z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_logout.xml
Normal file
9
app/src/main/res/drawable/ic_logout.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="32" android:viewportWidth="32" android:width="16dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M27.9,2.58a0.86,0.86 0,0 0,-0.07 -0.1,0.71 0.71,0 0,0 -0.19,-0.23l0,0 -0.09,0a1.12,1.12 0,0 0,-0.25 -0.11L27.1,2 27,2H12a1,1 0,0 0,-1 1V9a1,1 0,0 0,2 0V4h7.19L16.71,5A1,1 0,0 0,16 6V25H13V22a1,1 0,0 0,-2 0v4a1,1 0,0 0,1 1h4v2a1,1 0,0 0,0.4 0.8,1 1,0 0,0 0.6,0.2 1,1 0,0 0,0.29 0l10,-3A1,1 0,0 0,28 26V3A1,1 0,0 0,27.9 2.58ZM26,25.26l-8,2.4V6.74l8,-2.4Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M7.41,17H14a1,1 0,0 0,0 -2H7.41l1.3,-1.29a1,1 0,0 0,-1.42 -1.42l-3,3a1,1 0,0 0,-0.21 0.33,1 1,0 0,0 0,0.76 1,1 0,0 0,0.21 0.33l3,3a1,1 0,0 0,1.42 0,1 1,0 0,0 0,-1.42Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M20,17a1,1 0,0 0,0 -2h0a1,1 0,1 0,0 2Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_refresh.xml
Normal file
5
app/src/main/res/drawable/ic_refresh.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M12,3A8.959,8.959 0,0 0,5 6.339V4H3v6H9V8H6.274A6.982,6.982 0,1 1,5.22 13.751l-1.936,0.5A9,9 0,1 0,12 3Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Work</string>
|
<string name="app_name">Work</string>
|
||||||
<string name="title_activity_root">RootActivity</string>
|
<string name="title_activity_root">RootActivity</string>
|
||||||
<string name="auth_title">Привет! Введи код для авторизации</string>
|
<string name="auth_name">Система бронирования</string>
|
||||||
|
<string name="auth_title">Вход в систему</string>
|
||||||
<string name="auth_label">Код</string>
|
<string name="auth_label">Код</string>
|
||||||
<string name="auth_sign_in">Войти</string>
|
<string name="auth_sign_in">Войти</string>
|
||||||
|
<string name="main_screen_date">Дата:</string>
|
||||||
|
<string name="main_screen_place">Место:</string>
|
||||||
|
<string name="main_screen_title">Мои бронирования</string>
|
||||||
|
<string name="main_screen_refresh">Обновить</string>
|
||||||
|
<string name="main_screen_new_book">Новая бронь</string>
|
||||||
|
<string name="book_screen_book">Забронировать</string>
|
||||||
|
<string name="book_screen_back">Назад</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
androidApplication version Version.agp apply false
|
// id("com.android.application") version "8.5.2" apply false
|
||||||
kotlinJvm version Version.Kotlin.language apply false
|
id("com.android.application") version "8.6.0" apply false
|
||||||
kotlinAndroid version Version.Kotlin.language apply false
|
|
||||||
composeCompiler version Version.Kotlin.language apply false
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" apply false
|
||||||
|
id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,16 @@
|
|||||||
# Project-wide Gradle settings.
|
## For more details on how to configure your build environment visit
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
#
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||||
|
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
|
#
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. For more details, visit
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
#Thu Dec 04 14:16:52 GMT+07:00 2025
|
||||||
# Android operating system, and which are packaged with your app's APK
|
android.nonTransitiveRClass=true
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
|
||||||
# thereby reducing the size of the R class for that library
|
|
||||||
android.nonTransitiveRClass=true
|
|
||||||
|
|||||||
43
gradlew
vendored
43
gradlew
vendored
@@ -15,6 +15,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -80,13 +82,11 @@ do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -133,22 +133,29 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD=java
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
@@ -205,6 +216,12 @@ set -- \
|
|||||||
org.gradle.wrapper.GradleWrapperMain \
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
# Use "xargs" to parse quoted args.
|
||||||
#
|
#
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
|||||||
37
gradlew.bat
vendored
37
gradlew.bat
vendored
@@ -13,8 +13,10 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@@ -25,7 +27,8 @@
|
|||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
exit /b 1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|||||||
Reference in New Issue
Block a user