diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8987a8299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +# Gradle +.gradle/ +build/ +!gradle/wrapper/gradle-wrapper.jar + +# IDE +.idea/ +*.iml +*.ipr +*.iws +.DS_Store + +# Local configuration +local.properties +*.local.properties + +# Kotlin +.kotlin/ +*.kotlin_module + +# Android +/captures +.externalNativeBuild +.cxx + +# iOS +iosApp/Pods/ +iosApp/*.xcworkspace/ +iosApp/*.xcodeproj/xcuserdata/ +iosApp/*.xcodeproj/project.xcworkspace/ +iosApp/DerivedData/ + +# Desktop +/out/ + +# Test outputs +test_output.txt +*.log + +# Build artifacts +*.apk +*.aab +*.aar +*.jar +!gradle/wrapper/gradle-wrapper.jar +*.dex + +# Generated files +/generated/ + +# Temporary files +*.tmp +*.temp +*~ + +# Claude Code / AI tools +.skills +docs/LAST_SESSION.md +.claude +LAST_SESSION.md +CHANGELOG.md +.aider* +/.devilmcp/ + +# Reference/decompiled code (not for distribution) +/reference/ +/vitruvian-decompiled/ +/scripts/ +/apktool.jar +/vitruvian-official.apk diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..c0a931802 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Vitruvian Project Phoenix Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5a94ac0f2..90145d87f 100644 --- a/README.md +++ b/README.md @@ -1 +1,148 @@ -# Project-Phoenix-2.0 \ No newline at end of file +# Vitruvian Project Phoenix - Multiplatform Control App + +[![Latest Release](https://img.shields.io/github/v/release/DasBluEyedDevil/Project-Phoenix-2.0?include_prereleases)](https://github.com/DasBluEyedDevil/Project-Phoenix-2.0/releases) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Kotlin](https://img.shields.io/badge/Kotlin-2.0+-blue.svg)](https://kotlinlang.org) + +A Kotlin Multiplatform application for controlling Vitruvian Trainer workout machines via Bluetooth Low Energy (BLE). This is a multiplatform version of the original [VitruvianProjectPhoenix](https://github.com/DasBluEyedDevil/VitruvianProjectPhoenix) Android app. + +## Support the Project + +If you find this app useful and want to support its continued development: + +**[☕ Buy Me a Coffee](https://buymeacoffee.com/vitruvianredux)** + +Your support helps keep the machines running and the code flowing! + +--- + +## Project Overview + +This app enables local control of Vitruvian Trainer machines after the company's bankruptcy. It's a community rescue project providing native applications for multiple platforms to keep these machines functional and prevent them from becoming e-waste. + +## 🚀 Supported Platforms + +| Platform | Status | Notes | +|----------|--------|-------| +| **Android** | 🔄 In Development | BLE support via native APIs | +| **iOS** | 🔄 In Development | BLE support via CoreBluetooth | +| **Desktop (Linux)** | 🔄 In Development | BLE support via BlueZ | +| **Desktop (Windows/macOS)** | 🔄 In Development | BLE support via platform APIs | + +## Features + +### Core Functionality (Planned) +- **BLE Connectivity**: Reliable connection to Vitruvian Trainer devices +- **All Workout Modes**: Old School, Pump, TUT, TUT Beast, Eccentric-Only, Echo +- **Real-time Monitoring**: Live load, position, velocity, and power metrics +- **Rep Counting**: Accurate rep detection with visual feedback +- **Auto-Stop**: Automatic workout termination based on rep targets + +### Enhanced Features (Planned) +- **Exercise Library**: 200+ pre-loaded exercises categorized by muscle group +- **Personal Records**: Automatic PR detection and historical tracking +- **Workout History**: Complete history with metrics stored locally +- **Custom Routines**: Build and save your own workout routines +- **Program Builder**: Create structured multi-exercise programs + +### Analytics & Insights (Planned) +- **Muscle Balance Radar**: Visual balance across muscle groups +- **Consistency Gauge**: Monthly workout consistency tracking +- **Volume vs Intensity**: Session comparison charts +- **Total Volume History**: Track volume lifted over time +- **Mode Distribution**: Workout mode usage breakdown + +## Technology Stack + +| Category | Technology | +|----------|------------| +| **Language** | Kotlin 2.0+ | +| **UI** | Compose Multiplatform | +| **Architecture** | MVVM + Clean Architecture | +| **DI** | Koin (Multiplatform) | +| **BLE** | Platform-specific implementations | +| **Database** | SQLDelight (Multiplatform) | +| **Preferences** | Multiplatform Settings | +| **Async** | Coroutines + Flow | + +## Project Structure + +``` +Project-Phoenix-2.0/ +├── shared/ # Shared Kotlin Multiplatform code +│ └── src/ +│ ├── commonMain/ # Common business logic +│ ├── androidMain/ # Android-specific implementations +│ ├── iosMain/ # iOS-specific implementations +│ └── desktopMain/ # Desktop-specific implementations +├── androidApp/ # Android application +├── iosApp/ # iOS application (Xcode project) +├── desktopApp/ # Desktop application (Linux, Windows, macOS) +├── gradle/ # Gradle wrapper and version catalog +├── build.gradle.kts # Root build configuration +└── settings.gradle.kts # Project settings +``` + +## Building from Source + +### Prerequisites +- JDK 17+ +- Android Studio Hedgehog or newer (for Android development) +- Xcode 15+ (for iOS development, macOS only) +- Kotlin 2.0+ + +### Building the Android App +```bash +./gradlew :androidApp:assembleDebug +``` + +### Building the Desktop App +```bash +./gradlew :desktopApp:run +``` + +### Building for iOS +Open `iosApp/iosApp.xcodeproj` in Xcode and build from there. + +## Hardware Compatibility + +### Vitruvian V-Form Trainer (Euclid / VIT-200) +- **Status:** 🔄 In Development +- **Device Name:** `Vee_*` +- **Max Resistance:** 200 kg (440 lbs) + +### Vitruvian Trainer+ +- **Status:** 🔄 In Development +- **Max Resistance:** 220 kg (485 lbs) + +## Contributing + +This is an open-source community project. Contributions welcome! + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Test with real hardware if possible +5. Submit a pull request + +## License + +MIT License - See [LICENSE](LICENSE) file for details + +## Acknowledgments + +- Original [VitruvianProjectPhoenix](https://github.com/DasBluEyedDevil/VitruvianProjectPhoenix) Android app +- Original web app developers for reverse-engineering the BLE protocol +- Vitruvian machine owners community for testing and feedback +- JetBrains for Kotlin Multiplatform +- All contributors and supporters + +## Support + +- **Issues**: [GitHub Issues](https://github.com/DasBluEyedDevil/Project-Phoenix-2.0/issues) +- **Discussions**: [GitHub Discussions](https://github.com/DasBluEyedDevil/Project-Phoenix-2.0/discussions) +- **Support Development**: [Buy Me a Coffee](https://buymeacoffee.com/vitruvianredux) + +--- + +*This app is a community rescue project to keep Vitruvian Trainer machines functional after the company's bankruptcy. It is not affiliated with or endorsed by Vitruvian.* diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts new file mode 100644 index 000000000..25690f800 --- /dev/null +++ b/androidApp/build.gradle.kts @@ -0,0 +1,116 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.example.vitruvianredux" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.vitruvianredux" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "0.1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + signingConfigs { + getByName("debug") { + val keystorePath = file("${System.getProperty("user.home")}/.android/debug.keystore") + if (keystorePath.exists()) { + storeFile = keystorePath + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + debug { + isDebuggable = true + applicationIdSuffix = ".debug" + versionNameSuffix = "-DEBUG" + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + buildConfig = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + // Shared module + implementation(project(":shared")) + + // Core Android + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime) + implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.splashscreen) + + // Compose + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + implementation(libs.compose.material.icons.extended) + implementation(libs.compose.navigation) + + // Compose Tooling + debugImplementation(libs.compose.ui.tooling) + + // Koin DI + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + + // Coroutines + implementation(libs.kotlinx.coroutines.android) + + // Logging + implementation(libs.kermit) + + // Testing + testImplementation(libs.junit) + testImplementation(libs.mockk) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.turbine) + testImplementation(libs.truth) + + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso) + androidTestImplementation(platform(libs.compose.bom)) +} diff --git a/androidApp/proguard-rules.pro b/androidApp/proguard-rules.pro new file mode 100644 index 000000000..ba4124096 --- /dev/null +++ b/androidApp/proguard-rules.pro @@ -0,0 +1,16 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in the SDK tools. + +# Keep Koin classes +-keepclassmembers class * { + @org.koin.* ; +} + +# Keep Kotlin Serialization +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.AnnotationsKt + +# Keep Coroutines +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9954f5700 --- /dev/null +++ b/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/androidApp/src/main/kotlin/com/example/vitruvianredux/AppModule.kt b/androidApp/src/main/kotlin/com/example/vitruvianredux/AppModule.kt new file mode 100644 index 000000000..44a072d49 --- /dev/null +++ b/androidApp/src/main/kotlin/com/example/vitruvianredux/AppModule.kt @@ -0,0 +1,13 @@ +package com.example.vitruvianredux + +import org.koin.dsl.module + +/** + * Koin dependency injection module for the Android app. + */ +val appModule = module { + // Add dependencies here as the app grows + // Example: + // single { SomeRepository() } + // viewModel { SomeViewModel(get()) } +} diff --git a/androidApp/src/main/kotlin/com/example/vitruvianredux/MainActivity.kt b/androidApp/src/main/kotlin/com/example/vitruvianredux/MainActivity.kt new file mode 100644 index 000000000..754c1f8fc --- /dev/null +++ b/androidApp/src/main/kotlin/com/example/vitruvianredux/MainActivity.kt @@ -0,0 +1,60 @@ +package com.example.vitruvianredux + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.vitruvianredux.ui.theme.VitruvianTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + VitruvianTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + MainScreen() + } + } + } + } +} + +@Composable +fun MainScreen() { + val greeting = Greeting() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = greeting.greet(), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center + ) + Text( + text = "Kotlin Multiplatform App", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp) + ) + } +} diff --git a/androidApp/src/main/kotlin/com/example/vitruvianredux/VitruvianApp.kt b/androidApp/src/main/kotlin/com/example/vitruvianredux/VitruvianApp.kt new file mode 100644 index 000000000..e70eba46f --- /dev/null +++ b/androidApp/src/main/kotlin/com/example/vitruvianredux/VitruvianApp.kt @@ -0,0 +1,23 @@ +package com.example.vitruvianredux + +import android.app.Application +import co.touchlab.kermit.Logger +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin + +class VitruvianApp : Application() { + + override fun onCreate() { + super.onCreate() + + // Initialize Koin for dependency injection + startKoin { + androidLogger() + androidContext(this@VitruvianApp) + modules(appModule) + } + + Logger.d("VitruvianApp") { "Application initialized" } + } +} diff --git a/androidApp/src/main/kotlin/com/example/vitruvianredux/ui/theme/Theme.kt b/androidApp/src/main/kotlin/com/example/vitruvianredux/ui/theme/Theme.kt new file mode 100644 index 000000000..4cb5f826c --- /dev/null +++ b/androidApp/src/main/kotlin/com/example/vitruvianredux/ui/theme/Theme.kt @@ -0,0 +1,63 @@ +package com.example.vitruvianredux.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Color(0xFF6750A4), + secondary = Color(0xFF625B71), + tertiary = Color(0xFF7D5260), + background = Color(0xFF1C1B1F), + surface = Color(0xFF1C1B1F), +) + +private val LightColorScheme = lightColorScheme( + primary = Color(0xFF6750A4), + secondary = Color(0xFF625B71), + tertiary = Color(0xFF7D5260), + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), +) + +@Composable +fun VitruvianTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} diff --git a/androidApp/src/main/res/drawable/ic_launcher_background.xml b/androidApp/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..a526494b2 --- /dev/null +++ b/androidApp/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/androidApp/src/main/res/drawable/ic_launcher_foreground.xml b/androidApp/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..d6120cda9 --- /dev/null +++ b/androidApp/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..d378acd7a --- /dev/null +++ b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..d378acd7a --- /dev/null +++ b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml new file mode 100644 index 000000000..006fd1c85 --- /dev/null +++ b/androidApp/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Vitruvian Phoenix + diff --git a/androidApp/src/main/res/values/themes.xml b/androidApp/src/main/res/values/themes.xml new file mode 100644 index 000000000..666f24239 --- /dev/null +++ b/androidApp/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..6429e05e6 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,26 @@ +// Top-level build file for Vitruvian Project Phoenix - Multiplatform +plugins { + // Android plugins - apply false to configure in submodules + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + + // Kotlin plugins + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.compose) apply false + + // Compose Multiplatform + alias(libs.plugins.compose.multiplatform) apply false + + // SQLDelight + alias(libs.plugins.sqldelight) apply false +} + +allprojects { + // Common configuration for all projects +} + +tasks.register("clean", Delete::class) { + delete(rootProject.layout.buildDirectory) +} diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts new file mode 100644 index 000000000..1e258519c --- /dev/null +++ b/desktopApp/build.gradle.kts @@ -0,0 +1,73 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + jvm("desktop") { + compilations.all { + kotlinOptions { + jvmTarget = "17" + } + } + } + + sourceSets { + val desktopMain by getting { + dependencies { + // Shared module + implementation(project(":shared")) + + // Compose Desktop + implementation(compose.desktop.currentOs) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + + // Coroutines + implementation(libs.kotlinx.coroutines.swing) + + // Koin DI + implementation(libs.koin.core) + + // Logging + implementation(libs.kermit) + } + } + } +} + +compose.desktop { + application { + mainClass = "com.example.vitruvianredux.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm) + + packageName = "VitruvianProjectPhoenix" + packageVersion = "0.1.0" + description = "Vitruvian Project Phoenix - Control app for Vitruvian Trainer machines" + copyright = "© 2024 Vitruvian Project Phoenix Contributors" + vendor = "Vitruvian Project Phoenix" + + linux { + iconFile.set(project.file("src/main/resources/icon.png")) + } + + windows { + iconFile.set(project.file("src/main/resources/icon.ico")) + menuGroup = "Vitruvian Project Phoenix" + upgradeUuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + } + + macOS { + iconFile.set(project.file("src/main/resources/icon.icns")) + bundleID = "com.example.vitruvianredux" + } + } + } +} diff --git a/desktopApp/src/main/kotlin/com/example/vitruvianredux/Main.kt b/desktopApp/src/main/kotlin/com/example/vitruvianredux/Main.kt new file mode 100644 index 000000000..89491f63a --- /dev/null +++ b/desktopApp/src/main/kotlin/com/example/vitruvianredux/Main.kt @@ -0,0 +1,64 @@ +package com.example.vitruvianredux + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.application + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Vitruvian Project Phoenix", + state = WindowState(size = DpSize(800.dp, 600.dp)) + ) { + App() + } +} + +@Composable +fun App() { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + MainScreen() + } + } +} + +@Composable +fun MainScreen() { + val greeting = Greeting() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = greeting.greet(), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center + ) + Text( + text = "Kotlin Multiplatform App", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp) + ) + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..47587d05c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,30 @@ +# Project-wide Gradle settings. + +# JVM arguments for the Gradle daemon +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + +# Enable parallel execution for faster builds +org.gradle.parallel=true + +# Enable configuration cache for faster configuration phase +org.gradle.configuration-cache=true + +# AndroidX migration +android.useAndroidX=true + +# Non-transitive R classes +android.nonTransitiveRClass=true + +# Kotlin code style +kotlin.code.style=official + +# Enable Kotlin Multiplatform +kotlin.mpp.androidSourceSetLayoutVersion=2 + +# Compose Multiplatform settings +org.jetbrains.compose.experimental.jscanvas.enabled=true + +# Native targets (for iOS) +kotlin.native.cacheKind.iosArm64=none +kotlin.native.cacheKind.iosSimulatorArm64=none +kotlin.native.cacheKind.iosX64=none diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..616ac2003 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,127 @@ +[versions] +# Kotlin and KMP +kotlin = "2.0.21" +agp = "8.5.2" +compose-multiplatform = "1.7.1" + +# Kotlinx +kotlinx-coroutines = "1.9.0" +kotlinx-serialization = "1.7.3" +kotlinx-datetime = "0.6.1" + +# Compose +compose-bom = "2024.10.01" +compose-navigation = "2.8.3" +activity-compose = "1.9.3" + +# AndroidX +androidx-core = "1.13.1" +androidx-lifecycle = "2.8.6" +androidx-splashscreen = "1.0.1" + +# Dependency Injection - Koin (Multiplatform alternative to Hilt) +koin = "4.0.0" +koin-compose = "4.0.0" + +# Database - SQLDelight (Multiplatform alternative to Room) +sqldelight = "2.0.2" + +# Settings/Preferences - Multiplatform +multiplatform-settings = "1.2.0" + +# BLE +nordic-ble = "2.11.0" + +# Charts +# Note: Charts will need platform-specific implementations + +# Logging +kermit = "2.0.4" + +# Testing +junit = "4.13.2" +junit-version = "1.2.1" +espresso = "3.6.1" +mockk = "1.13.13" +turbine = "1.1.0" +truth = "1.4.4" + +[libraries] +# Kotlin +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } + +# Kotlinx +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } + +# Compose BOM (Android) +compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } + +# AndroidX +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" } + +# Compose (Android - via BOM) +compose-ui = { module = "androidx.compose.ui:ui" } +compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +compose-material3 = { module = "androidx.compose.material3:material3" } +compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navigation" } + +# Koin (Multiplatform DI) +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin-compose" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin-compose" } + +# SQLDelight (Multiplatform Database) +sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" } +sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } +sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } +sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } +sqldelight-sqlite-driver = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } + +# Multiplatform Settings +multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" } +multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatform-settings" } + +# BLE (Android-specific) +nordic-ble = { module = "no.nordicsemi.android:ble", version.ref = "nordic-ble" } +nordic-ble-ktx = { module = "no.nordicsemi.android:ble-ktx", version.ref = "nordic-ble" } + +# Logging (Multiplatform) +kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } + +# Testing +junit = { module = "junit:junit", version.ref = "junit" } +androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit-version" } +androidx-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } +truth = { module = "com.google.truth:truth", version.ref = "truth" } + +[plugins] +# Android +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } + +# Kotlin +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + +# Compose Multiplatform +compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } + +# SQLDelight +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..a4b76b953 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..9355b4155 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..7f57125ae --- /dev/null +++ b/gradlew @@ -0,0 +1,115 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +############################################################################## + +# Attempt to set APP_HOME +app_path=$0 +while + APP_HOME=${app_path%"${app_path##*/}"} + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in + /*) app_path=$link ;; + *) app_path=$APP_HOME$link ;; + esac +done + +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in + CYGWIN* ) cygwin=true ;; + Darwin* ) darwin=true ;; + MSYS* | MINGW* ) msys=true ;; + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME" + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in + '' | soft) :;; + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + JAVACMD=$( cygpath --unix "$JAVACMD" ) +fi + +# Add default JVM options here. +DEFAULT_JVM_OPTS="-Xmx64m -Xms64m" + +exec "$JAVACMD" $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..e1ab17578 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/iosApp/README.md b/iosApp/README.md new file mode 100644 index 000000000..9193dd703 --- /dev/null +++ b/iosApp/README.md @@ -0,0 +1,44 @@ +# iOS App Setup + +The iOS app uses the shared Kotlin Multiplatform module via an Xcode project. + +## Prerequisites + +- macOS +- Xcode 15 or later +- CocoaPods (optional, for dependency management) + +## Setup Instructions + +1. Open this directory in Xcode: + - Create a new Xcode project (iOS App, SwiftUI) + - Name it "VitruvianProjectPhoenix" + +2. Configure the shared framework: + - Add the shared framework from the Kotlin Multiplatform build + - The framework is built using `./gradlew :shared:assembleXCFramework` + +3. Import and use the shared code: + ```swift + import shared + + // Use the Greeting class + let greeting = Greeting() + print(greeting.greet()) + ``` + +## Building the Shared Framework + +From the project root: +```bash +./gradlew :shared:assembleXCFramework +``` + +The framework will be available at: +`shared/build/XCFrameworks/release/shared.xcframework` + +## Notes + +- The iOS app provides the native UI layer using SwiftUI +- Business logic is shared via the Kotlin Multiplatform shared module +- BLE functionality will use CoreBluetooth framework for iOS diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..9ab7b84bc --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} + +rootProject.name = "VitruvianProjectPhoenix" + +include(":shared") +include(":androidApp") +include(":desktopApp") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 000000000..4084f151e --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,147 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.sqldelight) +} + +kotlin { + // Android target + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "17" + } + } + } + + // iOS targets + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "shared" + isStatic = true + } + } + + // Desktop targets + jvm("desktop") { + compilations.all { + kotlinOptions { + jvmTarget = "17" + } + } + } + + sourceSets { + val commonMain by getting { + dependencies { + // Compose Multiplatform + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(compose.components.resources) + + // Kotlinx + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.datetime) + + // DI - Koin + implementation(libs.koin.core) + implementation(libs.koin.compose) + + // Database - SQLDelight + implementation(libs.sqldelight.runtime) + implementation(libs.sqldelight.coroutines) + + // Settings/Preferences + implementation(libs.multiplatform.settings) + implementation(libs.multiplatform.settings.coroutines) + + // Logging + implementation(libs.kermit) + } + } + + val commonTest by getting { + dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) + } + } + + val androidMain by getting { + dependencies { + // Android-specific Coroutines + implementation(libs.kotlinx.coroutines.android) + + // SQLDelight Android Driver + implementation(libs.sqldelight.android.driver) + + // Koin Android + implementation(libs.koin.android) + + // BLE - Nordic Library (Android only for now) + implementation(libs.nordic.ble) + implementation(libs.nordic.ble.ktx) + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + + dependencies { + // SQLDelight Native Driver + implementation(libs.sqldelight.native.driver) + } + } + + val desktopMain by getting { + dependencies { + // Compose Desktop + implementation(compose.desktop.currentOs) + + // Coroutines Swing for desktop + implementation(libs.kotlinx.coroutines.swing) + + // SQLDelight JVM Driver + implementation(libs.sqldelight.sqlite.driver) + } + } + } +} + +android { + namespace = "com.example.vitruvianredux.shared" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +sqldelight { + databases { + create("VitruvianDatabase") { + packageName.set("com.example.vitruvianredux.database") + } + } +} diff --git a/shared/src/androidMain/kotlin/com/example/vitruvianredux/Platform.android.kt b/shared/src/androidMain/kotlin/com/example/vitruvianredux/Platform.android.kt new file mode 100644 index 000000000..49dafd545 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/example/vitruvianredux/Platform.android.kt @@ -0,0 +1,12 @@ +package com.example.vitruvianredux + +import android.os.Build + +/** + * Android platform implementation. + */ +class AndroidPlatform : Platform { + override val name: String = "Android ${Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform() diff --git a/shared/src/commonMain/kotlin/com/example/vitruvianredux/Greeting.kt b/shared/src/commonMain/kotlin/com/example/vitruvianredux/Greeting.kt new file mode 100644 index 000000000..cd29652ca --- /dev/null +++ b/shared/src/commonMain/kotlin/com/example/vitruvianredux/Greeting.kt @@ -0,0 +1,12 @@ +package com.example.vitruvianredux + +/** + * Greeting class to verify multiplatform setup. + */ +class Greeting { + private val platform = getPlatform() + + fun greet(): String { + return "Vitruvian Project Phoenix running on ${platform.name}!" + } +} diff --git a/shared/src/commonMain/kotlin/com/example/vitruvianredux/Platform.kt b/shared/src/commonMain/kotlin/com/example/vitruvianredux/Platform.kt new file mode 100644 index 000000000..173d643fc --- /dev/null +++ b/shared/src/commonMain/kotlin/com/example/vitruvianredux/Platform.kt @@ -0,0 +1,14 @@ +package com.example.vitruvianredux + +/** + * Platform interface for platform-specific implementations. + * Each platform (Android, iOS, Desktop) will provide its own implementation. + */ +interface Platform { + val name: String +} + +/** + * Returns the current platform implementation. + */ +expect fun getPlatform(): Platform diff --git a/shared/src/commonMain/kotlin/com/example/vitruvianredux/data/ble/BleInterfaces.kt b/shared/src/commonMain/kotlin/com/example/vitruvianredux/data/ble/BleInterfaces.kt new file mode 100644 index 000000000..519c4d137 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/example/vitruvianredux/data/ble/BleInterfaces.kt @@ -0,0 +1,100 @@ +package com.example.vitruvianredux.data.ble + +import com.example.vitruvianredux.domain.model.ConnectionState +import com.example.vitruvianredux.domain.model.WorkoutMetrics +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** + * Interface for BLE device scanning and discovery. + * Platform-specific implementations will handle actual BLE scanning. + */ +interface BleScanner { + /** + * Flow of discovered Vitruvian devices. + */ + val discoveredDevices: Flow + + /** + * Whether scanning is currently active. + */ + val isScanning: StateFlow + + /** + * Start scanning for Vitruvian devices. + */ + suspend fun startScan() + + /** + * Stop scanning for devices. + */ + suspend fun stopScan() +} + +/** + * Interface for managing BLE connection to a Vitruvian device. + * Platform-specific implementations will handle actual BLE connections. + */ +interface BleConnection { + /** + * Current connection state. + */ + val connectionState: StateFlow + + /** + * Flow of real-time workout metrics from the device. + */ + val metrics: Flow + + /** + * Connect to a Vitruvian device. + * @param device The device to connect to. + */ + suspend fun connect(device: VitruvianDevice) + + /** + * Disconnect from the current device. + */ + suspend fun disconnect() + + /** + * Send a command to set the target weight. + * @param weightKg Weight in kilograms. + */ + suspend fun setWeight(weightKg: Float) + + /** + * Send a command to start a workout. + */ + suspend fun startWorkout() + + /** + * Send a command to stop the current workout. + */ + suspend fun stopWorkout() +} + +/** + * Represents a discovered Vitruvian device. + */ +data class VitruvianDevice( + val address: String, + val name: String, + val rssi: Int = 0, + val isConnected: Boolean = false +) { + /** + * Check if this is a valid Vitruvian device based on naming convention. + */ + val isVitruvian: Boolean + get() = name.startsWith("Vee_") || name.startsWith("VIT") +} + +/** + * Vitruvian BLE service and characteristic UUIDs. + */ +object VitruvianBleUuids { + const val SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e" + const val TX_CHARACTERISTIC = "6e400002-b5a3-f393-e0a9-e50e24dcca9e" + const val RX_CHARACTERISTIC = "6e400003-b5a3-f393-e0a9-e50e24dcca9e" +} diff --git a/shared/src/commonMain/kotlin/com/example/vitruvianredux/domain/model/ExerciseModels.kt b/shared/src/commonMain/kotlin/com/example/vitruvianredux/domain/model/ExerciseModels.kt new file mode 100644 index 000000000..35e90f24a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/example/vitruvianredux/domain/model/ExerciseModels.kt @@ -0,0 +1,80 @@ +package com.example.vitruvianredux.domain.model + +import kotlinx.serialization.Serializable + +/** + * Represents a muscle group for categorizing exercises. + */ +@Serializable +enum class MuscleGroup(val displayName: String) { + CHEST("Chest"), + BACK("Back"), + SHOULDERS("Shoulders"), + BICEPS("Biceps"), + TRICEPS("Triceps"), + FOREARMS("Forearms"), + CORE("Core"), + QUADS("Quadriceps"), + HAMSTRINGS("Hamstrings"), + GLUTES("Glutes"), + CALVES("Calves"), + FULL_BODY("Full Body") +} + +/** + * Represents the type of equipment used for an exercise. + */ +@Serializable +enum class EquipmentType(val displayName: String) { + BARBELL("Barbell"), + DUMBBELL("Dumbbell"), + CABLE("Cable"), + BODYWEIGHT("Bodyweight") +} + +/** + * Represents an exercise in the library. + */ +@Serializable +data class Exercise( + val id: Int, + val name: String, + val primaryMuscle: MuscleGroup, + val secondaryMuscles: List = emptyList(), + val equipment: EquipmentType = EquipmentType.CABLE, + val description: String = "", + val instructions: List = emptyList(), + val videoUrl: String? = null, + val thumbnailUrl: String? = null, + val defaultWeight: Float = 20f, + val defaultReps: Int = 10, + val defaultMode: WorkoutMode = WorkoutMode.OLD_SCHOOL +) + +/** + * Represents a custom workout routine. + */ +@Serializable +data class Routine( + val id: Long = 0, + val name: String, + val description: String? = null, + val exercises: List = emptyList(), + val createdAt: Long, + val updatedAt: Long +) + +/** + * Represents an exercise within a routine with specific configuration. + */ +@Serializable +data class RoutineExercise( + val id: Long = 0, + val routineId: Long, + val exercise: Exercise, + val orderIndex: Int, + val targetWeight: Float, + val targetReps: Int, + val targetSets: Int, + val mode: WorkoutMode +) diff --git a/shared/src/commonMain/kotlin/com/example/vitruvianredux/domain/model/WorkoutModels.kt b/shared/src/commonMain/kotlin/com/example/vitruvianredux/domain/model/WorkoutModels.kt new file mode 100644 index 000000000..2025db701 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/example/vitruvianredux/domain/model/WorkoutModels.kt @@ -0,0 +1,103 @@ +package com.example.vitruvianredux.domain.model + +import kotlinx.serialization.Serializable + +/** + * Represents the different workout modes available on Vitruvian machines. + */ +@Serializable +enum class WorkoutMode(val displayName: String, val description: String) { + OLD_SCHOOL( + displayName = "Old School", + description = "Traditional weight training - constant resistance throughout the movement" + ), + PUMP( + displayName = "Pump", + description = "Higher reps with moderate weight for muscle pump and endurance" + ), + TUT( + displayName = "Time Under Tension", + description = "Slower eccentric phase for increased time under tension" + ), + TUT_BEAST( + displayName = "TUT Beast", + description = "Extended time under tension with heavier loads" + ), + ECCENTRIC( + displayName = "Eccentric Only", + description = "Focus on the negative/lowering phase with increased resistance" + ), + ECHO( + displayName = "Echo", + description = "Adaptive resistance that matches your movement pattern" + ) +} + +/** + * Represents the current state of the Vitruvian machine connection. + */ +@Serializable +enum class ConnectionState { + DISCONNECTED, + SCANNING, + CONNECTING, + CONNECTED, + DISCONNECTING +} + +/** + * Real-time metrics from the Vitruvian machine during a workout. + */ +@Serializable +data class WorkoutMetrics( + val position: Float = 0f, + val velocity: Float = 0f, + val load: Float = 0f, + val power: Float = 0f, + val timestamp: Long = 0L +) + +/** + * Configuration for a workout set. + */ +@Serializable +data class WorkoutConfiguration( + val exerciseId: Int, + val exerciseName: String, + val mode: WorkoutMode, + val targetWeight: Float, + val targetReps: Int, + val useAmrap: Boolean = false +) + +/** + * Represents a completed workout session. + */ +@Serializable +data class WorkoutSession( + val id: Long = 0, + val exerciseId: Int, + val exerciseName: String, + val startTime: Long, + val endTime: Long? = null, + val mode: WorkoutMode, + val targetWeight: Float, + val targetReps: Int, + val completedReps: Int = 0, + val totalVolume: Float = 0f, + val metrics: List = emptyList() +) + +/** + * Represents a personal record for an exercise. + */ +@Serializable +data class PersonalRecord( + val id: Long = 0, + val exerciseId: Int, + val exerciseName: String, + val weight: Float, + val reps: Int, + val oneRepMax: Float, + val achievedAt: Long +) diff --git a/shared/src/commonMain/kotlin/com/example/vitruvianredux/util/Constants.kt b/shared/src/commonMain/kotlin/com/example/vitruvianredux/util/Constants.kt new file mode 100644 index 000000000..a7e446870 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/example/vitruvianredux/util/Constants.kt @@ -0,0 +1,85 @@ +package com.example.vitruvianredux.util + +/** + * Constants used throughout the application. + */ +object Constants { + // App version + const val APP_VERSION = "0.1.0" + + // Weight limits (in kg) + const val MIN_WEIGHT_KG = 0f + const val MAX_WEIGHT_KG = 220f + const val WEIGHT_INCREMENT_KG = 0.5f + + // Reps limits + const val MIN_REPS = 1 + const val MAX_REPS = 100 + + // BLE configuration + const val BLE_SCAN_TIMEOUT_MS = 10000L + const val BLE_CONNECTION_TIMEOUT_MS = 15000L + + // Workout detection thresholds + const val REP_DETECTION_THRESHOLD = 0.1f + const val POSITION_TOP_THRESHOLD = 0.95f + const val POSITION_BOTTOM_THRESHOLD = 0.05f + + // Data sampling + const val METRICS_SAMPLE_INTERVAL_MS = 50L +} + +/** + * Unit conversion utilities. + */ +object UnitConverter { + private const val KG_TO_LB = 2.20462f + private const val LB_TO_KG = 0.453592f + + /** + * Convert kilograms to pounds. + */ + fun kgToLb(kg: Float): Float = kg * KG_TO_LB + + /** + * Convert pounds to kilograms. + */ + fun lbToKg(lb: Float): Float = lb * LB_TO_KG + + /** + * Format weight for display with appropriate unit. + */ + fun formatWeight(kg: Float, useLb: Boolean): String { + return if (useLb) { + "${kgToLb(kg).toInt()} lb" + } else { + "${kg.toInt()} kg" + } + } +} + +/** + * Estimated one-rep max calculators. + */ +object OneRepMaxCalculator { + /** + * Calculate estimated 1RM using Brzycki formula. + * @param weight Weight lifted + * @param reps Number of reps completed + * @return Estimated one rep max + */ + fun brzycki(weight: Float, reps: Int): Float { + if (reps <= 0) return 0f + if (reps == 1) return weight + return weight * (36f / (37f - reps)) + } + + /** + * Calculate estimated 1RM using Epley formula. + */ + fun epley(weight: Float, reps: Int): Float { + if (reps <= 0) return 0f + if (reps == 1) return weight + return weight * (1f + reps / 30f) + } +} diff --git a/shared/src/commonMain/sqldelight/com/example/vitruvianredux/database/VitruvianDatabase.sq b/shared/src/commonMain/sqldelight/com/example/vitruvianredux/database/VitruvianDatabase.sq new file mode 100644 index 000000000..252bc130d --- /dev/null +++ b/shared/src/commonMain/sqldelight/com/example/vitruvianredux/database/VitruvianDatabase.sq @@ -0,0 +1,128 @@ +-- Vitruvian Project Phoenix Database Schema +-- This will be compiled by SQLDelight to generate type-safe Kotlin code + +-- Workout sessions table +CREATE TABLE WorkoutSession ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + exerciseId INTEGER NOT NULL, + exerciseName TEXT NOT NULL, + startTime INTEGER NOT NULL, + endTime INTEGER, + mode TEXT NOT NULL, + targetWeight REAL NOT NULL, + targetReps INTEGER NOT NULL, + completedReps INTEGER NOT NULL DEFAULT 0, + totalVolume REAL NOT NULL DEFAULT 0.0 +); + +-- Exercise metrics (samples during workout) +CREATE TABLE MetricSample ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sessionId INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + position REAL, + velocity REAL, + load REAL, + power REAL, + FOREIGN KEY (sessionId) REFERENCES WorkoutSession(id) ON DELETE CASCADE +); + +-- Personal records +CREATE TABLE PersonalRecord ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + exerciseId INTEGER NOT NULL, + exerciseName TEXT NOT NULL, + weight REAL NOT NULL, + reps INTEGER NOT NULL, + oneRepMax REAL NOT NULL, + achievedAt INTEGER NOT NULL +); + +-- Custom routines +CREATE TABLE Routine ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + createdAt INTEGER NOT NULL, + updatedAt INTEGER NOT NULL +); + +-- Routine exercises +CREATE TABLE RoutineExercise ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + routineId INTEGER NOT NULL, + exerciseId INTEGER NOT NULL, + exerciseName TEXT NOT NULL, + orderIndex INTEGER NOT NULL, + targetWeight REAL NOT NULL, + targetReps INTEGER NOT NULL, + targetSets INTEGER NOT NULL, + mode TEXT NOT NULL, + FOREIGN KEY (routineId) REFERENCES Routine(id) ON DELETE CASCADE +); + +-- Queries + +-- Get all workout sessions +selectAllSessions: +SELECT * FROM WorkoutSession ORDER BY startTime DESC; + +-- Get session by ID +selectSessionById: +SELECT * FROM WorkoutSession WHERE id = ?; + +-- Get recent sessions +selectRecentSessions: +SELECT * FROM WorkoutSession ORDER BY startTime DESC LIMIT ?; + +-- Insert session +insertSession: +INSERT INTO WorkoutSession (exerciseId, exerciseName, startTime, endTime, mode, targetWeight, targetReps, completedReps, totalVolume) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + +-- Update session +updateSession: +UPDATE WorkoutSession SET endTime = ?, completedReps = ?, totalVolume = ? WHERE id = ?; + +-- Get metrics for session +selectMetricsBySession: +SELECT * FROM MetricSample WHERE sessionId = ? ORDER BY timestamp ASC; + +-- Insert metric sample +insertMetric: +INSERT INTO MetricSample (sessionId, timestamp, position, velocity, load, power) +VALUES (?, ?, ?, ?, ?, ?); + +-- Get all personal records +selectAllRecords: +SELECT * FROM PersonalRecord ORDER BY achievedAt DESC; + +-- Get records for exercise +selectRecordsByExercise: +SELECT * FROM PersonalRecord WHERE exerciseId = ? ORDER BY oneRepMax DESC; + +-- Insert personal record +insertRecord: +INSERT INTO PersonalRecord (exerciseId, exerciseName, weight, reps, oneRepMax, achievedAt) +VALUES (?, ?, ?, ?, ?, ?); + +-- Get all routines +selectAllRoutines: +SELECT * FROM Routine ORDER BY name ASC; + +-- Get routine by ID +selectRoutineById: +SELECT * FROM Routine WHERE id = ?; + +-- Insert routine +insertRoutine: +INSERT INTO Routine (name, description, createdAt, updatedAt) VALUES (?, ?, ?, ?); + +-- Get exercises for routine +selectExercisesByRoutine: +SELECT * FROM RoutineExercise WHERE routineId = ? ORDER BY orderIndex ASC; + +-- Insert routine exercise +insertRoutineExercise: +INSERT INTO RoutineExercise (routineId, exerciseId, exerciseName, orderIndex, targetWeight, targetReps, targetSets, mode) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); diff --git a/shared/src/desktopMain/kotlin/com/example/vitruvianredux/Platform.desktop.kt b/shared/src/desktopMain/kotlin/com/example/vitruvianredux/Platform.desktop.kt new file mode 100644 index 000000000..bf5e5f83d --- /dev/null +++ b/shared/src/desktopMain/kotlin/com/example/vitruvianredux/Platform.desktop.kt @@ -0,0 +1,10 @@ +package com.example.vitruvianredux + +/** + * Desktop (JVM) platform implementation. + */ +class DesktopPlatform : Platform { + override val name: String = "Desktop ${System.getProperty("os.name")} ${System.getProperty("os.version")}" +} + +actual fun getPlatform(): Platform = DesktopPlatform() diff --git a/shared/src/iosMain/kotlin/com/example/vitruvianredux/Platform.ios.kt b/shared/src/iosMain/kotlin/com/example/vitruvianredux/Platform.ios.kt new file mode 100644 index 000000000..a1b2e1697 --- /dev/null +++ b/shared/src/iosMain/kotlin/com/example/vitruvianredux/Platform.ios.kt @@ -0,0 +1,12 @@ +package com.example.vitruvianredux + +import platform.UIKit.UIDevice + +/** + * iOS platform implementation. + */ +class IOSPlatform : Platform { + override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion +} + +actual fun getPlatform(): Platform = IOSPlatform()