From e254554532829ebb7c41a7a73254305a5d7e0283 Mon Sep 17 00:00:00 2001 From: calboo Date: Wed, 11 Feb 2026 15:58:30 +0100 Subject: [PATCH] mobile base --- AndroidApp/.idea/deploymentTargetSelector.xml | 10 + .../.kotlin/errors/errors-1770819867596.log | 193 ++++++++++++++ .../.kotlin/errors/errors-1770819878847.log | 193 ++++++++++++++ AndroidApp/app/build.gradle.kts | 28 +++ AndroidApp/app/src/main/AndroidManifest.xml | 17 +- .../erfmann/opentimetracker/MainActivity.kt | 236 ++++++++++++++++++ .../opentimetracker/data/ApiService.kt | 23 ++ .../erfmann/opentimetracker/data/Models.kt | 64 +++++ .../opentimetracker/data/UserPreferences.kt | 35 +++ .../opentimetracker/di/NetworkModule.kt | 53 ++++ .../opentimetracker/ui/AuthViewModel.kt | 47 ++++ .../opentimetracker/ui/DashboardViewModel.kt | 87 +++++++ AndroidApp/build.gradle.kts | 2 + AndroidApp/gradle/libs.versions.toml | 21 +- 14 files changed, 1007 insertions(+), 2 deletions(-) create mode 100644 AndroidApp/.idea/deploymentTargetSelector.xml create mode 100644 AndroidApp/.kotlin/errors/errors-1770819867596.log create mode 100644 AndroidApp/.kotlin/errors/errors-1770819878847.log create mode 100644 AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/MainActivity.kt create mode 100644 AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/ApiService.kt create mode 100644 AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/Models.kt create mode 100644 AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/UserPreferences.kt create mode 100644 AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/di/NetworkModule.kt create mode 100644 AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/AuthViewModel.kt create mode 100644 AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/DashboardViewModel.kt diff --git a/AndroidApp/.idea/deploymentTargetSelector.xml b/AndroidApp/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/AndroidApp/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/AndroidApp/.kotlin/errors/errors-1770819867596.log b/AndroidApp/.kotlin/errors/errors-1770819867596.log new file mode 100644 index 0000000..5adbaae --- /dev/null +++ b/AndroidApp/.kotlin/errors/errors-1770819867596.log @@ -0,0 +1,193 @@ +kotlin version: 2.0.21 +error message: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during IR lowering +File being compiled: /Volumes/SED/Code/OpenTimeTracker/AndroidApp/app/src/main/java/com/opentimetracker/MainActivity.kt +The root cause java.lang.RuntimeException was thrown at: org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:47) + at org.jetbrains.kotlin.backend.common.CodegenUtil.reportBackendException(CodegenUtil.kt:253) + at org.jetbrains.kotlin.backend.common.CodegenUtil.reportBackendException$default(CodegenUtil.kt:236) + at org.jetbrains.kotlin.backend.common.phaser.PerformByIrFilePhase.invokeSequential(performByIrFile.kt:65) + at org.jetbrains.kotlin.backend.common.phaser.PerformByIrFilePhase.invoke(performByIrFile.kt:52) + at org.jetbrains.kotlin.backend.common.phaser.PerformByIrFilePhase.invoke(performByIrFile.kt:38) + at org.jetbrains.kotlin.backend.common.phaser.NamedCompilerPhase.phaseBody(CompilerPhase.kt:166) + at org.jetbrains.kotlin.backend.common.phaser.AbstractNamedCompilerPhase.invoke(CompilerPhase.kt:113) + at org.jetbrains.kotlin.backend.common.phaser.CompositePhase.invoke(PhaseBuilders.kt:27) + at org.jetbrains.kotlin.backend.common.phaser.CompositePhase.invoke(PhaseBuilders.kt:14) + at org.jetbrains.kotlin.backend.common.phaser.NamedCompilerPhase.phaseBody(CompilerPhase.kt:166) + at org.jetbrains.kotlin.backend.common.phaser.AbstractNamedCompilerPhase.invoke(CompilerPhase.kt:113) + at org.jetbrains.kotlin.backend.common.phaser.CompilerPhaseKt.invokeToplevel(CompilerPhase.kt:62) + at org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory.invokeCodegen(JvmIrCodegenFactory.kt:371) + at org.jetbrains.kotlin.codegen.CodegenFactory.generateModule(CodegenFactory.kt:47) + at org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory.generateModuleInFrontendIRMode(JvmIrCodegenFactory.kt:433) + at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.generateCodeFromIr(jvmCompilerPipeline.kt:246) + at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:142) + 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.RuntimeException: Exception while generating code for: +FUN name:LoginScreen visibility:public modality:FINAL <> (viewModel:com.opentimetracker.ui.AuthViewModel, onLoginSuccess:kotlin.Function0) returnType:kotlin.Unit + annotations: + Composable + VALUE_PARAMETER name:viewModel index:0 type:com.opentimetracker.ui.AuthViewModel + VALUE_PARAMETER name:onLoginSuccess index:1 type:kotlin.Function0 + BLOCK_BODY + WHEN type=kotlin.Unit origin=IF + BRANCH + if: CALL 'public abstract fun (): T of androidx.compose.runtime.MutableState declared in androidx.compose.runtime.MutableState' type=kotlin.Boolean origin=GET_PROPERTY + $this: CALL 'public final fun (): androidx.compose.runtime.MutableState declared in com.opentimetracker.ui.AuthViewModel' type=androidx.compose.runtime.MutableState origin=GET_PROPERTY + $this: GET_VAR 'viewModel: com.opentimetracker.ui.AuthViewModel declared in com.opentimetracker.MainActivityKt.LoginScreen' type=com.opentimetracker.ui.AuthViewModel origin=null + then: BLOCK type=kotlin.Unit origin=null + CALL 'public final fun LaunchedEffect (key1: kotlin.Any?, block: @[ExtensionFunctionType] kotlin.coroutines.SuspendFunction1): kotlin.Unit declared in androidx.compose.runtime.EffectsKt' type=kotlin.Unit origin=null + key1: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:INSTANCE type:kotlin.Unit visibility:public [final,static] declared in kotlin.Unit' type=kotlin.Unit origin=null + block: BLOCK type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + CLASS SUSPEND_LAMBDA CLASS name: modality:FINAL visibility:public/*package*/ superTypes:[kotlin.coroutines.jvm.internal.SuspendLambda; kotlin.jvm.functions.Function2?, kotlin.Any?>] + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + FIELD name:label type:kotlin.Int visibility:public/*package*/ + CONSTRUCTOR SUSPEND_LAMBDA visibility:public/*package*/ <> ($onLoginSuccess:kotlin.Function0, $completion:kotlin.coroutines.Continuation>?) returnType:com.opentimetracker.MainActivityKt.LoginScreen. [primary] + VALUE_PARAMETER BOUND_VALUE_PARAMETER name:$onLoginSuccess index:0 type:kotlin.Function0 + VALUE_PARAMETER name:$completion index:1 type:kotlin.coroutines.Continuation>? + BLOCK_BODY + SET_FIELD 'FIELD FIELD_FOR_CAPTURED_VALUE name:$onLoginSuccess type:kotlin.Function0 visibility:public/*package*/ [final] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Unit origin=INITIALIZER_OF_FIELD_FOR_CAPTURED_VALUE + receiver: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + value: GET_VAR '$onLoginSuccess: kotlin.Function0 declared in com.opentimetracker.MainActivityKt.LoginScreen..' type=kotlin.Function0 origin=null + DELEGATING_CONSTRUCTOR_CALL 'public constructor (arity: kotlin.Int, $completion: kotlin.coroutines.Continuation?) declared in kotlin.coroutines.jvm.internal.SuspendLambda' + arity: CONST Int type=kotlin.Int value=2 + $completion: GET_VAR '$completion: kotlin.coroutines.Continuation>? declared in com.opentimetracker.MainActivityKt.LoginScreen..' type=kotlin.coroutines.Continuation>? origin=null + BLOCK type=kotlin.Unit origin=null + FUN name:invokeSuspend visibility:public modality:FINAL <> ($this:com.opentimetracker.MainActivityKt.LoginScreen., $result:kotlin.Result) returnType:kotlin.Any? + overridden: + protected abstract fun invokeSuspend ($result: kotlin.Result): kotlin.Any? declared in kotlin.coroutines.jvm.internal.SuspendLambda + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + VALUE_PARAMETER name:$result index:0 type:kotlin.Result + BLOCK_BODY + CALL 'public abstract fun invoke (): R of kotlin.Function0 [operator] declared in kotlin.Function0' type=kotlin.Unit origin=INVOKE + $this: GET_FIELD 'FIELD FIELD_FOR_CAPTURED_VALUE name:$onLoginSuccess type:kotlin.Function0 visibility:public/*package*/ [final] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Function0 origin=null + receiver: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen..invokeSuspend' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + FUN name:create visibility:public modality:FINAL <> ($this:com.opentimetracker.MainActivityKt.LoginScreen., value:kotlin.Any?, $completion:kotlin.coroutines.Continuation) returnType:kotlin.coroutines.Continuation + overridden: + public open fun create (value: kotlin.Any?, $completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation declared in kotlin.coroutines.jvm.internal.SuspendLambda + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + VALUE_PARAMETER name:value index:0 type:kotlin.Any? + VALUE_PARAMETER name:$completion index:1 type:kotlin.coroutines.Continuation + BLOCK_BODY + RETURN type=kotlin.Nothing from='public final fun create (value: kotlin.Any?, $completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation declared in com.opentimetracker.MainActivityKt.LoginScreen.' + CONSTRUCTOR_CALL 'public/*package*/ constructor ($onLoginSuccess: kotlin.Function0, $completion: kotlin.coroutines.Continuation>?) [primary] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + $onLoginSuccess: GET_FIELD 'FIELD FIELD_FOR_CAPTURED_VALUE name:$onLoginSuccess type:kotlin.Function0 visibility:public/*package*/ [final] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Function0 origin=null + receiver: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen..create' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + $completion: GET_VAR '$completion: kotlin.coroutines.Continuation declared in com.opentimetracker.MainActivityKt.LoginScreen..create' type=kotlin.coroutines.Continuation origin=null + FUN name:invoke visibility:public modality:FINAL <> ($this:com.opentimetracker.MainActivityKt.LoginScreen., p1:kotlinx.coroutines.CoroutineScope, p2:kotlin.coroutines.Continuation?) returnType:kotlin.Any? + overridden: + public abstract fun invoke (p1: P1 of kotlin.jvm.functions.Function2, p2: P2 of kotlin.jvm.functions.Function2): R of kotlin.jvm.functions.Function2 declared in kotlin.jvm.functions.Function2 + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + VALUE_PARAMETER name:p1 index:0 type:kotlinx.coroutines.CoroutineScope + VALUE_PARAMETER name:p2 index:1 type:kotlin.coroutines.Continuation? + BLOCK_BODY + RETURN type=kotlin.Nothing from='public final fun invoke (p1: kotlinx.coroutines.CoroutineScope, p2: kotlin.coroutines.Continuation?): kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen.' + CALL 'public final fun invokeSuspend ($result: kotlin.Result): kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Any? origin=null + $this: TYPE_OP type=com.opentimetracker.MainActivityKt.LoginScreen. origin=IMPLICIT_CAST typeOperand=com.opentimetracker.MainActivityKt.LoginScreen. + CALL 'public final fun create (value: kotlin.Any?, $completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.coroutines.Continuation origin=null + $this: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + value: GET_VAR 'p1: kotlinx.coroutines.CoroutineScope declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=kotlinx.coroutines.CoroutineScope origin=null + $completion: GET_VAR 'p2: kotlin.coroutines.Continuation? declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=kotlin.coroutines.Continuation? origin=null + $result: CALL 'public final fun (v: T of kotlin.jvm.internal.): R of kotlin.jvm.internal. declared in kotlin.jvm.internal' type=kotlin.Result origin=null + : kotlin.Any? + : kotlin.Result + v: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:INSTANCE type:kotlin.Unit visibility:public [final,static] declared in kotlin.Unit' type=kotlin.Unit origin=null + FIELD FIELD_FOR_CAPTURED_VALUE name:$onLoginSuccess type:kotlin.Function0 visibility:public/*package*/ [final] + FUN BRIDGE name:invoke visibility:public modality:OPEN <> ($this:com.opentimetracker.MainActivityKt.LoginScreen., p1:kotlin.Any?, p2:kotlin.Any?) returnType:kotlin.Any? + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + VALUE_PARAMETER BRIDGE name:p1 index:0 type:kotlin.Any? + VALUE_PARAMETER BRIDGE name:p2 index:1 type:kotlin.Any? + EXPRESSION_BODY + RETURN type=kotlin.Nothing from='public open fun invoke (p1: kotlin.Any?, p2: kotlin.Any?): kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen.' + CALL 'public final fun invoke (p1: kotlinx.coroutines.CoroutineScope, p2: kotlin.coroutines.Continuation?): kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Any? origin=BRIDGE_DELEGATION + $this: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + p1: TYPE_OP type=kotlinx.coroutines.CoroutineScope origin=IMPLICIT_CAST typeOperand=kotlinx.coroutines.CoroutineScope + GET_VAR 'p1: kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=kotlin.Any? origin=null + p2: TYPE_OP type=kotlin.coroutines.Continuation<*> origin=IMPLICIT_CAST typeOperand=kotlin.coroutines.Continuation<*> + GET_VAR 'p2: kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=kotlin.Any? origin=null + CONSTRUCTOR_CALL 'public/*package*/ constructor ($onLoginSuccess: kotlin.Function0, $completion: kotlin.coroutines.Continuation>?) [primary] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + $onLoginSuccess: GET_VAR 'onLoginSuccess: kotlin.Function0 declared in com.opentimetracker.MainActivityKt.LoginScreen' type=kotlin.Function0 origin=null + $completion: CONST Null type=kotlin.Nothing? value=null + CALL 'public final fun Column (modifier: androidx.compose.ui.Modifier, verticalArrangement: androidx.compose.foundation.layout.Arrangement.Vertical, horizontalAlignment: androidx.compose.ui.Alignment.Horizontal, content: @[Composable] @[ExtensionFunctionType] kotlin.Function1): kotlin.Unit [inline] declared in androidx.compose.foundation.layout.ColumnKt' type=kotlin.Unit origin=null + modifier: CALL 'public final fun padding-3ABfNKs (all: androidx.compose.ui.unit.Dp): androidx.compose.ui.Modifier declared in androidx.compose.foundation.layout.PaddingKt' type=androidx.compose.ui.Modifier origin=null + $receiver: CALL 'public final fun fillMaxSize$default (fraction: kotlin.Float, $mask0: kotlin.Int, $handler: kotlin.Any?): androidx.compose.ui.Modifier declared in androidx.compose.foundation.layout.SizeKt' type=androidx.compose.ui.Modifier origin=DEFAULT_DISPATCH_CALL + $receiver: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:Companion type:androidx.compose.ui.Modifier.Companion visibility:public [final,static] declared in androidx.compose.ui.Modifier' type=androidx.compose.ui.Modifier.Companion origin=null + fraction: COMPOSITE type=kotlin.Float origin=DEFAULT_VALUE + CONST Float type=kotlin.Float value=0.0 + $mask0: CONST Int type=kotlin.Int value=1 + $handler: CONST Null type=kotlin.Any? value=null + all: CALL 'public final fun (): androidx.compose.ui.unit.Dp [inline] declared in androidx.compose.ui.unit.DpKt' type=androidx.compose.ui.unit.Dp origin=GET_PROPERTY + $receiver: CONST Int type=kotlin.Int value=16 + verticalArrangement: CALL 'public final fun (): androidx.compose.foundation.layout.Arrangement.HorizontalOrVertical declared in androidx.compose.foundation.layout.Arrangement' type=androidx.compose.foundation.layout.Arrangement.HorizontalOrVertical origin=GET_PROPERTY + $this: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:INSTANCE type:androidx.compose.foundation.layout.Arrangement visibility:public [final,static] declared in androidx.compose.foundation.layout.Arrangement' type=androidx.compose.foundation.layout.Arrangement origin=null + horizontalAlignment: CALL 'public final fun (): androidx.compose.ui.Alignment.Horizontal declared in androidx.compose.ui.Alignment.Companion' type=androidx.compose.ui.Alignment.Horizontal origin=GET_PROPERTY + $this: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:Companion type:androidx.compose.ui.Alignment.Companion visibility:public [final,static] declared in androidx.compose.ui.Alignment' type=androidx.compose.ui.Alignment.Companion origin=null + content: BLOCK type=@[ExtensionFunctionType] kotlin.Function1 origin=LAMBDA + COMPOSITE type=kotlin.Unit origin=null + FUNCTION_REFERENCE 'private final fun LoginScreen$lambda$14 ($viewModel: com.opentimetracker.ui.AuthViewModel): kotlin.Unit declared in com.opentimetracker.MainActivityKt' type=@[ExtensionFunctionType] kotlin.Function1 origin=INLINE_LAMBDA reflectionTarget=null + $viewModel: GET_VAR 'viewModel: com.opentimetracker.ui.AuthViewModel declared in com.opentimetracker.MainActivityKt.LoginScreen' type=com.opentimetracker.ui.AuthViewModel origin=null + + at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:47) + at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate$default(FunctionCodegen.kt:40) + at org.jetbrains.kotlin.backend.jvm.codegen.ClassCodegen.generateMethodNode(ClassCodegen.kt:406) + at org.jetbrains.kotlin.backend.jvm.codegen.ClassCodegen.generateMethod(ClassCodegen.kt:423) + at org.jetbrains.kotlin.backend.jvm.codegen.ClassCodegen.generate(ClassCodegen.kt:168) + at org.jetbrains.kotlin.backend.jvm.FileCodegen.lower(JvmPhases.kt:39) + at org.jetbrains.kotlin.backend.common.phaser.PhaseFactoriesKt.createFilePhase$lambda$4(PhaseFactories.kt:71) + at org.jetbrains.kotlin.backend.common.phaser.PhaseBuildersKt$createSimpleNamedCompilerPhase$1.phaseBody(PhaseBuilders.kt:69) + at org.jetbrains.kotlin.backend.common.phaser.SimpleNamedCompilerPhase.phaseBody(CompilerPhase.kt:226) + at org.jetbrains.kotlin.backend.common.phaser.AbstractNamedCompilerPhase.invoke(CompilerPhase.kt:113) + at org.jetbrains.kotlin.backend.common.phaser.PerformByIrFilePhase.invokeSequential(performByIrFile.kt:62) + ... 43 more +Caused by: org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: Couldn't inline method call: CALL 'public final fun Column (modifier: androidx.compose.ui.Modifier, verticalArrangement: androidx.compose.foundation.layout.Arrangement.Vertical, horizontalAlignment: androidx.compose.ui.Alignment.Horizontal, content: @[Composable] @[ExtensionFunctionType] kotlin.Function1): kotlin.Unit [inline] declared in androidx.compose.foundation.layout.ColumnKt' type=kotlin.Unit origin=null +Method: null +File is unknown +The root cause java.lang.IllegalStateException was thrown at: org.jetbrains.kotlin.codegen.inline.SourceCompilerForInlineKt.getMethodNode(SourceCompilerForInline.kt:118) + at org.jetbrains.kotlin.codegen.inline.InlineCodegen.performInline(InlineCodegen.kt:65) + at org.jetbrains.kotlin.backend.jvm.codegen.IrInlineCodegen.genInlineCall(IrInlineCodegen.kt:163) + at org.jetbrains.kotlin.backend.jvm.codegen.IrInlineCallGenerator.genCall(IrInlineCallGenerator.kt:36) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:653) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:138) + at org.jetbrains.kotlin.ir.expressions.IrCall.accept(IrCall.kt:24) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitStatementContainer(ExpressionCodegen.kt:579) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlockBody(ExpressionCodegen.kt:584) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlockBody(ExpressionCodegen.kt:138) + at org.jetbrains.kotlin.ir.expressions.IrBlockBody.accept(IrBlockBody.kt:20) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.generate(ExpressionCodegen.kt:240) + at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.doGenerate(FunctionCodegen.kt:123) + at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:44) + ... 53 more +Caused by: java.lang.IllegalStateException: couldn't find inline method Landroidx/compose/foundation/layout/ColumnKt;.Column(Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/ui/Alignment$Horizontal;Lkotlin/jvm/functions/Function1;)V + at org.jetbrains.kotlin.codegen.inline.SourceCompilerForInlineKt.getMethodNode(SourceCompilerForInline.kt:118) + at org.jetbrains.kotlin.codegen.inline.SourceCompilerForInlineKt.loadCompiledInlineFunction(SourceCompilerForInline.kt:96) + at org.jetbrains.kotlin.backend.jvm.codegen.IrSourceCompilerForInline.compileInlineFunction(IrSourceCompilerForInline.kt:91) + at org.jetbrains.kotlin.codegen.inline.InlineCodegen.compileInline(InlineCodegen.kt:43) + at org.jetbrains.kotlin.codegen.inline.InlineCodegen.performInline(InlineCodegen.kt:51) + ... 65 more + + diff --git a/AndroidApp/.kotlin/errors/errors-1770819878847.log b/AndroidApp/.kotlin/errors/errors-1770819878847.log new file mode 100644 index 0000000..5adbaae --- /dev/null +++ b/AndroidApp/.kotlin/errors/errors-1770819878847.log @@ -0,0 +1,193 @@ +kotlin version: 2.0.21 +error message: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during IR lowering +File being compiled: /Volumes/SED/Code/OpenTimeTracker/AndroidApp/app/src/main/java/com/opentimetracker/MainActivity.kt +The root cause java.lang.RuntimeException was thrown at: org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:47) + at org.jetbrains.kotlin.backend.common.CodegenUtil.reportBackendException(CodegenUtil.kt:253) + at org.jetbrains.kotlin.backend.common.CodegenUtil.reportBackendException$default(CodegenUtil.kt:236) + at org.jetbrains.kotlin.backend.common.phaser.PerformByIrFilePhase.invokeSequential(performByIrFile.kt:65) + at org.jetbrains.kotlin.backend.common.phaser.PerformByIrFilePhase.invoke(performByIrFile.kt:52) + at org.jetbrains.kotlin.backend.common.phaser.PerformByIrFilePhase.invoke(performByIrFile.kt:38) + at org.jetbrains.kotlin.backend.common.phaser.NamedCompilerPhase.phaseBody(CompilerPhase.kt:166) + at org.jetbrains.kotlin.backend.common.phaser.AbstractNamedCompilerPhase.invoke(CompilerPhase.kt:113) + at org.jetbrains.kotlin.backend.common.phaser.CompositePhase.invoke(PhaseBuilders.kt:27) + at org.jetbrains.kotlin.backend.common.phaser.CompositePhase.invoke(PhaseBuilders.kt:14) + at org.jetbrains.kotlin.backend.common.phaser.NamedCompilerPhase.phaseBody(CompilerPhase.kt:166) + at org.jetbrains.kotlin.backend.common.phaser.AbstractNamedCompilerPhase.invoke(CompilerPhase.kt:113) + at org.jetbrains.kotlin.backend.common.phaser.CompilerPhaseKt.invokeToplevel(CompilerPhase.kt:62) + at org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory.invokeCodegen(JvmIrCodegenFactory.kt:371) + at org.jetbrains.kotlin.codegen.CodegenFactory.generateModule(CodegenFactory.kt:47) + at org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory.generateModuleInFrontendIRMode(JvmIrCodegenFactory.kt:433) + at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.generateCodeFromIr(jvmCompilerPipeline.kt:246) + at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:142) + 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.RuntimeException: Exception while generating code for: +FUN name:LoginScreen visibility:public modality:FINAL <> (viewModel:com.opentimetracker.ui.AuthViewModel, onLoginSuccess:kotlin.Function0) returnType:kotlin.Unit + annotations: + Composable + VALUE_PARAMETER name:viewModel index:0 type:com.opentimetracker.ui.AuthViewModel + VALUE_PARAMETER name:onLoginSuccess index:1 type:kotlin.Function0 + BLOCK_BODY + WHEN type=kotlin.Unit origin=IF + BRANCH + if: CALL 'public abstract fun (): T of androidx.compose.runtime.MutableState declared in androidx.compose.runtime.MutableState' type=kotlin.Boolean origin=GET_PROPERTY + $this: CALL 'public final fun (): androidx.compose.runtime.MutableState declared in com.opentimetracker.ui.AuthViewModel' type=androidx.compose.runtime.MutableState origin=GET_PROPERTY + $this: GET_VAR 'viewModel: com.opentimetracker.ui.AuthViewModel declared in com.opentimetracker.MainActivityKt.LoginScreen' type=com.opentimetracker.ui.AuthViewModel origin=null + then: BLOCK type=kotlin.Unit origin=null + CALL 'public final fun LaunchedEffect (key1: kotlin.Any?, block: @[ExtensionFunctionType] kotlin.coroutines.SuspendFunction1): kotlin.Unit declared in androidx.compose.runtime.EffectsKt' type=kotlin.Unit origin=null + key1: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:INSTANCE type:kotlin.Unit visibility:public [final,static] declared in kotlin.Unit' type=kotlin.Unit origin=null + block: BLOCK type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + CLASS SUSPEND_LAMBDA CLASS name: modality:FINAL visibility:public/*package*/ superTypes:[kotlin.coroutines.jvm.internal.SuspendLambda; kotlin.jvm.functions.Function2?, kotlin.Any?>] + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + FIELD name:label type:kotlin.Int visibility:public/*package*/ + CONSTRUCTOR SUSPEND_LAMBDA visibility:public/*package*/ <> ($onLoginSuccess:kotlin.Function0, $completion:kotlin.coroutines.Continuation>?) returnType:com.opentimetracker.MainActivityKt.LoginScreen. [primary] + VALUE_PARAMETER BOUND_VALUE_PARAMETER name:$onLoginSuccess index:0 type:kotlin.Function0 + VALUE_PARAMETER name:$completion index:1 type:kotlin.coroutines.Continuation>? + BLOCK_BODY + SET_FIELD 'FIELD FIELD_FOR_CAPTURED_VALUE name:$onLoginSuccess type:kotlin.Function0 visibility:public/*package*/ [final] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Unit origin=INITIALIZER_OF_FIELD_FOR_CAPTURED_VALUE + receiver: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + value: GET_VAR '$onLoginSuccess: kotlin.Function0 declared in com.opentimetracker.MainActivityKt.LoginScreen..' type=kotlin.Function0 origin=null + DELEGATING_CONSTRUCTOR_CALL 'public constructor (arity: kotlin.Int, $completion: kotlin.coroutines.Continuation?) declared in kotlin.coroutines.jvm.internal.SuspendLambda' + arity: CONST Int type=kotlin.Int value=2 + $completion: GET_VAR '$completion: kotlin.coroutines.Continuation>? declared in com.opentimetracker.MainActivityKt.LoginScreen..' type=kotlin.coroutines.Continuation>? origin=null + BLOCK type=kotlin.Unit origin=null + FUN name:invokeSuspend visibility:public modality:FINAL <> ($this:com.opentimetracker.MainActivityKt.LoginScreen., $result:kotlin.Result) returnType:kotlin.Any? + overridden: + protected abstract fun invokeSuspend ($result: kotlin.Result): kotlin.Any? declared in kotlin.coroutines.jvm.internal.SuspendLambda + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + VALUE_PARAMETER name:$result index:0 type:kotlin.Result + BLOCK_BODY + CALL 'public abstract fun invoke (): R of kotlin.Function0 [operator] declared in kotlin.Function0' type=kotlin.Unit origin=INVOKE + $this: GET_FIELD 'FIELD FIELD_FOR_CAPTURED_VALUE name:$onLoginSuccess type:kotlin.Function0 visibility:public/*package*/ [final] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Function0 origin=null + receiver: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen..invokeSuspend' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + FUN name:create visibility:public modality:FINAL <> ($this:com.opentimetracker.MainActivityKt.LoginScreen., value:kotlin.Any?, $completion:kotlin.coroutines.Continuation) returnType:kotlin.coroutines.Continuation + overridden: + public open fun create (value: kotlin.Any?, $completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation declared in kotlin.coroutines.jvm.internal.SuspendLambda + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + VALUE_PARAMETER name:value index:0 type:kotlin.Any? + VALUE_PARAMETER name:$completion index:1 type:kotlin.coroutines.Continuation + BLOCK_BODY + RETURN type=kotlin.Nothing from='public final fun create (value: kotlin.Any?, $completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation declared in com.opentimetracker.MainActivityKt.LoginScreen.' + CONSTRUCTOR_CALL 'public/*package*/ constructor ($onLoginSuccess: kotlin.Function0, $completion: kotlin.coroutines.Continuation>?) [primary] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + $onLoginSuccess: GET_FIELD 'FIELD FIELD_FOR_CAPTURED_VALUE name:$onLoginSuccess type:kotlin.Function0 visibility:public/*package*/ [final] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Function0 origin=null + receiver: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen..create' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + $completion: GET_VAR '$completion: kotlin.coroutines.Continuation declared in com.opentimetracker.MainActivityKt.LoginScreen..create' type=kotlin.coroutines.Continuation origin=null + FUN name:invoke visibility:public modality:FINAL <> ($this:com.opentimetracker.MainActivityKt.LoginScreen., p1:kotlinx.coroutines.CoroutineScope, p2:kotlin.coroutines.Continuation?) returnType:kotlin.Any? + overridden: + public abstract fun invoke (p1: P1 of kotlin.jvm.functions.Function2, p2: P2 of kotlin.jvm.functions.Function2): R of kotlin.jvm.functions.Function2 declared in kotlin.jvm.functions.Function2 + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + VALUE_PARAMETER name:p1 index:0 type:kotlinx.coroutines.CoroutineScope + VALUE_PARAMETER name:p2 index:1 type:kotlin.coroutines.Continuation? + BLOCK_BODY + RETURN type=kotlin.Nothing from='public final fun invoke (p1: kotlinx.coroutines.CoroutineScope, p2: kotlin.coroutines.Continuation?): kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen.' + CALL 'public final fun invokeSuspend ($result: kotlin.Result): kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Any? origin=null + $this: TYPE_OP type=com.opentimetracker.MainActivityKt.LoginScreen. origin=IMPLICIT_CAST typeOperand=com.opentimetracker.MainActivityKt.LoginScreen. + CALL 'public final fun create (value: kotlin.Any?, $completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.coroutines.Continuation origin=null + $this: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + value: GET_VAR 'p1: kotlinx.coroutines.CoroutineScope declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=kotlinx.coroutines.CoroutineScope origin=null + $completion: GET_VAR 'p2: kotlin.coroutines.Continuation? declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=kotlin.coroutines.Continuation? origin=null + $result: CALL 'public final fun (v: T of kotlin.jvm.internal.): R of kotlin.jvm.internal. declared in kotlin.jvm.internal' type=kotlin.Result origin=null + : kotlin.Any? + : kotlin.Result + v: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:INSTANCE type:kotlin.Unit visibility:public [final,static] declared in kotlin.Unit' type=kotlin.Unit origin=null + FIELD FIELD_FOR_CAPTURED_VALUE name:$onLoginSuccess type:kotlin.Function0 visibility:public/*package*/ [final] + FUN BRIDGE name:invoke visibility:public modality:OPEN <> ($this:com.opentimetracker.MainActivityKt.LoginScreen., p1:kotlin.Any?, p2:kotlin.Any?) returnType:kotlin.Any? + $this: VALUE_PARAMETER INSTANCE_RECEIVER name: type:com.opentimetracker.MainActivityKt.LoginScreen. + VALUE_PARAMETER BRIDGE name:p1 index:0 type:kotlin.Any? + VALUE_PARAMETER BRIDGE name:p2 index:1 type:kotlin.Any? + EXPRESSION_BODY + RETURN type=kotlin.Nothing from='public open fun invoke (p1: kotlin.Any?, p2: kotlin.Any?): kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen.' + CALL 'public final fun invoke (p1: kotlinx.coroutines.CoroutineScope, p2: kotlin.coroutines.Continuation?): kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=kotlin.Any? origin=BRIDGE_DELEGATION + $this: GET_VAR ': com.opentimetracker.MainActivityKt.LoginScreen. declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + p1: TYPE_OP type=kotlinx.coroutines.CoroutineScope origin=IMPLICIT_CAST typeOperand=kotlinx.coroutines.CoroutineScope + GET_VAR 'p1: kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=kotlin.Any? origin=null + p2: TYPE_OP type=kotlin.coroutines.Continuation<*> origin=IMPLICIT_CAST typeOperand=kotlin.coroutines.Continuation<*> + GET_VAR 'p2: kotlin.Any? declared in com.opentimetracker.MainActivityKt.LoginScreen..invoke' type=kotlin.Any? origin=null + CONSTRUCTOR_CALL 'public/*package*/ constructor ($onLoginSuccess: kotlin.Function0, $completion: kotlin.coroutines.Continuation>?) [primary] declared in com.opentimetracker.MainActivityKt.LoginScreen.' type=com.opentimetracker.MainActivityKt.LoginScreen. origin=null + $onLoginSuccess: GET_VAR 'onLoginSuccess: kotlin.Function0 declared in com.opentimetracker.MainActivityKt.LoginScreen' type=kotlin.Function0 origin=null + $completion: CONST Null type=kotlin.Nothing? value=null + CALL 'public final fun Column (modifier: androidx.compose.ui.Modifier, verticalArrangement: androidx.compose.foundation.layout.Arrangement.Vertical, horizontalAlignment: androidx.compose.ui.Alignment.Horizontal, content: @[Composable] @[ExtensionFunctionType] kotlin.Function1): kotlin.Unit [inline] declared in androidx.compose.foundation.layout.ColumnKt' type=kotlin.Unit origin=null + modifier: CALL 'public final fun padding-3ABfNKs (all: androidx.compose.ui.unit.Dp): androidx.compose.ui.Modifier declared in androidx.compose.foundation.layout.PaddingKt' type=androidx.compose.ui.Modifier origin=null + $receiver: CALL 'public final fun fillMaxSize$default (fraction: kotlin.Float, $mask0: kotlin.Int, $handler: kotlin.Any?): androidx.compose.ui.Modifier declared in androidx.compose.foundation.layout.SizeKt' type=androidx.compose.ui.Modifier origin=DEFAULT_DISPATCH_CALL + $receiver: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:Companion type:androidx.compose.ui.Modifier.Companion visibility:public [final,static] declared in androidx.compose.ui.Modifier' type=androidx.compose.ui.Modifier.Companion origin=null + fraction: COMPOSITE type=kotlin.Float origin=DEFAULT_VALUE + CONST Float type=kotlin.Float value=0.0 + $mask0: CONST Int type=kotlin.Int value=1 + $handler: CONST Null type=kotlin.Any? value=null + all: CALL 'public final fun (): androidx.compose.ui.unit.Dp [inline] declared in androidx.compose.ui.unit.DpKt' type=androidx.compose.ui.unit.Dp origin=GET_PROPERTY + $receiver: CONST Int type=kotlin.Int value=16 + verticalArrangement: CALL 'public final fun (): androidx.compose.foundation.layout.Arrangement.HorizontalOrVertical declared in androidx.compose.foundation.layout.Arrangement' type=androidx.compose.foundation.layout.Arrangement.HorizontalOrVertical origin=GET_PROPERTY + $this: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:INSTANCE type:androidx.compose.foundation.layout.Arrangement visibility:public [final,static] declared in androidx.compose.foundation.layout.Arrangement' type=androidx.compose.foundation.layout.Arrangement origin=null + horizontalAlignment: CALL 'public final fun (): androidx.compose.ui.Alignment.Horizontal declared in androidx.compose.ui.Alignment.Companion' type=androidx.compose.ui.Alignment.Horizontal origin=GET_PROPERTY + $this: GET_FIELD 'FIELD FIELD_FOR_OBJECT_INSTANCE name:Companion type:androidx.compose.ui.Alignment.Companion visibility:public [final,static] declared in androidx.compose.ui.Alignment' type=androidx.compose.ui.Alignment.Companion origin=null + content: BLOCK type=@[ExtensionFunctionType] kotlin.Function1 origin=LAMBDA + COMPOSITE type=kotlin.Unit origin=null + FUNCTION_REFERENCE 'private final fun LoginScreen$lambda$14 ($viewModel: com.opentimetracker.ui.AuthViewModel): kotlin.Unit declared in com.opentimetracker.MainActivityKt' type=@[ExtensionFunctionType] kotlin.Function1 origin=INLINE_LAMBDA reflectionTarget=null + $viewModel: GET_VAR 'viewModel: com.opentimetracker.ui.AuthViewModel declared in com.opentimetracker.MainActivityKt.LoginScreen' type=com.opentimetracker.ui.AuthViewModel origin=null + + at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:47) + at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate$default(FunctionCodegen.kt:40) + at org.jetbrains.kotlin.backend.jvm.codegen.ClassCodegen.generateMethodNode(ClassCodegen.kt:406) + at org.jetbrains.kotlin.backend.jvm.codegen.ClassCodegen.generateMethod(ClassCodegen.kt:423) + at org.jetbrains.kotlin.backend.jvm.codegen.ClassCodegen.generate(ClassCodegen.kt:168) + at org.jetbrains.kotlin.backend.jvm.FileCodegen.lower(JvmPhases.kt:39) + at org.jetbrains.kotlin.backend.common.phaser.PhaseFactoriesKt.createFilePhase$lambda$4(PhaseFactories.kt:71) + at org.jetbrains.kotlin.backend.common.phaser.PhaseBuildersKt$createSimpleNamedCompilerPhase$1.phaseBody(PhaseBuilders.kt:69) + at org.jetbrains.kotlin.backend.common.phaser.SimpleNamedCompilerPhase.phaseBody(CompilerPhase.kt:226) + at org.jetbrains.kotlin.backend.common.phaser.AbstractNamedCompilerPhase.invoke(CompilerPhase.kt:113) + at org.jetbrains.kotlin.backend.common.phaser.PerformByIrFilePhase.invokeSequential(performByIrFile.kt:62) + ... 43 more +Caused by: org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: Couldn't inline method call: CALL 'public final fun Column (modifier: androidx.compose.ui.Modifier, verticalArrangement: androidx.compose.foundation.layout.Arrangement.Vertical, horizontalAlignment: androidx.compose.ui.Alignment.Horizontal, content: @[Composable] @[ExtensionFunctionType] kotlin.Function1): kotlin.Unit [inline] declared in androidx.compose.foundation.layout.ColumnKt' type=kotlin.Unit origin=null +Method: null +File is unknown +The root cause java.lang.IllegalStateException was thrown at: org.jetbrains.kotlin.codegen.inline.SourceCompilerForInlineKt.getMethodNode(SourceCompilerForInline.kt:118) + at org.jetbrains.kotlin.codegen.inline.InlineCodegen.performInline(InlineCodegen.kt:65) + at org.jetbrains.kotlin.backend.jvm.codegen.IrInlineCodegen.genInlineCall(IrInlineCodegen.kt:163) + at org.jetbrains.kotlin.backend.jvm.codegen.IrInlineCallGenerator.genCall(IrInlineCallGenerator.kt:36) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:653) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:138) + at org.jetbrains.kotlin.ir.expressions.IrCall.accept(IrCall.kt:24) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitStatementContainer(ExpressionCodegen.kt:579) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlockBody(ExpressionCodegen.kt:584) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlockBody(ExpressionCodegen.kt:138) + at org.jetbrains.kotlin.ir.expressions.IrBlockBody.accept(IrBlockBody.kt:20) + at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.generate(ExpressionCodegen.kt:240) + at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.doGenerate(FunctionCodegen.kt:123) + at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:44) + ... 53 more +Caused by: java.lang.IllegalStateException: couldn't find inline method Landroidx/compose/foundation/layout/ColumnKt;.Column(Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/ui/Alignment$Horizontal;Lkotlin/jvm/functions/Function1;)V + at org.jetbrains.kotlin.codegen.inline.SourceCompilerForInlineKt.getMethodNode(SourceCompilerForInline.kt:118) + at org.jetbrains.kotlin.codegen.inline.SourceCompilerForInlineKt.loadCompiledInlineFunction(SourceCompilerForInline.kt:96) + at org.jetbrains.kotlin.backend.jvm.codegen.IrSourceCompilerForInline.compileInlineFunction(IrSourceCompilerForInline.kt:91) + at org.jetbrains.kotlin.codegen.inline.InlineCodegen.compileInline(InlineCodegen.kt:43) + at org.jetbrains.kotlin.codegen.inline.InlineCodegen.performInline(InlineCodegen.kt:51) + ... 65 more + + diff --git a/AndroidApp/app/build.gradle.kts b/AndroidApp/app/build.gradle.kts index afc7570..02c29bd 100644 --- a/AndroidApp/app/build.gradle.kts +++ b/AndroidApp/app/build.gradle.kts @@ -1,6 +1,12 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + // Kotlin 2.0+ requires the dedicated Compose Compiler plugin + id("org.jetbrains.kotlin.plugin.compose") + // Version constraint removed to allow sync with main Kotlin version (2.0.0+) + id("org.jetbrains.kotlin.plugin.serialization") + + } android { @@ -33,6 +39,10 @@ android { kotlinOptions { jvmTarget = "11" } + + buildFeatures { + compose = true + } } dependencies { @@ -43,4 +53,22 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + + // Networking + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + + // Storage + implementation("androidx.datastore:datastore-preferences:1.0.0") + + // Navigation + implementation("androidx.navigation:navigation-compose:2.7.7") + + // Compose + implementation(platform("androidx.compose:compose-bom:2024.02.01")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") } \ No newline at end of file diff --git a/AndroidApp/app/src/main/AndroidManifest.xml b/AndroidApp/app/src/main/AndroidManifest.xml index 57c3869..7cfc2b7 100644 --- a/AndroidApp/app/src/main/AndroidManifest.xml +++ b/AndroidApp/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + tools:targetApi="31"> + + + + + + + + + \ No newline at end of file diff --git a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/MainActivity.kt b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/MainActivity.kt new file mode 100644 index 0000000..10732e2 --- /dev/null +++ b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/MainActivity.kt @@ -0,0 +1,236 @@ +package calvin.erfmann.opentimetracker + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import calvin.erfmann.opentimetracker.data.Activity +import calvin.erfmann.opentimetracker.data.UserPreferences +import calvin.erfmann.opentimetracker.di.NetworkModule +import calvin.erfmann.opentimetracker.ui.AuthViewModel +import calvin.erfmann.opentimetracker.ui.DashboardViewModel + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val prefs = UserPreferences(this) + + setContent { + MaterialTheme { + AppNavigation(prefs) + } + } + } +} + +@Composable +fun AppNavigation(prefs: UserPreferences) { + val navController = rememberNavController() + + NavHost(navController = navController, startDestination = "login") { + composable("login") { + LoginScreen( + viewModel = viewModel(factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return AuthViewModel(prefs) as T + } + }), + onLoginSuccess = { navController.navigate("dashboard") { + popUpTo("login") { inclusive = true } + }} + ) + } + composable("dashboard") { + // Using remember to avoid recreating the API definition + val api = remember { NetworkModule.provideApiService(prefs) } + + // CORRECT: Using viewModel factory to keep the instance alive across recompositions + // This prevents the infinite loop of API requests + val viewModel: DashboardViewModel = viewModel( + factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return DashboardViewModel(api) as T + } + } + ) + DashboardScreen(viewModel = viewModel) + } + } +} + +@Composable +fun LoginScreen(viewModel: AuthViewModel, onLoginSuccess: () -> Unit) { + + if (viewModel.isLoggedIn.value) { + LaunchedEffect(Unit) { onLoginSuccess() } + } + + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Open Time Tracker", style = MaterialTheme.typography.headlineMedium) + Spacer(Modifier.height(32.dp)) + + OutlinedTextField( + value = viewModel.baseUrlInput.value, + onValueChange = { viewModel.baseUrlInput.value = it }, + label = { Text("Server URL (Self-hosted)") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = viewModel.username.value, + onValueChange = { viewModel.username.value = it }, + label = { Text("Username") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = viewModel.password.value, + onValueChange = { viewModel.password.value = it }, + label = { Text("Password") }, + modifier = Modifier.fillMaxWidth(), + visualTransformation = PasswordVisualTransformation() + ) + Spacer(Modifier.height(16.dp)) + + if (viewModel.error.value != null) { + Text(viewModel.error.value!!, color = Color.Red) + Spacer(Modifier.height(8.dp)) + } + + Button( + onClick = { viewModel.login() }, + enabled = !viewModel.isLoading.value, + modifier = Modifier.fillMaxWidth() + ) { + Text(if (viewModel.isLoading.value) "Logging in..." else "Login") + } + } +} + +@Composable +fun DashboardScreen(viewModel: DashboardViewModel) { + val state = viewModel.state.value + + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + // --- Timer Section --- + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + Column( + modifier = Modifier.padding(24.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (state?.isTracking == true) { + val activityName = state.activities.find { it.id == state.entry?.activityId }?.name ?: "Unknown" + Text(activityName, style = MaterialTheme.typography.titleMedium) + if (!state.entry?.subcategory.isNullOrEmpty()) { + SuggestionChip(onClick = {}, label = { Text(state.entry?.subcategory!!) }) + } + Text(viewModel.elapsedTime.value, style = MaterialTheme.typography.displayLarge) + Spacer(Modifier.height(16.dp)) + Button( + onClick = { viewModel.stopActivity() }, + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) + ) { + Text("STOP") + } + } else { + Text("Ready to work?", style = MaterialTheme.typography.headlineSmall) + Text("Select an activity below", style = MaterialTheme.typography.bodyMedium) + } + } + } + + Spacer(Modifier.height(24.dp)) + + // --- Content Switching --- + // Always show Active Tasks if present + if (!state?.activeTasks.isNullOrEmpty()) { + Text("Active Tasks", style = MaterialTheme.typography.titleMedium) + LazyColumn(modifier = Modifier.heightIn(max = 200.dp)) { // Limit height to not push activities off screen + items(state!!.activeTasks) { task -> + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = task.status == "completed", + onCheckedChange = { /* Call toggle api */ }) + Text(task.name) + } + } + } + Spacer(Modifier.height(16.dp)) + } + + // Always show Activities Grid + Text("Activities", style = MaterialTheme.typography.titleMedium) + Spacer(Modifier.height(8.dp)) + + // Show loading or empty state if null + if (state == null) { + Box(modifier = Modifier.fillMaxWidth().height(100.dp), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.weight(1f) // Fill remaining space + ) { + items(state.activities) { activity -> + ActivityCard(activity) { viewModel.startActivity(activity.id) } + } + } + } + } +} + +@Composable +fun ActivityCard(activity: Activity, onClick: () -> Unit) { + // Parse hex color lightly + val color = try { + Color(android.graphics.Color.parseColor(activity.color)) + } catch (e: Exception) { Color.Gray } + + Card( + modifier = Modifier + .height(100.dp) + .clickable { onClick() }, + colors = CardDefaults.cardColors(containerColor = color) + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Text( + activity.name, + color = Color.White, + style = MaterialTheme.typography.titleMedium + ) + } + } +} diff --git a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/ApiService.kt b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/ApiService.kt new file mode 100644 index 0000000..cd77748 --- /dev/null +++ b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/ApiService.kt @@ -0,0 +1,23 @@ +package calvin.erfmann.opentimetracker.data + +import retrofit2.http.* + +interface ApiService { + // Auth + @POST("api/login") + suspend fun login(@Body request: LoginRequest): AuthResponse + + // Dashboard + @GET("api/status") + suspend fun getStatus(): StatusResponse + + @POST("api/timer/start/{activity_id}") + suspend fun startActivity(@Path("activity_id") activityId: String): StartTimerResponse + + @POST("api/timer/stop") + suspend fun stopActivity() + + @POST("api/tasks/toggle") + suspend fun toggleTask(@Body body: Map): Any // Simplified +} + diff --git a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/Models.kt b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/Models.kt new file mode 100644 index 0000000..b2da0e7 --- /dev/null +++ b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/Models.kt @@ -0,0 +1,64 @@ +package calvin.erfmann.opentimetracker.data + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + + + +@Serializable +data class AuthResponse( + val token: String +) + +@Serializable +data class LoginRequest( + val username: String, + val password: String +) + +@Serializable +data class StatusResponse( + @SerialName("is_tracking") val isTracking: Boolean = false, + val entry: TimeEntry? = null, + @SerialName("active_tasks") val activeTasks: List = emptyList(), + val activities: List = emptyList() +) + +@Serializable +data class TimeEntry( + val id: String, + @SerialName("start_time") val startTime: String, + val note: String? = null, + val subcategory: String? = null, + @SerialName("activity_id") val activityId: String? = null, + @SerialName("activity_name") val activityName: String? = null, + @SerialName("activity_color") val activityColor: String? = null +) + +@Serializable +data class Activity( + val id: String, + val name: String, + val color: String, + val subcategories: List = emptyList() +) + +@Serializable +data class Task( + val id: String, + val name: String, + val status: String = "open", // Werte vom Server: "open" oder "completed" + val subcategory: String? = null, + @SerialName("activity_id") val activityId: String? = null, + @SerialName("completed_at") val completedAt: String? = null, + @SerialName("due_date") val dueDate: String? = null +) + +@Serializable +data class StartTimerResponse( + val id: String, + @SerialName("start_time") val startTime: String +) diff --git a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/UserPreferences.kt b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/UserPreferences.kt new file mode 100644 index 0000000..6860498 --- /dev/null +++ b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/UserPreferences.kt @@ -0,0 +1,35 @@ +package calvin.erfmann.opentimetracker.data + +import android.content.Context +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val Context.dataStore by preferencesDataStore(name = "settings") + +class UserPreferences(private val context: Context) { + companion object { + val KEY_BASE_URL = stringPreferencesKey("base_url") + val KEY_JWT = stringPreferencesKey("jwt") + const val DEFAULT_URL = "https://opentimetracker.app/" + } + + val baseUrl: Flow = context.dataStore.data.map { it[KEY_BASE_URL] ?: DEFAULT_URL } + val authToken: Flow = context.dataStore.data.map { it[KEY_JWT] } + + suspend fun saveBaseUrl(url: String) { + // Ensure trailing slash + val formattedUrl = if (url.endsWith("/")) url else "$url/" + context.dataStore.edit { it[KEY_BASE_URL] = formattedUrl } + } + + suspend fun saveToken(token: String) { + context.dataStore.edit { it[KEY_JWT] = token } + } + + suspend fun clearToken() { + context.dataStore.edit { it.remove(KEY_JWT) } + } +} diff --git a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/di/NetworkModule.kt b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/di/NetworkModule.kt new file mode 100644 index 0000000..425ce3c --- /dev/null +++ b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/di/NetworkModule.kt @@ -0,0 +1,53 @@ +package calvin.erfmann.opentimetracker.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import calvin.erfmann.opentimetracker.data.ApiService +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Interceptor +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import calvin.erfmann.opentimetracker.data.UserPreferences +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi + +object NetworkModule { + + private val json = Json { + ignoreUnknownKeys = true + isLenient = true + coerceInputValues = true + } + + @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) + fun provideApiService(preferences: UserPreferences): ApiService { + val authInterceptor = Interceptor { chain -> + val token = runBlocking { preferences.authToken.first() } + val requestBuilder = chain.request().newBuilder() + if (token != null) { + requestBuilder.addHeader("Authorization", "Bearer $token") + } + chain.proceed(requestBuilder.build()) + } + + val client = OkHttpClient.Builder() + .addInterceptor(authInterceptor) + .connectTimeout(10, TimeUnit.SECONDS) + .build() + + // Note: For dynamic Base URL changes in a real App, you'd reimplement this + // to rebuild Retrofit when prefs change, or use an OkHttp Interceptor to switch Host. + // For simplicity, we assume URL is set before Login and app restarts or re-initis logic. + val currentUrl = runBlocking { preferences.baseUrl.first() } + + return Retrofit.Builder() + .baseUrl(currentUrl) + .client(client) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + .create(ApiService::class.java) + } +} diff --git a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/AuthViewModel.kt b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/AuthViewModel.kt new file mode 100644 index 0000000..c054fff --- /dev/null +++ b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/AuthViewModel.kt @@ -0,0 +1,47 @@ +package calvin.erfmann.opentimetracker.ui + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import calvin.erfmann.opentimetracker.data.LoginRequest +import calvin.erfmann.opentimetracker.data.UserPreferences +import calvin.erfmann.opentimetracker.di.NetworkModule +import kotlinx.coroutines.launch + +class AuthViewModel(private val prefs: UserPreferences) : ViewModel() { + var baseUrlInput = mutableStateOf(UserPreferences.DEFAULT_URL) + var username = mutableStateOf("") + var password = mutableStateOf("") + var isLoading = mutableStateOf(false) + var error = mutableStateOf(null) + var isLoggedIn = mutableStateOf(false) + + init { + viewModelScope.launch { + prefs.baseUrl.collect { baseUrlInput.value = it } + } + } + + fun login() { + viewModelScope.launch { + try { + isLoading.value = true + error.value = null + // 1. Save URL first so NetworkModule picks it up + prefs.saveBaseUrl(baseUrlInput.value) + + // 2. Create temp service + val api = NetworkModule.provideApiService(prefs) + + // 3. Call Login + val response = api.login(LoginRequest(username.value, password.value)) + prefs.saveToken(response.token) + isLoggedIn.value = true + } catch (e: Exception) { + error.value = "Login failed: ${e.message}" + } finally { + isLoading.value = false + } + } + } +} diff --git a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/DashboardViewModel.kt b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/DashboardViewModel.kt new file mode 100644 index 0000000..d2aaefd --- /dev/null +++ b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/DashboardViewModel.kt @@ -0,0 +1,87 @@ +package calvin.erfmann.opentimetracker.ui + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import calvin.erfmann.opentimetracker.data.ApiService +import calvin.erfmann.opentimetracker.data.StatusResponse +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.time.Duration +import java.time.Instant + +class DashboardViewModel(private val api: ApiService) : ViewModel() { + + var state = mutableStateOf(null) + var elapsedTime = mutableStateOf("00:00:00") + + init { + startPolling() + startLocalTimer() + } + + private fun startPolling() { + viewModelScope.launch { + while (isActive) { + try { + val status = api.getStatus() + state.value = status + } catch (e: Exception) { + // Handle error (e.g., 401 -> Logout) + e.printStackTrace() + } + delay(3000) // Poll every 3 seconds + } + } + } + + private fun startLocalTimer() { + viewModelScope.launch { + while (isActive) { + state.value?.entry?.startTime?.let { isoTime -> + try { + // Assuming ISO format like "2023-10-27T10:00:00Z" + val start = Instant.parse(isoTime) + val now = Instant.now() + val seconds = Duration.between(start, now).seconds + if (seconds >= 0) { + val h = seconds / 3600 + val m = (seconds % 3600) / 60 + val s = seconds % 60 + elapsedTime.value = String.format("%02d:%02d:%02d", h, m, s) + } + } catch (e: Exception) { + elapsedTime.value = "--:--" + } + } + delay(1000) + } + } + } + + fun startActivity(activityId: String) { + viewModelScope.launch { + // Optimistic UI Update possible here + try { + api.startActivity(activityId) + // Polling will update the full state shortly + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun stopActivity() { + viewModelScope.launch { + try { + api.stopActivity() + // Explicitly refresh state immediately for better UX + state.value = api.getStatus() + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} + diff --git a/AndroidApp/build.gradle.kts b/AndroidApp/build.gradle.kts index 922f551..8cb7bed 100644 --- a/AndroidApp/build.gradle.kts +++ b/AndroidApp/build.gradle.kts @@ -2,4 +2,6 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) + kotlin("plugin.serialization") version "2.1.20" } \ No newline at end of file diff --git a/AndroidApp/gradle/libs.versions.toml b/AndroidApp/gradle/libs.versions.toml index 3408682..99dc849 100644 --- a/AndroidApp/gradle/libs.versions.toml +++ b/AndroidApp/gradle/libs.versions.toml @@ -8,6 +8,25 @@ espressoCore = "3.5.1" appcompat = "1.6.1" material = "1.10.0" +lifecycleRuntimeKtx = "2.6.1" +activityCompose = "1.8.0" +composeBom = "2024.04.01" +navigationRuntimeKtx = "2.8.8" +navigationCompose = "2.8.8" +places = "4.1.0" +okhttp = "4.12.0" +streamLog = "1.1.4" +landscapist = "2.4.6" +webrtc = "1.3.6" +kotlinxSerializationJson = "1.7.3" +ktlint = "0.43.0" + +retrofit = "2.11.0" +retrofitResultAdapter = "1.0.10" +retrofitKotlinxSerializationJson = "1.0.0" +spotless = "6.7.0" +credentials = "1.5.0" + [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -19,4 +38,4 @@ material = { group = "com.google.android.material", name = "material", version.r [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } - +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }