commit 0cff1e509d942301d63e42610d7d1e0981162f0e Author: ivan Date: Mon Mar 9 11:41:49 2026 +0800 新增:基础UI加登录接口 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce1ebbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code +.vscode/ + +# Flutter/Dart/Pub +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Android +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/Podfile.lock +**/ios/**/.symlinks/ +**/ios/**/Flutter/Flutter.framework +**/ios/**/Flutter/Flutter.podspec diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..921da61 --- /dev/null +++ b/.metadata @@ -0,0 +1,9 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. +version: + revision: "stable" + channel: "stable" + +project_type: app diff --git a/README.md b/README.md new file mode 100644 index 0000000..a934e7d --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# MagiEvery AI - Flutter 应用 + +基于 `design/MagiEveryAI-AB` 设计图实现的 AI 图像/视频生成应用。 + +## 功能 + +- **Home(图片生成)**:风格分类(All、Realistic、Anime、3D Render 等)、图片轮播、选择开始创作 +- **Video(视频生成)**:视频模板网格、积分消耗展示 +- **Gallery(我的画廊)**:图片/视频历史、网格展示 +- **Profile(个人中心)**:头像、UID、可用积分、Top Up、隐私政策、用户协议 +- **Create Masterpiece**:上传参考图、生成杰作 +- **Generation Progress**:生成进度、阶段提示、Pro Tip +- **Generation Result**:生成结果、保存与分享 +- **Credit Store**:积分商店、多种套餐购买 + +## 项目结构 + +``` +lib/ +├── main.dart # 应用入口与底部导航 +├── theme/app_theme.dart # MagiEveryAI-AB 设计系统 +├── screens/ # 各页面 +│ ├── image_generation_home_screen.dart +│ ├── video_generation_home_screen.dart +│ ├── gallery_screen.dart +│ ├── profile_screen.dart +│ ├── create_masterpiece_screen.dart +│ ├── generation_progress_screen.dart +│ ├── generation_result_screen.dart +│ └── credit_store_screen.dart +└── widgets/ + └── credit_badge.dart # 积分徽章 +``` + +## 运行 + +```bash +flutter pub get +flutter run +``` + +支持 iOS、Android 和 Web 平台。 diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..8d61579 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + prefer_const_constructors: true + prefer_const_declarations: true + avoid_print: false diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..8e449b0 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,65 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.petsheroai.app" + compileSdk 36 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "com.petsheroai.app" + minSdkVersion flutter.minSdkVersion + targetSdk 36 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + debug { + // 允许 HTTP 连接本地代理(手机无法直连域名时) + manifestPlaceholders = [usesCleartextTraffic: "true"] + } + release { + signingConfig signingConfigs.debug + manifestPlaceholders = [usesCleartextTraffic: "false"] + } + } +} + +flutter { + source '../..' +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6432857 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/petsheroai/app/MainActivity.kt b/android/app/src/main/kotlin/com/petsheroai/app/MainActivity.kt new file mode 100644 index 0000000..c3c9841 --- /dev/null +++ b/android/app/src/main/kotlin/com/petsheroai/app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.petsheroai.app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..634a1c0 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..634a1c0 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..760a6cc --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,21 @@ +allprojects { + repositories { + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/central' } + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..1079dd0 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,9 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true +systemProp.http.proxyHost= +systemProp.http.proxyPort= +systemProp.https.proxyHost= +systemProp.https.proxyPort= +systemProp.socksProxyHost= +systemProp.socksProxyPort= diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8520493 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..21603bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,28 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + + includeBuild("${flutterSdkPath()}/packages/flutter_tools/gradle") + + repositories { + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/public' } + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.9.1" apply false + id "org.jetbrains.kotlin.android" version "1.9.22" apply false +} + +include ":app" diff --git a/assets/images/.gitkeep b/assets/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/design/README.md b/design/README.md new file mode 100644 index 0000000..3aa8dad --- /dev/null +++ b/design/README.md @@ -0,0 +1,20 @@ +# Design + +设计文件目录,基于 [Pencil](https://pencil.dev) 的 `.pen` 格式。 + +## 文件说明 + +- **app-design.pen** - AI创作 App 设计稿,源自 [Figma 设计](https://blog-flat-29730549.figma.site/) + +## Pencil 格式 + +`.pen` 是 Pencil 设计工具的 JSON 格式,包含: + +- **variables** - 设计变量(颜色、尺寸等) +- **children** - 画布上的对象树(frame、text、rectangle 等) +- **reusable** - 可复用组件 +- **ref** - 组件实例引用 + +## 使用 + +在 [Pencil](https://pencil.dev) 中打开 `.pen` 文件即可编辑和预览设计。 diff --git a/design/images/generated-1772942434587.png b/design/images/generated-1772942434587.png new file mode 100644 index 0000000..ffdcb96 Binary files /dev/null and b/design/images/generated-1772942434587.png differ diff --git a/design/images/generated-1772942448443.png b/design/images/generated-1772942448443.png new file mode 100644 index 0000000..e358206 Binary files /dev/null and b/design/images/generated-1772942448443.png differ diff --git a/design/images/generated-1772942462462.png b/design/images/generated-1772942462462.png new file mode 100644 index 0000000..b298828 Binary files /dev/null and b/design/images/generated-1772942462462.png differ diff --git a/design/images/generated-1772942470719.png b/design/images/generated-1772942470719.png new file mode 100644 index 0000000..7a311f6 Binary files /dev/null and b/design/images/generated-1772942470719.png differ diff --git a/design/images/generated-1772942481069.png b/design/images/generated-1772942481069.png new file mode 100644 index 0000000..60a34f9 Binary files /dev/null and b/design/images/generated-1772942481069.png differ diff --git a/design/images/generated-1772942485221.png b/design/images/generated-1772942485221.png new file mode 100644 index 0000000..76286d0 Binary files /dev/null and b/design/images/generated-1772942485221.png differ diff --git a/design/images/generated-1772942494302.png b/design/images/generated-1772942494302.png new file mode 100644 index 0000000..3f358c9 Binary files /dev/null and b/design/images/generated-1772942494302.png differ diff --git a/design/images/generated-1772942500640.png b/design/images/generated-1772942500640.png new file mode 100644 index 0000000..e511370 Binary files /dev/null and b/design/images/generated-1772942500640.png differ diff --git a/design/images/generated-1772942509300.png b/design/images/generated-1772942509300.png new file mode 100644 index 0000000..1e3f26b Binary files /dev/null and b/design/images/generated-1772942509300.png differ diff --git a/design/images/generated-1772942514836.png b/design/images/generated-1772942514836.png new file mode 100644 index 0000000..393232d Binary files /dev/null and b/design/images/generated-1772942514836.png differ diff --git a/design/images/generated-1772942527064.png b/design/images/generated-1772942527064.png new file mode 100644 index 0000000..2a6d0f2 Binary files /dev/null and b/design/images/generated-1772942527064.png differ diff --git a/design/images/generated-1772942542657.png b/design/images/generated-1772942542657.png new file mode 100644 index 0000000..688758a Binary files /dev/null and b/design/images/generated-1772942542657.png differ diff --git a/design/pencil-app-client.pen b/design/pencil-app-client.pen new file mode 100644 index 0000000..ba169f9 --- /dev/null +++ b/design/pencil-app-client.pen @@ -0,0 +1,3408 @@ +{ + "version": "2.8", + "children": [ + { + "type": "frame", + "id": "bi8Au", + "x": 0, + "y": 0, + "name": "AI Video App", + "clip": true, + "width": 390, + "height": 844, + "fill": "#FAFAFA", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "C5k5y", + "name": "topNav", + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MnEWE", + "name": "navTitle", + "fill": "#18181B", + "content": "AI Video", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 20, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "k3CQZ", + "name": "creditsBadge", + "height": 32, + "fill": "#8B5CF620", + "cornerRadius": 14, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF620", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "gap": 6, + "padding": [ + 6, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "EWt3i", + "name": "creditsIcon", + "width": 16, + "height": 16, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#8B5CF6" + }, + { + "type": "text", + "id": "lqmbX", + "name": "creditsText", + "fill": "#8B5CF6", + "content": "1,280", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "oJ7P2", + "name": "contentArea", + "width": "fill_container", + "height": "fill_container", + "fill": "#FAFAFA", + "layout": "vertical", + "gap": 16, + "padding": [ + 0, + 20, + 24, + 20 + ], + "children": [ + { + "type": "frame", + "id": "bK6o6", + "name": "tabRow", + "width": "fill_container", + "height": 40, + "gap": 8, + "padding": [ + 4, + 0 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "M6pir", + "name": "tabAll", + "height": 32, + "fill": "#8B5CF6", + "cornerRadius": 20, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF630", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 4 + }, + "gap": 4, + "padding": [ + 6, + 14 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dKgTN", + "name": "tabAllText", + "fill": "#FFFFFF", + "content": "All", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "cqBRL", + "name": "tabTrending", + "height": 32, + "fill": "#F4F4F5", + "cornerRadius": 20, + "stroke": { + "thickness": 1, + "fill": "#E4E4E7" + }, + "gap": 4, + "padding": [ + 6, + 14 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eLVLh", + "name": "tabTrendingText", + "fill": "#71717A", + "content": "Trending", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Bfa7U", + "name": "tabNew", + "height": 32, + "fill": "#F4F4F5", + "cornerRadius": 20, + "stroke": { + "thickness": 1, + "fill": "#E4E4E7" + }, + "gap": 4, + "padding": [ + 6, + 14 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JEsyS", + "name": "tabNewText", + "fill": "#71717A", + "content": "New", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "2AP4w", + "name": "gridContainer", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "Ec101", + "name": "row1", + "width": "fill_container", + "gap": 16, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "CthJD", + "name": "card1", + "clip": true, + "width": 165, + "height": 248, + "fill": "#00000000", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "Q8BZG", + "x": 0, + "y": 0, + "name": "card1Top", + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "./images/generated-1772942470719.png", + "mode": "fill" + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "C8ERr", + "x": 58.5, + "y": 100, + "name": "card1PlayBtn", + "width": 48, + "height": 48, + "fill": "#FFFFFFE6", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000020", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "GL9ZN", + "name": "card1PlayIcon", + "width": 24, + "height": 24, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "frame", + "id": "XhQqY", + "x": 103, + "y": 12, + "name": "card1Credits", + "height": 24, + "fill": "#00000099", + "cornerRadius": 14, + "gap": 4, + "padding": [ + 4, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "nREv6", + "name": "card1CreditsIcon", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "hMnf5", + "name": "card1CreditsText", + "fill": "#FFFFFF", + "content": "50", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "8CMbL", + "x": 12, + "y": 192, + "name": "card1Btn", + "width": 141, + "height": 44, + "fill": "#8B5CF6", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UOYDo", + "name": "card1BtnText", + "fill": "#FFFFFF", + "content": "Generate Similar", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BRpaI", + "name": "card2", + "clip": true, + "width": 165, + "height": 248, + "fill": "#00000000", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "NlQjP", + "x": 0, + "y": 0, + "name": "card1Top", + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "./images/generated-1772942485221.png", + "mode": "fill" + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "u4Uhx", + "x": 58.5, + "y": 100, + "name": "card1PlayBtn", + "width": 48, + "height": 48, + "fill": "#FFFFFFE6", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000020", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "NVypk", + "name": "card1PlayIcon", + "width": 24, + "height": 24, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "frame", + "id": "PMX4R", + "x": 103, + "y": 12, + "name": "card1Credits", + "height": 24, + "fill": "#00000099", + "cornerRadius": 14, + "gap": 4, + "padding": [ + 4, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Rr2KZ", + "name": "card1CreditsIcon", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "SxYvO", + "name": "card1CreditsText", + "fill": "#FFFFFF", + "content": "50", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "5Hefn", + "x": 12, + "y": 192, + "name": "card1Btn", + "width": 141, + "height": 44, + "fill": "#8B5CF6", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VR6T8", + "name": "card1BtnText", + "fill": "#FFFFFF", + "content": "Generate Similar", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "q7PGB", + "name": "row2", + "width": "fill_container", + "gap": 16, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "5dW6M", + "name": "card3", + "clip": true, + "width": 165, + "height": 248, + "fill": "#00000000", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "uJwF2", + "x": 0, + "y": 0, + "name": "card3Top", + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "./images/generated-1772942500640.png", + "mode": "fill" + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "6OLca", + "x": 58.5, + "y": 100, + "name": "card3PlayBtn", + "width": 48, + "height": 48, + "fill": "#FFFFFFE6", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000020", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "0M694", + "name": "card3PlayIcon", + "width": 24, + "height": 24, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "frame", + "id": "eeJZ9", + "x": 103, + "y": 12, + "name": "card3Credits", + "height": 24, + "fill": "#00000099", + "cornerRadius": 14, + "gap": 4, + "padding": [ + 4, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "f0no7", + "name": "card3CreditsIcon", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "c0zKx", + "name": "card3CreditsText", + "fill": "#FFFFFF", + "content": "80", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "aMSxC", + "x": 12, + "y": 192, + "name": "card3Btn", + "width": 141, + "height": 44, + "fill": "#8B5CF6", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DEvyH", + "name": "card3BtnText", + "fill": "#FFFFFF", + "content": "Generate Similar", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ObYn3", + "name": "card4", + "clip": true, + "width": 165, + "height": 248, + "fill": "#00000000", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "FUx1Q", + "x": 0, + "y": 0, + "name": "card1Top", + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "./images/generated-1772942514836.png", + "mode": "fill" + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "zIoMr", + "x": 58.5, + "y": 100, + "name": "card1PlayBtn", + "width": 48, + "height": 48, + "fill": "#FFFFFFE6", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000020", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "tK64P", + "name": "card1PlayIcon", + "width": 24, + "height": 24, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "frame", + "id": "jFVjQ", + "x": 103, + "y": 12, + "name": "card1Credits", + "height": 24, + "fill": "#00000099", + "cornerRadius": 14, + "gap": 4, + "padding": [ + 4, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "fnoUt", + "name": "card1CreditsIcon", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "bkPE5", + "name": "card1CreditsText", + "fill": "#FFFFFF", + "content": "120", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dHeWm", + "x": 12, + "y": 192, + "name": "card1Btn", + "width": 141, + "height": 44, + "fill": "#8B5CF6", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HWGSC", + "name": "card1BtnText", + "fill": "#FFFFFF", + "content": "Generate Similar", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "1E5nB", + "name": "row3", + "width": "fill_container", + "gap": 16, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "vClnh", + "name": "card5", + "clip": true, + "width": 165, + "height": 248, + "fill": "#00000000", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "ZCkHy", + "x": 0, + "y": 0, + "name": "card1Top", + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "./images/generated-1772942527064.png", + "mode": "fill" + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "AckkY", + "x": 58.5, + "y": 100, + "name": "card1PlayBtn", + "width": 48, + "height": 48, + "fill": "#FFFFFFE6", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000020", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "f11MA", + "name": "card1PlayIcon", + "width": 24, + "height": 24, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "frame", + "id": "ewFxH", + "x": 103, + "y": 12, + "name": "card1Credits", + "height": 24, + "fill": "#00000099", + "cornerRadius": 14, + "gap": 4, + "padding": [ + 4, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "FL7O1", + "name": "card1CreditsIcon", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "Fz5Rc", + "name": "card1CreditsText", + "fill": "#FFFFFF", + "content": "35", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "aRh2M", + "x": 12, + "y": 192, + "name": "card1Btn", + "width": 141, + "height": 44, + "fill": "#8B5CF6", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BqonJ", + "name": "card1BtnText", + "fill": "#FFFFFF", + "content": "Generate Similar", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "9DhBm", + "name": "card6", + "clip": true, + "width": 165, + "height": 248, + "fill": "#00000000", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "WqBrj", + "x": 0, + "y": 0, + "name": "card1Top", + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "./images/generated-1772942542657.png", + "mode": "fill" + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "YgQME", + "x": 58.5, + "y": 100, + "name": "card1PlayBtn", + "width": 48, + "height": 48, + "fill": "#FFFFFFE6", + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000020", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "6F1rQ", + "name": "card1PlayIcon", + "width": 24, + "height": 24, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "frame", + "id": "Xpvb9", + "x": 103, + "y": 12, + "name": "card1Credits", + "height": 24, + "fill": "#00000099", + "cornerRadius": 14, + "gap": 4, + "padding": [ + 4, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "oHhXu", + "name": "card1CreditsIcon", + "width": 12, + "height": 12, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "m6O84", + "name": "card1CreditsText", + "fill": "#FFFFFF", + "content": "90", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "5qk7r", + "x": 12, + "y": 192, + "name": "card1Btn", + "width": 141, + "height": 44, + "fill": "#8B5CF6", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "bEBV9", + "name": "card1BtnText", + "fill": "#FFFFFF", + "content": "Generate Similar", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "2SAja", + "name": "bottomNav", + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000A", + "offset": { + "x": 0, + "y": -4 + }, + "blur": 12 + }, + "padding": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "NB1E6", + "name": "homeTab", + "width": 120, + "height": 41, + "fill": "#8B5CF6", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "i1MCD", + "name": "homeIcon", + "width": 18, + "height": 18, + "iconFontName": "house", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "UZWIi", + "name": "homeLabel", + "fill": "#FFFFFF", + "content": "HOME", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "5Bto3", + "name": "galleryTab", + "width": 120, + "height": 41, + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "nwW1m", + "name": "galleryIcon", + "width": 18, + "height": 18, + "iconFontName": "images", + "iconFontFamily": "lucide", + "fill": "#A1A1AA" + }, + { + "type": "text", + "id": "WhC7U", + "name": "galleryLabel", + "fill": "#A1A1AA", + "content": "GALLERY", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "nLNXH", + "name": "profileTab", + "width": 120, + "height": 41, + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "CovDA", + "name": "profileIcon", + "width": 18, + "height": 18, + "iconFontName": "user", + "iconFontFamily": "lucide", + "fill": "#A1A1AA" + }, + { + "type": "text", + "id": "pBcjv", + "name": "profileLabel", + "fill": "#A1A1AA", + "content": "PROFILE", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "hpwBg", + "x": 440, + "y": 0, + "name": "Gallery Screen", + "clip": true, + "width": 390, + "height": 844, + "fill": "#FAFAFA", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "EEASe", + "name": "topNav", + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "2ZFKo", + "name": "navTitle", + "fill": "#18181B", + "content": "Gallery", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 20, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "5yFeV", + "name": "creditsBadge", + "height": 32, + "fill": "#8B5CF620", + "cornerRadius": 14, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF620", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "gap": 6, + "padding": [ + 6, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Do6yd", + "name": "creditsIcon", + "width": 16, + "height": 16, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#8B5CF6" + }, + { + "type": "text", + "id": "4UpGk", + "name": "creditsText", + "fill": "#8B5CF6", + "content": "1,280", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "4iOnO", + "name": "contentArea", + "width": "fill_container", + "height": "fill_container", + "fill": "#FAFAFA", + "layout": "vertical", + "gap": 16, + "padding": [ + 0, + 20, + 24, + 20 + ], + "children": [ + { + "type": "frame", + "id": "g1ePM", + "name": "gridContainer", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "RDTYP", + "name": "row1", + "width": "fill_container", + "gap": 16, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "tc8IY", + "name": "card1", + "metadata": { + "type": "unsplash", + "username": "loganvoss", + "link": "https://unsplash.com/@loganvoss", + "author": "Logan Voss" + }, + "clip": true, + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1763929272543-0df093e4f659?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4NDM0ODN8MHwxfHJhbmRvbXx8fHx8fHx8fDE3NzI5NDgxODV8&ixlib=rb-4.1.0&q=80&w=1080", + "mode": "fill" + }, + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none" + }, + { + "type": "frame", + "id": "OOmgD", + "name": "card2", + "metadata": { + "type": "unsplash", + "username": "europeana", + "link": "https://unsplash.com/@europeana", + "author": "Europeana" + }, + "clip": true, + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1703592819695-ea63799b7315?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4NDM0ODN8MHwxfHJhbmRvbXx8fHx8fHx8fDE3NzI5NDgxODZ8&ixlib=rb-4.1.0&q=80&w=1080", + "mode": "fill" + }, + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none" + } + ] + }, + { + "type": "frame", + "id": "pfDBM", + "name": "row2", + "width": "fill_container", + "gap": 16, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "rtx0r", + "name": "card3", + "metadata": { + "type": "unsplash", + "username": "anyachernykh", + "link": "https://unsplash.com/@anyachernykh", + "author": "Anya Chernykh" + }, + "clip": true, + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1764787435677-1321e12559e3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4NDM0ODN8MHwxfHJhbmRvbXx8fHx8fHx8fDE3NzI5NDgxODd8&ixlib=rb-4.1.0&q=80&w=1080", + "mode": "fill" + }, + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none" + }, + { + "type": "frame", + "id": "5Frvd", + "name": "card4", + "metadata": { + "type": "unsplash", + "username": "ubeyonroad", + "link": "https://unsplash.com/@ubeyonroad", + "author": "ubeyonroad" + }, + "clip": true, + "width": 165, + "height": 248, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1759264244741-7175af0b7e75?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4NDM0ODN8MHwxfHJhbmRvbXx8fHx8fHx8fDE3NzI5NDgxODd8&ixlib=rb-4.1.0&q=80&w=1080", + "mode": "fill" + }, + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000D", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 12 + }, + "layout": "none" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "VI1Dg", + "name": "bottomNav", + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000A", + "offset": { + "x": 0, + "y": -4 + }, + "blur": 12 + }, + "padding": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "IqiVT", + "name": "homeTab", + "width": 120, + "height": 41, + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "fgjTN", + "width": 18, + "height": 18, + "iconFontName": "house", + "iconFontFamily": "lucide", + "fill": "#A1A1AA" + }, + { + "type": "text", + "id": "ykp6l", + "fill": "#A1A1AA", + "content": "HOME", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "FKfTO", + "name": "galleryTab", + "width": 120, + "height": 41, + "fill": "#8B5CF6", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "0vHHH", + "width": 18, + "height": 18, + "iconFontName": "images", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "wndfA", + "fill": "#FFFFFF", + "content": "GALLERY", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "4iQrj", + "name": "profileTab", + "width": 120, + "height": 41, + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "tsHTE", + "width": 18, + "height": 18, + "iconFontName": "user", + "iconFontFamily": "lucide", + "fill": "#A1A1AA" + }, + { + "type": "text", + "id": "GikYQ", + "fill": "#A1A1AA", + "content": "PROFILE", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "KXeow", + "x": 900, + "y": 0, + "name": "Profile Screen", + "clip": true, + "width": 390, + "height": 844, + "fill": "#FAFAFA", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "ZE4Rm", + "name": "topNav", + "enabled": false, + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XAJVJ", + "name": "navTitle", + "fill": "#18181B", + "content": "Profile", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 20, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "JkyAf", + "name": "creditsBadge", + "height": 32, + "fill": "#8B5CF620", + "cornerRadius": 14, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF620", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "gap": 6, + "padding": [ + 6, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "TEZ9I", + "width": 16, + "height": 16, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#8B5CF6" + }, + { + "type": "text", + "id": "sbbdw", + "fill": "#8B5CF6", + "content": "1,280", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "jCEqg", + "name": "contentArea", + "width": "fill_container", + "height": "fill_container", + "fill": "#FAFAFA", + "layout": "vertical", + "gap": 24, + "padding": [ + 0, + 20, + 24, + 20 + ], + "children": [ + { + "type": "frame", + "id": "UeFHl", + "name": "profileHeader", + "width": "fill_container", + "height": 220, + "layout": "vertical", + "gap": 12, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EUNTw", + "name": "avatar", + "width": 80, + "height": 80, + "fill": "#E4E4E7", + "cornerRadius": 40, + "stroke": { + "thickness": 2, + "fill": "#8B5CF640" + }, + "layout": "none", + "children": [ + { + "type": "icon_font", + "id": "iq2SL", + "x": 20, + "y": 20, + "name": "avatarIcon", + "width": 40, + "height": 40, + "iconFontName": "user", + "iconFontFamily": "lucide", + "fill": "#71717A" + } + ] + }, + { + "type": "text", + "id": "8S5MJ", + "name": "userName", + "fill": "#18181B", + "content": "Alex Johnson", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "dR1Sf", + "name": "uidBadge", + "fill": "#F4F4F5", + "cornerRadius": 16, + "stroke": { + "thickness": 1, + "fill": "#E4E4E7" + }, + "padding": [ + 6, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "gbir3", + "fill": "#71717A", + "content": "UID 84920133", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "hOevr", + "name": "balanceSection", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "mOfwH", + "name": "balanceLabel", + "fill": "#71717A", + "content": "AVAILABLE BALANCE", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "AiFQO", + "name": "balanceCard", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 16, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "padding": [ + 16, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MwNHN", + "fill": "#8B5CF6", + "content": "1,280", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "DuIYs", + "name": "rechargeBtn", + "fill": "#8B5CF6", + "cornerRadius": 12, + "gap": 6, + "padding": [ + 8, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VQdI5", + "fill": "#FFFFFF", + "content": "Recharge", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "h6zll", + "name": "menuSection", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "SLZ1a", + "name": "menuItem1", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 14, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "7LawH", + "fill": "#18181B", + "content": "Credit Store", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "sSk3F", + "width": 20, + "height": 20, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "#A1A1AA" + } + ] + }, + { + "type": "frame", + "id": "FygC4", + "name": "menuItem2", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 14, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "coRfB", + "fill": "#18181B", + "content": "Settings", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "F0GlP", + "width": 20, + "height": 20, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "#A1A1AA" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "Ava7f", + "name": "bottomNav", + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0000000A", + "offset": { + "x": 0, + "y": -4 + }, + "blur": 12 + }, + "padding": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "gIvG7", + "width": 120, + "height": 41, + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "XDqa1", + "width": 18, + "height": 18, + "iconFontName": "house", + "iconFontFamily": "lucide", + "fill": "#A1A1AA" + }, + { + "type": "text", + "id": "Z7oTj", + "fill": "#A1A1AA", + "content": "HOME", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "7vclf", + "width": 120, + "height": 41, + "fill": "#00000000", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "U67U7", + "width": 18, + "height": 18, + "iconFontName": "images", + "iconFontFamily": "lucide", + "fill": "#A1A1AA" + }, + { + "type": "text", + "id": "udpZp", + "fill": "#A1A1AA", + "content": "GALLERY", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "1Ynsh", + "width": 120, + "height": 41, + "fill": "#8B5CF6", + "cornerRadius": 26, + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "qIEXP", + "width": 18, + "height": 18, + "iconFontName": "user", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "w9SVq", + "fill": "#FFFFFF", + "content": "PROFILE", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "tPjdN", + "x": 1340, + "y": 0, + "name": "Recharge Screen", + "clip": true, + "width": 390, + "height": 844, + "fill": "#FAFAFA", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "6K6zM", + "name": "topNav", + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "o6xEI", + "name": "backBtn", + "width": 40, + "height": 40, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "jO9wi", + "width": 24, + "height": 24, + "iconFontName": "arrow-left", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "text", + "id": "hyvVe", + "name": "navTitle", + "fill": "#18181B", + "content": "Recharge", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 20, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "Lt3TV", + "name": "navSpacer", + "width": 40, + "height": 40 + } + ] + }, + { + "type": "frame", + "id": "tRdXp", + "name": "tiersSection", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": [ + 24, + 20 + ], + "children": [ + { + "type": "frame", + "id": "B0vCq", + "name": "creditsSection", + "width": "fill_container", + "fill": "#8B5CF6", + "cornerRadius": 16, + "stroke": { + "thickness": 1, + "fill": "#8B5CF680" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 20 + ], + "children": [ + { + "type": "text", + "id": "xnizd", + "name": "creditsLabel", + "fill": "#FFFFFFCC", + "content": "Current Credits", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "Y0Od9", + "name": "creditsDisplay", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "ZFHM9", + "width": 28, + "height": 28, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "mzNty", + "fill": "#FFFFFF", + "content": "1,280", + "fontFamily": "Inter", + "fontSize": 32, + "fontWeight": "700" + } + ] + } + ] + }, + { + "type": "text", + "id": "0qaAg", + "name": "tiersLabel", + "fill": "#18181B", + "content": "Select Tier", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "DC6NS", + "name": "tierNormal", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 16, + "stroke": { + "thickness": 1, + "fill": "#E4E4E7" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 16, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Dx03C", + "name": "tierContent", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "YZXkU", + "fill": "#18181B", + "content": "100 Credits", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Xvh9R", + "fill": "#71717A", + "content": "¥6", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "6Uie0", + "name": "buyBtn", + "fill": "#8B5CF6", + "cornerRadius": 12, + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FmjH7", + "fill": "#FFFFFF", + "content": "Buy", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "YRaOG", + "name": "tierRecommended", + "width": 350, + "height": 80, + "fill": "#00000000", + "stroke": { + "thickness": 0, + "fill": "#00000000" + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "osaPn", + "x": 0, + "y": 0, + "name": "cardContent", + "width": 350, + "height": 80, + "fill": "#FFFFFF", + "cornerRadius": 16, + "stroke": { + "thickness": 1, + "fill": "#E4E4E7" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 16, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "vrUpL", + "name": "tierContent", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "l4SNq", + "name": "creditsRec", + "fill": "#18181B", + "content": "500 Credits", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Rg4EJ", + "fill": "#71717A", + "content": "¥25 Save ¥5", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "szJrs", + "name": "btnWrapper", + "layout": "vertical", + "padding": [ + 15, + 0, + 0, + 0 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "TOcfz", + "name": "buyBtn", + "fill": "#8B5CF6", + "cornerRadius": 12, + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YB4Yj", + "fill": "#FFFFFF", + "content": "Buy", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "s95nA", + "x": 254, + "y": 1.7763568394002505e-15, + "name": "badge", + "rotation": -3.975693351829396e-16, + "fill": "#8B5CF620", + "cornerRadius": [ + 0, + 16, + 0, + 16 + ], + "padding": [ + 4, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nB0Dm", + "fill": "#8B5CF6", + "content": "Recommended", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "QlNz6", + "name": "tierPopular", + "width": 350, + "height": 80, + "fill": "#00000000", + "stroke": { + "thickness": 0, + "fill": "#00000000" + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "IE6aU", + "x": 0, + "y": 0, + "name": "cardContent", + "width": 350, + "height": 80, + "fill": "#FFFFFF", + "cornerRadius": 16, + "stroke": { + "thickness": 1, + "fill": "#E4E4E7" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "padding": [ + 16, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "wJBVM", + "name": "tierContent", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "bDAlg", + "name": "creditsPop", + "fill": "#18181B", + "content": "1,000 Credits", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "BxrWP", + "fill": "#71717A", + "content": "¥45 Save ¥15", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Pi42v", + "name": "btnWrapper", + "layout": "vertical", + "padding": [ + 15, + 0, + 0, + 0 + ], + "justifyContent": "center", + "alignItems": "end", + "children": [ + { + "type": "frame", + "id": "Yxmfw", + "name": "buyBtn", + "fill": "#8B5CF6", + "cornerRadius": 12, + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JU1WR", + "fill": "#FFFFFF", + "content": "Buy", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "yHXJl", + "x": 263, + "y": 0, + "name": "badge", + "width": "fit_content(87)", + "height": "fit_content(21)", + "fill": "#F59E0B20", + "cornerRadius": [ + 0, + 16, + 0, + 16 + ], + "layout": "none", + "children": [ + { + "type": "text", + "id": "QIebF", + "x": 8, + "y": 4, + "fill": "#F59E0B", + "content": "Most Popular", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "mmLB5", + "x": 1780, + "y": 0, + "name": "Generate Video Screen", + "clip": true, + "width": 390, + "height": 844, + "fill": "#FAFAFA", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "p4l77", + "name": "topNav", + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "q4LqH", + "name": "backBtn", + "width": 40, + "height": 40, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "uQJ0N", + "width": 24, + "height": 24, + "iconFontName": "arrow-left", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "text", + "id": "2wKUo", + "name": "navTitle", + "fill": "#18181B", + "content": "Generate Video", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 20, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "7F4MA", + "name": "navSpacer", + "width": 40, + "height": 40 + } + ] + }, + { + "type": "frame", + "id": "BNNL5", + "name": "contentArea", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 24, + "padding": [ + 24, + 20 + ], + "children": [ + { + "type": "frame", + "id": "ZbYgn", + "name": "creditsCard", + "width": "fill_container", + "fill": "#8B5CF6", + "cornerRadius": 16, + "stroke": { + "thickness": 1, + "fill": "#8B5CF680" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 20 + ], + "children": [ + { + "type": "text", + "id": "lSWYi", + "fill": "#FFFFFFCC", + "content": "Available Credits", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "Vx9RC", + "name": "creditsDisplay", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "UkBge", + "width": 28, + "height": 28, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "AFJOp", + "fill": "#FFFFFF", + "content": "1,280", + "fontFamily": "Inter", + "fontSize": 32, + "fontWeight": "700" + } + ] + } + ] + }, + { + "type": "frame", + "id": "vA8QJ", + "name": "uploadArea", + "width": "fill_container", + "height": 280, + "fill": "#F4F4F5", + "cornerRadius": 16, + "stroke": { + "thickness": 2, + "fill": "#E4E4E7" + }, + "layout": "vertical", + "gap": 12, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "1VkSk", + "width": 48, + "height": 48, + "iconFontName": "image-plus", + "iconFontFamily": "lucide", + "fill": "#A1A1AA" + }, + { + "type": "text", + "id": "flPeY", + "fill": "#71717A", + "textGrowth": "fixed-width", + "width": 280, + "content": "Please upload an image as the base for generation", + "textAlign": "center", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "yunXY", + "name": "generateBtn", + "width": "fill_container", + "height": 56, + "fill": "#8B5CF6", + "cornerRadius": 16, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "gap": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "94USx", + "fill": "#FFFFFF", + "content": "Generate Video", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "RGKvY", + "name": "creditsCost", + "fill": "#FFFFFF30", + "cornerRadius": 8, + "gap": 4, + "padding": [ + 4, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "GKJhs", + "width": 16, + "height": 16, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "XSMlK", + "fill": "#FFFFFF", + "content": "50", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "qGs6n", + "x": 2220, + "y": 0, + "name": "Generate Video Progress Screen", + "clip": true, + "width": 390, + "height": 844, + "fill": "#FAFAFA", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Qi840", + "name": "topNav", + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "GrcXB", + "name": "backBtn", + "width": 40, + "height": 40, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "xapUf", + "width": 24, + "height": 24, + "iconFontName": "arrow-left", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "text", + "id": "jj1wM", + "name": "navTitle", + "fill": "#18181B", + "content": "Generating", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 20, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "7yHyL", + "name": "navSpacer", + "width": 40, + "height": 40 + } + ] + }, + { + "type": "frame", + "id": "Gyt2o", + "name": "contentArea", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 24, + "padding": [ + 24, + 20 + ], + "children": [ + { + "type": "frame", + "id": "rSN3T", + "name": "videoDisplay", + "clip": true, + "width": "fill_container", + "height": 360, + "fill": "#18181B", + "cornerRadius": 16, + "layout": "vertical", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "RKplP", + "width": 64, + "height": 64, + "iconFontName": "film", + "iconFontFamily": "lucide", + "fill": "#71717A" + }, + { + "type": "text", + "id": "Dx0c0", + "fill": "#71717A", + "content": "Video Preview", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "YqM56", + "name": "hintText", + "fill": "#71717A", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Video generation may take some time. Please wait patiently.", + "textAlign": "center", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "nQMvt", + "name": "progressSection", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "Eghqc", + "name": "progressLabel", + "fill": "#18181B", + "content": "Generating... 45%", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "TYHdm", + "name": "progressBarBg", + "width": "fill_container", + "height": 8, + "fill": "#E4E4E7", + "cornerRadius": 4, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "4P4tn", + "x": 0, + "y": 0, + "name": "progressBarFill", + "width": 158, + "height": 8, + "fill": "#8B5CF6", + "cornerRadius": 4, + "layout": "none" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "cFA4T", + "x": 2660, + "y": 0, + "name": "Video Generation Result Screen", + "clip": true, + "width": 390, + "height": 844, + "fill": "#FAFAFA", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "5L8A2", + "name": "topNav", + "width": "fill_container", + "height": 56, + "fill": "#FFFFFF", + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 8 + }, + "padding": [ + 0, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Rw61A", + "name": "backBtn", + "width": 40, + "height": 40, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "JTbKe", + "width": 24, + "height": 24, + "iconFontName": "arrow-left", + "iconFontFamily": "lucide", + "fill": "#18181B" + } + ] + }, + { + "type": "text", + "id": "pAUzD", + "name": "navTitle", + "fill": "#18181B", + "content": "Video Ready", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 20, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "OKMsh", + "name": "navSpacer", + "width": 40, + "height": 40 + } + ] + }, + { + "type": "frame", + "id": "rDzoV", + "name": "contentArea", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 24, + "padding": [ + 24, + 20 + ], + "children": [ + { + "type": "frame", + "id": "GTuTn", + "name": "videoDisplay", + "clip": true, + "width": "fill_container", + "height": 360, + "fill": "#18181B", + "cornerRadius": 16, + "layout": "vertical", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "a3uQs", + "width": 72, + "height": 72, + "iconFontName": "play", + "iconFontFamily": "lucide", + "fill": "#FFFFFF80" + }, + { + "type": "text", + "id": "SuXL2", + "fill": "#FFFFFF99", + "content": "Your video is ready", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "jCww0", + "name": "buttonRow", + "width": "fill_container", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "En8rQ", + "name": "downloadBtn", + "width": "fill_container", + "height": 52, + "fill": "#8B5CF6", + "cornerRadius": 14, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#8B5CF640", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "gap": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "qSzz1", + "width": 20, + "height": 20, + "iconFontName": "download", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + }, + { + "type": "text", + "id": "KDr4j", + "fill": "#FFFFFF", + "content": "Download", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "FDy9O", + "name": "shareBtn", + "width": "fill_container", + "height": 52, + "fill": "#FFFFFF", + "cornerRadius": 14, + "stroke": { + "thickness": 1, + "fill": "#E4E4E7" + }, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000008", + "offset": { + "x": 0, + "y": 2 + }, + "blur": 6 + }, + "gap": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "eYjnB", + "width": 20, + "height": 20, + "iconFontName": "share-2", + "iconFontFamily": "lucide", + "fill": "#8B5CF6" + }, + { + "type": "text", + "id": "8hK5k", + "fill": "#18181B", + "content": "Share", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/api_flow_summary.md b/docs/api_flow_summary.md new file mode 100644 index 0000000..68b9322 --- /dev/null +++ b/docs/api_flow_summary.md @@ -0,0 +1,165 @@ +# petsHeroAI 接口调用流程说明 + +## 一、整体流程 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 客户端请求流程 │ +└─────────────────────────────────────────────────────────────────┘ + + 业务参数(原始字段) + │ + ▼ + ┌─────────────┐ + │ 字段名映射 │ body / params / headers 中的原始字段 → V2 字段 + └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ V2 包装 │ 将 body 包装为 arsenal/vault/tome/codex/grimoire/sanctum 结构 + └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ JSON 序列化 │ + └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ AES+Base64 │ AES-128-ECB, PKCS5Padding 加密 + └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ 构造代理请求 │ 填入 hero_class, pet_species, power_level 等参数 + └─────────────┘ + │ + ▼ + POST {baseUrl}/quester/defender/summoner +``` + +## 二、响应处理流程 + +``` + POST 代理入口响应 + │ + ▼ + ┌─────────────┐ + │ 提取密文 │ 从响应中获取加密字段 + └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ Base64 解码 │ + └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ AES 解密 │ AES-128-ECB 解密 + └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ JSON 解析 │ + └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ 字段逆映射 │ V2 字段 → 原始字段 (便于业务使用) + └─────────────┘ + │ + ▼ + 业务数据 (code/msg/data) +``` + +## 三、接口分类与调用顺序 + +### 3.1 登录与用户 + +| 顺序 | 接口 | 方法 | 说明 | +|------|------|------|------| +| 1 | `/v1/user/fast_login` | POST | 设备快速登录,获取 userToken | +| 2 | `/v1/user/common_info` | GET | 获取用户通用信息(含积分、头像等) | +| 3 | `/v1/user/account` | GET | 获取用户账户信息 | +| 4 | `/v1/user/referrer` | POST | 归因上报 | +| 5 | `/v1/user/delete` | GET | 注销账户 | + +### 3.2 支付 + +| 顺序 | 接口 | 方法 | 说明 | +|------|------|------|------| +| 1 | `/v1/payment/getGooglePayActivities` | GET | 获取 Google 商品列表 | +| 2 | `/v1/payment/getApplePayActivities` | GET | 获取 Apple 商品列表 | +| 3 | `/v1/payment/createPayment` | POST | 创建支付订单 | +| 4 | `/v1/payment/googlepay` | POST | Google 支付结果回调 | +| 5 | `/v1/payment/applepay` | POST | Apple 支付结果回调 | +| 6 | `/v1/payment/getPaymentDetailList` | GET | 获取支付订单列表 | + +### 3.3 图片生成 + +| 顺序 | 接口 | 方法 | 说明 | +|------|------|------|------| +| 1 | `/v1/image/prompt/recomends` | GET | 获取推荐提示词 | +| 2 | `/v1/image/txt2img_tags` | GET | 获取文生图标签 | +| 3 | `/v1/image/txt2img_prompts` | POST | 获取文生图提示词模板 | +| 4 | `/v1/image/txt2img_create` | POST | 创建文生图任务 | +| 5 | `/v1/image/progress` | GET | 查询图片生成进度 | + +### 3.4 图转视频 + +| 顺序 | 接口 | 方法 | 说明 | +|------|------|------|------| +| 1 | `/v1/image/img2Video_pose_template` | GET | 获取图转视频姿态模板 | +| 2 | `/v1/image/img2video_pose_task` | POST | 创建图转视频姿态任务 | +| 3 | `/v1/image/progress` | GET | 查询任务进度 | + +### 3.5 换衣 / 换脸 + +| 顺序 | 接口 | 方法 | 说明 | +|------|------|------|------| +| 1 | `/v1/image/clothes_template` | GET | 获取换衣模板 | +| 2 | `/v1/image/clothes_swap_ex` | POST | 创建换衣任务 | +| 3 | `/v1/image/faceswap_task` | POST | 创建换脸任务 | +| 4 | `/v1/image/video_facewap_task` | POST | 创建视频换脸任务 | + +### 3.6 其他 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/v1/image/category-list` | GET | 获取分类列表 | +| `/v1/log/appevent` | POST | App 事件打点上报 | +| `/v1/image/getCreditsPageInfo` | GET | 获取积分页面信息 | +| `/v1/log/uploadUrl` | POST | 获取预签名上传 URL | + +## 四、通用请求头 + +登录后所有请求需携带: + +| 原始字段 | V2 字段 | 说明 | +|----------|---------|------| +| pkg | portal | 应用包名,必填,如 `com.petsheroai.app` | +| User_token | knight | 用户登录 token | + +## 五、通用响应结构 + +```json +{ + "code": 0, // helm,0=成功 + "msg": "", // rampart,消息 + "data": {} // sidekick,业务数据 +} +``` + +## 六、错误码 + +| code | 说明 | +|------|------| +| 0 | 成功 | +| -1 | 系统错误 | +| -2 | 未登录 | +| -3 | 无权限 | +| -4 | 请求过于频繁 | +| -5 | 参数错误 | +| 1001 | 积分不足 | +| 1002 | 免费次数已用完 | +| 1003 | 免费次数和积分均已用完 | diff --git a/docs/petsHeroAI_client_guide.md b/docs/petsHeroAI_client_guide.md new file mode 100644 index 0000000..6091a40 --- /dev/null +++ b/docs/petsHeroAI_client_guide.md @@ -0,0 +1,2301 @@ +# petsHeroAI V2 客户端接入指引 + +## 通用信息 + +| 项目 | 值 | +|------|----| +| 加密方式 | AES-128-ECB, PKCS5Padding, Base64 | +| AES Key | `liyP4LkMfP68XvCt` | +| 预发域名 | `pre-ai.petsheroai.xyz` | +| 生产域名 | `ai.petsheroai.xyz` | +| 代理入口 | `POST {baseUrl}/quester/defender/summoner` | +| 包名 | `com.petsheroai.app` | + +## 加密流程 + +``` + 构造业务参数 + | + v + 字段名映射 + (body + params + headers) + | + v + V2包装 + (body only) + | + v + JSON序列化 + | + v + AES + Base64 + | + v + 填入代理请求 b/p 参数 + | + v + POST 代理入口 +``` + +## 代理请求结构 + +所有接口统一 POST 到代理入口,请求体结构如下: + +```json +{ + "hero_class": "petsHeroAI", // appId 明文 + "pet_species": "", // AES(原始path) → Base64 + "power_level": "", // AES("POST"或"GET") → Base64 + "quest_rank": "", // AES(映射后的Header字段名JSON) → Base64 + "battle_score": "", // AES(映射后的参数名JSON) → Base64 + "loyalty_index": "", // AES(V2包装后的业务数据) → Base64 + "billing_addr": "<每次请求随机 Base64>", // 噪音 + "utm_term": "<每次请求随机 Base64>", // 噪音 + "cluster_id": "<每次请求随机 Base64>", // 噪音 + "lsn_value": "<每次请求随机 Base64>", // 噪音 + "accuracy_val": "<每次请求随机 Base64>", // 噪音 + "dir_path": "<每次请求随机 Base64>" // 噪音 +} +``` + +> 注意: 即使原始接口为 GET(无请求体),走代理后也统一使用 POST,b 参数仍需传入 V2 包装后的空业务体。 +> 噪音 key 的值为每次请求随机生成的 Base64 字符串(模拟 AES 密文外观),服务端会忽略。 + +## V2 包装结构 + +业务数据经字段映射后,按以下结构包装: + +```json +{ + "arsenal": 4, // 固定值 + "vault": { + "tome": { + "codex": { + "grimoire": { + "sanctum": { /* 映射后的业务字段 */ } + } + } + } + }, + "roar": "<每次请求随机生成8位字母数字组合>", + "clash": "<每次请求随机生成8位字母数字组合>", + "thunder": "<每次请求随机生成8位字母数字组合>", + "rumble": "<每次请求随机生成8位字母数字组合>", + "howl": "<每次请求随机生成8位字母数字组合>", + "growl": "<每次请求随机生成8位字母数字组合>" +} +``` + +## 登录态接口 + +> 登录后,所有请求 Header 需带 `knight` + +### 1. 获取推荐提示词 + +`GET /v1/image/prompt/recomends` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "crest": "", // ch — 渠道号 + "asset": "" // userId — 用户ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": [ // data (array) 推荐提示词列表 + { + "constrain": "", // tag (string) 标签 + "ledger": "" // prompt (string) 文本提示词 + } + ] +} +``` + +### 2. 设备登录(快速登录) + +`POST /v1/user/fast_login` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "crest": "", // ch — 渠道号 + "portal": "", // pkg — 应用包名(必填) + "accolade": "" // type — 类型 +} +``` + +请求体 (字段映射后): + +```json +{ + "digest": "", // referer (string) 归因信息,如 utm_source=googleplay + "resolution": "", // sign (string) 签名,MD5(deviceId)大写32位 + "origin": "" // deviceId (string) 设备ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "surge": "", // extConfig (string) 客户端界面配置下发 + "accolade": "", // type (string) 类型 + "evolve": {}, // appFbConfig (object) 应用FB配置 + "retrospect": "", // usign (string) 用户签名 + "navigate": "", // countryCode (string) 国家代码 + "conquer": "", // creditsRecordUrl (string) 积分记录URL + "concession": "", // tgId (string) Telegram ID + "reveal": 0, // credit (int) 积分余额 + "defer": "", // tgName (string) Telegram 用户名 + "galaxy": "", // email (string) 邮箱 + "nexus": {}, // ext (object) 扩展信息 + "upgrade": false, // forcePayCenter (boolean) 是否强制跳转支付中心 + "regulate": {}, // t2IConfig (object) T2I配置 + "pursue": {}, // h5UrlConfig (object) H5 URL配置 + "switch": "", // payCenterUrl (string) 支付中心URL + "vow": 0, // freeBlurTimes (int) 免费去模糊次数 + "realm": "", // avatar (string) 头像URL + "terminal": "", // userName (string) 用户名 + "equip": false, // firstRegister (boolean) 是否首次注册 + "asset": "", // userId (string) 用户ID + "generate": false, // isVip (boolean) 是否VIP + "rally": [], // tags (array) 用户标签 + "decree": 0, // freeTimes (int) 免费次数 + "reevaluate": "", // userToken (string) 用户登录token + "tokenize": 0, // subScribeValidTime (int) 订阅有效时间戳 + "line": 0 // status (int) 用户状态 + } +} +``` + +### 3. 注销账户 + +`GET /v1/user/delete` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "" // userId — 用户ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "traverse": "", // hint (string) + "accolade": "" // type (string) + } +} +``` + +### 4. 获取换衣模板 + +`GET /v1/image/clothes_template` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "crest": "", // ch — 渠道号 + "asset": "", // userId — 用户ID + "insignia": "1" // categoryId — 分类ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": [ // data (array) 换衣模板列表 + { + "congregation": "", // templateName (string) 模板名称 + "greaves": 0, // credits (int) 积分 + "realm": "", // avatar (string) 头像 + "accolade": 0, // type (int) 类型 + "rebalance": "", // user (string) 用户 + "inject": "" // templateUrl (string) 模板URL + } + ] +} +``` + +### 5. 获取用户通用信息 + +`GET /v1/user/common_info` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "shield": "", // client — 客户端标识 + "asset": "", // userId — 用户ID + "crest": "", // ch — 渠道号 + "item": "", // inviteBy — 邀请人 + "origin": "", // deviceId — 设备ID + "gauntlet": "", // clientId — 客户端ID + "portal": "" // pkg — 应用包名(必填) +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "surge": "", // extConfig (string) 客户端界面配置下发 + "accolade": "", // type (string) 类型 + "evolve": {}, // appFbConfig (object) 应用FB配置 + "retrospect": "", // usign (string) 用户签名 + "navigate": "", // countryCode (string) 国家代码 + "conquer": "", // creditsRecordUrl (string) 积分记录URL + "concession": "", // tgId (string) Telegram ID + "reveal": 0, // credit (int) 积分余额 + "defer": "", // tgName (string) Telegram 用户名 + "galaxy": "", // email (string) 邮箱 + "nexus": {}, // ext (object) 扩展信息 + "upgrade": false, // forcePayCenter (boolean) 是否强制跳转支付中心 + "regulate": {}, // t2IConfig (object) T2I配置 + "pursue": {}, // h5UrlConfig (object) H5 URL配置 + "switch": "", // payCenterUrl (string) 支付中心URL + "vow": 0, // freeBlurTimes (int) 免费去模糊次数 + "realm": "", // avatar (string) 头像URL + "terminal": "", // userName (string) 用户名 + "equip": false, // firstRegister (boolean) 是否首次注册 + "asset": "", // userId (string) 用户ID + "generate": false, // isVip (boolean) 是否VIP + "rally": [], // tags (array) 用户标签 + "decree": 0, // freeTimes (int) 免费次数 + "reevaluate": "", // userToken (string) 用户登录token + "tokenize": 0, // subScribeValidTime (int) 订阅有效时间戳 + "line": 0 // status (int) 用户状态 + } +} +``` + +### 6. 归因上报 + +`POST /v1/user/referrer` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "", // userId — 用户ID + "accolade": "", // type — 类型 + "portal": "" // pkg — 应用包名(必填) +} +``` + +请求体 (字段映射后): + +```json +{ + "digest": "", // referer (string) 归因信息,如 utm_source=googleplay + "origin": "" // deviceId (string) 设备ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "traverse": "", // hint (string) + "accolade": "" // type (string) + } +} +``` + +### 7. 获取用户账户信息 + +`GET /v1/user/account` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "" // userId — 用户ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "surge": "", // extConfig (string) 客户端界面配置下发 + "accolade": "", // type (string) 类型 + "evolve": {}, // appFbConfig (object) 应用FB配置 + "retrospect": "", // usign (string) 用户签名 + "navigate": "", // countryCode (string) 国家代码 + "conquer": "", // creditsRecordUrl (string) 积分记录URL + "concession": "", // tgId (string) Telegram ID + "reveal": 0, // credit (int) 积分余额 + "defer": "", // tgName (string) Telegram 用户名 + "galaxy": "", // email (string) 邮箱 + "nexus": {}, // ext (object) 扩展信息 + "upgrade": false, // forcePayCenter (boolean) 是否强制跳转支付中心 + "regulate": {}, // t2IConfig (object) T2I配置 + "pursue": {}, // h5UrlConfig (object) H5 URL配置 + "switch": "", // payCenterUrl (string) 支付中心URL + "vow": 0, // freeBlurTimes (int) 免费去模糊次数 + "realm": "", // avatar (string) 头像URL + "terminal": "", // userName (string) 用户名 + "equip": false, // firstRegister (boolean) 是否首次注册 + "asset": "", // userId (string) 用户ID + "generate": false, // isVip (boolean) 是否VIP + "rally": [], // tags (array) 用户标签 + "decree": 0, // freeTimes (int) 免费次数 + "reevaluate": "", // userToken (string) 用户登录token + "tokenize": 0, // subScribeValidTime (int) 订阅有效时间戳 + "line": 0 // status (int) 用户状态 + } +} +``` + +### 8. 获取Google商品列表 + +`GET /v1/payment/getGooglePayActivities` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "shield": "", // client — 客户端 + "vambrace": "", // country — 国家代码 + "portal": "" // pkg — 应用包名(必填) +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "cleanse": [ // subscriptionActivitys (array) 订阅商品列表 + { + "extrapolate": "", // note (string) 备注 + "helm": "", // code (string) 产品代码 + "forge": 0, // bonus (int) 赠送积分 + "guardian": "", // actualAmount (string) 实际金额 + "lead": "", // discountOff (string) 折扣 + "glossary": "", // title (string) 标题 + "curriculum": "", // originAmount (string) 原价 + "warrior": "", // activityId (string) 活动ID + "distribute": "", // subscriptionPeriod (string) 订阅周期 + "greaves": 0, // credits (int) 积分数 + "shield": "", // client (string) 客户端 + "species": 0, // days (int) 天数 + "familiar": "", // currency (string) 货币 + "subtract": 0 // productType (int) 产品类型 + } + ], + "summon": [ // activitys (array) 商品列表 + { + "extrapolate": "", // note (string) 备注 + "helm": "", // code (string) 产品代码 + "forge": 0, // bonus (int) 赠送积分 + "guardian": "", // actualAmount (string) 实际金额 + "lead": "", // discountOff (string) 折扣 + "glossary": "", // title (string) 标题 + "curriculum": "", // originAmount (string) 原价 + "warrior": "", // activityId (string) 活动ID + "distribute": "", // subscriptionPeriod (string) 订阅周期 + "greaves": 0, // credits (int) 积分数 + "shield": "", // client (string) 客户端 + "species": 0, // days (int) 天数 + "familiar": "", // currency (string) 货币 + "subtract": 0 // productType (int) 产品类型 + } + ] + } +} +``` + +### 9. 获取Apple商品列表 + +`GET /v1/payment/getApplePayActivities` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "shield": "", // client — 客户端 + "vambrace": "", // country — 国家代码 + "portal": "" // pkg — 应用包名(必填) +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "cleanse": [ // subscriptionActivitys (array) 订阅商品列表 + { + "extrapolate": "", // note (string) 备注 + "helm": "", // code (string) 产品代码 + "forge": 0, // bonus (int) 赠送积分 + "guardian": "", // actualAmount (string) 实际金额 + "lead": "", // discountOff (string) 折扣 + "glossary": "", // title (string) 标题 + "curriculum": "", // originAmount (string) 原价 + "warrior": "", // activityId (string) 活动ID + "distribute": "", // subscriptionPeriod (string) 订阅周期 + "greaves": 0, // credits (int) 积分数 + "shield": "", // client (string) 客户端 + "species": 0, // days (int) 天数 + "familiar": "", // currency (string) 货币 + "subtract": 0 // productType (int) 产品类型 + } + ], + "summon": [ // activitys (array) 商品列表 + { + "extrapolate": "", // note (string) 备注 + "helm": "", // code (string) 产品代码 + "forge": 0, // bonus (int) 赠送积分 + "guardian": "", // actualAmount (string) 实际金额 + "lead": "", // discountOff (string) 折扣 + "glossary": "", // title (string) 标题 + "curriculum": "", // originAmount (string) 原价 + "warrior": "", // activityId (string) 活动ID + "distribute": "", // subscriptionPeriod (string) 订阅周期 + "greaves": 0, // credits (int) 积分数 + "shield": "", // client (string) 客户端 + "species": 0, // days (int) 天数 + "familiar": "", // currency (string) 货币 + "subtract": 0 // productType (int) 产品类型 + } + ] + } +} +``` + +### 10. 创建支付订单 + +`POST /v1/payment/createPayment` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "" // userId — 用户ID +} +``` + +请求体 (字段映射后): + +```json +{ + "sentinel": "", // app (string) 应用标识(必填) + "bazaar": "", // lastName (string) 姓 + "vambrace": "", // country (string) 国家代码 + "dominion": "", // expireMonth (string) 过期月 + "captain": "", // accountName (string) 账户名 + "frame": "", // userInfoType (string) 用户信息类型 + "legend": false, // automaticRenewal (boolean) 是否自动续费 + "lineage": "", // channel (string) 渠道 + "companion": "", // cvcCode (string) CVC安全码 + "armor": "", // channelType (string) 渠道类型 + "asset": "", // userId (string) 用户ID + "warrior": "", // activityId (string) 活动模板ID + "appointment": "", // firstName (string) 名 + "ceremony": "", // subPaymentMethod (string) 子支付方式 + "block": "", // phone (string) 电话 + "insurer": "", // tgOrderId (string) Telegram订单ID + "concession": "", // tgId (string) Telegram ID + "brigade": "", // name (string) 姓名 + "resource": "", // paymentMethod (string) 支付方式,如 GOOGLEPAY/APPLEPAY + "sanctuary": "", // expireYear (string) 过期年 + "artifact": "", // card (string) 卡号 + "line": "" // status (string) 状态 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "scout": {}, // features (object) 功能特性 + "handshake": [ // payTypeList (array) 支付类型列表 + { + "mold": "", // showName (string) 展示名称 + "brigade": "", // name (string) 支付方式 + "greylist": "", // icon (string) 展示图标 + "unzip": "", // paylink (string) 支付链接 + "federation": 0, // id (long) ID + "invent": 0 // sort (int) 排序 + } + ], + "merge": "", // issuerUrl (string) 发卡方URL + "protect": "", // cryptoInvoiceUrl (string) 加密发票URL + "convert": "", // payUrl (string) 支付URL + "federation": "", // id (string) 支付ID/订单ID + "destroy": {}, // payStatus (object) 支付状态 + "transplant": 0 // openType (int) URL打开方式,0=内部/1=外部浏览器 + } +} +``` + +### 11. Google支付结果回调 + +`POST /v1/payment/googlepay` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "" // userId — 用户ID +} +``` + +请求体 (字段映射后): + +```json +{ + "sample": "", // signature (string) 签名 + "merchant": "", // purchaseData (string) Google支付凭据JSON + "federation": "", // id (string) 支付ID/订单ID + "asset": "" // userId (string) 用户ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "warrior": "", // activityId (string) 活动模板ID + "disambiguate": "", // redirectUrl (string) 重定向URL + "awaken": false, // addCredits (boolean) 是否添加积分 + "visor": "", // count (string) 数量 + "familiar": "", // currency (string) 货币 + "federation": "", // id (string) 支付ID/订单ID + "line": {} // status (object) 状态 + } +} +``` + +### 12. Apple支付结果回调 + +`POST /v1/payment/applepay` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "portal": "" // pkg — 应用包名(必填) +} +``` + +请求体 (字段映射后): + +```json +{ + "attendee": "", // receipt (string) Apple支付凭据 + "federation": "", // id (string) 支付ID/订单ID + "asset": "", // userId (string) 用户ID + "chronicle": "" // transactionId (string) 交易ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "warrior": "", // activityId (string) 活动模板ID + "disambiguate": "", // redirectUrl (string) 重定向URL + "awaken": false, // addCredits (boolean) 是否添加积分 + "visor": "", // count (string) 数量 + "familiar": "", // currency (string) 货币 + "federation": "", // id (string) 支付ID/订单ID + "line": {} // status (object) 状态 + } +} +``` + +### 13. 获取支付订单列表 + +`GET /v1/payment/getPaymentDetailList` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "", // userId — 用户ID + "resource": "", // paymentMethod — 支付方式,如 GOOGLEPAY/APPLEPAY + "specimen": "" // filterStatus — 筛选状态 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": [] // data (array) 订单列表,每项含id/status/currency/count/activityId/addCredits/redirectUrl +} +``` + +### 14. 创建文生图任务 + +`POST /v1/image/txt2img_create` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "crest": "", // ch — 渠道号 + "asset": "" // userId — 用户ID +} +``` + +请求体 (字段映射后): + +```json +{ + "declaration": 1, // 固定传参 + "quest": "", // aspectRatio (string) 宽高比 + "ledger": "" // prompt (string) 文本提示词 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 15. 创建换衣任务 + +`POST /v1/image/clothes_swap_ex` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "", // userId — 用户ID + "congregation": "", // templateName — 模板名称 + "accolade": "1" // type — 类型 +} +``` + +请求体 (字段映射后): + +```json +{ + "profit": "", // srcImgBase64 (string) 源图片Base64数据 + "statute": "" // srcImgUrl (string) 源图片URL +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 16. 查询图片生成进度 + +`GET /v1/image/progress` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "", // userId — 用户ID + "tree": "" // taskId — 任务ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "curate": [ // imageInfos (array) 图片信息列表 + { + "reconfigure": "", // imgUrl (string) 图片URL + "reify": false, // isBlur (boolean) 是否模糊 + "royalty": 0, // imageId (long) 图片ID + "booking": "", // resolution (string) 分辨率 + "reconnect": 0 // imgType (int) 图片类型 + } + ], + "dice": 0, // progress (int) 进度 + "listing": 0, // state (int) 任务状态 + "accolade": 0, // type (int) 任务类型 + "flowStep": 0, // waitTime (int) 等待时间 + "tree": 0, // taskId (int) 任务ID + "revert": 0 // queueCnt (int) 队列数量 + } +} +``` + +### 17. 创建视频换脸任务 + +`POST /v1/image/video_facewap_task` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "" // userId — 用户ID +} +``` + +请求体 (字段映射后): + +```json +{ + "fragment": 10, // 固定传参 + "bond": "", // headPos (string) 头像位置 + "promotion": "", // videoUrl (string) 视频URL + "arena": 2, // 固定传参 + "agent": "", // headImg (string) 头像图片 + "congregation": "", // templateName (string) 模板名称 + "applicant": 1, // 固定传参 + "accolade": 0, // 固定传参 + "histogram": 3 // 固定传参 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 18. 创建图转视频姿态任务 + +`POST /v1/image/img2video_pose_task` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "", // userId — 用户ID + "congregation": "", // templateName — 模板名称 + "notification": "25", // fps — 帧率 + "allowance": "false", // needopt — 是否优化,通过配置控制 + "cosmos": "12", // duration — 时长 + "profit": "", // srcImgBase64 — 源图片Base64数据 + "compendium": "" // srcVideo — 源视频 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 19. 获取图转视频姿态模板 + +`GET /v1/image/img2Video_pose_template` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "crest": "", // ch — 渠道号 + "asset": "", // userId — 用户ID + "insignia": "1" // categoryId — 分类ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": [ // data (array) 图转视频姿态模板列表 + { + "cosmos": 0, // duration (int) 时长 + "extend": "", // templateTfUrl (string) 模板TF URL + "pipe": 0, // templateTfCredits (int) 模板TF积分 + "acknowledge": "", // templateFifUrl (string) 模板FIF URL + "congregation": "", // templateName (string) 模板名称 + "greaves": {}, // credits (object) 积分配置 + "unwrap": 0, // templateTfDuration (int) 模板TF时长 + "decommission": 0, // templateFifDuration (int) 模板FIF时长 + "ascend": "", // animate (string) 动画 + "pause": 0, // templateFifCredits (int) 模板FIF积分 + "inject": "" // templateUrl (string) 模板URL + } + ] +} +``` + +### 20. 创建换脸任务 + +`POST /v1/image/faceswap_task` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "" // app — 应用标识(必填) +} +``` + +请求体 (字段映射后): + +```json +{ + "revenue": "", // img (string) 图片 + "bond": "", // headPos (string) 头像位置 + "arena": 0, // 固定传参 + "agent": "", // headImg (string) 头像图片 + "booking": "", // resolution (string) 分辨率 + "histogram": 2, // 固定传参 + "stadium": 1 // 固定传参 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 21. App事件打点上报 + +`POST /v1/log/appevent` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "fortress": "", // event — 事件名称(必填) + "asset": "", // userId — 用户ID + "crest": "" // ch — 渠道 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "traverse": "", // hint (string) + "accolade": "" // type (string) + } +} +``` + +### 22. 创建黏土风格任务 + +`POST /v1/image/clay_stylization` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "", // userId — 用户ID + "allowance": "false", // needopt — 是否优化,通过配置控制 + "crest": "" // ch — 渠道号 +} +``` + +请求体 (字段映射后): + +```json +{ + "profit": "", // srcImgBase64 (string) 源图片Base64数据 + "statute": "" // srcImgUrl (string) 源图片URL +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 23. 创建自定义换衣任务 + +`POST /v1/image/custom_clothes_swap` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "asset": "", // userId — 用户ID + "ranger": "3" // appType — 应用类型 +} +``` + +请求体 (字段映射后): + +```json +{ + "balance": "", // srcImg2 (string) 源图片2的文件路径 + "endeavor": "" // srcImg1 (string) 源图片1的文件路径 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 24. 获取文生图标签 + +`GET /v1/image/txt2img_tags` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "portal": "", // pkg — 应用包名(必填) + "asset": "" // userId — 用户ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": [ // data (array) 标签列表 + { + "parallelize": "", // theme (string) 主题 + "rally": [] // tags (array) 标签 + } + ] +} +``` + +### 25. 获取文生图提示词模板 + +`POST /v1/image/txt2img_prompts` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +请求体 (字段映射后): + +```json +{ + "sidekick": [] // data (array) 标签数据列表 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "ledger": "", // prompt (string) 文本提示词 + "rally": "" // tags (string) 标签 + } +} +``` + +### 26. 获取分类列表 + +`GET /v1/image/category-list` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "portal": "", // pkg — 应用包名(必填) + "endowment": "" // templateTypes — 模板类型列表 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": [ // data (array) 分类列表 + { + "recharge": "", // templateType (string) 模板类型 + "patrol": "", // categoryName (string) 分类名称 + "insignia": 0 // categoryId (int) 分类ID + } + ] +} +``` + +### 27. 获取App语言配置 + +`GET /v1/config/app-language` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "portal": "", // pkg — 应用包名(必填) + "seminar": "" // lang — 语言代码 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "charge": "" // config (string) 语言配置JSON + } +} +``` + +### 28. 图片动作风格化gif + +`POST /v1/image/offtop_video` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "", // userId — 用户ID + "allowance": "false" // needopt — 是否优化,通过配置控制 +} +``` + +请求体 (字段映射后): + +```json +{ + "summary": "480p", // 固定传参 + "refuge": "" // srcImg (string) 源图片 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 29. 获取我的任务列表 + +`GET /v1/image/my-tasks` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "trophy": "", // page — 页码 + "heatmap": "", // size — 每页数量 + "platoon": "1,3,4" // taskTypes — 任务类型列表 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "weigh": 0, // total (int) 总数 + "empower": 0, // current (int) 当前页 + "heatmap": 0, // size (int) 每页大小 + "intensify": [ // records (array) 记录列表 + { + "cipher": 0, // taskType (int) 任务类型 + "discover": 0, // createTime (long) 创建时间 + "listing": "", // state (string) 任务状态 + "tree": 0, // taskId (long) 任务ID + "downsample": [] // imgList (array) 图片列表 + } + ], + "manifest": false // hasNext (boolean) 是否有下一页 + } +} +``` + +### 30. 创建增强换脸任务 + +`POST /v1/image/faceswap_ex_task` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "sentinel": "", // app — 应用标识(必填) + "asset": "" // userId — 用户ID +} +``` + +请求体 (字段映射后): + +```json +{ + "balance": "", // srcImg2 (string) 源图片2的文件路径 + "endeavor": "" // srcImg1 (string) 源图片1的文件路径 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 31. 创建生图任务 + +`POST /v1/image/create-task` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "asset": "" // userId — 用户ID +} +``` + +请求体 (字段映射后): + +```json +{ + "nexus": "", // ext (string) 扩展参数 + "cipher": "", // taskType (string) 任务类型 + "heatmap": "", // size (string) 输出尺寸,如 480p + "congregation": "", // templateName (string) 模板名称 + "balance": "", // srcImg2 (string) 源图片2的文件路径 + "guild": "", // srcImg1Url (string) 源图片1的URL + "allowance": false, // 固定传参 + "endeavor": "", // srcImg1 (string) 源图片1的文件路径 + "commission": "", // srcImg2Url (string) 源图片2的URL + "ledger": "" // prompt (string) 文本提示词 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "listing": 0, // state (int) 任务状态 + "tree": 0 // taskId (int) 任务ID + } +} +``` + +### 32. 获取积分页面信息 + +`GET /v1/user/credits-page` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "trophy": "1", // page — 页码 + "heatmap": "10", // size — 每页条数 + "accolade": "1" // type — 积分类型 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "weigh": 0, // total (int) 总条数 + "empower": 0, // current (int) 当前页 + "coordinate": 0, // pages (int) 总页数 + "heatmap": 0, // size (int) 每页大小 + "intensify": [ // records (array) 积分记录列表 + { + "tailor": "", // subBusinessType (string) 子业务类型 + "greaves": 0, // credits (int) 积分数 + "discover": 0, // createTime (long) 创建时间 + "vanquish": "", // businessId (string) 业务ID + "accolade": 0, // type (int) 类型,1=增加,2=扣除 + "defend": "" // businessType (string) 业务类型 + } + ] + } +} +``` + +### 33. 删除任务 + +`POST /v1/image/delete-task` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +请求体 (字段映射后): + +```json +{ + "tree": 0 // taskId (int) 任务ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "traverse": "", // hint (string) + "accolade": "" // type (string) + } +} +``` + +### 34. 获取支付方式列表 + +`POST /v1/payment/get-payment-methods` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +请求体 (字段映射后): + +```json +{ + "warrior": 0, // activityId (int) 活动模板ID + "vambrace": "" // country (string) 国家代码 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "renew": [ // paymentMethods (array) 支付方式列表 + { + "ceremony": "", // subPaymentMethod (string) 子支付方式 + "conjure": 0, // bonusCredits (int) 赠送积分 + "brigade": "", // name (string) 名称 + "greylist": "", // icon (string) 图标 + "finalize": false, // recentlyUsed (boolean) 是否最近使用 + "resource": "", // paymentMethod (string) 支付方式 + "deny": false, // recommend (boolean) 是否推荐 + "enchant": 0.0, // bonusRatio (double) 赠送比例 + "deploy": false // selected (boolean) 是否默认选中 + } + ] + } +} +``` + +### 35. 获取订单详情 + +`GET /v1/payment/getOrderDetail` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "asset": "", // userId — 用户ID + "federation": "" // id — 支付ID/订单ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "warrior": "", // activityId (string) 活动模板ID + "disambiguate": "", // redirectUrl (string) 重定向URL + "awaken": false, // addCredits (boolean) 是否添加积分 + "visor": "", // count (string) 数量 + "familiar": "", // currency (string) 货币 + "federation": "", // id (string) 支付ID/订单ID + "line": {} // status (object) 状态 + } +} +``` + +### 36. 获取反馈上传URL + +`POST /v1/feedback/upload-presigned-url` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +请求体 (字段映射后): + +```json +{ + "layer": "" // fileName (string) 文件名 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "shed": "", // uploadUrl (string) 上传URL + "hunt": "" // filePath (string) 文件路径 + } +} +``` + +### 37. 提交反馈 + +`POST /v1/feedback/submit` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +请求体 (字段映射后): + +```json +{ + "inventory": [], // fileUrls (array) 文件URL列表 + "pauldron": "", // contentType (string) 内容类型 + "cloak": "" // content (string) 内容 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "traverse": "", // hint (string) + "accolade": "" // type (string) + } +} +``` + +### 38. 获取反馈列表 + +`GET /v1/feedback/list` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "trophy": "1", // page — 页码 + "heatmap": "10" // size — 每页条数 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "weigh": 0, // total (int) 总条数 + "empower": 0, // current (int) 当前页 + "heatmap": 0, // size (int) 每页大小 + "intensify": [ // records (array) 反馈记录列表 + { + "redact": false, // read (boolean) 是否已读 + "track": 0, // feedbackId (long) 反馈ID + "inventory": [], // fileUrls (array) 附件URL列表 + "voxelize": 0, // replyStatus (int) 回复状态 + "uncover": "", // createTimeText (string) 创建时间 + "cloak": "" // content (string) 反馈内容 + } + ], + "manifest": false // hasNext (boolean) 是否有下一页 + } +} +``` + +### 39. 标记反馈已读 + +`POST /v1/feedback/read` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +请求体 (字段映射后): + +```json +{ + "registry": [] // feedbackIds (array) 反馈ID列表 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "traverse": "", // hint (string) + "accolade": "" // type (string) + } +} +``` + +### 40. 获取未读消息数 + +`GET /v1/user/unread-message-count` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "municipality": "" // types — 消息类型列表 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "footprint": [ // unreadCountList (array) 各类型未读数列表 + { + "visor": 0, // count (int) 未读数 + "accolade": "" // type (string) 消息类型 + } + ], + "defragment": 0, // totalUnreadCount (int) 总未读消息数 + "probe": 0 // lastUpdateTime (int) 最后更新时间戳 + } +} +``` + +### 41. 获取图转视频分类 + +`GET /v1/image/img2video/categories` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": [ // data (array) 分类列表 + { + "brigade": "", // name (string) 分类名称 + "greylist": "", // icon (string) 图标 + "federation": 0 // id (int) 分类ID + } + ] +} +``` + +### 42. 获取图转视频任务列表 + +`GET /v1/image/img2video/tasks` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +查询参数 (字段映射后): + +```json +{ + "insignia": "" // categoryId — 分类ID +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": [ // data (array) 任务列表 + { + "preempt": {}, // previewVideo (object) 预览视频 + "preprocess": {}, // resolution720p (object) 720p分辨率 + "simplify": 0, // imageCount (int) 图片数量 + "nexus": "", // ext (string) 扩展参数 + "cipher": "", // taskType (string) 任务类型 + "reverify": {}, // resolution480p (object) 480p分辨率 + "congregation": "", // templateName (string) 模板名称 + "allowance": false, // needopt (boolean) 是否优化,通过配置控制 + "subscribe": {}, // label (object) 标签 + "glossary": "", // title (string) 标题 + "extract": {} // previewImage (object) 预览图 + } + ] +} +``` + +### 43. 获取预签名上传URL + +`POST /v1/image/upload-presigned-url` | 鉴权: 需要 + +请求头 (字段映射后): + +``` + portal 应用包名(必填) (原始: pkg) + knight 用户登录token (原始: User_token) +``` + +请求体 (字段映射后): + +```json +{ + "gateway": "", // fileName1 (string) 文件名1 + "action": "", // fileName2 (string) 文件名2 + "pauldron": "", // contentType (string) 文件MIME类型 + "stronghold": 0 // expectedSize (int) 预期文件大小 +} +``` + +响应 (解密后, 字段映射后): + +```json +{ + "rampart": "", // msg (string) 消息 + "helm": 0, // code (int) 状态码,0=成功 + "sidekick": { // data + "bolster": "", // uploadUrl1 (string) 上传URL 1 + "soar": 0, // expiresIn (int) 过期时间(秒) + "remap": {}, // requiredHeaders (object) 上传所需请求头 + "expound": "", // uploadUrl2 (string) 上传URL 2 + "train": "", // filePath2 (string) 文件路径 2 + "recruit": "" // filePath1 (string) 文件路径 1 + } +} +``` + +## 错误码 + +| code | 说明 | +|------|------| +| 0 | 成功 | +| -1 | 失败 | +| -2 | 系统错误 | +| -3 | 鉴权失败 | +| -5 | 参数错误 | +| 1001 | 积分不足 | +| 1002 | 免费次数已用完 | +| 1003 | 免费次数和积分均已用完 | + +## 字段映射全表 + +| 原始字段 | V2字段 | +|----------|--------| +| User_token | knight | +| accountId | wizard | +| accountName | captain | +| activityId | warrior | +| actualAmount | guardian | +| adsTokenId | champion | +| amount | hero | +| app | sentinel | +| appType | ranger | +| approveState | paladin | +| aspectRatio | quest | +| automaticRenewal | legend | +| baseUrl | destiny | +| begTime | emblem | +| campaignInfo | relic | +| card | artifact | +| ch | crest | +| channel | lineage | +| channelType | armor | +| client | shield | +| clientId | gauntlet | +| code | helm | +| coder | mantle | +| contentType | pauldron | +| count | visor | +| country | vambrace | +| currency | familiar | +| cvcCode | companion | +| data | sidekick | +| days | species | +| devId | ancestry | +| deviceId | origin | +| domain | totem | +| dt | sigil | +| endTime | citadel | +| event | fortress | +| expectedSize | stronghold | +| expireMonth | dominion | +| expireYear | sanctuary | +| ext | nexus | +| extJson | vanguard | +| feedbackIds | registry | +| fileName | layer | +| fileName1 | gateway | +| fileName2 | action | +| filterCountry | blueprint | +| filterDomain | vendor | +| filterOwnerId | participant | +| filterStatus | specimen | +| filterTaskType | package | +| filterVip | quotation | +| firstName | appointment | +| fps | notification | +| googleClientId | discourse | +| hash | mediation | +| hashIsZero | corpus | +| headFaceIndex | histogram | +| headImg | agent | +| headPos | bond | +| headType | arena | +| host | initiative | +| imageFaceIndex | committee | +| imageId | royalty | +| img | revenue | +| imgCount | declaration | +| imgId | criterion | +| inviteBy | item | +| keepUserId | series | +| lang | seminar | +| lastName | bazaar | +| link | festival | +| list | junction | +| method | heap | +| model | authorization | +| msg | rampart | +| needImage | assembly | +| needUndress | directive | +| needopt | allowance | +| negativePrompt | underwriter | +| orderId | gazette | +| originAmount | curriculum | +| owner | genealogy | +| p12KeyPath | arraignment | +| packageName | folio | +| page | trophy | +| password | township | +| payMethods | wallet | +| paymentMethod | resource | +| phone | block | +| pkg | portal | +| platform | trigger | +| prompt | ledger | +| publicKey | template | +| purchaseData | merchant | +| receipt | attendee | +| referer | digest | +| refererType | campaign | +| resolution | booking | +| serviceAccountEmail | alert | +| sharedSecret | comment | +| sign | resolution | +| signature | sample | +| size | heatmap | +| srcFaceImgBase64 | attachment | +| srcFaceIndex | stadium | +| srcImg | refuge | +| srcImg1 | endeavor | +| srcImg1Url | guild | +| srcImg2 | balance | +| srcImg2Url | commission | +| srcImgBase64 | profit | +| srcImgUrl | statute | +| srcVideo | compendium | +| status | line | +| style | webinar | +| subPaymentMethod | ceremony | +| targetId | node | +| taskIds | privilege | +| taskType | cipher | +| taskTypes | platoon | +| templateName | congregation | +| templateTypes | endowment | +| tgId | concession | +| tgOrderId | insurer | +| token | thesis | +| transactionId | chronicle | +| truePrompt | deposition | +| ts | quarto | +| type | accolade | +| types | municipality | +| userApplePayList | gallery | +| userId | asset | +| userInfoType | frame | +| username | pulse | +| val | journal | +| value | schema | +| verifyCode | supplier | +| videoFaceIndex | applicant | +| videoFrameIndex | fragment | +| videoSize | summary | +| activitys | summon | +| addCredits | awaken | +| appFbConfig | evolve | +| appraise | unleash | +| config | charge | +| countId | command | +| countryCode | navigate | +| credit | reveal | +| creditPromotion | unlock | +| creditsRecordUrl | conquer | +| cryptoInvoiceUrl | protect | +| current | empower | +| department | inspire | +| description | pedigree | +| domains | avatar | +| duration | cosmos | +| email | galaxy | +| enabled | nebula | +| expiresIn | soar | +| extConfig | surge | +| faceInfos | strike | +| feature | dodge | +| features | scout | +| filePath | hunt | +| filePath1 | recruit | +| filePath2 | train | +| firstRegister | equip | +| forcePayCenter | upgrade | +| freeBlurTimes | vow | +| freeTimes | decree | +| h5UrlConfig | pursue | +| hasNext | manifest | +| hint | traverse | +| imgUrl | reconfigure | +| invitedCnt | deduce | +| isBlur | reify | +| isVip | generate | +| issuerUrl | merge | +| lastUpdateTime | probe | +| maxLimit | architect | +| methodActivitys | modify | +| methodChannels | restore | +| needCredits | deserialize | +| openType | transplant | +| optimizeCountSql | harvest | +| optimizeJoinOfCountSql | seek | +| orders | identify | +| pages | coordinate | +| payCenterUrl | switch | +| payStatus | destroy | +| payTypeList | handshake | +| payUrl | convert | +| paymentMethods | renew | +| permissions | sandbox | +| recent | entrap | +| records | intensify | +| redirectUrl | disambiguate | +| requiredHeaders | remap | +| role | evaluate | +| searchCount | suspend | +| subScribeValidTime | tokenize | +| subscriptionActivitys | cleanse | +| sumCredits | emit | +| sys | associate | +| t2IConfig | regulate | +| tags | rally | +| targetInvitedCnt | authorize | +| tgName | defer | +| total | weigh | +| totalUnreadCount | defragment | +| unreadCountList | footprint | +| uploadUrl | shed | +| uploadUrl1 | bolster | +| uploadUrl2 | expound | +| userName | terminal | +| userToken | reevaluate | +| userType | prefetch | +| usign | retrospect | +| videoUrl | promotion | +| age | transform | +| animate | ascend | +| avatar | realm | +| baseCredits | wield | +| bonus | forge | +| bonusCredits | conjure | +| bonusRatio | enchant | +| businessId | vanquish | +| businessType | defend | +| categoryId | insignia | +| categoryName | patrol | +| content | cloak | +| creatTime | explore | +| createTime | discover | +| createTimeText | uncover | +| credits | greaves | +| discountOff | lead | +| face | dash | +| faceIndex | leap | +| face_index | parry | +| feedbackId | track | +| fileUrls | inventory | +| frameIndex | unite | +| frame_index | pledge | +| gender | venture | +| icon | greylist | +| id | federation | +| imageCount | simplify | +| imageInfos | curate | +| imgList | downsample | +| imgType | reconnect | +| index | prewarm | +| label | subscribe | +| name | brigade | +| note | extrapolate | +| payId | assert | +| payMethod | intercept | +| payTime | restart | +| paylink | unzip | +| previewImage | extract | +| previewVideo | preempt | +| productType | subtract | +| progress | dice | +| queueCnt | revert | +| read | redact | +| recentlyUsed | finalize | +| recommend | deny | +| replyStatus | voxelize | +| resolution480p | reverify | +| resolution720p | preprocess | +| resulotion | theorize | +| rewardCredits | represent | +| rewardRatio | render | +| selected | deploy | +| showName | mold | +| sort | invent | +| state | listing | +| subBusinessType | tailor | +| subscriptionPeriod | distribute | +| tag | constrain | +| taskId | tree | +| templateFaceUrl | simulate | +| templateFifCredits | pause | +| templateFifDuration | decommission | +| templateFifUrl | acknowledge | +| templateTfCredits | pipe | +| templateTfDuration | unwrap | +| templateTfUrl | extend | +| templateType | recharge | +| templateUrl | inject | +| theme | parallelize | +| title | glossary | +| updateTime | fork | +| url | digitize | +| user | rebalance | +| videoDuration | demonstrate | +| waitTime | flowStep | +| x1 | invoke | +| x2 | sync | +| y1 | configure | +| y2 | manufacture | + +--- +*本文档由 ProtocolEncTool 自动生成,API 变更后请重新执行生成命令。* diff --git a/ios/Flutter/Generated.xcconfig b/ios/Flutter/Generated.xcconfig new file mode 100644 index 0000000..b8a8721 --- /dev/null +++ b/ios/Flutter/Generated.xcconfig @@ -0,0 +1,14 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/sven/flutter +FLUTTER_APPLICATION_PATH=/Users/sven/Project/Workplace/app_client +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_TARGET=lib/main.dart +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NUMBER=1 +EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 +EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 0000000..a88caf9 --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 0000000..e3ba6fb --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh new file mode 100755 index 0000000..811a0e2 --- /dev/null +++ b/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/sven/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/sven/Project/Workplace/app_client" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/GeneratedPluginRegistrant.h b/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 0000000..7a89092 --- /dev/null +++ b/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +NS_ASSUME_NONNULL_END +#endif /* GeneratedPluginRegistrant_h */ diff --git a/ios/Runner/GeneratedPluginRegistrant.m b/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000..ef90571 --- /dev/null +++ b/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,28 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#import "GeneratedPluginRegistrant.h" + +#if __has_include() +#import +#else +@import device_info_plus; +#endif + +#if __has_include() +#import +#else +@import sqflite_darwin; +#endif + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { + [FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]]; + [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; +} + +@end diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..c8466f9 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + MagiEvery AI + NSPhotoLibraryUsageDescription + We need access to your photos to use as reference for AI image generation. + NSCameraUsageDescription + We need camera access to capture reference images for AI generation. + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + app_client + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..e5c3dad --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'core/theme/app_theme.dart'; +import 'features/gallery/gallery_screen.dart'; +import 'features/generate_video/generate_progress_screen.dart'; +import 'features/generate_video/generate_video_screen.dart'; +import 'features/generate_video/generation_result_screen.dart'; +import 'features/home/home_screen.dart'; +import 'features/profile/profile_screen.dart'; +import 'features/recharge/recharge_screen.dart'; +import 'shared/widgets/bottom_nav_bar.dart'; + +/// Root app widget with navigation +class App extends StatefulWidget { + const App({super.key}); + + @override + State createState() => _AppState(); +} + +class _AppState extends State { + NavTab _currentTab = NavTab.home; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'AI Video App', + theme: AppTheme.light, + debugShowCheckedModeBanner: false, + initialRoute: '/', + routes: { + '/': (_) => _MainScaffold( + currentTab: _currentTab, + onTabSelected: (tab) => setState(() => _currentTab = tab), + ), + '/recharge': (_) => const RechargeScreen(), + '/generate': (_) => const GenerateVideoScreen(), + '/progress': (_) => const GenerateProgressScreen(), + '/result': (_) => const GenerationResultScreen(), + }, + ); + } +} + +class _MainScaffold extends StatelessWidget { + const _MainScaffold({ + required this.currentTab, + required this.onTabSelected, + }); + + final NavTab currentTab; + final ValueChanged onTabSelected; + + static const _screens = [ + HomeScreen(), + GalleryScreen(), + ProfileScreen(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: IndexedStack( + index: currentTab.index, + children: _screens, + ), + bottomNavigationBar: BottomNavBar( + currentTab: currentTab, + onTabSelected: onTabSelected, + ), + ); + } +} diff --git a/lib/core/api/api.dart b/lib/core/api/api.dart new file mode 100644 index 0000000..42564a2 --- /dev/null +++ b/lib/core/api/api.dart @@ -0,0 +1,22 @@ +/// petsHeroAI API 模块 +/// +/// 使用方式: +/// ```dart +/// // 登录后设置 token +/// ApiClient.instance.setUserToken('xxx'); +/// +/// // 调用 API +/// final res = await UserApi.getAccount(sentinel: ApiConfig.appId); +/// if (res.isSuccess) { +/// final credit = res.data['credit']; +/// } +/// ``` +library; + +export 'api_client.dart'; +export 'api_config.dart'; +export 'api_crypto.dart'; +export 'proxy_client.dart'; +export 'services/image_api.dart'; +export 'services/payment_api.dart'; +export 'services/user_api.dart'; diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart new file mode 100644 index 0000000..b2a0a50 --- /dev/null +++ b/lib/core/api/api_client.dart @@ -0,0 +1,22 @@ +import 'api_config.dart'; +import 'proxy_client.dart'; + +/// 全局 API 客户端单例 +class ApiClient { + ApiClient._(); + + static final ApiClient _instance = ApiClient._(); + + static ApiClient get instance => _instance; + + late final ProxyClient _proxy = ProxyClient( + packageName: ApiConfig.packageName, + ); + + /// 设置用户 Token(登录后调用) + void setUserToken(String? token) { + _proxy.userToken = token; + } + + ProxyClient get proxy => _proxy; +} diff --git a/lib/core/api/api_config.dart b/lib/core/api/api_config.dart new file mode 100644 index 0000000..038c8f2 --- /dev/null +++ b/lib/core/api/api_config.dart @@ -0,0 +1,36 @@ +import 'package:flutter/foundation.dart'; + +/// petsHeroAI API 配置 +abstract final class ApiConfig { + /// AES 密钥 + static const String aesKey = 'liyP4LkMfP68XvCt'; + + /// 应用 ID + static const String appId = 'com.petsheroai.app'; + + /// 应用包名 + static const String packageName = 'com.petsheroai.app'; + + /// 预发环境域名 + static const String preBaseUrl = 'https://pre-ai.petsheroai.xyz'; + + /// 生产环境域名 + static const String prodBaseUrl = 'https://ai.petsheroai.xyz'; + + /// 代理入口路径 + static const String proxyPath = '/quester/defender/summoner'; + + /// 调试时本地代理地址(手机无法直连域名时,用电脑做转发) + /// 例如: 'http://192.168.1.100:8010'(电脑 IP) + /// 设为 null 则直连预发域名 + static const String? debugBaseUrlOverride = null; + + /// 当前使用的 baseUrl(调试用预发,打包用生产) + static String get baseUrl { + if (!kDebugMode) return prodBaseUrl; + return debugBaseUrlOverride ?? preBaseUrl; + } + + /// 代理入口完整 URL + static String get proxyUrl => '$baseUrl$proxyPath'; +} diff --git a/lib/core/api/api_crypto.dart b/lib/core/api/api_crypto.dart new file mode 100644 index 0000000..dc12c3c --- /dev/null +++ b/lib/core/api/api_crypto.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import 'package:encrypt/encrypt.dart'; + +import 'api_config.dart'; + +/// AES-128-ECB 加解密 +abstract final class ApiCrypto { + static final _key = Key.fromUtf8(ApiConfig.aesKey); + + static final _encrypter = Encrypter( + AES( + _key, + mode: AESMode.ecb, + padding: 'PKCS7', + ), + ); + + /// AES 加密,返回 Base64 字符串 + static String encrypt(String plainText) { + final encrypted = _encrypter.encrypt(plainText); + return encrypted.base64; + } + + /// AES 解密,输入 Base64 字符串 + static String decrypt(String base64Cipher) { + final encrypted = Encrypted.fromBase64(base64Cipher); + return _encrypter.decrypt(encrypted); + } + + /// 生成随机 Base64 字符串(用于噪音字段) + static String randomBase64([int byteLength = 16]) { + final bytes = List.generate(byteLength, (_) => DateTime.now().millisecondsSinceEpoch % 256); + return base64Encode(bytes); + } + + /// 生成 8 位随机字母数字 + static String randomAlnum() { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + return List.generate(8, (_) => chars[DateTime.now().microsecondsSinceEpoch % chars.length]).join(); + } +} diff --git a/lib/core/api/proxy_client.dart b/lib/core/api/proxy_client.dart new file mode 100644 index 0000000..9089fe2 --- /dev/null +++ b/lib/core/api/proxy_client.dart @@ -0,0 +1,195 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; + +import 'api_config.dart'; +import 'api_crypto.dart'; + +const _logTag = '[ProxyClient]'; + +void _log(String msg) { + if (kDebugMode) debugPrint('$_logTag $msg'); +} + +/// 代理请求体字段名(统一请求参数) +abstract final class ProxyKeys { + static const String heroClass = 'hero_class'; + static const String petSpecies = 'pet_species'; + static const String powerLevel = 'power_level'; + static const String questRank = 'quest_rank'; + static const String battleScore = 'battle_score'; + static const String loyaltyIndex = 'loyalty_index'; + static const String billingAddr = 'billing_addr'; + static const String utmTerm = 'utm_term'; + static const String clusterId = 'cluster_id'; + static const String lsnValue = 'lsn_value'; + static const String accuracyVal = 'accuracy_val'; + static const String dirPath = 'dir_path'; +} + +/// 代理请求客户端 +class ProxyClient { + ProxyClient({ + this.baseUrl, + this.packageName = ApiConfig.packageName, + String? userToken, + }) : _userToken = userToken; + + final String? baseUrl; + final String packageName; + String? _userToken; + + String? get userToken => _userToken; + set userToken(String? value) => _userToken = value; + + String get _baseUrl => baseUrl ?? ApiConfig.baseUrl; + + /// 构建 V2 包装体,业务参数填入 sanctum + Map _buildV2Wrapper(Map sanctum) { + return { + 'arsenal': 4, + 'vault': { + 'tome': { + 'codex': { + 'grimoire': { + 'sanctum': sanctum, + }, + }, + }, + }, + 'roar': ApiCrypto.randomAlnum(), + 'clash': ApiCrypto.randomAlnum(), + 'thunder': ApiCrypto.randomAlnum(), + 'rumble': ApiCrypto.randomAlnum(), + 'howl': ApiCrypto.randomAlnum(), + 'growl': ApiCrypto.randomAlnum(), + }; + } + + /// 发送代理请求 + /// [path] 接口路径,如 /v1/user/fast_login + /// [method] HTTP 方法,POST 或 GET + /// [headers] 请求头,使用 V2 字段名(portal、knight 等) + /// [queryParams] 查询参数,使用 V2 字段名(sentinel、asset 等) + /// [body] 请求体,使用 V2 字段名,将填入 sanctum + Future request({ + required String path, + required String method, + Map? headers, + Map? queryParams, + Map? body, + }) async { + final headersMap = Map.from(headers ?? {}); + if (packageName.isNotEmpty) { + headersMap['portal'] = packageName; + } + if (_userToken != null && _userToken!.isNotEmpty) { + headersMap['knight'] = _userToken!; + } + + final paramsMap = Map.from( + queryParams?.map((k, v) => MapEntry(k, v)) ?? {}, + ); + + final sanctum = body ?? {}; + final v2Body = _buildV2Wrapper(sanctum); + + // 原始入参 + final headersEncoded = jsonEncode(headersMap); + final paramsEncoded = jsonEncode(paramsMap); + final v2BodyEncoded = jsonEncode(v2Body); + + _log('========== 原始入参 =========='); + _log('path: $path'); + _log('method: $method'); + _log('headers: $headersEncoded'); + _log('queryParams: $paramsEncoded'); + _log('body(sanctum): ${jsonEncode(sanctum)}'); + _log('v2Body: $v2BodyEncoded'); + + final petSpeciesEnc = ApiCrypto.encrypt(path); + final powerLevelEnc = ApiCrypto.encrypt(method); + final questRankEnc = ApiCrypto.encrypt(headersEncoded); + final battleScoreEnc = ApiCrypto.encrypt(paramsEncoded); + final loyaltyIndexEnc = ApiCrypto.encrypt(v2BodyEncoded); + + _log('========== 加密后 =========='); + _log('pet_species: $petSpeciesEnc'); + _log('power_level: $powerLevelEnc'); + _log('quest_rank: $questRankEnc'); + _log('battle_score: $battleScoreEnc'); + _log('loyalty_index: $loyaltyIndexEnc'); + + final proxyBody = { + ProxyKeys.heroClass: ApiConfig.appId, + ProxyKeys.petSpecies: petSpeciesEnc, + ProxyKeys.powerLevel: powerLevelEnc, + ProxyKeys.questRank: questRankEnc, + ProxyKeys.battleScore: battleScoreEnc, + ProxyKeys.loyaltyIndex: loyaltyIndexEnc, + ProxyKeys.billingAddr: ApiCrypto.randomBase64(), + ProxyKeys.utmTerm: ApiCrypto.randomBase64(), + ProxyKeys.clusterId: ApiCrypto.randomBase64(), + ProxyKeys.lsnValue: ApiCrypto.randomBase64(), + ProxyKeys.accuracyVal: ApiCrypto.randomBase64(), + ProxyKeys.dirPath: ApiCrypto.randomBase64(), + }; + + final url = '$_baseUrl${ApiConfig.proxyPath}'; + _log('========== 请求 URL =========='); + _log('$url'); + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(proxyBody), + ); + + _log('========== 响应 =========='); + _log('statusCode: ${response.statusCode}'); + _log('body: ${response.body}'); + + return _parseResponse(response); + } + + ApiResponse _parseResponse(http.Response response) { + try { + // 响应解密流程:Base64 解码 → AES-ECB 解密 → PKCS5 去填充 → UTF-8 字符串 + final decrypted = ApiCrypto.decrypt(response.body); + final json = jsonDecode(decrypted) as Map; + _log('json: $json'); + // 解析 helm=code, rampart=msg, sidekick=data + final sanctum = json['vault']?['tome']?['codex']?['grimoire']?['sanctum']; + if (sanctum is Map) { + return ApiResponse( + code: sanctum['helm'] as int? ?? -1, + msg: sanctum['rampart'] as String? ?? '', + data: sanctum['sidekick'], + ); + } + return ApiResponse( + code: json['helm'] as int? ?? -1, + msg: json['rampart'] as String? ?? '', + data: json['sidekick'], + ); + } catch (e) { + return ApiResponse(code: -1, msg: e.toString()); + } + } +} + +/// 统一 API 响应 +class ApiResponse { + ApiResponse({ + required this.code, + this.msg = '', + this.data, + }); + + final int code; + final String msg; + final dynamic data; + + bool get isSuccess => code == 0; +} diff --git a/lib/core/api/services/image_api.dart b/lib/core/api/services/image_api.dart new file mode 100644 index 0000000..f7ec9e0 --- /dev/null +++ b/lib/core/api/services/image_api.dart @@ -0,0 +1,128 @@ +import '../api_client.dart'; +import '../proxy_client.dart'; + +/// 图片/视频生成相关 API +abstract final class ImageApi { + static final _client = ApiClient.instance.proxy; + + /// 获取推荐提示词 + static Future getPromptRecommends({ + required String sentinel, + String? crest, + String? asset, + }) async { + return _client.request( + path: '/v1/image/prompt/recomends', + method: 'GET', + queryParams: { + 'sentinel': sentinel, + if (crest != null) 'crest': crest, + if (asset != null) 'asset': asset, + }, + ); + } + + /// 创建文生图任务 + static Future createTxt2Img({ + required String sentinel, + required String ledger, + String? crest, + String? asset, + String? quest, + }) async { + return _client.request( + path: '/v1/image/txt2img_create', + method: 'POST', + queryParams: { + 'sentinel': sentinel, + if (crest != null) 'crest': crest, + if (asset != null) 'asset': asset, + }, + body: { + 'declaration': 1, + if (quest != null) 'quest': quest, + 'ledger': ledger, + }, + ); + } + + /// 创建图转视频姿态任务 + static Future createImg2VideoPose({ + required String sentinel, + required String asset, + String? congregation, + String? profit, + String? compendium, + String? notification, + String? allowance, + String? cosmos, + }) async { + return _client.request( + path: '/v1/image/img2video_pose_task', + method: 'POST', + queryParams: { + 'sentinel': sentinel, + 'asset': asset, + if (congregation != null) 'congregation': congregation, + if (notification != null) 'notification': notification, + if (allowance != null) 'allowance': allowance, + if (cosmos != null) 'cosmos': cosmos, + if (profit != null) 'profit': profit, + if (compendium != null) 'compendium': compendium, + }, + ); + } + + /// 查询任务进度 + static Future getProgress({ + required String sentinel, + required String tree, + String? asset, + }) async { + return _client.request( + path: '/v1/image/progress', + method: 'GET', + queryParams: { + 'sentinel': sentinel, + 'tree': tree, + if (asset != null) 'asset': asset, + }, + ); + } + + /// 获取图转视频姿态模板 + static Future getImg2VideoPoseTemplates({ + required String sentinel, + String? crest, + String? asset, + String? insignia, + }) async { + return _client.request( + path: '/v1/image/img2Video_pose_template', + method: 'GET', + queryParams: { + 'sentinel': sentinel, + if (crest != null) 'crest': crest, + if (asset != null) 'asset': asset, + if (insignia != null) 'insignia': insignia, + }, + ); + } + + /// 获取积分页面信息 + static Future getCreditsPageInfo({ + required String sentinel, + String? asset, + String? crest, + }) async { + return _client.request( + path: '/v1/image/getCreditsPageInfo', + method: 'GET', + queryParams: { + 'sentinel': sentinel, + if (asset != null) 'asset': asset, + if (crest != null) 'crest': crest, + }, + ); + } +} diff --git a/lib/core/api/services/payment_api.dart b/lib/core/api/services/payment_api.dart new file mode 100644 index 0000000..9a547fc --- /dev/null +++ b/lib/core/api/services/payment_api.dart @@ -0,0 +1,72 @@ +import '../api_client.dart'; +import '../proxy_client.dart'; + +/// 支付相关 API +abstract final class PaymentApi { + static final _client = ApiClient.instance.proxy; + + /// 获取 Google 商品列表 + static Future getGooglePayActivities({ + required String sentinel, + String? shield, + String? vambrace, + String? portal, + }) async { + return _client.request( + path: '/v1/payment/getGooglePayActivities', + method: 'GET', + queryParams: { + 'sentinel': sentinel, + if (shield != null) 'shield': shield, + if (vambrace != null) 'vambrace': vambrace, + if (portal != null) 'portal': portal, + }, + ); + } + + /// 获取 Apple 商品列表 + static Future getApplePayActivities({ + required String sentinel, + String? shield, + String? vambrace, + String? portal, + }) async { + return _client.request( + path: '/v1/payment/getApplePayActivities', + method: 'GET', + queryParams: { + 'sentinel': sentinel, + if (shield != null) 'shield': shield, + if (vambrace != null) 'vambrace': vambrace, + if (portal != null) 'portal': portal, + }, + ); + } + + /// 创建支付订单 + static Future createPayment({ + required String sentinel, + required String asset, + required String warrior, + required String resource, + String? lineage, + String? armor, + }) async { + return _client.request( + path: '/v1/payment/createPayment', + method: 'POST', + queryParams: { + 'sentinel': sentinel, + 'asset': asset, + }, + body: { + 'sentinel': sentinel, + 'asset': asset, + 'warrior': warrior, + 'resource': resource, + if (lineage != null) 'lineage': lineage, + if (armor != null) 'armor': armor, + }, + ); + } +} diff --git a/lib/core/api/services/user_api.dart b/lib/core/api/services/user_api.dart new file mode 100644 index 0000000..332c851 --- /dev/null +++ b/lib/core/api/services/user_api.dart @@ -0,0 +1,75 @@ +import '../api_client.dart'; +import '../api_config.dart'; +import '../proxy_client.dart'; + +/// 用户相关 API +abstract final class UserApi { + static final _client = ApiClient.instance.proxy; + + /// 设备快速登录 + /// 参数使用 V2 字段名:digest=referer, resolution=sign, origin=deviceId + static Future fastLogin({ + required String origin, + required String resolution, + String? digest, + String? crest, + String? accolade, + }) async { + return _client.request( + path: '/v1/user/fast_login', + method: 'POST', + queryParams: { + if (crest != null) 'crest': crest, + 'portal': ApiConfig.packageName, + if (accolade != null) 'accolade': accolade, + }, + body: { + 'digest': digest ?? '', + 'resolution': resolution, + 'origin': origin, + }, + ); + } + + /// 获取用户通用信息 + static Future getCommonInfo({ + required String sentinel, + String? shield, + String? asset, + String? crest, + String? item, + String? origin, + String? gauntlet, + String? portal, + }) async { + return _client.request( + path: '/v1/user/common_info', + method: 'GET', + queryParams: { + 'sentinel': sentinel, + if (shield != null) 'shield': shield, + if (asset != null) 'asset': asset, + if (crest != null) 'crest': crest, + if (item != null) 'item': item, + if (origin != null) 'origin': origin, + if (gauntlet != null) 'gauntlet': gauntlet, + if (portal != null) 'portal': portal, + }, + ); + } + + /// 获取用户账户信息 + static Future getAccount({ + required String sentinel, + String? asset, + }) async { + return _client.request( + path: '/v1/user/account', + method: 'GET', + queryParams: { + 'sentinel': sentinel, + if (asset != null) 'asset': asset, + }, + ); + } +} diff --git a/lib/core/auth/auth_service.dart b/lib/core/auth/auth_service.dart new file mode 100644 index 0000000..0616c16 --- /dev/null +++ b/lib/core/auth/auth_service.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; + +import '../api/api_client.dart'; +import '../api/proxy_client.dart'; +import '../api/services/user_api.dart'; + +/// 认证服务:APP 启动时执行快速登录 +class AuthService { + AuthService._(); + + static const _tag = '[AuthService]'; + + static void _log(String msg) { + debugPrint('$_tag $msg'); + } + + /// 获取设备 ID(Android: androidId, iOS: identifierForVendor, Web: fallback) + static Future _getDeviceId() async { + final deviceInfo = DeviceInfoPlugin(); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + final android = await deviceInfo.androidInfo; + return android.id; + case TargetPlatform.iOS: + final ios = await deviceInfo.iosInfo; + return ios.identifierForVendor ?? 'ios-unknown'; + default: + return 'device-${DateTime.now().millisecondsSinceEpoch}'; + } + } + + /// 计算 sign:MD5(deviceId) 大写 32 位 + static String _computeSign(String deviceId) { + final bytes = utf8.encode(deviceId); + final digest = md5.convert(bytes); + return digest.toString().toUpperCase(); + } + + /// APP 启动时调用快速登录 + /// 启动时网络可能未就绪,会延迟后重试 + static Future init() async { + _log('init: 开始快速登录'); + const maxRetries = 3; + const retryDelay = Duration(seconds: 2); + + try { + // 等待网络就绪(浏览器能访问但 App 报错时,多为启动时网络未初始化) + await Future.delayed(const Duration(seconds: 2)); + + final deviceId = await _getDeviceId(); + _log('init: deviceId=$deviceId'); + + final sign = _computeSign(deviceId); + _log('init: sign=$sign'); + + ApiResponse? res; + for (var i = 0; i < maxRetries; i++) { + if (i > 0) { + _log('init: 第 ${i + 1} 次重试,等待 ${retryDelay.inSeconds}s...'); + await Future.delayed(retryDelay); + } + try { + res = await UserApi.fastLogin( + origin: deviceId, + resolution: sign, + digest: '', + ); + break; + } catch (e) { + _log('init: 第 ${i + 1} 次请求失败: $e'); + if (i == maxRetries - 1) rethrow; + } + } + + if (res == null) return; + + _log('init: 登录结果 code=${res.code} msg=${res.msg}'); + + if (res.isSuccess && res.data != null) { + final data = res.data as Map?; + final token = data?['reevaluate'] as String?; + if (token != null && token.isNotEmpty) { + ApiClient.instance.setUserToken(token); + _log('init: 已设置 userToken'); + } else { + _log('init: 响应中无 reevaluate (userToken)'); + } + } else { + _log('init: 登录失败'); + } + } catch (e, st) { + _log('init: 异常 $e'); + _log('init: 堆栈 $st'); + } + } +} diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart new file mode 100644 index 0000000..f554d1d --- /dev/null +++ b/lib/core/theme/app_colors.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +/// Design tokens for AI Video App - 1:1 from Pencil design +abstract final class AppColors { + // Primary + static const Color primary = Color(0xFF8B5CF6); + static const Color primaryLight = Color(0x338B5CF6); // #8B5CF620 + static const Color primaryShadow = Color(0x338B5CF6); // #8B5CF620 for shadow + + // Neutrals + static const Color background = Color(0xFFFAFAFA); + static const Color surface = Color(0xFFFFFFFF); + static const Color surfaceAlt = Color(0xFFF4F4F5); + static const Color border = Color(0xFFE4E4E7); + + // Text + static const Color textPrimary = Color(0xFF18181B); + static const Color textSecondary = Color(0xFF71717A); + static const Color textMuted = Color(0xFFA1A1AA); + + // Accent + static const Color accentOrange = Color(0xFFF59E0B); + static const Color accentOrangeLight = Color(0x33F59E0B); // #F59E0B20 + + // Overlay + static const Color overlayDark = Color(0x99000000); + static const Color shadowLight = Color(0x14000000); // #00000008 + static const Color shadowMedium = Color(0x0D000000); // #0000000D + static const Color shadowSoft = Color(0x0A000000); // #0000000A +} diff --git a/lib/core/theme/app_spacing.dart b/lib/core/theme/app_spacing.dart new file mode 100644 index 0000000..97d94e7 --- /dev/null +++ b/lib/core/theme/app_spacing.dart @@ -0,0 +1,14 @@ +/// Spacing tokens from Pencil design +abstract final class AppSpacing { + static const double xs = 4; + static const double sm = 6; + static const double md = 8; + static const double lg = 12; + static const double xl = 16; + static const double xxl = 20; + static const double xxxl = 24; + + // Horizontal padding + static const double screenPadding = 20; + static const double screenPaddingLarge = 24; +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..7a1080f --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'app_colors.dart'; + +/// App theme configuration +abstract final class AppTheme { + static ThemeData get light => ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primary, + brightness: Brightness.light, + primary: AppColors.primary, + surface: AppColors.surface, + ), + scaffoldBackgroundColor: AppColors.background, + fontFamily: 'Inter', + ); +} diff --git a/lib/core/theme/app_typography.dart b/lib/core/theme/app_typography.dart new file mode 100644 index 0000000..62be528 --- /dev/null +++ b/lib/core/theme/app_typography.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +/// Typography tokens from Pencil design +abstract final class AppTypography { + // Plus Jakarta Sans - for nav titles + static TextStyle navTitle = GoogleFonts.plusJakartaSans( + fontSize: 20, + fontWeight: FontWeight.w700, + color: const Color(0xFF18181B), + ); + + // Inter - body text + static TextStyle bodyLarge = GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + ); + static TextStyle bodyMedium = GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + ); + static TextStyle bodyRegular = GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w500, + ); + static TextStyle bodySmall = GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + ); + static TextStyle caption = GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + ); + static TextStyle label = GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w500, + ); + static TextStyle tabLabel = GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w600, + ); + static TextStyle tabLabelInactive = GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w500, + ); +} diff --git a/lib/features/gallery/gallery_screen.dart b/lib/features/gallery/gallery_screen.dart new file mode 100644 index 0000000..3b55003 --- /dev/null +++ b/lib/features/gallery/gallery_screen.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../shared/widgets/top_nav_bar.dart'; + +/// Gallery screen - matches Pencil hpwBg +class GalleryScreen extends StatelessWidget { + const GalleryScreen({super.key}); + + static const _galleryImages = [ + 'https://images.unsplash.com/photo-1763929272543-0df093e4f659?w=400', + 'https://images.unsplash.com/photo-1703592819695-ea63799b7315?w=400', + 'https://images.unsplash.com/photo-1764787435677-1321e12559e3?w=400', + 'https://images.unsplash.com/photo-1759264244741-7175af0b7e75?w=400', + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: TopNavBar( + title: 'Gallery', + credits: '1,280', + onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'), + ), + ), + body: LayoutBuilder( + builder: (context, constraints) { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 390), + child: GridView.builder( + padding: const EdgeInsets.fromLTRB( + AppSpacing.screenPadding, + AppSpacing.xl, + AppSpacing.screenPadding, + AppSpacing.screenPaddingLarge, + ), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 165 / 248, + mainAxisSpacing: AppSpacing.xl, + crossAxisSpacing: AppSpacing.xl, + ), + itemCount: _galleryImages.length, + itemBuilder: (context, index) => + _GalleryCard(imageUrl: _galleryImages[index]), + ), + ), + ); + }, + ), + ); + } + +} + +class _GalleryCard extends StatelessWidget { + const _GalleryCard({required this.imageUrl}); + + final String imageUrl; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return Container( + width: constraints.maxWidth, + height: constraints.maxHeight, + decoration: BoxDecoration( + color: AppColors.surfaceAlt, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: AppColors.border, width: 1), + boxShadow: [ + BoxShadow( + color: AppColors.shadowMedium, + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: CachedNetworkImage( + imageUrl: imageUrl, + fit: BoxFit.cover, + placeholder: (_, __) => Container( + color: AppColors.surfaceAlt, + ), + errorWidget: (_, __, ___) => Container( + color: AppColors.surfaceAlt, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/generate_video/generate_progress_screen.dart b/lib/features/generate_video/generate_progress_screen.dart new file mode 100644 index 0000000..043cf26 --- /dev/null +++ b/lib/features/generate_video/generate_progress_screen.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../core/theme/app_typography.dart'; +import '../../shared/widgets/top_nav_bar.dart'; + +/// Generate Video Progress screen - matches Pencil qGs6n +class GenerateProgressScreen extends StatefulWidget { + const GenerateProgressScreen({super.key}); + + @override + State createState() => _GenerateProgressScreenState(); +} + +class _GenerateProgressScreenState extends State { + double _progress = 0.45; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: TopNavBar( + title: 'Generating', + showBackButton: true, + onBack: () => Navigator.of(context).pop(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.screenPaddingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _VideoPreview(), + const SizedBox(height: AppSpacing.xxl), + Text( + 'Video generation may take some time. Please wait patiently.', + textAlign: TextAlign.center, + style: AppTypography.bodyRegular.copyWith( + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: AppSpacing.xxl), + _ProgressSection(progress: _progress), + ], + ), + ), + ); + } +} + +class _VideoPreview extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + height: 360, + decoration: BoxDecoration( + color: AppColors.textPrimary, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border, width: 1), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + LucideIcons.film, + size: 64, + color: AppColors.textSecondary, + ), + const SizedBox(height: AppSpacing.lg), + Text( + 'Video Preview', + style: AppTypography.bodyRegular.copyWith( + color: AppColors.textSecondary, + ), + ), + ], + ), + ); + } +} + +class _ProgressSection extends StatelessWidget { + const _ProgressSection({required this.progress}); + + final double progress; + + @override + Widget build(BuildContext context) { + final percentage = (progress * 100).round(); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Generating... $percentage%', + style: AppTypography.bodyMedium.copyWith( + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: AppSpacing.lg), + LayoutBuilder( + builder: (context, constraints) { + final fillWidth = + constraints.maxWidth * progress.clamp(0.0, 1.0); + return Stack( + children: [ + Container( + height: 8, + decoration: BoxDecoration( + color: AppColors.border, + borderRadius: BorderRadius.circular(4), + ), + ), + Positioned( + left: 0, + top: 0, + bottom: 0, + child: Container( + width: fillWidth, + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ], + ); + }, + ), + ], + ); + } +} + diff --git a/lib/features/generate_video/generate_video_screen.dart b/lib/features/generate_video/generate_video_screen.dart new file mode 100644 index 0000000..867a098 --- /dev/null +++ b/lib/features/generate_video/generate_video_screen.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../core/theme/app_typography.dart'; +import '../../shared/widgets/top_nav_bar.dart'; + +/// Generate Video screen - matches Pencil mmLB5 +class GenerateVideoScreen extends StatelessWidget { + const GenerateVideoScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: TopNavBar( + title: 'Generate Video', + showBackButton: true, + onBack: () => Navigator.of(context).pop(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.screenPaddingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _CreditsCard(credits: '1,280'), + const SizedBox(height: AppSpacing.xxl), + _UploadArea(onUpload: () {}), + const SizedBox(height: AppSpacing.xxl), + _GenerateButton( + onGenerate: () => + Navigator.of(context).pushReplacementNamed('/progress'), + ), + ], + ), + ), + ); + } +} + +class _CreditsCard extends StatelessWidget { + const _CreditsCard({required this.credits}); + + final String credits; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xxl, + vertical: AppSpacing.xl, + ), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColors.primary.withValues(alpha: 0.5), + ), + boxShadow: [ + BoxShadow( + color: AppColors.primaryShadow.withValues(alpha: 0.25), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Icon(LucideIcons.sparkles, size: 28, color: AppColors.surface), + const SizedBox(width: AppSpacing.md), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Available Credits', + style: AppTypography.bodyRegular.copyWith( + color: AppColors.surface.withValues(alpha: 0.8), + ), + ), + Text( + credits, + style: AppTypography.bodyLarge.copyWith( + fontSize: 32, + fontWeight: FontWeight.w700, + color: AppColors.surface, + ), + ), + ], + ), + ], + ), + ); + } +} + +class _UploadArea extends StatelessWidget { + const _UploadArea({required this.onUpload}); + + final VoidCallback onUpload; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onUpload, + child: Container( + height: 280, + decoration: BoxDecoration( + color: AppColors.surfaceAlt, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColors.border, + width: 2, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + LucideIcons.image_plus, + size: 48, + color: AppColors.textMuted, + ), + const SizedBox(height: AppSpacing.lg), + SizedBox( + width: 280, + child: Text( + 'Please upload an image as the base for generation', + textAlign: TextAlign.center, + style: AppTypography.bodyRegular.copyWith( + color: AppColors.textSecondary, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _GenerateButton extends StatelessWidget { + const _GenerateButton({required this.onGenerate}); + + final VoidCallback onGenerate; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onGenerate, + child: Container( + height: 56, + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppColors.primaryShadow.withValues(alpha: 0.25), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Generate Video', + style: AppTypography.bodyMedium.copyWith( + color: AppColors.surface, + ), + ), + const SizedBox(width: AppSpacing.md), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: AppColors.surface.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + LucideIcons.sparkles, + size: 16, + color: AppColors.surface, + ), + const SizedBox(width: AppSpacing.xs), + Text( + '50', + style: AppTypography.bodyRegular.copyWith( + color: AppColors.surface, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/generate_video/generation_result_screen.dart b/lib/features/generate_video/generation_result_screen.dart new file mode 100644 index 0000000..2cd15de --- /dev/null +++ b/lib/features/generate_video/generation_result_screen.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../core/theme/app_typography.dart'; +import '../../shared/widgets/top_nav_bar.dart'; + +/// Video Generation Result screen - matches Pencil cFA4T +class GenerationResultScreen extends StatelessWidget { + const GenerationResultScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: TopNavBar( + title: 'Video Ready', + showBackButton: true, + onBack: () => Navigator.of(context).pop(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.screenPaddingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _VideoDisplay(), + const SizedBox(height: AppSpacing.xxl), + _DownloadButton(onDownload: () {}), + const SizedBox(height: AppSpacing.lg), + _ShareButton(onShare: () {}), + ], + ), + ), + ); + } +} + +class _VideoDisplay extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + height: 360, + decoration: BoxDecoration( + color: AppColors.textPrimary, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border, width: 1), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + LucideIcons.play, + size: 72, + color: AppColors.surface.withValues(alpha: 0.5), + ), + const SizedBox(height: AppSpacing.lg), + Text( + 'Your video is ready', + style: AppTypography.bodyRegular.copyWith( + color: AppColors.surface.withValues(alpha: 0.6), + ), + ), + ], + ), + ); + } +} + +class _DownloadButton extends StatelessWidget { + const _DownloadButton({required this.onDownload}); + + final VoidCallback onDownload; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onDownload, + child: Container( + height: 52, + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: AppColors.primaryShadow.withValues(alpha: 0.25), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(LucideIcons.download, size: 20, color: AppColors.surface), + const SizedBox(width: AppSpacing.md), + Text( + 'Download', + style: AppTypography.bodyMedium.copyWith( + color: AppColors.surface, + ), + ), + ], + ), + ), + ); + } +} + +class _ShareButton extends StatelessWidget { + const _ShareButton({required this.onShare}); + + final VoidCallback onShare; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onShare, + child: Container( + height: 52, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.border), + boxShadow: [ + BoxShadow( + color: AppColors.shadowLight, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(LucideIcons.share_2, size: 20, color: AppColors.primary), + const SizedBox(width: AppSpacing.md), + Text( + 'Share', + style: AppTypography.bodyMedium.copyWith( + color: AppColors.textPrimary, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart new file mode 100644 index 0000000..028c8e4 --- /dev/null +++ b/lib/features/home/home_screen.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../shared/widgets/top_nav_bar.dart'; +import 'widgets/home_tab_row.dart'; +import 'widgets/video_card.dart'; + +/// AI Video App home screen - matches Pencil bi8Au +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + HomeTab _selectedTab = HomeTab.all; + + static const _placeholderImages = [ + 'https://images.unsplash.com/photo-1763929272543-0df093e4f659?w=400', + 'https://images.unsplash.com/photo-1703592819695-ea63799b7315?w=400', + 'https://images.unsplash.com/photo-1764787435677-1321e12559e3?w=400', + 'https://images.unsplash.com/photo-1759264244741-7175af0b7e75?w=400', + 'https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=400', + 'https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=400', + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFAFAFA), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: TopNavBar( + title: 'AI Video', + credits: '1,280', + onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'), + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.screenPadding, + vertical: AppSpacing.xs, + ), + child: HomeTabRow( + selectedTab: _selectedTab, + onTabChanged: (tab) => setState(() => _selectedTab = tab), + ), + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 390, + ), + child: GridView.builder( + padding: const EdgeInsets.fromLTRB( + AppSpacing.screenPadding, + AppSpacing.xl, + AppSpacing.screenPadding, + AppSpacing.screenPaddingLarge, + ), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 165 / 248, + mainAxisSpacing: AppSpacing.xl, + crossAxisSpacing: AppSpacing.xl, + ), + itemCount: _placeholderImages.length, + itemBuilder: (context, index) => VideoCard( + imageUrl: _placeholderImages[index], + onGenerateSimilar: () => + Navigator.of(context).pushNamed('/generate'), + ), + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + +} diff --git a/lib/features/home/widgets/home_tab_row.dart b/lib/features/home/widgets/home_tab_row.dart new file mode 100644 index 0000000..ba22579 --- /dev/null +++ b/lib/features/home/widgets/home_tab_row.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import '../../../core/theme/app_colors.dart'; +import '../../../core/theme/app_spacing.dart'; +import '../../../core/theme/app_typography.dart'; + +enum HomeTab { all, trending, newTab } + +/// Tab row for home screen - matches Pencil tabRow +class HomeTabRow extends StatelessWidget { + const HomeTabRow({ + super.key, + required this.selectedTab, + required this.onTabChanged, + }); + + final HomeTab selectedTab; + final ValueChanged onTabChanged; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + _TabChip( + label: 'All', + isSelected: selectedTab == HomeTab.all, + onTap: () => onTabChanged(HomeTab.all), + ), + const SizedBox(width: AppSpacing.md), + _TabChip( + label: 'Trending', + isSelected: selectedTab == HomeTab.trending, + onTap: () => onTabChanged(HomeTab.trending), + ), + const SizedBox(width: AppSpacing.md), + _TabChip( + label: 'New', + isSelected: selectedTab == HomeTab.newTab, + onTap: () => onTabChanged(HomeTab.newTab), + ), + ], + ), + ), + ); + } +} + +class _TabChip extends StatelessWidget { + const _TabChip({ + required this.label, + required this.isSelected, + required this.onTap, + }); + + final String label; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 32, + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 6, + ), + decoration: BoxDecoration( + color: isSelected ? AppColors.primary : AppColors.surfaceAlt, + borderRadius: BorderRadius.circular(20), + border: isSelected ? null : Border.all(color: AppColors.border), + boxShadow: isSelected + ? [ + BoxShadow( + color: AppColors.primaryShadow.withValues(alpha: 0.19), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Center( + child: Text( + label, + style: AppTypography.bodySmall.copyWith( + color: isSelected ? AppColors.surface : AppColors.textSecondary, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/home/widgets/video_card.dart b/lib/features/home/widgets/video_card.dart new file mode 100644 index 0000000..d66a96c --- /dev/null +++ b/lib/features/home/widgets/video_card.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import '../../../core/theme/app_colors.dart'; +import '../../../core/theme/app_spacing.dart'; + +/// Video card for home grid - matches Pencil card1 +class VideoCard extends StatelessWidget { + const VideoCard({ + super.key, + required this.imageUrl, + this.credits = '50', + this.onTap, + this.onGenerateSimilar, + }); + + final String imageUrl; + final String credits; + final VoidCallback? onTap; + final VoidCallback? onGenerateSimilar; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return Container( + width: constraints.maxWidth, + height: constraints.maxHeight, + decoration: BoxDecoration( + color: AppColors.surfaceAlt, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: AppColors.border, width: 1), + boxShadow: [ + BoxShadow( + color: AppColors.shadowMedium, + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Stack( + fit: StackFit.expand, + children: [ + CachedNetworkImage( + imageUrl: imageUrl, + fit: BoxFit.cover, + placeholder: (_, __) => Container( + color: AppColors.surfaceAlt, + ), + errorWidget: (_, __, ___) => Container( + color: AppColors.surfaceAlt, + ), + ), + Positioned( + top: 12, + right: 12, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: AppColors.overlayDark, + borderRadius: BorderRadius.circular(14), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + LucideIcons.sparkles, + size: 12, + color: AppColors.surface, + ), + const SizedBox(width: AppSpacing.sm), + Text( + credits, + style: const TextStyle( + color: AppColors.surface, + fontSize: 11, + fontWeight: FontWeight.w600, + fontFamily: 'Inter', + ), + ), + ], + ), + ), + ), + Positioned.fill( + child: Center( + child: GestureDetector( + onTap: onTap, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppColors.surface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.13), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + LucideIcons.play, + size: 24, + color: AppColors.textPrimary, + ), + ), + ), + ), + ), + Positioned( + bottom: 12, + left: 12, + right: 12, + child: GestureDetector( + onTap: onGenerateSimilar, + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppColors.primaryShadow.withValues(alpha: 0.25), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: const Text( + 'Generate Similar', + style: TextStyle( + color: AppColors.surface, + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'Inter', + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/profile/profile_screen.dart b/lib/features/profile/profile_screen.dart new file mode 100644 index 0000000..c024f9f --- /dev/null +++ b/lib/features/profile/profile_screen.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../core/theme/app_typography.dart'; +import '../../shared/widgets/top_nav_bar.dart'; + +/// Profile screen - matches Pencil KXeow +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: TopNavBar( + title: 'Profile', + credits: '1,280', + onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB( + AppSpacing.screenPadding, + AppSpacing.xxl, + AppSpacing.screenPadding, + AppSpacing.screenPaddingLarge, + ), + child: Column( + children: [ + _ProfileHeader( + userName: 'Alex Johnson', + uid: 'UID 84920133', + ), + const SizedBox(height: AppSpacing.xl), + _BalanceCard( + balance: '1,280', + onRecharge: () => Navigator.of(context).pushNamed('/recharge'), + ), + const SizedBox(height: AppSpacing.xxl), + _MenuSection( + items: [ + _MenuItem( + title: 'Credit Store', + icon: LucideIcons.chevron_right, + onTap: () => Navigator.of(context).pushNamed('/recharge'), + ), + _MenuItem( + title: 'Settings', + icon: LucideIcons.chevron_right, + onTap: () {}, + ), + ], + ), + ], + ), + ), + ); + } +} + +class _ProfileHeader extends StatelessWidget { + const _ProfileHeader({ + required this.userName, + required this.uid, + }); + + final String userName; + final String uid; + + @override + Widget build(BuildContext context) { + return Container( + height: 220, + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: AppColors.border, + borderRadius: BorderRadius.circular(40), + border: Border.all( + color: AppColors.primaryLight, + width: 2, + ), + ), + child: const Icon( + LucideIcons.user, + size: 40, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: AppSpacing.lg), + Text( + userName, + style: AppTypography.bodyLarge.copyWith( + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: AppSpacing.lg), + Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), + decoration: BoxDecoration( + color: AppColors.surfaceAlt, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border), + ), + child: Text( + uid, + style: AppTypography.caption.copyWith( + color: AppColors.textSecondary, + ), + ), + ), + ], + ), + ); + } +} + +class _BalanceCard extends StatelessWidget { + const _BalanceCard({ + required this.balance, + required this.onRecharge, + }); + + final String balance; + final VoidCallback onRecharge; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xxl, + vertical: AppSpacing.xl, + ), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppColors.shadowLight, + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'AVAILABLE BALANCE', + style: AppTypography.label.copyWith( + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: AppSpacing.sm), + Text( + balance, + style: AppTypography.bodyLarge.copyWith( + fontSize: 24, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ], + ), + GestureDetector( + onTap: onRecharge, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: AppSpacing.md, + ), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Recharge', + style: AppTypography.bodyRegular.copyWith( + color: AppColors.surface, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ); + } +} + +class _MenuSection extends StatelessWidget { + const _MenuSection({required this.items}); + + final List<_MenuItem> items; + + @override + Widget build(BuildContext context) { + return Column( + children: items + .map( + (item) => Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.md), + child: _MenuItem( + title: item.title, + icon: item.icon, + onTap: item.onTap, + ), + ), + ) + .toList(), + ); + } +} + +class _MenuItem extends StatelessWidget { + const _MenuItem({ + required this.title, + required this.icon, + required this.onTap, + }); + + final String title; + final IconData icon; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xl, + vertical: 14, + ), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppColors.shadowLight, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: AppTypography.bodyRegular.copyWith( + color: AppColors.textPrimary, + ), + ), + Icon(icon, size: 20, color: AppColors.textMuted), + ], + ), + ), + ); + } +} diff --git a/lib/features/recharge/recharge_screen.dart b/lib/features/recharge/recharge_screen.dart new file mode 100644 index 0000000..7eb4078 --- /dev/null +++ b/lib/features/recharge/recharge_screen.dart @@ -0,0 +1,383 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../core/theme/app_typography.dart'; +import '../../shared/widgets/top_nav_bar.dart'; + +/// Recharge screen - matches Pencil tPjdN +class RechargeScreen extends StatelessWidget { + const RechargeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: TopNavBar( + title: 'Recharge', + showBackButton: true, + onBack: () => Navigator.of(context).pop(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: AppSpacing.screenPaddingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _CreditsSection(currentCredits: '1,280'), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.screenPaddingLarge, + vertical: AppSpacing.xxl, + ), + child: Text( + 'Select Tier', + style: AppTypography.bodyMedium.copyWith( + color: AppColors.textPrimary, + ), + ), + ), + _TierCard( + credits: '100 Credits', + price: '¥6', + onBuy: () {}, + ), + const SizedBox(height: AppSpacing.xl), + _TierCardRecommended( + credits: '500 Credits', + price: '¥25 Save ¥5', + onBuy: () {}, + ), + const SizedBox(height: AppSpacing.xl), + _TierCardPopular( + credits: '1,000 Credits', + price: '¥45 Save ¥15', + onBuy: () {}, + ), + ], + ), + ), + ); + } +} + +class _CreditsSection extends StatelessWidget { + const _CreditsSection({required this.currentCredits}); + + final String currentCredits; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(AppSpacing.screenPadding), + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xxl, + vertical: AppSpacing.xl, + ), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColors.primary.withValues(alpha: 0.5), + ), + boxShadow: [ + BoxShadow( + color: AppColors.primaryShadow.withValues(alpha: 0.25), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Icon(LucideIcons.sparkles, size: 28, color: AppColors.surface), + const SizedBox(width: AppSpacing.md), + Text( + currentCredits, + style: AppTypography.bodyLarge.copyWith( + fontSize: 32, + fontWeight: FontWeight.w700, + color: AppColors.surface, + ), + ), + const Spacer(), + Text( + 'Current Credits', + style: AppTypography.caption.copyWith( + color: AppColors.surface.withValues(alpha: 0.8), + ), + ), + ], + ), + ); + } +} + +class _TierCard extends StatelessWidget { + const _TierCard({ + required this.credits, + required this.price, + required this.onBuy, + }); + + final String credits; + final String price; + final VoidCallback onBuy; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding), + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xxl, + vertical: AppSpacing.xl, + ), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border), + boxShadow: [ + BoxShadow( + color: AppColors.shadowLight, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + credits, + style: AppTypography.bodyLarge.copyWith( + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: AppSpacing.xs), + Text( + price, + style: AppTypography.bodyRegular.copyWith( + color: AppColors.textSecondary, + ), + ), + ], + ), + _BuyButton(onTap: onBuy), + ], + ), + ); + } +} + +class _TierCardRecommended extends StatelessWidget { + const _TierCardRecommended({ + required this.credits, + required this.price, + required this.onBuy, + }); + + final String credits; + final String price; + final VoidCallback onBuy; + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + margin: + const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding), + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xxl, + vertical: AppSpacing.xl, + ), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border), + boxShadow: [ + BoxShadow( + color: AppColors.shadowLight, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + credits, + style: AppTypography.bodyLarge.copyWith( + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: AppSpacing.sm), + Text( + price, + style: AppTypography.bodyRegular.copyWith( + color: AppColors.textSecondary, + ), + ), + ], + ), + _BuyButton(onTap: onBuy), + ], + ), + ), + Positioned( + top: 0, + right: AppSpacing.screenPadding, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: AppColors.primaryLight, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), + ), + child: Text( + 'Recommended', + style: AppTypography.label.copyWith( + color: AppColors.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ); + } +} + +class _TierCardPopular extends StatelessWidget { + const _TierCardPopular({ + required this.credits, + required this.price, + required this.onBuy, + }); + + final String credits; + final String price; + final VoidCallback onBuy; + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + margin: + const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding), + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xxl, + vertical: AppSpacing.xl, + ), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border), + boxShadow: [ + BoxShadow( + color: AppColors.shadowLight, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + credits, + style: AppTypography.bodyLarge.copyWith( + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: AppSpacing.sm), + Text( + price, + style: AppTypography.bodyRegular.copyWith( + color: AppColors.textSecondary, + ), + ), + ], + ), + _BuyButton(onTap: onBuy), + ], + ), + ), + Positioned( + top: 0, + right: AppSpacing.screenPadding, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: AppColors.accentOrangeLight, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), + ), + child: Text( + 'Most Popular', + style: AppTypography.label.copyWith( + color: AppColors.accentOrange, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ); + } +} + +class _BuyButton extends StatelessWidget { + const _BuyButton({required this.onTap}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xl, + vertical: AppSpacing.md, + ), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Buy', + style: AppTypography.bodyRegular.copyWith( + color: AppColors.surface, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..fe6810e --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +import 'app.dart'; +import 'core/auth/auth_service.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const App()); + // APP 打开时后台执行快速登录 + AuthService.init(); +} diff --git a/lib/shared/widgets/bottom_nav_bar.dart b/lib/shared/widgets/bottom_nav_bar.dart new file mode 100644 index 0000000..1e303c4 --- /dev/null +++ b/lib/shared/widgets/bottom_nav_bar.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../core/theme/app_typography.dart'; + +enum NavTab { home, gallery, profile } + +/// Bottom navigation bar - matches Pencil bottomNav +class BottomNavBar extends StatelessWidget { + const BottomNavBar({ + super.key, + required this.currentTab, + required this.onTabSelected, + }); + + final NavTab currentTab; + final ValueChanged onTabSelected; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 56, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.screenPadding, + vertical: 7.5, + ), + decoration: BoxDecoration( + color: AppColors.surface, + boxShadow: [ + BoxShadow( + color: AppColors.shadowSoft, + blurRadius: 12, + offset: const Offset(0, -4), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: _NavTabItem( + icon: LucideIcons.house, + label: 'HOME', + isSelected: currentTab == NavTab.home, + onTap: () => onTabSelected(NavTab.home), + ), + ), + Expanded( + child: _NavTabItem( + icon: LucideIcons.images, + label: 'GALLERY', + isSelected: currentTab == NavTab.gallery, + onTap: () => onTabSelected(NavTab.gallery), + ), + ), + Expanded( + child: _NavTabItem( + icon: LucideIcons.user, + label: 'PROFILE', + isSelected: currentTab == NavTab.profile, + onTap: () => onTabSelected(NavTab.profile), + ), + ), + ], + ), + ), + ); + } +} + +class _NavTabItem extends StatelessWidget { + const _NavTabItem({ + required this.icon, + required this.label, + required this.isSelected, + required this.onTap, + }); + + final IconData icon; + final String label; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + height: 41, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isSelected ? AppColors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(26), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 18, + color: isSelected ? AppColors.surface : AppColors.textMuted, + ), + const SizedBox(width: AppSpacing.xs), + Text( + label, + style: (isSelected + ? AppTypography.tabLabel + : AppTypography.tabLabelInactive) + .copyWith( + color: isSelected ? AppColors.surface : AppColors.textMuted, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/shared/widgets/credits_badge.dart b/lib/shared/widgets/credits_badge.dart new file mode 100644 index 0000000..9426c7f --- /dev/null +++ b/lib/shared/widgets/credits_badge.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../core/theme/app_typography.dart'; + +/// Credits badge with sparkles icon - matches Pencil creditsBadge +class CreditsBadge extends StatelessWidget { + const CreditsBadge({ + super.key, + required this.credits, + this.onTap, + }); + + final String credits; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), + decoration: BoxDecoration( + color: AppColors.primaryLight, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: AppColors.primaryShadow.withValues(alpha: 0.13), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.sparkles, size: 16, color: AppColors.primary), + const SizedBox(width: AppSpacing.sm), + Text( + credits, + style: AppTypography.bodyRegular.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.primary, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/shared/widgets/top_nav_bar.dart b/lib/shared/widgets/top_nav_bar.dart new file mode 100644 index 0000000..9071473 --- /dev/null +++ b/lib/shared/widgets/top_nav_bar.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../core/theme/app_typography.dart'; +import 'credits_badge.dart'; + +/// Top navigation bar - matches Pencil topNav +class TopNavBar extends StatelessWidget implements PreferredSizeWidget { + const TopNavBar({ + super.key, + required this.title, + this.credits, + this.showBackButton = false, + this.onBack, + this.onCreditsTap, + }); + + final String title; + final String? credits; + final bool showBackButton; + final VoidCallback? onBack; + final VoidCallback? onCreditsTap; + + @override + Size get preferredSize => const Size.fromHeight(56); + + @override + Widget build(BuildContext context) { + return Container( + height: 56, + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding), + decoration: BoxDecoration( + color: AppColors.surface, + boxShadow: [ + BoxShadow( + color: AppColors.shadowLight, + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (showBackButton) + GestureDetector( + onTap: onBack ?? () => Navigator.of(context).pop(), + behavior: HitTestBehavior.opaque, + child: SizedBox( + width: 40, + height: 40, + child: Center( + child: Icon( + LucideIcons.arrow_left, + size: 24, + color: AppColors.textPrimary, + ), + ), + ), + ) + else + const SizedBox(width: 40), + Text( + title, + style: AppTypography.navTitle, + ), + if (credits != null) + CreditsBadge(credits: credits!, onTap: onCreditsTap) + else + const SizedBox(width: 40), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..12e5b06 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,27 @@ +name: app_client +description: A new Flutter project. +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.6 + flutter_lucide: ^1.8.2 + google_fonts: ^6.2.1 + cached_network_image: ^3.3.1 + crypto: ^3.0.3 + device_info_plus: ^11.1.0 + encrypt: ^5.0.3 + http: ^1.2.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: + uses-material-design: true diff --git a/scripts/dev_proxy.js b/scripts/dev_proxy.js new file mode 100644 index 0000000..bf6a900 --- /dev/null +++ b/scripts/dev_proxy.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node +/** + * 本地转发代理:当手机无法直连 pre-ai.petsheroai.xyz 时使用 + * 电脑能访问域名,手机连电脑同一 WiFi,通过此代理转发请求 + * + * 使用: + * 1. 手机和电脑连同一 WiFi + * 2. 运行: node scripts/dev_proxy.js + * 3. 在 api_config.dart 中设置 debugBaseUrlOverride = 'http://<电脑IP>:8010' + * 4. 手机运行 app + * + * 获取电脑 IP: ifconfig (Mac/Linux) 或 ipconfig (Windows) + */ + +const http = require('http'); +const https = require('https'); + +const TARGET = 'https://pre-ai.petsheroai.xyz'; +const PORT = 8010; + +const server = http.createServer((req, res) => { + const url = TARGET + req.url; + console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} -> ${url}`); + + const options = { + hostname: 'pre-ai.petsheroai.xyz', + port: 443, + path: req.url, + method: req.method, + headers: { ...req.headers, host: 'pre-ai.petsheroai.xyz' }, + }; + + const proxy = https.request(options, (proxyRes) => { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.pipe(res); + }); + + proxy.on('error', (e) => { + console.error('Proxy error:', e.message); + res.writeHead(502); + res.end('Proxy error: ' + e.message); + }); + + req.pipe(proxy); +}); + +server.listen(PORT, '0.0.0.0', () => { + console.log(`Dev proxy listening on http://0.0.0.0:${PORT}`); + console.log(`Forwarding to ${TARGET}`); + console.log('Set debugBaseUrlOverride = "http://:8010" in api_config.dart'); +}); diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..c4d5991 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:app_client/app.dart'; + +void main() { + testWidgets('App launches and shows AI Video home content', (WidgetTester tester) async { + await tester.pumpWidget(const App()); + await tester.pumpAndSettle(); + expect(find.text('AI Video'), findsOneWidget); + expect(find.text('HOME'), findsOneWidget); + }); +}