新增:首页视频图片UI展示功能

This commit is contained in:
ivan 2026-04-09 16:26:28 +08:00
parent 8c5e14e00d
commit 22f712c5a8
47 changed files with 5519 additions and 734 deletions

4
.gitignore vendored
View File

@ -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
View 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"
}
]
}

View File

@ -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")
} }
} }
} }

View File

@ -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"

View 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=

Binary file not shown.

0
assets/images/.gitkeep Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
assets/images/xiabiao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -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,

View File

@ -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",

View File

@ -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`
--- ---

View File

@ -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>

View File

@ -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 WiFi 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
View 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;
}

View File

@ -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;
} }

View 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;
}
}
}

View File

@ -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);
} }

View 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),
),
);
}

View File

@ -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;
}
} }

View 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;
}

View 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,
),
),
],
],
),
),
),
],
),
),
),
);
}
}

View 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'),
),
],
),
),
],
),
),
),
);
}
}

View 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,
),
),
],
),
),
],
),
),
),
);
}
/// 稿 Lcol114 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,
),
),
),
),
);
}
}

View 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';
/// WBRp4Credit 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,
),
),
],
),
);
},
),
);
}
}

View 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 Tab24h
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,
),
),
],
),
);
}
}

View 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 20Download 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';
}

File diff suppressed because it is too large Load Diff

View 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,
),
),
),
),
),
);
}
}

View 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)),
);
}
}
}

View 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 PointBonheur + [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),
),
),
),
],
),
),
),
);
}
}

View 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'),
),
],
),
),
),
],
),
),
),
);
}
}

View 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();
}
}

View 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),
),
),
),
),
),
),
],
);
}
}

View File

@ -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());
} }

View 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 40pill 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)),
),
),
);
}
}

View File

@ -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);
} }

View File

@ -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)

View File

@ -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
View 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

View File

@ -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;

View File

@ -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>

View File

@ -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:

View File

@ -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

View File

@ -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"));
} }

View File

@ -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)