更新:启动图和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"?>
<resources>
<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 name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<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"?>
<resources>
<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 name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<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",
"id": "OKMsh",
"name": "navSpacer",
"id": "RGAdu",
"name": "reportBtn",
"width": 40,
"height": 40
"height": 40,
"justifyContent": "center",
"alignItems": "center"
}
]
},
@ -4117,26 +4119,102 @@
},
"cornerRadius": 16,
"layout": "vertical",
"justifyContent": "center",
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "a3uQs",
"width": 72,
"height": 72,
"iconFontName": "play",
"iconFontFamily": "lucide",
"fill": "#FFFFFF80"
"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": "text",
"id": "SuXL2",
"fill": "#FFFFFF99",
"content": "Your video is ready",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "500"
"type": "frame",
"id": "wD2sT",
"name": "centerGroup",
"width": "fill_container",
"layout": "vertical",
"gap": 8,
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "a3uQs",
"enabled": false,
"width": 72,
"height": 72,
"iconFontName": "play",
"iconFontFamily": "lucide",
"fill": "#FFFFFF80"
},
{
"type": "text",
"id": "SuXL2",
"fill": "#FFFFFF99",
"content": "your work is ready",
"fontFamily": "Inter",
"fontSize": 14,
"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"
}
]
}
]
},

View File

@ -1,7 +1,7 @@
{
// 下面的三个字段说明A面都是 falseB面都是 true
"need_wait": false, // 是否展示 Video 菜单
"safe_area": false, // 是否防止截屏
"safe_area": false, // 是否防止截屏;为 true 时启用系统截屏/录屏防护Android FLAG_SECUREiOS 检测)
"lucky": false, // 是否显示第三方支付
"privacy": "https://www.petsheroai.xyz/privacy.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;
#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>)
#import <gal/GalPlugin.h>
#else
@ -36,6 +42,12 @@
@import in_app_purchase_storekit;
#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>)
#import <shared_preferences_foundation/SharedPreferencesPlugin.h>
#else
@ -77,9 +89,11 @@
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[AdjustSdk registerWithRegistrar:[registry registrarForPlugin:@"AdjustSdk"]];
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
[FlutterNativeSplashPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterNativeSplashPlugin"]];
[GalPlugin registerWithRegistrar:[registry registrarForPlugin:@"GalPlugin"]];
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
[InAppPurchasePlugin registerWithRegistrar:[registry registrarForPlugin:@"InAppPurchasePlugin"]];
[ScreenSecurePlugin registerWithRegistrar:[registry registrarForPlugin:@"ScreenSecurePlugin"]];
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];

View File

@ -1,51 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>PetsHero AI</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos to use as reference for AI image generation.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need permission to save videos and images to your photo library.</string>
<key>NSCameraUsageDescription</key>
<string>We need camera access to capture reference images for AI generation.</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>PetsHero AI</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>PetsHero AI</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos to use as reference for AI image generation.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need permission to save videos and images to your photo library.</string>
<key>NSCameraUsageDescription</key>
<string>We need camera access to capture reference images for AI generation.</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>PetsHero AI</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
</dict>
</plist>

View File

@ -1,5 +1,7 @@
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/user/user_state.dart';
import 'features/gallery/gallery_screen.dart';
@ -36,12 +38,37 @@ class _AppState extends State<App> {
debugShowCheckedModeBanner: false,
initialRoute: '/',
builder: (context, child) {
return SafeArea(
top: true,
left: false,
right: false,
bottom: false,
child: child ?? const SizedBox.shrink(),
return Stack(
fit: StackFit.expand,
children: [
SafeArea(
top: true,
left: false,
right: false,
bottom: false,
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: {

View File

@ -12,7 +12,8 @@ abstract final class ApiConfig {
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';

View File

@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:screen_secure/screen_secure.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../adjust/adjust_events.dart';
@ -23,6 +24,9 @@ class AuthService {
static Future<void>? _loginFuture;
/// UI
static final ValueNotifier<bool> isLoginComplete = ValueNotifier(false);
/// Future await Future
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value();
@ -52,6 +56,28 @@ class AuthService {
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
static void _saveCommonInfoToState(Map<String, dynamic> data) {
final reveal = data['reveal'] as int?;
@ -70,13 +96,16 @@ class AuthService {
if (surge != null) {
final enable = surge['lucky'] as bool?;
UserState.setEnableThirdPartyPayment(enable);
// extConfigneed_wait = Video items = docs/extConfig.md
// extConfigneed_wait = Video safe_area = items = docs/extConfig.md
final needWait = surge['need_wait'] as bool?;
final safeArea = surge['safe_area'] as bool?;
final items = surge['items'] as List<dynamic>?;
UserState.setExtConfig(
needShowVideoMenuValue: needWait,
safeAreaValue: safeArea,
items: items,
);
_applyScreenSecure(safeArea);
}
} catch (e) {
_logMsg('surge JSON 解析失败: $e');
@ -224,7 +253,10 @@ class AuthService {
_logMsg('init: 异常 $e');
_logMsg('init: 堆栈 $st');
} 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
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
static final ValueNotifier<List<dynamic>?> extConfigItems = ValueNotifier<List<dynamic>?>(null);
@ -43,8 +45,13 @@ class UserState {
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 (safeAreaValue != null) safeArea.value = safeAreaValue;
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/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 36 are stage 3.
const _stateLabels = <int, String>{
1: 'Queued',
@ -72,6 +72,7 @@ class GenerateProgressScreen extends StatefulWidget {
class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
int? _state;
Timer? _pollTimer;
bool _isFetching = false;
double get _progress => _progressForState(_state);
@ -104,7 +105,7 @@ class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
Future<void> _startPolling() async {
await AuthService.loginComplete;
_pollTimer = Timer.periodic(
const Duration(seconds: 1),
const Duration(seconds: 5),
(_) => _fetchProgress(),
);
_fetchProgress();
@ -112,7 +113,9 @@ class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
Future<void> _fetchProgress() async {
if (widget.taskId == null) return;
if (_isFetching) return;
_isFetching = true;
try {
final res = await ImageApi.getProgress(
sentinel: ApiConfig.appId,
@ -163,11 +166,14 @@ class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
break;
}
} catch (_) {}
finally {
_isFetching = false;
}
}
@override
Widget build(BuildContext context) {
final labelText = _stateLabels[_state] ?? '队列中';
final labelText = _stateLabels[_state] ?? 'Queued';
return Scaffold(
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 {
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:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:http/http.dart' as http;
@ -179,12 +178,13 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
throw Exception('User not logged in');
}
final templateName = widget.task?.templateName ?? '';
final createRes = await ImageApi.createTask(
asset: userId,
guild: filePath,
allowance: false,
cipher: widget.task?.taskType ?? '',
congregation: widget.task?.templateName ?? '',
congregation: templateName == 'BananaTask' ? null : templateName,
heatmap: _heatmap,
ext: widget.task?.ext,
);

View File

@ -192,35 +192,74 @@ class _MediaDisplay extends StatelessWidget {
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: videoUrl != null && videoController != null
? _VideoPlayer(
controller: videoController!,
)
: videoUrl != null && videoLoading
? Container(
color: AppColors.textPrimary,
alignment: Alignment.center,
child: const CircularProgressIndicator(
color: AppColors.surface,
),
child: Stack(
fit: StackFit.expand,
children: [
videoUrl != null && videoController != null
? _VideoPlayer(
controller: videoController!,
)
: videoUrl != null && videoLoadError != null
: videoUrl != null && videoLoading
? Container(
color: AppColors.textPrimary,
alignment: Alignment.center,
child: const Text(
'Load failed',
style: TextStyle(color: AppColors.surface),
child: const CircularProgressIndicator(
color: AppColors.surface,
),
)
: imageUrl != null
? CachedNetworkImage(
imageUrl: imageUrl!,
fit: BoxFit.cover,
placeholder: (_, __) => _Placeholder(),
errorWidget: (_, __, ___) => _Placeholder(),
: videoUrl != null && videoLoadError != null
? Container(
color: AppColors.textPrimary,
alignment: Alignment.center,
child: const Text(
'Load failed',
style: TextStyle(color: AppColors.surface),
),
)
: _Placeholder(),
: imageUrl != null
? CachedNetworkImage(
imageUrl: imageUrl!,
fit: BoxFit.cover,
placeholder: (_, __) => _Placeholder(),
errorWidget: (_, __, ___) => _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,49 +302,26 @@ class _VideoPlayerState extends State<_VideoPlayer> {
);
}
return Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
FittedBox(
fit: BoxFit.contain,
child: SizedBox(
width: widget.controller.value.size.width > 0
? widget.controller.value.size.width
: 16,
height: widget.controller.value.size.height > 0
? widget.controller.value.size.height
: 9,
child: VideoPlayer(widget.controller),
),
return GestureDetector(
onTap: () {
if (widget.controller.value.isPlaying) {
widget.controller.pause();
} else {
widget.controller.play();
}
},
child: FittedBox(
fit: BoxFit.contain,
child: SizedBox(
width: widget.controller.value.size.width > 0
? widget.controller.value.size.width
: 16,
height: widget.controller.value.size.height > 0
? widget.controller.value.size.height
: 9,
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,22 +331,12 @@ class _Placeholder extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
color: AppColors.textPrimary,
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 work is ready',
style: AppTypography.bodyRegular.copyWith(
color: AppColors.surface.withValues(alpha: 0.6),
),
),
],
alignment: Alignment.center,
child: Text(
'Your work is ready',
style: AppTypography.bodyRegular.copyWith(
color: AppColors.surface.withValues(alpha: 0.6),
),
),
);
}

View File

@ -38,6 +38,7 @@ class _HomeScreenState extends State<HomeScreen> {
super.initState();
UserState.needShowVideoMenu.addListener(_onExtConfigChanged);
UserState.extConfigItems.addListener(_onExtConfigChanged);
AuthService.isLoginComplete.addListener(_onExtConfigChanged);
_loadCategories();
if (widget.isActive) refreshAccount();
}
@ -46,6 +47,7 @@ class _HomeScreenState extends State<HomeScreen> {
void dispose() {
UserState.needShowVideoMenu.removeListener(_onExtConfigChanged);
UserState.extConfigItems.removeListener(_onExtConfigChanged);
AuthService.isLoginComplete.removeListener(_onExtConfigChanged);
super.dispose();
}
@ -75,6 +77,24 @@ class _HomeScreenState extends State<HomeScreen> {
.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.itemstrue extConfig.items _tasks
List<TaskItem> get _displayTasks {
if (!_showVideoMenu) {
@ -187,7 +207,7 @@ class _HomeScreenState extends State<HomeScreen> {
horizontal: AppSpacing.screenPadding,
vertical: AppSpacing.xs,
),
child: _categoriesLoading
child: _showCategoriesLoading
? const SizedBox(
height: 40,
child: Center(child: CircularProgressIndicator()))
@ -198,9 +218,7 @@ class _HomeScreenState extends State<HomeScreen> {
),
),
Expanded(
child: _showVideoMenu &&
_selectedCategory?.id != kExtCategoryId &&
_tasksLoading
child: _isListLoading
? const Center(child: CircularProgressIndicator())
: LayoutBuilder(
builder: (context, constraints) {

View File

@ -23,9 +23,9 @@ void main() async {
statusBarBrightness: Brightness.light,
),
);
runApp(const App());
// APP
// loginComplete
AuthService.init();
runApp(const App());
// purchaseStream queryPastPurchases
if (defaultTargetPlatform == TargetPlatform.android) {
GooglePlayPurchaseService.startPendingPurchaseListener();

View File

@ -1,7 +1,7 @@
name: pets_hero_ai
description: PetsHero AI Application.
publish_to: 'none'
version: 1.0.4+5
version: 1.0.5+6
environment:
sdk: '>=3.0.0 <4.0.0'
@ -30,18 +30,31 @@ dependencies:
url_launcher: ^6.2.5
in_app_purchase: ^3.2.0
webview_flutter: ^4.10.0
screen_secure: ^1.0.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter_launcher_icons: ^0.14.4
flutter_native_splash: ^2.4.7
flutter_launcher_icons:
android: true
ios: true
image_path: "design/images/generated-1773069485332.png"
ios: false # iOS xcodeproj not present; run with ios:true after adding iOS project
image_path: "assets/images/logo.png"
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:
uses-material-design: true
assets:
- assets/images/