新增:首页视频图片UI展示功能
This commit is contained in:
parent
8c5e14e00d
commit
22f712c5a8
4
.gitignore
vendored
4
.gitignore
vendored
@ -43,3 +43,7 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
# Local Android signing (debug / release)
|
||||||
|
/android/key.properties
|
||||||
|
/android/debug.keystore
|
||||||
|
|||||||
25
.vscode/launch.json
vendored
Normal file
25
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "FunyMee",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FunyMee (profile mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FunyMee (release mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
import java.io.FileInputStream
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
@ -5,6 +8,13 @@ plugins {
|
|||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
|
val hasCustomDebugSigning = keystorePropertiesFile.exists()
|
||||||
|
if (hasCustomDebugSigning) {
|
||||||
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.funymeeai.app"
|
namespace = "com.funymeeai.app"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
@ -30,11 +40,39 @@ android {
|
|||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
getByName("debug") {
|
||||||
|
if (hasCustomDebugSigning) {
|
||||||
|
val storeRel = keystoreProperties.getProperty("debug.storeFile")
|
||||||
|
?: error("key.properties: missing debug.storeFile")
|
||||||
|
storeFile = rootProject.file(storeRel)
|
||||||
|
storePassword = keystoreProperties.getProperty("debug.storePassword")
|
||||||
|
?: error("key.properties: missing debug.storePassword")
|
||||||
|
keyAlias = keystoreProperties.getProperty("debug.keyAlias")
|
||||||
|
?: error("key.properties: missing debug.keyAlias")
|
||||||
|
keyPassword = keystoreProperties.getProperty("debug.keyPassword")
|
||||||
|
?: error("key.properties: missing debug.keyPassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseStoreFile = keystoreProperties.getProperty("release.storeFile")?.trim()
|
||||||
|
if (!releaseStoreFile.isNullOrEmpty()) {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(releaseStoreFile)
|
||||||
|
storePassword = keystoreProperties.getProperty("release.storePassword")
|
||||||
|
?: error("key.properties: missing release.storePassword")
|
||||||
|
keyAlias = keystoreProperties.getProperty("release.keyAlias")
|
||||||
|
?: error("key.properties: missing release.keyAlias")
|
||||||
|
keyPassword = keystoreProperties.getProperty("release.keyPassword")
|
||||||
|
?: error("key.properties: missing release.keyPassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig = signingConfigs.findByName("release")
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
?: signingConfigs.getByName("debug")
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
<!-- Adjust -->
|
<!-- Adjust -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.adjust.sdk.appToken"
|
android:name="com.adjust.sdk.appToken"
|
||||||
|
|||||||
13
android/key.properties.example
Normal file
13
android/key.properties.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# 复制为 key.properties 后填写;key.properties 已加入 .gitignore,勿提交。
|
||||||
|
# debug.storeFile 路径相对于 android/ 目录;release.storeFile 可为绝对路径。
|
||||||
|
|
||||||
|
debug.storeFile=debug.keystore
|
||||||
|
debug.storePassword=你的store密码
|
||||||
|
debug.keyAlias=androiddebugkey
|
||||||
|
debug.keyPassword=你的key密码
|
||||||
|
|
||||||
|
# 正式版 / Play 上架(可选;未配置时 release 构建仍使用 debug 签名)
|
||||||
|
# release.storeFile=/absolute/or/relative/path/to/release.keystore
|
||||||
|
# release.storePassword=
|
||||||
|
# release.keyAlias=
|
||||||
|
# release.keyPassword=
|
||||||
BIN
assets/fonts/BonheurRoyale-Regular.ttf
Normal file
BIN
assets/fonts/BonheurRoyale-Regular.ttf
Normal file
Binary file not shown.
0
assets/images/.gitkeep
Normal file
0
assets/images/.gitkeep
Normal file
BIN
assets/images/credit_tag.png
Normal file
BIN
assets/images/credit_tag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/images/funymee_home_bg.png
Normal file
BIN
assets/images/funymee_home_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/xiabiao.png
Normal file
BIN
assets/images/xiabiao.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@ -4,6 +4,10 @@
|
|||||||
"id": "com.funymeeai.app",
|
"id": "com.funymeeai.app",
|
||||||
"packageName": "com.funymeeai.app"
|
"packageName": "com.funymeeai.app"
|
||||||
},
|
},
|
||||||
|
"videoHome": {
|
||||||
|
"imagesTabLabel": "images",
|
||||||
|
"imagesTabFirst": false
|
||||||
|
},
|
||||||
"backend": {
|
"backend": {
|
||||||
"iosAppType": "HIOS",
|
"iosAppType": "HIOS",
|
||||||
"androidAppType": "HAndroid"
|
"androidAppType": "HAndroid"
|
||||||
@ -89,7 +93,8 @@
|
|||||||
"cost": ["cost"],
|
"cost": ["cost"],
|
||||||
"title": ["title"],
|
"title": ["title"],
|
||||||
"params": ["params"],
|
"params": ["params"],
|
||||||
"detail": ["detail"]
|
"detail": ["detail"],
|
||||||
|
"videoUrl": ["video", "video_url", "videoUrl", "preview_video"]
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"go_run": false,
|
"go_run": false,
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./首页.png",
|
"url": "../assets/images/funymee_home_bg.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -45,68 +45,6 @@
|
|||||||
16
|
16
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "G5vCS",
|
|
||||||
"name": "statusBar",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 48,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "InQvB",
|
|
||||||
"name": "timeLabel",
|
|
||||||
"fill": "#FFFFFF",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "yynpI",
|
|
||||||
"name": "statusRight",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "e63C5",
|
|
||||||
"name": "sig",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#FFFFFF"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "GnJcx",
|
|
||||||
"name": "wifi",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#FFFFFF"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "sTeY3",
|
|
||||||
"name": "batt",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#FFFFFF"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "W3KlO",
|
"id": "W3KlO",
|
||||||
@ -482,69 +420,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "zgDvz",
|
|
||||||
"name": "st",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "tLwo0",
|
|
||||||
"name": "stT",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "tdfob",
|
|
||||||
"name": "stR",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "OKuBW",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "dStgj",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "xEV1H",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "NmIKq",
|
"id": "NmIKq",
|
||||||
@ -1135,68 +1010,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "xMQpm",
|
|
||||||
"name": "stCr",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "wwpdw",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "Djsv3",
|
|
||||||
"name": "stRCr",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "wCcUb",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "6jmR0",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "o50tz",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "3QoZ9",
|
"id": "3QoZ9",
|
||||||
@ -1610,68 +1423,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "0KtQc",
|
|
||||||
"name": "stpp",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "nt515",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "Tjn3l",
|
|
||||||
"name": "stRpp",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "uLjle",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "B4tWH",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "Fee0a",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "tpyga",
|
"id": "tpyga",
|
||||||
@ -2375,68 +2126,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "339Z8",
|
|
||||||
"name": "stGen",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "GoGUj",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "HWI8w",
|
|
||||||
"name": "stRGn",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "yACne",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "Eyg7x",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "c83Bo",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "ehHA6",
|
"id": "ehHA6",
|
||||||
@ -2928,68 +2617,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "9VjRH",
|
|
||||||
"name": "stGen",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "HiNlF",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "MCdcZ",
|
|
||||||
"name": "stRGn",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "McCxM",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "qgZXX",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "kWfbu",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "l2HgR",
|
"id": "l2HgR",
|
||||||
@ -3511,68 +3138,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "rAKA8",
|
|
||||||
"name": "stGen",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "feD9B",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "kGW9M",
|
|
||||||
"name": "stRGn",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "f0wkY",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "QP8Gu",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "Guu6U",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "t9TSj",
|
"id": "t9TSj",
|
||||||
@ -4064,68 +3629,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "qGsnm",
|
|
||||||
"name": "stGen",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "gaudb",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "QidsY",
|
|
||||||
"name": "stRGn",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "4qUnm",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "0QEyp",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "hdyeN",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "cjQgH",
|
"id": "cjQgH",
|
||||||
@ -4702,68 +4205,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "DFOGa",
|
|
||||||
"name": "stGen",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "yhvTg",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "77lNZ",
|
|
||||||
"name": "stRGn",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "8Ci3L",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "O33hZ",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "r9bHd",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "PCc0N",
|
"id": "PCc0N",
|
||||||
@ -5196,68 +4637,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "aYrOu",
|
|
||||||
"name": "stBar",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "l5Vff",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "09DZE",
|
|
||||||
"name": "stRGn",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "6DSNp",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "7pPRd",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "MsiYo",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "GQFk7",
|
"id": "GQFk7",
|
||||||
@ -6288,72 +5667,6 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "DkFCC",
|
|
||||||
"name": "st",
|
|
||||||
"width": "fill_container",
|
|
||||||
"height": 62,
|
|
||||||
"fill": "#00000000",
|
|
||||||
"padding": [
|
|
||||||
0,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"justifyContent": "space_between",
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"id": "U5yFs",
|
|
||||||
"name": "tSt",
|
|
||||||
"fill": "#171717",
|
|
||||||
"content": "9:41",
|
|
||||||
"fontFamily": "Inter",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontWeight": "600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "frame",
|
|
||||||
"id": "1OsM1",
|
|
||||||
"name": "icWrap",
|
|
||||||
"fill": "#00000000",
|
|
||||||
"gap": 6,
|
|
||||||
"alignItems": "center",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "OOFdw",
|
|
||||||
"name": "i1",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "signal",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "C8auU",
|
|
||||||
"name": "i2",
|
|
||||||
"width": 18,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "wifi",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "icon_font",
|
|
||||||
"id": "d6o9j",
|
|
||||||
"name": "i3",
|
|
||||||
"width": 22,
|
|
||||||
"height": 18,
|
|
||||||
"iconFontName": "battery-medium",
|
|
||||||
"iconFontFamily": "lucide",
|
|
||||||
"fill": "#404040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "1Vs8a",
|
"id": "1Vs8a",
|
||||||
@ -6809,4 +6122,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -394,7 +394,7 @@ lib/
|
|||||||
|
|
||||||
- 颜色/圆角:对齐 Pencil;主强调色与 Tab 下划线可参考 `#c99304`。
|
- 颜色/圆角:对齐 Pencil;主强调色与 Tab 下划线可参考 `#c99304`。
|
||||||
- 字体:`Inter` + `Bonheur Royale`(见历史卡片 Download 标签);在 `pubspec.yaml` 注册 `fonts:` 并放入 `assets/fonts/`。
|
- 字体:`Inter` + `Bonheur Royale`(见历史卡片 Download 标签);在 `pubspec.yaml` 注册 `fonts:` 并放入 `assets/fonts/`。
|
||||||
- 首页背景:设计使用 `首页.png`,可复制为 `assets/images/` 或完全用 `LinearGradient` 复刻 `suXxr`。
|
- 首页背景:Pencil `bi8Au` 使用 `./首页.png`。请将设计导出为 **`assets/images/home_background.png`**(与代码中 `Image.asset` 一致);缺失时客户端使用深色渐变占位,非最终 1:1 效果。子页通用渐变见 `PencilTheme.yellowWhitePageGradient`(对齐 `suXxr` / `WBRp4`)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -80,5 +80,9 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>FacebookAdvertiserIDCollectionEnabled</key>
|
<key>FacebookAdvertiserIDCollectionEnabled</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>FunyMee needs camera access to take photos for generation.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>FunyMee needs photo library access to choose images for generation.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
121
lib/app.dart
121
lib/app.dart
@ -1,6 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import 'core/auth/auth_service.dart';
|
||||||
import 'core/theme/app_colors.dart';
|
import 'core/theme/app_colors.dart';
|
||||||
|
import 'core/theme/app_theme.dart';
|
||||||
|
import 'features/shell/main_screen.dart';
|
||||||
|
|
||||||
class App extends StatelessWidget {
|
class App extends StatelessWidget {
|
||||||
const App({super.key, required this.title});
|
const App({super.key, required this.title});
|
||||||
@ -12,15 +18,110 @@ class App extends StatelessWidget {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: title,
|
title: title,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: buildFunyMeeTheme(),
|
||||||
colorScheme: ColorScheme.fromSeed(
|
home: const MainScreen(),
|
||||||
seedColor: AppColors.primary,
|
builder: (context, child) {
|
||||||
brightness: Brightness.dark,
|
return Stack(
|
||||||
),
|
fit: StackFit.expand,
|
||||||
useMaterial3: true,
|
children: [
|
||||||
scaffoldBackgroundColor: AppColors.background,
|
SafeArea(
|
||||||
),
|
top: false,
|
||||||
home: Scaffold(body: Center(child: Text(title))),
|
left: false,
|
||||||
|
right: false,
|
||||||
|
bottom: false,
|
||||||
|
child: child ?? const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: AuthService.isLoginComplete,
|
||||||
|
builder: (context, done, _) {
|
||||||
|
if (done) return const SizedBox.shrink();
|
||||||
|
return const _StartupLoginOverlay();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 对齐 app_client:首帧即进入壳层,登录完成前遮罩 + 久等网络提示。
|
||||||
|
class _StartupLoginOverlay extends StatefulWidget {
|
||||||
|
const _StartupLoginOverlay();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_StartupLoginOverlay> createState() => _StartupLoginOverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StartupLoginOverlayState extends State<_StartupLoginOverlay> {
|
||||||
|
static const _longWaitAfter = Duration(seconds: 22);
|
||||||
|
Timer? _longWaitTimer;
|
||||||
|
bool _showNetworkHint = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_longWaitTimer = Timer(_longWaitAfter, () {
|
||||||
|
if (mounted) setState(() => _showNetworkHint = true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_longWaitTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned.fill(
|
||||||
|
child: AbsorbPointer(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withValues(alpha: 0.22),
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 28),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(color: AppColors.primary),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Signing in…',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
color: Colors.white.withValues(alpha: 0.92),
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_showNetworkHint) ...[
|
||||||
|
const SizedBox(height: 22),
|
||||||
|
Text(
|
||||||
|
'Network is slow or unavailable',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
color: Colors.white.withValues(alpha: 0.96),
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'Check Wi‑Fi or mobile data. If the connection is fine, the server may be busy—try again shortly.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
color: Colors.white.withValues(alpha: 0.78),
|
||||||
|
fontSize: 14,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
lib/core/app_env.dart
Normal file
9
lib/core/app_env.dart
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// `ImageApi.getMyTasks` / `getProgress` 等使用的 `app` 渠道标识。
|
||||||
|
String currentBackendAppType() {
|
||||||
|
return defaultTargetPlatform == TargetPlatform.iOS
|
||||||
|
? ClientBootstrap.skin.backendAppTypeIOS
|
||||||
|
: ClientBootstrap.skin.backendAppTypeAndroid;
|
||||||
|
}
|
||||||
@ -52,10 +52,12 @@ class AppAuthCallbacks implements AuthServiceCallbacks {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onLoginSuccess(FastLoginResponse data) {
|
void onLoginSuccess(FastLoginResponse data) {
|
||||||
if (data.userId != null) UserState.setUserId(data.userId!);
|
UserState.applyLogin(
|
||||||
if (data.credits != null) UserState.setCredits(data.credits!);
|
userId: data.userId,
|
||||||
if (data.avatar != null) UserState.setAvatar(data.avatar!);
|
credits: data.credits,
|
||||||
if (data.userName != null) UserState.setUserName(data.userName!);
|
avatar: data.avatar,
|
||||||
|
userName: data.userName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -81,4 +83,8 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> get loginComplete => FrameworkAuthService.loginComplete;
|
static Future<void> get loginComplete => FrameworkAuthService.loginComplete;
|
||||||
|
|
||||||
|
/// 登录流程是否已结束(含 fast_login + common_info 链路);用于遮罩,勿用 [loginComplete] 的 Future(首帧可能为 null)。
|
||||||
|
static ValueNotifier<bool> get isLoginComplete =>
|
||||||
|
FrameworkAuthService.isLoginComplete;
|
||||||
}
|
}
|
||||||
|
|||||||
47
lib/core/ext_config_document_urls.dart
Normal file
47
lib/core/ext_config_document_urls.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
|
||||||
|
/// 从 `common_info.extConfig`([ExtConfigRuntime])与 `skin_config.extConfig.defaults` 解析文档类 URL。
|
||||||
|
/// 用于用户协议、隐私政策及后续其他 H5 页面。
|
||||||
|
abstract final class ExtConfigDocumentUrls {
|
||||||
|
ExtConfigDocumentUrls._();
|
||||||
|
|
||||||
|
/// 用户协议(wire 键由 skin 的 `agreementUrl` → `agreement` 等映射)。
|
||||||
|
static String? get agreementUrl =>
|
||||||
|
_resolve((d) => d.agreementUrl);
|
||||||
|
|
||||||
|
/// 隐私政策(wire 键由 skin 的 `privacyUrl` → `privacy` 等映射)。
|
||||||
|
static String? get privacyUrl =>
|
||||||
|
_resolve((d) => d.privacyUrl);
|
||||||
|
|
||||||
|
/// 通用:优先 [ExtConfigRuntime.data],再回退到换皮默认 [AppConfig.extConfigDefaults]。
|
||||||
|
static String? resolve(String? Function(ExtConfigData d) pick) => _resolve(pick);
|
||||||
|
|
||||||
|
static String? _resolve(String? Function(ExtConfigData d) pick) {
|
||||||
|
final runtime = ExtConfigRuntime.data.value;
|
||||||
|
if (runtime != null) {
|
||||||
|
final u = pick(runtime)?.trim();
|
||||||
|
if (u != null && u.isNotEmpty) return u;
|
||||||
|
}
|
||||||
|
final skin = _fromSkinDefaults();
|
||||||
|
if (skin != null) {
|
||||||
|
final u = pick(skin)?.trim();
|
||||||
|
if (u != null && u.isNotEmpty) return u;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ExtConfigData? _fromSkinDefaults() {
|
||||||
|
final cfg = ApiClient.instance.config;
|
||||||
|
final raw = cfg.extConfigDefaults;
|
||||||
|
final schema = cfg.extConfigKeySchema;
|
||||||
|
if (raw == null || raw.isEmpty) return null;
|
||||||
|
try {
|
||||||
|
return ExtConfigData.fromJson(
|
||||||
|
Map<String, dynamic>.from(raw),
|
||||||
|
schema: schema,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// FunyMee 画板主强调色参考 §15(#c99304)。
|
||||||
class AppColors {
|
class AppColors {
|
||||||
static const Color primary = Color(0xFF6366F1);
|
static const Color primary = Color(0xFFC99304);
|
||||||
static const Color secondary = Color(0xFF8B5CF6);
|
static const Color primaryDim = Color(0xFF9A7010);
|
||||||
static const Color background = Color(0xFF0F0F23);
|
static const Color secondary = Color(0xFFFFD966);
|
||||||
static const Color surface = Color(0xFF1A1A2E);
|
static const Color background = Color(0xFF0D0D0F);
|
||||||
static const Color error = Color(0xFFEF4444);
|
static const Color surface = Color(0xFF1A1A1E);
|
||||||
static const Color onPrimary = Colors.white;
|
static const Color surfaceVariant = Color(0xFF2A2A30);
|
||||||
static const Color onBackground = Colors.white;
|
static const Color error = Color(0xFFE85D4C);
|
||||||
static const Color onSurface = Colors.white;
|
static const Color onPrimary = Color(0xFF1A1200);
|
||||||
|
static const Color onBackground = Color(0xFFF5F0E6);
|
||||||
|
static const Color onSurface = Color(0xFFE8E3D9);
|
||||||
|
static const Color muted = Color(0xFF8A8578);
|
||||||
}
|
}
|
||||||
|
|||||||
62
lib/core/theme/app_theme.dart
Normal file
62
lib/core/theme/app_theme.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import 'app_colors.dart';
|
||||||
|
|
||||||
|
ThemeData buildFunyMeeTheme() {
|
||||||
|
final base = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
scaffoldBackgroundColor: AppColors.background,
|
||||||
|
colorScheme: const ColorScheme.dark(
|
||||||
|
primary: AppColors.primary,
|
||||||
|
onPrimary: AppColors.onPrimary,
|
||||||
|
secondary: AppColors.secondary,
|
||||||
|
surface: AppColors.surface,
|
||||||
|
error: AppColors.error,
|
||||||
|
onSurface: AppColors.onSurface,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return base.copyWith(
|
||||||
|
textTheme: GoogleFonts.interTextTheme(base.textTheme).apply(
|
||||||
|
bodyColor: AppColors.onBackground,
|
||||||
|
displayColor: AppColors.onBackground,
|
||||||
|
),
|
||||||
|
cardTheme: const CardThemeData(clipBehavior: Clip.antiAlias),
|
||||||
|
dialogTheme: const DialogThemeData(clipBehavior: Clip.antiAlias),
|
||||||
|
bottomSheetTheme: const BottomSheetThemeData(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
foregroundColor: AppColors.onBackground,
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
|
backgroundColor: AppColors.surface.withValues(alpha: 0.94),
|
||||||
|
indicatorColor: AppColors.primary.withValues(alpha: 0.25),
|
||||||
|
labelTextStyle: WidgetStateProperty.all(
|
||||||
|
GoogleFonts.inter(fontSize: 12, color: AppColors.onSurface),
|
||||||
|
),
|
||||||
|
iconTheme: WidgetStateProperty.all(
|
||||||
|
const IconThemeData(color: AppColors.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
foregroundColor: AppColors.onPrimary,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
backgroundColor: AppColors.surfaceVariant,
|
||||||
|
contentTextStyle: GoogleFonts.inter(color: AppColors.onSurface),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,19 +1,38 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// 全局用户快照;积分等用 [ValueNotifier] 驱动 UI。
|
||||||
class UserState {
|
class UserState {
|
||||||
static String? _userId;
|
UserState._();
|
||||||
static int _credits = 0;
|
|
||||||
static String? _avatar;
|
|
||||||
static String? _userName;
|
|
||||||
static String? _countryCode;
|
|
||||||
|
|
||||||
static String? get userId => _userId;
|
static final ValueNotifier<String?> userId = ValueNotifier<String?>(null);
|
||||||
static int get credits => _credits;
|
static final ValueNotifier<int> credits = ValueNotifier<int>(0);
|
||||||
static String? get avatar => _avatar;
|
static final ValueNotifier<String?> avatar = ValueNotifier<String?>(null);
|
||||||
static String? get userName => _userName;
|
static final ValueNotifier<String?> userName = ValueNotifier<String?>(null);
|
||||||
static String? get countryCode => _countryCode;
|
static final ValueNotifier<String?> countryCode = ValueNotifier<String?>(null);
|
||||||
|
|
||||||
static void setUserId(String id) => _userId = id;
|
static void setUserId(String? id) => userId.value = id;
|
||||||
static void setCredits(int credits) => _credits = credits;
|
static void setCredits(int v) => credits.value = v;
|
||||||
static void setAvatar(String avatar) => _avatar = avatar;
|
static void setAvatar(String? v) => avatar.value = v;
|
||||||
static void setUserName(String name) => _userName = name;
|
static void setUserName(String? v) => userName.value = v;
|
||||||
static void setCountryCode(String code) => _countryCode = code;
|
static void setCountryCode(String? v) => countryCode.value = v;
|
||||||
|
|
||||||
|
static void applyLogin({
|
||||||
|
String? userId,
|
||||||
|
int? credits,
|
||||||
|
String? avatar,
|
||||||
|
String? userName,
|
||||||
|
}) {
|
||||||
|
if (userId != null) UserState.userId.value = userId;
|
||||||
|
if (credits != null) UserState.credits.value = credits;
|
||||||
|
if (avatar != null) UserState.avatar.value = avatar;
|
||||||
|
if (userName != null) UserState.userName.value = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear() {
|
||||||
|
userId.value = null;
|
||||||
|
credits.value = 0;
|
||||||
|
avatar.value = null;
|
||||||
|
userName.value = null;
|
||||||
|
countryCode.value = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
lib/design/pencil_theme.dart
Normal file
56
lib/design/pencil_theme.dart
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 自 `desgin/funymee_home.pen` 抽取的色值与尺寸(390×844 基准)。
|
||||||
|
abstract final class PencilTheme {
|
||||||
|
/// FunyMee Home (bi8Au) 玻璃按钮填充 #c4c4c499
|
||||||
|
static const Color homeGlassFill = Color(0x99C4C4C4);
|
||||||
|
|
||||||
|
static const Color homeTextPrimary = Colors.white;
|
||||||
|
static const Color homeTabDivider = Color(0x66FFFFFF);
|
||||||
|
static const Color gemYellow = Color(0xFFFFD60A);
|
||||||
|
|
||||||
|
/// Create Now 磨砂 pill
|
||||||
|
static const Color createPillFill = Color(0x4DFFFFFF);
|
||||||
|
static const Color createPlusDisc = Color(0xFFFFD60A);
|
||||||
|
|
||||||
|
/// 黄→白渐变页(suXxr / WBRp4 / EYsUi / 5J8Po)
|
||||||
|
static const LinearGradient yellowWhitePageGradient = LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFDE047),
|
||||||
|
Color(0xFFFCFCFC),
|
||||||
|
Color(0xFFFFFFFF),
|
||||||
|
],
|
||||||
|
stops: [0, 0.3,1],
|
||||||
|
);
|
||||||
|
|
||||||
|
static const Color ink = Color(0xFF171717);
|
||||||
|
static const Color inkMuted = Color(0xFF404040);
|
||||||
|
static const Color inkSoft = Color(0xFF525252);
|
||||||
|
static const Color underlineGold = Color(0xFFC99304);
|
||||||
|
static const Color stone900 = Color(0xFF1C1917);
|
||||||
|
static const Color stone600 = Color(0xFF57534E);
|
||||||
|
static const Color stone700 = Color(0xFF44403C);
|
||||||
|
|
||||||
|
static const Color expiryBg = Color(0xFFFFFBEB);
|
||||||
|
static const Color expiryBorder = Color(0xFFFDE68A);
|
||||||
|
static const Color expiryHead = Color(0xFF92400E);
|
||||||
|
static const Color expiryBody = Color(0xFF78350F);
|
||||||
|
|
||||||
|
static const Color cardThumbBg = Color(0xFFF5F5F4);
|
||||||
|
static const Color downloadPillBorder = Color(0xFFD4D4D4);
|
||||||
|
static const Color downloadPillInk = Color(0xFF171717);
|
||||||
|
|
||||||
|
static const Color profileAvatarRing = Color(0xFFFBBF24);
|
||||||
|
static const Color profileAvatarIcon = Color(0xFFCA8A04);
|
||||||
|
static const Color profileCredits = Color(0xFFB45309);
|
||||||
|
|
||||||
|
static const Color genHintBorder = Color(0xFFFDE68A);
|
||||||
|
static const Color genHintTitle = Color(0xFF44403C);
|
||||||
|
static const Color genSlotBorder = Color(0xFFF5D08A);
|
||||||
|
static const Color genNavBackStroke = Color(0xFFE7E5E4);
|
||||||
|
|
||||||
|
/// 设计宽度用于按比例缩放(可选)。
|
||||||
|
static const double designWidth = 390;
|
||||||
|
}
|
||||||
213
lib/features/generate/generate_progress_screen.dart
Normal file
213
lib/features/generate/generate_progress_screen.dart
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../../core/app_env.dart';
|
||||||
|
import '../../core/user/user_state.dart';
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
import '../../widgets/pencil_chrome.dart';
|
||||||
|
import 'generate_result_screen.dart';
|
||||||
|
|
||||||
|
/// `YoZaK` 生成中 — 与 EYsUi 同款顶栏结构,标题「生成中」。
|
||||||
|
class GenerateProgressScreen extends StatefulWidget {
|
||||||
|
const GenerateProgressScreen({
|
||||||
|
super.key,
|
||||||
|
required this.taskId,
|
||||||
|
this.localPreviewPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String taskId;
|
||||||
|
final String? localPreviewPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GenerateProgressScreen> createState() => _GenerateProgressScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
|
||||||
|
Timer? _timer;
|
||||||
|
String _status = '';
|
||||||
|
int _progress = 0;
|
||||||
|
String? _resultUrl;
|
||||||
|
String? _error;
|
||||||
|
bool _finished = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_poll();
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 2), (_) => _poll());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _poll() async {
|
||||||
|
if (_error != null || _finished) return;
|
||||||
|
final uid = UserState.userId.value;
|
||||||
|
final res = await ImageApi.getProgress(
|
||||||
|
app: currentBackendAppType(),
|
||||||
|
taskId: widget.taskId,
|
||||||
|
userId: uid,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!res.isSuccess || res.data == null) {
|
||||||
|
setState(() => _error = res.msg.isNotEmpty ? res.msg : 'Progress error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final p = res.data!;
|
||||||
|
setState(() {
|
||||||
|
_status = p.status ?? '';
|
||||||
|
_progress = p.progress ?? 0;
|
||||||
|
_resultUrl = p.resultUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_isTerminal(_status) || _hasUsableResult(_resultUrl)) {
|
||||||
|
_timer?.cancel();
|
||||||
|
if (_isSuccess(_status) || _hasUsableResult(_resultUrl)) {
|
||||||
|
_finished = true;
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => GenerateResultScreen(
|
||||||
|
taskId: widget.taskId,
|
||||||
|
resultUrl: _resultUrl ?? '',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (_isFailure(_status)) {
|
||||||
|
setState(() => _error ??= 'Task failed ($_status)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasUsableResult(String? url) {
|
||||||
|
if (url == null || url.isEmpty) return false;
|
||||||
|
return url.startsWith('http://') || url.startsWith('https://');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTerminal(String s) {
|
||||||
|
final t = s.toLowerCase();
|
||||||
|
return t == 'success' ||
|
||||||
|
t == 'completed' ||
|
||||||
|
t == 'complete' ||
|
||||||
|
t == 'failed' ||
|
||||||
|
t == 'failure' ||
|
||||||
|
t == 'error' ||
|
||||||
|
t == 'cancelled' ||
|
||||||
|
t == 'canceled';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isSuccess(String s) {
|
||||||
|
final t = s.toLowerCase();
|
||||||
|
return t == 'success' || t == 'completed' || t == 'complete';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isFailure(String s) {
|
||||||
|
final t = s.toLowerCase();
|
||||||
|
return t == 'failed' ||
|
||||||
|
t == 'failure' ||
|
||||||
|
t == 'error' ||
|
||||||
|
t == 'cancelled' ||
|
||||||
|
t == 'canceled';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: PencilTheme.yellowWhitePageGradient,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
PencilRoundBackButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Generating',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 44),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (widget.localPreviewPath != null)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: Image.file(
|
||||||
|
File(widget.localPreviewPath!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (_error != null)
|
||||||
|
Text(
|
||||||
|
_error!,
|
||||||
|
style: GoogleFonts.inter(color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: _progress > 0 ? _progress / 100 : null,
|
||||||
|
color: PencilTheme.underlineGold,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_status.isEmpty ? 'Processing…' : _status,
|
||||||
|
style: GoogleFonts.inter(color: PencilTheme.stone600),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$_progress%',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/features/generate/generate_result_screen.dart
Normal file
109
lib/features/generate/generate_result_screen.dart
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
import '../../widgets/pencil_chrome.dart';
|
||||||
|
import '../report/report_screen.dart';
|
||||||
|
|
||||||
|
class GenerateResultScreen extends StatelessWidget {
|
||||||
|
const GenerateResultScreen({
|
||||||
|
super.key,
|
||||||
|
required this.taskId,
|
||||||
|
required this.resultUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String taskId;
|
||||||
|
final String resultUrl;
|
||||||
|
|
||||||
|
bool get _hasUrl =>
|
||||||
|
resultUrl.startsWith('http://') || resultUrl.startsWith('https://');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: PencilTheme.yellowWhitePageGradient,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
PencilRoundBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context)
|
||||||
|
.popUntil((r) => r.isFirst);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Done',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 44),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
children: [
|
||||||
|
if (_hasUrl)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 3 / 4,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: resultUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
progressIndicatorBuilder: (_, _, _) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
errorWidget: (_, _, _) =>
|
||||||
|
const Icon(Icons.broken_image),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
'The result is not ready yet. Check History later.\nTask: $taskId',
|
||||||
|
style: GoogleFonts.inter(color: PencilTheme.stone600),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => ReportScreen(taskId: taskId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.flag_outlined),
|
||||||
|
label: const Text('Report / feedback'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
671
lib/features/generate/generate_screen.dart
Normal file
671
lib/features/generate/generate_screen.dart
Normal file
@ -0,0 +1,671 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
import '../../core/app_env.dart';
|
||||||
|
import '../../core/user/user_state.dart';
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
import '../../widgets/pencil_chrome.dart';
|
||||||
|
import 'generate_progress_screen.dart';
|
||||||
|
|
||||||
|
/// `EYsUi` 生成图片页 — 左 112×108 槽位 +「=」+ 右侧效果图;分辨率在「开始生成」上方横排。
|
||||||
|
/// [ExtConfigItem.imgNeed] == 2 时需两张图并上传;其余(含视频类模板)只传一张,请在配置里将视频项 `img_need` 设为 1。
|
||||||
|
/// [template] 来自首页选中的 [ExtConfigItem]。
|
||||||
|
class GenerateScreen extends StatefulWidget {
|
||||||
|
const GenerateScreen({super.key, this.template});
|
||||||
|
|
||||||
|
final ExtConfigItem? template;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GenerateScreen> createState() => _GenerateScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GenerateScreenState extends State<GenerateScreen> {
|
||||||
|
final _picker = ImagePicker();
|
||||||
|
File? _picked;
|
||||||
|
File? _picked2;
|
||||||
|
String _heatmap = '720p';
|
||||||
|
bool _busy = false;
|
||||||
|
|
||||||
|
static const double _slotW = 112;
|
||||||
|
static const double _slotH = 108;
|
||||||
|
static const double _previewH = 359;
|
||||||
|
static const double _previewOuterR = 20;
|
||||||
|
static const double _previewBorderW = 1.5;
|
||||||
|
|
||||||
|
File? get _primaryFile => _picked ?? _picked2;
|
||||||
|
|
||||||
|
/// 双图:仅当扩展配置 `img_need == 2`(与 [ExtConfigItem.imgNeed] 一致)。
|
||||||
|
bool get _needTwoImages => widget.template?.imgNeed == 2;
|
||||||
|
|
||||||
|
Future<void> _pickSlot(int slot) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
final source = await _showPickImageSourceSheet(context);
|
||||||
|
if (source == null || !mounted) return;
|
||||||
|
final x = await _picker.pickImage(
|
||||||
|
source: source,
|
||||||
|
imageQuality: 92,
|
||||||
|
);
|
||||||
|
if (x == null || !mounted) return;
|
||||||
|
setState(() {
|
||||||
|
if (slot == 0) {
|
||||||
|
_picked = File(x.path);
|
||||||
|
} else {
|
||||||
|
_picked2 = File(x.path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ImageSource?> _showPickImageSourceSheet(BuildContext context) {
|
||||||
|
return showModalBottomSheet<ImageSource>(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (ctx) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x20000000),
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: Offset(0, -4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: PencilTheme.genNavBackStroke,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.camera_alt_outlined,
|
||||||
|
color: PencilTheme.profileAvatarIcon,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'Camera',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.pop(ctx, ImageSource.camera),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.photo_library_outlined,
|
||||||
|
color: PencilTheme.profileAvatarIcon,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'Photo library',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.pop(ctx, ImageSource.gallery),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
'Cancel',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.pop(ctx),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _start() async {
|
||||||
|
final uid = UserState.userId.value;
|
||||||
|
if (uid == null || uid.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please sign in first.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_needTwoImages) {
|
||||||
|
if (_picked == null || _picked2 == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please select two images first.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final file = _primaryFile;
|
||||||
|
if (file == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please select an image first.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _busy = true);
|
||||||
|
try {
|
||||||
|
final ImagePresignedUploadCreateTaskResult result;
|
||||||
|
if (_needTwoImages) {
|
||||||
|
result = await ImagePresignedUploadCreateTaskFlow.runTwoSourceFiles(
|
||||||
|
sourceFile1: _picked!,
|
||||||
|
sourceFile2: _picked2!,
|
||||||
|
userId: uid,
|
||||||
|
heatmap: _heatmap,
|
||||||
|
cipher: '',
|
||||||
|
compressFirst: true,
|
||||||
|
compressOptions: const CompressImageForUploadOptions(
|
||||||
|
maxSide: 1024,
|
||||||
|
jpegQuality: 75,
|
||||||
|
),
|
||||||
|
saveLocalUploadCover: true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = await ImagePresignedUploadCreateTaskFlow.run(
|
||||||
|
sourceFile: _primaryFile!,
|
||||||
|
userId: uid,
|
||||||
|
heatmap: _heatmap,
|
||||||
|
cipher: '',
|
||||||
|
compressFirst: true,
|
||||||
|
compressOptions: const CompressImageForUploadOptions(
|
||||||
|
maxSide: 1024,
|
||||||
|
jpegQuality: 75,
|
||||||
|
),
|
||||||
|
saveLocalUploadCover: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final taskId = result.createResponse.taskId;
|
||||||
|
if (taskId == null || taskId.isEmpty) {
|
||||||
|
throw StateError('No task id');
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserAccountRefresh.fetchAndNotify(
|
||||||
|
app: currentBackendAppType(),
|
||||||
|
userId: uid,
|
||||||
|
onAccount: (a) {
|
||||||
|
if (a.credits != null) UserState.setCredits(a.credits!);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => GenerateProgressScreen(
|
||||||
|
taskId: taskId,
|
||||||
|
localPreviewPath: result.fileUsedForUpload.path,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('$e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _busy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get _estimatedCost {
|
||||||
|
final c = widget.template?.cost ?? 0;
|
||||||
|
return c > 0 ? c : 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final credits = UserState.credits.value;
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: PencilTheme.yellowWhitePageGradient,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
PencilRoundBackButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Generate',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 44),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: _hintBox(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 6, 16, 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_needTwoImages ? _leftTwoSlotsColumn() : _leftSingleSlotColumn(),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
_equalsColumn(),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(child: _effectPreviewPanel()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 28),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_resoChip(
|
||||||
|
'480p',
|
||||||
|
_heatmap == '480p',
|
||||||
|
() => setState(() => _heatmap = '480p'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_resoChip(
|
||||||
|
'720p',
|
||||||
|
_heatmap == '720p',
|
||||||
|
() => setState(() => _heatmap = '720p'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: PencilTheme.underlineGold,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size.fromHeight(54),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: _busy ? null : _start,
|
||||||
|
child: _busy
|
||||||
|
? const SizedBox(
|
||||||
|
height: 22,
|
||||||
|
width: 22,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'Start',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Est. cost · $_estimatedCost credits',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Balance · ${credits.toStringAsFixed(2)}',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
color: PencilTheme.inkSoft,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设计稿 Lcol:114 宽,双槽 112×108,中间 32 高加号。
|
||||||
|
Widget _leftTwoSlotsColumn() {
|
||||||
|
return SizedBox(
|
||||||
|
width: 114,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_imageSlot(slotIndex: 0, label: 'Image 1'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.add,
|
||||||
|
size: 24,
|
||||||
|
color: PencilTheme.profileCredits,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_imageSlot(slotIndex: 1, label: 'Image 2'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 单图:仅 `img_need != 2` 或视频菜单开启时;槽位尺寸与设计一致。
|
||||||
|
Widget _leftSingleSlotColumn() {
|
||||||
|
return SizedBox(
|
||||||
|
width: 114,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_imageSlot(slotIndex: 0, label: 'Image 1'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _equalsColumn() {
|
||||||
|
return SizedBox(
|
||||||
|
width: 32,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'=',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: PencilTheme.profileCredits,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设计稿 prevBx:高 359,圆角 20,描边 #FBBF24。
|
||||||
|
/// 描边叠在图片之上(否则子组件会先盖住 decoration 内缘的线)。
|
||||||
|
Widget _effectPreviewPanel() {
|
||||||
|
final t = widget.template;
|
||||||
|
final url = t?.image.trim() ?? '';
|
||||||
|
final fix = t?.imageFix?.trim();
|
||||||
|
|
||||||
|
final innerR = _previewOuterR - _previewBorderW;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: _previewH,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(_previewOuterR),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
|
blurRadius: 24,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(innerR),
|
||||||
|
child: _buildPreviewImageLayer(url, fix),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(_previewOuterR),
|
||||||
|
border: Border.all(
|
||||||
|
color: PencilTheme.profileAvatarRing,
|
||||||
|
width: _previewBorderW,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPreviewImageLayer(String url, String? fix) {
|
||||||
|
if (url.isNotEmpty) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: url,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
height: _previewH,
|
||||||
|
placeholder: (_, _) => _previewPlaceholder(loading: true),
|
||||||
|
errorWidget: (_, _, _) {
|
||||||
|
if (fix != null && fix.isNotEmpty) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: fix,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
height: _previewH,
|
||||||
|
errorWidget: (_, _, _) => _previewPlaceholder(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _previewPlaceholder();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (fix != null && fix.isNotEmpty) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: fix,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
height: _previewH,
|
||||||
|
errorWidget: (_, _, _) => _previewPlaceholder(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _previewPlaceholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _previewPlaceholder({bool loading = false}) {
|
||||||
|
return Container(
|
||||||
|
color: PencilTheme.cardThumbBg,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: loading
|
||||||
|
? SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: PencilTheme.profileAvatarIcon.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.auto_awesome_outlined,
|
||||||
|
size: 40,
|
||||||
|
color: PencilTheme.stone600.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Preview',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.stone600.withValues(alpha: 0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _imageSlot({required int slotIndex, required String label}) {
|
||||||
|
final file = slotIndex == 0 ? _picked : _picked2;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: _slotW,
|
||||||
|
height: _slotH,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: PencilTheme.genSlotBorder,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _pickSlot(slotIndex),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: file == null
|
||||||
|
? Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.add_photo_alternate_outlined,
|
||||||
|
color: PencilTheme.profileAvatarIcon,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: Image.file(file, fit: BoxFit.cover),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _hintBox() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(18, 14, 18, 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: PencilTheme.genHintBorder),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color(0x30CA8A04),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.auto_awesome,
|
||||||
|
size: 18, color: PencilTheme.profileAvatarIcon),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Upload tips',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: PencilTheme.genHintTitle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Upload JPG or PNG (≤ 5 MB each, up to 2). You can use the camera or photo library. Use clear, front-facing photos when possible.',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 1.45,
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _resoChip(String t, bool on, VoidCallback fn) {
|
||||||
|
return Material(
|
||||||
|
color: on ? PencilTheme.underlineGold.withValues(alpha: 0.2) : Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(
|
||||||
|
color: on ? PencilTheme.underlineGold : PencilTheme.genNavBackStroke,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: fn,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
child: Text(
|
||||||
|
t,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
lib/features/history/credit_record_tab.dart
Normal file
124
lib/features/history/credit_record_tab.dart
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
|
||||||
|
/// WBRp4「Credit Record」内容区样式。
|
||||||
|
class CreditRecordTab extends StatefulWidget {
|
||||||
|
const CreditRecordTab({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreditRecordTab> createState() => _CreditRecordTabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreditRecordTabState extends State<CreditRecordTab> {
|
||||||
|
bool _loading = true;
|
||||||
|
String? _error;
|
||||||
|
List<CreditRecordItem> _records = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
final res = await UserApi.getCreditsPage(page: '1', size: '30', type: '1');
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!res.isSuccess || res.data == null) {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_error = res.msg.isNotEmpty ? res.msg : 'Failed';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_records = res.data!.records ?? [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTime(int? t) {
|
||||||
|
if (t == null) return '—';
|
||||||
|
var ms = t;
|
||||||
|
if (t < 2000000000) ms = t * 1000;
|
||||||
|
final dt = DateTime.fromMillisecondsSinceEpoch(ms);
|
||||||
|
return DateFormat('yyyy-MM-dd HH:mm').format(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_loading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (_error != null) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(_error!),
|
||||||
|
TextButton(onPressed: _load, child: const Text('Retry')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_records.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text('No records.',
|
||||||
|
style: GoogleFonts.inter(color: PencilTheme.inkSoft)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: _load,
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 28),
|
||||||
|
itemCount: _records.length,
|
||||||
|
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final r = _records[i];
|
||||||
|
final c = r.credits ?? 0;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: PencilTheme.genHintBorder),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color(0x30CA8A04),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${c > 0 ? '+' : ''}$c credits',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_formatTime(r.createTime),
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
234
lib/features/history/history_screen.dart
Normal file
234
lib/features/history/history_screen.dart
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../../core/app_env.dart';
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
import '../../widgets/pencil_chrome.dart';
|
||||||
|
import 'credit_record_tab.dart';
|
||||||
|
import 'widgets/history_grid_card.dart';
|
||||||
|
|
||||||
|
/// `WBRp4` My History — 黄→白渐变、顶栏、双 Tab、24h 提示、双列卡片。
|
||||||
|
class HistoryScreen extends StatefulWidget {
|
||||||
|
const HistoryScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HistoryScreen> createState() => _HistoryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
|
int _tab = 0;
|
||||||
|
bool _loading = true;
|
||||||
|
String? _error;
|
||||||
|
List<MyTaskItem> _items = [];
|
||||||
|
Map<String, String> _localCovers = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
final res = await ImageApi.getMyTasks(
|
||||||
|
app: currentBackendAppType(),
|
||||||
|
page: '1',
|
||||||
|
pageSize: '30',
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!res.isSuccess || res.data == null) {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_error = res.msg.isNotEmpty ? res.msg : 'Failed to load';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final tasks = res.data!.tasks ?? [];
|
||||||
|
final locals = await ImageTaskHistory.localCoverPathsForMyTaskItems(tasks);
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_items = tasks;
|
||||||
|
_localCovers = locals;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: PencilTheme.yellowWhitePageGradient,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 58,
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
PencilRoundBackButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_headerTab('My History', 0),
|
||||||
|
const SizedBox(width: 26),
|
||||||
|
_headerTab('Credit Record', 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 44),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _tab == 0 ? _myHistoryBody() : const CreditRecordTab(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _headerTab(String label, int index) {
|
||||||
|
final selected = _tab == index;
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => setState(() => _tab = index),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: selected ? PencilTheme.ink : PencilTheme.inkSoft,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (selected)
|
||||||
|
Container(
|
||||||
|
width: 50,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: PencilTheme.underlineGold,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _myHistoryBody() {
|
||||||
|
if (_loading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (_error != null) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(_error!),
|
||||||
|
TextButton(onPressed: _load, child: const Text('Retry')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: _load,
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
|
||||||
|
sliver: SliverToBoxAdapter(child: _expiryNotice()),
|
||||||
|
),
|
||||||
|
if (_items.isEmpty)
|
||||||
|
SliverFillRemaining(
|
||||||
|
child: Center(
|
||||||
|
child: Text('No tasks yet.',
|
||||||
|
style: GoogleFonts.inter(color: PencilTheme.inkSoft)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 28),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 14,
|
||||||
|
crossAxisSpacing: 14,
|
||||||
|
childAspectRatio: 171 / 182,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, i) {
|
||||||
|
final t = _items[i];
|
||||||
|
final id = t.taskId ?? '';
|
||||||
|
return HistoryGridCard(
|
||||||
|
item: t,
|
||||||
|
localCoverPath:
|
||||||
|
id.isEmpty ? null : _localCovers[id],
|
||||||
|
onDownload: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: _items.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _expiryNotice() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: PencilTheme.expiryBg,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: PencilTheme.expiryBorder),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'24-hour expiry',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.expiryHead,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Each item is kept for 24 hours after creation. Download before it expires.',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 11,
|
||||||
|
height: 1.35,
|
||||||
|
color: PencilTheme.expiryBody,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
142
lib/features/history/widgets/history_grid_card.dart
Normal file
142
lib/features/history/widgets/history_grid_card.dart
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../../../design/pencil_theme.dart';
|
||||||
|
|
||||||
|
/// WBRp4 单张卡片:171×182 比例,圆角 20,Download pill。
|
||||||
|
class HistoryGridCard extends StatelessWidget {
|
||||||
|
const HistoryGridCard({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
this.localCoverPath,
|
||||||
|
this.onDownload,
|
||||||
|
});
|
||||||
|
|
||||||
|
final MyTaskItem item;
|
||||||
|
final String? localCoverPath;
|
||||||
|
final VoidCallback? onDownload;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final url = item.resultUrl?.trim() ?? '';
|
||||||
|
final created = item.createTime ?? '—';
|
||||||
|
final remainder = _remainderLabel(item.createTime);
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, c) {
|
||||||
|
final w = c.maxWidth;
|
||||||
|
final h = w * (182 / 171);
|
||||||
|
return SizedBox(
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: url.isNotEmpty
|
||||||
|
? CachedNetworkImage(imageUrl: url, fit: BoxFit.cover)
|
||||||
|
: localCoverPath != null
|
||||||
|
? Image.file(File(localCoverPath!), fit: BoxFit.cover)
|
||||||
|
: Container(color: PencilTheme.cardThumbBg),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 8,
|
||||||
|
top: 10,
|
||||||
|
right: 8,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
created,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: PencilTheme.inkMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
remainder,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.underlineGold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(21),
|
||||||
|
side: const BorderSide(color: PencilTheme.downloadPillBorder),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onDownload,
|
||||||
|
borderRadius: BorderRadius.circular(21),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 6),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Download',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'BonheurRoyale',
|
||||||
|
fontSize: 12,
|
||||||
|
color: PencilTheme.downloadPillInk,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Icon(Icons.download_rounded,
|
||||||
|
size: 10, color: PencilTheme.downloadPillInk),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _remainderLabel(String? createTimeRaw) {
|
||||||
|
if (createTimeRaw == null || createTimeRaw.isEmpty) return '—';
|
||||||
|
DateTime? created;
|
||||||
|
final asInt = int.tryParse(createTimeRaw);
|
||||||
|
if (asInt != null) {
|
||||||
|
var ms = asInt;
|
||||||
|
if (asInt < 2000000000) ms = asInt * 1000;
|
||||||
|
created = DateTime.fromMillisecondsSinceEpoch(ms);
|
||||||
|
} else {
|
||||||
|
created = DateTime.tryParse(createTimeRaw);
|
||||||
|
}
|
||||||
|
if (created == null) return '—';
|
||||||
|
final deadline = created.add(const Duration(hours: 24));
|
||||||
|
final left = deadline.difference(DateTime.now());
|
||||||
|
if (left.isNegative) return 'Expired';
|
||||||
|
final h = left.inHours;
|
||||||
|
final m = left.inMinutes.remainder(60);
|
||||||
|
return '${h}h ${m.toString().padLeft(2, '0')}m left';
|
||||||
|
}
|
||||||
1181
lib/features/home/home_screen.dart
Normal file
1181
lib/features/home/home_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
469
lib/features/profile/delete_account_flow.dart
Normal file
469
lib/features/profile/delete_account_flow.dart
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
|
||||||
|
/// 设计:`desgin/funymee_home.pen`「注销账户 · 步骤1」「注销账户 · 步骤2 二次验证」。
|
||||||
|
/// 返回 `true` 表示用户在第二步确认注销(可继续调接口)。
|
||||||
|
Future<bool> showDeleteAccountConfirmationFlow(BuildContext context) async {
|
||||||
|
while (true) {
|
||||||
|
final step1Ok = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
barrierColor: _kScrim,
|
||||||
|
builder: (ctx) => const _DeleteAccountStep1Dialog(),
|
||||||
|
);
|
||||||
|
if (!context.mounted) return false;
|
||||||
|
if (step1Ok != true) return false;
|
||||||
|
|
||||||
|
final step2 = await showDialog<_DeleteStep2Result>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
barrierColor: _kScrim,
|
||||||
|
builder: (ctx) => const _DeleteAccountStep2Dialog(),
|
||||||
|
);
|
||||||
|
if (!context.mounted) return false;
|
||||||
|
if (step2 == _DeleteStep2Result.confirmed) return true;
|
||||||
|
if (step2 == _DeleteStep2Result.backToStep1) continue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _DeleteStep2Result { backToStep1, confirmed }
|
||||||
|
|
||||||
|
// --- 设计 token(与 .pen 一致)---
|
||||||
|
|
||||||
|
const _kModalWidth = 350.0;
|
||||||
|
const _kScrim = Color(0xB31C1917);
|
||||||
|
const _kModalBorder = Color(0xFFFECACA);
|
||||||
|
const _kDanger = Color(0xFFDC2626);
|
||||||
|
const _kDangerIconBg = Color(0xFFFEE2E2);
|
||||||
|
const _kTitle = PencilTheme.stone900;
|
||||||
|
const _kBody = Color(0xFF57534E);
|
||||||
|
const _kChkLabel = PencilTheme.stone700;
|
||||||
|
const _kCancelFill = Color(0xFFF5F5F4);
|
||||||
|
const _kCancelStroke = Color(0xFFE7E5E4);
|
||||||
|
const _kPillBg = Color(0xFFFEF3C7);
|
||||||
|
const _kPillInk = Color(0xFFB45309);
|
||||||
|
const _kShieldBg = Color(0xFFFFEDD5);
|
||||||
|
const _kShieldIcon = Color(0xFFC2410C);
|
||||||
|
const _kInputFill = Color(0xFFFAFAF9);
|
||||||
|
const _kInputStroke = Color(0xFFD6D3D1);
|
||||||
|
const _kHintInfo = Color(0xFF78716C);
|
||||||
|
|
||||||
|
class _DeleteAccountStep1Dialog extends StatefulWidget {
|
||||||
|
const _DeleteAccountStep1Dialog();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DeleteAccountStep1Dialog> createState() =>
|
||||||
|
_DeleteAccountStep1DialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeleteAccountStep1DialogState extends State<_DeleteAccountStep1Dialog> {
|
||||||
|
bool _ack = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: _kModalWidth),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(22),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: _kModalBorder),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x18000000),
|
||||||
|
blurRadius: 28,
|
||||||
|
offset: Offset(0, 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _kDangerIconBg,
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
size: 28,
|
||||||
|
color: _kDanger,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Delete account',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: _kTitle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'This cannot be undone. Your account, creations, credits, and all related data will be permanently removed.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 1.45,
|
||||||
|
color: _kBody,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
child: Checkbox(
|
||||||
|
value: _ack,
|
||||||
|
onChanged: (v) =>
|
||||||
|
setState(() => _ack = v ?? false),
|
||||||
|
activeColor: _kDanger,
|
||||||
|
side: const BorderSide(color: _kDanger, width: 2),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
materialTapTargetSize:
|
||||||
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'I understand the consequences and accept the risk of losing my data.',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.4,
|
||||||
|
color: _kChkLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _DialogSecondaryButton(
|
||||||
|
label: 'Cancel',
|
||||||
|
onTap: () => Navigator.of(context).pop(false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: _DialogPrimaryButton(
|
||||||
|
label: 'Continue',
|
||||||
|
enabled: _ack,
|
||||||
|
onTap: _ack
|
||||||
|
? () => Navigator.of(context).pop(true)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeleteAccountStep2Dialog extends StatefulWidget {
|
||||||
|
const _DeleteAccountStep2Dialog();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DeleteAccountStep2Dialog> createState() =>
|
||||||
|
_DeleteAccountStep2DialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeleteAccountStep2DialogState extends State<_DeleteAccountStep2Dialog> {
|
||||||
|
static const _phrase = 'PERMANENTLY DELETE';
|
||||||
|
|
||||||
|
final _controller = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.addListener(() => setState(() {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _canConfirm => _controller.text == _phrase;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: _kModalWidth),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(22),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: _kModalBorder),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x18000000),
|
||||||
|
blurRadius: 28,
|
||||||
|
offset: Offset(0, 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _kPillBg,
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Verification',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: _kPillInk,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _kShieldBg,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.gpp_maybe_rounded,
|
||||||
|
size: 26,
|
||||||
|
color: _kShieldIcon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Text(
|
||||||
|
'Final confirmation',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: _kTitle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Text(
|
||||||
|
'Type PERMANENTLY DELETE below exactly as shown. Wrong text cannot be submitted.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 1.45,
|
||||||
|
color: _kBody,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _kTitle,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: _kInputFill,
|
||||||
|
hintText: _phrase,
|
||||||
|
hintStyle: GoogleFonts.inter(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _kBody.withValues(alpha: 0.45),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: _kInputStroke),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: _kInputStroke),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: _kDanger, width: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 2),
|
||||||
|
child: Icon(
|
||||||
|
Icons.info_outline_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: PencilTheme.underlineGold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'The button stays disabled until the phrase matches exactly.',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 1.35,
|
||||||
|
color: _kHintInfo,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _DialogSecondaryButton(
|
||||||
|
label: 'Back',
|
||||||
|
onTap: () => Navigator.of(context)
|
||||||
|
.pop(_DeleteStep2Result.backToStep1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: _DialogPrimaryButton(
|
||||||
|
label: 'Delete account',
|
||||||
|
enabled: _canConfirm,
|
||||||
|
onTap: _canConfirm
|
||||||
|
? () => Navigator.of(context)
|
||||||
|
.pop(_DeleteStep2Result.confirmed)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogSecondaryButton extends StatelessWidget {
|
||||||
|
const _DialogSecondaryButton({
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: _kCancelFill,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: _kCancelStroke),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _kChkLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogPrimaryButton extends StatelessWidget {
|
||||||
|
const _DialogPrimaryButton({
|
||||||
|
required this.label,
|
||||||
|
required this.enabled,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final bool enabled;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: enabled ? _kDanger : _kDanger.withValues(alpha: 0.38),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
320
lib/features/profile/profile_screen.dart
Normal file
320
lib/features/profile/profile_screen.dart
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
|
import '../../core/app_env.dart';
|
||||||
|
import '../../core/ext_config_document_urls.dart';
|
||||||
|
import '../../core/user/user_state.dart';
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
import '../../widgets/pencil_chrome.dart';
|
||||||
|
import '../purchase/purchase_screen.dart';
|
||||||
|
import '../web/app_web_view_screen.dart';
|
||||||
|
import 'delete_account_flow.dart';
|
||||||
|
|
||||||
|
/// `5J8Po` 个人中心。
|
||||||
|
class ProfileScreen extends StatefulWidget {
|
||||||
|
const ProfileScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfileScreen> createState() => _ProfileScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
|
String _version = '…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
PackageInfo.fromPlatform().then((p) {
|
||||||
|
if (mounted) setState(() => _version = p.version);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: PencilTheme.yellowWhitePageGradient,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: PencilRoundCloseButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.only(bottom: 28),
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border.all(
|
||||||
|
color: PencilTheme.profileAvatarRing,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ValueListenableBuilder<String?>(
|
||||||
|
valueListenable: UserState.avatar,
|
||||||
|
builder: (context, url, _) {
|
||||||
|
if (url != null && url.isNotEmpty) {
|
||||||
|
return ClipOval(
|
||||||
|
child: Image.network(
|
||||||
|
url,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
errorBuilder: (_, _, _) =>
|
||||||
|
_avatarFallback(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _avatarFallback();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ValueListenableBuilder<String?>(
|
||||||
|
valueListenable: UserState.userId,
|
||||||
|
builder: (context, id, _) {
|
||||||
|
return Text(
|
||||||
|
'ID:${id ?? '—'}',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
ValueListenableBuilder<int>(
|
||||||
|
valueListenable: UserState.credits,
|
||||||
|
builder: (context, c, _) {
|
||||||
|
return Text(
|
||||||
|
'Credits · ${_formatCredits(c)}',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.profileCredits,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||||
|
child: _menuCard(context),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => const PurchaseScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Buy credits',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.underlineGold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await UserAccountRefresh.fetchAndNotify(
|
||||||
|
app: currentBackendAppType(),
|
||||||
|
userId: UserState.userId.value,
|
||||||
|
onAccount: (a) {
|
||||||
|
if (a.credits != null) {
|
||||||
|
UserState.setCredits(a.credits!);
|
||||||
|
}
|
||||||
|
if (a.avatar != null) {
|
||||||
|
UserState.setAvatar(a.avatar!);
|
||||||
|
}
|
||||||
|
if (a.userName != null) {
|
||||||
|
UserState.setUserName(a.userName!);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure: (m) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text(m)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Refresh',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _avatarFallback() {
|
||||||
|
return Icon(Icons.person_rounded,
|
||||||
|
size: 44, color: PencilTheme.profileAvatarIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatCredits(int c) {
|
||||||
|
final s = c.toString();
|
||||||
|
if (s.length <= 3) return s;
|
||||||
|
return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openAppWebView(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required String? url,
|
||||||
|
}) {
|
||||||
|
if (url == null || url.trim().isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Link not configured')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.of(context).push<void>(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => AppWebViewScreen(
|
||||||
|
title: title,
|
||||||
|
initialUrl: url.trim(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _menuCard(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: PencilTheme.genHintBorder),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color(0x30CA8A04),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_row(
|
||||||
|
'Terms of Service',
|
||||||
|
trailing: Icons.chevron_right_rounded,
|
||||||
|
onTap: () => _openAppWebView(
|
||||||
|
context,
|
||||||
|
title: 'Terms of Service',
|
||||||
|
url: ExtConfigDocumentUrls.agreementUrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_divider(),
|
||||||
|
_row(
|
||||||
|
'Privacy Policy',
|
||||||
|
trailing: Icons.chevron_right_rounded,
|
||||||
|
onTap: () => _openAppWebView(
|
||||||
|
context,
|
||||||
|
title: 'Privacy Policy',
|
||||||
|
url: ExtConfigDocumentUrls.privacyUrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_divider(),
|
||||||
|
_row('Version', value: 'v$_version'),
|
||||||
|
_divider(),
|
||||||
|
_row('Delete account',
|
||||||
|
danger: true,
|
||||||
|
trailing: Icons.chevron_right_rounded,
|
||||||
|
onTap: () => _delete(context)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _row(String title,
|
||||||
|
{IconData? trailing, String? value, bool danger = false, VoidCallback? onTap}) {
|
||||||
|
return ListTile(
|
||||||
|
onTap: onTap,
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: danger ? const Color(0xFFDC2626) : PencilTheme.stone700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: value != null
|
||||||
|
? Text(value,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
color: const Color(0xFF78716C)))
|
||||||
|
: Icon(trailing,
|
||||||
|
color: danger ? const Color(0xFFFCA292) : const Color(0xFFA8A29E)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _divider() =>
|
||||||
|
Container(height: 1, color: const Color(0xFFF5F5F4));
|
||||||
|
|
||||||
|
Future<void> _delete(BuildContext context) async {
|
||||||
|
final ok = await showDeleteAccountConfirmationFlow(context);
|
||||||
|
if (ok != true || !context.mounted) return;
|
||||||
|
final res = await UserApi.deleteAccount(
|
||||||
|
app: currentBackendAppType(),
|
||||||
|
userId: UserState.userId.value,
|
||||||
|
);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (res.isSuccess) {
|
||||||
|
ApiClient.instance.setUserToken(null);
|
||||||
|
UserState.clear();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Account deleted. Please restart the app and sign in again.')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(res.msg)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
593
lib/features/purchase/purchase_screen.dart
Normal file
593
lib/features/purchase/purchase_screen.dart
Normal file
@ -0,0 +1,593 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../../core/app_env.dart';
|
||||||
|
import '../../core/user/user_state.dart';
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
import '../../widgets/pencil_chrome.dart';
|
||||||
|
|
||||||
|
/// `ETbdo` Purchase Point:黄渐变、Bonheur 标题、积分区 + [xiabiao]、双列档位 + [credit_tag] 角标。
|
||||||
|
/// 商品来自 [PaymentFlowCatalog.loadStoreActivities];Android 走 [NativeIapCoordinator.purchaseGooglePlay]。
|
||||||
|
class PurchaseScreen extends StatefulWidget {
|
||||||
|
const PurchaseScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PurchaseScreen> createState() => _PurchaseScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PurchaseScreenState extends State<PurchaseScreen> {
|
||||||
|
List<PaymentProductItem> _products = [];
|
||||||
|
bool _loading = true;
|
||||||
|
String? _loadError;
|
||||||
|
bool _paying = false;
|
||||||
|
int? _selectedIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Defer network + setState until after first frame so the route can paint and
|
||||||
|
// the main isolate stays responsive (avoids input ANR when opening this screen).
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) _loadProducts(isInitial: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadProducts({bool isInitial = false}) async {
|
||||||
|
if (!isInitial) {
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_loadError = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
final res = await PaymentFlowCatalog.loadStoreActivities();
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!res.isSuccess || res.data == null) {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_loadError = res.msg.isNotEmpty ? res.msg : 'Failed to load products';
|
||||||
|
_products = [];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final list = res.data!.productList ?? [];
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_products = list;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onBuy(PaymentProductItem item, int index) async {
|
||||||
|
final uid = UserState.userId.value;
|
||||||
|
if (uid == null || uid.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please sign in first.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final aid = item.activityId;
|
||||||
|
final pid = item.productId;
|
||||||
|
if (aid == null || aid.isEmpty || pid == null || pid.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Product data is incomplete.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'In-app purchases on iOS use the App Store flow. This build only wires Google Play on Android.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_paying = true;
|
||||||
|
_selectedIndex = index;
|
||||||
|
});
|
||||||
|
|
||||||
|
final sink = _PurchaseSink(
|
||||||
|
context: context,
|
||||||
|
onRefresh: () {
|
||||||
|
if (mounted) setState(() => _paying = false);
|
||||||
|
},
|
||||||
|
onSuccess: () {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await NativeIapCoordinator.purchaseGooglePlay(
|
||||||
|
sink: sink,
|
||||||
|
userId: uid,
|
||||||
|
activityId: aid,
|
||||||
|
storeProductId: pid,
|
||||||
|
createPaymentApp: currentBackendAppType(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_paying = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: PencilTheme.yellowWhitePageGradient,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 0, 16, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
PencilRoundCloseButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 6),
|
||||||
|
child: Text(
|
||||||
|
'Purchase Point',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'BonheurRoyale',
|
||||||
|
fontSize: 44,
|
||||||
|
height: 1.05,
|
||||||
|
color: const Color(0xFF5C3D2E),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ValueListenableBuilder<int>(
|
||||||
|
valueListenable: UserState.credits,
|
||||||
|
builder: (_, credits, _) {
|
||||||
|
return _CreditHeaderSection(
|
||||||
|
creditsText: credits.toStringAsFixed(2),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _loading
|
||||||
|
? const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: PencilTheme.underlineGold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _loadError != null
|
||||||
|
? Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_loadError!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _loadProducts,
|
||||||
|
child: Text(
|
||||||
|
'Retry',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.underlineGold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _products.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
'No products available',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _ProductGrid(
|
||||||
|
products: _products,
|
||||||
|
paying: _paying,
|
||||||
|
selectedIndex: _selectedIndex,
|
||||||
|
onTap: _onBuy,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PurchaseSink implements PaymentSettlementSink {
|
||||||
|
_PurchaseSink({
|
||||||
|
required this.context,
|
||||||
|
required this.onRefresh,
|
||||||
|
required this.onSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BuildContext context;
|
||||||
|
final VoidCallback onRefresh;
|
||||||
|
final VoidCallback onSuccess;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPaymentSettled(PaymentSettlement settlement) {
|
||||||
|
onRefresh();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
switch (settlement.type) {
|
||||||
|
case PaymentFlowOutcomeType.success:
|
||||||
|
UserAccountRefresh.fetchAndNotify(
|
||||||
|
app: currentBackendAppType(),
|
||||||
|
userId: UserState.userId.value,
|
||||||
|
onAccount: (a) {
|
||||||
|
if (a.credits != null) UserState.setCredits(a.credits!);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
settlement.message ?? 'Payment successful',
|
||||||
|
style: GoogleFonts.inter(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
onSuccess();
|
||||||
|
break;
|
||||||
|
case PaymentFlowOutcomeType.failure:
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
settlement.message ?? 'Payment failed',
|
||||||
|
style: GoogleFonts.inter(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case PaymentFlowOutcomeType.cancelled:
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
settlement.message ?? 'Cancelled',
|
||||||
|
style: GoogleFonts.inter(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case PaymentFlowOutcomeType.timeout:
|
||||||
|
case PaymentFlowOutcomeType.nativePendingHostVerification:
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
settlement.message ?? 'Payment pending',
|
||||||
|
style: GoogleFonts.inter(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreditHeaderSection extends StatelessWidget {
|
||||||
|
const _CreditHeaderSection({required this.creditsText});
|
||||||
|
|
||||||
|
final String creditsText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Explicit height: as a non-flex child above [Expanded], this Column gets
|
||||||
|
// unbounded max height; [Stack] must have bounded constraints.
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 120,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFFCE952),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(22),
|
||||||
|
topRight: Radius.circular(22),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 17,
|
||||||
|
top: 11,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFAE238),
|
||||||
|
Color(0xFFF5BE5D),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
border: Border.all(color: Colors.white, width: 1.5),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Credit :',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 56,
|
||||||
|
child: Text(
|
||||||
|
creditsText,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: const Color(0xFF1F2937),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/xiabiao.png',
|
||||||
|
width: 60,
|
||||||
|
height: 17,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (_, _, _) => const SizedBox(height: 17),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductGrid extends StatelessWidget {
|
||||||
|
const _ProductGrid({
|
||||||
|
required this.products,
|
||||||
|
required this.paying,
|
||||||
|
required this.selectedIndex,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<PaymentProductItem> products;
|
||||||
|
final bool paying;
|
||||||
|
final int? selectedIndex;
|
||||||
|
final void Function(PaymentProductItem item, int index) onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 28),
|
||||||
|
itemCount: (products.length + 1) ~/ 2,
|
||||||
|
itemBuilder: (context, row) {
|
||||||
|
final i0 = row * 2;
|
||||||
|
final i1 = i0 + 1;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _ProductCard(
|
||||||
|
item: products[i0],
|
||||||
|
index: i0,
|
||||||
|
paying: paying,
|
||||||
|
selected: selectedIndex == i0,
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
Expanded(
|
||||||
|
child: i1 < products.length
|
||||||
|
? _ProductCard(
|
||||||
|
item: products[i1],
|
||||||
|
index: i1,
|
||||||
|
paying: paying,
|
||||||
|
selected: selectedIndex == i1,
|
||||||
|
onTap: onTap,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductCard extends StatelessWidget {
|
||||||
|
const _ProductCard({
|
||||||
|
required this.item,
|
||||||
|
required this.index,
|
||||||
|
required this.paying,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PaymentProductItem item;
|
||||||
|
final int index;
|
||||||
|
final bool paying;
|
||||||
|
final bool selected;
|
||||||
|
final void Function(PaymentProductItem item, int index) onTap;
|
||||||
|
|
||||||
|
static final _money = RegExp(r'[\d.]+');
|
||||||
|
|
||||||
|
static int? _discountPercent(String? actual, String? origin) {
|
||||||
|
final a = double.tryParse(_money.firstMatch(actual ?? '')?.group(0) ?? '');
|
||||||
|
final o = double.tryParse(_money.firstMatch(origin ?? '')?.group(0) ?? '');
|
||||||
|
if (a == null || o == null || o <= 0 || a >= o) return null;
|
||||||
|
return ((1 - a / o) * 100).round();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final rawTitle = item.title;
|
||||||
|
final title = (rawTitle != null && rawTitle.trim().isNotEmpty)
|
||||||
|
? rawTitle
|
||||||
|
: 'Credit';
|
||||||
|
final actual = item.actualAmount ?? '—';
|
||||||
|
final origin = item.originAmount;
|
||||||
|
final bonus = item.bonus;
|
||||||
|
final pct = _discountPercent(item.actualAmount, item.originAmount);
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: const Color(0xE6FEE56A),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: paying ? null : () => onTap(item, index),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 125,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 4, 12, 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
actual,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: const Color(0xFF0A0A0A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (origin != null && origin.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0x80FFE3E3),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
origin,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: const Color(0xFFDB6525),
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
decorationColor: const Color(0xFFDB6525),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (bonus != null && bonus > 0) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
'+$bonus Bonus',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (pct != null && pct > 0)
|
||||||
|
Positioned(
|
||||||
|
right: -4,
|
||||||
|
top: -8,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 60,
|
||||||
|
height: 33,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/credit_tag.png',
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
errorBuilder: (_, _, _) => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
'$pct% Off',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (paying && selected)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: Colors.white24,
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
116
lib/features/report/report_screen.dart
Normal file
116
lib/features/report/report_screen.dart
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
import '../../widgets/pencil_chrome.dart';
|
||||||
|
|
||||||
|
class ReportScreen extends StatefulWidget {
|
||||||
|
const ReportScreen({super.key, required this.taskId});
|
||||||
|
|
||||||
|
final String taskId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ReportScreen> createState() => _ReportScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReportScreenState extends State<ReportScreen> {
|
||||||
|
final _controller = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: PencilTheme.yellowWhitePageGradient,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
PencilRoundBackButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Report',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 44),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Task: ${widget.taskId}',
|
||||||
|
style: GoogleFonts.inter(color: PencilTheme.stone600),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
maxLines: 5,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Describe the issue…',
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: PencilTheme.genNavBackStroke),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: PencilTheme.underlineGold,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Submit wired in FeedbackApi (see §13).',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Submit'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
lib/features/shell/main_screen.dart
Normal file
13
lib/features/shell/main_screen.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../home/home_screen.dart';
|
||||||
|
|
||||||
|
/// 根壳:设计稿首页无底部 Tab,子页由首页按钮/路由 [Navigator.push] 进入。
|
||||||
|
class MainScreen extends StatelessWidget {
|
||||||
|
const MainScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const HomeScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
154
lib/features/web/app_web_view_screen.dart
Normal file
154
lib/features/web/app_web_view_screen.dart
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
|
|
||||||
|
import '../../design/pencil_theme.dart';
|
||||||
|
|
||||||
|
/// 通用应用内 Web 页(任意 HTTPS/HTTP 链接),供用户协议、隐私政策及其他 H5 复用。
|
||||||
|
class AppWebViewScreen extends StatefulWidget {
|
||||||
|
const AppWebViewScreen({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.initialUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// 完整 URL(含 `http`/`https`)。
|
||||||
|
final String initialUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppWebViewScreen> createState() => _AppWebViewScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppWebViewScreenState extends State<AppWebViewScreen> {
|
||||||
|
WebViewController? _controller;
|
||||||
|
bool _loading = true;
|
||||||
|
String? _loadError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final uri = _parseHttpUrl(widget.initialUrl);
|
||||||
|
if (uri == null) {
|
||||||
|
_loadError = 'Invalid link';
|
||||||
|
_loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_controller = WebViewController()
|
||||||
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
|
..setNavigationDelegate(
|
||||||
|
NavigationDelegate(
|
||||||
|
onPageStarted: (_) {
|
||||||
|
if (mounted) setState(() => _loading = true);
|
||||||
|
},
|
||||||
|
onPageFinished: (_) {
|
||||||
|
if (mounted) setState(() => _loading = false);
|
||||||
|
},
|
||||||
|
onWebResourceError: (WebResourceError e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_loadError = e.description.isNotEmpty
|
||||||
|
? e.description
|
||||||
|
: 'Failed to load page';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..loadRequest(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uri? _parseHttpUrl(String raw) {
|
||||||
|
final u = Uri.tryParse(raw.trim());
|
||||||
|
if (u == null || !u.hasScheme) return null;
|
||||||
|
if (u.scheme != 'http' && u.scheme != 'https') return null;
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
foregroundColor: PencilTheme.stone900,
|
||||||
|
title: Text(
|
||||||
|
widget.title,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.stone900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: _buildBody(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody() {
|
||||||
|
if (_loadError != null && _controller == null) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Text(
|
||||||
|
_loadError!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 15,
|
||||||
|
color: PencilTheme.stone600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final c = _controller;
|
||||||
|
if (c == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
WebViewWidget(controller: c),
|
||||||
|
if (_loading)
|
||||||
|
const ColoredBox(
|
||||||
|
color: Color(0x0F000000),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: PencilTheme.underlineGold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_loadError != null && _controller != null)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Material(
|
||||||
|
color: const Color(0xFFFFF7ED),
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
_loadError!,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
color: const Color(0xFF9A3412),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
@ -5,13 +7,15 @@ import 'package:client_proxy_framework/client_proxy_framework.dart';
|
|||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
import 'core/auth/auth_service.dart';
|
import 'core/auth/auth_service.dart';
|
||||||
|
|
||||||
void main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
await ClientBootstrap.initFromAsset('assets/skin_config.json');
|
await ClientBootstrap.initFromAsset('assets/skin_config.json');
|
||||||
await ClientBootstrap.initAnalytics();
|
await ClientBootstrap.initAnalytics();
|
||||||
await AnalyticsService.initAttribution();
|
await AnalyticsService.initAttribution();
|
||||||
|
|
||||||
|
await ensureDeviceMemoryProfileInitialized();
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
statusBarColor: Colors.transparent,
|
statusBarColor: Colors.transparent,
|
||||||
@ -19,6 +23,7 @@ void main() async {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 与 app_client 一致:先首帧再异步登录,由 [App] 内遮罩等待 [loginComplete]
|
||||||
runApp(App(title: ClientBootstrap.skin.appName));
|
runApp(App(title: ClientBootstrap.skin.appName));
|
||||||
AuthService.init();
|
unawaited(AuthService.init());
|
||||||
}
|
}
|
||||||
|
|||||||
182
lib/widgets/pencil_chrome.dart
Normal file
182
lib/widgets/pencil_chrome.dart
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../design/pencil_theme.dart';
|
||||||
|
|
||||||
|
/// bi8Au 玻璃方钮 35×35 / blur 半径约 20。
|
||||||
|
class PencilGlassSquareButton extends StatelessWidget {
|
||||||
|
const PencilGlassSquareButton({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
required this.onTap,
|
||||||
|
this.size = 35,
|
||||||
|
this.borderRadius = 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final double size;
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||||
|
child: Material(
|
||||||
|
color: PencilTheme.homeGlassFill,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: Center(child: child),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// bi8Au 积分胶囊:竖向 padding 由外层控制,横向 padding 约 14。
|
||||||
|
class PencilGlassCreditsPill extends StatelessWidget {
|
||||||
|
const PencilGlassCreditsPill({
|
||||||
|
super.key,
|
||||||
|
required this.amountText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String amountText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
|
color: PencilTheme.homeGlassFill,
|
||||||
|
height: 35,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.diamond_rounded,
|
||||||
|
size: 18, color: PencilTheme.gemYellow),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
amountText,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: PencilTheme.homeTextPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// bi8Au Create Now:宽 186 高 40,pill blur 28。
|
||||||
|
class PencilCreateNowButton extends StatelessWidget {
|
||||||
|
const PencilCreateNowButton({super.key, required this.onPressed});
|
||||||
|
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 28, sigmaY: 28),
|
||||||
|
child: Material(
|
||||||
|
color: PencilTheme.createPillFill,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 186,
|
||||||
|
height: 40,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: PencilTheme.createPlusDisc,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.add, size: 14, color: Colors.black),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Text(
|
||||||
|
'Create Now',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: PencilTheme.homeTextPrimary,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EYsUi / WBRp4 返回钮(无底色、无描边)。
|
||||||
|
class PencilRoundBackButton extends StatelessWidget {
|
||||||
|
const PencilRoundBackButton({super.key, required this.onPressed});
|
||||||
|
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onPressed,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
child: Icon(Icons.chevron_left_rounded,
|
||||||
|
size: 26, color: Color(0xFF374151)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 5J8Po 关闭钮(无底色、无描边)。
|
||||||
|
class PencilRoundCloseButton extends StatelessWidget {
|
||||||
|
const PencilRoundCloseButton({super.key, required this.onPressed});
|
||||||
|
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onPressed,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
child: Icon(Icons.close_rounded, size: 24, color: Color(0xFF374151)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
@ -6,11 +6,21 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
|
import file_selector_macos
|
||||||
import in_app_purchase_storekit
|
import in_app_purchase_storekit
|
||||||
|
import package_info_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import sqflite_darwin
|
||||||
|
import video_player_avfoundation
|
||||||
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin"))
|
InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin"))
|
||||||
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
|
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||||
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
62
macos/Podfile.lock
Normal file
62
macos/Podfile.lock
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
PODS:
|
||||||
|
- device_info_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- file_selector_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- FlutterMacOS (1.0.0)
|
||||||
|
- in_app_purchase_storekit (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- package_info_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- shared_preferences_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- sqflite_darwin (0.0.4):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- webview_flutter_wkwebview (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||||
|
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||||
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
- in_app_purchase_storekit (from `Flutter/ephemeral/.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||||
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
|
- webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
device_info_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||||
|
file_selector_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||||
|
FlutterMacOS:
|
||||||
|
:path: Flutter/ephemeral
|
||||||
|
in_app_purchase_storekit:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/in_app_purchase_storekit/darwin
|
||||||
|
package_info_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||||
|
shared_preferences_foundation:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
|
sqflite_darwin:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||||
|
webview_flutter_wkwebview:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||||
|
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
|
||||||
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
|
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
||||||
|
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||||
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
|
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
@ -27,6 +27,8 @@
|
|||||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||||
|
445A7D54FC8B5C080EC23AA7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50197F9962E93C4832D19A60 /* Pods_Runner.framework */; };
|
||||||
|
46077B05EFE9D8418248A9BA /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E0FA2F360DF233F6381AD5E /* Pods_RunnerTests.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -60,11 +62,12 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
097E77AEFC55DC7392A2A2D2 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||||
33CC10ED2044A3C60003C045 /* funymee_ai.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "funymee_ai.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
33CC10ED2044A3C60003C045 /* funymee_ai.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = funymee_ai.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||||
@ -76,8 +79,15 @@
|
|||||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||||
|
4E0FA2F360DF233F6381AD5E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
50197F9962E93C4832D19A60 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
570F5EADCFC9266B280CD997 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
96CD0E5D9FC7D510CE706E5A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9955E766342B2725A4BCE747 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
9EDC27A3BE6708BF5C126BE6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
F2644650A94F105FB404E139 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -85,6 +95,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
46077B05EFE9D8418248A9BA /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -92,6 +103,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
445A7D54FC8B5C080EC23AA7 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -125,6 +137,7 @@
|
|||||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||||
33CC10EE2044A3C60003C045 /* Products */,
|
33CC10EE2044A3C60003C045 /* Products */,
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||||
|
98B6BE56A423F6204EFD147F /* Pods */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -172,9 +185,25 @@
|
|||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
98B6BE56A423F6204EFD147F /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
F2644650A94F105FB404E139 /* Pods-Runner.debug.xcconfig */,
|
||||||
|
9955E766342B2725A4BCE747 /* Pods-Runner.release.xcconfig */,
|
||||||
|
9EDC27A3BE6708BF5C126BE6 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
570F5EADCFC9266B280CD997 /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
96CD0E5D9FC7D510CE706E5A /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
097E77AEFC55DC7392A2A2D2 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
50197F9962E93C4832D19A60 /* Pods_Runner.framework */,
|
||||||
|
4E0FA2F360DF233F6381AD5E /* Pods_RunnerTests.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -186,6 +215,7 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
ABCE1FA7DB91F688D59E364B /* [CP] Check Pods Manifest.lock */,
|
||||||
331C80D1294CF70F00263BE5 /* Sources */,
|
331C80D1294CF70F00263BE5 /* Sources */,
|
||||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||||
331C80D3294CF70F00263BE5 /* Resources */,
|
331C80D3294CF70F00263BE5 /* Resources */,
|
||||||
@ -204,11 +234,13 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
05B7C1A243B9A0B0C01B504E /* [CP] Check Pods Manifest.lock */,
|
||||||
33CC10E92044A3C60003C045 /* Sources */,
|
33CC10E92044A3C60003C045 /* Sources */,
|
||||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||||
33CC10EB2044A3C60003C045 /* Resources */,
|
33CC10EB2044A3C60003C045 /* Resources */,
|
||||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||||
|
39FF77A5C6E3B0F781285A62 /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -291,6 +323,28 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
05B7C1A243B9A0B0C01B504E /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@ -329,6 +383,45 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||||
};
|
};
|
||||||
|
39FF77A5C6E3B0F781285A62 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
ABCE1FA7DB91F688D59E364B /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@ -380,6 +473,7 @@
|
|||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 570F5EADCFC9266B280CD997 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@ -394,6 +488,7 @@
|
|||||||
};
|
};
|
||||||
331C80DC294CF71000263BE5 /* Release */ = {
|
331C80DC294CF71000263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 96CD0E5D9FC7D510CE706E5A /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@ -408,6 +503,7 @@
|
|||||||
};
|
};
|
||||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 097E77AEFC55DC7392A2A2D2 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
|||||||
@ -4,4 +4,7 @@
|
|||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
352
pubspec.lock
352
pubspec.lock
@ -57,6 +57,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -104,6 +128,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -112,6 +144,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -176,11 +216,59 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.5"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+5"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_cache_manager:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_cache_manager
|
||||||
|
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -189,6 +277,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.34"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -207,6 +303,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
|
google_fonts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_fonts
|
||||||
|
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.3"
|
||||||
hooks:
|
hooks:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -215,6 +319,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -239,6 +351,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.8.0"
|
version: "4.8.0"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+16"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+6"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2+1"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
in_app_purchase:
|
in_app_purchase:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -271,6 +447,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.8+1"
|
version: "0.4.8+1"
|
||||||
|
intl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.20.2"
|
||||||
jni:
|
jni:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -375,6 +559,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
native_toolchain_c:
|
native_toolchain_c:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -391,6 +583,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.3.0"
|
version: "9.3.0"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -399,6 +599,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
package_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: package_info_plus
|
||||||
|
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.3.1"
|
||||||
|
package_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_platform_interface
|
||||||
|
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -511,6 +727,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.28.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -580,6 +804,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.2"
|
version: "1.10.2"
|
||||||
|
sqflite:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_android
|
||||||
|
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2+3"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6"
|
||||||
|
sqflite_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_darwin
|
||||||
|
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_platform_interface
|
||||||
|
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -604,6 +868,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -628,6 +900,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.3"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -636,6 +916,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
video_player:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: video_player
|
||||||
|
sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
video_player_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_android
|
||||||
|
sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.9.5"
|
||||||
|
video_player_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_avfoundation
|
||||||
|
sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.9.4"
|
||||||
|
video_player_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_platform_interface
|
||||||
|
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.0"
|
||||||
|
video_player_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_web
|
||||||
|
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
video_thumbnail:
|
video_thumbnail:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -660,6 +980,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
webview_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: webview_flutter
|
||||||
|
sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.13.1"
|
||||||
|
webview_flutter_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_android
|
||||||
|
sha256: "0f7fcd2c86bf36bdcf94881f7941ce0cbc4f8d104b9fdcd5fcbef90e2199db76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.10.15"
|
||||||
|
webview_flutter_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_platform_interface
|
||||||
|
sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.15.1"
|
||||||
|
webview_flutter_wkwebview:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_wkwebview
|
||||||
|
sha256: d7219cfabc6f5fc2032e0fa980ec36d71f308a35a823395af1abc34d9a2ede83
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.24.2"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
13
pubspec.yaml
13
pubspec.yaml
@ -20,6 +20,14 @@ dependencies:
|
|||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
android_id: ^0.5.1
|
android_id: ^0.5.1
|
||||||
device_info_plus: ^11.1.0
|
device_info_plus: ^11.1.0
|
||||||
|
image_picker: ^1.1.2
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
flutter_cache_manager: ^3.4.1
|
||||||
|
video_player: ^2.9.2
|
||||||
|
intl: ^0.20.2
|
||||||
|
google_fonts: ^6.2.1
|
||||||
|
package_info_plus: ^8.1.2
|
||||||
|
webview_flutter: ^4.13.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -30,3 +38,8 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
- assets/skin_config.json
|
- assets/skin_config.json
|
||||||
|
- assets/images/
|
||||||
|
fonts:
|
||||||
|
- family: BonheurRoyale
|
||||||
|
fonts:
|
||||||
|
- asset: assets/fonts/BonheurRoyale-Regular.ttf
|
||||||
|
|||||||
@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user