mirror of
https://github.com/m5stack/StackChan.git
synced 2026-05-20 11:49:45 -07:00
@@ -1,21 +1,32 @@
|
||||
# StackChan Open Source
|
||||
# StackChan Open-Source
|
||||
|
||||
<img src="https://vm.pledgebox.com/uploads/20251229/5sSmfjGLj9YYIzfHOSXQm4K00adu4Gv1.png" width="60%">
|
||||
<img src="https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/1205/K151_stack_chan_main_pictures_01.webp" width="60%">
|
||||
|
||||
**Pre-order your StackChan**: https://m5stack.com/stackchan
|
||||
Here are StackChan related open-source resources, including source code of the StackChan firmware, remote controller firmware, mobile app (iOS and Android), and server.
|
||||
|
||||
> The software development is still in progress. Final features and documentation may change. Thank you for your understanding.
|
||||
Update of this repo could be a little late than the released firmware and mobile app.
|
||||
|
||||
----
|
||||
|
||||
<img src="https://cdn.shopify.com/s/files/1/0056/7689/2250/files/5a589623895f65487717894d9240f6b8.png" width="60%">
|
||||
|
||||
**StackChan is a super kawaii AI desktop robot co-created by M5Stack and the user community.** It uses the M5Stack **flagship IoT development kit [CoreS3](https://docs.m5stack.com/en/core/CoreS3)** as its main controller, powered by an ESP32-S3 SoC featuring a 240 MHz dual-core processor, with 16MB Flash and 8MB PSRAM onboard, and supporting Wi-Fi and BLE. The main unit also integrates a 2.0-inch capacitive touch display with a high-strength glass cover, a 0.3 MP camera, a proximity sensor, a 9-axis IMU (accelerometer + gyroscope + magnetometer), a microSD card slot, a 1W speaker, dual microphones, and power/reset buttons.
|
||||
**StackChan is a super kawaii AI desktop robot co-created by M5Stack and the user community.** It uses the M5Stack **flagship IoT development kit [CoreS3](https://docs.m5stack.com/en/core/CoreS3)** as its main controller, powered by an ESP32-S3 SoC featuring a 240 MHz dual-core processor, with 16MB Flash and 8MB PSRAM onboard, and supporting Wi-Fi and BLE. The main unit also integrates a 2.0-inch capacitive touch display with a high-strength glass cover, a 0.3 MP camera, a proximity & ambient light sensor, a 9-axis IMU (accelerometer + gyroscope + magnetometer), a microSD card slot, a 1W speaker, dual microphones, and power/reset buttons.
|
||||
|
||||
The **robot body**, connected to the main unit, includes a USB-C interface for power and data, a 700 mAh battery, two feedback servos (360-degree continuous rotation on the horizontal axis and 90-degree movement on the vertical axis), two rows totaling 12 RGB LEDs, infrared transmitter and receiver, a three-zone touch panel, and a full-featured NFC module.
|
||||
The **robot body**, connected to the main unit, includes a USB-C interface for power and data, a 550 mAh battery, two feedback servos (360-degree continuous rotation on the horizontal axis and 90-degree movement on the vertical axis), two rows totaling 12 RGB LEDs, infrared transmitter and receiver, a three-zone touch panel, and a full-featured NFC module.
|
||||
|
||||
The **factory firmware** comes with rich features, including vivid and cute facial expressions and motions, the XiaoZhi AI agent, as well as support for iOS app video calls, remote avatars, and discovering other nearby StackChan devices. The product also supports programming via Arduino and UiFlow2, making it easy to implement a wide range of custom functionalities.
|
||||
The **factory firmware** is feature-rich, including an AI Agent, lively and expressive animations, ESP-NOW wireless remote control, and online app downloads. It can connect to a mobile app for video viewing, remote avatar control, and more, and also supports online updates (OTA). The product also supports programming via Arduino, UiFlow2, and other methods, and can connect to various expansion units in the M5Stack ecosystem, making it easy to implement a wide range of custom functions.
|
||||
|
||||
> Do not forcibly rotate any movable parts connected to the motors by hand when you are unsure whether the motors are powered and under control, as this may cause hardware damage.
|
||||
> ⚠️ Do not forcibly rotate any movable parts connected to the motors by hand when you are unsure whether the motors are powered and under control, as this may cause hardware damage.
|
||||
|
||||
- **StackChan World iOS app**: https://apps.apple.com/app/stackchan-world/id6756086326
|
||||
- Purchase link: [M5Stack Official Store](https://shop.m5stack.com/products/stackchan-kawaii-co-created-open-source-ai-desktop-robot) | [淘宝 Taobao](https://item.taobao.com/item.htm?id=1042238294510)
|
||||
|
||||
- **StackChan World website**: https://stackchan.world/home
|
||||
- Product document page: [English](https://docs.m5stack.com/en/StackChan) | [日本語](https://docs.m5stack.com/ja/StackChan) | [中文](https://docs.m5stack.com/zh_CN/StackChan)
|
||||
|
||||
- Board support package: https://github.com/m5stack/StackChan-BSP
|
||||
|
||||
Thank you to the contributors of the StackChan community, especially:
|
||||
|
||||
|  |  |
|
||||
| -------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| [@stack_chan](https://x.com/stack_chan) | [@mongonta555](https://x.com/mongonta555) |
|
||||
| Shinya Ishikawa | Takao Akaki |
|
||||
|
||||
+361
-51
@@ -1,62 +1,372 @@
|
||||
# Project Setup and Running Instructions
|
||||
# StackChan App
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://flutter.dev/)
|
||||
[](https://dart.dev/)
|
||||
|
||||
A powerful Flutter application for controlling and interacting with the StackChan AI robot companion. Features include
|
||||
Bluetooth connectivity, AI conversation capabilities, facial expression rendering, and dance choreography.
|
||||
|
||||
## Features
|
||||
|
||||
- 🤖 **Bluetooth Device Management** - Connect and control StackChan robots via BLE
|
||||
- 💬 **AI Conversation** - Natural language interaction powered by XiaoZhi AI
|
||||
- 🎭 **Facial Expression Rendering** - Real-time 3D face animation using Three.js
|
||||
- 🎵 **Music & Dance** - Create and play dance choreographies with music
|
||||
- 📷 **Camera Integration** - AR features and face detection
|
||||
- 🔐 **Secure Communication** - RSA encryption for data transmission
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Flutter SDK**: 3.0+
|
||||
- **Dart SDK**: 3.0+
|
||||
- **iOS**: 14.0+ (for iOS deployment)
|
||||
- **Android**: API 21+ (for Android deployment)
|
||||
- **macOS**: 11.0+ (for macOS deployment)
|
||||
## Installation
|
||||
|
||||
### 1. Install Flutter
|
||||
|
||||
Follow the official Flutter installation guide for your operating system:
|
||||
|
||||
#### macOS
|
||||
|
||||
## 1. Clone the repository
|
||||
```bash
|
||||
git clone https://github.com/m5stack/StackChan
|
||||
cd StackChan/app
|
||||
# Download Flutter SDK
|
||||
git clone https://github.com/flutter/flutter.git -b stable
|
||||
export PATH="$PATH:`pwd`/flutter/bin"
|
||||
|
||||
# Verify installation
|
||||
flutter doctor
|
||||
```
|
||||
|
||||
## 2. Open the project in Xcode
|
||||
Open the project in Xcode:
|
||||
#### Windows
|
||||
|
||||
Double‑click the `.xcodeproj` file, or open Xcode → File → Open, then select the project.
|
||||
```bash
|
||||
# Download Flutter SDK from https://flutter.dev/docs/get-started/install/windows
|
||||
# Extract and add to PATH
|
||||
|
||||
1. Select your target device or simulator.
|
||||
|
||||
### Connect an iPhone (Optional but Recommended)
|
||||
- Connect your iPhone to the Mac using a USB cable.
|
||||
- Unlock the iPhone and tap **Trust This Computer** if prompted.
|
||||
- In Xcode, select your iPhone as the run destination at the top.
|
||||
|
||||
### Enable Developer Mode on iPhone (iOS 16+)
|
||||
> **Important:** Developer Mode will only appear after the iPhone has been connected to Xcode at least once.
|
||||
If you do not see this option, make sure your iPhone is connected to the Mac, unlocked, trusted, and recognized by Xcode.
|
||||
- On the iPhone, go to **Settings → Privacy & Security → Developer Mode**.
|
||||
- Turn on Developer Mode and restart the iPhone.
|
||||
- After restart, confirm enabling Developer Mode.
|
||||
|
||||
## 3. Configure Signing & Capabilities
|
||||
This step allows Xcode to install the app on your iPhone.
|
||||
|
||||
1. In Xcode, select the project in the left sidebar.
|
||||
2. Select the app target.
|
||||
3. Open the **Signing & Capabilities** tab.
|
||||
4. Sign in with your Apple ID (Xcode → Settings → Accounts → Add Apple ID).
|
||||
5. Set **Team** to your Apple ID.
|
||||
6. Change **Bundle Identifier** to a unique value, for example:
|
||||
`com.yourname.stackchan`
|
||||
7. Ensure no red error messages remain.
|
||||
|
||||
> **Note:** A free Apple ID is sufficient for testing on your own iPhone.
|
||||
|
||||
## 4. Modify network configuration
|
||||
Before running the app, you need to set the correct server IP:
|
||||
|
||||
1. Open the file `Network/Urls.swift`.
|
||||
2. Find the line defining the base URL, for example:
|
||||
```swift
|
||||
// Base URL configured according to the server's IP
|
||||
static let url = "192.168.51.24:12800/"
|
||||
# Verify installation
|
||||
flutter doctor
|
||||
```
|
||||
3. Replace the IP address (`192.168.51.24`) with the IP of the computer where the server is running.
|
||||
4. Save the file.
|
||||
|
||||
## 5. Run the project
|
||||
Press `Cmd + R` to build and run the app.
|
||||
#### Linux
|
||||
|
||||
> **Note:** The first build may take several minutes as Xcode prepares the environment.
|
||||
```bash
|
||||
# Download Flutter SDK
|
||||
git clone https://github.com/flutter/flutter.git -b stable
|
||||
export PATH="$PATH:`pwd`/flutter/bin"
|
||||
|
||||
If running on an iPhone for the first time, you may need to trust yourself as a developer:
|
||||
- On your iPhone, go to **Settings → General → VPN & Device Management → Trust Developer** and trust the developer profile that appears.
|
||||
# Install dependencies
|
||||
sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev
|
||||
|
||||
The app will now connect to the server at the IP you configured.
|
||||
# Verify installation
|
||||
flutter doctor
|
||||
```
|
||||
|
||||
### 2. Set Up Project
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd StackChan
|
||||
|
||||
# Install dependencies
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
### 3. Configure Backend Server
|
||||
|
||||
The application requires a backend server for full functionality. Configure the server endpoints before building:
|
||||
|
||||
#### Option A: Using Environment Configuration (Recommended)
|
||||
|
||||
Create a `.env` file in the project root or modify the configuration directly in code.
|
||||
|
||||
#### Option B: Direct Code Configuration
|
||||
|
||||
Modify `lib/network/urls.dart` to set your backend server URL:
|
||||
|
||||
```dart
|
||||
// lib/network/urls.dart
|
||||
class Urls {
|
||||
// Update this to your backend server address
|
||||
static const String url = "your-backend-server:port/";
|
||||
|
||||
// ... rest of the configuration
|
||||
}
|
||||
```
|
||||
|
||||
#### Option C: Configure Value Constants
|
||||
|
||||
Update `lib/util/value_constant.dart` for encryption keys and other constants:
|
||||
|
||||
```dart
|
||||
// lib/util/value_constant.dart
|
||||
class ValueConstant {
|
||||
// Server RSA Public Key for encryption
|
||||
static const String serverPublicKey = """
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
YOUR_SERVER_PUBLIC_KEY_HERE
|
||||
-----END PUBLIC KEY-----
|
||||
""";
|
||||
|
||||
// Client RSA Private Key for decryption
|
||||
static const String clientPrivateKey = """
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
YOUR_CLIENT_PRIVATE_KEY_HERE
|
||||
-----END RSA PRIVATE KEY-----
|
||||
""";
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: For production deployments, use environment variables or secure key management instead of hardcoding
|
||||
keys.
|
||||
|
||||
## Building the Application
|
||||
|
||||
### iOS
|
||||
|
||||
```bash
|
||||
# Install CocoaPods dependencies
|
||||
cd ios
|
||||
pod install
|
||||
cd ..
|
||||
|
||||
# Run on iOS simulator
|
||||
flutter run -d ios
|
||||
|
||||
# Build for release (iOS device)
|
||||
flutter build ios --release
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
# Run on Android emulator or connected device
|
||||
flutter run -d android
|
||||
|
||||
# Build APK for release
|
||||
flutter build apk --release
|
||||
|
||||
# Build App Bundle for Google Play
|
||||
flutter build appbundle --release
|
||||
```
|
||||
|
||||
### Android Release Signing (JKS)
|
||||
|
||||
For release builds (`apk --release` / `appbundle --release`), configure a keystore instead of hardcoding passwords in
|
||||
`build.gradle.kts`.
|
||||
|
||||
#### 1. Generate a JKS file
|
||||
|
||||
```bash
|
||||
keytool -genkeypair -v \
|
||||
-keystore android/app/release.jks \
|
||||
-alias release \
|
||||
-keyalg RSA -keysize 2048 -validity 10000
|
||||
```
|
||||
|
||||
#### 2. Create `android/key.properties`
|
||||
|
||||
```properties
|
||||
storePassword=YOUR_STORE_PASSWORD
|
||||
keyPassword=YOUR_KEY_PASSWORD
|
||||
keyAlias=release
|
||||
storeFile=../app/release.jks
|
||||
```
|
||||
|
||||
> `android/.gitignore` already ignores `key.properties` and `*.jks`. Keep these files private.
|
||||
|
||||
#### 3. Load the properties in `android/app/build.gradle.kts`
|
||||
|
||||
Use Kotlin DSL style configuration:
|
||||
|
||||
```kotlin
|
||||
import java.util.Properties
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(keystorePropertiesFile.inputStream())
|
||||
}
|
||||
|
||||
android {
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Build release artifacts
|
||||
|
||||
```bash
|
||||
flutter build apk --release
|
||||
flutter build appbundle --release
|
||||
```
|
||||
|
||||
#### 5. CI/CD recommendation
|
||||
|
||||
In CI, inject signing values through environment variables or secure secrets storage. Do not commit keystore passwords or
|
||||
private keys to the repository.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart # Application entry point
|
||||
├── app_state.dart # Global state management
|
||||
├── model/ # Data models
|
||||
│ ├── XiaoZhi/ # AI service models
|
||||
│ ├── blue_device_info.dart # Bluetooth device models
|
||||
│ ├── dance_list.dart # Dance choreography models
|
||||
│ └── ...
|
||||
├── network/ # Network layer
|
||||
│ ├── http.dart # HTTP client with interceptors
|
||||
│ ├── urls.dart # API endpoint configurations
|
||||
│ └── web_socket_util.dart # WebSocket management
|
||||
├── util/ # Utilities
|
||||
│ ├── value_constant.dart # App constants and keys
|
||||
│ ├── rsa_util.dart # RSA encryption/decryption
|
||||
│ ├── blue_util.dart # Bluetooth utilities
|
||||
│ ├── music_util.dart # Music and audio processing
|
||||
│ └── ...
|
||||
└── view/ # UI layer
|
||||
├── home/ # Home screens
|
||||
├── popup/ # Modal screens
|
||||
└── util/ # UI components and widgets
|
||||
```
|
||||
|
||||
## Backend API Integration
|
||||
|
||||
The application integrates with two main backend services:
|
||||
|
||||
### 1. StackChan Backend (`lib/network/urls.dart`)
|
||||
|
||||
- Device registration and management
|
||||
- Dance choreography storage
|
||||
- User authentication
|
||||
- File upload and media management
|
||||
|
||||
### 2. XiaoZhi AI Service (`lib/util/XiaoZhi_util.dart`)
|
||||
|
||||
- AI conversation and chat functionality
|
||||
- Agent management and configuration
|
||||
- TTS (Text-to-Speech) voice selection
|
||||
- License and activation management
|
||||
|
||||
**Base URL Configuration:**
|
||||
|
||||
- StackChan Backend: `http://<server-ip>:<port>/stackChan/`
|
||||
- XiaoZhi AI: `https://XiaoZhi.me/`
|
||||
|
||||
## Development
|
||||
|
||||
### Code Style
|
||||
|
||||
This project follows the official Dart and Flutter style guidelines:
|
||||
|
||||
- Use `camelCase` for variables and functions
|
||||
- Use `PascalCase` for classes and types
|
||||
- Use `snake_case` for JSON keys (API communication)
|
||||
- Document public APIs with doc comments
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
flutter test
|
||||
|
||||
# Run specific test file
|
||||
flutter test test/widget_test.dart
|
||||
|
||||
# Run with coverage
|
||||
flutter test --coverage
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
# Run static analysis
|
||||
flutter analyze
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details.
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. Flutter doctor reports missing dependencies**
|
||||
|
||||
- Follow the instructions provided by `flutter doctor` to install missing components
|
||||
- For iOS: Ensure Xcode is installed and command line tools are selected
|
||||
- For Android: Ensure Android Studio and SDK are properly configured
|
||||
|
||||
**2. Bluetooth not working**
|
||||
|
||||
- Ensure Bluetooth permissions are granted
|
||||
- For iOS: Check `NSBluetoothAlwaysUsageDescription` in Info.plist
|
||||
- For Android: Check `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` permissions
|
||||
|
||||
**3. Backend connection fails**
|
||||
|
||||
- Verify server URL is correct in `lib/network/urls.dart`
|
||||
- Check network connectivity
|
||||
- Verify backend server is running and accessible
|
||||
- Check SSL certificates for HTTPS connections
|
||||
|
||||
**4. Build fails on iOS**
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
rm -rf Pods Podfile.lock
|
||||
pod install --repo-update
|
||||
cd ..
|
||||
flutter clean
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- M5Stack Technology CO LTD for the StackChan hardware
|
||||
- Flutter team for the amazing framework
|
||||
- Three.js for 3D rendering capabilities
|
||||
- All contributors and open source libraries used in this project
|
||||
|
||||
## Support
|
||||
|
||||
For support, please:
|
||||
|
||||
1. Check the [Issues](../../issues) page for known problems
|
||||
2. Create a new issue if your problem isn't already listed
|
||||
3. For security issues, please contact security@m5stack.com directly
|
||||
|
||||
---
|
||||
|
||||
**Note**: This application requires compatible StackChan hardware and backend services for full functionality.
|
||||
@@ -1,404 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0E4478B92F0A538600010197 /* README.MD in Resources */ = {isa = PBXBuildFile; fileRef = 0E4478B82F0A538600010197 /* README.MD */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0E4478B82F0A538600010197 /* README.MD */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.MD; sourceTree = "<group>"; };
|
||||
0EBD7D382ECDA27C0001A9D1 /* StackChan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StackChan.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
0EBD7E222ECDC9510001A9D1 /* Exceptions for "StackChan" folder in "StackChan" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 0EBD7D372ECDA27C0001A9D1 /* StackChan */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
0EBD7D3A2ECDA27C0001A9D1 /* StackChan */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
0EBD7E222ECDC9510001A9D1 /* Exceptions for "StackChan" folder in "StackChan" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
Info.plist = text.xml;
|
||||
};
|
||||
path = StackChan;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
0EBD7D352ECDA27C0001A9D1 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0EBD7D2F2ECDA27C0001A9D1 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0EBD7D3A2ECDA27C0001A9D1 /* StackChan */,
|
||||
0EBD7D392ECDA27C0001A9D1 /* Products */,
|
||||
0E4478B82F0A538600010197 /* README.MD */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0EBD7D392ECDA27C0001A9D1 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0EBD7D382ECDA27C0001A9D1 /* StackChan.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
0EBD7D372ECDA27C0001A9D1 /* StackChan */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 0EBD7D432ECDA27D0001A9D1 /* Build configuration list for PBXNativeTarget "StackChan" */;
|
||||
buildPhases = (
|
||||
0EBD7D342ECDA27C0001A9D1 /* Sources */,
|
||||
0EBD7D352ECDA27C0001A9D1 /* Frameworks */,
|
||||
0EBD7D362ECDA27C0001A9D1 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
0EBD7D3A2ECDA27C0001A9D1 /* StackChan */,
|
||||
);
|
||||
name = StackChan;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = StackChan;
|
||||
productReference = 0EBD7D382ECDA27C0001A9D1 /* StackChan.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
0EBD7D302ECDA27C0001A9D1 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 2610;
|
||||
LastUpgradeCheck = 2620;
|
||||
TargetAttributes = {
|
||||
0EBD7D372ECDA27C0001A9D1 = {
|
||||
CreatedOnToolsVersion = 26.1.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 0EBD7D332ECDA27C0001A9D1 /* Build configuration list for PBXProject "StackChan" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 0EBD7D2F2ECDA27C0001A9D1;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 0EBD7D392ECDA27C0001A9D1 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
0EBD7D372ECDA27C0001A9D1 /* StackChan */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
0EBD7D362ECDA27C0001A9D1 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0E4478B92F0A538600010197 /* README.MD in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
0EBD7D342ECDA27C0001A9D1 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
0EBD7D412ECDA27D0001A9D1 /* 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 = NG678HLKHZ;
|
||||
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;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
0EBD7D422ECDA27D0001A9D1 /* 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 = NG678HLKHZ;
|
||||
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;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
0EBD7D442ECDA27D0001A9D1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = 0EBD7D3A2ECDA27C0001A9D1 /* StackChan */;
|
||||
baseConfigurationReferenceRelativePath = App.xcconfig;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = StackChan/StackChan.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = NG678HLKHZ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
HEADER_SEARCH_PATHS = "";
|
||||
INFOPLIST_FILE = StackChan/Info.plist;
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth permission is required to connect to nearby devices";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Camera permission is required to scan the code";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is required to discover devices";
|
||||
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Location permission is required to access Wi-Fi information";
|
||||
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Location permission is required to access Wi-Fi information";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.StackChan;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
0EBD7D452ECDA27D0001A9D1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = 0EBD7D3A2ECDA27C0001A9D1 /* StackChan */;
|
||||
baseConfigurationReferenceRelativePath = App.xcconfig;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = StackChan/StackChan.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = NG678HLKHZ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
HEADER_SEARCH_PATHS = "";
|
||||
INFOPLIST_FILE = StackChan/Info.plist;
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth permission is required to connect to nearby devices";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Camera permission is required to scan the code";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is required to discover devices";
|
||||
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Location permission is required to access Wi-Fi information";
|
||||
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Location permission is required to access Wi-Fi information";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.StackChan;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
0EBD7D332ECDA27C0001A9D1 /* Build configuration list for PBXProject "StackChan" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
0EBD7D412ECDA27D0001A9D1 /* Debug */,
|
||||
0EBD7D422ECDA27D0001A9D1 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
0EBD7D432ECDA27D0001A9D1 /* Build configuration list for PBXNativeTarget "StackChan" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
0EBD7D442ECDA27D0001A9D1 /* Debug */,
|
||||
0EBD7D452ECDA27D0001A9D1 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 0EBD7D302ECDA27C0001A9D1 /* Project object */;
|
||||
}
|
||||
BIN
Binary file not shown.
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "632D2E91-955D-4E23-9652-CB7F63F6388B"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>StackChan.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>0EBD7D372ECDA27C0001A9D1</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
@@ -1,268 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import CoreBluetooth
|
||||
import ARKit
|
||||
import Combine
|
||||
|
||||
enum PageType: Hashable {
|
||||
case minicryEmotion
|
||||
case cameraPage
|
||||
case dance
|
||||
}
|
||||
|
||||
class AppState: ObservableObject {
|
||||
static let shared = AppState()
|
||||
private init() {}
|
||||
|
||||
static let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
||||
|
||||
static var isRelease : Bool = true
|
||||
@Published var showAlert: Bool = false
|
||||
@Published var alertTitle: String = ""
|
||||
var alertAction: (() -> Void)? = nil
|
||||
|
||||
func presentAlert(title: String,action: (() -> Void)? = nil) {
|
||||
self.alertTitle = title
|
||||
self.alertAction = action
|
||||
self.showAlert = true
|
||||
}
|
||||
|
||||
@AppStorage("deviceMac") var deviceMac: String = ""
|
||||
|
||||
@Published var stackChanPath: [PageType] = []
|
||||
@Published var nearbyPath: [PageType] = []
|
||||
@Published var settingsPath: [PageType] = []
|
||||
|
||||
@Published var showBindingDevice = false
|
||||
@Published var forcedDisplayBindingDevice = true
|
||||
|
||||
@Published var showCjamgeNameAlert: Bool = false
|
||||
|
||||
@Published var showBindingDeviceAlert: Bool = false
|
||||
|
||||
@Published var newName: String = ""
|
||||
@Published var deviceInfo: Device = Device()
|
||||
|
||||
let detector = DistanceDetector()
|
||||
@Published var showSwitchFace: Bool = false
|
||||
|
||||
@Published var blufDeviceList: [BlufiDeviceInfo] = []
|
||||
|
||||
/// Whether currently pairing a device
|
||||
@Published var showDeviceWifiSet = false
|
||||
|
||||
// Manual shutdown time, if just manually shut down, temporarily do not configure
|
||||
var manualShutdownTime: Date? = nil
|
||||
|
||||
@Published var deviceIsOnline: Bool = false
|
||||
|
||||
func connectBulDevice(macAddress: String) {
|
||||
if BlufiUtil.shared.blueSwitch {
|
||||
BlufiUtil.shared.startScan()
|
||||
} else {
|
||||
BlufiUtil.shared.centralManagerDidUpdateState = { state in
|
||||
switch state {
|
||||
case .poweredOn:
|
||||
BlufiUtil.shared.startScan()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
BlufiUtil.shared.characteristicCallback = { characteristic in
|
||||
if characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) {
|
||||
|
||||
if characteristic.uuid.uuidString == "E2E5E5E2-1234-5678-1234-56789ABCDEF0" {
|
||||
BlufiUtil.shared.writeExpressionCharacteristic = characteristic
|
||||
print("✏️ Expression writable characteristic assigned: \(characteristic.uuid)")
|
||||
}
|
||||
|
||||
if characteristic.uuid.uuidString == "E2E5E5E1-1234-5678-1234-56789ABCDEF0" {
|
||||
BlufiUtil.shared.writeHeadCharacteristic = characteristic
|
||||
print("✏️ Head writable characteristic assigned: \(characteristic.uuid)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func connectWebSocket() {
|
||||
let webSocketUrl = Urls.getWebSocketUrl() + "?mac=" + deviceMac + "&deviceType=App&deviceId=" + AppState.deviceId
|
||||
WebSocketUtil.shared.connect(urlString: webSocketUrl)
|
||||
}
|
||||
|
||||
func sendWebSocketMessage(_ msgType: MsgType,_ data: Data? = nil) {
|
||||
var buffer = Data([msgType.rawValue])
|
||||
|
||||
let payload = data ?? Data()
|
||||
|
||||
// payload length
|
||||
let dataLen = UInt32(payload.count)
|
||||
buffer.append(UInt8((dataLen >> 24) & 0xFF))
|
||||
buffer.append(UInt8((dataLen >> 16) & 0xFF))
|
||||
buffer.append(UInt8((dataLen >> 8) & 0xFF))
|
||||
buffer.append(UInt8(dataLen & 0xFF))
|
||||
|
||||
// data
|
||||
buffer.append(payload)
|
||||
|
||||
WebSocketUtil.shared.send(data: buffer)
|
||||
}
|
||||
|
||||
/// Parse message data
|
||||
func parseMessage(message: Data) -> (MsgType?,Data?) {
|
||||
guard message.count >= 5 else {
|
||||
return (nil,nil)
|
||||
}
|
||||
|
||||
let typeByte = message[0]
|
||||
guard let msgType = MsgType(rawValue: typeByte) else {
|
||||
return (nil,nil)
|
||||
}
|
||||
|
||||
let lengthData = message[1...4]
|
||||
let dataLength = lengthData.reduce(0) { (result, byte) -> UInt32 in
|
||||
return (result << 8) | UInt32(byte)
|
||||
}
|
||||
|
||||
if message.count < 5 + Int(dataLength) {
|
||||
return (nil,nil)
|
||||
}
|
||||
|
||||
let payload = message[5..<(5 + Int(dataLength))]
|
||||
|
||||
return (msgType, Data(payload))
|
||||
}
|
||||
|
||||
func updateDeviceInfo() {
|
||||
let map = [
|
||||
ValueConstant.mac: deviceMac,
|
||||
ValueConstant.name: deviceInfo.name,
|
||||
]
|
||||
Networking.shared.put(pathUrl: Urls.deviceInfo, parameters: map) { result in
|
||||
switch result {
|
||||
case .success(let success):
|
||||
do {
|
||||
let response = try Response<String>.decode(from: success)
|
||||
if response.isSuccess {
|
||||
print("Update successful")
|
||||
}
|
||||
} catch {
|
||||
print("Failed to parse data")
|
||||
}
|
||||
case .failure(let failure):
|
||||
print("Request failed:", failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Distance detection, callback when close
|
||||
func startDistanceDetection() {
|
||||
detector.startDistanceDetection(
|
||||
distanceUpdate: { distance in
|
||||
let distanceInCm = distance * 100
|
||||
if distanceInCm < 5 {
|
||||
if self.showSwitchFace == false {
|
||||
self.showSwitchFace = true
|
||||
}
|
||||
}
|
||||
},
|
||||
belowThreshold: {
|
||||
// Execute your business logic
|
||||
// For example: stop machine, send notification, etc.
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func stopDistanceDetection() {
|
||||
detector.stopDistanceDetection()
|
||||
}
|
||||
|
||||
// Wrapper for ARSessionDelegate
|
||||
class ARSessionDelegateWrapper: NSObject, ARSessionDelegate {
|
||||
var onFrameUpdate: (ARFrame) -> Void
|
||||
|
||||
init(onFrameUpdate: @escaping (ARFrame) -> Void) {
|
||||
self.onFrameUpdate = onFrameUpdate
|
||||
}
|
||||
|
||||
func session(_ session: ARSession, didUpdate frame: ARFrame) {
|
||||
onFrameUpdate(frame)
|
||||
}
|
||||
}
|
||||
|
||||
func getDeviceInfo() {
|
||||
let map = [
|
||||
ValueConstant.mac: deviceMac
|
||||
]
|
||||
Networking.shared.get(pathUrl: Urls.deviceInfo,parameters: map) { result in
|
||||
switch result {
|
||||
case .success(let success):
|
||||
do {
|
||||
let response = try Response<Device>.decode(from: success)
|
||||
if response.isSuccess, let deviceInfo = response.data {
|
||||
withAnimation {
|
||||
self.deviceInfo = deviceInfo
|
||||
self.newName = self.deviceInfo.name ?? ""
|
||||
}
|
||||
if deviceInfo.name == "" {
|
||||
self.showCjamgeNameAlert = true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Failed to parse data")
|
||||
}
|
||||
case .failure(let failure):
|
||||
print("Request failed:", failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable Bluetooth functionality
|
||||
func openBlufi() {
|
||||
BlufiUtil.shared.blufDevicesMonitoring = { discovereDevices in
|
||||
self.blufDeviceList = discovereDevices
|
||||
|
||||
// Check manualShutdownTime, if exists and not exceeding 5 seconds, temporarily do not show popup
|
||||
if let shutdownTime = self.manualShutdownTime {
|
||||
let timeInterval = Date().timeIntervalSince(shutdownTime)
|
||||
if timeInterval < 5 {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !self.showDeviceWifiSet {
|
||||
if !self.blufDeviceList.isEmpty {
|
||||
self.showDeviceWifiSet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// webSocket Message Monitoring
|
||||
func webSocketMessageMonitoring() {
|
||||
WebSocketUtil.shared.addObserver(for: "App") { (message: URLSessionWebSocketTask.Message) in
|
||||
switch message {
|
||||
case .data(let data):
|
||||
let result = self.parseMessage(message: data)
|
||||
if let msgType = result.0 {
|
||||
switch msgType {
|
||||
case MsgType.deviceOnline:
|
||||
self.deviceIsOnline = false
|
||||
case MsgType.deviceOffline:
|
||||
self.deviceIsOnline = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
case .string(let text):
|
||||
print("Received a regular message: \(text)")
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.057",
|
||||
"green" : "0.689",
|
||||
"red" : "0.936"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"localizable" : true
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app_logo.jpg",
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "7.595.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_stackchan-mpc._tcp</string>
|
||||
</array>
|
||||
<key>com.apple.developer.networking.wifi-info</key>
|
||||
<true/>
|
||||
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>This app requires access to the local network to communicate with devices.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We need access to the microphone to capture audio data.</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>StackChan World</string>
|
||||
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Save the photo to the album</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
struct BlufiModel<T:Codable>: Codable {
|
||||
|
||||
var cmd: String? = nil
|
||||
var data: T? = nil
|
||||
|
||||
func toJson() -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
guard let jsonData = try? encoder.encode(self) else { return nil }
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func fromJson(_ json: String) -> BlufiModel<T>? {
|
||||
guard let jsonData = json.data(using: .utf8) else { return nil }
|
||||
let decoder = JSONDecoder()
|
||||
return try? decoder.decode(BlufiModel<T>.self, from: jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
struct BlufiWifi : Codable {
|
||||
var ssid: String?
|
||||
var password: String?
|
||||
|
||||
func toJson() -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
guard let jsonData = try? encoder.encode(self) else { return nil }
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func fromJson(_ json: String) -> BlufiWifi? {
|
||||
guard let jsonData = json.data(using: .utf8) else { return nil }
|
||||
let decoder = JSONDecoder()
|
||||
return try? decoder.decode(BlufiWifi.self, from: jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
struct BlufiNotifyState : Codable {
|
||||
var type: Int?
|
||||
var state: String?
|
||||
|
||||
func toJson() -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
guard let jsonData = try? encoder.encode(self) else { return nil }
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func fromJson(_ json: String) -> BlufiNotifyState? {
|
||||
guard let jsonData = json.data(using: .utf8) else { return nil }
|
||||
let decoder = JSONDecoder()
|
||||
return try? decoder.decode(BlufiNotifyState.self, from: jsonData)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Device : Codable {
|
||||
var mac: String = UUID().uuidString
|
||||
var name: String? = nil
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
enum MsgType: UInt8, Codable {
|
||||
case opus = 0x01
|
||||
case jpeg = 0x02
|
||||
case controlAvatar = 0x03
|
||||
case controlMotion = 0x04
|
||||
|
||||
case onCamera = 0x05
|
||||
case offCamera = 0x06
|
||||
|
||||
case textMessage = 0x07
|
||||
case requestCall = 0x09
|
||||
case refuseCall = 0x0A
|
||||
case agreeCall = 0x0B
|
||||
case hangupCall = 0x0C
|
||||
|
||||
case updateDeviceName = 0x0D
|
||||
case getDeviceName = 0x0E
|
||||
|
||||
case ping = 0x10
|
||||
case pong = 0x11
|
||||
|
||||
case onPhoneScreen = 0x12
|
||||
case offPhoneScreen = 0x13
|
||||
|
||||
case dance = 0x14
|
||||
|
||||
case getAvatarPosture = 0x15
|
||||
|
||||
case deviceOffline = 0x16
|
||||
case deviceOnline = 0x17
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Post : Codable{
|
||||
var id: Int
|
||||
var mac: String? = nil
|
||||
var name: String? = nil
|
||||
var contentText: String? = nil
|
||||
var contentImage: String? = nil
|
||||
var createdAt: String? = nil
|
||||
var postCommentList: [PostComment]? = nil
|
||||
}
|
||||
|
||||
struct PostComment: Codable {
|
||||
var id: Int? = nil
|
||||
var postId: Int? = nil
|
||||
var mac: String? = nil
|
||||
var name: String? = nil
|
||||
var content: String? = nil
|
||||
var createAt: String? = nil
|
||||
}
|
||||
|
||||
|
||||
struct GetPostComment: Codable {
|
||||
var list: [PostComment]? = nil
|
||||
var total: Int? = nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Response<T: Codable>: Codable {
|
||||
let code: Int?
|
||||
let message: String?
|
||||
let data: T?
|
||||
|
||||
var isSuccess: Bool {
|
||||
return code == 0
|
||||
}
|
||||
|
||||
func unwrap(or defaultValue: T) -> T {
|
||||
return data ?? defaultValue
|
||||
}
|
||||
|
||||
static func decode(from jsonData: Data) throws -> Response<T> {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
do {
|
||||
return try decoder.decode(Response<T>.self, from: jsonData)
|
||||
} catch let DecodingError.dataCorrupted(context) {
|
||||
print("🔴 Data corrupted: \(context.debugDescription)")
|
||||
printCodingPath(context.codingPath)
|
||||
printJSON(jsonData)
|
||||
throw DecodingError.dataCorrupted(context)
|
||||
} catch let DecodingError.keyNotFound(key, context) {
|
||||
print("🔴 Key '\(key.stringValue)' not found: \(context.debugDescription)")
|
||||
printCodingPath(context.codingPath)
|
||||
printJSON(jsonData)
|
||||
throw DecodingError.keyNotFound(key, context)
|
||||
} catch let DecodingError.typeMismatch(type, context) {
|
||||
print("🔴 Type '\(type)' mismatch: \(context.debugDescription)")
|
||||
printCodingPath(context.codingPath)
|
||||
printJSON(jsonData)
|
||||
throw DecodingError.typeMismatch(type, context)
|
||||
} catch let DecodingError.valueNotFound(value, context) {
|
||||
print("🔴 Value '\(value)' not found: \(context.debugDescription)")
|
||||
printCodingPath(context.codingPath)
|
||||
printJSON(jsonData)
|
||||
throw DecodingError.valueNotFound(value, context)
|
||||
} catch {
|
||||
print("🔴 Other errors in the analysis: \(error)")
|
||||
printJSON(jsonData)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
static func decode(from json: [String: Any]) throws -> Response<T> {
|
||||
let data = try JSONSerialization.data(withJSONObject: json, options: [])
|
||||
return try decode(from: data)
|
||||
}
|
||||
|
||||
func debugDescription() -> String {
|
||||
return "Response(code: \(code ?? 0), message: \(message ?? ""), data: \(String(describing: data)))"
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func printCodingPath(_ codingPath: [CodingKey]) {
|
||||
let path = codingPath.map { $0.stringValue }.joined(separator: ".")
|
||||
print("📍 Error path: \(path)")
|
||||
}
|
||||
|
||||
fileprivate func printJSON(_ data: Data) {
|
||||
if let obj = try? JSONSerialization.jsonObject(with: data, options: []),
|
||||
let prettyData = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted]),
|
||||
let str = String(data: prettyData, encoding: .utf8) {
|
||||
print("📄 Original JSON:\n\(str)")
|
||||
} else if let str = String(data: data, encoding: .utf8) {
|
||||
print("📄 Original JSON:\n\(str)")
|
||||
} else {
|
||||
print("⚠️ Unable to parse the original JSON")
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
struct UploadFile : Codable {
|
||||
var path: String? = nil
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
class Networking {
|
||||
|
||||
static let shared = Networking()
|
||||
private init() {}
|
||||
|
||||
enum HTTPMethod: String {
|
||||
case GET,POST,PUT,DELETE
|
||||
}
|
||||
|
||||
private func request(
|
||||
urlString: String,
|
||||
method: HTTPMethod,
|
||||
parameters: Any? = nil,
|
||||
headers: [String: String] = [:],
|
||||
completion: @escaping (Result<Data, Error>) -> Void
|
||||
) {
|
||||
var finalURLString = urlString
|
||||
var httpBody: Data? = nil
|
||||
|
||||
if method == .GET {
|
||||
if let params = parameters as? [String: Any], !params.isEmpty {
|
||||
var components = URLComponents(string: urlString)
|
||||
components?.queryItems = params.map { URLQueryItem(name: $0.key, value: "\($0.value)") }
|
||||
if let urlWithQuery = components?.url?.absoluteString {
|
||||
finalURLString = urlWithQuery
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let params = parameters {
|
||||
requestSetContentType: do {
|
||||
requestSetBody: do {
|
||||
do {
|
||||
if let dict = params as? [String: Any] {
|
||||
httpBody = try JSONSerialization.data(withJSONObject: dict, options: [])
|
||||
} else if let array = params as? [Any] {
|
||||
httpBody = try JSONSerialization.data(withJSONObject: array, options: [])
|
||||
} else {
|
||||
httpBody = try JSONSerialization.data(withJSONObject: params, options: [])
|
||||
}
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let url = URL(string: finalURLString) else {
|
||||
completion(.failure(NSError(domain: "Invalid URL", code: -1)))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
|
||||
if method != .GET, httpBody != nil {
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = httpBody
|
||||
}
|
||||
|
||||
setHandler(request: &request, headers: headers)
|
||||
logRequest(request)
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
completion(.failure(NSError(domain: "No data returned", code: -2)))
|
||||
return
|
||||
}
|
||||
self.logResponse(data: data)
|
||||
completion(.success(data))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func get(pathUrl: String, parameters: [String: Any] = [:], headers: [String: String] = [:], baseUrlString: String? = nil, completion: @escaping (Result<Data, Error>) -> Void) {
|
||||
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
|
||||
request(urlString: finalUrl, method: .GET, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
func post(pathUrl: String, parameters: Any? = nil, headers: [String: String] = [:], baseUrlString: String? = nil, completion: @escaping (Result<Data, Error>) -> Void) {
|
||||
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
|
||||
request(urlString: finalUrl, method: .POST, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
private func setHandler(request: inout URLRequest, headers: [String: String]) {
|
||||
for (key, value) in headers {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
if let token = UserDefaults.standard.string(forKey: ValueConstant.token), !token.isEmpty {
|
||||
request.setValue(token, forHTTPHeaderField: ValueConstant.Authorization)
|
||||
}
|
||||
}
|
||||
|
||||
func postFromData(pathUrl: String,
|
||||
parameters: [String: Any?] = [:],
|
||||
headers: [String: String] = [:],
|
||||
baseUrlString: String? = nil,
|
||||
suffix:String? = nil,
|
||||
completion: @escaping (Result<Data, Error>) -> Void) {
|
||||
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
|
||||
guard let url = URL(string: finalUrl) else {
|
||||
completion(.failure(NSError(domain: "Invalid URL", code: -1)))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = HTTPMethod.POST.rawValue
|
||||
|
||||
let boundary = "Boundary-\(UUID().uuidString)"
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
setHandler(request: &request, headers: headers)
|
||||
|
||||
var requestBody = Data()
|
||||
|
||||
for (key, value) in parameters {
|
||||
if let value = value {
|
||||
if let fileData = value as? Data {
|
||||
let type = mimeType(for: fileData)
|
||||
let fileName = UUID().uuidString + (suffix ?? "")
|
||||
requestBody.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
requestBody.append("Content-Disposition: form-data; name=\"\(key)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
|
||||
requestBody.append("Content-Type: \(type)\r\n\r\n".data(using: .utf8)!)
|
||||
requestBody.append(fileData)
|
||||
requestBody.append("\r\n".data(using: .utf8)!)
|
||||
} else if let array = value as? [Any] {
|
||||
if let jsonData = try? JSONSerialization.data(withJSONObject: array, options: []) {
|
||||
let jsonString = String(data: jsonData, encoding: .utf8)
|
||||
requestBody.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
requestBody.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
|
||||
requestBody.append("\(jsonString ?? "[]")\r\n".data(using: .utf8)!)
|
||||
}
|
||||
} else if let dict = value as? [String:Any] {
|
||||
if let jsonData = try? JSONSerialization.data(withJSONObject: dict, options: []) {
|
||||
let jsonString = String(data: jsonData, encoding: .utf8)
|
||||
requestBody.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
requestBody.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
|
||||
requestBody.append("\(jsonString ?? "{}")\r\n".data(using: .utf8)!)
|
||||
}
|
||||
} else {
|
||||
let str = "\(value)"
|
||||
requestBody.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
requestBody.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
|
||||
requestBody.append("\(str)\r\n".data(using: .utf8)!)
|
||||
}
|
||||
}
|
||||
}
|
||||
requestBody.append("--\(boundary)--\r\n".data(using: .utf8)!)
|
||||
request.httpBody = requestBody
|
||||
|
||||
logRequest(request)
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
completion(.failure(NSError(domain: "No data returned", code: -2)))
|
||||
return
|
||||
}
|
||||
self.logResponse(data: data)
|
||||
completion(.success(data))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func anyToJson(data: Any) -> String {
|
||||
func convert(_ value: Any) -> Any {
|
||||
if let dict = value as? [String: Any] {
|
||||
var newDict: [String: Any] = [:]
|
||||
for (k, v) in dict {
|
||||
newDict[k] = convert(v)
|
||||
}
|
||||
return newDict
|
||||
} else if let dict = value as? [String: Any?] {
|
||||
var newDict: [String: Any] = [:]
|
||||
for (k, v) in dict {
|
||||
if let unwrapped = v {
|
||||
newDict[k] = convert(unwrapped)
|
||||
} else {
|
||||
newDict[k] = NSNull()
|
||||
}
|
||||
}
|
||||
return newDict
|
||||
} else if let array = value as? [Any] {
|
||||
return array.map { convert($0) }
|
||||
} else if let array = value as? [Any?] {
|
||||
return array.map { $0 == nil ? NSNull() : convert($0!) }
|
||||
} else if value is Int || value is Double || value is Bool || value is String {
|
||||
return value
|
||||
} else {
|
||||
return "\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
let converted = convert(data)
|
||||
|
||||
if JSONSerialization.isValidJSONObject(converted) {
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: converted, options: [])
|
||||
return String(data: jsonData, encoding: .utf8) ?? "[]"
|
||||
} catch {
|
||||
print("JSON Serialization error: \(error)")
|
||||
return "[]"
|
||||
}
|
||||
} else {
|
||||
if let str = converted as? String {
|
||||
return "\"\(str)\""
|
||||
} else {
|
||||
return "\(converted)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func put(pathUrl: String, parameters: Any? = nil, headers: [String: String] = [:], baseUrlString: String? = nil, completion: @escaping (Result<Data, Error>) -> Void) {
|
||||
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
|
||||
request(urlString: finalUrl, method: .PUT, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
func delete(pathUrl: String, parameters: Any? = nil, headers: [String: String] = [:], baseUrlString: String? = nil, completion: @escaping (Result<Data, Error>) -> Void) {
|
||||
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
|
||||
request(urlString: finalUrl, method: .DELETE, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
func download(pathUrl: String,
|
||||
parameters: [String: Any] = [:],
|
||||
headers: [String: String] = [:],
|
||||
baseUrlString: String? = nil,
|
||||
completion: @escaping (Result<String, Error>) -> Void) {
|
||||
|
||||
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
|
||||
let key = FileUtils.shared.hashedKey(for: finalUrl)
|
||||
let cacheURL = FileUtils.shared.cacheDirectory().appendingPathComponent(key)
|
||||
|
||||
if FileManager.default.fileExists(atPath: cacheURL.path) {
|
||||
completion(.success(cacheURL.path))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: finalUrl)!)
|
||||
request.httpMethod = "GET"
|
||||
|
||||
setHandler(request: &request, headers: headers)
|
||||
|
||||
URLSession.shared.downloadTask(with: request) { tempURL, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
completion(.failure(NSError(domain: "No file downloaded", code: -3)))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let directory = cacheURL.deletingLastPathComponent()
|
||||
if !FileManager.default.fileExists(atPath: directory.path) {
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: cacheURL.path) {
|
||||
try FileManager.default.removeItem(at: cacheURL)
|
||||
}
|
||||
try FileManager.default.moveItem(at: tempURL, to: cacheURL)
|
||||
|
||||
completion(.success(cacheURL.path))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
private func logRequest(_ request: URLRequest) {
|
||||
print("➡️ Request URL: \(request.url?.absoluteString ?? "")")
|
||||
print("➡️ Method: \(request.httpMethod ?? "")")
|
||||
print("➡️ Headers: \(request.allHTTPHeaderFields ?? [:])")
|
||||
if let body = request.httpBody {
|
||||
if let bodyString = String(data: body, encoding: .utf8) {
|
||||
print("➡️ Body:")
|
||||
bodyString.jsonPrint()
|
||||
} else {
|
||||
let sizeInMB = Double(body.count) / (1024 * 1024)
|
||||
print(String(format: "➡️ Body (binary data, size: %.2f MB)", sizeInMB))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func logResponse(data: Data) {
|
||||
if let responseString = String(data: data, encoding: .utf8) {
|
||||
print("⬅️ Response:")
|
||||
responseString.jsonPrint()
|
||||
} else {
|
||||
print("⬅️ Response (binary data, length: \(data.count) bytes)")
|
||||
}
|
||||
}
|
||||
|
||||
private func mimeType(for data: Data) -> String {
|
||||
var bytes = [UInt8](repeating: 0, count: 1)
|
||||
data.copyBytes(to: &bytes, count: 1)
|
||||
switch bytes[0] {
|
||||
case 0xFF: return "image/jpeg"
|
||||
case 0x89: return "image/png"
|
||||
case 0x47: return "image/gif"
|
||||
case 0x25: return "application/pdf"
|
||||
case 0x49, 0x4D: return "image/tiff"
|
||||
default: return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user