Repository: KrishKrosh/TrackWeight Branch: main Commit: e322cae241d2 Files: 28 Total size: 109.8 KB Directory structure: gitextract_foro58fa/ ├── .github/ │ └── workflows/ │ ├── README.md │ └── build-and-sign-dmg.yml ├── .gitignore ├── ExportOptions.plist ├── LICENSE ├── README.md ├── TrackWeight/ │ ├── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── ContentViewModel.swift │ ├── DebugView.swift │ ├── HomeView.swift │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ ├── ScaleView.swift │ ├── ScaleViewModel.swift │ ├── SettingsView.swift │ ├── TrackWeight.entitlements │ ├── TrackWeightApp.swift │ ├── TrackWeightView.swift │ ├── WeighingState.swift │ └── WeighingViewModel.swift ├── TrackWeight.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ ├── IDEWorkspaceChecks.plist │ └── swiftpm/ │ └── Package.resolved └── scripts/ ├── README.md ├── setup-signing.sh └── test-build-locally.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/README.md ================================================ # GitHub Actions Workflows This directory contains GitHub Actions workflows for the TrackWeight project. ## build-and-sign-dmg.yml This workflow builds, signs (if certificates are provided), and packages the TrackWeight macOS application into a DMG file. ### Features - **Automated Building**: Builds the Xcode project using the latest stable Xcode - **Code Signing**: Supports optional code signing with certificates - **DMG Creation**: Creates a professional DMG with proper layout and attribution - **Attribution**: Includes proper credits to the original repository (https://github.com/KrishKrosh/TrackWeight) - **Release Integration**: Can create GitHub releases with the built DMG - **Artifact Upload**: Uploads DMG as a GitHub Actions artifact ### Triggers The workflow runs on: - Git tags starting with 'v' (e.g., v1.0.0) - Published releases - Manual workflow dispatch ### Setup Instructions #### Required Secrets (for signed builds) To enable code signing, add these secrets to your GitHub repository: 1. **BUILD_CERTIFICATE_BASE64**: Base64-encoded Developer ID Application certificate (.p12 file) ```bash base64 -i YourCertificate.p12 | pbcopy ``` 2. **P12_PASSWORD**: Password for the .p12 certificate file 3. **BUILD_PROVISION_PROFILE_BASE64**: Base64-encoded provisioning profile (optional for Developer ID) ```bash base64 -i YourProvisioningProfile.mobileprovision | pbcopy ``` #### Setting up Certificates 1. Export your Developer ID Application certificate from Keychain Access as a .p12 file 2. Convert to base64 and add as `BUILD_CERTIFICATE_BASE64` secret 3. Add the certificate password as `P12_PASSWORD` secret #### Unsigned Builds If you don't have code signing certificates, the workflow will automatically create an unsigned development build. These builds can still be used but may require users to manually allow them in System Preferences. ## build-unsigned-dmg.yml This workflow specifically builds unsigned development versions of the TrackWeight app without requiring any certificates. ### Features - **No Certificate Requirements**: Builds completely without code signing - **Development Build**: Creates unsigned development builds that work on any Mac - **Same DMG Features**: Includes all the same DMG features as the signed version - **Clear User Instructions**: Includes instructions for running unsigned apps - **Attribution**: Maintains proper credits to the original repository ### Triggers The workflow runs on: - Pushes to main branches and the current working branch - Manual workflow dispatch ### Benefits - **Easy Testing**: Perfect for testing builds without setting up certificates - **No Configuration**: Works immediately without any secrets or setup - **User-Friendly**: Includes clear instructions for users on how to run unsigned apps ## Choosing the Right Workflow ### Use `build-and-sign-dmg.yml` when: - You have Apple Developer certificates - You want to distribute signed, trusted builds - You're creating official releases ### Use `build-unsigned-dmg.yml` when: - You don't have certificates - You want to test builds quickly - You're developing or experimenting - You need a simple build process If no signing certificates are provided, the workflow will create an unsigned development build that can still be distributed and run locally (users may need to allow it in System Preferences > Security & Privacy). ### Usage #### Manual Trigger 1. Go to Actions tab in your GitHub repository 2. Select "Build and Sign DMG" workflow 3. Click "Run workflow" 4. Optionally check "Create a GitHub release" to create a release #### Automatic Trigger 1. Create a git tag: `git tag v1.0.0` 2. Push the tag: `git push origin v1.0.0` 3. The workflow will automatically build and create a release ### Output The workflow produces: - **DMG file**: TrackWeight-{version}.dmg containing the app and attribution - **GitHub Release**: (if triggered by tag or manual release creation) - **Artifacts**: DMG file uploaded as GitHub Actions artifact ### Attribution This workflow ensures proper attribution to the original TrackWeight repository: - README.txt file included in DMG with credits - Release notes include attribution - Links to original repository: https://github.com/KrishKrosh/TrackWeight ## update-homebrew.yml This workflow automatically updates the Homebrew cask when a new release is published. ### Features - Updates version in Homebrew tap repository - Automatically triggered on release publication - Can be manually triggered with version input For more information about the original TrackWeight project, visit: https://github.com/KrishKrosh/TrackWeight ================================================ FILE: .github/workflows/build-and-sign-dmg.yml ================================================ name: Build and Sign DMG on: push: tags: - 'v*' branches: - 'copilot/fix-1' # Enable testing on current working branch release: types: [published] workflow_dispatch: inputs: create_release: description: 'Create a GitHub release' required: false default: false type: boolean env: APP_NAME: TrackWeight SCHEME: TrackWeight CONFIGURATION: Release jobs: build-and-sign: runs-on: macos-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: recursive - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - name: Import Code-Signing Certificates if: ${{ env.BUILD_CERTIFICATE_BASE64 != '' }} env: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} uses: Apple-Actions/import-codesign-certs@v2 with: p12-file-base64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} p12-password: ${{ secrets.P12_PASSWORD }} - name: Install provisioning profile if: ${{ env.BUILD_PROVISION_PROFILE_BASE64 != '' }} env: BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} run: | PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles - name: Build and Archive App run: | xcodebuild \ -project TrackWeight.xcodeproj \ -scheme ${{ env.SCHEME }} \ -configuration ${{ env.CONFIGURATION }} \ -archivePath "$RUNNER_TEMP/${{ env.APP_NAME }}.xcarchive" \ -destination 'generic/platform=macOS' \ ARCHS="arm64 x86_64" \ ONLY_ACTIVE_ARCH=NO \ archive - name: Export App run: | # Use development export method if no signing certificates are available if [ -z "${{ secrets.BUILD_CERTIFICATE_BASE64 }}" ]; then # Create export options for unsigned build cat > "$RUNNER_TEMP/ExportOptions.plist" << EOF method debugging signingStyle manual stripSwiftSymbols destination export signingCertificate - teamID - uploadBitcode uploadSymbols EOF else # Use the existing ExportOptions.plist for signed builds cp ExportOptions.plist "$RUNNER_TEMP/ExportOptions.plist" fi xcodebuild \ -archivePath "$RUNNER_TEMP/${{ env.APP_NAME }}.xcarchive" \ -exportPath "$RUNNER_TEMP/export" \ -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \ -exportArchive - name: Re-sign App Components if: ${{ env.BUILD_CERTIFICATE_BASE64 != '' }} env: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | echo "🔏 Re-signing framework with Developer ID certificate..." FRAMEWORK_PATH="$RUNNER_TEMP/export/${{ env.APP_NAME }}.app/Contents/Frameworks/OpenMultitouchSupportXCF.framework" if [[ -d "$FRAMEWORK_PATH" ]]; then codesign --force --sign "Developer ID Application" \ --options runtime \ --timestamp \ "$FRAMEWORK_PATH/Versions/A/OpenMultitouchSupportXCF" codesign --force --sign "Developer ID Application" \ --options runtime \ --timestamp \ "$FRAMEWORK_PATH" echo "✅ Framework re-signed successfully" fi # Re-sign the main app to ensure everything is consistent echo "🔏 Re-signing main application..." codesign --force --sign "Developer ID Application" \ --options runtime \ --entitlements "TrackWeight/TrackWeight.entitlements" \ --timestamp \ --deep \ --strict \ "$RUNNER_TEMP/export/${{ env.APP_NAME }}.app" echo "✅ Application re-signed successfully" - name: Verify Universal Binary and Code Signatures run: | echo "🏗️ Verifying Universal Binary Architecture..." APP_BINARY="$RUNNER_TEMP/export/${{ env.APP_NAME }}.app/Contents/MacOS/${{ env.APP_NAME }}" if [[ -f "$APP_BINARY" ]]; then echo "📊 Binary architectures:" lipo -archs "$APP_BINARY" if lipo -archs "$APP_BINARY" | grep -q "arm64" && lipo -archs "$APP_BINARY" | grep -q "x86_64"; then echo "✅ Universal binary confirmed: Contains both ARM64 and x86_64" else echo "❌ Warning: Binary may not be universal" lipo -detailed_info "$APP_BINARY" fi fi # Check framework architecture if it exists FRAMEWORK_PATH="$RUNNER_TEMP/export/${{ env.APP_NAME }}.app/Contents/Frameworks/OpenMultitouchSupportXCF.framework" if [[ -d "$FRAMEWORK_PATH" ]]; then FRAMEWORK_BINARY="$FRAMEWORK_PATH/Versions/A/OpenMultitouchSupportXCF" if [[ -f "$FRAMEWORK_BINARY" ]]; then echo "📊 Framework architectures:" lipo -archs "$FRAMEWORK_BINARY" fi fi # Only verify signatures if certificates are available if [[ -n "${{ secrets.BUILD_CERTIFICATE_BASE64 }}" ]]; then echo "🔍 Verifying main application signature..." codesign --verify --verbose "$RUNNER_TEMP/export/${{ env.APP_NAME }}.app" echo "🔍 Verifying framework signature..." if [[ -d "$FRAMEWORK_PATH" ]]; then codesign --verify --verbose "$FRAMEWORK_PATH" fi echo "🔍 Deep verification with online validation..." codesign --verify --deep --strict --verbose=2 "$RUNNER_TEMP/export/${{ env.APP_NAME }}.app" echo "✅ All signature verifications passed" else echo "ℹ️ Skipping signature verification (no certificates provided)" fi - name: Notarize App if: ${{ env.BUILD_CERTIFICATE_BASE64 != '' && env.APPLE_ID != '' }} env: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | echo "🍎 Starting notarization process..." # Create a zip file for notarization (notarytool prefers zip over raw .app) cd "$RUNNER_TEMP/export" # Use ditto for creating zip compatible with Apple's notarization service ditto -c -k --keepParent "${{ env.APP_NAME }}.app" "${{ env.APP_NAME }}.zip" # Submit for notarization echo "📤 Submitting app for notarization..." xcrun notarytool submit "${{ env.APP_NAME }}.zip" \ --apple-id "$APPLE_ID" \ --password "$APPLE_ID_PASSWORD" \ --team-id "$APPLE_TEAM_ID" \ --wait \ --timeout 20m # Staple the notarization ticket to the app echo "📎 Stapling notarization ticket..." xcrun stapler staple "${{ env.APP_NAME }}.app" # Verify the stapling worked echo "✅ Verifying notarization..." xcrun stapler validate "${{ env.APP_NAME }}.app" echo "🎉 Notarization complete!" - name: Install create-dmg run: | brew install create-dmg - name: Create DMG run: | # Create a clean directory with only the app for DMG creation echo "📁 Preparing clean DMG contents..." DMG_STAGING="$RUNNER_TEMP/dmg_staging" rm -rf "$DMG_STAGING" mkdir -p "$DMG_STAGING" # Copy only the app to staging directory cp -R "$RUNNER_TEMP/export/${{ env.APP_NAME }}.app" "$DMG_STAGING/" # Create clean professional DMG create-dmg \ --volname "${{ env.APP_NAME }}" \ --volicon "$DMG_STAGING/${{ env.APP_NAME }}.app/Contents/Resources/AppIcon.icns" \ --window-pos 200 120 \ --window-size 600 300 \ --icon-size 100 \ --icon "${{ env.APP_NAME }}.app" 175 120 \ --hide-extension "${{ env.APP_NAME }}.app" \ --app-drop-link 425 120 \ --hdiutil-quiet \ "$RUNNER_TEMP/${{ env.APP_NAME }}.dmg" \ "$DMG_STAGING/" - name: Get version info id: version_info run: | if [[ "${{ github.ref }}" == refs/tags/* ]]; then VERSION=${GITHUB_REF#refs/tags/} else VERSION=$(date +%Y%m%d-%H%M%S) fi echo "version=$VERSION" >> $GITHUB_OUTPUT echo "dmg_name=${{ env.APP_NAME }}.dmg" >> $GITHUB_OUTPUT - name: Rename DMG with version run: | mv "$RUNNER_TEMP/${{ env.APP_NAME }}.dmg" "$RUNNER_TEMP/${{ steps.version_info.outputs.dmg_name }}" - name: Upload DMG as artifact uses: actions/upload-artifact@v4 with: name: ${{ steps.version_info.outputs.dmg_name }} path: ${{ runner.temp }}/${{ steps.version_info.outputs.dmg_name }} retention-days: 30 - name: Create Release if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.create_release == 'true') uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.version_info.outputs.version }} name: ${{ env.APP_NAME }} ${{ steps.version_info.outputs.version }} body: | # TrackWeight ${{ steps.version_info.outputs.version }} Transform your MacBook's trackpad into a precise digital weighing scale! ## Installation 1. Download the DMG file below 2. Open the DMG and drag TrackWeight.app to your Applications folder 3. Run the app and follow the setup instructions ## Requirements - macOS 13.0 or later (Ventura or newer) - MacBook with Force Touch trackpad (2015 or newer MacBook Pro, 2016 or newer MacBook) ## Usage 1. Open the scale 2. Rest your finger on the trackpad 3. While maintaining finger contact, put your object on the trackpad 4. Apply minimal pressure while maintaining contact to get the weight files: | ${{ runner.temp }}/${{ steps.version_info.outputs.dmg_name }} draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Summary run: | echo "## Build Summary" >> $GITHUB_STEP_SUMMARY echo "✅ Successfully built and packaged TrackWeight DMG" >> $GITHUB_STEP_SUMMARY echo "📦 DMG file: ${{ steps.version_info.outputs.dmg_name }}" >> $GITHUB_STEP_SUMMARY echo "🔗 Original repository: https://github.com/KrishKrosh/TrackWeight" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "The DMG includes:" >> $GITHUB_STEP_SUMMARY echo "- TrackWeight.app (signed and notarized if certificates provided)" >> $GITHUB_STEP_SUMMARY echo "- Clean professional DMG with just the app and Applications link" >> $GITHUB_STEP_SUMMARY echo "- Proper code signing with hardened runtime enabled" >> $GITHUB_STEP_SUMMARY ================================================ FILE: .gitignore ================================================ # Mac .DS_Store # Xcode xcuserdata/ *.xcuserstate # Swift Package Manager Packages.resolved .swiftpm/ .build/ # Framework *.framework/ *.xcframework/ *.zip build/ # AI .claude/ .env .secrets local_build/ ================================================ FILE: ExportOptions.plist ================================================ method developer-id teamID 9ZRLG6277G signingStyle manual signingCertificate Developer ID Application: Krish Shah (9ZRLG6277G) stripSwiftSymbols destination export manageAppVersionAndBuildNumber compileBitcode uploadBitcode uploadSymbols ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 Krish Shah 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. ================================================ FILE: README.md ================================================ # TrackWeight **Turn your MacBook's trackpad into a precise digital weighing scale** [TrackWeight]( https://x.com/KrishRShah/status/1947186835811193330) is a macOS application that transforms your MacBook's trackpad into an accurate weighing scale by leveraging the Force Touch pressure sensors built into modern MacBook trackpads. https://github.com/user-attachments/assets/7eaf9e0b-3dec-4829-b868-f54a8fd53a84 To use it yourself: 1. Open the scale 2. Rest your finger on the trackpad 3. While maintaining finger contact, put your object on the trackpad 4. Try to put as little pressure on the trackpad while still maintaining contact. This is the weight of your object ## How It Works TrackWeight utilizes a custom fork of the [Open Multi-Touch Support library](https://github.com/krishkrosh/OpenMultitouchSupport) by [Takuto Nakamura](https://github.com/Kyome22) to gain private access to all mouse and trackpad events on macOS. This library provides detailed touch data including pressure readings that are normally inaccessible to standard applications. The key insight is that trackpad pressure events are only generated when there's capacitance detected on the trackpad surface - meaning your finger (or another conductive object) must be in contact with the trackpad. When this condition is met, the trackpad's Force Touch sensors provide precise pressure readings that can be calibrated and converted into weight measurements. ## Requirements - **macOS 13.0+** (Ventura or later) - **MacBook with Force Touch trackpad** (2015 or newer MacBook Pro, 2016 or newer MacBook) - **App Sandbox disabled** (required for low-level trackpad access) - **Xcode 16.0+** and **Swift 6.0+** (for development) ## Installation ### Option 1: Download DMG (Recommended) 1. Go to the [Releases](https://github.com/krishkrosh/TrackWeight/releases) page 2. Download the latest TrackWeight DMG file 3. Open the DMG and drag TrackWeight.app to your Applications folder 4. Run the application (you may need to allow it in System Preferences > Security & Privacy for unsigned builds) ### Option 2: Homebrew ```bash brew install --cask krishkrosh/apps/trackweight --force ``` ### Option 3: Build from Source 1. Clone this repository 2. Open `TrackWeight.xcodeproj` in Xcode 3. Disable App Sandbox in the project settings (required for trackpad access) 4. Build and run the application For more information about setting up the build pipeline, see [.github/workflows/README.md](.github/workflows/README.md). ### Calibration Process The weight calculations have been validated by: 1. Placing the MacBook trackpad directly on top of a conventional digital scale 2. Applying various known weights while maintaining finger contact with the trackpad 3. Comparing and calibrating the pressure readings against the reference scale measurements 4. Ensuring consistent accuracy across different weight ranges It turns out that the data we get from MultitouchSupport is already in grams! ## Limitations - **Finger contact required**: The trackpad only provides pressure readings when it detects capacitance (finger touch), so you cannot weigh objects directly without maintaining contact - **Surface contact**: Objects being weighed must be placed in a way that doesn't interfere with the required finger contact - **Metal objects**: Metal objects may be detected as a finger touch, so you may need to place a piece of paper or a cloth between the object and the trackpad to get an accurate reading ## Technical Details The application is built using: - **SwiftUI** for the user interface - **Combine** for reactive data flow - **Open Multi-Touch Support library** for low-level trackpad access ### Open Multi-Touch Support Library This project relies heavily on the excellent work by **Takuto Nakamura** ([@Kyome22](https://github.com/Kyome22)) and the [Open Multi-Touch Support library](https://github.com/krishkrosh/OpenMultitouchSupport). The library provides: - Access to global multitouch events on macOS trackpads - Detailed touch data including position, pressure, angle, and density - Thread-safe async/await support for touch event streams - Touch state tracking and comprehensive sensor data ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Disclaimer This application is for experimental and educational purposes. While efforts have been made to ensure accuracy, TrackWeight should not be used for critical measurements or commercial applications where precision is essential. Always verify measurements with a calibrated scale for important use cases. ================================================ FILE: TrackWeight/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: TrackWeight/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: TrackWeight/ContentView.swift ================================================ // // ContentView.swift // TrackWeight // import SwiftUI struct ContentView: View { @State private var showHomePage = true @State private var selectedTab = 1 // Start with Scale tab (index 1) var body: some View { if showHomePage { HomeView { showHomePage = false } .frame(minWidth: 700, minHeight: 500) } else { TabView(selection: $selectedTab) { TrackWeightView() .tabItem { Image(systemName: "arrow.3.trianglepath") Text("Guided (Experimental)") } .tag(0) ScaleView() .tabItem { Image(systemName: "scalemass") Text("Scale") } .tag(1) SettingsView() .tabItem { Image(systemName: "gearshape") Text("Settings") } .tag(2) } .frame(minWidth: 700, minHeight: 500) } } } #Preview { ContentView() } ================================================ FILE: TrackWeight/ContentViewModel.swift ================================================ // // ContentViewModel.swift // OMSDemo // // Created by Takuto Nakamura on 2024/03/02. // import OpenMultitouchSupport import SwiftUI @MainActor final class ContentViewModel: ObservableObject { @Published var touchData = [OMSTouchData]() @Published var isListening: Bool = false @Published var availableDevices = [OMSDeviceInfo]() @Published var selectedDevice: OMSDeviceInfo? private let manager = OMSManager.shared private var task: Task? init() { loadDevices() } func onAppear() { task = Task { [weak self, manager] in for await touchData in manager.touchDataStream { await MainActor.run { self?.touchData = touchData } } } } func onDisappear() { task?.cancel() stop() } func start() { if manager.startListening() { isListening = true } } func stop() { if manager.stopListening() { isListening = false } } func loadDevices() { availableDevices = manager.availableDevices selectedDevice = manager.currentDevice } func selectDevice(_ device: OMSDeviceInfo) { if manager.selectDevice(device) { selectedDevice = device } } } ================================================ FILE: TrackWeight/DebugView.swift ================================================ // // DebugView.swift // TrackWeight // // Created by Takuto Nakamura on 2024/03/02. // import OpenMultitouchSupport import SwiftUI struct DebugView: View { @StateObject var viewModel = ContentViewModel() @Environment(\.dismiss) private var dismiss var body: some View { VStack { // Header with close button HStack { Text("Debug Console") .font(.title2) .fontWeight(.semibold) Spacer() Button(action: { dismiss() }) { Image(systemName: "xmark.circle.fill") .font(.title2) .foregroundColor(.secondary) } .buttonStyle(PlainButtonStyle()) .help("Close Debug Console") } .padding(.bottom) // Device Selector if !viewModel.availableDevices.isEmpty { VStack(alignment: .leading) { Text("Trackpad Device:") .font(.headline) Picker("Select Device", selection: Binding( get: { viewModel.selectedDevice }, set: { device in if let device = device { viewModel.selectDevice(device) } } )) { ForEach(viewModel.availableDevices, id: \.self) { device in Text("\(device.deviceName) (ID: \(device.deviceID))") .tag(device as OMSDeviceInfo?) } } .pickerStyle(MenuPickerStyle()) } .padding(.bottom) } if viewModel.isListening { Button { viewModel.stop() } label: { Text("Stop") } } else { Button { viewModel.start() } label: { Text("Start") } } Canvas { context, size in viewModel.touchData.forEach { touch in let path = makeEllipse(touch: touch, size: size) context.fill(path, with: .color(.primary.opacity(Double(touch.total)))) } } .frame(width: 600, height: 400) .border(Color.primary) } .fixedSize() .padding() .onAppear { viewModel.onAppear() } .onDisappear { viewModel.onDisappear() } } private func makeEllipse(touch: OMSTouchData, size: CGSize) -> Path { let x = Double(touch.position.x) * size.width let y = Double(1.0 - touch.position.y) * size.height let u = size.width / 100.0 let w = Double(touch.axis.major) * u let h = Double(touch.axis.minor) * u return Path(ellipseIn: CGRect(x: -0.5 * w, y: -0.5 * h, width: w, height: h)) .rotation(.radians(Double(-touch.angle)), anchor: .topLeading) .offset(x: x, y: y) .path(in: CGRect(origin: .zero, size: size)) } } #Preview { DebugView() } ================================================ FILE: TrackWeight/HomeView.swift ================================================ // // HomeView.swift // TrackWeight // import SwiftUI struct HomeView: View { let onBegin: () -> Void var body: some View { VStack(spacing: 40) { Spacer() // Title section VStack(spacing: 15) { Image(systemName: "scalemass") .font(.system(size: 80, weight: .ultraLight)) .foregroundStyle(Color.blue) Text("TrackWeight") .font(.system(size: 48, weight: .bold, design: .rounded)) .foregroundStyle( LinearGradient( colors: [.blue, .teal, .cyan], startPoint: .leading, endPoint: .trailing ) ) } // Description section VStack(spacing: 20) { Text("Transform your MacBook trackpad into a precision scale using Apple's private MultitouchSupport framework to read pressure values with gram-level accuracy.") .font(.system(size: 18, weight: .medium)) .foregroundStyle(Color.primary) .multilineTextAlignment(.center) .frame(maxWidth: 550) // Limitations section VStack(spacing: 12) { Text("Important Limitations") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(Color.orange) VStack(spacing: 8) { LimitationRow( icon: "hand.point.up.left", text: "Requires finger contact for capacitive detection" ) LimitationRow( icon: "chart.line.downtrend.xyaxis", text: "May experience pressure drift when placing objects" ) LimitationRow( icon: "cube.fill", text: "Metal/magnetic objects may not work" ) } } .padding(.horizontal, 30) .padding(.vertical, 20) .background( RoundedRectangle(cornerRadius: 15) .foregroundColor(Color.orange.opacity(0.05)) .overlay( RoundedRectangle(cornerRadius: 15) .stroke(Color.orange.opacity(0.2), lineWidth: 1) ) ) .frame(maxWidth: 500) } Spacer() // Begin button Button(action: onBegin) { HStack(spacing: 10) { Text("Begin") .font(.system(size: 18, weight: .semibold)) Image(systemName: "arrow.right") .font(.system(size: 16, weight: .semibold)) } .foregroundStyle(Color.white) .frame(width: 140, height: 50) .background( RoundedRectangle(cornerRadius: 25) .fill( LinearGradient( colors: [.blue, .teal], startPoint: .leading, endPoint: .trailing ) ) .shadow(color: .blue.opacity(0.3), radius: 10, x: 0, y: 5) ) } .buttonStyle(.plain) .scaleEffect(1.0) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: true) .padding(.vertical, 10) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, 40) } } struct LimitationRow: View { let icon: String let text: String var body: some View { HStack(spacing: 12) { Image(systemName: icon) .font(.system(size: 14, weight: .medium)) .foregroundStyle(Color.orange) .frame(width: 20) Text(text) .font(.system(size: 14, weight: .medium)) .foregroundStyle(Color.secondary) .multilineTextAlignment(.leading) Spacer() } } } #Preview { HomeView(onBegin: {}) } ================================================ FILE: TrackWeight/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: TrackWeight/ScaleView.swift ================================================ // // ScaleView.swift // TrackWeight // import SwiftUI struct ScaleView: View { @StateObject private var viewModel = ScaleViewModel() @State private var scaleCompression: CGFloat = 0 @State private var displayShake = false @State private var particleOffset: CGFloat = 0 @State private var keyMonitor: Any? var body: some View { GeometryReader { geometry in ZStack { // Animated gradient background // LinearGradient( // colors: [ // Color(red: 0.95, green: 0.97, blue: 1.0), // Color(red: 0.85, green: 0.92, blue: 0.98) // ], // startPoint: .topLeading, // endPoint: .bottomTrailing // ) // .ignoresSafeArea() VStack(spacing: geometry.size.height * 0.06) { // Title with subtitle directly underneath VStack(spacing: 8) { Text("Track Weight") .font(.system(size: min(max(geometry.size.width * 0.05, 24), 42), weight: .bold, design: .rounded)) .foregroundStyle( LinearGradient( colors: [.blue, .teal, .cyan], startPoint: .leading, endPoint: .trailing ) ) .minimumScaleFactor(0.7) .lineLimit(1) Text("Place your finger on the trackpad to begin") .font(.system(size: min(max(geometry.size.width * 0.022, 14), 18), weight: .medium)) .foregroundStyle(.gray) .multilineTextAlignment(.center) .frame(maxWidth: geometry.size.width * 0.8) .opacity(viewModel.hasTouch ? 0 : 1) .animation(.easeInOut(duration: 0.5), value: viewModel.hasTouch) } .frame(height: max(geometry.size.height * 0.15, 80)) // Fixed height for title + subtitle .frame(maxWidth: .infinity) // Ensure full width for centering Spacer() // Cartoon Digital Scale - responsive size HStack { Spacer() CartoonScaleView( weight: viewModel.currentWeight, hasTouch: viewModel.hasTouch, compression: $scaleCompression, displayShake: $displayShake, scaleFactor: min(geometry.size.width / 700, geometry.size.height / 500) ) Spacer() } Spacer() // Fixed container for button to prevent jumping VStack(spacing: 10) { if viewModel.hasTouch { Text("Press spacebar or click to zero") .font(.system(size: min(max(geometry.size.width * 0.018, 12), 16), weight: .medium)) .foregroundStyle(.gray) } Button(action: { viewModel.zeroScale() }) { HStack(spacing: 8) { Image(systemName: "arrow.clockwise") .font(.system(size: min(max(geometry.size.width * 0.02, 14), 18), weight: .semibold)) Text("Zero Scale") .font(.system(size: min(max(geometry.size.width * 0.02, 14), 18), weight: .semibold)) } .foregroundStyle(.white) .frame(width: min(max(geometry.size.width * 0.2, 140), 180), height: min(max(geometry.size.height * 0.08, 40), 55)) .background( RoundedRectangle(cornerRadius: 25) .fill( LinearGradient( colors: [.blue, .teal], startPoint: .leading, endPoint: .trailing ) ) ) } .buttonStyle(.plain) .opacity(viewModel.hasTouch ? 1 : 0) .scaleEffect(viewModel.hasTouch ? 1 : 0.8) .animation(.spring(response: 0.4, dampingFraction: 0.8), value: viewModel.hasTouch) } .frame(height: min(max(geometry.size.height * 0.15, 80), 100)) // Fixed space for button + instruction .frame(maxWidth: .infinity) // Ensure full width for centering } .padding(.horizontal, max(geometry.size.width * 0.05, 20)) .padding(.vertical, max(geometry.size.height * 0.03, 20)) .frame(maxWidth: .infinity, maxHeight: .infinity) // Ensure the VStack takes full available space } } .focusable() .modifier(FocusEffectModifier()) .onChange(of: viewModel.currentWeight) { newWeight in withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { scaleCompression = CGFloat(min(newWeight / 100.0, 0.2)) } } .onAppear { viewModel.startListening() setupKeyMonitoring() } .onDisappear { viewModel.stopListening() removeKeyMonitoring() } } private func setupKeyMonitoring() { keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in // Space key code is 49 if event.keyCode == 49 && viewModel.hasTouch { viewModel.zeroScale() } return event } } private func removeKeyMonitoring() { if let monitor = keyMonitor { NSEvent.removeMonitor(monitor) keyMonitor = nil } } } struct CartoonScaleView: View { let weight: Float let hasTouch: Bool @Binding var compression: CGFloat @Binding var displayShake: Bool let scaleFactor: CGFloat var body: some View { VStack(spacing: 0) { // Scale platform (top) - responsive to weight RoundedRectangle(cornerRadius: 8) .fill( LinearGradient( colors: [.gray.opacity(0.3), .gray.opacity(0.6)], startPoint: .top, endPoint: .bottom ) ) .frame(width: 200 * scaleFactor, height: 12 * scaleFactor) .offset(y: compression * 15) // Scale body ZStack { // Main body RoundedRectangle(cornerRadius: 20) .fill( LinearGradient( colors: [ Color(red: 0.95, green: 0.95, blue: 0.97), Color(red: 0.85, green: 0.85, blue: 0.90) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 250 * scaleFactor, height: 150 * scaleFactor) .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 8) // Display screen RoundedRectangle(cornerRadius: 12) .fill(.black) .frame(width: 180 * scaleFactor, height: 60 * scaleFactor) .offset(y: -10) .overlay( RoundedRectangle(cornerRadius: 12) .fill( LinearGradient( colors: [.teal.opacity(0.8), .blue.opacity(0.6)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 176 * scaleFactor, height: 56 * scaleFactor) .offset(y: -10) ) // Weight display VStack(spacing: 2) { Text(String(format: "%.1f", weight)) .font(.system(size: 32 * scaleFactor, weight: .bold, design: .monospaced)) .foregroundStyle(.white) .shadow(color: .teal, radius: hasTouch ? 2 : 0) .animation(.easeInOut(duration: 0.2), value: weight) Text("grams") .font(.system(size: 12 * scaleFactor, weight: .medium)) .foregroundStyle(.white.opacity(0.8)) } .offset(y: -10) // Status indicator - simple and clean if hasTouch { Circle() .fill(.teal) .frame(width: 8 * scaleFactor, height: 8 * scaleFactor) .offset(x: 90 * scaleFactor, y: -50 * scaleFactor) } // Fun face on the scale - positioned below the display screen VStack(spacing: 8 * scaleFactor) { // Eyes HStack(spacing: 15 * scaleFactor) { Circle() .fill(.black) .frame(width: 8 * scaleFactor, height: 8 * scaleFactor) Circle() .fill(.black) .frame(width: 8 * scaleFactor, height: 8 * scaleFactor) } // Responsive mouth expression Group { if hasTouch && weight > 5 { // Happy mouth when weighing something substantial Path { path in path.move(to: CGPoint(x: 0, y: 0)) path.addQuadCurve(to: CGPoint(x: 20, y: 0), control: CGPoint(x: 0, y: 15)) } .stroke(.black, lineWidth: 2 * scaleFactor) .frame(width: 20 * scaleFactor, height: 10 * scaleFactor) } else { // Neutral mouth Rectangle() .fill(.black) .frame(width: 12 * scaleFactor, height: 2 * scaleFactor) } } .animation(.easeInOut(duration: 0.3), value: weight > 5) } .offset(y: 60 * scaleFactor) // Position well below the display screen } // Scale legs HStack(spacing: 140 * scaleFactor) { ForEach(0..<2, id: \.self) { _ in RoundedRectangle(cornerRadius: 4) .fill(.gray.opacity(0.7)) .frame(width: 12 * scaleFactor, height: 25 * scaleFactor) .offset(y: compression * 3) } } .offset(y: -5) } .animation(.spring(response: 0.4, dampingFraction: 0.8), value: compression) } } struct FocusEffectModifier: ViewModifier { func body(content: Content) -> some View { if #available(macOS 14.0, *) { content.focusEffectDisabled() } else { content } } } #Preview { ScaleView() } ================================================ FILE: TrackWeight/ScaleViewModel.swift ================================================ // // ScaleViewModel.swift // TrackWeight // import OpenMultitouchSupport import SwiftUI import Combine @MainActor final class ScaleViewModel: ObservableObject { @Published var currentWeight: Float = 0.0 @Published var zeroOffset: Float = 0.0 @Published var isListening = false @Published var hasTouch = false private let manager = OMSManager.shared private var task: Task? private var rawWeight: Float = 0.0 func startListening() { if manager.startListening() { isListening = true } task = Task { [weak self, manager] in for await touchData in manager.touchDataStream { await MainActor.run { self?.processTouchData(touchData) } } } } func stopListening() { task?.cancel() if manager.stopListening() { isListening = false hasTouch = false currentWeight = 0.0 } } func zeroScale() { if hasTouch { zeroOffset = rawWeight } } private func processTouchData(_ touchData: [OMSTouchData]) { if touchData.isEmpty { hasTouch = false currentWeight = 0.0 zeroOffset = 0.0 // Reset zero when finger is lifted } else { hasTouch = true rawWeight = touchData.first?.pressure ?? 0.0 currentWeight = max(0, rawWeight - zeroOffset) } } deinit { task?.cancel() manager.stopListening() } } ================================================ FILE: TrackWeight/SettingsView.swift ================================================ // // SettingsView.swift // TrackWeight // import OpenMultitouchSupport import SwiftUI struct SettingsView: View { @StateObject private var viewModel = ContentViewModel() @State private var showDebugView = false var body: some View { VStack(spacing: 0) { // Minimal Header Text("Settings") .font(.title) .fontWeight(.medium) .padding(.top, 32) .padding(.bottom, 32) // Settings Cards VStack(spacing: 20) { // Device Card SettingsCard { VStack(spacing: 20) { // Status Row HStack { HStack(spacing: 12) { Text("Trackpad") .font(.headline) .fontWeight(.medium) } Spacer() if !viewModel.availableDevices.isEmpty { Text("\(viewModel.availableDevices.count) device\(viewModel.availableDevices.count == 1 ? "" : "s")") .font(.caption) .foregroundColor(.secondary) } } // Device Selector if !viewModel.availableDevices.isEmpty { HStack { Picker("", selection: Binding( get: { viewModel.selectedDevice }, set: { device in if let device = device { viewModel.selectDevice(device) } } )) { ForEach(viewModel.availableDevices, id: \.self) { device in Text(device.deviceName) .tag(device as OMSDeviceInfo?) } } .pickerStyle(MenuPickerStyle()) Spacer() } } else { HStack { Text("No devices available") .foregroundColor(.secondary) Spacer() } } } } // Debug Card SettingsCard { Button(action: { showDebugView = true }) { HStack(spacing: 16) { VStack(alignment: .leading, spacing: 4) { Text("Debug Console") .font(.headline) .fontWeight(.medium) .foregroundColor(.primary) Text("Raw touch data & diagnostics") .font(.caption) .foregroundColor(.secondary) } Spacer() Image(systemName: "chevron.right") .font(.caption) .fontWeight(.medium) .foregroundColor(.secondary.opacity(0.6)) } .contentShape(Rectangle()) } .buttonStyle(CardButtonStyle()) } } .frame(maxWidth: 480) .padding(.horizontal, 40) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(NSColor.windowBackgroundColor)) .sheet(isPresented: $showDebugView) { DebugView() .frame(minWidth: 700, minHeight: 500) } .onAppear { viewModel.loadDevices() } } } struct SettingsCard: View { let content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { VStack { content } .padding(24) .background(Color(NSColor.controlBackgroundColor)) .cornerRadius(16) .shadow(color: Color.black.opacity(0.03), radius: 1, x: 0, y: 1) .shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 4) } } struct CardButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.98 : 1.0) .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) } } #Preview { SettingsView() } ================================================ FILE: TrackWeight/TrackWeight.entitlements ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-dyld-environment-variables com.apple.security.cs.disable-library-validation com.apple.security.app-sandbox com.apple.security.device.usb com.apple.security.device.serial com.apple.security.network.client ================================================ FILE: TrackWeight/TrackWeightApp.swift ================================================ // // TrackWeightApp.swift // TrackWeight // // Created by Takuto Nakamura on 2024/03/02. // import SwiftUI @main struct TrackWeightApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } final class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } } ================================================ FILE: TrackWeight/TrackWeightView.swift ================================================ // // TrackWeightView.swift // TrackWeight // import SwiftUI struct TrackWeightView: View { @StateObject private var viewModel = WeighingViewModel() var body: some View { VStack(spacing: 30) { switch viewModel.state { case .welcome: WelcomeView { viewModel.startWeighing() } case .waitingForFinger: FingerTimerView( progress: viewModel.fingerTimer, hasDetectedFinger: viewModel.fingerTimer > 0 ) case .waitingForItem: InstructionView( title: "Place your item", subtitle: "While maintaining contact with the trackpad, gently place your item. Use as little pressure as possible with your reference finger.", disclaimer: "Keep your finger still and apply minimal pressure", icon: "cube.box" ) case .weighing: WeighingView( currentPressure: viewModel.currentPressure, isStabilizing: viewModel.isStabilizing, stabilityProgress: viewModel.stabilityProgress ) case .result(let weight): ResultView(weight: weight) { viewModel.restart() } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.windowBackgroundColor)) .animation(.easeInOut(duration: 0.6), value: viewModel.state) } } struct WelcomeView: View { let onStart: () -> Void var body: some View { VStack(spacing: 25) { Image(systemName: "scalemass") .font(.system(size: 80, weight: .ultraLight)) .foregroundStyle(.primary) Text("TrackWeight") .font(.system(size: 36, weight: .bold, design: .rounded)) .foregroundStyle(.primary) Text("Turn your trackpad into a precision scale. Place objects and get their weight in grams.") .font(.system(size: 16, weight: .medium)) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .frame(maxWidth: 400) VStack(spacing: 15) { Button(action: onStart) { Text("Begin") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.white) .frame(width: 120, height: 40) .background( RoundedRectangle(cornerRadius: 20) .fill(.blue) ) } .buttonStyle(.plain) } } } } struct FingerTimerView: View { let progress: Float let hasDetectedFinger: Bool var body: some View { VStack(spacing: 30) { Image(systemName: "hand.point.up.left") .font(.system(size: 60, weight: .light)) .foregroundStyle(.blue) Text("Hold your finger steady") .font(.system(size: 28, weight: .bold, design: .rounded)) .foregroundStyle(.primary) VStack(spacing: 8) { Text("Keep your finger on the trackpad for 3 seconds") .font(.system(size: 16, weight: .medium)) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .frame(maxWidth: 300) Text("This establishes your baseline pressure") .font(.system(size: 14, weight: .medium)) .foregroundStyle(.tertiary) .multilineTextAlignment(.center) .frame(maxWidth: 300) } // Bubble filling animation ZStack { Circle() .stroke(.blue.opacity(0.3), lineWidth: 4) .frame(width: 100, height: 100) Circle() .fill(.blue.opacity(0.2)) .frame(width: 100, height: 100) if hasDetectedFinger { Circle() .trim(from: 0, to: CGFloat(progress)) .stroke(.blue, style: StrokeStyle(lineWidth: 4, lineCap: .round)) .frame(width: 100, height: 100) .rotationEffect(.degrees(-90)) .animation(.linear(duration: 0.1), value: progress) // Gentle bubble effect Circle() .fill( RadialGradient( colors: [.blue.opacity(0.3), .blue.opacity(0.1)], center: .center, startRadius: 0, endRadius: 50 ) ) .frame(width: CGFloat(progress) * 80 + 20, height: CGFloat(progress) * 80 + 20) .animation(.easeInOut(duration: 0.2), value: progress) Text("\(Int((1 - progress) * 3) + 1)") .font(.system(size: 24, weight: .bold, design: .monospaced)) .foregroundStyle(.blue) } } .scaleEffect(hasDetectedFinger ? 1.0 : 0.8) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: hasDetectedFinger) } } } struct InstructionView: View { let title: String let subtitle: String let disclaimer: String? let icon: String init(title: String, subtitle: String, disclaimer: String? = nil, icon: String) { self.title = title self.subtitle = subtitle self.disclaimer = disclaimer self.icon = icon } var body: some View { VStack(spacing: 20) { Image(systemName: icon) .font(.system(size: 60, weight: .light)) .foregroundStyle(.blue) Text(title) .font(.system(size: 28, weight: .bold, design: .rounded)) .foregroundStyle(.primary) VStack(spacing: 10) { Text(subtitle) .font(.system(size: 16, weight: .medium)) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .frame(maxWidth: 350) if let disclaimer = disclaimer { Text(disclaimer) .font(.system(size: 14, weight: .medium)) .foregroundStyle(.orange) .multilineTextAlignment(.center) .frame(maxWidth: 300) } } } } } struct WeighingView: View { let currentPressure: Float let isStabilizing: Bool let stabilityProgress: Float var body: some View { VStack(spacing: 30) { Text("Weighing...") .font(.system(size: 24, weight: .semibold, design: .rounded)) .foregroundStyle(.primary) VStack(spacing: 10) { Text(String(format: "%.1f", currentPressure)) .font(.system(size: 64, weight: .bold, design: .monospaced)) .foregroundStyle(.blue) .animation(.easeInOut(duration: 0.2), value: currentPressure) Text("grams") .font(.system(size: 20, weight: .medium)) .foregroundStyle(.secondary) } VStack(spacing: 12) { Text("Release pressure while maintaining contact") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.orange) .multilineTextAlignment(.center) Text("Keep your finger on the trackpad but apply as little pressure as possible") .font(.system(size: 14, weight: .medium)) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .frame(maxWidth: 350) if isStabilizing { VStack(spacing: 8) { Text("Stabilizing...") .font(.system(size: 14, weight: .medium)) .foregroundStyle(.orange) // Progress bar ZStack { RoundedRectangle(cornerRadius: 4) .fill(.orange.opacity(0.2)) .frame(width: 200, height: 8) HStack { RoundedRectangle(cornerRadius: 4) .fill(.orange) .frame(width: 200 * CGFloat(stabilityProgress), height: 8) .animation(.linear(duration: 0.1), value: stabilityProgress) Spacer() } } .frame(width: 200) Text("\(Int((1 - stabilityProgress) * 2) + 1)s remaining") .font(.system(size: 12, weight: .medium, design: .monospaced)) .foregroundStyle(.orange.opacity(0.8)) } } } .frame(maxWidth: 350) } } } struct ResultView: View { let weight: Float let onRestart: () -> Void var body: some View { VStack(spacing: 30) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 60)) .foregroundStyle(.green) .scaleEffect(1.0) .onAppear { withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { // Animation handled by parent view } } Text("Your object weighs") .font(.system(size: 20, weight: .medium)) .foregroundStyle(.secondary) VStack(spacing: 5) { Text(String(format: "%.1f", weight)) .font(.system(size: 56, weight: .bold, design: .monospaced)) .foregroundStyle(.primary) Text("grams") .font(.system(size: 18, weight: .medium)) .foregroundStyle(.secondary) } Button(action: onRestart) { Image(systemName: "arrow.clockwise") .font(.system(size: 20, weight: .medium)) .foregroundStyle(.blue) .frame(width: 44, height: 44) .background( Circle() .fill(.blue.opacity(0.1)) ) } .buttonStyle(.plain) } } } #Preview { TrackWeightView() } ================================================ FILE: TrackWeight/WeighingState.swift ================================================ // // WeighingState.swift // TrackWeight // import Foundation enum WeighingState: Equatable { case welcome case waitingForFinger case waitingForItem case weighing case result(weight: Float) } ================================================ FILE: TrackWeight/WeighingViewModel.swift ================================================ // // WeighingViewModel.swift // TrackWeight // import OpenMultitouchSupport import SwiftUI import Combine @MainActor final class WeighingViewModel: ObservableObject { @Published var state: WeighingState = .welcome @Published var currentPressure: Float = 0.0 @Published var maxPressure: Float = 0.0 @Published var isListening = false @Published var fingerTimer: Float = 0.0 // 0.0 to 1.0 for animation private let manager = OMSManager.shared private var task: Task? private var timerTask: Task? private var baselinePressure: Float = 0.0 private var hasDetectedFinger = false private var hasDetectedItem = false private var finalWeight: Float = 0.0 private let fingerHoldDuration: TimeInterval = 3.0 private var pressureHistory: [Float] = [] private let historySize = 10 private let rateOfChangeThreshold: Float = 5 // Weighing stability properties private let stabilityDuration: TimeInterval = 3.0 private let stabilityAnimationDelay: TimeInterval = 1.0 // Show animation after 1s of stability private var stabilityStartTime: Date? private var stableWeight: Float = 0.0 private let stabilityThreshold: Float = 2.0 // Max allowed weight variation @Published var stabilityProgress: Float = 0.0 @Published var isStabilizing: Bool = false func startWeighing() { state = .waitingForFinger hasDetectedFinger = false hasDetectedItem = false baselinePressure = 0.0 currentPressure = 0.0 maxPressure = 0.0 finalWeight = 0.0 fingerTimer = 0.0 stabilityProgress = 0.0 stabilityStartTime = nil stableWeight = 0.0 isStabilizing = false pressureHistory.removeAll() if manager.startListening() { isListening = true } task = Task { [weak self, manager] in for await touchData in manager.touchDataStream { await MainActor.run { self?.processTouchData(touchData) } } } } func restart() { stopListening() state = .welcome fingerTimer = 0.0 stabilityProgress = 0.0 stabilityStartTime = nil isStabilizing = false } private func stopListening() { task?.cancel() timerTask?.cancel() if manager.stopListening() { isListening = false } } private func startFingerTimer() { timerTask?.cancel() fingerTimer = 0.0 timerTask = Task { [weak self] in let startTime = Date() while !Task.isCancelled { let elapsed = Date().timeIntervalSince(startTime) let progress = min(elapsed / (self?.fingerHoldDuration ?? 3.0), 1.0) await MainActor.run { self?.fingerTimer = Float(progress) } if progress >= 1.0 { await MainActor.run { self?.completeFingerTimer() } break } try? await Task.sleep(nanoseconds: 16_666_667) // ~60fps } } } private func resetFingerTimer() { timerTask?.cancel() fingerTimer = 0.0 } private func completeFingerTimer() { hasDetectedFinger = true baselinePressure = currentPressure state = .waitingForItem timerTask?.cancel() } private func startStabilityTimer(with weight: Float) { // stabilityStartTime and stableWeight are already set in the calling code stabilityProgress = 0.0 isStabilizing = true // Start showing animation since we're already past the 1s delay timerTask = Task { [weak self] in // We start from the point where animation should begin (after 1s delay) let animationStartTime = Date() let remainingDuration = (self?.stabilityDuration ?? 3.0) - (self?.stabilityAnimationDelay ?? 1.0) while !Task.isCancelled { let elapsed = Date().timeIntervalSince(animationStartTime) let progress = min(elapsed / remainingDuration, 1.0) await MainActor.run { self?.stabilityProgress = Float(progress) } if progress >= 1.0 { await MainActor.run { self?.completeWeighing() } break } try? await Task.sleep(nanoseconds: 16_666_667) // ~60fps } } } private func completeWeighing() { state = .result(weight: currentPressure) stopListening() } private func resetStabilityTimer() { stabilityStartTime = nil stabilityProgress = 0.0 isStabilizing = false timerTask?.cancel() } private func processTouchData(_ touchData: [OMSTouchData]) { guard !touchData.isEmpty else { // Reset timer if finger is lifted during waiting if state == .waitingForFinger && !hasDetectedFinger { resetFingerTimer() } if state == .weighing { if hasDetectedItem && finalWeight > 0 { state = .result(weight: finalWeight) stopListening() } } return } let mainTouch = touchData.first! currentPressure = mainTouch.pressure // Add current pressure to history pressureHistory.append(currentPressure) if pressureHistory.count > historySize { pressureHistory.removeFirst() } // log the average pressure (moving avg) let avgPressure = pressureHistory.reduce(0, +) / Float(pressureHistory.count) print("average pressure: \(avgPressure)") currentPressure = avgPressure switch state { case .waitingForFinger: if !hasDetectedFinger { currentPressure = mainTouch.pressure if fingerTimer == 0.0 { startFingerTimer() } } case .waitingForItem: if hasDetectedFinger { // Calculate rate of change if we have enough history if pressureHistory.count == historySize && !hasDetectedItem { let rateOfChange = pressureHistory.last! - pressureHistory.first! if rateOfChange > rateOfChangeThreshold { print("pressure before item: \(pressureHistory)") print("Old baseline: \(baselinePressure)") baselinePressure = pressureHistory.first! print("New baseline: \(baselinePressure)") hasDetectedItem = true state = .weighing resetStabilityTimer() } } } else { state = .waitingForFinger pressureHistory.removeAll() } case .weighing: // Check if weight is stable (hasn't changed by more than 2g) let weightDifference = stabilityStartTime != nil ? abs(currentPressure - stableWeight) : 0 if stabilityStartTime == nil { // First reading in weighing state - start tracking stability stabilityStartTime = Date() stableWeight = currentPressure } else if weightDifference > stabilityThreshold { // Weight became unstable, reset stability tracking resetStabilityTimer() stabilityStartTime = Date() stableWeight = currentPressure } else { // Weight is stable, check if we should start the stabilization process let timeSinceStable = Date().timeIntervalSince(stabilityStartTime!) if timeSinceStable >= stabilityAnimationDelay && !isStabilizing { // Weight has been stable for at least 1 second, start stabilization timer startStabilityTimer(with: stableWeight) } } default: break } } deinit { task?.cancel() timerTask?.cancel() manager.stopListening() } } ================================================ FILE: TrackWeight.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 56; objects = { /* Begin PBXBuildFile section */ 77292A882B931953001CA3F6 /* TrackWeightApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A872B931953001CA3F6 /* TrackWeightApp.swift */; }; 77292A8A2B931953001CA3F6 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A892B931953001CA3F6 /* ContentView.swift */; }; 77292A8C2B931954001CA3F6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77292A8B2B931954001CA3F6 /* Assets.xcassets */; }; 77292A8F2B931954001CA3F6 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77292A8E2B931954001CA3F6 /* Preview Assets.xcassets */; }; 77292A982B931D60001CA3F6 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A972B931D60001CA3F6 /* ContentViewModel.swift */; }; 77292A9C2B931E01001CA3F6 /* WeighingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A9D2B931E01001CA3F6 /* WeighingState.swift */; }; 77292A9E2B931E02001CA3F6 /* WeighingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A9F2B931E02001CA3F6 /* WeighingViewModel.swift */; }; 77292AA02B931E03001CA3F6 /* TrackWeightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292AA12B931E03001CA3F6 /* TrackWeightView.swift */; }; 77292AA22B931E04001CA3F6 /* DebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292AA32B931E04001CA3F6 /* DebugView.swift */; }; 77292AA42B931E05001CA3F6 /* ScaleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292AA52B931E05001CA3F6 /* ScaleView.swift */; }; 77292AA62B931E06001CA3F6 /* ScaleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292AA72B931E06001CA3F6 /* ScaleViewModel.swift */; }; 93A095122E33359600E1E1D1 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A095112E33359600E1E1D1 /* SettingsView.swift */; }; 93A095162E33624200E1E1D1 /* OpenMultitouchSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 93A095152E33624200E1E1D1 /* OpenMultitouchSupport */; }; 93ABD0212E2E01E200668D4F /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ABD0202E2E01E200668D4F /* HomeView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 77292A842B931953001CA3F6 /* TrackWeight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TrackWeight.app; sourceTree = BUILT_PRODUCTS_DIR; }; 77292A872B931953001CA3F6 /* TrackWeightApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackWeightApp.swift; sourceTree = ""; }; 77292A892B931953001CA3F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 77292A8B2B931954001CA3F6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 77292A8E2B931954001CA3F6 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 77292A902B931954001CA3F6 /* TrackWeight.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TrackWeight.entitlements; sourceTree = ""; }; 77292A972B931D60001CA3F6 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; 77292A9D2B931E01001CA3F6 /* WeighingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeighingState.swift; sourceTree = ""; }; 77292A9F2B931E02001CA3F6 /* WeighingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeighingViewModel.swift; sourceTree = ""; }; 77292AA12B931E03001CA3F6 /* TrackWeightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackWeightView.swift; sourceTree = ""; }; 77292AA32B931E04001CA3F6 /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = ""; }; 77292AA52B931E05001CA3F6 /* ScaleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleView.swift; sourceTree = ""; }; 77292AA72B931E06001CA3F6 /* ScaleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleViewModel.swift; sourceTree = ""; }; 93A095112E33359600E1E1D1 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 93ABD0202E2E01E200668D4F /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 77292A812B931953001CA3F6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 93A095162E33624200E1E1D1 /* OpenMultitouchSupport in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 77292A7B2B931953001CA3F6 = { isa = PBXGroup; children = ( 77292A862B931953001CA3F6 /* TrackWeight */, 77292A852B931953001CA3F6 /* Products */, 77292A992B931D7A001CA3F6 /* Frameworks */, ); sourceTree = ""; }; 77292A852B931953001CA3F6 /* Products */ = { isa = PBXGroup; children = ( 77292A842B931953001CA3F6 /* TrackWeight.app */, ); name = Products; sourceTree = ""; }; 77292A862B931953001CA3F6 /* TrackWeight */ = { isa = PBXGroup; children = ( 77292A902B931954001CA3F6 /* TrackWeight.entitlements */, 77292A8B2B931954001CA3F6 /* Assets.xcassets */, 77292A872B931953001CA3F6 /* TrackWeightApp.swift */, 77292A892B931953001CA3F6 /* ContentView.swift */, 77292A972B931D60001CA3F6 /* ContentViewModel.swift */, 77292A9D2B931E01001CA3F6 /* WeighingState.swift */, 93ABD0202E2E01E200668D4F /* HomeView.swift */, 77292A9F2B931E02001CA3F6 /* WeighingViewModel.swift */, 93A095112E33359600E1E1D1 /* SettingsView.swift */, 77292AA12B931E03001CA3F6 /* TrackWeightView.swift */, 77292AA32B931E04001CA3F6 /* DebugView.swift */, 77292AA52B931E05001CA3F6 /* ScaleView.swift */, 77292AA72B931E06001CA3F6 /* ScaleViewModel.swift */, 77292A8D2B931954001CA3F6 /* Preview Content */, ); path = TrackWeight; sourceTree = ""; }; 77292A8D2B931954001CA3F6 /* Preview Content */ = { isa = PBXGroup; children = ( 77292A8E2B931954001CA3F6 /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; 77292A992B931D7A001CA3F6 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 77292A832B931953001CA3F6 /* TrackWeight */ = { isa = PBXNativeTarget; buildConfigurationList = 77292A932B931954001CA3F6 /* Build configuration list for PBXNativeTarget "TrackWeight" */; buildPhases = ( 77292A802B931953001CA3F6 /* Sources */, 77292A812B931953001CA3F6 /* Frameworks */, 77292A822B931953001CA3F6 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = TrackWeight; packageProductDependencies = ( 93A095152E33624200E1E1D1 /* OpenMultitouchSupport */, ); productName = TrackWeight; productReference = 77292A842B931953001CA3F6 /* TrackWeight.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 77292A7C2B931953001CA3F6 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1620; TargetAttributes = { 77292A832B931953001CA3F6 = { CreatedOnToolsVersion = 15.2; }; }; }; buildConfigurationList = 77292A7F2B931953001CA3F6 /* Build configuration list for PBXProject "TrackWeight" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 77292A7B2B931953001CA3F6; packageReferences = ( 93A095142E33624200E1E1D1 /* XCRemoteSwiftPackageReference "OpenMultitouchSupport" */, ); productRefGroup = 77292A852B931953001CA3F6 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 77292A832B931953001CA3F6 /* TrackWeight */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 77292A822B931953001CA3F6 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 77292A8F2B931954001CA3F6 /* Preview Assets.xcassets in Resources */, 77292A8C2B931954001CA3F6 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 77292A802B931953001CA3F6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 77292A8A2B931953001CA3F6 /* ContentView.swift in Sources */, 77292A982B931D60001CA3F6 /* ContentViewModel.swift in Sources */, 77292A882B931953001CA3F6 /* TrackWeightApp.swift in Sources */, 93ABD0212E2E01E200668D4F /* HomeView.swift in Sources */, 77292A9C2B931E01001CA3F6 /* WeighingState.swift in Sources */, 77292A9E2B931E02001CA3F6 /* WeighingViewModel.swift in Sources */, 77292AA02B931E03001CA3F6 /* TrackWeightView.swift in Sources */, 77292AA22B931E04001CA3F6 /* DebugView.swift in Sources */, 77292AA42B931E05001CA3F6 /* ScaleView.swift in Sources */, 77292AA62B931E06001CA3F6 /* ScaleViewModel.swift in Sources */, 93A095122E33359600E1E1D1 /* SettingsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 77292A912B931954001CA3F6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = "$(ARCHS_STANDARD)"; 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; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; 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; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 77292A922B931954001CA3F6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = "$(ARCHS_STANDARD)"; 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; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 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; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; }; 77292A942B931954001CA3F6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = TrackWeight/TrackWeight.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 9ZRLG6277G; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.krishkrosh.trackweight; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 6.0; }; name = Debug; }; 77292A952B931954001CA3F6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = TrackWeight/TrackWeight.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 9ZRLG6277G; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.krishkrosh.trackweight; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 6.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 77292A7F2B931953001CA3F6 /* Build configuration list for PBXProject "TrackWeight" */ = { isa = XCConfigurationList; buildConfigurations = ( 77292A912B931954001CA3F6 /* Debug */, 77292A922B931954001CA3F6 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 77292A932B931954001CA3F6 /* Build configuration list for PBXNativeTarget "TrackWeight" */ = { isa = XCConfigurationList; buildConfigurations = ( 77292A942B931954001CA3F6 /* Debug */, 77292A952B931954001CA3F6 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 93A095142E33624200E1E1D1 /* XCRemoteSwiftPackageReference "OpenMultitouchSupport" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/KrishKrosh/OpenMultitouchSupport.git"; requirement = { branch = main; kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 93A095152E33624200E1E1D1 /* OpenMultitouchSupport */ = { isa = XCSwiftPackageProductDependency; package = 93A095142E33624200E1E1D1 /* XCRemoteSwiftPackageReference "OpenMultitouchSupport" */; productName = OpenMultitouchSupport; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 77292A7C2B931953001CA3F6 /* Project object */; } ================================================ FILE: TrackWeight.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: TrackWeight.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: TrackWeight.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "originHash" : "231443e4712351510ab418164c13b6d5894774039d8c118ed8208a5fc9fe884e", "pins" : [ { "identity" : "openmultitouchsupport", "kind" : "remoteSourceControl", "location" : "https://github.com/KrishKrosh/OpenMultitouchSupport.git", "state" : { "branch" : "main", "revision" : "fb6991fa1ece8c5faa8eef49190beb2baf071694" } } ], "version" : 3 } ================================================ FILE: scripts/README.md ================================================ # Scripts This directory contains helper scripts for the TrackWeight project. ## setup-signing.sh A helper script to set up code signing certificates for automated DMG builds. ### Usage ```bash ./scripts/setup-signing.sh ``` ### What it does 1. **Guides you through certificate export**: Provides step-by-step instructions to export your Developer ID Application certificate from Keychain Access 2. **Encodes certificates**: Converts your .p12 certificate file to base64 format required for GitHub Secrets 3. **Generates secret values**: Provides the exact values you need to add as GitHub repository secrets 4. **Optional provisioning profile**: Handles provisioning profile encoding if needed ### Prerequisites - macOS (required for Keychain Access and signing tools) - Valid Apple Developer ID Application certificate - Access to GitHub repository settings to add secrets ### Output The script will generate the values for these GitHub repository secrets: - `BUILD_CERTIFICATE_BASE64`: Base64-encoded .p12 certificate - `P12_PASSWORD`: Certificate password - `BUILD_PROVISION_PROFILE_BASE64`: Base64-encoded provisioning profile (optional) ### Adding Secrets to GitHub 1. Go to your GitHub repository 2. Navigate to Settings > Secrets and variables > Actions 3. Click "New repository secret" 4. Add each secret with the name and value provided by the script ### Attribution This script is part of the enhanced TrackWeight fork that adds automated build pipelines. **Original TrackWeight project**: https://github.com/KrishKrosh/TrackWeight **Created by**: Krish Shah (@KrishKrosh) ================================================ FILE: scripts/setup-signing.sh ================================================ #!/bin/bash # setup-signing.sh # Helper script to set up code signing certificates for TrackWeight DMG builds # # This script helps you prepare the necessary secrets for GitHub Actions # to build signed DMG files. set -e echo "🔐 TrackWeight Code Signing Setup" echo "==================================" echo "" echo "This script helps you set up code signing for automated DMG builds." echo "You'll need a valid Apple Developer ID Application certificate." echo "" # Check if we're on macOS if [[ "$OSTYPE" != "darwin"* ]]; then echo "❌ This script must be run on macOS to access Keychain and signing tools." exit 1 fi # Function to encode file to base64 encode_file() { local file_path="$1" if [[ -f "$file_path" ]]; then base64 -i "$file_path" else echo "❌ File not found: $file_path" return 1 fi } echo "Step 1: Export your Developer ID Application certificate" echo "--------------------------------------------------------" echo "1. Open Keychain Access" echo "2. Find your 'Developer ID Application' certificate" echo "3. Right-click and select 'Export'" echo "4. Save as .p12 format with a password" echo "" read -p "Enter the path to your exported .p12 certificate: " cert_path if [[ ! -f "$cert_path" ]]; then echo "❌ Certificate file not found: $cert_path" exit 1 fi echo "" read -s -p "Enter the password for your .p12 certificate: " cert_password echo "" echo "" echo "Step 2: Encoding certificate for GitHub Secrets" echo "----------------------------------------------" # Encode the certificate echo "Encoding certificate..." cert_base64=$(encode_file "$cert_path") if [[ -z "$cert_base64" ]]; then echo "❌ Failed to encode certificate" exit 1 fi echo "✅ Certificate encoded successfully" echo "" echo "Step 3: GitHub Repository Secrets" echo "--------------------------------" echo "Add these secrets to your GitHub repository:" echo "(Go to Settings > Secrets and variables > Actions)" echo "" echo "1. Secret name: BUILD_CERTIFICATE_BASE64" echo " Value: (copy the text below)" echo "" echo "$cert_base64" echo "" echo "2. Secret name: P12_PASSWORD" echo " Value: $cert_password" echo "" echo "3. Secret name: APPLE_ID" echo " Value: your-apple-id@example.com" echo "" echo "4. Secret name: APPLE_ID_PASSWORD" echo " Value: (App-specific password - see instructions below)" echo "" echo "5. Secret name: APPLE_TEAM_ID" echo " Value: (Your 10-character Team ID - see instructions below)" echo "" # Check for provisioning profile (optional for Developer ID) echo "Step 4: Provisioning Profile (Optional)" echo "--------------------------------------" read -p "Do you have a provisioning profile to include? (y/n): " include_profile if [[ "$include_profile" =~ ^[Yy]$ ]]; then read -p "Enter the path to your .mobileprovision file: " profile_path if [[ -f "$profile_path" ]]; then profile_base64=$(encode_file "$profile_path") echo "" echo "3. Secret name: BUILD_PROVISION_PROFILE_BASE64" echo " Value: (copy the text below)" echo "" echo "$profile_base64" else echo "❌ Provisioning profile not found: $profile_path" fi else echo "📝 Skipping provisioning profile (Developer ID usually doesn't need one)" fi echo "" echo "Step 5: Additional Secrets for Notarization" echo "-------------------------------------------" echo "For full notarization (eliminates security warnings), you'll also need:" echo "" echo "📧 Apple ID (APPLE_ID):" echo " - Use your Apple Developer account email" echo "" echo "🔑 App-Specific Password (APPLE_ID_PASSWORD):" echo " 1. Go to appleid.apple.com" echo " 2. Sign in with your Apple ID" echo " 3. In the 'App-Specific Passwords' section, click 'Generate Password'" echo " 4. Label it something like 'GitHub Actions Notarization'" echo " 5. Copy the generated password (xxxx-xxxx-xxxx-xxxx)" echo "" echo "🏢 Team ID (APPLE_TEAM_ID):" echo " 1. Go to developer.apple.com" echo " 2. Sign in and go to 'Membership'" echo " 3. Your Team ID is the 10-character string (e.g., ABC1234567)" echo "" echo "ℹ️ Without these notarization secrets, the app will still be signed but users" echo " will see security warnings when trying to run it." echo "" echo "🎉 Setup Complete!" echo "=================" echo "" echo "Next steps:" echo "1. Add ALL the secrets to your GitHub repository" echo "2. Create a git tag to trigger the build: git tag v1.0.0 && git push origin v1.0.0" echo "3. Or manually trigger the workflow from the Actions tab" echo "" echo "The workflow will:" echo "- Build and sign your app with the provided certificate" echo "- Notarize the app with Apple (eliminates security warnings)" echo "- Create a professional DMG with attribution to the original repo" echo "- Upload the DMG as a release artifact" echo "" echo "Original TrackWeight project: https://github.com/KrishKrosh/TrackWeight" ================================================ FILE: scripts/test-build-locally.sh ================================================ #!/bin/bash # test-build-locally.sh # Test the GitHub Actions workflow steps locally on macOS set -e echo "🧪 Testing TrackWeight Build Workflow Locally" echo "==============================================" # Load environment variables from .env file if [[ -f ".env" ]]; then echo "📄 Loading environment variables from .env file..." # Export all variables from .env file set -a # automatically export all variables source .env set +a # turn off automatic export echo "✅ Environment variables loaded" else echo "⚠️ No .env file found - some features may not work" fi # Configuration export APP_NAME="TrackWeight" export SCHEME="TrackWeight" export CONFIGURATION="Release" export BUILD_DIR="$(pwd)/local_build" # Clean up previous builds rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR" echo "" echo "Step 1: Building and Archiving App (Universal Binary)" echo "=====================================================" xcodebuild \ -project TrackWeight.xcodeproj \ -scheme "$SCHEME" \ -configuration "$CONFIGURATION" \ -archivePath "$BUILD_DIR/$APP_NAME.xcarchive" \ -destination 'generic/platform=macOS' \ ARCHS="arm64 x86_64" \ ONLY_ACTIVE_ARCH=NO \ archive echo "" echo "Step 1.5: Setting up Code Signing (if available)" echo "================================================" if [[ -n "$BUILD_CERTIFICATE_BASE64" && -n "$P12_PASSWORD" ]]; then echo "🔐 Setting up code signing certificate from .env..." # Decode and import certificate echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > "$BUILD_DIR/certificate.p12" # Import into keychain (temporary) security import "$BUILD_DIR/certificate.p12" -k ~/Library/Keychains/login.keychain-db -P "$P12_PASSWORD" -T /usr/bin/codesign echo "✅ Certificate imported successfully" echo "📝 Note: Certificate will remain in keychain after script completion" else echo "⚠️ No certificate in .env - will use existing keychain certificates" fi echo "" echo "Step 2: Exporting App" echo "====================" # Use existing ExportOptions.plist or create a basic one if [[ ! -f "ExportOptions.plist" ]]; then echo "Creating basic ExportOptions.plist..." cat > "$BUILD_DIR/ExportOptions.plist" << EOF method developer-id destination export EOF else cp ExportOptions.plist "$BUILD_DIR/ExportOptions.plist" fi xcodebuild \ -archivePath "$BUILD_DIR/$APP_NAME.xcarchive" \ -exportPath "$BUILD_DIR/export" \ -exportOptionsPlist "$BUILD_DIR/ExportOptions.plist" \ -exportArchive # Re-sign the framework explicitly to ensure proper signature if [[ -n "$BUILD_CERTIFICATE_BASE64" && -n "$P12_PASSWORD" ]]; then echo "🔏 Re-signing framework with Developer ID certificate..." FRAMEWORK_PATH="$BUILD_DIR/export/$APP_NAME.app/Contents/Frameworks/OpenMultitouchSupportXCF.framework" if [[ -d "$FRAMEWORK_PATH" ]]; then codesign --force --sign "Developer ID Application: Krish Shah (9ZRLG6277G)" \ --options runtime \ --timestamp \ "$FRAMEWORK_PATH/Versions/A/OpenMultitouchSupportXCF" codesign --force --sign "Developer ID Application: Krish Shah (9ZRLG6277G)" \ --options runtime \ --timestamp \ "$FRAMEWORK_PATH" echo "✅ Framework re-signed successfully" fi # Re-sign the main app to ensure everything is consistent echo "🔏 Re-signing main application..." codesign --force --sign "Developer ID Application: Krish Shah (9ZRLG6277G)" \ --options runtime \ --entitlements "TrackWeight/TrackWeight.entitlements" \ --timestamp \ --deep \ --strict \ "$BUILD_DIR/export/$APP_NAME.app" echo "✅ Application re-signed successfully" fi echo "" echo "Step 2.5: Verifying Universal Binary and Code Signatures" echo "========================================================" echo "🏗️ Verifying Universal Binary Architecture..." APP_BINARY="$BUILD_DIR/export/$APP_NAME.app/Contents/MacOS/$APP_NAME" if [[ -f "$APP_BINARY" ]]; then echo "📊 Binary architectures:" lipo -archs "$APP_BINARY" if lipo -archs "$APP_BINARY" | grep -q "arm64" && lipo -archs "$APP_BINARY" | grep -q "x86_64"; then echo "✅ Universal binary confirmed: Contains both ARM64 and x86_64" else echo "❌ Warning: Binary may not be universal" lipo -detailed_info "$APP_BINARY" fi fi # Check framework architecture if it exists FRAMEWORK_PATH="$BUILD_DIR/export/$APP_NAME.app/Contents/Frameworks/OpenMultitouchSupportXCF.framework" if [[ -d "$FRAMEWORK_PATH" ]]; then FRAMEWORK_BINARY="$FRAMEWORK_PATH/Versions/A/OpenMultitouchSupportXCF" if [[ -f "$FRAMEWORK_BINARY" ]]; then echo "📊 Framework architectures:" lipo -archs "$FRAMEWORK_BINARY" fi fi echo "🔍 Verifying main application signature..." codesign --verify --verbose "$BUILD_DIR/export/$APP_NAME.app" || echo "⚠️ Main app signature verification failed" echo "🔍 Verifying framework signature..." if [[ -d "$FRAMEWORK_PATH" ]]; then codesign --verify --verbose "$FRAMEWORK_PATH" || echo "⚠️ Framework signature verification failed" fi echo "🔍 Checking for hardened runtime..." RUNTIME_FLAGS=$(codesign --display --verbose "$BUILD_DIR/export/$APP_NAME.app" 2>&1 | grep "flags=") if [[ "$RUNTIME_FLAGS" == *"runtime"* ]]; then echo "✅ Hardened runtime enabled: $RUNTIME_FLAGS" else echo "⚠️ No hardened runtime detected: $RUNTIME_FLAGS" fi echo "🔍 Checking certificate validity..." codesign --display --verbose=4 "$BUILD_DIR/export/$APP_NAME.app" | grep -E "(Authority|Timestamp|TeamIdentifier)" || echo "Certificate details extracted" echo "🔍 Deep verification with online validation..." if codesign --verify --deep --strict --verbose=2 "$BUILD_DIR/export/$APP_NAME.app"; then echo "✅ Deep verification passed" else echo "⚠️ Deep verification failed but continuing..." fi echo "" echo "Step 3: Testing Notarization (Optional)" echo "=======================================" if [[ -n "$APPLE_ID" && -n "$APPLE_ID_PASSWORD" && -n "$APPLE_TEAM_ID" ]]; then echo "🍎 Starting notarization with provided credentials..." cd "$BUILD_DIR/export" echo "📦 Creating zip file for notarization..." # Use ditto for creating zip compatible with Apple's notarization service ditto -c -k --keepParent "$APP_NAME.app" "$APP_NAME.zip" echo "📤 Submitting for notarization..." if xcrun notarytool submit "$APP_NAME.zip" \ --apple-id "$APPLE_ID" \ --password "$APPLE_ID_PASSWORD" \ --team-id "$APPLE_TEAM_ID" \ --wait \ --timeout 20m; then echo "✅ Notarization successful!" echo "📎 Stapling notarization ticket..." xcrun stapler staple "$APP_NAME.app" echo "✅ Verifying notarization..." xcrun stapler validate "$APP_NAME.app" echo "✅ Notarization complete!" else echo "❌ Notarization failed!" echo "🔍 This could be due to:" echo " - Invalid Apple credentials" echo " - App signing issues" echo " - Missing hardened runtime" echo " - Sandbox/entitlement issues" echo " - Deprecated APIs" echo "" echo "⚠️ Continuing without notarization..." fi # Return to original directory cd "$BUILD_DIR" else echo "⚠️ Skipping notarization (set APPLE_ID, APPLE_ID_PASSWORD, APPLE_TEAM_ID to test)" fi echo "" echo "Step 4: Creating DMG" echo "===================" # Install create-dmg if not present if ! command -v create-dmg &> /dev/null; then echo "Installing create-dmg..." brew install create-dmg fi # Create a clean directory with only the app for DMG creation echo "📁 Preparing clean DMG contents..." DMG_STAGING="$BUILD_DIR/dmg_staging" rm -rf "$DMG_STAGING" mkdir -p "$DMG_STAGING" # Copy only the app to staging directory cp -R "$BUILD_DIR/export/$APP_NAME.app" "$DMG_STAGING/" # Create DMG with appropriate naming if [[ -n "$APPLE_ID" && -n "$APPLE_ID_PASSWORD" && -n "$APPLE_TEAM_ID" ]]; then DMG_NAME="$APP_NAME-local-NOTARIZED.dmg" else DMG_NAME="$APP_NAME-local-SIGNED.dmg" fi echo "📀 Creating professional DMG..." create-dmg \ --volname "$APP_NAME" \ --window-pos 200 120 \ --window-size 600 300 \ --icon-size 100 \ --icon "$APP_NAME.app" 175 120 \ --hide-extension "$APP_NAME.app" \ --app-drop-link 425 120 \ --hdiutil-quiet \ "$BUILD_DIR/$DMG_NAME" \ "$DMG_STAGING/" echo "" echo "🎉 Local Build Complete!" echo "=======================" echo "📦 DMG created: $BUILD_DIR/$DMG_NAME" echo "📁 Build directory: $BUILD_DIR" echo "" if [[ -n "$APPLE_ID" && -n "$APPLE_ID_PASSWORD" && -n "$APPLE_TEAM_ID" ]]; then echo "✅ Notarization was attempted using credentials from .env" else echo "ℹ️ To enable notarization, add these to your .env file:" echo " APPLE_ID=your@email.com" echo " APPLE_ID_PASSWORD=xxxx-xxxx-xxxx-xxxx" echo " APPLE_TEAM_ID=ABC1234567" fi echo "" echo "🔧 Cleaning up temporary files..." rm -f "$BUILD_DIR/certificate.p12" rm -rf "$DMG_STAGING" echo "✅ Cleanup complete"