优化:应用设计图更改
@ -2,6 +2,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
<application
|
<application
|
||||||
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||||
android:label="PetsHero"
|
android:label="PetsHero"
|
||||||
|
|||||||
@ -21,8 +21,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.9.1" apply false
|
id "com.android.application" version "8.7.3" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|||||||
26
design/exports/EXPORT_GUIDE.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# 设计图导出指南
|
||||||
|
|
||||||
|
将 `pencil-app-client.pen` 中的各页面导出为 PNG 图片。
|
||||||
|
|
||||||
|
## 导出步骤
|
||||||
|
|
||||||
|
1. 在 Pencil 中打开 `design/pencil-app-client.pen`
|
||||||
|
2. 在画布或图层面板中选中对应 Frame(可按 Node ID 查找)
|
||||||
|
3. 右键 → **Export** → 选择 **PNG**
|
||||||
|
4. 保存到 `design/exports/` 目录,使用下方对应文件名
|
||||||
|
|
||||||
|
## 页面与 Node ID 对照表
|
||||||
|
|
||||||
|
| 页面 | Node ID | 导出文件名 |
|
||||||
|
|------|---------|------------|
|
||||||
|
| AI Video App (Home) | `bi8Au` | `home.png` |
|
||||||
|
| Gallery Screen | `hpwBg` | `gallery.png` |
|
||||||
|
| Profile Screen | `KXeow` | `profile.png` |
|
||||||
|
| Recharge Screen | `tPjdN` | `recharge.png` |
|
||||||
|
| Generate Video Screen | `mmLB5` | `generate_video.png` |
|
||||||
|
| Generate Video Progress Screen | `qGs6n` | `generate_progress.png` |
|
||||||
|
| Video Generation Result Screen | `cFA4T` | `result.png` |
|
||||||
|
|
||||||
|
## 快速查找 Node ID
|
||||||
|
|
||||||
|
在 Pencil 中选中 Frame 后,可在属性面板或图层信息中查看其 Node ID。
|
||||||
BIN
design/exports/app设计图/AI Video App.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
design/exports/app设计图/Gallery Screen.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
design/exports/app设计图/Generate Video Progress Screen.png
Normal file
|
After Width: | Height: | Size: 893 KiB |
BIN
design/exports/app设计图/Generate Video Screen.png
Normal file
|
After Width: | Height: | Size: 843 KiB |
BIN
design/exports/app设计图/Profile Screen.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
design/exports/app设计图/Recharge Screen.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
design/exports/app设计图/Video Generation Result Screen.png
Normal file
|
After Width: | Height: | Size: 858 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
BIN
design/images/generated-1773134913667.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
design/images/generated-1773156157712.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
design/images/generated-1773156213563.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
design/images/generated-1773156252029.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
design/images/generated-1773156289353.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
design/images/generated-1773156316903.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
design/images/generated-1773156352910.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
design/images/generated-1773156363524.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
design/images/generated-1773156393690.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
design/images/generated-1773156472555.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
design/images/generated-1773156511265.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
design/images/pet-hero-1.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
39
docs/adjuest.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
PetsHero AI-and
|
||||||
|
APP应用识别码:2z2mly0afgqo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 购买档位:
|
||||||
|
对应UI在充值页面 recharge_screen.dart
|
||||||
|
|事件名称|识别码|
|
||||||
|
|:------:|:-----:|
|
||||||
|
|19.99 |m0r9u9|
|
||||||
|
|49.99 |aht1ve|
|
||||||
|
|5.99 |47hhx9|
|
||||||
|
|9.99 |w775gd|
|
||||||
|
|99.99 |y7994m|
|
||||||
|
## 首日充值
|
||||||
|
用户注册当天的所有充值判定为首充
|
||||||
|
|
||||||
|
|事件名称|识别码|
|
||||||
|
|:------:|:-----:|
|
||||||
|
|first purchase |r6w4bi|
|
||||||
|
## 支付失败
|
||||||
|
|事件名称|识别码|
|
||||||
|
|:------:|:-----:|
|
||||||
|
|Order abnormal |4txlo1|
|
||||||
|
## 支付成功
|
||||||
|
如果是首日充值需要额外上报first purchase事件
|
||||||
|
|事件名称|识别码|
|
||||||
|
|:------:|:-----:|
|
||||||
|
|Purchase |ek780r|
|
||||||
|
## 注册
|
||||||
|
用户登录和注册调用的是同一个接口,当第一次调用的时候为注册事件
|
||||||
|
|事件名称|识别码|
|
||||||
|
|:------:|:-----:|
|
||||||
|
|register | 2z3dm4|
|
||||||
|
## 其他
|
||||||
|
|事件名称|识别码|
|
||||||
|
|:------:|:-----:|
|
||||||
|
|PetsHero AI Monthly VIP |96o5ez|
|
||||||
|
|PetsHero AI Weekly VIP |95yg4o|
|
||||||
@ -30,6 +30,12 @@
|
|||||||
@import image_picker_ios;
|
@import image_picker_ios;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
|
||||||
|
#import <shared_preferences_foundation/SharedPreferencesPlugin.h>
|
||||||
|
#else
|
||||||
|
@import shared_preferences_foundation;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<sqflite_darwin/SqflitePlugin.h>)
|
#if __has_include(<sqflite_darwin/SqflitePlugin.h>)
|
||||||
#import <sqflite_darwin/SqflitePlugin.h>
|
#import <sqflite_darwin/SqflitePlugin.h>
|
||||||
#else
|
#else
|
||||||
@ -55,6 +61,7 @@
|
|||||||
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
|
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
|
||||||
[GalPlugin registerWithRegistrar:[registry registrarForPlugin:@"GalPlugin"]];
|
[GalPlugin registerWithRegistrar:[registry registrarForPlugin:@"GalPlugin"]];
|
||||||
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
||||||
|
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
||||||
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
||||||
[FVPVideoPlayerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FVPVideoPlayerPlugin"]];
|
[FVPVideoPlayerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FVPVideoPlayerPlugin"]];
|
||||||
[VideoThumbnailPlugin registerWithRegistrar:[registry registrarForPlugin:@"VideoThumbnailPlugin"]];
|
[VideoThumbnailPlugin registerWithRegistrar:[registry registrarForPlugin:@"VideoThumbnailPlugin"]];
|
||||||
|
|||||||
112
lib/core/adjust/adjust_events.dart
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import 'package:adjust_sdk/adjust.dart';
|
||||||
|
import 'package:adjust_sdk/adjust_event.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// Adjust 事件埋点(识别码见 docs/adjuest.md)
|
||||||
|
abstract final class AdjustEvents {
|
||||||
|
// 购买档位(充值页选择档位时上报)
|
||||||
|
static const String tier1999 = 'm0r9u9'; // 19.99
|
||||||
|
static const String tier4999 = 'aht1ve'; // 49.99
|
||||||
|
static const String tier599 = '47hhx9'; // 5.99
|
||||||
|
static const String tier999 = 'w775gd'; // 9.99
|
||||||
|
static const String tier9999 = 'y7994m'; // 99.99
|
||||||
|
|
||||||
|
// 首日充值
|
||||||
|
static const String firstPurchase = 'r6w4bi';
|
||||||
|
|
||||||
|
// 支付失败
|
||||||
|
static const String orderAbnormal = '4txlo1';
|
||||||
|
|
||||||
|
// 支付成功
|
||||||
|
static const String purchase = 'ek780r';
|
||||||
|
|
||||||
|
// 注册(首次调用 fast_login 成功时)
|
||||||
|
static const String register = '2z3dm4';
|
||||||
|
|
||||||
|
// 其他
|
||||||
|
static const String monthlyVip = '96o5ez'; // PetsHero AI Monthly VIP
|
||||||
|
static const String weeklyVip = '95yg4o'; // PetsHero AI Weekly VIP
|
||||||
|
|
||||||
|
/// 根据金额(如 19.99, 9.99)返回档位事件 token,无法匹配时返回 purchase
|
||||||
|
static String? tierTokenFromPrice(num price) {
|
||||||
|
final p = price.toStringAsFixed(2);
|
||||||
|
switch (p) {
|
||||||
|
case '5.99':
|
||||||
|
return tier599;
|
||||||
|
case '9.99':
|
||||||
|
return tier999;
|
||||||
|
case '19.99':
|
||||||
|
return tier1999;
|
||||||
|
case '49.99':
|
||||||
|
return tier4999;
|
||||||
|
case '99.99':
|
||||||
|
return tier9999;
|
||||||
|
default:
|
||||||
|
return purchase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从金额字符串解析数字(如 "¥19.99" / "\$19.99")
|
||||||
|
static num? parsePrice(String amount) {
|
||||||
|
final match = RegExp(r'[\d.]+').firstMatch(amount);
|
||||||
|
if (match == null) return null;
|
||||||
|
return num.tryParse(match.group(0)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _track(String eventToken) {
|
||||||
|
Adjust.trackEvent(AdjustEvent(eventToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 购买档位(用户点击某档位购买时)
|
||||||
|
static void trackTier(String eventToken) {
|
||||||
|
_track(eventToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 首日充值
|
||||||
|
static void trackFirstPurchase() {
|
||||||
|
_track(firstPurchase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 支付失败
|
||||||
|
static void trackOrderAbnormal() {
|
||||||
|
_track(orderAbnormal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 支付成功(若为首日充值需额外调用 trackFirstPurchase)
|
||||||
|
static void trackPurchase() {
|
||||||
|
_track(purchase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 注册(首次 fast_login 成功)
|
||||||
|
static void trackRegister() {
|
||||||
|
_track(register);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PetsHero AI Monthly VIP
|
||||||
|
static void trackMonthlyVip() {
|
||||||
|
_track(monthlyVip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PetsHero AI Weekly VIP
|
||||||
|
static void trackWeeklyVip() {
|
||||||
|
_track(weeklyVip);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const String _keyRegisterDate = 'adjust_register_date';
|
||||||
|
|
||||||
|
/// 支付成功时调用:上报 Purchase,若为首日充值则同时上报 first purchase
|
||||||
|
static Future<void> trackPurchaseSuccess() async {
|
||||||
|
_track(purchase);
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final registerDate = prefs.getString(_keyRegisterDate);
|
||||||
|
if (registerDate != null &&
|
||||||
|
registerDate == DateTime.now().toIso8601String().substring(0, 10)) {
|
||||||
|
_track(firstPurchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 支付失败时调用
|
||||||
|
static void trackPaymentFailed() {
|
||||||
|
_track(orderAbnormal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,9 @@ import 'dart:convert';
|
|||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../adjust/adjust_events.dart';
|
||||||
import '../api/api_client.dart';
|
import '../api/api_client.dart';
|
||||||
import '../api/proxy_client.dart';
|
import '../api/proxy_client.dart';
|
||||||
import '../api/services/user_api.dart';
|
import '../api/services/user_api.dart';
|
||||||
@ -108,6 +110,17 @@ class AuthService {
|
|||||||
} else {
|
} else {
|
||||||
_log('init: 响应中无 reevaluate (userToken)');
|
_log('init: 响应中无 reevaluate (userToken)');
|
||||||
}
|
}
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final hadLoggedIn = prefs.getBool('adjust_has_logged_in') ?? false;
|
||||||
|
if (!hadLoggedIn) {
|
||||||
|
AdjustEvents.trackRegister();
|
||||||
|
await prefs.setBool('adjust_has_logged_in', true);
|
||||||
|
await prefs.setString(
|
||||||
|
'adjust_register_date',
|
||||||
|
DateTime.now().toIso8601String().substring(0, 10),
|
||||||
|
);
|
||||||
|
_log('init: 首次登录,已上报 register');
|
||||||
|
}
|
||||||
final credits = data?['reveal'] as int?;
|
final credits = data?['reveal'] as int?;
|
||||||
if (credits != null) {
|
if (credits != null) {
|
||||||
UserState.setCredits(credits);
|
UserState.setCredits(credits);
|
||||||
|
|||||||
@ -88,7 +88,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(56),
|
preferredSize: const Size.fromHeight(56),
|
||||||
child: TopNavBar(
|
child: TopNavBar(
|
||||||
title: 'AI Video',
|
title: 'PetsHero AI',
|
||||||
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||||
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
||||||
),
|
),
|
||||||
@ -101,7 +101,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
vertical: AppSpacing.xs,
|
vertical: AppSpacing.xs,
|
||||||
),
|
),
|
||||||
child: _categoriesLoading
|
child: _categoriesLoading
|
||||||
? const SizedBox(height: 40, child: Center(child: CircularProgressIndicator()))
|
? const SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: Center(child: CircularProgressIndicator()))
|
||||||
: HomeTabRow(
|
: HomeTabRow(
|
||||||
categories: _categories,
|
categories: _categories,
|
||||||
selectedId: _selectedCategory?.id ?? -1,
|
selectedId: _selectedCategory?.id ?? -1,
|
||||||
@ -137,7 +139,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
? task.credits480p.toString()
|
? task.credits480p.toString()
|
||||||
: '50';
|
: '50';
|
||||||
return VideoCard(
|
return VideoCard(
|
||||||
imageUrl: task.previewImageUrl ?? _placeholderImage,
|
imageUrl:
|
||||||
|
task.previewImageUrl ?? _placeholderImage,
|
||||||
videoUrl: task.previewVideoUrl,
|
videoUrl: task.previewVideoUrl,
|
||||||
credits: credits,
|
credits: credits,
|
||||||
isActive: _activeCardIndex == index,
|
isActive: _activeCardIndex == index,
|
||||||
@ -162,5 +165,4 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
|
|
||||||
|
import '../../core/adjust/adjust_events.dart';
|
||||||
import '../../core/api/services/payment_api.dart';
|
import '../../core/api/services/payment_api.dart';
|
||||||
import '../../core/auth/auth_service.dart';
|
import '../../core/auth/auth_service.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
@ -80,7 +81,20 @@ class _RechargeScreenState extends State<RechargeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onBuy(ActivityItem item) {
|
void _onBuy(ActivityItem item) {
|
||||||
// TODO: 创建支付订单
|
final price = AdjustEvents.parsePrice(item.actualAmount);
|
||||||
|
if (price != null) {
|
||||||
|
final tierToken = AdjustEvents.tierTokenFromPrice(price);
|
||||||
|
if (tierToken != null) {
|
||||||
|
AdjustEvents.trackTier(tierToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final titleLower = item.title.toLowerCase();
|
||||||
|
if (titleLower.contains('monthly vip')) {
|
||||||
|
AdjustEvents.trackMonthlyVip();
|
||||||
|
} else if (titleLower.contains('weekly vip')) {
|
||||||
|
AdjustEvents.trackWeeklyVip();
|
||||||
|
}
|
||||||
|
// TODO: 创建支付订单;成功时调用 AdjustEvents.trackPurchaseSuccess(),失败时调用 AdjustEvents.trackPaymentFailed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -24,6 +24,7 @@ dependencies:
|
|||||||
video_thumbnail: ^0.5.3
|
video_thumbnail: ^0.5.3
|
||||||
gal: ^2.3.0
|
gal: ^2.3.0
|
||||||
path_provider: ^2.1.2
|
path_provider: ^2.1.2
|
||||||
|
shared_preferences: ^2.2.2
|
||||||
flutter_cache_manager: ^3.3.1
|
flutter_cache_manager: ^3.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||