更新:启动图和logo,登录还没成功的时候显示加载指示器
BIN
android/app/src/main/res/drawable-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
android/app/src/main/res/drawable-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
android/app/src/main/res/drawable-night-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
android/app/src/main/res/drawable-night-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 346 KiB |
|
After Width: | Height: | Size: 601 KiB |
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
@ -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>
|
||||||
BIN
android/app/src/main/res/drawable-xhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 601 KiB |
BIN
android/app/src/main/res/drawable/background.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
6
android/app/src/main/res/drawable/launch_background.xml
Normal 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>
|
||||||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 94 KiB |
22
android/app/src/main/res/values-night-v31/styles.xml
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
22
android/app/src/main/res/values-v31/styles.xml
Normal 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>
|
||||||
@ -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
|
After Width: | Height: | Size: 603 KiB |
BIN
assets/images/splash.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 893 KiB |
|
Before Width: | Height: | Size: 843 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 858 KiB |
BIN
design/images/generated-1773675899343.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1014 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 63 KiB |
21
ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
vendored
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/background@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/background@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 247 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 69 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 69 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 69 B |
38
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal 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>
|
||||||
@ -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"]];
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
29
lib/app.dart
@ -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: {
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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});
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
19
pubspec.yaml
@ -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/
|
||||||
|
|||||||