This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

initial commit

author
nathan holland
date (Apr 4, 2025, 2:16 PM +0300) commit ca9e9ccc
+3596
+17
.gitignore
··· 1 + 2 + *.iml 3 + .gradle 4 + .idea 5 + .kotlin 6 + .DS_Store 7 + build 8 + */build 9 + captures 10 + .externalNativeBuild 11 + .cxx 12 + local.properties 13 + xcuserdata/ 14 + Pods/ 15 + *.jks 16 + *.gpg 17 + *yarn.lock
+22
README.MD
··· 1 + # Compose Multiplatform Application 2 + 3 + ## Before running! 4 + - check your system with [KDoctor](https://github.com/Kotlin/kdoctor) 5 + - install JDK 17 or higher on your machine 6 + - add `local.properties` file to the project root and set a path to Android SDK there 7 + 8 + ### Android 9 + To run the application on android device/emulator: 10 + - open project in Android Studio and run imported android run configuration 11 + 12 + To build the application bundle: 13 + - run `./gradlew :composeApp:assembleDebug` 14 + - find `.apk` file in `composeApp/build/outputs/apk/debug/composeApp-debug.apk` 15 + Run android UI tests on the connected device: `./gradlew :composeApp:connectedDebugAndroidTest` 16 + 17 + ### iOS 18 + To run the application on iPhone device/simulator: 19 + - Open `iosApp/iosApp.xcproject` in Xcode and run standard configuration 20 + - Or use [Kotlin Multiplatform Mobile plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile) for Android Studio 21 + Run iOS simulator UI tests: `./gradlew :composeApp:iosSimulatorArm64Test` 22 +
+7
build.gradle.kts
··· 1 + plugins { 2 + alias(libs.plugins.multiplatform).apply(false) 3 + alias(libs.plugins.compose.compiler).apply(false) 4 + alias(libs.plugins.compose).apply(false) 5 + alias(libs.plugins.android.application).apply(false) 6 + alias(libs.plugins.android.library).apply(false) 7 + }
+33
gradle/libs.versions.toml
··· 1 + [versions] 2 + 3 + kotlin = "2.1.20" 4 + compose = "1.8.0-beta01" 5 + agp = "8.6.1" 6 + androidx-activityCompose = "1.10.1" 7 + androidx-uiTest = "1.7.8" 8 + cameraCore = "1.4.1" 9 + cameraCompose = "1.5.0-alpha06" 10 + poseDetectionVersion = "18.0.0-beta5" 11 + 12 + [libraries] 13 + 14 + androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 15 + androidx-uitest-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" } 16 + androidx-uitest-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" } 17 + androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" } 18 + androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraCore" } 19 + androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraCore" } 20 + androidx-camera-compose = { module = "androidx.camera:camera-compose", version.ref = "cameraCompose" } 21 + androidx-camera-video = { module = "androidx.camera:camera-video", version.ref = "cameraCore" } 22 + androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraCore" } 23 + androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraCore" } 24 + pose-detection = { module = "com.google.mlkit:pose-detection", version.ref = "poseDetectionVersion" } 25 + pose-detection-common = { group = "com.google.mlkit", name = "pose-detection-common", version.ref = "poseDetectionVersion" } 26 + 27 + [plugins] 28 + 29 + multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 30 + compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 31 + compose = { id = "org.jetbrains.compose", version.ref = "compose" } 32 + android-application = { id = "com.android.application", version.ref = "agp" } 33 + android-library = { id = "com.android.library", version.ref = "agp" }
gradle/wrapper/gradle-wrapper.jar

This is a binary file and will not be displayed.

+7
gradle/wrapper/gradle-wrapper.properties
··· 1 + distributionBase=GRADLE_USER_HOME 2 + distributionPath=wrapper/dists 3 + distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 + networkTimeout=10000 5 + validateDistributionUrl=true 6 + zipStoreBase=GRADLE_USER_HOME 7 + zipStorePath=wrapper/dists
+249
gradlew
··· 1 + #!/bin/sh 2 + 3 + # 4 + # Copyright © 2015-2021 the original authors. 5 + # 6 + # Licensed under the Apache License, Version 2.0 (the "License"); 7 + # you may not use this file except in compliance with the License. 8 + # You may obtain a copy of the License at 9 + # 10 + # https://www.apache.org/licenses/LICENSE-2.0 11 + # 12 + # Unless required by applicable law or agreed to in writing, software 13 + # distributed under the License is distributed on an "AS IS" BASIS, 14 + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + # See the License for the specific language governing permissions and 16 + # limitations under the License. 17 + # 18 + 19 + ############################################################################## 20 + # 21 + # Gradle start up script for POSIX generated by Gradle. 22 + # 23 + # Important for running: 24 + # 25 + # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 + # noncompliant, but you have some other compliant shell such as ksh or 27 + # bash, then to run this script, type that shell name before the whole 28 + # command line, like: 29 + # 30 + # ksh Gradle 31 + # 32 + # Busybox and similar reduced shells will NOT work, because this script 33 + # requires all of these POSIX shell features: 34 + # * functions; 35 + # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 + # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 + # * compound commands having a testable exit status, especially «case»; 38 + # * various built-in commands including «command», «set», and «ulimit». 39 + # 40 + # Important for patching: 41 + # 42 + # (2) This script targets any POSIX shell, so it avoids extensions provided 43 + # by Bash, Ksh, etc; in particular arrays are avoided. 44 + # 45 + # The "traditional" practice of packing multiple parameters into a 46 + # space-separated string is a well documented source of bugs and security 47 + # problems, so this is (mostly) avoided, by progressively accumulating 48 + # options in "$@", and eventually passing that to Java. 49 + # 50 + # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 + # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 + # see the in-line comments for details. 53 + # 54 + # There are tweaks for specific operating systems such as AIX, CygWin, 55 + # Darwin, MinGW, and NonStop. 56 + # 57 + # (3) This script is generated from the Groovy template 58 + # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 + # within the Gradle project. 60 + # 61 + # You can find Gradle at https://github.com/gradle/gradle/. 62 + # 63 + ############################################################################## 64 + 65 + # Attempt to set APP_HOME 66 + 67 + # Resolve links: $0 may be a link 68 + app_path=$0 69 + 70 + # Need this for daisy-chained symlinks. 71 + while 72 + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 + [ -h "$app_path" ] 74 + do 75 + ls=$( ls -ld "$app_path" ) 76 + link=${ls#*' -> '} 77 + case $link in #( 78 + /*) app_path=$link ;; #( 79 + *) app_path=$APP_HOME$link ;; 80 + esac 81 + done 82 + 83 + # This is normally unused 84 + # shellcheck disable=SC2034 85 + APP_BASE_NAME=${0##*/} 86 + # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 + APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 + 89 + # Use the maximum available, or set MAX_FD != -1 to use that value. 90 + MAX_FD=maximum 91 + 92 + warn () { 93 + echo "$*" 94 + } >&2 95 + 96 + die () { 97 + echo 98 + echo "$*" 99 + echo 100 + exit 1 101 + } >&2 102 + 103 + # OS specific support (must be 'true' or 'false'). 104 + cygwin=false 105 + msys=false 106 + darwin=false 107 + nonstop=false 108 + case "$( uname )" in #( 109 + CYGWIN* ) cygwin=true ;; #( 110 + Darwin* ) darwin=true ;; #( 111 + MSYS* | MINGW* ) msys=true ;; #( 112 + NONSTOP* ) nonstop=true ;; 113 + esac 114 + 115 + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 + 117 + 118 + # Determine the Java command to use to start the JVM. 119 + if [ -n "$JAVA_HOME" ] ; then 120 + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 + # IBM's JDK on AIX uses strange locations for the executables 122 + JAVACMD=$JAVA_HOME/jre/sh/java 123 + else 124 + JAVACMD=$JAVA_HOME/bin/java 125 + fi 126 + if [ ! -x "$JAVACMD" ] ; then 127 + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 + 129 + Please set the JAVA_HOME variable in your environment to match the 130 + location of your Java installation." 131 + fi 132 + else 133 + JAVACMD=java 134 + if ! command -v java >/dev/null 2>&1 135 + then 136 + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 + 138 + Please set the JAVA_HOME variable in your environment to match the 139 + location of your Java installation." 140 + fi 141 + fi 142 + 143 + # Increase the maximum file descriptors if we can. 144 + if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 + case $MAX_FD in #( 146 + max*) 147 + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 + # shellcheck disable=SC2039,SC3045 149 + MAX_FD=$( ulimit -H -n ) || 150 + warn "Could not query maximum file descriptor limit" 151 + esac 152 + case $MAX_FD in #( 153 + '' | soft) :;; #( 154 + *) 155 + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 + # shellcheck disable=SC2039,SC3045 157 + ulimit -n "$MAX_FD" || 158 + warn "Could not set maximum file descriptor limit to $MAX_FD" 159 + esac 160 + fi 161 + 162 + # Collect all arguments for the java command, stacking in reverse order: 163 + # * args from the command line 164 + # * the main class name 165 + # * -classpath 166 + # * -D...appname settings 167 + # * --module-path (only if needed) 168 + # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 + 170 + # For Cygwin or MSYS, switch paths to Windows format before running java 171 + if "$cygwin" || "$msys" ; then 172 + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 + 175 + JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 + 177 + # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 + for arg do 179 + if 180 + case $arg in #( 181 + -*) false ;; # don't mess with options #( 182 + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 + [ -e "$t" ] ;; #( 184 + *) false ;; 185 + esac 186 + then 187 + arg=$( cygpath --path --ignore --mixed "$arg" ) 188 + fi 189 + # Roll the args list around exactly as many times as the number of 190 + # args, so each arg winds up back in the position where it started, but 191 + # possibly modified. 192 + # 193 + # NB: a `for` loop captures its iteration list before it begins, so 194 + # changing the positional parameters here affects neither the number of 195 + # iterations, nor the values presented in `arg`. 196 + shift # remove old arg 197 + set -- "$@" "$arg" # push replacement arg 198 + done 199 + fi 200 + 201 + 202 + # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 + DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 + 205 + # Collect all arguments for the java command: 206 + # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 + # and any embedded shellness will be escaped. 208 + # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 + # treated as '${Hostname}' itself on the command line. 210 + 211 + set -- \ 212 + "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 + -classpath "$CLASSPATH" \ 214 + org.gradle.wrapper.GradleWrapperMain \ 215 + "$@" 216 + 217 + # Stop when "xargs" is not available. 218 + if ! command -v xargs >/dev/null 2>&1 219 + then 220 + die "xargs is not available" 221 + fi 222 + 223 + # Use "xargs" to parse quoted args. 224 + # 225 + # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 + # 227 + # In Bash we could simply go: 228 + # 229 + # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 + # set -- "${ARGS[@]}" "$@" 231 + # 232 + # but POSIX shell has neither arrays nor command substitution, so instead we 233 + # post-process each arg (as a line of input to sed) to backslash-escape any 234 + # character that might be a shell metacharacter, then use eval to reverse 235 + # that process (while maintaining the separation between arguments), and wrap 236 + # the whole thing up as a single "set" statement. 237 + # 238 + # This will of course break if any of these variables contains a newline or 239 + # an unmatched quote. 240 + # 241 + 242 + eval "set -- $( 243 + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 + xargs -n1 | 245 + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 + tr '\n' ' ' 247 + )" '"$@"' 248 + 249 + exec "$JAVACMD" "$@"
+92
gradlew.bat
··· 1 + @rem 2 + @rem Copyright 2015 the original author or authors. 3 + @rem 4 + @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 + @rem you may not use this file except in compliance with the License. 6 + @rem You may obtain a copy of the License at 7 + @rem 8 + @rem https://www.apache.org/licenses/LICENSE-2.0 9 + @rem 10 + @rem Unless required by applicable law or agreed to in writing, software 11 + @rem distributed under the License is distributed on an "AS IS" BASIS, 12 + @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + @rem See the License for the specific language governing permissions and 14 + @rem limitations under the License. 15 + @rem 16 + 17 + @if "%DEBUG%"=="" @echo off 18 + @rem ########################################################################## 19 + @rem 20 + @rem Gradle startup script for Windows 21 + @rem 22 + @rem ########################################################################## 23 + 24 + @rem Set local scope for the variables with windows NT shell 25 + if "%OS%"=="Windows_NT" setlocal 26 + 27 + set DIRNAME=%~dp0 28 + if "%DIRNAME%"=="" set DIRNAME=. 29 + @rem This is normally unused 30 + set APP_BASE_NAME=%~n0 31 + set APP_HOME=%DIRNAME% 32 + 33 + @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 + for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 + 36 + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 + set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 + 39 + @rem Find java.exe 40 + if defined JAVA_HOME goto findJavaFromJavaHome 41 + 42 + set JAVA_EXE=java.exe 43 + %JAVA_EXE% -version >NUL 2>&1 44 + if %ERRORLEVEL% equ 0 goto execute 45 + 46 + echo. 1>&2 47 + echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 + echo. 1>&2 49 + echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 + echo location of your Java installation. 1>&2 51 + 52 + goto fail 53 + 54 + :findJavaFromJavaHome 55 + set JAVA_HOME=%JAVA_HOME:"=% 56 + set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 + 58 + if exist "%JAVA_EXE%" goto execute 59 + 60 + echo. 1>&2 61 + echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 + echo. 1>&2 63 + echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 + echo location of your Java installation. 1>&2 65 + 66 + goto fail 67 + 68 + :execute 69 + @rem Setup the command line 70 + 71 + set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 + 73 + 74 + @rem Execute Gradle 75 + "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 + 77 + :end 78 + @rem End local scope for the variables with windows NT shell 79 + if %ERRORLEVEL% equ 0 goto mainEnd 80 + 81 + :fail 82 + rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 + rem the _cmd.exe /c_ return code! 84 + set EXIT_CODE=%ERRORLEVEL% 85 + if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 + if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 + exit /b %EXIT_CODE% 88 + 89 + :mainEnd 90 + if "%OS%"=="Windows_NT" endlocal 91 + 92 + :omega
+108
posedetection/build.gradle.kts
··· 1 + import org.jetbrains.compose.ExperimentalComposeLibrary 2 + import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree 3 + import com.vanniktech.maven.publish.SonatypeHost 4 + 5 + mavenPublishing { 6 + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 7 + coordinates("com.performancecoachlab.posedetection", "posedetection-compose", "1.0.0") 8 + 9 + pom { 10 + name.set("Pose Detection") 11 + description.set("real time body tracking for compose multiplatform mobile apps") 12 + inceptionYear.set("2025") 13 + url.set("https://tangled.sh/@nateholland.bsky.social/PoseDetection") 14 + licenses { 15 + license { 16 + name.set("The Apache License, Version 2.0") 17 + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 18 + distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 19 + } 20 + } 21 + developers { 22 + developer { 23 + id.set("nateholland") 24 + name.set("Nate") 25 + url.set("https://tangled.sh/@nateholland.bsky.social") 26 + } 27 + } 28 + scm { 29 + url.set("https://tangled.sh/@nateholland.bsky.social/PoseDetection") 30 + connection.set("scm:git:git://tangled.sh/@nateholland.bsky.social/PoseDetection.git") 31 + developerConnection.set("scm:git:ssh://git@tangled.sh:nateholland.bsky.social/PoseDetection") 32 + } 33 + } 34 + signAllPublications() 35 + } 36 + plugins { 37 + alias(libs.plugins.multiplatform) 38 + alias(libs.plugins.compose.compiler) 39 + alias(libs.plugins.compose) 40 + alias(libs.plugins.android.library) 41 + id("com.vanniktech.maven.publish") version "0.31.0" 42 + } 43 + 44 + kotlin { 45 + jvmToolchain(11) 46 + androidTarget { 47 + //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html 48 + instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test) 49 + } 50 + 51 + listOf( 52 + iosX64(), 53 + iosArm64(), 54 + iosSimulatorArm64() 55 + ).forEach { 56 + it.binaries.framework { 57 + baseName = "ComposeApp" 58 + isStatic = true 59 + } 60 + } 61 + 62 + sourceSets { 63 + commonMain.dependencies { 64 + implementation(compose.runtime) 65 + implementation(compose.foundation) 66 + implementation(compose.material3) 67 + implementation(compose.components.resources) 68 + implementation(compose.components.uiToolingPreview) 69 + } 70 + 71 + commonTest.dependencies { 72 + implementation(kotlin("test")) 73 + @OptIn(ExperimentalComposeLibrary::class) 74 + implementation(compose.uiTest) 75 + } 76 + 77 + androidMain.dependencies { 78 + implementation(compose.uiTooling) 79 + implementation(libs.androidx.activityCompose) 80 + implementation(libs.androidx.camera.core) 81 + implementation(libs.androidx.camera.camera2) 82 + implementation(libs.androidx.camera.lifecycle) 83 + implementation(libs.androidx.camera.video) 84 + implementation(libs.androidx.camera.view) 85 + implementation(libs.androidx.camera.extensions) 86 + implementation(libs.androidx.camera.compose) 87 + implementation(libs.pose.detection) 88 + implementation(libs.pose.detection.common) 89 + } 90 + 91 + } 92 + } 93 + 94 + android { 95 + namespace = "com.performancecoachlab.posedetection" 96 + compileSdk = 35 97 + 98 + defaultConfig { 99 + minSdk = 21 100 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 101 + } 102 + } 103 + 104 + //https://developer.android.com/develop/ui/compose/testing#setup 105 + dependencies { 106 + androidTestImplementation(libs.androidx.uitest.junit4) 107 + debugImplementation(libs.androidx.uitest.testManifest) 108 + }
+139
posedetection/src/androidMain/kotlin/com.performancecoachlab/posedetection/camera/CameraView.android.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + import android.graphics.Bitmap 4 + import androidx.annotation.OptIn 5 + import androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA 6 + import androidx.camera.core.ExperimentalGetImage 7 + import androidx.camera.view.LifecycleCameraController 8 + import androidx.camera.view.PreviewView 9 + import androidx.compose.foundation.Image 10 + import androidx.compose.foundation.layout.Box 11 + import androidx.compose.foundation.layout.fillMaxSize 12 + import androidx.compose.foundation.layout.height 13 + import androidx.compose.foundation.layout.padding 14 + import androidx.compose.foundation.layout.width 15 + import androidx.compose.material3.Surface 16 + import androidx.compose.runtime.Composable 17 + import androidx.compose.runtime.LaunchedEffect 18 + import androidx.compose.runtime.getValue 19 + import androidx.compose.runtime.mutableStateOf 20 + import androidx.compose.runtime.remember 21 + import androidx.compose.runtime.setValue 22 + import androidx.compose.ui.Modifier 23 + import androidx.compose.ui.graphics.ImageBitmap 24 + import androidx.compose.ui.graphics.asAndroidBitmap 25 + import androidx.compose.ui.graphics.asImageBitmap 26 + import androidx.compose.ui.layout.ContentScale 27 + import androidx.compose.ui.platform.LocalContext 28 + import androidx.compose.ui.unit.dp 29 + import androidx.compose.ui.viewinterop.AndroidView 30 + import androidx.lifecycle.LifecycleOwner 31 + import androidx.lifecycle.compose.LocalLifecycleOwner 32 + import com.google.mlkit.vision.common.InputImage 33 + import com.google.mlkit.vision.pose.Pose 34 + import com.google.mlkit.vision.pose.PoseDetection 35 + import com.google.mlkit.vision.pose.PoseLandmark 36 + import com.google.mlkit.vision.pose.defaults.PoseDetectorOptions 37 + import com.performancecoachlab.posedetection.skeleton.Skeleton 38 + import com.performancecoachlab.posedetection.skeleton.SkeletonRepository 39 + import java.util.concurrent.Executors 40 + 41 + @OptIn(ExperimentalGetImage::class) 42 + @Composable 43 + actual fun CameraView( 44 + skeletonRepository: SkeletonRepository, drawSkeleton: Boolean, modifier: Modifier 45 + ) { 46 + var bitmap by remember { mutableStateOf<ImageBitmap?>(null) } 47 + var skeleton by remember { mutableStateOf(Skeleton()) } 48 + val options = 49 + PoseDetectorOptions.Builder().setDetectorMode(PoseDetectorOptions.STREAM_MODE).build() 50 + val poseDetector = PoseDetection.getClient(options) 51 + val context = LocalContext.current 52 + val cameraController = remember { LifecycleCameraController(context) } 53 + val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current 54 + val previewView: PreviewView = remember { PreviewView(context) } 55 + val executor = remember { Executors.newSingleThreadExecutor() } 56 + LaunchedEffect(lifecycleOwner) { 57 + cameraController.bindToLifecycle(lifecycleOwner) 58 + cameraController.cameraSelector = DEFAULT_FRONT_CAMERA 59 + previewView.controller = cameraController 60 + previewView.scaleType = PreviewView.ScaleType.FIT_CENTER 61 + cameraController.setImageAnalysisAnalyzer(executor) { imageProxy -> 62 + imageProxy.image?.let { image -> 63 + val img = InputImage.fromMediaImage( 64 + image, imageProxy.imageInfo.rotationDegrees 65 + ) 66 + poseDetector.process(img).addOnSuccessListener { pose -> 67 + skeleton = skeleton(pose, imageProxy.imageInfo.timestamp) 68 + skeletonRepository.updateSkeleton(skeleton) 69 + bitmap = 70 + imageProxy.toBitmap().rotate(imageProxy.imageInfo.rotationDegrees.toFloat()) 71 + .asImageBitmap().drawSkeleton(if (drawSkeleton) skeleton else null, 0f) 72 + }.addOnFailureListener { e -> 73 + }.addOnCompleteListener { 74 + imageProxy.close() 75 + } 76 + } 77 + } 78 + } 79 + Box( 80 + modifier = Modifier 81 + .height(100.dp) 82 + .width(100.dp) 83 + .padding(20.dp) 84 + ) { 85 + AndroidView( 86 + factory = { previewView }, modifier = Modifier.fillMaxSize() 87 + ) 88 + 89 + } 90 + Surface(modifier = Modifier.fillMaxSize()) { 91 + bitmap?.also { 92 + Image( 93 + it, 94 + contentDescription = "video feed", 95 + modifier = Modifier.fillMaxSize(), 96 + contentScale = ContentScale.Crop 97 + ) 98 + } 99 + 100 + } 101 + } 102 + 103 + 104 + private fun PoseLandmark?.toSkeletonCoords(): Skeleton.SkeletonCoordinate? { 105 + return this?.position?.let { 106 + Skeleton.SkeletonCoordinate( 107 + x = it.x, 108 + y = it.y, 109 + ) 110 + } 111 + } 112 + 113 + fun Bitmap.rotate(angle: Float): Bitmap { 114 + val matrix = android.graphics.Matrix() 115 + matrix.postRotate(angle) 116 + return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) 117 + } 118 + 119 + fun ImageBitmap.rotate(angle: Float): ImageBitmap { 120 + return asAndroidBitmap().rotate(angle).asImageBitmap() 121 + } 122 + 123 + fun skeleton(pose: Pose?, timestamp: Long): Skeleton { 124 + return Skeleton( 125 + timestamp = timestamp, 126 + leftShoulder = pose?.getPoseLandmark(PoseLandmark.LEFT_SHOULDER)?.toSkeletonCoords(), 127 + rightShoulder = pose?.getPoseLandmark(PoseLandmark.RIGHT_SHOULDER)?.toSkeletonCoords(), 128 + leftElbow = pose?.getPoseLandmark(PoseLandmark.LEFT_ELBOW)?.toSkeletonCoords(), 129 + rightElbow = pose?.getPoseLandmark(PoseLandmark.RIGHT_ELBOW)?.toSkeletonCoords(), 130 + leftWrist = pose?.getPoseLandmark(PoseLandmark.LEFT_WRIST)?.toSkeletonCoords(), 131 + rightWrist = pose?.getPoseLandmark(PoseLandmark.RIGHT_WRIST)?.toSkeletonCoords(), 132 + leftHip = pose?.getPoseLandmark(PoseLandmark.LEFT_HIP)?.toSkeletonCoords(), 133 + rightHip = pose?.getPoseLandmark(PoseLandmark.RIGHT_HIP)?.toSkeletonCoords(), 134 + leftKnee = pose?.getPoseLandmark(PoseLandmark.LEFT_KNEE)?.toSkeletonCoords(), 135 + rightKnee = pose?.getPoseLandmark(PoseLandmark.RIGHT_KNEE)?.toSkeletonCoords(), 136 + leftAnkle = pose?.getPoseLandmark(PoseLandmark.LEFT_ANKLE)?.toSkeletonCoords(), 137 + rightAnkle = pose?.getPoseLandmark(PoseLandmark.RIGHT_ANKLE)?.toSkeletonCoords(), 138 + ) 139 + }
+50
posedetection/src/androidMain/kotlin/com.performancecoachlab/posedetection/permissions/PermissionProvider.android.kt
··· 1 + package com.performancecoachlab.posedetection.permissions 2 + 3 + import android.Manifest 4 + import android.content.pm.PackageManager 5 + import androidx.activity.compose.rememberLauncherForActivityResult 6 + import androidx.activity.result.contract.ActivityResultContracts 7 + import androidx.compose.runtime.Composable 8 + import androidx.compose.runtime.LaunchedEffect 9 + import androidx.compose.runtime.remember 10 + import androidx.compose.ui.platform.LocalContext 11 + import androidx.core.content.ContextCompat 12 + 13 + @Composable 14 + actual fun PermissionProvider(): Permissions { 15 + return rememberAndroidPermissions() 16 + } 17 + 18 + @Composable 19 + fun rememberAndroidPermissions(): Permissions { 20 + LocalContext.current.also { context -> 21 + return remember { 22 + object : Permissions { 23 + override fun hasCameraPermission(): Boolean { 24 + return ContextCompat.checkSelfPermission( 25 + context, Manifest.permission.CAMERA 26 + ) == PackageManager.PERMISSION_GRANTED 27 + } 28 + 29 + @Composable 30 + override fun RequestCameraPermission(onGranted: () -> Unit, onDenied: () -> Unit) { 31 + val launcher = rememberLauncherForActivityResult( 32 + contract = ActivityResultContracts.RequestPermission(), 33 + onResult = { isGranted -> 34 + if (isGranted) onGranted() 35 + else onDenied() 36 + }) 37 + val permissionStatus = ContextCompat.checkSelfPermission( 38 + context, Manifest.permission.CAMERA 39 + ) 40 + when (permissionStatus) { 41 + PackageManager.PERMISSION_GRANTED -> onGranted() 42 + PackageManager.PERMISSION_DENIED -> LaunchedEffect(Unit) { 43 + launcher.launch(Manifest.permission.CAMERA) 44 + } 45 + } 46 + } 47 + } 48 + } 49 + } 50 + }
+12
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/camera/CameraView.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + import androidx.compose.runtime.Composable 4 + import androidx.compose.ui.Modifier 5 + import com.performancecoachlab.posedetection.skeleton.SkeletonRepository 6 + 7 + @Composable 8 + expect fun CameraView( 9 + skeletonRepository: SkeletonRepository, 10 + drawSkeleton: Boolean = true, 11 + modifier: Modifier = Modifier 12 + )
+80
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/camera/Utils.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + import androidx.compose.ui.geometry.Size 4 + import androidx.compose.ui.graphics.BlendMode 5 + import androidx.compose.ui.graphics.Brush 6 + import androidx.compose.ui.graphics.Canvas 7 + import androidx.compose.ui.graphics.Color 8 + import androidx.compose.ui.graphics.ImageBitmap 9 + import androidx.compose.ui.graphics.Paint 10 + import androidx.compose.ui.graphics.PaintingStyle.Companion.Fill 11 + import androidx.compose.ui.graphics.PaintingStyle.Companion.Stroke 12 + import androidx.compose.ui.graphics.drawscope.CanvasDrawScope 13 + import androidx.compose.ui.graphics.drawscope.Stroke 14 + import androidx.compose.ui.graphics.rotate 15 + import androidx.compose.ui.unit.Density 16 + import androidx.compose.ui.unit.LayoutDirection 17 + import com.performancecoachlab.posedetection.skeleton.Skeleton 18 + 19 + 20 + fun ImageBitmap.drawSkeleton(skeleton: Skeleton?, rotation: Float = 90f): ImageBitmap { 21 + also { 22 + val bitmap = ImageBitmap(it.width, it.height) 23 + val canvas = Canvas(bitmap).apply { 24 + rotate(rotation, it.width.toFloat() / 2f, it.height.toFloat() / 2f) 25 + } 26 + val drawScope = CanvasDrawScope() 27 + val size = Size(it.width.toFloat(), it.height.toFloat()) 28 + drawScope.draw( 29 + Density(1f), 30 + LayoutDirection.Ltr, 31 + canvas, 32 + size, 33 + ) { 34 + drawImage(it) 35 + skeleton?.apply { 36 + val paintWhite = Paint().apply { 37 + color = Color.White 38 + strokeWidth = 5f 39 + style = Stroke 40 + } 41 + val paintBlue = Paint().apply { 42 + color = Color.Blue 43 + strokeWidth = 4f 44 + style = Fill 45 + } 46 + bones().forEach { line -> 47 + drawLine( 48 + color = paintWhite.color, start = androidx.compose.ui.geometry.Offset( 49 + line.first.x, line.first.y 50 + ), end = androidx.compose.ui.geometry.Offset( 51 + line.second.x, line.second.y 52 + ), strokeWidth = paintWhite.strokeWidth, 53 + blendMode = BlendMode.Softlight 54 + ) 55 + drawLine( 56 + color = paintBlue.color, start = androidx.compose.ui.geometry.Offset( 57 + line.first.x, line.first.y 58 + ), end = androidx.compose.ui.geometry.Offset( 59 + line.second.x, line.second.y 60 + ), strokeWidth = paintBlue.strokeWidth, 61 + blendMode = BlendMode.Color 62 + ) 63 + } 64 + 65 + joints().forEach { joint -> 66 + drawCircle( 67 + brush = Brush.radialGradient( 68 + colors = listOf(Color.Blue, Color.Transparent), 69 + center = androidx.compose.ui.geometry.Offset(joint.x, joint.y), 70 + radius = 15f 71 + ), 72 + radius = 15f, 73 + center = androidx.compose.ui.geometry.Offset(joint.x, joint.y) 74 + ) 75 + } 76 + } 77 + } 78 + return bitmap 79 + } 80 + }
+7
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/permissions/PermissionProvider.kt
··· 1 + package com.performancecoachlab.posedetection.permissions 2 + 3 + import androidx.compose.runtime.Composable 4 + 5 + 6 + @Composable 7 + expect fun PermissionProvider(): Permissions
+10
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/permissions/Permissions.kt
··· 1 + package com.performancecoachlab.posedetection.permissions 2 + 3 + import androidx.compose.runtime.Composable 4 + 5 + interface Permissions { 6 + fun hasCameraPermission(): Boolean 7 + 8 + @Composable 9 + fun RequestCameraPermission(onGranted: () -> Unit, onDenied: () -> Unit) 10 + }
+44
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/skeleton/Pose.kt
··· 1 + package com.performancecoachlab.posedetection.skeleton 2 + 3 + 4 + data class Pose( 5 + val leftShoulder: PoseRange? = null, 6 + val rightShoulder: PoseRange? = null, 7 + val leftElbow: PoseRange? = null, 8 + val rightElbow: PoseRange? = null, 9 + val leftHip: PoseRange? = null, 10 + val rightHip: PoseRange? = null, 11 + val leftKnee: PoseRange? = null, 12 + val rightKnee: PoseRange? = null, 13 + ) { 14 + data class PoseRange( 15 + val min: Double, val max: Double 16 + ) 17 + 18 + fun matches(skeleton: Skeleton): Boolean = matches(skeleton.getAngles()) 19 + 20 + private fun matches(skeletonAngles: Skeleton.SkeletonAngles): Boolean { 21 + return angleInRange( 22 + leftShoulder, 23 + skeletonAngles.leftShoulder 24 + ) && angleInRange(rightShoulder, skeletonAngles.rightShoulder) && angleInRange( 25 + leftElbow, 26 + skeletonAngles.leftElbow 27 + ) && angleInRange(rightElbow, skeletonAngles.rightElbow) && angleInRange( 28 + leftHip, 29 + skeletonAngles.leftHip 30 + ) && angleInRange(rightHip, skeletonAngles.rightHip) && angleInRange( 31 + leftKnee, 32 + skeletonAngles.leftKnee 33 + ) && angleInRange(rightKnee, skeletonAngles.rightKnee) 34 + 35 + } 36 + 37 + private fun angleInRange(angleInPose: PoseRange?, angleInSkeleton: Double?): Boolean { 38 + return angleInPose?.let { poseAngleRange -> 39 + angleInSkeleton?.let { skeletonAngle -> 40 + skeletonAngle >= poseAngleRange.min && skeletonAngle <= poseAngleRange.max 41 + } ?: false 42 + } ?: true 43 + } 44 + }
+148
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/skeleton/Skeleton.kt
··· 1 + package com.performancecoachlab.posedetection.skeleton 2 + 3 + import kotlin.math.PI 4 + import kotlin.math.abs 5 + import kotlin.math.atan2 6 + 7 + data class Skeleton( 8 + val timestamp: Long = 0L, 9 + val leftShoulder : SkeletonCoordinate? = null, 10 + val rightShoulder : SkeletonCoordinate? = null, 11 + val leftElbow : SkeletonCoordinate? = null, 12 + val rightElbow : SkeletonCoordinate? = null, 13 + val leftWrist : SkeletonCoordinate? = null, 14 + val rightWrist : SkeletonCoordinate? = null, 15 + val leftHip : SkeletonCoordinate? = null, 16 + val rightHip : SkeletonCoordinate? = null, 17 + val leftKnee : SkeletonCoordinate? = null, 18 + val rightKnee : SkeletonCoordinate? = null, 19 + val leftAnkle : SkeletonCoordinate? = null, 20 + val rightAnkle : SkeletonCoordinate? = null, 21 + ) { 22 + data class SkeletonCoordinate( 23 + val x: Float, 24 + val y: Float 25 + ) 26 + 27 + data class SkeletonAngles( 28 + val leftShoulder: Double?, 29 + val rightShoulder: Double?, 30 + val leftElbow: Double?, 31 + val rightElbow: Double?, 32 + val leftHip: Double?, 33 + val rightHip: Double?, 34 + val leftKnee: Double?, 35 + val rightKnee: Double?, 36 + ) 37 + 38 + fun bones(): List<Pair<SkeletonCoordinate, SkeletonCoordinate>> { 39 + val lines = emptyList<Pair<SkeletonCoordinate, SkeletonCoordinate>>().toMutableList() 40 + if(leftShoulder != null && rightShoulder != null) lines += Pair(leftShoulder, rightShoulder) 41 + if(leftShoulder != null && leftElbow != null) lines += Pair(leftShoulder, leftElbow) 42 + if(rightShoulder != null && rightElbow != null) lines += Pair(rightShoulder, rightElbow) 43 + if(leftElbow != null && leftWrist != null) lines += Pair(leftElbow, leftWrist) 44 + if(rightElbow != null && rightWrist != null) lines += Pair(rightElbow, rightWrist) 45 + if(leftShoulder != null && leftHip != null) lines += Pair(leftShoulder, leftHip) 46 + if(rightShoulder != null && rightHip != null) lines += Pair(rightShoulder, rightHip) 47 + if(leftHip != null && rightHip != null) lines += Pair(leftHip, rightHip) 48 + if(leftHip != null && leftKnee != null) lines += Pair(leftHip, leftKnee) 49 + if(rightHip != null && rightKnee != null) lines += Pair(rightHip, rightKnee) 50 + if(leftKnee != null && leftAnkle != null) lines += Pair(leftKnee, leftAnkle) 51 + if(rightKnee != null && rightAnkle != null) lines += Pair(rightKnee, rightAnkle) 52 + return lines.toList() 53 + } 54 + 55 + fun joints(): List<SkeletonCoordinate> { 56 + val joints = emptyList<SkeletonCoordinate>().toMutableList() 57 + if(leftShoulder != null) joints += leftShoulder 58 + if(rightShoulder != null) joints += rightShoulder 59 + if(leftElbow != null) joints += leftElbow 60 + if(rightElbow != null) joints += rightElbow 61 + if(leftWrist != null) joints += leftWrist 62 + if(rightWrist != null) joints += rightWrist 63 + if(leftHip != null) joints += leftHip 64 + if(rightHip != null) joints += rightHip 65 + if(leftKnee != null) joints += leftKnee 66 + if(rightKnee != null) joints += rightKnee 67 + if(leftAnkle != null) joints += leftAnkle 68 + if(rightAnkle != null) joints += rightAnkle 69 + return joints.toList() 70 + } 71 + 72 + fun getAngle(firstJoint: SkeletonCoordinate, middleJoint: SkeletonCoordinate, lastJoint: SkeletonCoordinate): Double { 73 + var result = toDegrees( 74 + atan2(lastJoint.y - middleJoint.y, lastJoint.x - middleJoint.x) 75 + - atan2(firstJoint.y - middleJoint.y, firstJoint.x - middleJoint.x) 76 + ) 77 + result = abs(result) 78 + if (result > 180) { 79 + result = 360.0 - result 80 + } 81 + return result 82 + } 83 + 84 + private fun toDegrees(radians: Float): Double { 85 + return radians * 180.0 / PI 86 + } 87 + 88 + fun getAngles(): SkeletonAngles { 89 + return SkeletonAngles( 90 + leftShoulder = leftShoulder?.let { leftShoulder -> 91 + leftElbow?.let { leftElbow -> 92 + leftHip?.let { leftHip -> 93 + getAngle(leftElbow, leftShoulder, leftHip) 94 + } 95 + } 96 + }, 97 + rightShoulder = rightShoulder?.let { rightShoulder -> 98 + rightElbow?.let { rightElbow -> 99 + rightHip?.let { rightHip -> 100 + getAngle(rightElbow, rightShoulder, rightHip) 101 + } 102 + } 103 + }, 104 + leftElbow = leftShoulder?.let { leftShoulder -> 105 + leftElbow?.let { leftElbow -> 106 + leftWrist?.let { leftWrist -> 107 + getAngle(leftShoulder, leftElbow, leftWrist) 108 + } 109 + } 110 + }, 111 + rightElbow = rightShoulder?.let { rightShoulder -> 112 + rightElbow?.let { rightElbow -> 113 + rightWrist?.let { rightWrist -> 114 + getAngle(rightShoulder, rightElbow, rightWrist) 115 + } 116 + } 117 + }, 118 + leftHip = leftShoulder?.let { leftShoulder -> 119 + leftHip?.let { leftHip -> 120 + leftKnee?.let { leftKnee -> 121 + getAngle(leftShoulder, leftHip, leftKnee) 122 + } 123 + } 124 + }, 125 + rightHip = rightShoulder?.let { rightShoulder -> 126 + rightHip?.let { rightHip -> 127 + rightKnee?.let { rightKnee -> 128 + getAngle(rightShoulder, rightHip, rightKnee) 129 + } 130 + } 131 + }, 132 + leftKnee = leftHip?.let { leftHip -> 133 + leftKnee?.let { leftKnee -> 134 + leftAnkle?.let { leftAnkle -> 135 + getAngle(leftHip, leftKnee, leftAnkle) 136 + } 137 + } 138 + }, 139 + rightKnee = rightHip?.let { rightHip -> 140 + rightKnee?.let { rightKnee -> 141 + rightAnkle?.let { rightAnkle -> 142 + getAngle(rightHip, rightKnee, rightAnkle) 143 + } 144 + } 145 + }, 146 + ) 147 + } 148 + }
+13
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/skeleton/SkeletonRepository.kt
··· 1 + package com.performancecoachlab.posedetection.skeleton 2 + 3 + import kotlinx.coroutines.flow.MutableStateFlow 4 + import kotlinx.coroutines.flow.StateFlow 5 + 6 + class SkeletonRepository { 7 + private val _skeletonFlow = MutableStateFlow<Skeleton?>(null) 8 + val skeletonFlow: StateFlow<Skeleton?> get() = _skeletonFlow 9 + 10 + fun updateSkeleton(skeleton: Skeleton) { 11 + _skeletonFlow.value = skeleton 12 + } 13 + }
+642
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraEngine.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + import androidx.compose.ui.geometry.Size 4 + import androidx.compose.ui.graphics.Canvas 5 + import androidx.compose.ui.graphics.ImageBitmap 6 + import androidx.compose.ui.graphics.drawscope.CanvasDrawScope 7 + import androidx.compose.ui.graphics.rotate 8 + import androidx.compose.ui.graphics.toComposeImageBitmap 9 + import androidx.compose.ui.unit.Density 10 + import androidx.compose.ui.unit.LayoutDirection 11 + import kotlinx.atomicfu.atomic 12 + import kotlinx.cinterop.BetaInteropApi 13 + import kotlinx.cinterop.ExperimentalForeignApi 14 + import kotlinx.cinterop.addressOf 15 + import kotlinx.cinterop.autoreleasepool 16 + import kotlinx.cinterop.get 17 + import kotlinx.cinterop.refTo 18 + import kotlinx.cinterop.usePinned 19 + import kotlinx.coroutines.suspendCancellableCoroutine 20 + import org.jetbrains.skia.ColorAlphaType 21 + import org.jetbrains.skia.ColorType 22 + import org.jetbrains.skia.Image 23 + import org.jetbrains.skia.ImageInfo 24 + import platform.AVFoundation.AVCaptureDevice 25 + import platform.AVFoundation.AVCaptureDeviceDiscoverySession 26 + import platform.AVFoundation.AVCaptureDeviceInput 27 + import platform.AVFoundation.AVCaptureDevicePositionBack 28 + import platform.AVFoundation.AVCaptureDevicePositionFront 29 + import platform.AVFoundation.AVCaptureDevicePositionUnspecified 30 + import platform.AVFoundation.AVCaptureDeviceTypeBuiltInWideAngleCamera 31 + import platform.AVFoundation.AVCaptureFlashModeOff 32 + import platform.AVFoundation.AVCaptureInput 33 + import platform.AVFoundation.AVCapturePhoto 34 + import platform.AVFoundation.AVCapturePhotoCaptureDelegateProtocol 35 + import platform.AVFoundation.AVCapturePhotoOutput 36 + import platform.AVFoundation.AVCapturePhotoSettings 37 + import platform.AVFoundation.AVCaptureSession 38 + import platform.AVFoundation.AVCaptureSessionPreset640x480 39 + import platform.AVFoundation.AVCaptureVideoOrientation 40 + import platform.AVFoundation.AVCaptureVideoOrientationLandscapeLeft 41 + import platform.AVFoundation.AVCaptureVideoOrientationLandscapeRight 42 + import platform.AVFoundation.AVCaptureVideoOrientationPortrait 43 + import platform.AVFoundation.AVCaptureVideoOrientationPortraitUpsideDown 44 + import platform.AVFoundation.AVCaptureVideoPreviewLayer 45 + import platform.AVFoundation.AVLayerVideoGravityResizeAspectFill 46 + import platform.AVFoundation.AVMediaTypeVideo 47 + import platform.AVFoundation.AVVideoCodecJPEG 48 + import platform.AVFoundation.AVVideoCodecKey 49 + import platform.AVFoundation.fileDataRepresentation 50 + import platform.AVFoundation.position 51 + import platform.CoreFoundation.CFDataGetBytePtr 52 + import platform.CoreFoundation.CFDataGetLength 53 + import platform.CoreFoundation.CFRelease 54 + import platform.CoreGraphics.CGBitmapContextCreate 55 + import platform.CoreGraphics.CGBitmapContextCreateImage 56 + import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB 57 + import platform.CoreGraphics.CGDataProviderCopyData 58 + import platform.CoreGraphics.CGImageAlphaInfo 59 + import platform.CoreGraphics.CGImageCreateCopyWithColorSpace 60 + import platform.CoreGraphics.CGImageGetAlphaInfo 61 + import platform.CoreGraphics.CGImageGetBytesPerRow 62 + import platform.CoreGraphics.CGImageGetDataProvider 63 + import platform.CoreGraphics.CGImageGetHeight 64 + import platform.CoreGraphics.CGImageGetWidth 65 + import platform.Foundation.NSData 66 + import platform.Foundation.NSError 67 + import platform.UIKit.UIDevice 68 + import platform.UIKit.UIDeviceOrientation 69 + import platform.UIKit.UIImage 70 + import platform.UIKit.UIView 71 + import platform.UIKit.UIViewController 72 + import platform.darwin.DISPATCH_QUEUE_PRIORITY_HIGH 73 + import platform.darwin.NSObject 74 + import platform.darwin.dispatch_async 75 + import platform.darwin.dispatch_get_global_queue 76 + import platform.darwin.dispatch_get_main_queue 77 + import platform.posix.memcpy 78 + import kotlin.coroutines.resume 79 + 80 + class CameraEngine( 81 + ) : UIViewController(null, null) { 82 + private var isCapturing = atomic(false) 83 + val cameraController = CameraController() 84 + private var imageCaptureListeners = mutableListOf<(ByteArray) -> Unit>() 85 + 86 + 87 + private val memoryManager = MemoryManager 88 + 89 + override fun viewDidLoad() { 90 + super.viewDidLoad() 91 + 92 + MemoryManager.initialize() 93 + setupCamera() 94 + 95 + } 96 + 97 + override fun viewWillAppear(animated: Boolean) { 98 + super.viewWillAppear(animated) 99 + MemoryManager.updateMemoryStatus() 100 + } 101 + 102 + override fun viewDidDisappear(animated: Boolean) { 103 + super.viewDidDisappear(animated) 104 + 105 + MemoryManager.clearBufferPools() 106 + } 107 + 108 + fun getCameraPreviewLayer() = cameraController.cameraPreviewLayer 109 + 110 + internal fun currentVideoOrientation(): AVCaptureVideoOrientation { 111 + val orientation = UIDevice.currentDevice.orientation 112 + return when (orientation) { 113 + UIDeviceOrientation.UIDeviceOrientationPortrait -> AVCaptureVideoOrientationPortrait 114 + UIDeviceOrientation.UIDeviceOrientationPortraitUpsideDown -> AVCaptureVideoOrientationPortraitUpsideDown 115 + UIDeviceOrientation.UIDeviceOrientationLandscapeLeft -> AVCaptureVideoOrientationLandscapeRight 116 + UIDeviceOrientation.UIDeviceOrientationLandscapeRight -> AVCaptureVideoOrientationLandscapeLeft 117 + else -> AVCaptureVideoOrientationPortrait 118 + } 119 + } 120 + 121 + private fun setupCamera() { 122 + cameraController.setupSession() 123 + cameraController.setupPreviewLayer(view) 124 + startSession() 125 + 126 + cameraController.onPhotoCapture = { image -> 127 + image?.let { 128 + processImageCapture(it) 129 + } 130 + } 131 + 132 + cameraController.onError = { error -> 133 + println("Camera Error: $error") 134 + } 135 + } 136 + 137 + 138 + @OptIn(BetaInteropApi::class) 139 + private fun processImageCapture(imageData: NSData) { 140 + 141 + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.toLong(), 0u)) { 142 + autoreleasepool { 143 + 144 + MemoryManager.updateMemoryStatus() 145 + 146 + try { 147 + 148 + val estimatedSize = imageData.length.toInt() 149 + val buffer = if (estimatedSize > 0) { 150 + MemoryManager.getBuffer(estimatedSize) 151 + } else { 152 + 153 + ByteArray(imageData.length.toInt()) 154 + } 155 + 156 + 157 + val data = imageData.toByteArray(reuseBuffer = buffer) 158 + 159 + 160 + dispatch_async(dispatch_get_main_queue()) { 161 + imageCaptureListeners.forEach { it(data) } 162 + } 163 + 164 + 165 + if (buffer.size >= estimatedSize) { 166 + MemoryManager.recycleBuffer(buffer) 167 + } 168 + 169 + } catch (e: Exception) { 170 + println("Error processing image data: ${e.message}") 171 + } 172 + } 173 + } 174 + } 175 + 176 + 177 + @OptIn(ExperimentalForeignApi::class) 178 + fun NSData.toByteArray(reuseBuffer: ByteArray? = null): ByteArray { 179 + val length = this.length.toInt() 180 + 181 + 182 + val buffer = if (reuseBuffer != null && reuseBuffer.size >= length) { 183 + reuseBuffer 184 + } else { 185 + ByteArray(length) 186 + } 187 + 188 + buffer.usePinned { 189 + memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length) 190 + } 191 + 192 + return buffer 193 + } 194 + 195 + 196 + @OptIn(ExperimentalForeignApi::class) 197 + override fun viewDidLayoutSubviews() { 198 + super.viewDidLayoutSubviews() 199 + cameraController.cameraPreviewLayer?.setFrame(view.bounds) 200 + } 201 + 202 + fun normaliseImageRotation(image: NSData): ImageBitmap { 203 + val uiImage = UIImage(image).toImageBitmap() 204 + return when (UIDevice.currentDevice.orientation) { 205 + UIDeviceOrientation.UIDeviceOrientationPortrait -> uiImage.rotateLeft() 206 + UIDeviceOrientation.UIDeviceOrientationPortraitUpsideDown -> uiImage.rotateRight() 207 + UIDeviceOrientation.UIDeviceOrientationLandscapeLeft -> if(cameraController.isUsingFrontCamera) uiImage.rotate180() else uiImage 208 + else -> if(cameraController.isUsingFrontCamera) uiImage else uiImage.rotate180() 209 + } 210 + } 211 + 212 + @OptIn(BetaInteropApi::class) 213 + suspend fun getFrame(): FrameCaptureResult = suspendCancellableCoroutine { continuation -> 214 + if (!isCapturing.compareAndSet(expect = false, update = true)) { 215 + continuation.resume(FrameCaptureResult.Error(Exception("Capture in progress"))) 216 + return@suspendCancellableCoroutine 217 + } 218 + MemoryManager.updateMemoryStatus() 219 + 220 + val captureHandler = object { 221 + var completed = false 222 + 223 + fun process(image: NSData?, error: String?) { 224 + if (completed) return 225 + completed = true 226 + 227 + if (image != null) { 228 + 229 + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.toLong(), 0u)) { 230 + try { 231 + autoreleasepool { 232 + 233 + val result = 234 + FrameCaptureResult.Success(normaliseImageRotation(image)) 235 + 236 + dispatch_async(dispatch_get_main_queue()) { 237 + isCapturing.value = false 238 + continuation.resume( 239 + result 240 + ) 241 + } 242 + } 243 + } catch (e: Exception) { 244 + dispatch_async(dispatch_get_main_queue()) { 245 + isCapturing.value = false 246 + continuation.resume(FrameCaptureResult.Error(e)) 247 + } 248 + } 249 + } 250 + } else { 251 + isCapturing.value = false 252 + continuation.resume( 253 + FrameCaptureResult.Error( 254 + Exception( 255 + error ?: "Capture failed" 256 + ) 257 + ) 258 + ) 259 + } 260 + } 261 + } 262 + 263 + cameraController.onPhotoCapture = { image -> 264 + captureHandler.process(image, null) 265 + } 266 + 267 + cameraController.onError = { error -> 268 + captureHandler.process(null, error.toString()) 269 + } 270 + 271 + continuation.invokeOnCancellation { 272 + captureHandler.process(null, "Capture cancelled") 273 + } 274 + cameraController.captureFrame() 275 + } 276 + 277 + 278 + fun toggleCameraLens() { 279 + MemoryManager.updateMemoryStatus() 280 + if (MemoryManager.isUnderMemoryPressure()) MemoryManager.clearBufferPools() 281 + cameraController.switchCamera() 282 + } 283 + 284 + private fun startSession() { 285 + MemoryManager.clearBufferPools() 286 + cameraController.startSession() 287 + } 288 + 289 + fun stopSession() { 290 + cameraController.stopSession() 291 + } 292 + } 293 + 294 + class CameraController : NSObject(), AVCapturePhotoCaptureDelegateProtocol { 295 + var captureSession: AVCaptureSession? = null 296 + private var backCamera: AVCaptureDevice? = null 297 + private var frontCamera: AVCaptureDevice? = null 298 + private var currentCamera: AVCaptureDevice? = null 299 + private var photoOutput: AVCapturePhotoOutput? = null 300 + var cameraPreviewLayer: AVCaptureVideoPreviewLayer? = null 301 + var isUsingFrontCamera = true 302 + 303 + var onPhotoCapture: ((NSData?) -> Unit)? = null 304 + var onError: ((CameraException) -> Unit)? = null 305 + 306 + sealed class CameraException : Exception() { 307 + class DeviceNotAvailable : CameraException() 308 + class ConfigurationError(message: String) : CameraException() 309 + class CaptureError(message: String) : CameraException() 310 + } 311 + 312 + fun setupSession() { 313 + try { 314 + captureSession = AVCaptureSession() 315 + captureSession?.beginConfiguration() 316 + 317 + 318 + captureSession?.sessionPreset = AVCaptureSessionPreset640x480 319 + 320 + if (!setupInputs()) { 321 + throw CameraException.DeviceNotAvailable() 322 + } 323 + 324 + setupPhotoOutput() 325 + captureSession?.commitConfiguration() 326 + } catch (e: CameraException) { 327 + cleanupSession() 328 + onError?.invoke(e) 329 + } 330 + } 331 + 332 + private fun setupPhotoOutput() { 333 + photoOutput = AVCapturePhotoOutput() 334 + photoOutput?.setHighResolutionCaptureEnabled(false) 335 + val connection = photoOutput?.connectionWithMediaType(AVMediaTypeVideo) 336 + connection?.videoOrientation = AVCaptureVideoOrientationPortrait 337 + photoOutput?.setPreparedPhotoSettingsArray( 338 + emptyList<String>(), 339 + completionHandler = { _, error -> 340 + if (error != null)onError?.invoke(CameraException.ConfigurationError(error.localizedDescription)) 341 + }) 342 + 343 + if (captureSession?.canAddOutput(photoOutput!!) == true) { 344 + captureSession?.addOutput(photoOutput!!) 345 + } else { 346 + throw CameraException.ConfigurationError("Cannot add photo output") 347 + } 348 + } 349 + 350 + @OptIn(ExperimentalForeignApi::class) 351 + private fun setupInputs(): Boolean { 352 + val availableDevices = AVCaptureDeviceDiscoverySession.discoverySessionWithDeviceTypes( 353 + listOf(AVCaptureDeviceTypeBuiltInWideAngleCamera), 354 + AVMediaTypeVideo, 355 + AVCaptureDevicePositionUnspecified 356 + ).devices 357 + 358 + if (availableDevices.isEmpty()) return false 359 + 360 + for (device in availableDevices) { 361 + when ((device as AVCaptureDevice).position) { 362 + AVCaptureDevicePositionBack -> backCamera = device 363 + AVCaptureDevicePositionFront -> frontCamera = device 364 + } 365 + } 366 + currentCamera = if(isUsingFrontCamera) frontCamera ?: backCamera ?: return false 367 + else backCamera ?: frontCamera ?: return false 368 + 369 + 370 + 371 + try { 372 + val input = AVCaptureDeviceInput.deviceInputWithDevice( 373 + currentCamera!!, 374 + null 375 + ) ?: return false 376 + 377 + if (captureSession?.canAddInput(input) == true) { 378 + captureSession?.addInput(input) 379 + return true 380 + } 381 + } catch (e: Exception) { 382 + throw CameraException.ConfigurationError(e.message ?: "Unknown error") 383 + } 384 + return false 385 + } 386 + 387 + fun startSession() { 388 + if (captureSession?.isRunning() == false) { 389 + dispatch_async( 390 + dispatch_get_global_queue( 391 + DISPATCH_QUEUE_PRIORITY_HIGH.toLong(), 392 + 0u 393 + ) 394 + ) { 395 + captureSession?.startRunning() 396 + } 397 + } 398 + } 399 + 400 + fun stopSession() { 401 + if (captureSession?.isRunning() == true) { 402 + captureSession?.stopRunning() 403 + } 404 + } 405 + 406 + fun cleanupSession() { 407 + stopSession() 408 + cameraPreviewLayer?.removeFromSuperlayer() 409 + cameraPreviewLayer = null 410 + captureSession = null 411 + photoOutput = null 412 + currentCamera = null 413 + backCamera = null 414 + frontCamera = null 415 + } 416 + 417 + @OptIn(ExperimentalForeignApi::class) 418 + fun setupPreviewLayer(view: UIView) { 419 + captureSession?.let { session -> 420 + val newPreviewLayer = AVCaptureVideoPreviewLayer(session = session).apply { 421 + videoGravity = AVLayerVideoGravityResizeAspectFill 422 + setFrame(view.bounds) 423 + connection?.videoOrientation = currentVideoOrientation() 424 + } 425 + 426 + view.layer.addSublayer(newPreviewLayer) 427 + cameraPreviewLayer = newPreviewLayer 428 + } 429 + } 430 + 431 + fun currentVideoOrientation(): AVCaptureVideoOrientation { 432 + val orientation = UIDevice.currentDevice.orientation 433 + return when (orientation) { 434 + UIDeviceOrientation.UIDeviceOrientationPortrait -> AVCaptureVideoOrientationPortrait 435 + UIDeviceOrientation.UIDeviceOrientationPortraitUpsideDown -> AVCaptureVideoOrientationPortraitUpsideDown 436 + UIDeviceOrientation.UIDeviceOrientationLandscapeLeft -> AVCaptureVideoOrientationLandscapeRight 437 + UIDeviceOrientation.UIDeviceOrientationLandscapeRight -> AVCaptureVideoOrientationLandscapeLeft 438 + else -> AVCaptureVideoOrientationPortrait 439 + } 440 + } 441 + 442 + @OptIn(ExperimentalForeignApi::class) 443 + fun switchCamera() { 444 + guard(captureSession != null) { return@guard } 445 + 446 + captureSession?.beginConfiguration() 447 + 448 + try { 449 + captureSession?.inputs?.firstOrNull()?.let { input -> 450 + captureSession?.removeInput(input as AVCaptureInput) 451 + } 452 + 453 + isUsingFrontCamera = !isUsingFrontCamera 454 + currentCamera = if (isUsingFrontCamera) frontCamera else backCamera 455 + 456 + val newCamera = currentCamera ?: throw CameraException.DeviceNotAvailable() 457 + 458 + val newInput = AVCaptureDeviceInput.deviceInputWithDevice( 459 + newCamera, 460 + null 461 + ) ?: throw CameraException.ConfigurationError("Failed to create input") 462 + 463 + if (captureSession?.canAddInput(newInput) == true) { 464 + captureSession?.addInput(newInput) 465 + } else { 466 + throw CameraException.ConfigurationError("Cannot add input") 467 + } 468 + 469 + cameraPreviewLayer?.connection?.let { connection -> 470 + if (connection.isVideoMirroringSupported()) { 471 + connection.automaticallyAdjustsVideoMirroring = false 472 + connection.setVideoMirrored(isUsingFrontCamera) 473 + } 474 + } 475 + 476 + captureSession?.commitConfiguration() 477 + } catch (e: CameraException) { 478 + captureSession?.commitConfiguration() 479 + onError?.invoke(e) 480 + } catch (e: Exception) { 481 + captureSession?.commitConfiguration() 482 + onError?.invoke(CameraException.ConfigurationError(e.message ?: "Unknown error")) 483 + } 484 + } 485 + 486 + override fun captureOutput( 487 + output: AVCapturePhotoOutput, 488 + didFinishProcessingPhoto: AVCapturePhoto, 489 + error: NSError? 490 + ) { 491 + if (error != null) { 492 + onError?.invoke(CameraException.CaptureError(error.localizedDescription)) 493 + return 494 + } 495 + 496 + val imageData = didFinishProcessingPhoto.fileDataRepresentation() 497 + onPhotoCapture?.invoke(imageData) 498 + } 499 + 500 + private inline fun guard(condition: Boolean, crossinline block: () -> Unit) { 501 + if (!condition) block() 502 + } 503 + 504 + fun captureFrame() { 505 + if (photoOutput == null || captureSession?.isRunning() != true) { 506 + onError?.invoke(CameraException.ConfigurationError("Camera not ready for capture")) 507 + return 508 + } 509 + 510 + val settings = AVCapturePhotoSettings.photoSettingsWithFormat( 511 + mapOf( 512 + AVVideoCodecKey to AVVideoCodecJPEG 513 + ) 514 + ) 515 + 516 + settings.setHighResolutionPhotoEnabled(false) 517 + settings.flashMode = AVCaptureFlashModeOff 518 + settings.setAutoStillImageStabilizationEnabled(false) 519 + 520 + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.toLong(), 0u)) { 521 + photoOutput?.capturePhotoWithSettings(settings, delegate = this) 522 + } 523 + } 524 + } 525 + 526 + 527 + @OptIn(ExperimentalForeignApi::class) 528 + internal fun UIImage.toSkiaImage(): Image? { 529 + val imageRef = CGImageCreateCopyWithColorSpace(this.CGImage, CGColorSpaceCreateDeviceRGB()) ?: return null 530 + 531 + val width = CGImageGetWidth(imageRef).toInt() 532 + val height = CGImageGetHeight(imageRef).toInt() 533 + 534 + val bytesPerRow = CGImageGetBytesPerRow(imageRef) 535 + val data = CGDataProviderCopyData(CGImageGetDataProvider(imageRef)) 536 + val bytePointer = CFDataGetBytePtr(data) 537 + val length = CFDataGetLength(data) 538 + val alphaInfo = CGImageGetAlphaInfo(imageRef) 539 + 540 + val alphaType = when (alphaInfo) { 541 + CGImageAlphaInfo.kCGImageAlphaPremultipliedFirst, CGImageAlphaInfo.kCGImageAlphaPremultipliedLast -> ColorAlphaType.PREMUL 542 + CGImageAlphaInfo.kCGImageAlphaFirst, CGImageAlphaInfo.kCGImageAlphaLast -> ColorAlphaType.UNPREMUL 543 + CGImageAlphaInfo.kCGImageAlphaNone, CGImageAlphaInfo.kCGImageAlphaNoneSkipFirst, CGImageAlphaInfo.kCGImageAlphaNoneSkipLast -> ColorAlphaType.OPAQUE 544 + else -> ColorAlphaType.UNKNOWN 545 + } 546 + 547 + val byteArray = ByteArray(length.toInt()) { index -> 548 + bytePointer!![index].toByte() 549 + } 550 + CFRelease(data) 551 + CFRelease(imageRef) 552 + 553 + return Image.makeRaster( 554 + imageInfo = ImageInfo(width = width, height = height, colorType = ColorType.RGBA_8888, alphaType = alphaType), 555 + bytes = byteArray, 556 + rowBytes = bytesPerRow.toInt(), 557 + ) 558 + } 559 + 560 + fun UIImage.toImageBitmap(): ImageBitmap { 561 + return this.toSkiaImage()!!.toComposeImageBitmap() 562 + } 563 + 564 + fun ImageBitmap.rotateRight(): ImageBitmap { 565 + val original = this 566 + val rotated = ImageBitmap(height, width, config, hasAlpha, colorSpace) 567 + val canvas = Canvas(rotated).also { 568 + it.rotate(-90f, width.toFloat()/ 2f, width.toFloat()/ 2f) 569 + } 570 + val drawScope = CanvasDrawScope() 571 + val size = Size(width.toFloat(), height.toFloat()) 572 + drawScope.draw( 573 + Density(1f), 574 + LayoutDirection.Ltr, 575 + canvas, 576 + size, 577 + ) { 578 + drawImage(original) 579 + } 580 + return rotated 581 + } 582 + 583 + fun ImageBitmap.rotateLeft(): ImageBitmap { 584 + val original = this 585 + val rotated = ImageBitmap(height,width,config, hasAlpha,colorSpace) 586 + val canvas = Canvas(rotated).also { 587 + it.rotate(90f, height.toFloat() / 2f, height.toFloat() / 2f) 588 + } 589 + val drawScope = CanvasDrawScope() 590 + val size = Size(width.toFloat(), height.toFloat()) 591 + drawScope.draw( 592 + Density(1f), 593 + LayoutDirection.Ltr, 594 + canvas, 595 + size, 596 + ) { 597 + drawImage(original) 598 + } 599 + return rotated 600 + } 601 + 602 + fun ImageBitmap.rotate180():ImageBitmap { 603 + val original = this 604 + val rotated = ImageBitmap(width,height,config, hasAlpha,colorSpace) 605 + val canvas = Canvas(rotated).also { 606 + it.rotate(180f, width.toFloat() / 2f, height.toFloat() / 2f) 607 + } 608 + val drawScope = CanvasDrawScope() 609 + val size = Size(width.toFloat(), height.toFloat()) 610 + drawScope.draw( 611 + Density(1f), 612 + LayoutDirection.Ltr, 613 + canvas, 614 + size, 615 + ) { 616 + drawImage(original) 617 + } 618 + return rotated 619 + } 620 + 621 + @OptIn(ExperimentalForeignApi::class) 622 + fun ImageBitmap.toUIImage(): UIImage? { 623 + val width = this.width 624 + val height = this.height 625 + val buffer = IntArray(width * height) 626 + 627 + this.readPixels(buffer) 628 + 629 + val colorSpace = CGColorSpaceCreateDeviceRGB() 630 + val context = CGBitmapContextCreate( 631 + data = buffer.refTo(0), 632 + width = width.toULong(), 633 + height = height.toULong(), 634 + bitsPerComponent = 8u, 635 + bytesPerRow = (4 * width).toULong(), 636 + space = colorSpace, 637 + bitmapInfo = CGImageAlphaInfo.kCGImageAlphaPremultipliedLast.value 638 + ) 639 + 640 + val cgImage = CGBitmapContextCreateImage(context) 641 + return cgImage?.let { UIImage.imageWithCGImage(it) } 642 + }
+47
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraPreview.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + 4 + import androidx.compose.runtime.Composable 5 + import androidx.compose.runtime.DisposableEffect 6 + import androidx.compose.runtime.LaunchedEffect 7 + import androidx.compose.runtime.remember 8 + import androidx.compose.ui.Modifier 9 + import androidx.compose.ui.viewinterop.UIKitViewController 10 + import platform.Foundation.NSNotificationCenter 11 + import platform.UIKit.UIDeviceOrientationDidChangeNotification 12 + 13 + @Composable 14 + fun CameraPreview( 15 + modifier: Modifier, 16 + onCameraControllerReady: (CameraEngine) -> Unit 17 + ) { 18 + 19 + val cameraEngine = remember { 20 + CameraEngine() 21 + } 22 + 23 + LaunchedEffect(cameraEngine) { 24 + onCameraControllerReady(cameraEngine) 25 + } 26 + 27 + DisposableEffect(Unit) { 28 + val notificationCenter = NSNotificationCenter.defaultCenter 29 + val observer = notificationCenter.addObserverForName( 30 + UIDeviceOrientationDidChangeNotification, 31 + null, 32 + null 33 + ) { _ -> 34 + cameraEngine.getCameraPreviewLayer()?.connection?.videoOrientation = 35 + cameraEngine.currentVideoOrientation() 36 + } 37 + 38 + onDispose { 39 + notificationCenter.removeObserver(observer) 40 + } 41 + } 42 + 43 + UIKitViewController( 44 + factory = { cameraEngine }, 45 + modifier = modifier, 46 + ) 47 + }
+225
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraView.ios.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + import androidx.compose.foundation.Image 4 + import androidx.compose.foundation.layout.Box 5 + import androidx.compose.foundation.layout.Column 6 + import androidx.compose.foundation.layout.fillMaxSize 7 + import androidx.compose.foundation.layout.height 8 + import androidx.compose.foundation.layout.width 9 + import androidx.compose.material3.Button 10 + import androidx.compose.material3.Surface 11 + import androidx.compose.material3.Text 12 + import androidx.compose.runtime.Composable 13 + import androidx.compose.runtime.LaunchedEffect 14 + import androidx.compose.runtime.getValue 15 + import androidx.compose.runtime.mutableStateOf 16 + import androidx.compose.runtime.remember 17 + import androidx.compose.runtime.rememberCoroutineScope 18 + import androidx.compose.runtime.setValue 19 + import androidx.compose.ui.Modifier 20 + import androidx.compose.ui.graphics.Color 21 + import androidx.compose.ui.graphics.ImageBitmap 22 + import androidx.compose.ui.layout.ContentScale 23 + import androidx.compose.ui.unit.dp 24 + import com.performancecoachlab.posedetection.skeleton.Skeleton 25 + import com.performancecoachlab.posedetection.skeleton.SkeletonRepository 26 + import kotlinx.cinterop.CValue 27 + import kotlinx.cinterop.ExperimentalForeignApi 28 + import kotlinx.cinterop.useContents 29 + import kotlinx.coroutines.launch 30 + import platform.CoreGraphics.CGImageGetHeight 31 + import platform.CoreGraphics.CGImageGetWidth 32 + import platform.CoreGraphics.CGImageRef 33 + import platform.CoreGraphics.CGPoint 34 + import platform.Foundation.NSData 35 + import platform.UIKit.UIImage 36 + import platform.Vision.VNDetectHumanBodyPoseRequest 37 + import platform.Vision.VNHumanBodyPoseObservation 38 + import platform.Vision.VNHumanBodyPoseObservationJointName 39 + import platform.Vision.VNHumanBodyPoseObservationJointNameLeftAnkle 40 + import platform.Vision.VNHumanBodyPoseObservationJointNameLeftElbow 41 + import platform.Vision.VNHumanBodyPoseObservationJointNameLeftHip 42 + import platform.Vision.VNHumanBodyPoseObservationJointNameLeftKnee 43 + import platform.Vision.VNHumanBodyPoseObservationJointNameLeftShoulder 44 + import platform.Vision.VNHumanBodyPoseObservationJointNameLeftWrist 45 + import platform.Vision.VNHumanBodyPoseObservationJointNameRightAnkle 46 + import platform.Vision.VNHumanBodyPoseObservationJointNameRightElbow 47 + import platform.Vision.VNHumanBodyPoseObservationJointNameRightHip 48 + import platform.Vision.VNHumanBodyPoseObservationJointNameRightKnee 49 + import platform.Vision.VNHumanBodyPoseObservationJointNameRightShoulder 50 + import platform.Vision.VNHumanBodyPoseObservationJointNameRightWrist 51 + import platform.Vision.VNImagePointForNormalizedPoint 52 + import platform.Vision.VNImageRequestHandler 53 + import platform.Vision.VNRecognizedPoint 54 + import platform.Vision.VNRequest 55 + import kotlin.time.Clock 56 + import kotlin.time.ExperimentalTime 57 + 58 + @Composable 59 + actual fun CameraView( 60 + skeletonRepository: SkeletonRepository, 61 + drawSkeleton: Boolean, 62 + modifier: Modifier 63 + ) { 64 + val cameraEngine = remember { mutableStateOf<CameraEngine?>(null) } 65 + Box(modifier = Modifier.fillMaxSize()) { 66 + CameraPreview( 67 + modifier = Modifier.width(50.dp).height(50.dp), 68 + onCameraControllerReady = { engine -> 69 + cameraEngine.value = engine 70 + }) 71 + Surface(modifier = Modifier.fillMaxSize()) { 72 + cameraEngine.value?.also { 73 + DetectPoseView(cameraEngine = it, skeletonRepository = skeletonRepository, drawSkeleton = drawSkeleton,modifier = Modifier.fillMaxSize()) 74 + } 75 + } 76 + } 77 + } 78 + 79 + @OptIn(ExperimentalTime::class) 80 + @Composable 81 + fun DetectPoseView(cameraEngine: CameraEngine, skeletonRepository: SkeletonRepository, drawSkeleton: Boolean, modifier: Modifier) { 82 + val scope = rememberCoroutineScope() 83 + var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) } 84 + LaunchedEffect(cameraEngine) { 85 + scope.launch { 86 + while(true){ 87 + cameraEngine.getFrame().also { result -> 88 + if (result is FrameCaptureResult.Success) { 89 + result.imageBitmap?.also { 90 + it.processPose(Clock.System.now().toEpochMilliseconds(), skeletonRepository) 91 + imageBitmap = it.drawSkeleton(if(drawSkeleton)skeletonRepository.skeletonFlow.value else null,0f) 92 + } 93 + } else if (result is FrameCaptureResult.Error) { 94 + println("Error capturing frame: ${result.exception}") 95 + } 96 + } 97 + } 98 + } 99 + } 100 + 101 + imageBitmap?.also { 102 + Image( 103 + bitmap = it, 104 + contentDescription = "video frame", 105 + modifier = modifier, 106 + contentScale = ContentScale.Inside, 107 + ) 108 + } 109 + 110 + } 111 + 112 + @OptIn(ExperimentalForeignApi::class) 113 + private fun NSData.processPose(timestamp: Long, skeletonRepository: SkeletonRepository) { 114 + UIImage(this).let { image -> 115 + val cgImage = image.CGImage 116 + processPose(cgImage, timestamp, skeletonRepository) 117 + } 118 + } 119 + 120 + @OptIn(ExperimentalForeignApi::class) 121 + fun ImageBitmap.processPose(timestamp: Long, skeletonRepository: SkeletonRepository){ 122 + toUIImage()?.also { 123 + image -> 124 + val cgImage = image.CGImage 125 + processPose(cgImage, timestamp,skeletonRepository) 126 + } 127 + } 128 + fun bodyPoseHandler(request: VNRequest): MutableMap<VNHumanBodyPoseObservationJointName, VNRecognizedPoint>? { 129 + try { 130 + val observations = request.results as List<VNHumanBodyPoseObservation> 131 + // Process each observation to find the recognized body pose points. 132 + return observations.lastOrNull()?.let { processObservation(it) } 133 + } catch (e: Exception) { 134 + println("Error processing observations: ${e.message}") 135 + return null 136 + } 137 + } 138 + 139 + @OptIn(ExperimentalForeignApi::class) 140 + fun processObservation(observation: VNHumanBodyPoseObservation): MutableMap<VNHumanBodyPoseObservationJointName, VNRecognizedPoint> { 141 + val points = emptyMap<VNHumanBodyPoseObservationJointName, VNRecognizedPoint>().toMutableMap() 142 + observation.availableJointNames.forEach { 143 + observation.recognizedPointForJointName(it as VNHumanBodyPoseObservationJointName, null) 144 + ?.also { point -> 145 + if (point.confidence > 0f) { 146 + points[it] = point 147 + } 148 + } 149 + } 150 + return points 151 + } 152 + 153 + @OptIn(ExperimentalForeignApi::class) 154 + fun CValue<CGPoint>.toSkeletonPoint(width: ULong, height: ULong): Skeleton.SkeletonCoordinate { 155 + return VNImagePointForNormalizedPoint( 156 + this, width, height 157 + ).let { 158 + it.useContents { Skeleton.SkeletonCoordinate(x.toFloat(), height.toFloat() - y.toFloat()) } 159 + } 160 + } 161 + 162 + @OptIn(ExperimentalForeignApi::class) 163 + fun processPose(cgImage: CGImageRef?, timestamp: Long, skeletonRepository: SkeletonRepository) { 164 + val width = CGImageGetWidth(cgImage) 165 + val height = CGImageGetHeight(cgImage) 166 + val requestHandler = VNImageRequestHandler(cgImage, mapOf<Any?, Any?>()) 167 + val request = VNDetectHumanBodyPoseRequest { request, error -> 168 + if (error != null) { 169 + println("Unable to perform the request: $error") 170 + } else { 171 + request?.also { vnRequest -> 172 + val recognizedPoints = bodyPoseHandler(vnRequest) 173 + val updatedSkeleton = Skeleton( 174 + timestamp = timestamp, 175 + leftShoulder = recognizedPoints?.get( 176 + VNHumanBodyPoseObservationJointNameLeftShoulder 177 + )?.location?.toSkeletonPoint(width, height), 178 + rightShoulder = recognizedPoints?.get( 179 + VNHumanBodyPoseObservationJointNameRightShoulder 180 + )?.location?.toSkeletonPoint(width, height), 181 + leftElbow = recognizedPoints?.get( 182 + VNHumanBodyPoseObservationJointNameLeftElbow 183 + )?.location?.toSkeletonPoint(width, height), 184 + rightElbow = recognizedPoints?.get( 185 + VNHumanBodyPoseObservationJointNameRightElbow 186 + )?.location?.toSkeletonPoint(width, height), 187 + leftWrist = recognizedPoints?.get( 188 + VNHumanBodyPoseObservationJointNameLeftWrist 189 + )?.location?.toSkeletonPoint(width, height), 190 + rightWrist = recognizedPoints?.get( 191 + VNHumanBodyPoseObservationJointNameRightWrist 192 + )?.location?.toSkeletonPoint(width, height), 193 + leftHip = recognizedPoints?.get( 194 + VNHumanBodyPoseObservationJointNameLeftHip 195 + )?.location?.toSkeletonPoint(width, height), 196 + rightHip = recognizedPoints?.get( 197 + VNHumanBodyPoseObservationJointNameRightHip 198 + )?.location?.toSkeletonPoint(width, height), 199 + leftKnee = recognizedPoints?.get( 200 + VNHumanBodyPoseObservationJointNameLeftKnee 201 + )?.location?.toSkeletonPoint(width, height), 202 + rightKnee = recognizedPoints?.get( 203 + VNHumanBodyPoseObservationJointNameRightKnee 204 + )?.location?.toSkeletonPoint(width, height), 205 + leftAnkle = recognizedPoints?.get( 206 + VNHumanBodyPoseObservationJointNameLeftAnkle 207 + )?.location?.toSkeletonPoint(width, height), 208 + rightAnkle = recognizedPoints?.get( 209 + VNHumanBodyPoseObservationJointNameRightAnkle 210 + )?.location?.toSkeletonPoint(width, height) 211 + ) 212 + println("Skeleton: $updatedSkeleton") 213 + skeletonRepository.updateSkeleton( 214 + updatedSkeleton 215 + ) 216 + } 217 + } 218 + } 219 + 220 + try { 221 + requestHandler.performRequests(listOf(request), null) 222 + } catch (e: Exception) { 223 + println("Unable to perform the request: ${e.message}") 224 + } 225 + }
+8
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/FrameCaptureResult.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + import androidx.compose.ui.graphics.ImageBitmap 4 + 5 + sealed class FrameCaptureResult { 6 + data class Success(val imageBitmap: ImageBitmap?) : FrameCaptureResult() 7 + data class Error(val exception: Exception) : FrameCaptureResult() 8 + }
+223
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/MemoryManager.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + import kotlinx.atomicfu.atomic 4 + import platform.Foundation.NSLock 5 + import platform.Foundation.NSNotificationCenter 6 + import platform.Foundation.NSOperationQueue 7 + import platform.Foundation.NSProcessInfo 8 + import platform.UIKit.UIApplicationDidReceiveMemoryWarningNotification 9 + 10 + /** 11 + * Manages memory resources for camera operations 12 + * Monitors memory pressure and optimizes memory usage for image capture operations 13 + */ 14 + object MemoryManager { 15 + 16 + private const val MEMORY_PRESSURE_THRESHOLD = 0.8 17 + 18 + 19 + private val smallBufferLock = NSLock() 20 + private val mediumBufferLock = NSLock() 21 + private val largeBufferLock = NSLock() 22 + 23 + 24 + private val memoryPressure = atomic(false) 25 + 26 + 27 + private var memoryUsage = atomic(0.0) 28 + 29 + 30 + private val smallBufferPool = mutableListOf<ByteArray>() 31 + private val mediumBufferPool = mutableListOf<ByteArray>() 32 + private val largeBufferPool = mutableListOf<ByteArray>() 33 + 34 + /** 35 + * Initialize memory monitoring 36 + */ 37 + fun initialize() { 38 + registerMemoryWarningNotification() 39 + updateMemoryStatus() 40 + } 41 + 42 + /** 43 + * Register for system memory warning notifications 44 + */ 45 + private fun registerMemoryWarningNotification() { 46 + NSNotificationCenter.defaultCenter.addObserverForName( 47 + UIApplicationDidReceiveMemoryWarningNotification, 48 + null, 49 + NSOperationQueue.mainQueue, 50 + { _ -> 51 + memoryPressure.value = true 52 + handleHighMemoryPressure() 53 + } 54 + ) 55 + } 56 + 57 + /** 58 + * Update current memory status 59 + * This should be called periodically, especially before major memory operations 60 + */ 61 + fun updateMemoryStatus() { 62 + val usedMemory = getUsedMemory() 63 + val totalMemory = getTotalMemory() 64 + 65 + if (totalMemory > 0) { 66 + val usage = usedMemory / totalMemory 67 + memoryUsage.value = usage 68 + 69 + 70 + if (usage > MEMORY_PRESSURE_THRESHOLD && !memoryPressure.value) { 71 + memoryPressure.value = true 72 + handleHighMemoryPressure() 73 + } else if (usage <= MEMORY_PRESSURE_THRESHOLD && memoryPressure.value) { 74 + memoryPressure.value = false 75 + } 76 + } 77 + } 78 + 79 + /** 80 + * Handle high memory pressure situation 81 + */ 82 + private fun handleHighMemoryPressure() { 83 + clearBufferPools() 84 + } 85 + 86 + /** 87 + * Clear all buffer pools to free memory 88 + * Should be called when memory pressure is detected 89 + */ 90 + fun clearBufferPools() { 91 + smallBufferLock.lock() 92 + try { 93 + smallBufferPool.clear() 94 + } finally { 95 + smallBufferLock.unlock() 96 + } 97 + 98 + mediumBufferLock.lock() 99 + try { 100 + mediumBufferPool.clear() 101 + } finally { 102 + mediumBufferLock.unlock() 103 + } 104 + 105 + largeBufferLock.lock() 106 + try { 107 + largeBufferPool.clear() 108 + } finally { 109 + largeBufferLock.unlock() 110 + } 111 + } 112 + 113 + /** 114 + * Get buffer from pool or create new one 115 + * Uses sized pools to efficiently reuse memory 116 + * @param size Required buffer size in bytes 117 + * @return ByteArray of at least the requested size 118 + */ 119 + fun getBuffer(size: Int): ByteArray { 120 + return when { 121 + size <= 16 * 1024 -> getFromPool(smallBufferPool, smallBufferLock, size) 122 + size <= 1 * 1024 * 1024 -> getFromPool(mediumBufferPool, mediumBufferLock, size) 123 + else -> getFromPool(largeBufferPool, largeBufferLock, size) 124 + } 125 + } 126 + 127 + /** 128 + * Return buffer to pool when done 129 + * Helps reduce memory allocations and GC pressure 130 + * @param buffer ByteArray to recycle 131 + */ 132 + fun recycleBuffer(buffer: ByteArray) { 133 + when { 134 + buffer.size <= 16 * 1024 -> returnToPool(smallBufferPool, smallBufferLock, buffer) 135 + buffer.size <= 1 * 1024 * 1024 -> returnToPool( 136 + mediumBufferPool, 137 + mediumBufferLock, 138 + buffer 139 + ) 140 + 141 + else -> returnToPool(largeBufferPool, largeBufferLock, buffer) 142 + } 143 + } 144 + 145 + /** 146 + * Helper function to get buffer from a pool 147 + */ 148 + private fun getFromPool(pool: MutableList<ByteArray>, lock: NSLock, size: Int): ByteArray { 149 + lock.lock() 150 + try { 151 + 152 + val index = pool.indexOfFirst { it.size >= size } 153 + 154 + return if (index >= 0) { 155 + 156 + pool.removeAt(index) 157 + } else { 158 + 159 + ByteArray(size) 160 + } 161 + } finally { 162 + lock.unlock() 163 + } 164 + } 165 + 166 + /** 167 + * Helper function to return buffer to a pool 168 + */ 169 + private fun returnToPool(pool: MutableList<ByteArray>, lock: NSLock, buffer: ByteArray) { 170 + 171 + val maxPoolSize = 5 172 + 173 + lock.lock() 174 + try { 175 + if (pool.size < maxPoolSize) { 176 + pool.add(buffer) 177 + } 178 + } finally { 179 + lock.unlock() 180 + } 181 + } 182 + 183 + /** 184 + * Get optimal image quality based on memory conditions 185 + */ 186 + fun getOptimalImageQuality(): Double { 187 + return when { 188 + memoryPressure.value -> 0.6 189 + memoryUsage.value > 0.7 -> 0.75 190 + else -> 0.95 191 + } 192 + } 193 + 194 + /** 195 + * Check if memory is under pressure 196 + */ 197 + fun isUnderMemoryPressure(): Boolean { 198 + return memoryPressure.value 199 + } 200 + 201 + /** 202 + * Get memory usage as a percentage 203 + */ 204 + fun getMemoryUsagePercentage(): Double { 205 + return memoryUsage.value * 100 206 + } 207 + 208 + /** 209 + * Get used memory in bytes - uses physical footprint for accurate measurement 210 + */ 211 + private fun getUsedMemory(): Double { 212 + 213 + return NSProcessInfo.processInfo.physicalMemory.toDouble() 214 + } 215 + 216 + /** 217 + * Get total available memory in bytes 218 + */ 219 + private fun getTotalMemory(): Double { 220 + 221 + return NSProcessInfo.processInfo.physicalMemory.toDouble() 222 + } 223 + }
+40
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/permissions/PermissionProvider.ios.kt
··· 1 + package com.performancecoachlab.posedetection.permissions 2 + 3 + import androidx.compose.runtime.Composable 4 + import androidx.compose.runtime.remember 5 + import platform.AVFoundation.AVAuthorizationStatusAuthorized 6 + import platform.AVFoundation.AVCaptureDevice 7 + import platform.AVFoundation.AVMediaTypeVideo 8 + import platform.AVFoundation.authorizationStatusForMediaType 9 + import platform.AVFoundation.requestAccessForMediaType 10 + 11 + 12 + @Composable 13 + actual fun PermissionProvider(): Permissions { 14 + return rememberIOSPermissions() 15 + } 16 + 17 + @Composable 18 + fun rememberIOSPermissions(): Permissions { 19 + return remember { 20 + object : Permissions { 21 + override fun hasCameraPermission(): Boolean { 22 + val status = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo) 23 + return status == AVAuthorizationStatusAuthorized 24 + } 25 + 26 + @Composable 27 + override fun RequestCameraPermission(onGranted: () -> Unit, onDenied: () -> Unit) { 28 + AVCaptureDevice.requestAccessForMediaType( 29 + AVMediaTypeVideo 30 + ) { granted -> 31 + if (granted) { 32 + onGranted() 33 + } else { 34 + onDenied() 35 + } 36 + } 37 + } 38 + } 39 + } 40 + }
+72
sample/composeApp/build.gradle.kts
··· 1 + import org.jetbrains.compose.ExperimentalComposeLibrary 2 + import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree 3 + 4 + plugins { 5 + alias(libs.plugins.multiplatform) 6 + alias(libs.plugins.compose.compiler) 7 + alias(libs.plugins.compose) 8 + alias(libs.plugins.android.application) 9 + } 10 + 11 + kotlin { 12 + androidTarget { 13 + //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html 14 + instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test) 15 + } 16 + 17 + listOf( 18 + iosX64(), 19 + iosArm64(), 20 + iosSimulatorArm64() 21 + ).forEach { 22 + it.binaries.framework { 23 + baseName = "ComposeApp" 24 + isStatic = true 25 + } 26 + } 27 + 28 + sourceSets { 29 + commonMain.dependencies { 30 + implementation(compose.runtime) 31 + implementation(compose.foundation) 32 + implementation(compose.material3) 33 + implementation(compose.components.resources) 34 + implementation(compose.components.uiToolingPreview) 35 + implementation(project(":posedetection")) 36 + } 37 + 38 + commonTest.dependencies { 39 + implementation(kotlin("test")) 40 + @OptIn(ExperimentalComposeLibrary::class) 41 + implementation(compose.uiTest) 42 + } 43 + 44 + androidMain.dependencies { 45 + implementation(compose.uiTooling) 46 + implementation(libs.androidx.activityCompose) 47 + } 48 + 49 + } 50 + } 51 + 52 + android { 53 + namespace = "com.nate.posedetection" 54 + compileSdk = 35 55 + 56 + defaultConfig { 57 + minSdk = 21 58 + targetSdk = 35 59 + 60 + applicationId = "com.nate.posedetection.androidApp" 61 + versionCode = 1 62 + versionName = "1.0.0" 63 + 64 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 65 + } 66 + } 67 + 68 + //https://developer.android.com/develop/ui/compose/testing#setup 69 + dependencies { 70 + androidTestImplementation(libs.androidx.uitest.junit4) 71 + debugImplementation(libs.androidx.uitest.testManifest) 72 + }
+25
sample/composeApp/src/androidMain/AndroidManifest.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> 3 + 4 + <uses-feature 5 + android:name="android.hardware.camera" 6 + android:required="false" /> 7 + <uses-permission android:name="android.permission.CAMERA" /> 8 + <application 9 + android:icon="@mipmap/ic_launcher" 10 + android:label="PoseDetection" 11 + android:theme="@android:style/Theme.Material.NoActionBar"> 12 + <activity 13 + android:name=".AppActivity" 14 + android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" 15 + android:launchMode="singleInstance" 16 + android:windowSoftInputMode="adjustPan" 17 + android:exported="true"> 18 + <intent-filter> 19 + <action android:name="android.intent.action.MAIN" /> 20 + <category android:name="android.intent.category.LAUNCHER" /> 21 + </intent-filter> 22 + </activity> 23 + </application> 24 + 25 + </manifest>
+20
sample/composeApp/src/androidMain/kotlin/com/nate/posedetection/App.android.kt
··· 1 + package com.nate.posedetection 2 + 3 + import android.os.Bundle 4 + import androidx.activity.ComponentActivity 5 + import androidx.activity.compose.setContent 6 + import androidx.activity.enableEdgeToEdge 7 + import androidx.compose.runtime.Composable 8 + import androidx.compose.ui.tooling.preview.Preview 9 + 10 + class AppActivity : ComponentActivity() { 11 + override fun onCreate(savedInstanceState: Bundle?) { 12 + super.onCreate(savedInstanceState) 13 + enableEdgeToEdge() 14 + setContent { App() } 15 + } 16 + } 17 + 18 + @Preview 19 + @Composable 20 + fun AppPreview() { App() }
+19
sample/composeApp/src/androidMain/kotlin/com/nate/posedetection/theme/Theme.android.kt
··· 1 + package com.nate.posedetection.theme 2 + 3 + import android.app.Activity 4 + import androidx.compose.runtime.Composable 5 + import androidx.compose.runtime.LaunchedEffect 6 + import androidx.compose.ui.platform.LocalView 7 + import androidx.core.view.WindowInsetsControllerCompat 8 + 9 + @Composable 10 + internal actual fun SystemAppearance(isDark: Boolean) { 11 + val view = LocalView.current 12 + LaunchedEffect(isDark) { 13 + val window = (view.context as Activity).window 14 + WindowInsetsControllerCompat(window, window.decorView).apply { 15 + isAppearanceLightStatusBars = isDark 16 + isAppearanceLightNavigationBars = isDark 17 + } 18 + } 19 + }
+6
sample/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 3 + <background android:drawable="@mipmap/ic_launcher_background"/> 4 + <foreground android:drawable="@mipmap/ic_launcher_foreground"/> 5 + <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/> 6 + </adaptive-icon>
sample/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.png

This is a binary file and will not be displayed.

+12
sample/composeApp/src/commonMain/composeResources/drawable/ic_cyclone.xml
··· 1 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 + android:width="24dp" 3 + android:height="24dp" 4 + android:viewportWidth="24" 5 + android:viewportHeight="24"> 6 + <path 7 + android:fillColor="#000000" 8 + android:pathData="M12,8c-2.21,0 -4,1.79 -4,4c0,2.21 1.79,4 4,4c2.21,0 4,-1.79 4,-4C16,9.79 14.21,8 12,8zM12,14c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C14,13.1 13.1,14 12,14z" /> 9 + <path 10 + android:fillColor="#000000" 11 + android:pathData="M22,7.47V5.35C20.05,4.77 16.56,4 12,4C9.85,4 7.89,4.86 6.46,6.24C6.59,5.39 6.86,3.84 7.47,2H5.35C4.77,3.95 4,7.44 4,12c0,2.15 0.86,4.11 2.24,5.54c-0.85,-0.14 -2.4,-0.4 -4.24,-1.01v2.12C3.95,19.23 7.44,20 12,20c2.15,0 4.11,-0.86 5.54,-2.24c-0.14,0.85 -0.4,2.4 -1.01,4.24h2.12C19.23,20.05 20,16.56 20,12c0,-2.15 -0.86,-4.11 -2.24,-5.54C18.61,6.59 20.16,6.86 22,7.47zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6s6,2.69 6,6S15.31,18 12,18z" /> 12 + </vector>
+9
sample/composeApp/src/commonMain/composeResources/drawable/ic_dark_mode.xml
··· 1 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 + android:width="24dp" 3 + android:height="24dp" 4 + android:viewportWidth="24" 5 + android:viewportHeight="24"> 6 + <path 7 + android:fillColor="#000000" 8 + android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9c0,-0.46 -0.04,-0.92 -0.1,-1.36c-0.98,1.37 -2.58,2.26 -4.4,2.26c-2.98,0 -5.4,-2.42 -5.4,-5.4c0,-1.81 0.89,-3.42 2.26,-4.4C12.92,3.04 12.46,3 12,3L12,3z" /> 9 + </vector>
+9
sample/composeApp/src/commonMain/composeResources/drawable/ic_light_mode.xml
··· 1 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 + android:width="24dp" 3 + android:height="24dp" 4 + android:viewportWidth="24" 5 + android:viewportHeight="24"> 6 + <path 7 + android:fillColor="#000000" 8 + android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S14.76,7 12,7L12,7zM2,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S1.45,13 2,13zM20,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S19.45,13 20,13zM11,2v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1V2c0,-0.55 -0.45,-1 -1,-1S11,1.45 11,2zM11,20v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1C11.45,19 11,19.45 11,20zM5.99,4.58c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0s0.39,-1.03 0,-1.41L5.99,4.58zM18.36,16.95c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0c0.39,-0.39 0.39,-1.03 0,-1.41L18.36,16.95zM19.42,5.99c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L19.42,5.99zM7.05,18.36c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L7.05,18.36z" /> 9 + </vector>
+10
sample/composeApp/src/commonMain/composeResources/drawable/ic_rotate_right.xml
··· 1 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 + android:width="24dp" 3 + android:height="24dp" 4 + android:autoMirrored="true" 5 + android:viewportWidth="24" 6 + android:viewportHeight="24"> 7 + <path 8 + android:fillColor="#000000" 9 + android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z" /> 10 + </vector>
sample/composeApp/src/commonMain/composeResources/font/IndieFlower-Regular.ttf

This is a binary file and will not be displayed.

+7
sample/composeApp/src/commonMain/composeResources/values/strings.xml
··· 1 + <resources> 2 + <string name="cyclone">Cyclone</string> 3 + <string name="open_github">Open github</string> 4 + <string name="run">Run</string> 5 + <string name="stop">Stop</string> 6 + <string name="theme">Theme</string> 7 + </resources>
+52
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/App.kt
··· 1 + package com.nate.posedetection 2 + 3 + import androidx.compose.foundation.layout.* 4 + import androidx.compose.material3.* 5 + import androidx.compose.runtime.* 6 + import androidx.compose.ui.Modifier 7 + import androidx.compose.ui.text.TextStyle 8 + import androidx.compose.ui.unit.TextUnit 9 + import androidx.compose.ui.unit.sp 10 + import com.performancecoachlab.posedetection.permissions.PermissionProvider 11 + import com.nate.posedetection.theme.AppTheme 12 + import com.performancecoachlab.posedetection.skeleton.SkeletonRepository 13 + import com.performancecoachlab.posedetection.camera.CameraView 14 + import com.performancecoachlab.posedetection.skeleton.Pose 15 + 16 + @Composable 17 + internal fun App() = AppTheme { 18 + val skeletonRepository = remember { SkeletonRepository() } 19 + var permissionGranted by remember { mutableStateOf(false) } 20 + val skeleton by skeletonRepository.skeletonFlow.collectAsState() 21 + PermissionProvider().apply { 22 + if (!hasCameraPermission()) RequestCameraPermission(onGranted = { 23 + permissionGranted = true 24 + }, onDenied = { permissionGranted = false }) else permissionGranted = true 25 + } 26 + 27 + if (permissionGranted) { 28 + CameraView( 29 + skeletonRepository = skeletonRepository, 30 + drawSkeleton = true, 31 + modifier = Modifier.fillMaxSize() 32 + ) 33 + } else { 34 + Text("Camera permission not granted") 35 + } 36 + 37 + val upRightPose = Pose( 38 + leftShoulder = Pose.PoseRange(0.0, 40.0), 39 + rightShoulder = Pose.PoseRange(0.0, 40.0), 40 + leftHip = Pose.PoseRange(160.0, 180.0), 41 + rightHip = Pose.PoseRange(160.0, 180.0), 42 + leftKnee = Pose.PoseRange(160.0, 180.0), 43 + rightKnee = Pose.PoseRange(160.0, 180.0) 44 + ) 45 + skeleton?.let { 46 + if (upRightPose.matches(it)) { 47 + Text("Standing", fontSize = 80.sp) 48 + } else { 49 + Text("No Pose" , fontSize = 80.sp) 50 + } 51 + } 52 + }
+80
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/theme/Color.kt
··· 1 + package com.nate.posedetection.theme 2 + 3 + import androidx.compose.ui.graphics.Color 4 + 5 + //generated by https://materialkolor.com 6 + //Color palette was taken here: https://coolors.co/palette/e63946-f1faee-a8dadc-457b9d-1d3557 7 + 8 + internal val Seed = Color(0xFF1D3557) 9 + 10 + internal val PrimaryLight = Color(0xFF485F84) 11 + internal val OnPrimaryLight = Color(0xFFFFFFFF) 12 + internal val PrimaryContainerLight = Color(0xFFD5E3FF) 13 + internal val OnPrimaryContainerLight = Color(0xFF30476A) 14 + internal val SecondaryLight = Color(0xFF2B6485) 15 + internal val OnSecondaryLight = Color(0xFFFFFFFF) 16 + internal val SecondaryContainerLight = Color(0xFFC7E7FF) 17 + internal val OnSecondaryContainerLight = Color(0xFF064C6B) 18 + internal val TertiaryLight = Color(0xFF356668) 19 + internal val OnTertiaryLight = Color(0xFFFFFFFF) 20 + internal val TertiaryContainerLight = Color(0xFFB9ECEE) 21 + internal val OnTertiaryContainerLight = Color(0xFF1A4E50) 22 + internal val ErrorLight = Color(0xFFBB152C) 23 + internal val OnErrorLight = Color(0xFFFFFFFF) 24 + internal val ErrorContainerLight = Color(0xFFFFDAD8) 25 + internal val OnErrorContainerLight = Color(0xFF410007) 26 + internal val BackgroundLight = Color(0xFFF9F9F9) 27 + internal val OnBackgroundLight = Color(0xFF1A1C1C) 28 + internal val SurfaceLight = Color(0xFFF9F9F9) 29 + internal val OnSurfaceLight = Color(0xFF1A1C1C) 30 + internal val SurfaceVariantLight = Color(0xFFDCE5D9) 31 + internal val OnSurfaceVariantLight = Color(0xFF404941) 32 + internal val OutlineLight = Color(0xFF717970) 33 + internal val OutlineVariantLight = Color(0xFFC0C9BE) 34 + internal val ScrimLight = Color(0xFF000000) 35 + internal val InverseSurfaceLight = Color(0xFF2F3131) 36 + internal val InverseOnSurfaceLight = Color(0xFFF0F1F1) 37 + internal val InversePrimaryLight = Color(0xFFB0C7F1) 38 + internal val SurfaceDimLight = Color(0xFFDADADA) 39 + internal val SurfaceBrightLight = Color(0xFFF9F9F9) 40 + internal val SurfaceContainerLowestLight = Color(0xFFFFFFFF) 41 + internal val SurfaceContainerLowLight = Color(0xFFF3F3F4) 42 + internal val SurfaceContainerLight = Color(0xFFEEEEEE) 43 + internal val SurfaceContainerHighLight = Color(0xFFE8E8E8) 44 + internal val SurfaceContainerHighestLight = Color(0xFFE2E2E2) 45 + 46 + internal val PrimaryDark = Color(0xFFB0C7F1) 47 + internal val OnPrimaryDark = Color(0xFF183153) 48 + internal val PrimaryContainerDark = Color(0xFF30476A) 49 + internal val OnPrimaryContainerDark = Color(0xFFD5E3FF) 50 + internal val SecondaryDark = Color(0xFF98CDF2) 51 + internal val OnSecondaryDark = Color(0xFF00344C) 52 + internal val SecondaryContainerDark = Color(0xFF064C6B) 53 + internal val OnSecondaryContainerDark = Color(0xFFC7E7FF) 54 + internal val TertiaryDark = Color(0xFF9ECFD1) 55 + internal val OnTertiaryDark = Color(0xFF003739) 56 + internal val TertiaryContainerDark = Color(0xFF1A4E50) 57 + internal val OnTertiaryContainerDark = Color(0xFFB9ECEE) 58 + internal val ErrorDark = Color(0xFFFFB3B1) 59 + internal val OnErrorDark = Color(0xFF680011) 60 + internal val ErrorContainerDark = Color(0xFF92001C) 61 + internal val OnErrorContainerDark = Color(0xFFFFDAD8) 62 + internal val BackgroundDark = Color(0xFF121414) 63 + internal val OnBackgroundDark = Color(0xFFE2E2E2) 64 + internal val SurfaceDark = Color(0xFF121414) 65 + internal val OnSurfaceDark = Color(0xFFE2E2E2) 66 + internal val SurfaceVariantDark = Color(0xFF404941) 67 + internal val OnSurfaceVariantDark = Color(0xFFC0C9BE) 68 + internal val OutlineDark = Color(0xFF8A9389) 69 + internal val OutlineVariantDark = Color(0xFF404941) 70 + internal val ScrimDark = Color(0xFF000000) 71 + internal val InverseSurfaceDark = Color(0xFFE2E2E2) 72 + internal val InverseOnSurfaceDark = Color(0xFF2F3131) 73 + internal val InversePrimaryDark = Color(0xFF485F84) 74 + internal val SurfaceDimDark = Color(0xFF121414) 75 + internal val SurfaceBrightDark = Color(0xFF37393A) 76 + internal val SurfaceContainerLowestDark = Color(0xFF0C0F0F) 77 + internal val SurfaceContainerLowDark = Color(0xFF1A1C1C) 78 + internal val SurfaceContainerDark = Color(0xFF1E2020) 79 + internal val SurfaceContainerHighDark = Color(0xFF282A2B) 80 + internal val SurfaceContainerHighestDark = Color(0xFF333535)
+107
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/theme/Theme.kt
··· 1 + package com.nate.posedetection.theme 2 + 3 + import androidx.compose.foundation.isSystemInDarkTheme 4 + import androidx.compose.material3.MaterialTheme 5 + import androidx.compose.material3.Surface 6 + import androidx.compose.material3.darkColorScheme 7 + import androidx.compose.material3.lightColorScheme 8 + import androidx.compose.runtime.* 9 + 10 + private val LightColorScheme = lightColorScheme( 11 + primary = PrimaryLight, 12 + onPrimary = OnPrimaryLight, 13 + primaryContainer = PrimaryContainerLight, 14 + onPrimaryContainer = OnPrimaryContainerLight, 15 + secondary = SecondaryLight, 16 + onSecondary = OnSecondaryLight, 17 + secondaryContainer = SecondaryContainerLight, 18 + onSecondaryContainer = OnSecondaryContainerLight, 19 + tertiary = TertiaryLight, 20 + onTertiary = OnTertiaryLight, 21 + tertiaryContainer = TertiaryContainerLight, 22 + onTertiaryContainer = OnTertiaryContainerLight, 23 + error = ErrorLight, 24 + onError = OnErrorLight, 25 + errorContainer = ErrorContainerLight, 26 + onErrorContainer = OnErrorContainerLight, 27 + background = BackgroundLight, 28 + onBackground = OnBackgroundLight, 29 + surface = SurfaceLight, 30 + onSurface = OnSurfaceLight, 31 + surfaceVariant = SurfaceVariantLight, 32 + onSurfaceVariant = OnSurfaceVariantLight, 33 + outline = OutlineLight, 34 + outlineVariant = OutlineVariantLight, 35 + scrim = ScrimLight, 36 + inverseSurface = InverseSurfaceLight, 37 + inverseOnSurface = InverseOnSurfaceLight, 38 + inversePrimary = InversePrimaryLight, 39 + surfaceDim = SurfaceDimLight, 40 + surfaceBright = SurfaceBrightLight, 41 + surfaceContainerLowest = SurfaceContainerLowestLight, 42 + surfaceContainerLow = SurfaceContainerLowLight, 43 + surfaceContainer = SurfaceContainerLight, 44 + surfaceContainerHigh = SurfaceContainerHighLight, 45 + surfaceContainerHighest = SurfaceContainerHighestLight, 46 + ) 47 + 48 + private val DarkColorScheme = darkColorScheme( 49 + primary = PrimaryDark, 50 + onPrimary = OnPrimaryDark, 51 + primaryContainer = PrimaryContainerDark, 52 + onPrimaryContainer = OnPrimaryContainerDark, 53 + secondary = SecondaryDark, 54 + onSecondary = OnSecondaryDark, 55 + secondaryContainer = SecondaryContainerDark, 56 + onSecondaryContainer = OnSecondaryContainerDark, 57 + tertiary = TertiaryDark, 58 + onTertiary = OnTertiaryDark, 59 + tertiaryContainer = TertiaryContainerDark, 60 + onTertiaryContainer = OnTertiaryContainerDark, 61 + error = ErrorDark, 62 + onError = OnErrorDark, 63 + errorContainer = ErrorContainerDark, 64 + onErrorContainer = OnErrorContainerDark, 65 + background = BackgroundDark, 66 + onBackground = OnBackgroundDark, 67 + surface = SurfaceDark, 68 + onSurface = OnSurfaceDark, 69 + surfaceVariant = SurfaceVariantDark, 70 + onSurfaceVariant = OnSurfaceVariantDark, 71 + outline = OutlineDark, 72 + outlineVariant = OutlineVariantDark, 73 + scrim = ScrimDark, 74 + inverseSurface = InverseSurfaceDark, 75 + inverseOnSurface = InverseOnSurfaceDark, 76 + inversePrimary = InversePrimaryDark, 77 + surfaceDim = SurfaceDimDark, 78 + surfaceBright = SurfaceBrightDark, 79 + surfaceContainerLowest = SurfaceContainerLowestDark, 80 + surfaceContainerLow = SurfaceContainerLowDark, 81 + surfaceContainer = SurfaceContainerDark, 82 + surfaceContainerHigh = SurfaceContainerHighDark, 83 + surfaceContainerHighest = SurfaceContainerHighestDark, 84 + ) 85 + 86 + internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } 87 + 88 + @Composable 89 + internal fun AppTheme( 90 + content: @Composable () -> Unit 91 + ) { 92 + val systemIsDark = isSystemInDarkTheme() 93 + val isDarkState = remember(systemIsDark) { mutableStateOf(systemIsDark) } 94 + CompositionLocalProvider( 95 + LocalThemeIsDark provides isDarkState 96 + ) { 97 + val isDark by isDarkState 98 + SystemAppearance(!isDark) 99 + MaterialTheme( 100 + colorScheme = if (isDark) DarkColorScheme else LightColorScheme, 101 + content = { Surface(content = content) } 102 + ) 103 + } 104 + } 105 + 106 + @Composable 107 + internal expect fun SystemAppearance(isDark: Boolean)
+45
sample/composeApp/src/commonTest/kotlin/com/nate/posedetection/ComposeTest.kt
··· 1 + package com.nate.posedetection 2 + 3 + import androidx.compose.foundation.layout.Column 4 + import androidx.compose.material3.Button 5 + import androidx.compose.material3.Text 6 + import androidx.compose.runtime.getValue 7 + import androidx.compose.runtime.mutableStateOf 8 + import androidx.compose.runtime.remember 9 + import androidx.compose.runtime.setValue 10 + import androidx.compose.ui.Modifier 11 + import androidx.compose.ui.platform.testTag 12 + import androidx.compose.ui.test.ExperimentalTestApi 13 + import androidx.compose.ui.test.assertTextEquals 14 + import androidx.compose.ui.test.onNodeWithTag 15 + import androidx.compose.ui.test.performClick 16 + import androidx.compose.ui.test.runComposeUiTest 17 + import kotlin.test.Test 18 + 19 + @OptIn(ExperimentalTestApi::class) 20 + class ComposeTest { 21 + 22 + @Test 23 + fun simpleCheck() = runComposeUiTest { 24 + setContent { 25 + var txt by remember { mutableStateOf("Go") } 26 + Column { 27 + Text( 28 + text = txt, 29 + modifier = Modifier.testTag("t_text") 30 + ) 31 + Button( 32 + onClick = { txt += "." }, 33 + modifier = Modifier.testTag("t_button") 34 + ) { 35 + Text("click me") 36 + } 37 + } 38 + } 39 + 40 + onNodeWithTag("t_button").apply { 41 + repeat(3) { performClick() } 42 + } 43 + onNodeWithTag("t_text").assertTextEquals("Go...") 44 + } 45 + }
+17
sample/composeApp/src/iosMain/kotlin/com/nate/posedetection/theme/Theme.ios.kt
··· 1 + package com.nate.posedetection.theme 2 + 3 + import androidx.compose.runtime.Composable 4 + import androidx.compose.runtime.LaunchedEffect 5 + import platform.UIKit.UIApplication 6 + import platform.UIKit.UIStatusBarStyleDarkContent 7 + import platform.UIKit.UIStatusBarStyleLightContent 8 + import platform.UIKit.setStatusBarStyle 9 + 10 + @Composable 11 + internal actual fun SystemAppearance(isDark: Boolean) { 12 + LaunchedEffect(isDark) { 13 + UIApplication.sharedApplication.setStatusBarStyle( 14 + if (isDark) UIStatusBarStyleDarkContent else UIStatusBarStyleLightContent 15 + ) 16 + } 17 + }
+5
sample/composeApp/src/iosMain/kotlin/main.kt
··· 1 + import androidx.compose.ui.window.ComposeUIViewController 2 + import com.nate.posedetection.App 3 + import platform.UIKit.UIViewController 4 + 5 + fun MainViewController(): UIViewController = ComposeUIViewController { App() }
+14
sample/gradle.properties
··· 1 + #Gradle 2 + org.gradle.jvmargs=-Xmx4G 3 + org.gradle.caching=true 4 + org.gradle.configuration-cache=true 5 + org.gradle.daemon=true 6 + org.gradle.parallel=true 7 + 8 + #Kotlin 9 + kotlin.code.style=official 10 + kotlin.daemon.jvmargs=-Xmx4G 11 + 12 + #Android 13 + android.useAndroidX=true 14 + android.nonTransitiveRClass=true
+21
sample/gradle/libs.versions.toml
··· 1 + [versions] 2 + 3 + kotlin = "2.1.20" 4 + compose = "1.8.0-beta01" 5 + agp = "8.6.1" 6 + androidx-activityCompose = "1.10.1" 7 + androidx-uiTest = "1.7.8" 8 + 9 + [libraries] 10 + 11 + androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 12 + androidx-uitest-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" } 13 + androidx-uitest-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" } 14 + 15 + [plugins] 16 + 17 + multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 18 + compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 19 + compose = { id = "org.jetbrains.compose", version.ref = "compose" } 20 + android-application = { id = "com.android.application", version.ref = "agp" } 21 + android-library = { id = "com.android.library", version.ref = "agp" }
sample/gradle/wrapper/gradle-wrapper.jar

This is a binary file and will not be displayed.

+7
sample/gradle/wrapper/gradle-wrapper.properties
··· 1 + distributionBase=GRADLE_USER_HOME 2 + distributionPath=wrapper/dists 3 + distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 + networkTimeout=10000 5 + validateDistributionUrl=true 6 + zipStoreBase=GRADLE_USER_HOME 7 + zipStorePath=wrapper/dists
+249
sample/gradlew
··· 1 + #!/bin/sh 2 + 3 + # 4 + # Copyright © 2015-2021 the original authors. 5 + # 6 + # Licensed under the Apache License, Version 2.0 (the "License"); 7 + # you may not use this file except in compliance with the License. 8 + # You may obtain a copy of the License at 9 + # 10 + # https://www.apache.org/licenses/LICENSE-2.0 11 + # 12 + # Unless required by applicable law or agreed to in writing, software 13 + # distributed under the License is distributed on an "AS IS" BASIS, 14 + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + # See the License for the specific language governing permissions and 16 + # limitations under the License. 17 + # 18 + 19 + ############################################################################## 20 + # 21 + # Gradle start up script for POSIX generated by Gradle. 22 + # 23 + # Important for running: 24 + # 25 + # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 + # noncompliant, but you have some other compliant shell such as ksh or 27 + # bash, then to run this script, type that shell name before the whole 28 + # command line, like: 29 + # 30 + # ksh Gradle 31 + # 32 + # Busybox and similar reduced shells will NOT work, because this script 33 + # requires all of these POSIX shell features: 34 + # * functions; 35 + # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 + # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 + # * compound commands having a testable exit status, especially «case»; 38 + # * various built-in commands including «command», «set», and «ulimit». 39 + # 40 + # Important for patching: 41 + # 42 + # (2) This script targets any POSIX shell, so it avoids extensions provided 43 + # by Bash, Ksh, etc; in particular arrays are avoided. 44 + # 45 + # The "traditional" practice of packing multiple parameters into a 46 + # space-separated string is a well documented source of bugs and security 47 + # problems, so this is (mostly) avoided, by progressively accumulating 48 + # options in "$@", and eventually passing that to Java. 49 + # 50 + # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 + # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 + # see the in-line comments for details. 53 + # 54 + # There are tweaks for specific operating systems such as AIX, CygWin, 55 + # Darwin, MinGW, and NonStop. 56 + # 57 + # (3) This script is generated from the Groovy template 58 + # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 + # within the Gradle project. 60 + # 61 + # You can find Gradle at https://github.com/gradle/gradle/. 62 + # 63 + ############################################################################## 64 + 65 + # Attempt to set APP_HOME 66 + 67 + # Resolve links: $0 may be a link 68 + app_path=$0 69 + 70 + # Need this for daisy-chained symlinks. 71 + while 72 + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 + [ -h "$app_path" ] 74 + do 75 + ls=$( ls -ld "$app_path" ) 76 + link=${ls#*' -> '} 77 + case $link in #( 78 + /*) app_path=$link ;; #( 79 + *) app_path=$APP_HOME$link ;; 80 + esac 81 + done 82 + 83 + # This is normally unused 84 + # shellcheck disable=SC2034 85 + APP_BASE_NAME=${0##*/} 86 + # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 + APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 + 89 + # Use the maximum available, or set MAX_FD != -1 to use that value. 90 + MAX_FD=maximum 91 + 92 + warn () { 93 + echo "$*" 94 + } >&2 95 + 96 + die () { 97 + echo 98 + echo "$*" 99 + echo 100 + exit 1 101 + } >&2 102 + 103 + # OS specific support (must be 'true' or 'false'). 104 + cygwin=false 105 + msys=false 106 + darwin=false 107 + nonstop=false 108 + case "$( uname )" in #( 109 + CYGWIN* ) cygwin=true ;; #( 110 + Darwin* ) darwin=true ;; #( 111 + MSYS* | MINGW* ) msys=true ;; #( 112 + NONSTOP* ) nonstop=true ;; 113 + esac 114 + 115 + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 + 117 + 118 + # Determine the Java command to use to start the JVM. 119 + if [ -n "$JAVA_HOME" ] ; then 120 + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 + # IBM's JDK on AIX uses strange locations for the executables 122 + JAVACMD=$JAVA_HOME/jre/sh/java 123 + else 124 + JAVACMD=$JAVA_HOME/bin/java 125 + fi 126 + if [ ! -x "$JAVACMD" ] ; then 127 + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 + 129 + Please set the JAVA_HOME variable in your environment to match the 130 + location of your Java installation." 131 + fi 132 + else 133 + JAVACMD=java 134 + if ! command -v java >/dev/null 2>&1 135 + then 136 + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 + 138 + Please set the JAVA_HOME variable in your environment to match the 139 + location of your Java installation." 140 + fi 141 + fi 142 + 143 + # Increase the maximum file descriptors if we can. 144 + if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 + case $MAX_FD in #( 146 + max*) 147 + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 + # shellcheck disable=SC2039,SC3045 149 + MAX_FD=$( ulimit -H -n ) || 150 + warn "Could not query maximum file descriptor limit" 151 + esac 152 + case $MAX_FD in #( 153 + '' | soft) :;; #( 154 + *) 155 + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 + # shellcheck disable=SC2039,SC3045 157 + ulimit -n "$MAX_FD" || 158 + warn "Could not set maximum file descriptor limit to $MAX_FD" 159 + esac 160 + fi 161 + 162 + # Collect all arguments for the java command, stacking in reverse order: 163 + # * args from the command line 164 + # * the main class name 165 + # * -classpath 166 + # * -D...appname settings 167 + # * --module-path (only if needed) 168 + # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 + 170 + # For Cygwin or MSYS, switch paths to Windows format before running java 171 + if "$cygwin" || "$msys" ; then 172 + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 + 175 + JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 + 177 + # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 + for arg do 179 + if 180 + case $arg in #( 181 + -*) false ;; # don't mess with options #( 182 + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 + [ -e "$t" ] ;; #( 184 + *) false ;; 185 + esac 186 + then 187 + arg=$( cygpath --path --ignore --mixed "$arg" ) 188 + fi 189 + # Roll the args list around exactly as many times as the number of 190 + # args, so each arg winds up back in the position where it started, but 191 + # possibly modified. 192 + # 193 + # NB: a `for` loop captures its iteration list before it begins, so 194 + # changing the positional parameters here affects neither the number of 195 + # iterations, nor the values presented in `arg`. 196 + shift # remove old arg 197 + set -- "$@" "$arg" # push replacement arg 198 + done 199 + fi 200 + 201 + 202 + # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 + DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 + 205 + # Collect all arguments for the java command: 206 + # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 + # and any embedded shellness will be escaped. 208 + # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 + # treated as '${Hostname}' itself on the command line. 210 + 211 + set -- \ 212 + "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 + -classpath "$CLASSPATH" \ 214 + org.gradle.wrapper.GradleWrapperMain \ 215 + "$@" 216 + 217 + # Stop when "xargs" is not available. 218 + if ! command -v xargs >/dev/null 2>&1 219 + then 220 + die "xargs is not available" 221 + fi 222 + 223 + # Use "xargs" to parse quoted args. 224 + # 225 + # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 + # 227 + # In Bash we could simply go: 228 + # 229 + # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 + # set -- "${ARGS[@]}" "$@" 231 + # 232 + # but POSIX shell has neither arrays nor command substitution, so instead we 233 + # post-process each arg (as a line of input to sed) to backslash-escape any 234 + # character that might be a shell metacharacter, then use eval to reverse 235 + # that process (while maintaining the separation between arguments), and wrap 236 + # the whole thing up as a single "set" statement. 237 + # 238 + # This will of course break if any of these variables contains a newline or 239 + # an unmatched quote. 240 + # 241 + 242 + eval "set -- $( 243 + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 + xargs -n1 | 245 + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 + tr '\n' ' ' 247 + )" '"$@"' 248 + 249 + exec "$JAVACMD" "$@"
+365
sample/iosApp/iosApp.xcodeproj/project.pbxproj
··· 1 + // !$*UTF8*$! 2 + { 3 + archiveVersion = 1; 4 + classes = { 5 + }; 6 + objectVersion = 56; 7 + objects = { 8 + 9 + /* Begin PBXBuildFile section */ 10 + A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A953A29CC810C00F8E227 /* iosApp.swift */; }; 11 + A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A953E29CC810D00F8E227 /* Assets.xcassets */; }; 12 + A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A954129CC810D00F8E227 /* Preview Assets.xcassets */; }; 13 + /* End PBXBuildFile section */ 14 + 15 + /* Begin PBXFileReference section */ 16 + A93A953729CC810C00F8E227 /* PoseDetection.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PoseDetection.app; sourceTree = BUILT_PRODUCTS_DIR; }; 17 + A93A953A29CC810C00F8E227 /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = "<group>"; }; 18 + A93A953E29CC810D00F8E227 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 19 + A93A954129CC810D00F8E227 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 20 + /* End PBXFileReference section */ 21 + 22 + /* Begin PBXFrameworksBuildPhase section */ 23 + A93A953429CC810C00F8E227 /* Frameworks */ = { 24 + isa = PBXFrameworksBuildPhase; 25 + buildActionMask = 2147483647; 26 + files = ( 27 + ); 28 + runOnlyForDeploymentPostprocessing = 0; 29 + }; 30 + /* End PBXFrameworksBuildPhase section */ 31 + 32 + /* Begin PBXGroup section */ 33 + A93A952E29CC810C00F8E227 = { 34 + isa = PBXGroup; 35 + children = ( 36 + A93A953929CC810C00F8E227 /* iosApp */, 37 + A93A953829CC810C00F8E227 /* Products */, 38 + C4127409AE3703430489E7BC /* Frameworks */, 39 + ); 40 + sourceTree = "<group>"; 41 + }; 42 + A93A953829CC810C00F8E227 /* Products */ = { 43 + isa = PBXGroup; 44 + children = ( 45 + A93A953729CC810C00F8E227 /* PoseDetection.app */, 46 + ); 47 + name = Products; 48 + sourceTree = "<group>"; 49 + }; 50 + A93A953929CC810C00F8E227 /* iosApp */ = { 51 + isa = PBXGroup; 52 + children = ( 53 + A93A953A29CC810C00F8E227 /* iosApp.swift */, 54 + A93A953E29CC810D00F8E227 /* Assets.xcassets */, 55 + A93A954029CC810D00F8E227 /* Preview Content */, 56 + ); 57 + path = iosApp; 58 + sourceTree = "<group>"; 59 + }; 60 + A93A954029CC810D00F8E227 /* Preview Content */ = { 61 + isa = PBXGroup; 62 + children = ( 63 + A93A954129CC810D00F8E227 /* Preview Assets.xcassets */, 64 + ); 65 + path = "Preview Content"; 66 + sourceTree = "<group>"; 67 + }; 68 + C4127409AE3703430489E7BC /* Frameworks */ = { 69 + isa = PBXGroup; 70 + children = ( 71 + ); 72 + name = Frameworks; 73 + sourceTree = "<group>"; 74 + }; 75 + /* End PBXGroup section */ 76 + 77 + /* Begin PBXNativeTarget section */ 78 + A93A953629CC810C00F8E227 /* iosApp */ = { 79 + isa = PBXNativeTarget; 80 + buildConfigurationList = A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */; 81 + buildPhases = ( 82 + A9D80A052AAB5CDE006C8738 /* ShellScript */, 83 + A93A953329CC810C00F8E227 /* Sources */, 84 + A93A953429CC810C00F8E227 /* Frameworks */, 85 + A93A953529CC810C00F8E227 /* Resources */, 86 + ); 87 + buildRules = ( 88 + ); 89 + dependencies = ( 90 + ); 91 + name = iosApp; 92 + productName = iosApp; 93 + productReference = A93A953729CC810C00F8E227 /* PoseDetection.app */; 94 + productType = "com.apple.product-type.application"; 95 + }; 96 + /* End PBXNativeTarget section */ 97 + 98 + /* Begin PBXProject section */ 99 + A93A952F29CC810C00F8E227 /* Project object */ = { 100 + isa = PBXProject; 101 + attributes = { 102 + BuildIndependentTargetsInParallel = YES; 103 + LastSwiftUpdateCheck = 1420; 104 + LastUpgradeCheck = 1620; 105 + TargetAttributes = { 106 + A93A953629CC810C00F8E227 = { 107 + CreatedOnToolsVersion = 14.2; 108 + }; 109 + }; 110 + }; 111 + buildConfigurationList = A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */; 112 + compatibilityVersion = "Xcode 14.0"; 113 + developmentRegion = en; 114 + hasScannedForEncodings = 0; 115 + knownRegions = ( 116 + en, 117 + Base, 118 + ); 119 + mainGroup = A93A952E29CC810C00F8E227; 120 + productRefGroup = A93A953829CC810C00F8E227 /* Products */; 121 + projectDirPath = ""; 122 + projectRoot = ""; 123 + targets = ( 124 + A93A953629CC810C00F8E227 /* iosApp */, 125 + ); 126 + }; 127 + /* End PBXProject section */ 128 + 129 + /* Begin PBXResourcesBuildPhase section */ 130 + A93A953529CC810C00F8E227 /* Resources */ = { 131 + isa = PBXResourcesBuildPhase; 132 + buildActionMask = 2147483647; 133 + files = ( 134 + A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */, 135 + A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */, 136 + ); 137 + runOnlyForDeploymentPostprocessing = 0; 138 + }; 139 + /* End PBXResourcesBuildPhase section */ 140 + 141 + /* Begin PBXShellScriptBuildPhase section */ 142 + A9D80A052AAB5CDE006C8738 /* ShellScript */ = { 143 + isa = PBXShellScriptBuildPhase; 144 + buildActionMask = 2147483647; 145 + files = ( 146 + ); 147 + inputFileListPaths = ( 148 + ); 149 + inputPaths = ( 150 + ); 151 + outputFileListPaths = ( 152 + ); 153 + outputPaths = ( 154 + ); 155 + runOnlyForDeploymentPostprocessing = 0; 156 + shellPath = /bin/sh; 157 + shellScript = "export JAVA_HOME=/Applications/Android\\ Studio.app/Contents/jbr/Contents/Home/\nexport JDK_HOME=/Applications/Android\\ Studio.app/Contents/jbr/Contents/\ncd \"$SRCROOT/..\"\n./gradlew :sample:composeApp:embedAndSignAppleFrameworkForXcode\n"; 158 + }; 159 + /* End PBXShellScriptBuildPhase section */ 160 + 161 + /* Begin PBXSourcesBuildPhase section */ 162 + A93A953329CC810C00F8E227 /* Sources */ = { 163 + isa = PBXSourcesBuildPhase; 164 + buildActionMask = 2147483647; 165 + files = ( 166 + A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */, 167 + ); 168 + runOnlyForDeploymentPostprocessing = 0; 169 + }; 170 + /* End PBXSourcesBuildPhase section */ 171 + 172 + /* Begin XCBuildConfiguration section */ 173 + A93A954329CC810D00F8E227 /* Debug */ = { 174 + isa = XCBuildConfiguration; 175 + buildSettings = { 176 + ALWAYS_SEARCH_USER_PATHS = NO; 177 + CLANG_ANALYZER_NONNULL = YES; 178 + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 179 + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 180 + CLANG_ENABLE_MODULES = YES; 181 + CLANG_ENABLE_OBJC_ARC = YES; 182 + CLANG_ENABLE_OBJC_WEAK = YES; 183 + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 184 + CLANG_WARN_BOOL_CONVERSION = YES; 185 + CLANG_WARN_COMMA = YES; 186 + CLANG_WARN_CONSTANT_CONVERSION = YES; 187 + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 188 + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 189 + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 190 + CLANG_WARN_EMPTY_BODY = YES; 191 + CLANG_WARN_ENUM_CONVERSION = YES; 192 + CLANG_WARN_INFINITE_RECURSION = YES; 193 + CLANG_WARN_INT_CONVERSION = YES; 194 + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 195 + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 196 + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 197 + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 198 + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 199 + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 200 + CLANG_WARN_STRICT_PROTOTYPES = YES; 201 + CLANG_WARN_SUSPICIOUS_MOVE = YES; 202 + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 203 + CLANG_WARN_UNREACHABLE_CODE = YES; 204 + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 205 + COPY_PHASE_STRIP = NO; 206 + DEBUG_INFORMATION_FORMAT = dwarf; 207 + ENABLE_STRICT_OBJC_MSGSEND = YES; 208 + ENABLE_TESTABILITY = YES; 209 + ENABLE_USER_SCRIPT_SANDBOXING = NO; 210 + GCC_C_LANGUAGE_STANDARD = gnu11; 211 + GCC_DYNAMIC_NO_PIC = NO; 212 + GCC_NO_COMMON_BLOCKS = YES; 213 + GCC_OPTIMIZATION_LEVEL = 0; 214 + GCC_PREPROCESSOR_DEFINITIONS = ( 215 + "DEBUG=1", 216 + "$(inherited)", 217 + ); 218 + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 219 + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 220 + GCC_WARN_UNDECLARED_SELECTOR = YES; 221 + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 222 + GCC_WARN_UNUSED_FUNCTION = YES; 223 + GCC_WARN_UNUSED_VARIABLE = YES; 224 + IPHONEOS_DEPLOYMENT_TARGET = 16.2; 225 + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 226 + MTL_FAST_MATH = YES; 227 + ONLY_ACTIVE_ARCH = YES; 228 + SDKROOT = iphoneos; 229 + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 230 + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 231 + }; 232 + name = Debug; 233 + }; 234 + A93A954429CC810D00F8E227 /* Release */ = { 235 + isa = XCBuildConfiguration; 236 + buildSettings = { 237 + ALWAYS_SEARCH_USER_PATHS = NO; 238 + CLANG_ANALYZER_NONNULL = YES; 239 + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 240 + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 241 + CLANG_ENABLE_MODULES = YES; 242 + CLANG_ENABLE_OBJC_ARC = YES; 243 + CLANG_ENABLE_OBJC_WEAK = YES; 244 + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 245 + CLANG_WARN_BOOL_CONVERSION = YES; 246 + CLANG_WARN_COMMA = YES; 247 + CLANG_WARN_CONSTANT_CONVERSION = YES; 248 + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 249 + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 250 + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 251 + CLANG_WARN_EMPTY_BODY = YES; 252 + CLANG_WARN_ENUM_CONVERSION = YES; 253 + CLANG_WARN_INFINITE_RECURSION = YES; 254 + CLANG_WARN_INT_CONVERSION = YES; 255 + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 256 + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 257 + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 258 + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 259 + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 260 + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 261 + CLANG_WARN_STRICT_PROTOTYPES = YES; 262 + CLANG_WARN_SUSPICIOUS_MOVE = YES; 263 + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 264 + CLANG_WARN_UNREACHABLE_CODE = YES; 265 + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 266 + COPY_PHASE_STRIP = NO; 267 + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 268 + ENABLE_NS_ASSERTIONS = NO; 269 + ENABLE_STRICT_OBJC_MSGSEND = YES; 270 + ENABLE_USER_SCRIPT_SANDBOXING = NO; 271 + GCC_C_LANGUAGE_STANDARD = gnu11; 272 + GCC_NO_COMMON_BLOCKS = YES; 273 + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 274 + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 275 + GCC_WARN_UNDECLARED_SELECTOR = YES; 276 + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 277 + GCC_WARN_UNUSED_FUNCTION = YES; 278 + GCC_WARN_UNUSED_VARIABLE = YES; 279 + IPHONEOS_DEPLOYMENT_TARGET = 16.2; 280 + MTL_ENABLE_DEBUG_INFO = NO; 281 + MTL_FAST_MATH = YES; 282 + SDKROOT = iphoneos; 283 + SWIFT_COMPILATION_MODE = wholemodule; 284 + SWIFT_OPTIMIZATION_LEVEL = "-O"; 285 + VALIDATE_PRODUCT = YES; 286 + }; 287 + name = Release; 288 + }; 289 + A93A954629CC810D00F8E227 /* Debug */ = { 290 + isa = XCBuildConfiguration; 291 + buildSettings = { 292 + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 293 + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 294 + CODE_SIGN_STYLE = Automatic; 295 + CURRENT_PROJECT_VERSION = 1; 296 + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 297 + DEVELOPMENT_TEAM = FAGG2XS28P; 298 + ENABLE_PREVIEWS = YES; 299 + GENERATE_INFOPLIST_FILE = YES; 300 + INFOPLIST_FILE = iosApp/Info.plist; 301 + INFOPLIST_KEY_UILaunchScreen_Generation = YES; 302 + LD_RUNPATH_SEARCH_PATHS = ( 303 + "$(inherited)", 304 + "@executable_path/Frameworks", 305 + ); 306 + MARKETING_VERSION = 1.0; 307 + PRODUCT_BUNDLE_IDENTIFIER = com.nate.posedetection.iosApp; 308 + PRODUCT_NAME = PoseDetection; 309 + SWIFT_EMIT_LOC_STRINGS = YES; 310 + SWIFT_VERSION = 5.0; 311 + TARGETED_DEVICE_FAMILY = "1,2"; 312 + }; 313 + name = Debug; 314 + }; 315 + A93A954729CC810D00F8E227 /* Release */ = { 316 + isa = XCBuildConfiguration; 317 + buildSettings = { 318 + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 320 + CODE_SIGN_STYLE = Automatic; 321 + CURRENT_PROJECT_VERSION = 1; 322 + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 323 + DEVELOPMENT_TEAM = FAGG2XS28P; 324 + ENABLE_PREVIEWS = YES; 325 + GENERATE_INFOPLIST_FILE = YES; 326 + INFOPLIST_FILE = iosApp/Info.plist; 327 + INFOPLIST_KEY_UILaunchScreen_Generation = YES; 328 + LD_RUNPATH_SEARCH_PATHS = ( 329 + "$(inherited)", 330 + "@executable_path/Frameworks", 331 + ); 332 + MARKETING_VERSION = 1.0; 333 + PRODUCT_BUNDLE_IDENTIFIER = com.nate.posedetection.iosApp; 334 + PRODUCT_NAME = PoseDetection; 335 + SWIFT_EMIT_LOC_STRINGS = YES; 336 + SWIFT_VERSION = 5.0; 337 + TARGETED_DEVICE_FAMILY = "1,2"; 338 + }; 339 + name = Release; 340 + }; 341 + /* End XCBuildConfiguration section */ 342 + 343 + /* Begin XCConfigurationList section */ 344 + A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */ = { 345 + isa = XCConfigurationList; 346 + buildConfigurations = ( 347 + A93A954329CC810D00F8E227 /* Debug */, 348 + A93A954429CC810D00F8E227 /* Release */, 349 + ); 350 + defaultConfigurationIsVisible = 0; 351 + defaultConfigurationName = Release; 352 + }; 353 + A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 354 + isa = XCConfigurationList; 355 + buildConfigurations = ( 356 + A93A954629CC810D00F8E227 /* Debug */, 357 + A93A954729CC810D00F8E227 /* Release */, 358 + ); 359 + defaultConfigurationIsVisible = 0; 360 + defaultConfigurationName = Release; 361 + }; 362 + /* End XCConfigurationList section */ 363 + }; 364 + rootObject = A93A952F29CC810C00F8E227 /* Project object */; 365 + }
+7
sample/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <Workspace 3 + version = "1.0"> 4 + <FileRef 5 + location = "self:"> 6 + </FileRef> 7 + </Workspace>
+11
sample/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
··· 1 + { 2 + "colors" : [ 3 + { 4 + "idiom" : "universal" 5 + } 6 + ], 7 + "info" : { 8 + "author" : "xcode", 9 + "version" : 1 10 + } 11 + }
sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png

This is a binary file and will not be displayed.

sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png

This is a binary file and will not be displayed.

+130
sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
··· 1 + { 2 + "images": [ 3 + { 4 + "filename": "AppIcon@2x.png", 5 + "idiom": "iphone", 6 + "scale": "2x", 7 + "size": "60x60" 8 + }, 9 + { 10 + "filename": "AppIcon@3x.png", 11 + "idiom": "iphone", 12 + "scale": "3x", 13 + "size": "60x60" 14 + }, 15 + { 16 + "filename": "AppIcon~ipad.png", 17 + "idiom": "ipad", 18 + "scale": "1x", 19 + "size": "76x76" 20 + }, 21 + { 22 + "filename": "AppIcon@2x~ipad.png", 23 + "idiom": "ipad", 24 + "scale": "2x", 25 + "size": "76x76" 26 + }, 27 + { 28 + "filename": "AppIcon-83.5@2x~ipad.png", 29 + "idiom": "ipad", 30 + "scale": "2x", 31 + "size": "83.5x83.5" 32 + }, 33 + { 34 + "filename": "AppIcon-40@2x.png", 35 + "idiom": "iphone", 36 + "scale": "2x", 37 + "size": "40x40" 38 + }, 39 + { 40 + "filename": "AppIcon-40@3x.png", 41 + "idiom": "iphone", 42 + "scale": "3x", 43 + "size": "40x40" 44 + }, 45 + { 46 + "filename": "AppIcon-40~ipad.png", 47 + "idiom": "ipad", 48 + "scale": "1x", 49 + "size": "40x40" 50 + }, 51 + { 52 + "filename": "AppIcon-40@2x~ipad.png", 53 + "idiom": "ipad", 54 + "scale": "2x", 55 + "size": "40x40" 56 + }, 57 + { 58 + "filename": "AppIcon-20@2x.png", 59 + "idiom": "iphone", 60 + "scale": "2x", 61 + "size": "20x20" 62 + }, 63 + { 64 + "filename": "AppIcon-20@3x.png", 65 + "idiom": "iphone", 66 + "scale": "3x", 67 + "size": "20x20" 68 + }, 69 + { 70 + "filename": "AppIcon-20~ipad.png", 71 + "idiom": "ipad", 72 + "scale": "1x", 73 + "size": "20x20" 74 + }, 75 + { 76 + "filename": "AppIcon-20@2x~ipad.png", 77 + "idiom": "ipad", 78 + "scale": "2x", 79 + "size": "20x20" 80 + }, 81 + { 82 + "filename": "AppIcon-29.png", 83 + "idiom": "iphone", 84 + "scale": "1x", 85 + "size": "29x29" 86 + }, 87 + { 88 + "filename": "AppIcon-29@2x.png", 89 + "idiom": "iphone", 90 + "scale": "2x", 91 + "size": "29x29" 92 + }, 93 + { 94 + "filename": "AppIcon-29@3x.png", 95 + "idiom": "iphone", 96 + "scale": "3x", 97 + "size": "29x29" 98 + }, 99 + { 100 + "filename": "AppIcon-29~ipad.png", 101 + "idiom": "ipad", 102 + "scale": "1x", 103 + "size": "29x29" 104 + }, 105 + { 106 + "filename": "AppIcon-29@2x~ipad.png", 107 + "idiom": "ipad", 108 + "scale": "2x", 109 + "size": "29x29" 110 + }, 111 + { 112 + "filename": "AppIcon-60@2x~car.png", 113 + "idiom": "car", 114 + "scale": "2x", 115 + "size": "60x60" 116 + }, 117 + { 118 + "filename": "AppIcon-60@3x~car.png", 119 + "idiom": "car", 120 + "scale": "3x", 121 + "size": "60x60" 122 + }, 123 + { 124 + "filename": "AppIcon~ios-marketing.png", 125 + "idiom": "ios-marketing", 126 + "scale": "1x", 127 + "size": "1024x1024" 128 + } 129 + ] 130 + }
+6
sample/iosApp/iosApp/Assets.xcassets/Contents.json
··· 1 + { 2 + "info" : { 3 + "author" : "xcode", 4 + "version" : 1 5 + } 6 + }
+10
sample/iosApp/iosApp/Info.plist
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>CADisableMinimumFrameDurationOnPhone</key> 6 + <true/> 7 + <key>NSCameraUsageDescription</key> 8 + <string>We need access to your camera to analyse your performance.</string> 9 + </dict> 10 + </plist>
+6
sample/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
··· 1 + { 2 + "info" : { 3 + "author" : "xcode", 4 + "version" : 1 5 + } 6 + }
+19
sample/iosApp/iosApp/iosApp.swift
··· 1 + import UIKit 2 + import ComposeApp 3 + 4 + @main 5 + class AppDelegate: UIResponder, UIApplicationDelegate { 6 + var window: UIWindow? 7 + 8 + func application( 9 + _ application: UIApplication, 10 + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 11 + ) -> Bool { 12 + window = UIWindow(frame: UIScreen.main.bounds) 13 + if let window = window { 14 + window.rootViewController = MainKt.MainViewController() 15 + window.makeKeyAndVisible() 16 + } 17 + return true 18 + } 19 + }
+33
settings.gradle.kts
··· 1 + rootProject.name = "PoseDetection" 2 + 3 + pluginManagement { 4 + repositories { 5 + google { 6 + content { 7 + includeGroupByRegex("com\\.android.*") 8 + includeGroupByRegex("com\\.google.*") 9 + includeGroupByRegex("androidx.*") 10 + includeGroupByRegex("android.*") 11 + } 12 + } 13 + gradlePluginPortal() 14 + mavenCentral() 15 + } 16 + } 17 + 18 + dependencyResolutionManagement { 19 + repositories { 20 + google { 21 + content { 22 + includeGroupByRegex("com\\.android.*") 23 + includeGroupByRegex("com\\.google.*") 24 + includeGroupByRegex("androidx.*") 25 + includeGroupByRegex("android.*") 26 + } 27 + } 28 + mavenCentral() 29 + } 30 + } 31 + include(":sample:composeApp") 32 + include(":posedetection") 33 +