更新:启动图和logo,登录还没成功的时候显示加载指示器

This commit is contained in:
ivan 2026-03-17 09:38:49 +08:00
parent 596fe05e09
commit eb72fb4979
77 changed files with 523 additions and 173 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#0a0a12</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#0a0a12</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 843 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -4082,10 +4082,12 @@
}, },
{ {
"type": "frame", "type": "frame",
"id": "OKMsh", "id": "RGAdu",
"name": "navSpacer", "name": "reportBtn",
"width": 40, "width": 40,
"height": 40 "height": 40,
"justifyContent": "center",
"alignItems": "center"
} }
] ]
}, },
@ -4117,12 +4119,62 @@
}, },
"cornerRadius": 16, "cornerRadius": 16,
"layout": "vertical", "layout": "vertical",
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "Ne80z",
"name": "topRow",
"width": "fill_container",
"height": 40,
"padding": [
16,
20,
0,
0
],
"justifyContent": "end",
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "h2IK3",
"name": "reportBtn",
"width": 44,
"height": 44,
"fill": "#00000099",
"cornerRadius": 8,
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "GtWHh",
"width": 24,
"height": 24,
"iconFontName": "triangle-alert",
"iconFontFamily": "lucide",
"fill": "#FFFFFF"
}
]
}
]
},
{
"type": "frame",
"id": "wD2sT",
"name": "centerGroup",
"width": "fill_container",
"layout": "vertical",
"gap": 8,
"justifyContent": "center", "justifyContent": "center",
"alignItems": "center", "alignItems": "center",
"children": [ "children": [
{ {
"type": "icon_font", "type": "icon_font",
"id": "a3uQs", "id": "a3uQs",
"enabled": false,
"width": 72, "width": 72,
"height": 72, "height": 72,
"iconFontName": "play", "iconFontName": "play",
@ -4133,13 +4185,39 @@
"type": "text", "type": "text",
"id": "SuXL2", "id": "SuXL2",
"fill": "#FFFFFF99", "fill": "#FFFFFF99",
"content": "Your video is ready", "content": "your work is ready",
"fontFamily": "Inter", "fontFamily": "Inter",
"fontSize": 14, "fontSize": 14,
"fontWeight": "500" "fontWeight": "500"
} }
] ]
}, },
{
"type": "frame",
"id": "nNSbD",
"name": "watermarkRow",
"width": "fill_container",
"padding": [
0,
20,
20,
0
],
"justifyContent": "end",
"children": [
{
"type": "text",
"id": "BqSHo",
"fill": "#FFFFFF99",
"content": "PetsHero AI",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "500"
}
]
}
]
},
{ {
"type": "frame", "type": "frame",
"id": "jCww0", "id": "jCww0",

View File

@ -1,7 +1,7 @@
{ {
// 下面的三个字段说明:A面都是 false,B面都是 true // 下面的三个字段说明:A面都是 false,B面都是 true
"need_wait": false, // 是否展示 Video 菜单 "need_wait": false, // 是否展示 Video 菜单
"safe_area": false, // 是否防止截屏 "safe_area": false, // 是否防止截屏;为 true 时启用系统截屏/录屏防护(Android FLAG_SECURE,iOS 检测)
"lucky": false, // 是否显示第三方支付 "lucky": false, // 是否显示第三方支付
"privacy": "https://www.petsheroai.xyz/privacy.html", "privacy": "https://www.petsheroai.xyz/privacy.html",
"agreement": "https://www.petsheroai.xyz/terms.html", "agreement": "https://www.petsheroai.xyz/terms.html",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
</subviews>
<color key="backgroundColor" red="0.04" green="0.04" blue="0.07" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchBackground" width="375" height="812"/>
</resources>
</document>

View File

@ -18,6 +18,12 @@
@import device_info_plus; @import device_info_plus;
#endif #endif
#if __has_include(<flutter_native_splash/FlutterNativeSplashPlugin.h>)
#import <flutter_native_splash/FlutterNativeSplashPlugin.h>
#else
@import flutter_native_splash;
#endif
#if __has_include(<gal/GalPlugin.h>) #if __has_include(<gal/GalPlugin.h>)
#import <gal/GalPlugin.h> #import <gal/GalPlugin.h>
#else #else
@ -36,6 +42,12 @@
@import in_app_purchase_storekit; @import in_app_purchase_storekit;
#endif #endif
#if __has_include(<screen_secure/ScreenSecurePlugin.h>)
#import <screen_secure/ScreenSecurePlugin.h>
#else
@import screen_secure;
#endif
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>) #if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
#import <shared_preferences_foundation/SharedPreferencesPlugin.h> #import <shared_preferences_foundation/SharedPreferencesPlugin.h>
#else #else
@ -77,9 +89,11 @@
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry { + (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[AdjustSdk registerWithRegistrar:[registry registrarForPlugin:@"AdjustSdk"]]; [AdjustSdk registerWithRegistrar:[registry registrarForPlugin:@"AdjustSdk"]];
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]]; [FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
[FlutterNativeSplashPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterNativeSplashPlugin"]];
[GalPlugin registerWithRegistrar:[registry registrarForPlugin:@"GalPlugin"]]; [GalPlugin registerWithRegistrar:[registry registrarForPlugin:@"GalPlugin"]];
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]]; [FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
[InAppPurchasePlugin registerWithRegistrar:[registry registrarForPlugin:@"InAppPurchasePlugin"]]; [InAppPurchasePlugin registerWithRegistrar:[registry registrarForPlugin:@"InAppPurchasePlugin"]];
[ScreenSecurePlugin registerWithRegistrar:[registry registrarForPlugin:@"ScreenSecurePlugin"]];
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]]; [URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@ -47,5 +47,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
</dict> <key>UIStatusBarHidden</key>
<false/>
</dict>
</plist> </plist>

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'core/auth/auth_service.dart';
import 'core/theme/app_colors.dart';
import 'core/theme/app_theme.dart'; import 'core/theme/app_theme.dart';
import 'core/user/user_state.dart'; import 'core/user/user_state.dart';
import 'features/gallery/gallery_screen.dart'; import 'features/gallery/gallery_screen.dart';
@ -36,12 +38,37 @@ class _AppState extends State<App> {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
initialRoute: '/', initialRoute: '/',
builder: (context, child) { builder: (context, child) {
return SafeArea( return Stack(
fit: StackFit.expand,
children: [
SafeArea(
top: true, top: true,
left: false, left: false,
right: false, right: false,
bottom: false, bottom: false,
child: child ?? const SizedBox.shrink(), child: child ?? const SizedBox.shrink(),
),
FutureBuilder<void>(
future: AuthService.loginComplete,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return const SizedBox.shrink();
}
return Positioned.fill(
child: AbsorbPointer(
child: Container(
color: Colors.black.withValues(alpha: 0.2),
child: const Center(
child: CircularProgressIndicator(
color: AppColors.primary,
),
),
),
),
);
},
),
],
); );
}, },
routes: { routes: {

View File

@ -12,7 +12,8 @@ abstract final class ApiConfig {
static const String packageName = 'com.petsheroai.app'; static const String packageName = 'com.petsheroai.app';
/// 预发环境域名 /// 预发环境域名
static const String preBaseUrl = 'https://pre-ai.petsheroai.xyz'; static const String preBaseUrl =
'https://ai.petsheroai.xyz'; //'https://pre-ai.petsheroai.xyz';
/// 生产环境域名 /// 生产环境域名
static const String prodBaseUrl = 'https://ai.petsheroai.xyz'; static const String prodBaseUrl = 'https://ai.petsheroai.xyz';

View File

@ -4,6 +4,7 @@ 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:screen_secure/screen_secure.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../adjust/adjust_events.dart'; import '../adjust/adjust_events.dart';
@ -23,6 +24,9 @@ class AuthService {
static Future<void>? _loginFuture; static Future<void>? _loginFuture;
/// 登录是否已完成,用于 UI 控制(如登录中隐藏页面加载指示器)
static final ValueNotifier<bool> isLoginComplete = ValueNotifier(false);
/// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求 /// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value(); static Future<void> get loginComplete => _loginFuture ?? Future<void>.value();
@ -52,6 +56,28 @@ class AuthService {
return digest.toString().toUpperCase(); return digest.toString().toUpperCase();
} }
/// 根据 extConfig.safe_area 启用/禁用系统截屏防护
static Future<void> _applyScreenSecure(bool? safeArea) async {
if (defaultTargetPlatform != TargetPlatform.android &&
defaultTargetPlatform != TargetPlatform.iOS) {
return;
}
try {
await ScreenSecure.init(screenshotBlock: false, screenRecordBlock: false);
if (safeArea == true) {
await ScreenSecure.enableScreenshotBlock();
await ScreenSecure.enableScreenRecordBlock();
_logMsg('safe_area=true: 已启用截屏/录屏防护');
} else {
await ScreenSecure.disableScreenshotBlock();
await ScreenSecure.disableScreenRecordBlock();
_logMsg('safe_area=$safeArea: 已关闭截屏/录屏防护');
}
} on ScreenSecureException catch (e) {
_logMsg('ScreenSecure 设置失败: ${e.message}');
}
}
/// 将 common_info 响应保存到全局,并解析 surge 中的 lucky(是否开启三方支付) /// 将 common_info 响应保存到全局,并解析 surge 中的 lucky(是否开启三方支付)
static void _saveCommonInfoToState(Map<String, dynamic> data) { static void _saveCommonInfoToState(Map<String, dynamic> data) {
final reveal = data['reveal'] as int?; final reveal = data['reveal'] as int?;
@ -70,13 +96,16 @@ class AuthService {
if (surge != null) { if (surge != null) {
final enable = surge['lucky'] as bool?; final enable = surge['lucky'] as bool?;
UserState.setEnableThirdPartyPayment(enable); UserState.setEnableThirdPartyPayment(enable);
// extConfig:need_wait = 是否展示 Video 菜单,items = 图片列表(见 docs/extConfig.md) // extConfig:need_wait = 是否展示 Video 菜单,safe_area = 是否防止截屏,items = 图片列表(见 docs/extConfig.md)
final needWait = surge['need_wait'] as bool?; final needWait = surge['need_wait'] as bool?;
final safeArea = surge['safe_area'] as bool?;
final items = surge['items'] as List<dynamic>?; final items = surge['items'] as List<dynamic>?;
UserState.setExtConfig( UserState.setExtConfig(
needShowVideoMenuValue: needWait, needShowVideoMenuValue: needWait,
safeAreaValue: safeArea,
items: items, items: items,
); );
_applyScreenSecure(safeArea);
} }
} catch (e) { } catch (e) {
_logMsg('surge JSON 解析失败: $e'); _logMsg('surge JSON 解析失败: $e');
@ -224,7 +253,10 @@ class AuthService {
_logMsg('init: 异常 $e'); _logMsg('init: 异常 $e');
_logMsg('init: 堆栈 $st'); _logMsg('init: 堆栈 $st');
} finally { } finally {
if (!completer.isCompleted) completer.complete(); if (!completer.isCompleted) {
completer.complete();
isLoginComplete.value = true;
}
} }
} }
} }

View File

@ -16,6 +16,8 @@ class UserState {
/// 是否展示 Video 分类栏(来自 common_info surge.need_wait,见 docs/extConfig.md) /// 是否展示 Video 分类栏(来自 common_info surge.need_wait,见 docs/extConfig.md)
static final ValueNotifier<bool?> needShowVideoMenu = ValueNotifier<bool?>(null); static final ValueNotifier<bool?> needShowVideoMenu = ValueNotifier<bool?>(null);
/// 是否防止截屏(来自 common_info surge.safe_area,见 docs/extConfig.md)
static final ValueNotifier<bool?> safeArea = ValueNotifier<bool?>(null);
/// extConfig.items 图片列表(来自 common_info surge.items) /// extConfig.items 图片列表(来自 common_info surge.items)
static final ValueNotifier<List<dynamic>?> extConfigItems = ValueNotifier<List<dynamic>?>(null); static final ValueNotifier<List<dynamic>?> extConfigItems = ValueNotifier<List<dynamic>?>(null);
@ -43,8 +45,13 @@ class UserState {
enableThirdPartyPayment.value = value; enableThirdPartyPayment.value = value;
} }
static void setExtConfig({bool? needShowVideoMenuValue, List<dynamic>? items}) { static void setExtConfig({
bool? needShowVideoMenuValue,
bool? safeAreaValue,
List<dynamic>? items,
}) {
if (needShowVideoMenuValue != null) needShowVideoMenu.value = needShowVideoMenuValue; if (needShowVideoMenuValue != null) needShowVideoMenu.value = needShowVideoMenuValue;
if (safeAreaValue != null) safeArea.value = safeAreaValue;
if (items != null) extConfigItems.value = items; if (items != null) extConfigItems.value = items;
} }

View File

@ -18,7 +18,7 @@ import '../../features/gallery/models/gallery_task_item.dart';
import '../../shared/tab_selector_scope.dart'; import '../../shared/tab_selector_scope.dart';
import '../../shared/widgets/bottom_nav_bar.dart'; import '../../shared/widgets/bottom_nav_bar.dart';
/// Progress states: 1=队列中 2=处理中 3=完成 4=超时 5=错误 6=中止 /// Progress states: 1=Queued 2=Processing 3=Completed 4=Timeout 5=Error 6=Aborted
/// Progress bar has 3 stages; states 3–6 are stage 3. /// Progress bar has 3 stages; states 3–6 are stage 3.
const _stateLabels = <int, String>{ const _stateLabels = <int, String>{
1: 'Queued', 1: 'Queued',
@ -72,6 +72,7 @@ class GenerateProgressScreen extends StatefulWidget {
class _GenerateProgressScreenState extends State<GenerateProgressScreen> { class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
int? _state; int? _state;
Timer? _pollTimer; Timer? _pollTimer;
bool _isFetching = false;
double get _progress => _progressForState(_state); double get _progress => _progressForState(_state);
@ -104,7 +105,7 @@ class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
Future<void> _startPolling() async { Future<void> _startPolling() async {
await AuthService.loginComplete; await AuthService.loginComplete;
_pollTimer = Timer.periodic( _pollTimer = Timer.periodic(
const Duration(seconds: 1), const Duration(seconds: 5),
(_) => _fetchProgress(), (_) => _fetchProgress(),
); );
_fetchProgress(); _fetchProgress();
@ -112,7 +113,9 @@ class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
Future<void> _fetchProgress() async { Future<void> _fetchProgress() async {
if (widget.taskId == null) return; if (widget.taskId == null) return;
if (_isFetching) return;
_isFetching = true;
try { try {
final res = await ImageApi.getProgress( final res = await ImageApi.getProgress(
sentinel: ApiConfig.appId, sentinel: ApiConfig.appId,
@ -163,11 +166,14 @@ class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
break; break;
} }
} catch (_) {} } catch (_) {}
finally {
_isFetching = false;
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labelText = _stateLabels[_state] ?? '队列中'; final labelText = _stateLabels[_state] ?? 'Queued';
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
@ -250,7 +256,7 @@ class _VideoPreview extends StatelessWidget {
} }
} }
/// Progress bar: 3 stages. Label Eghqc shows state (队列中|处理中|完成|超时|错误|中止) /// Progress bar: 3 stages. Label shows state (Queued|Processing|Completed|Timeout|Error|Aborted)
class _ProgressSection extends StatelessWidget { class _ProgressSection extends StatelessWidget {
const _ProgressSection({required this.progress, required this.label}); const _ProgressSection({required this.progress, required this.label});

View File

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -179,12 +178,13 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
throw Exception('User not logged in'); throw Exception('User not logged in');
} }
final templateName = widget.task?.templateName ?? '';
final createRes = await ImageApi.createTask( final createRes = await ImageApi.createTask(
asset: userId, asset: userId,
guild: filePath, guild: filePath,
allowance: false, allowance: false,
cipher: widget.task?.taskType ?? '', cipher: widget.task?.taskType ?? '',
congregation: widget.task?.templateName ?? '', congregation: templateName == 'BananaTask' ? null : templateName,
heatmap: _heatmap, heatmap: _heatmap,
ext: widget.task?.ext, ext: widget.task?.ext,
); );

View File

@ -192,7 +192,10 @@ class _MediaDisplay extends StatelessWidget {
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: videoUrl != null && videoController != null child: Stack(
fit: StackFit.expand,
children: [
videoUrl != null && videoController != null
? _VideoPlayer( ? _VideoPlayer(
controller: videoController!, controller: videoController!,
) )
@ -221,6 +224,42 @@ class _MediaDisplay extends StatelessWidget {
errorWidget: (_, __, ___) => _Placeholder(), errorWidget: (_, __, ___) => _Placeholder(),
) )
: _Placeholder(), : _Placeholder(),
Positioned(
top: 16,
right: 20,
child: GestureDetector(
onTap: () {
// TODO: Report action
},
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Icon(
LucideIcons.triangle_alert,
size: 24,
color: AppColors.surface,
),
),
),
),
Positioned(
bottom: 20,
right: 20,
child: Text(
'PetsHero AI',
style: AppTypography.bodyMedium.copyWith(
color: AppColors.surface.withValues(alpha: 0.6),
fontSize: 12,
),
),
),
],
),
), ),
); );
} }
@ -263,11 +302,15 @@ class _VideoPlayerState extends State<_VideoPlayer> {
); );
} }
return Stack( return GestureDetector(
fit: StackFit.expand, onTap: () {
alignment: Alignment.center, if (widget.controller.value.isPlaying) {
children: [ widget.controller.pause();
FittedBox( } else {
widget.controller.play();
}
},
child: FittedBox(
fit: BoxFit.contain, fit: BoxFit.contain,
child: SizedBox( child: SizedBox(
width: widget.controller.value.size.width > 0 width: widget.controller.value.size.width > 0
@ -279,33 +322,6 @@ class _VideoPlayerState extends State<_VideoPlayer> {
child: VideoPlayer(widget.controller), child: VideoPlayer(widget.controller),
), ),
), ),
Center(
child: GestureDetector(
onTap: () {
if (widget.controller.value.isPlaying) {
widget.controller.pause();
} else {
widget.controller.play();
}
},
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.surface.withValues(alpha: 0.8),
shape: BoxShape.circle,
),
child: Icon(
widget.controller.value.isPlaying
? LucideIcons.pause
: LucideIcons.play,
size: 32,
color: AppColors.textPrimary,
),
),
),
),
],
); );
} }
} }
@ -315,23 +331,13 @@ class _Placeholder extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
color: AppColors.textPrimary, color: AppColors.textPrimary,
child: Column( alignment: Alignment.center,
mainAxisAlignment: MainAxisAlignment.center, child: Text(
children: [
Icon(
LucideIcons.play,
size: 72,
color: AppColors.surface.withValues(alpha: 0.5),
),
const SizedBox(height: AppSpacing.lg),
Text(
'Your work is ready', 'Your work is ready',
style: AppTypography.bodyRegular.copyWith( style: AppTypography.bodyRegular.copyWith(
color: AppColors.surface.withValues(alpha: 0.6), color: AppColors.surface.withValues(alpha: 0.6),
), ),
), ),
],
),
); );
} }
} }

View File

@ -38,6 +38,7 @@ class _HomeScreenState extends State<HomeScreen> {
super.initState(); super.initState();
UserState.needShowVideoMenu.addListener(_onExtConfigChanged); UserState.needShowVideoMenu.addListener(_onExtConfigChanged);
UserState.extConfigItems.addListener(_onExtConfigChanged); UserState.extConfigItems.addListener(_onExtConfigChanged);
AuthService.isLoginComplete.addListener(_onExtConfigChanged);
_loadCategories(); _loadCategories();
if (widget.isActive) refreshAccount(); if (widget.isActive) refreshAccount();
} }
@ -46,6 +47,7 @@ class _HomeScreenState extends State<HomeScreen> {
void dispose() { void dispose() {
UserState.needShowVideoMenu.removeListener(_onExtConfigChanged); UserState.needShowVideoMenu.removeListener(_onExtConfigChanged);
UserState.extConfigItems.removeListener(_onExtConfigChanged); UserState.extConfigItems.removeListener(_onExtConfigChanged);
AuthService.isLoginComplete.removeListener(_onExtConfigChanged);
super.dispose(); super.dispose();
} }
@ -75,6 +77,24 @@ class _HomeScreenState extends State<HomeScreen> {
.toList(); .toList();
} }
/// 是否处于首次加载中(分类/任务/extConfig 尚未就绪)
/// 登录未完成时不显示页面加载指示器,由登录遮罩负责
bool get _isListLoading {
if (!AuthService.isLoginComplete.value) return false;
if (_showVideoMenu) {
if (_categoriesLoading) return true;
if (_selectedCategory?.id == kExtCategoryId) {
return UserState.extConfigItems.value == null;
}
return _tasksLoading;
}
return UserState.extConfigItems.value == null;
}
/// 分类栏加载中时是否显示加载指示器(登录未完成时不显示)
bool get _showCategoriesLoading =>
AuthService.isLoginComplete.value && _categoriesLoading;
/// 当前列表:need_wait false 时用 extConfig.items;true 且选中固定分类时用 extConfig.items;否则用 _tasks /// 当前列表:need_wait false 时用 extConfig.items;true 且选中固定分类时用 extConfig.items;否则用 _tasks
List<TaskItem> get _displayTasks { List<TaskItem> get _displayTasks {
if (!_showVideoMenu) { if (!_showVideoMenu) {
@ -187,7 +207,7 @@ class _HomeScreenState extends State<HomeScreen> {
horizontal: AppSpacing.screenPadding, horizontal: AppSpacing.screenPadding,
vertical: AppSpacing.xs, vertical: AppSpacing.xs,
), ),
child: _categoriesLoading child: _showCategoriesLoading
? const SizedBox( ? const SizedBox(
height: 40, height: 40,
child: Center(child: CircularProgressIndicator())) child: Center(child: CircularProgressIndicator()))
@ -198,9 +218,7 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
), ),
Expanded( Expanded(
child: _showVideoMenu && child: _isListLoading
_selectedCategory?.id != kExtCategoryId &&
_tasksLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: LayoutBuilder( : LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {

View File

@ -23,9 +23,9 @@ void main() async {
statusBarBrightness: Brightness.light, statusBarBrightness: Brightness.light,
), ),
); );
runApp(const App()); // 先启动登录,确保首次构建时 loginComplete 可被监听
// APP 打开时后台执行快速登录
AuthService.init(); AuthService.init();
runApp(const App());
// 尽早订阅 purchaseStream,否则未确认订单不会出现在 queryPastPurchases 中,补单会为空 // 尽早订阅 purchaseStream,否则未确认订单不会出现在 queryPastPurchases 中,补单会为空
if (defaultTargetPlatform == TargetPlatform.android) { if (defaultTargetPlatform == TargetPlatform.android) {
GooglePlayPurchaseService.startPendingPurchaseListener(); GooglePlayPurchaseService.startPendingPurchaseListener();

View File

@ -1,7 +1,7 @@
name: pets_hero_ai name: pets_hero_ai
description: PetsHero AI Application. description: PetsHero AI Application.
publish_to: 'none' publish_to: 'none'
version: 1.0.4+5 version: 1.0.5+6
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'
@ -30,18 +30,31 @@ dependencies:
url_launcher: ^6.2.5 url_launcher: ^6.2.5
in_app_purchase: ^3.2.0 in_app_purchase: ^3.2.0
webview_flutter: ^4.10.0 webview_flutter: ^4.10.0
screen_secure: ^1.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^3.0.0 flutter_lints: ^3.0.0
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
flutter_native_splash: ^2.4.7
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true
ios: true ios: false # iOS xcodeproj not present; run with ios:true after adding iOS project
image_path: "design/images/generated-1773069485332.png" image_path: "assets/images/logo.png"
remove_alpha_ios: true remove_alpha_ios: true
flutter_native_splash:
background_image: "assets/images/splash.png"
android: true
ios: true
# Android 12+ 使用不同 API,不支持全屏背景图,需单独配置
android_12:
color: "#0a0a12"
image: "assets/images/logo.png"
flutter: flutter:
uses-material-design: true uses-material-design: true
assets:
- assets/images/