diff --git a/README.md b/README.md index 18ed6df..e3d090f 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,24 @@ # MagicCounter -This is a FOSS Android app meant to allow MTG Commander players to keep track of player health and commander damage. This app is offline-only and requires no special permissions to run. Its built using Jetpack Compose with Material You support. +This is a FOSS Android and iOS app meant to allow MTG Commander players to keep track of player health and commander damage. This app is offline-only and requires no special permissions to run. ## Download +### Android + You have two options: 1. Download the latest APK from the Released page 2. [Obtainium](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.atridad.magiccounter%22%2C%22url%22%3A%22https%3A%2F%2Fgit.atri.dad%2Fatridad%2FMagicCounter%2Freleases%22%2C%22author%22%3A%22git.atri.dad%22%2C%22name%22%3A%22MagicCounter%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla%2F5.0%20(Linux%3B%20Android%2010%3B%20K)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F114.0.0.0%20Mobile%20Safari%2F537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22partialAPKHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D) +### iOS + +TBD + ## Requirements - Android 15+ +- iOS 17+ ## Contribution diff --git a/.gitignore b/android/.gitignore similarity index 100% rename from .gitignore rename to android/.gitignore diff --git a/.idea/.gitignore b/android/.idea/.gitignore similarity index 100% rename from .idea/.gitignore rename to android/.idea/.gitignore diff --git a/.idea/AndroidProjectSystem.xml b/android/.idea/AndroidProjectSystem.xml similarity index 100% rename from .idea/AndroidProjectSystem.xml rename to android/.idea/AndroidProjectSystem.xml diff --git a/.idea/appInsightsSettings.xml b/android/.idea/appInsightsSettings.xml similarity index 100% rename from .idea/appInsightsSettings.xml rename to android/.idea/appInsightsSettings.xml diff --git a/.idea/compiler.xml b/android/.idea/compiler.xml similarity index 100% rename from .idea/compiler.xml rename to android/.idea/compiler.xml diff --git a/.idea/deploymentTargetSelector.xml b/android/.idea/deploymentTargetSelector.xml similarity index 100% rename from .idea/deploymentTargetSelector.xml rename to android/.idea/deploymentTargetSelector.xml diff --git a/.idea/gradle.xml b/android/.idea/gradle.xml similarity index 100% rename from .idea/gradle.xml rename to android/.idea/gradle.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/android/.idea/inspectionProfiles/Project_Default.xml similarity index 100% rename from .idea/inspectionProfiles/Project_Default.xml rename to android/.idea/inspectionProfiles/Project_Default.xml diff --git a/.idea/kotlinc.xml b/android/.idea/kotlinc.xml similarity index 100% rename from .idea/kotlinc.xml rename to android/.idea/kotlinc.xml diff --git a/.idea/migrations.xml b/android/.idea/migrations.xml similarity index 100% rename from .idea/migrations.xml rename to android/.idea/migrations.xml diff --git a/.idea/misc.xml b/android/.idea/misc.xml similarity index 100% rename from .idea/misc.xml rename to android/.idea/misc.xml diff --git a/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml similarity index 100% rename from .idea/runConfigurations.xml rename to android/.idea/runConfigurations.xml diff --git a/.idea/vcs.xml b/android/.idea/vcs.xml similarity index 100% rename from .idea/vcs.xml rename to android/.idea/vcs.xml diff --git a/app/.gitignore b/android/app/.gitignore similarity index 100% rename from app/.gitignore rename to android/app/.gitignore diff --git a/app/build.gradle.kts b/android/app/build.gradle.kts similarity index 100% rename from app/build.gradle.kts rename to android/app/build.gradle.kts diff --git a/app/proguard-rules.pro b/android/app/proguard-rules.pro similarity index 100% rename from app/proguard-rules.pro rename to android/app/proguard-rules.pro diff --git a/app/src/androidTest/java/com/atridad/magiccounter/ExampleInstrumentedTest.kt b/android/app/src/androidTest/java/com/atridad/magiccounter/ExampleInstrumentedTest.kt similarity index 100% rename from app/src/androidTest/java/com/atridad/magiccounter/ExampleInstrumentedTest.kt rename to android/app/src/androidTest/java/com/atridad/magiccounter/ExampleInstrumentedTest.kt diff --git a/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml similarity index 100% rename from app/src/main/AndroidManifest.xml rename to android/app/src/main/AndroidManifest.xml diff --git a/app/src/main/java/com/atridad/magiccounter/MainActivity.kt b/android/app/src/main/java/com/atridad/magiccounter/MainActivity.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/MainActivity.kt rename to android/app/src/main/java/com/atridad/magiccounter/MainActivity.kt diff --git a/app/src/main/java/com/atridad/magiccounter/ui/MagicCounterApp.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/MagicCounterApp.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/ui/MagicCounterApp.kt rename to android/app/src/main/java/com/atridad/magiccounter/ui/MagicCounterApp.kt diff --git a/app/src/main/java/com/atridad/magiccounter/ui/screens/GameScreen.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/screens/GameScreen.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/ui/screens/GameScreen.kt rename to android/app/src/main/java/com/atridad/magiccounter/ui/screens/GameScreen.kt diff --git a/app/src/main/java/com/atridad/magiccounter/ui/screens/SetupScreen.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/screens/SetupScreen.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/ui/screens/SetupScreen.kt rename to android/app/src/main/java/com/atridad/magiccounter/ui/screens/SetupScreen.kt diff --git a/app/src/main/java/com/atridad/magiccounter/ui/settings/AppSettings.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/settings/AppSettings.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/ui/settings/AppSettings.kt rename to android/app/src/main/java/com/atridad/magiccounter/ui/settings/AppSettings.kt diff --git a/app/src/main/java/com/atridad/magiccounter/ui/settings/AppSettingsViewModel.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/settings/AppSettingsViewModel.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/ui/settings/AppSettingsViewModel.kt rename to android/app/src/main/java/com/atridad/magiccounter/ui/settings/AppSettingsViewModel.kt diff --git a/app/src/main/java/com/atridad/magiccounter/ui/state/GameState.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/state/GameState.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/ui/state/GameState.kt rename to android/app/src/main/java/com/atridad/magiccounter/ui/state/GameState.kt diff --git a/app/src/main/java/com/atridad/magiccounter/ui/theme/CustomIcons.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/theme/CustomIcons.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/ui/theme/CustomIcons.kt rename to android/app/src/main/java/com/atridad/magiccounter/ui/theme/CustomIcons.kt diff --git a/app/src/main/java/com/atridad/magiccounter/ui/theme/Theme.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/theme/Theme.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/ui/theme/Theme.kt rename to android/app/src/main/java/com/atridad/magiccounter/ui/theme/Theme.kt diff --git a/app/src/main/java/com/atridad/magiccounter/ui/theme/Type.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/theme/Type.kt similarity index 100% rename from app/src/main/java/com/atridad/magiccounter/ui/theme/Type.kt rename to android/app/src/main/java/com/atridad/magiccounter/ui/theme/Type.kt diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/android/app/src/main/res/drawable-hdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-hdpi/ic_launcher.png rename to android/app/src/main/res/drawable-hdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/android/app/src/main/res/drawable-mdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-mdpi/ic_launcher.png rename to android/app/src/main/res/drawable-mdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/ic_launcher.png rename to android/app/src/main/res/drawable-xhdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_launcher.png rename to android/app/src/main/res/drawable-xxhdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/ic_launcher.png rename to android/app/src/main/res/drawable-xxxhdpi/ic_launcher.png diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_foreground.xml rename to android/app/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/app/src/main/res/drawable/ic_menu_camera.xml b/android/app/src/main/res/drawable/ic_menu_camera.xml similarity index 100% rename from app/src/main/res/drawable/ic_menu_camera.xml rename to android/app/src/main/res/drawable/ic_menu_camera.xml diff --git a/app/src/main/res/drawable/ic_menu_gallery.xml b/android/app/src/main/res/drawable/ic_menu_gallery.xml similarity index 100% rename from app/src/main/res/drawable/ic_menu_gallery.xml rename to android/app/src/main/res/drawable/ic_menu_gallery.xml diff --git a/app/src/main/res/drawable/ic_menu_slideshow.xml b/android/app/src/main/res/drawable/ic_menu_slideshow.xml similarity index 100% rename from app/src/main/res/drawable/ic_menu_slideshow.xml rename to android/app/src/main/res/drawable/ic_menu_slideshow.xml diff --git a/app/src/main/res/drawable/side_nav_bar.xml b/android/app/src/main/res/drawable/side_nav_bar.xml similarity index 100% rename from app/src/main/res/drawable/side_nav_bar.xml rename to android/app/src/main/res/drawable/side_nav_bar.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher.webp rename to android/app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher.webp rename to android/app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/values-land/dimens.xml b/android/app/src/main/res/values-land/dimens.xml similarity index 100% rename from app/src/main/res/values-land/dimens.xml rename to android/app/src/main/res/values-land/dimens.xml diff --git a/app/src/main/res/values-night/themes.xml b/android/app/src/main/res/values-night/themes.xml similarity index 100% rename from app/src/main/res/values-night/themes.xml rename to android/app/src/main/res/values-night/themes.xml diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/android/app/src/main/res/values-w1240dp/dimens.xml similarity index 100% rename from app/src/main/res/values-w1240dp/dimens.xml rename to android/app/src/main/res/values-w1240dp/dimens.xml diff --git a/app/src/main/res/values-w600dp/dimens.xml b/android/app/src/main/res/values-w600dp/dimens.xml similarity index 100% rename from app/src/main/res/values-w600dp/dimens.xml rename to android/app/src/main/res/values-w600dp/dimens.xml diff --git a/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml similarity index 100% rename from app/src/main/res/values/colors.xml rename to android/app/src/main/res/values/colors.xml diff --git a/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml similarity index 100% rename from app/src/main/res/values/dimens.xml rename to android/app/src/main/res/values/dimens.xml diff --git a/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml similarity index 100% rename from app/src/main/res/values/strings.xml rename to android/app/src/main/res/values/strings.xml diff --git a/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml similarity index 100% rename from app/src/main/res/values/themes.xml rename to android/app/src/main/res/values/themes.xml diff --git a/app/src/main/res/xml/backup_rules.xml b/android/app/src/main/res/xml/backup_rules.xml similarity index 100% rename from app/src/main/res/xml/backup_rules.xml rename to android/app/src/main/res/xml/backup_rules.xml diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/android/app/src/main/res/xml/data_extraction_rules.xml similarity index 100% rename from app/src/main/res/xml/data_extraction_rules.xml rename to android/app/src/main/res/xml/data_extraction_rules.xml diff --git a/app/src/test/java/com/atridad/magiccounter/ExampleUnitTest.kt b/android/app/src/test/java/com/atridad/magiccounter/ExampleUnitTest.kt similarity index 100% rename from app/src/test/java/com/atridad/magiccounter/ExampleUnitTest.kt rename to android/app/src/test/java/com/atridad/magiccounter/ExampleUnitTest.kt diff --git a/build.gradle.kts b/android/build.gradle.kts similarity index 100% rename from build.gradle.kts rename to android/build.gradle.kts diff --git a/gradle.properties b/android/gradle.properties similarity index 100% rename from gradle.properties rename to android/gradle.properties diff --git a/gradle/libs.versions.toml b/android/gradle/libs.versions.toml similarity index 100% rename from gradle/libs.versions.toml rename to android/gradle/libs.versions.toml diff --git a/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from gradle/wrapper/gradle-wrapper.jar rename to android/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from gradle/wrapper/gradle-wrapper.properties rename to android/gradle/wrapper/gradle-wrapper.properties diff --git a/gradlew b/android/gradlew similarity index 100% rename from gradlew rename to android/gradlew diff --git a/gradlew.bat b/android/gradlew.bat similarity index 100% rename from gradlew.bat rename to android/gradlew.bat diff --git a/settings.gradle.kts b/android/settings.gradle.kts similarity index 100% rename from settings.gradle.kts rename to android/settings.gradle.kts diff --git a/ios/MagicCounter.xcodeproj/project.pbxproj b/ios/MagicCounter.xcodeproj/project.pbxproj new file mode 100644 index 0000000..54dccbe --- /dev/null +++ b/ios/MagicCounter.xcodeproj/project.pbxproj @@ -0,0 +1,601 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + D2DB9B832EE4DD5100372366 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D2DB9B6B2EE4DD5000372366 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2DB9B722EE4DD5000372366; + remoteInfo = MagicCounter; + }; + D2DB9B8D2EE4DD5100372366 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D2DB9B6B2EE4DD5000372366 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2DB9B722EE4DD5000372366; + remoteInfo = MagicCounter; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + D2D1FABE2EE4DFF4000700F5 /* MagicCounter.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = MagicCounter.xcodeproj; sourceTree = ""; }; + D2DB9B732EE4DD5000372366 /* MagicCounter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MagicCounter.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D2DB9B822EE4DD5100372366 /* MagicCounterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MagicCounterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D2DB9B8C2EE4DD5100372366 /* MagicCounterUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MagicCounterUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + D2DB9B752EE4DD5000372366 /* MagicCounter */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = MagicCounter; + sourceTree = ""; + }; + D2DB9B852EE4DD5100372366 /* MagicCounterTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = MagicCounterTests; + sourceTree = ""; + }; + D2DB9B8F2EE4DD5100372366 /* MagicCounterUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = MagicCounterUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + D2DB9B702EE4DD5000372366 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DB9B7F2EE4DD5100372366 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DB9B892EE4DD5100372366 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D2D1FAC22EE4DFF4000700F5 /* Products */ = { + isa = PBXGroup; + children = ( + ); + name = Products; + sourceTree = ""; + }; + D2DB9B6A2EE4DD5000372366 = { + isa = PBXGroup; + children = ( + D2DB9B752EE4DD5000372366 /* MagicCounter */, + D2DB9B852EE4DD5100372366 /* MagicCounterTests */, + D2DB9B8F2EE4DD5100372366 /* MagicCounterUITests */, + D2DB9B742EE4DD5000372366 /* Products */, + ); + sourceTree = ""; + }; + D2DB9B742EE4DD5000372366 /* Products */ = { + isa = PBXGroup; + children = ( + D2DB9B732EE4DD5000372366 /* MagicCounter.app */, + D2DB9B822EE4DD5100372366 /* MagicCounterTests.xctest */, + D2DB9B8C2EE4DD5100372366 /* MagicCounterUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D2DB9B722EE4DD5000372366 /* MagicCounter */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2DB9B962EE4DD5100372366 /* Build configuration list for PBXNativeTarget "MagicCounter" */; + buildPhases = ( + D2DB9B6F2EE4DD5000372366 /* Sources */, + D2DB9B702EE4DD5000372366 /* Frameworks */, + D2DB9B712EE4DD5000372366 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + D2DB9B752EE4DD5000372366 /* MagicCounter */, + ); + name = MagicCounter; + packageProductDependencies = ( + ); + productName = MagicCounter; + productReference = D2DB9B732EE4DD5000372366 /* MagicCounter.app */; + productType = "com.apple.product-type.application"; + }; + D2DB9B812EE4DD5100372366 /* MagicCounterTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2DB9B992EE4DD5100372366 /* Build configuration list for PBXNativeTarget "MagicCounterTests" */; + buildPhases = ( + D2DB9B7E2EE4DD5100372366 /* Sources */, + D2DB9B7F2EE4DD5100372366 /* Frameworks */, + D2DB9B802EE4DD5100372366 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D2DB9B842EE4DD5100372366 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + D2DB9B852EE4DD5100372366 /* MagicCounterTests */, + ); + name = MagicCounterTests; + packageProductDependencies = ( + ); + productName = MagicCounterTests; + productReference = D2DB9B822EE4DD5100372366 /* MagicCounterTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D2DB9B8B2EE4DD5100372366 /* MagicCounterUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2DB9B9C2EE4DD5100372366 /* Build configuration list for PBXNativeTarget "MagicCounterUITests" */; + buildPhases = ( + D2DB9B882EE4DD5100372366 /* Sources */, + D2DB9B892EE4DD5100372366 /* Frameworks */, + D2DB9B8A2EE4DD5100372366 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D2DB9B8E2EE4DD5100372366 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + D2DB9B8F2EE4DD5100372366 /* MagicCounterUITests */, + ); + name = MagicCounterUITests; + packageProductDependencies = ( + ); + productName = MagicCounterUITests; + productReference = D2DB9B8C2EE4DD5100372366 /* MagicCounterUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D2DB9B6B2EE4DD5000372366 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2610; + LastUpgradeCheck = 2610; + TargetAttributes = { + D2DB9B722EE4DD5000372366 = { + CreatedOnToolsVersion = 26.1.1; + }; + D2DB9B812EE4DD5100372366 = { + CreatedOnToolsVersion = 26.1.1; + TestTargetID = D2DB9B722EE4DD5000372366; + }; + D2DB9B8B2EE4DD5100372366 = { + CreatedOnToolsVersion = 26.1.1; + TestTargetID = D2DB9B722EE4DD5000372366; + }; + }; + }; + buildConfigurationList = D2DB9B6E2EE4DD5000372366 /* Build configuration list for PBXProject "MagicCounter" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D2DB9B6A2EE4DD5000372366; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = D2DB9B742EE4DD5000372366 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = D2D1FAC22EE4DFF4000700F5 /* Products */; + ProjectRef = D2D1FABE2EE4DFF4000700F5 /* MagicCounter.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + D2DB9B722EE4DD5000372366 /* MagicCounter */, + D2DB9B812EE4DD5100372366 /* MagicCounterTests */, + D2DB9B8B2EE4DD5100372366 /* MagicCounterUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D2DB9B712EE4DD5000372366 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DB9B802EE4DD5100372366 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DB9B8A2EE4DD5100372366 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D2DB9B6F2EE4DD5000372366 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DB9B7E2EE4DD5100372366 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DB9B882EE4DD5100372366 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + D2DB9B842EE4DD5100372366 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2DB9B722EE4DD5000372366 /* MagicCounter */; + targetProxy = D2DB9B832EE4DD5100372366 /* PBXContainerItemProxy */; + }; + D2DB9B8E2EE4DD5100372366 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2DB9B722EE4DD5000372366 /* MagicCounter */; + targetProxy = D2DB9B8D2EE4DD5100372366 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + D2DB9B942EE4DD5100372366 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 4BC9Y2LL4B; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + D2DB9B952EE4DD5100372366 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 4BC9Y2LL4B; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + D2DB9B972EE4DD5100372366 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Logo; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4BC9Y2LL4B; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.MagicCounter.MagicCounter; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D2DB9B982EE4DD5100372366 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Logo; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4BC9Y2LL4B; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.MagicCounter.MagicCounter; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D2DB9B9A2EE4DD5100372366 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4BC9Y2LL4B; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.MagicCounter.MagicCounterTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MagicCounter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MagicCounter"; + }; + name = Debug; + }; + D2DB9B9B2EE4DD5100372366 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4BC9Y2LL4B; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.MagicCounter.MagicCounterTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MagicCounter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MagicCounter"; + }; + name = Release; + }; + D2DB9B9D2EE4DD5100372366 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4BC9Y2LL4B; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.MagicCounter.MagicCounterUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MagicCounter; + }; + name = Debug; + }; + D2DB9B9E2EE4DD5100372366 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4BC9Y2LL4B; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.MagicCounter.MagicCounterUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MagicCounter; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D2DB9B6E2EE4DD5000372366 /* Build configuration list for PBXProject "MagicCounter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2DB9B942EE4DD5100372366 /* Debug */, + D2DB9B952EE4DD5100372366 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2DB9B962EE4DD5100372366 /* Build configuration list for PBXNativeTarget "MagicCounter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2DB9B972EE4DD5100372366 /* Debug */, + D2DB9B982EE4DD5100372366 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2DB9B992EE4DD5100372366 /* Build configuration list for PBXNativeTarget "MagicCounterTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2DB9B9A2EE4DD5100372366 /* Debug */, + D2DB9B9B2EE4DD5100372366 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2DB9B9C2EE4DD5100372366 /* Build configuration list for PBXNativeTarget "MagicCounterUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2DB9B9D2EE4DD5100372366 /* Debug */, + D2DB9B9E2EE4DD5100372366 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D2DB9B6B2EE4DD5000372366 /* Project object */; +} diff --git a/ios/MagicCounter.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/MagicCounter.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/MagicCounter.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/MagicCounter.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/MagicCounter.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..0707aeb Binary files /dev/null and b/ios/MagicCounter.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/MagicCounter.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist b/ios/MagicCounter.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..5776e2c --- /dev/null +++ b/ios/MagicCounter.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + MagicCounter.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/ios/MagicCounter/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/MagicCounter/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/ios/MagicCounter/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/MagicCounter/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/MagicCounter/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/ios/MagicCounter/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/MagicCounter/Assets.xcassets/Contents.json b/ios/MagicCounter/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ios/MagicCounter/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/MagicCounter/Components.swift b/ios/MagicCounter/Components.swift new file mode 100644 index 0000000..6f1110e --- /dev/null +++ b/ios/MagicCounter/Components.swift @@ -0,0 +1,114 @@ +// +// Components.swift +// MagicCounter +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import SwiftUI + +/** + * A circular button with an icon, used for game controls. + */ +struct CircleButton: View { + let icon: String + let color: Color + let size: CGFloat + let action: () -> Void + + init(icon: String, color: Color, size: CGFloat = 44, action: @escaping () -> Void) { + self.icon = icon + self.color = color + self.size = size + self.action = action + } + + var body: some View { + Button(action: action) { + Image(systemName: icon) + .font(size < 40 ? .caption : .title3) + .frame(width: size, height: size) + .background(color.opacity(0.2)) + .clipShape(Circle()) + .foregroundStyle(color) + } + } +} + +/** + * A control for adjusting a large numerical value (like Life). + */ +struct LifeCounterControl: View { + let value: Int + let onDecrease: () -> Void + let onIncrease: () -> Void + + var body: some View { + HStack(spacing: 16) { + CircleButton(icon: "minus", color: .red, action: onDecrease) + + Text("\(value)") + .font(.system(size: 56, weight: .bold, design: .rounded)) + .minimumScaleFactor(0.5) + .lineLimit(1) + .foregroundStyle(.primary) + + CircleButton(icon: "plus", color: .green, action: onIncrease) + } + .padding(.horizontal, 16) + } +} + +/** + * A smaller control for adjusting secondary values (like Poison). + */ +struct SmallCounterControl: View { + let value: Int + let icon: String + let color: Color + let onDecrease: () -> Void + let onIncrease: () -> Void + + var body: some View { + HStack(spacing: 4) { + CircleButton(icon: "minus", color: .gray, size: 24, action: onDecrease) + + VStack(spacing: 0) { + Image(systemName: icon) + .font(.caption2) + .foregroundStyle(color) + Text("\(value)") + .font(.title3.bold()) + .foregroundStyle(color) + } + .frame(minWidth: 30) + + CircleButton(icon: "plus", color: .gray, size: 24, action: onIncrease) + } + .padding(6) + .background(color.opacity(0.1)) + .cornerRadius(12) + } +} + +/** + * A reusable slider row for settings. + */ +struct SettingSlider: View { + let title: String + let value: Binding + let range: ClosedRange + let step: Double + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(title) + Spacer() + Text("\(Int(value.wrappedValue))") + .bold() + } + Slider(value: value, in: range, step: step) + } + } +} diff --git a/ios/MagicCounter/ContentView.swift b/ios/MagicCounter/ContentView.swift new file mode 100644 index 0000000..61d998f --- /dev/null +++ b/ios/MagicCounter/ContentView.swift @@ -0,0 +1,129 @@ +// +// ContentView.swift +// MagicCounter +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import SwiftUI + +struct ContentView: View { + @EnvironmentObject var gameManager: GameManager + @State private var showSetup = false + @AppStorage("accentColorName") private var accentColorName = "Blue" + + var selectedColor: Color { + switch accentColorName { + case "Blue": return .blue + case "Purple": return .purple + case "Pink": return .pink + case "Red": return .red + case "Orange": return .orange + case "Green": return .green + case "Teal": return .teal + case "Indigo": return .indigo + case "Mint": return .mint + case "Brown": return .brown + case "Cyan": return .cyan + default: return .blue + } + } + + var body: some View { + Group { + if let activeMatch = gameManager.activeMatch { + GameView(match: activeMatch) + .transition(.move(edge: .bottom)) + } else { + TabView { + NavigationStack { + List { + if gameManager.matchHistory.isEmpty { + ContentUnavailableView("No Matches", systemImage: "gamecontroller", description: Text("Start a new game to begin tracking.")) + } else { + ForEach(gameManager.matchHistory) { match in + MatchHistoryRow(match: match) + .contentShape(Rectangle()) // Ensure the whole row is tappable + .onTapGesture { + withAnimation { + gameManager.resumeMatch(match) + } + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button(role: .destructive) { + withAnimation { + gameManager.deleteMatch(id: match.id) + } + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) + } + } + } + } + .listStyle(.insetGrouped) + .navigationTitle("History") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(action: { showSetup = true }) { + Image(systemName: "plus") + } + } + } + } + .tabItem { + Label("History", systemImage: "clock.fill") + } + + SettingsView() + .tabItem { + Label("Settings", systemImage: "gear") + } + } + } + } + .tint(selectedColor) + .sheet(isPresented: $showSetup) { + SetupView() + .tint(selectedColor) + } + } +} + +struct MatchHistoryRow: View { + let match: MatchRecord + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text(match.name) + .font(.headline) + .foregroundStyle(.primary) + Text(match.startedAt.formatted(date: .abbreviated, time: .shortened)) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + + if match.state.stopped { + Text("Stopped") + .font(.caption) + .foregroundStyle(.secondary) + } else if let winner = match.state.winner { + Text("Winner: \(winner.name)") + .font(.caption) + .foregroundStyle(.secondary) + } else { + Text("Ongoing") + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.green.opacity(0.2)) + .foregroundStyle(.green) + .cornerRadius(8) + } + } + .padding(.vertical, 8) + } +} diff --git a/ios/MagicCounter/GameManager.swift b/ios/MagicCounter/GameManager.swift new file mode 100644 index 0000000..6da2954 --- /dev/null +++ b/ios/MagicCounter/GameManager.swift @@ -0,0 +1,137 @@ +// +// GameManager.swift +// MagicCounter +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import Foundation +import SwiftUI +import Combine + +/** + * Manages the global game state and match history. + * + * - matchHistory: List of all past and current matches. + * - activeMatch: The currently active match, if any. + */ +@MainActor +final class GameManager: ObservableObject { + @Published var matchHistory: [MatchRecord] = [] + @Published var activeMatch: MatchRecord? + + private let historyKey = "match_history_json" + private let saveSubject = PassthroughSubject() + private var cancellables = Set() + + nonisolated init() { + Task { @MainActor in + self.loadHistory() + self.setupAutoSave() + } + } + + private func setupAutoSave() { + saveSubject + .debounce(for: .seconds(1), scheduler: RunLoop.main) + .sink { [weak self] _ in + self?.saveHistory() + } + .store(in: &cancellables) + } + + func startNewGame(players: [String], startingLife: Int, trackPoison: Bool, trackCommander: Bool, matchName: String) { + let playerStates = players.enumerated().map { (index, name) in + PlayerState( + id: index, + name: name, + life: startingLife, + poison: 0, + commanderDamages: [:], + scooped: false + ) + } + + let gameState = GameState( + players: playerStates, + startingLife: startingLife, + trackPoison: trackPoison, + trackCommanderDamage: trackCommander, + stopped: false + ) + + let newMatch = MatchRecord( + id: UUID().uuidString, + name: matchName.isEmpty ? "Match \(Date().formatted())" : matchName, + startedAt: Date(), + lastUpdated: Date(), + ongoing: true, + winnerPlayerId: nil, + state: gameState + ) + + activeMatch = newMatch + matchHistory.insert(newMatch, at: 0) + saveHistory() + } + + func updateActiveGame(state: GameState) { + guard var match = activeMatch else { return } + match.state = state + match.lastUpdated = Date() + + if state.stopped { + match.ongoing = false + } else if let winner = state.winner { + match.ongoing = false + match.winnerPlayerId = winner.id + } else { + match.ongoing = true + match.winnerPlayerId = nil + } + + activeMatch = match + if let index = matchHistory.firstIndex(where: { $0.id == match.id }) { + matchHistory[index] = match + } + saveSubject.send() + } + + func stopGame() { + guard var match = activeMatch else { return } + match.ongoing = false + match.state.stopped = true + activeMatch = nil + + if let index = matchHistory.firstIndex(where: { $0.id == match.id }) { + matchHistory[index] = match + } + saveHistory() + } + + func deleteMatch(id: String) { + if activeMatch?.id == id { + activeMatch = nil + } + matchHistory.removeAll { $0.id == id } + saveHistory() + } + + func resumeMatch(_ match: MatchRecord) { + activeMatch = match + } + + private func loadHistory() { + if let data = UserDefaults.standard.data(forKey: historyKey) { + if let decoded = try? JSONDecoder().decode([MatchRecord].self, from: data) { + matchHistory = decoded + } + } + } + + private func saveHistory() { + if let encoded = try? JSONEncoder().encode(matchHistory) { + UserDefaults.standard.set(encoded, forKey: historyKey) + } + } +} diff --git a/ios/MagicCounter/GameView.swift b/ios/MagicCounter/GameView.swift new file mode 100644 index 0000000..320302c --- /dev/null +++ b/ios/MagicCounter/GameView.swift @@ -0,0 +1,338 @@ +// +// GameView.swift +// MagicCounter +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import SwiftUI + +/** + * Main game screen displaying the grid of players. + * + * - match: The match record to initialize the game state from. + */ +struct GameView: View { + @EnvironmentObject var gameManager: GameManager + @State private var gameState: GameState + @State private var showingStopConfirmation = false + @State private var selectedPlayerForCommander: PlayerState? + + init(match: MatchRecord) { + self._gameState = State(initialValue: match.state) + } + + var body: some View { + ZStack { + // Background + LinearGradient(colors: [Color.black, Color.indigo.opacity(0.5)], startPoint: .top, endPoint: .bottom) + .ignoresSafeArea() + + VStack(spacing: 0) { + // Custom Toolbar + HStack { + Button(action: { gameManager.activeMatch = nil }) { + Image(systemName: "xmark") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.white) + .padding(10) + .background(.ultraThinMaterial) + .clipShape(Circle()) + } + + Spacer() + + if gameState.stopped { + Text("Game Stopped") + .font(.headline) + .foregroundStyle(.red) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(.ultraThinMaterial) + .cornerRadius(8) + } else if let winner = gameState.winner { + Text("Winner: \(winner.name)") + .font(.headline) + .foregroundStyle(.green) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(.ultraThinMaterial) + .cornerRadius(8) + } else { + Text("Magic Counter") + .font(.headline) + .foregroundStyle(.white) + } + + Spacer() + + if !gameState.stopped && gameState.winner == nil { + Button(action: { showingStopConfirmation = true }) { + Image(systemName: "stop.fill") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.white) + .padding(10) + .background(.ultraThinMaterial) + .clipShape(Circle()) + } + } else { + // Placeholder for balance + Color.clear.frame(width: 40, height: 40) + } + } + .padding() + .background(.ultraThinMaterial) + + // Players Grid + GeometryReader { geometry in + // Use adaptive grid with minimum width to handle responsiveness + // 320pt minimum ensures 1 column on iPhone Portrait (width ~390) + // and 2 columns on iPhone Landscape (width ~844) or iPad + let columns = [GridItem(.adaptive(minimum: 320), spacing: 24)] + ScrollView { + LazyVGrid(columns: columns, spacing: 24) { + ForEach(gameState.players) { player in + PlayerCell( + player: player, + gameState: gameState, + isWinner: gameState.winner?.id == player.id, + onUpdate: updatePlayer, + onCommanderTap: { selectedPlayerForCommander = player }, + onScoop: { scoopPlayer(player) } + ) + .frame(height: 260) + } + } + .padding(24) + } + } + } + } + .alert("Stop Game?", isPresented: $showingStopConfirmation) { + Button("Cancel", role: .cancel) { } + Button("Stop", role: .destructive) { + gameManager.stopGame() + gameState.stopped = true + } + } message: { + Text("Are you sure you want to stop this game?") + } + .sheet(item: $selectedPlayerForCommander) { player in + CommanderDamageView( + targetPlayer: player, + gameState: gameState, + onUpdate: { updatedPlayer in + updatePlayer(player: updatedPlayer) + } + ) + .presentationDetents([.medium]) + } + .onChange(of: gameState) { newState in + gameManager.updateActiveGame(state: newState) + } + } + + private func updatePlayer(player: PlayerState) { + if gameState.stopped || gameState.winner != nil { return } + + if let index = gameState.players.firstIndex(where: { $0.id == player.id }) { + gameState.players[index] = player + } + + // Update the sheet state if this is the player being edited to ensure the view refreshes + if selectedPlayerForCommander?.id == player.id { + selectedPlayerForCommander = player + } + } + + private func scoopPlayer(_ player: PlayerState) { + if gameState.stopped || gameState.winner != nil { return } + + var newPlayer = player + newPlayer.scooped = true + updatePlayer(player: newPlayer) + } +} + +/** + * Individual player cell component. + * + * - player: The player state to display. + * - gameState: The global game state. + * - isWinner: Whether this player is the winner. + * - onUpdate: Callback to update the player state. + * - onCommanderTap: Callback when commander damage button is tapped. + * - onScoop: Callback when scoop action is triggered. + */ +struct PlayerCell: View { + let player: PlayerState + let gameState: GameState + let isWinner: Bool + let onUpdate: (PlayerState) -> Void + let onCommanderTap: () -> Void + let onScoop: () -> Void + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 24) + .fill(.ultraThinMaterial) + .shadow(color: isWinner ? .green.opacity(0.5) : .black.opacity(0.2), radius: isWinner ? 20 : 10, x: 0, y: 5) + .overlay( + RoundedRectangle(cornerRadius: 24) + .stroke(isWinner ? Color.green : Color.clear, lineWidth: 3) + ) + + VStack(spacing: 12) { + // Header + HStack { + Text(player.name) + .font(.headline) + .lineLimit(1) + .minimumScaleFactor(0.8) + .foregroundStyle(.primary) + Spacer() + + Menu { + Button(role: .destructive, action: onScoop) { + Label("Scoop", systemImage: "flag.fill") + } + } label: { + Image(systemName: "ellipsis.circle") + .foregroundStyle(.secondary) + .padding(4) + } + } + .padding(.horizontal) + .padding(.top, 12) + + // Life + LifeCounterControl( + value: player.life, + onDecrease: { adjustLife(by: -1) }, + onIncrease: { adjustLife(by: 1) } + ) + + Spacer() + + // Counters Row + HStack(spacing: 12) { + if gameState.trackPoison { + SmallCounterControl( + value: player.poison, + icon: "drop.fill", + color: .purple, + onDecrease: { adjustPoison(by: -1) }, + onIncrease: { adjustPoison(by: 1) } + ) + } + + if gameState.trackCommanderDamage { + Button(action: onCommanderTap) { + VStack(spacing: 2) { + Image(systemName: "shield.fill") + .font(.caption) + Text("CMD") + .font(.caption2.bold()) + } + .frame(width: 44, height: 44) + .background(Color.orange.opacity(0.1)) + .foregroundStyle(.orange) + .clipShape(Circle()) + } + } + } + .padding(.bottom, 16) + } + + if player.isEliminated && !isWinner { + ZStack { + Color.black.opacity(0.6) + .cornerRadius(24) + Image(systemName: "xmark") + .font(.system(size: 60, weight: .bold)) + .foregroundStyle(.white.opacity(0.8)) + } + } + } + .opacity(player.isEliminated && !isWinner ? 0.8 : 1) + .scaleEffect(isWinner ? 1.05 : 1) + .animation(.spring, value: isWinner) + } + + private func adjustLife(by amount: Int) { + if player.isEliminated { return } + var newPlayer = player + newPlayer.life += amount + onUpdate(newPlayer) + } + + private func adjustPoison(by amount: Int) { + if player.isEliminated { return } + var newPlayer = player + newPlayer.poison = max(0, newPlayer.poison + amount) + onUpdate(newPlayer) + } +} + +/** + * Sheet for managing commander damage received by a player. + * + * - targetPlayer: The player receiving damage. + * - gameState: The global game state. + * - onUpdate: Callback to update the player state. + */ +struct CommanderDamageView: View { + @Environment(\.dismiss) var dismiss + let targetPlayer: PlayerState + let gameState: GameState + let onUpdate: (PlayerState) -> Void + + var body: some View { + NavigationStack { + List { + ForEach(gameState.players.filter { $0.id != targetPlayer.id }) { attacker in + HStack { + Text(attacker.name) + .font(.headline) + Spacer() + HStack(spacing: 16) { + CircleButton( + icon: "minus", + color: .secondary, + action: { adjustCommanderDamage(attackerId: attacker.id, by: -1) } + ) + + Text("\(targetPlayer.commanderDamages[attacker.id] ?? 0)") + .font(.title.bold()) + .frame(minWidth: 40) + .multilineTextAlignment(.center) + + CircleButton( + icon: "plus", + color: .primary, + action: { adjustCommanderDamage(attackerId: attacker.id, by: 1) } + ) + } + } + .padding(.vertical, 8) + } + } + .navigationTitle("Commander Damage to \(targetPlayer.name)") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { dismiss() } + } + } + } + } + + private func adjustCommanderDamage(attackerId: Int, by amount: Int) { + var newPlayer = targetPlayer + var damages = newPlayer.commanderDamages + let current = damages[attackerId] ?? 0 + damages[attackerId] = max(0, current + amount) + newPlayer.commanderDamages = damages + onUpdate(newPlayer) + } +} diff --git a/ios/MagicCounter/Item.swift b/ios/MagicCounter/Item.swift new file mode 100644 index 0000000..70b6495 --- /dev/null +++ b/ios/MagicCounter/Item.swift @@ -0,0 +1,18 @@ +// +// Item.swift +// MagicCounter +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import Foundation +import SwiftData + +@Model +final class Item { + var timestamp: Date + + init(timestamp: Date) { + self.timestamp = timestamp + } +} diff --git a/ios/MagicCounter/Logo.icon/Assets/MagicCounter 2.png b/ios/MagicCounter/Logo.icon/Assets/MagicCounter 2.png new file mode 100644 index 0000000..412d3de Binary files /dev/null and b/ios/MagicCounter/Logo.icon/Assets/MagicCounter 2.png differ diff --git a/ios/MagicCounter/Logo.icon/icon.json b/ios/MagicCounter/Logo.icon/icon.json new file mode 100644 index 0000000..7be127c --- /dev/null +++ b/ios/MagicCounter/Logo.icon/icon.json @@ -0,0 +1,37 @@ +{ + "color-space-for-untagged-svg-colors" : "display-p3", + "fill" : { + "automatic-gradient" : "display-p3:0.20140,0.16683,0.29537,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "image-name" : "MagicCounter 2.png", + "name" : "MagicCounter 2", + "position" : { + "scale" : 0.8, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/ios/MagicCounter/MagicCounterApp.swift b/ios/MagicCounter/MagicCounterApp.swift new file mode 100644 index 0000000..ffcfff7 --- /dev/null +++ b/ios/MagicCounter/MagicCounterApp.swift @@ -0,0 +1,21 @@ +// +// MagicCounterApp.swift +// MagicCounter +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import SwiftUI + +@main +struct MagicCounterApp: App { + @StateObject private var gameManager = GameManager() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(gameManager) + .preferredColorScheme(.dark) // Force dark mode for that "Liquid Glass" feel usually looks better, or let it adapt. + } + } +} diff --git a/ios/MagicCounter/Models.swift b/ios/MagicCounter/Models.swift new file mode 100644 index 0000000..7e4ec5b --- /dev/null +++ b/ios/MagicCounter/Models.swift @@ -0,0 +1,50 @@ +// +// Models.swift +// MagicCounter +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import Foundation + +// A single player's state +struct PlayerState: Codable, Identifiable, Equatable { + var id: Int + var name: String + var life: Int + var poison: Int + var commanderDamages: [Int: Int] // attackerId: damage + var scooped: Bool + + var isEliminated: Bool { + life <= 0 || scooped || commanderDamages.values.contains { $0 >= 21 } || poison >= 10 + } +} + +// The full game state +struct GameState: Codable, Equatable { + var players: [PlayerState] + var startingLife: Int + var trackPoison: Bool + var trackCommanderDamage: Bool + var stopped: Bool + + var winner: PlayerState? { + let activePlayers = players.filter { !$0.isEliminated } + if activePlayers.count == 1 { + return activePlayers.first + } + return nil + } +} + +// A record of a match +struct MatchRecord: Codable, Identifiable, Equatable { + var id: String + var name: String + var startedAt: Date + var lastUpdated: Date + var ongoing: Bool + var winnerPlayerId: Int? + var state: GameState +} diff --git a/ios/MagicCounter/SettingsView.swift b/ios/MagicCounter/SettingsView.swift new file mode 100644 index 0000000..9737af9 --- /dev/null +++ b/ios/MagicCounter/SettingsView.swift @@ -0,0 +1,99 @@ +// +// SettingsView.swift +// MagicCounter +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import SwiftUI + +struct SettingsView: View { + @AppStorage("accentColorName") private var accentColorName = "Blue" + + private let colors: [(name: String, color: Color)] = [ + ("Blue", .blue), + ("Purple", .purple), + ("Pink", .pink), + ("Red", .red), + ("Orange", .orange), + ("Green", .green), + ("Teal", .teal), + ("Indigo", .indigo), + ("Mint", .mint), + ("Brown", .brown), + ("Cyan", .cyan) + ] + + private var currentVersion: String { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" + } + + private var buildNumber: String { + Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" + } + + private func foregroundColor(for colorName: String) -> Color { + switch colorName { + case "Mint", "Cyan", "Yellow": + return .black + default: + return .white + } + } + + var body: some View { + NavigationStack { + Form { + Section("Appearance") { + VStack(alignment: .leading, spacing: 16) { + Text("ACCENT COLOR") + .font(.caption) + .foregroundStyle(.secondary) + + LazyVGrid(columns: [GridItem(.adaptive(minimum: 44))], spacing: 12) { + ForEach(colors, id: \.name) { item in + Circle() + .fill(item.color) + .frame(width: 44, height: 44) + .overlay { + if accentColorName == item.name { + Image(systemName: "checkmark") + .font(.headline) + .foregroundStyle(foregroundColor(for: item.name)) + } + } + .onTapGesture { + accentColorName = item.name + } + } + } + + Divider() + + Button("Reset to Default") { + accentColorName = "Blue" + } + .foregroundStyle(.red) + } + .padding(.vertical, 8) + } + + Section("About") { + HStack { + Text("App Name") + Spacer() + Text("Magic Counter") + .foregroundStyle(.secondary) + } + HStack { + Text("Version") + Spacer() + Text("\(currentVersion) (\(buildNumber))") + .foregroundStyle(.secondary) + } + } + } + .navigationTitle("Settings") + } + } +} diff --git a/ios/MagicCounter/SetupView.swift b/ios/MagicCounter/SetupView.swift new file mode 100644 index 0000000..5313376 --- /dev/null +++ b/ios/MagicCounter/SetupView.swift @@ -0,0 +1,100 @@ +// +// SetupView.swift +// MagicCounter +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import SwiftUI + +/** + * Screen for configuring a new game. + * + * Allows setting player count, starting life, and game options. + */ +struct SetupView: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var gameManager: GameManager + + @State private var playerCount: Double = 4 + @State private var startingLife: Double = 40 + @State private var trackPoison = true + @State private var trackCommander = true + @State private var matchName = "" + @State private var playerNames: [String] = [] + + var body: some View { + NavigationStack { + Form { + Section("Game Settings") { + SettingSlider( + title: "Starting Life", + value: $startingLife, + range: 10...40, + step: 5 + ) + + SettingSlider( + title: "Players", + value: $playerCount, + range: 2...8, + step: 1 + ) + .onChange(of: playerCount) { newValue in + updatePlayerNames(count: Int(newValue)) + } + } + + Section("Options") { + Toggle("Track Poison", isOn: $trackPoison) + Toggle("Track Commander Damage", isOn: $trackCommander) + TextField("Match Name (Optional)", text: $matchName) + } + + Section("Player Names") { + ForEach(0.. count { + playerNames.removeLast() + } + } + + private func startGame() { + gameManager.startNewGame( + players: playerNames, + startingLife: Int(startingLife), + trackPoison: trackPoison, + trackCommander: trackCommander, + matchName: matchName + ) + dismiss() + } +} diff --git a/ios/MagicCounterTests/MagicCounterTests.swift b/ios/MagicCounterTests/MagicCounterTests.swift new file mode 100644 index 0000000..8b56ec9 --- /dev/null +++ b/ios/MagicCounterTests/MagicCounterTests.swift @@ -0,0 +1,17 @@ +// +// MagicCounterTests.swift +// MagicCounterTests +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import Testing +@testable import MagicCounter + +struct MagicCounterTests { + + @Test func example() async throws { + // Todo: Add SOME form of test lol + } + +} diff --git a/ios/MagicCounterUITests/MagicCounterUITests.swift b/ios/MagicCounterUITests/MagicCounterUITests.swift new file mode 100644 index 0000000..cabf3f7 --- /dev/null +++ b/ios/MagicCounterUITests/MagicCounterUITests.swift @@ -0,0 +1,31 @@ +// +// MagicCounterUITests.swift +// MagicCounterUITests +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import XCTest + +final class MagicCounterUITests: XCTestCase { + + override func setUpWithError() throws { + continueAfterFailure = false + } + + override func tearDownWithError() throws { + } + + @MainActor + func testExample() throws { + let app = XCUIApplication() + app.launch() + } + + @MainActor + func testLaunchPerformance() throws { + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/ios/MagicCounterUITests/MagicCounterUITestsLaunchTests.swift b/ios/MagicCounterUITests/MagicCounterUITestsLaunchTests.swift new file mode 100644 index 0000000..f39a213 --- /dev/null +++ b/ios/MagicCounterUITests/MagicCounterUITestsLaunchTests.swift @@ -0,0 +1,30 @@ +// +// MagicCounterUITestsLaunchTests.swift +// MagicCounterUITests +// +// Created by Atridad Lahiji on 2025-12-06. +// + +import XCTest + +final class MagicCounterUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..412d3de Binary files /dev/null and b/logo.png differ