打包:版本发布1.1.1
This commit is contained in:
parent
eb72fb4979
commit
cacb32c25f
@ -1,7 +1,6 @@
|
|||||||
<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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
|
||||||
<uses-permission android:name="com.android.vending.BILLING" />
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
<application
|
<application
|
||||||
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||||
|
|||||||
@ -2293,6 +2293,52 @@
|
|||||||
"fill": "#A1A1AA"
|
"fill": "#A1A1AA"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "WYY7b",
|
||||||
|
"name": "menuItem3",
|
||||||
|
"width": "fill_container",
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"cornerRadius": 12,
|
||||||
|
"effect": {
|
||||||
|
"type": "shadow",
|
||||||
|
"shadowType": "outer",
|
||||||
|
"color": "#00000008",
|
||||||
|
"offset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 2
|
||||||
|
},
|
||||||
|
"blur": 6
|
||||||
|
},
|
||||||
|
"padding": [
|
||||||
|
14,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"justifyContent": "space_between",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "P6isW",
|
||||||
|
"name": "textNode",
|
||||||
|
"fill": "#DC2626",
|
||||||
|
"content": "Delete Account",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": "500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "icon_font",
|
||||||
|
"id": "8OWH9",
|
||||||
|
"name": "iconNode",
|
||||||
|
"width": 20,
|
||||||
|
"height": 20,
|
||||||
|
"iconFontName": "trash-2",
|
||||||
|
"iconFontFamily": "lucide",
|
||||||
|
"fill": "#DC2626"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -4272,6 +4318,312 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "5qKUB",
|
||||||
|
"x": 3150,
|
||||||
|
"y": 100,
|
||||||
|
"name": "Report Dialog",
|
||||||
|
"width": 342,
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"cornerRadius": 16,
|
||||||
|
"effect": {
|
||||||
|
"type": "shadow",
|
||||||
|
"shadowType": "outer",
|
||||||
|
"color": "#00000026",
|
||||||
|
"offset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"blur": 24
|
||||||
|
},
|
||||||
|
"layout": "vertical",
|
||||||
|
"gap": 20,
|
||||||
|
"padding": 24,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "6i0Kw",
|
||||||
|
"name": "dialogHeader",
|
||||||
|
"width": "fill_container",
|
||||||
|
"justifyContent": "space_between",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "ISmLf",
|
||||||
|
"name": "title",
|
||||||
|
"fill": "#18181B",
|
||||||
|
"content": "Report",
|
||||||
|
"fontFamily": "Plus Jakarta Sans",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontWeight": "700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "yxaWM",
|
||||||
|
"name": "closeBtn",
|
||||||
|
"width": 40,
|
||||||
|
"height": 40,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "icon_font",
|
||||||
|
"id": "g1Nfm",
|
||||||
|
"width": 24,
|
||||||
|
"height": 24,
|
||||||
|
"iconFontName": "x",
|
||||||
|
"iconFontFamily": "lucide",
|
||||||
|
"fill": "#71717A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "884m3",
|
||||||
|
"name": "descriptionInput",
|
||||||
|
"width": "fill_container",
|
||||||
|
"height": 120,
|
||||||
|
"fill": "#FAFAFA",
|
||||||
|
"cornerRadius": 12,
|
||||||
|
"stroke": {
|
||||||
|
"align": "inside",
|
||||||
|
"thickness": 1,
|
||||||
|
"fill": "#E4E4E7"
|
||||||
|
},
|
||||||
|
"layout": "vertical",
|
||||||
|
"padding": 16,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "JDjJq",
|
||||||
|
"name": "placeholder",
|
||||||
|
"fill": "#A1A1AA",
|
||||||
|
"textGrowth": "fixed-width",
|
||||||
|
"width": "fill_container",
|
||||||
|
"content": "Describe the issue...",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": "normal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "2ofcH",
|
||||||
|
"name": "imageUpload",
|
||||||
|
"width": "fill_container",
|
||||||
|
"height": 120,
|
||||||
|
"fill": "#F4F4F5",
|
||||||
|
"cornerRadius": 12,
|
||||||
|
"stroke": {
|
||||||
|
"align": "center",
|
||||||
|
"thickness": 2,
|
||||||
|
"fill": "#D4D4D8"
|
||||||
|
},
|
||||||
|
"layout": "vertical",
|
||||||
|
"gap": 8,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "icon_font",
|
||||||
|
"id": "1NYr7",
|
||||||
|
"name": "uploadIcon",
|
||||||
|
"width": 32,
|
||||||
|
"height": 32,
|
||||||
|
"iconFontName": "image-plus",
|
||||||
|
"iconFontFamily": "lucide",
|
||||||
|
"fill": "#A1A1AA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "jnJim",
|
||||||
|
"name": "uploadLabel",
|
||||||
|
"fill": "#71717A",
|
||||||
|
"content": "Tap to upload image",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": "normal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "ZOA9y",
|
||||||
|
"name": "submitBtn",
|
||||||
|
"width": "fill_container",
|
||||||
|
"height": 52,
|
||||||
|
"fill": "#7C3AED",
|
||||||
|
"cornerRadius": 14,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "Jfj7q",
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"content": "Submit",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "600"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "Xp6Qz",
|
||||||
|
"x": 900,
|
||||||
|
"y": 0,
|
||||||
|
"name": "Delete Account Overlay",
|
||||||
|
"width": 390,
|
||||||
|
"height": 844,
|
||||||
|
"fill": "#00000080",
|
||||||
|
"layout": "vertical",
|
||||||
|
"justifyContent": "center",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "vFAbj",
|
||||||
|
"name": "Delete Account Dialog",
|
||||||
|
"width": 342,
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"cornerRadius": 16,
|
||||||
|
"effect": {
|
||||||
|
"type": "shadow",
|
||||||
|
"shadowType": "outer",
|
||||||
|
"color": "#00000026",
|
||||||
|
"offset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"blur": 24
|
||||||
|
},
|
||||||
|
"layout": "vertical",
|
||||||
|
"gap": 20,
|
||||||
|
"padding": 24,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "YR8mP",
|
||||||
|
"name": "header",
|
||||||
|
"width": "fill_container",
|
||||||
|
"justifyContent": "space_between",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "Pck1A",
|
||||||
|
"name": "title",
|
||||||
|
"fill": "#18181B",
|
||||||
|
"content": "Delete Account?",
|
||||||
|
"fontFamily": "Plus Jakarta Sans",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontWeight": "700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "d8c1P",
|
||||||
|
"name": "closeBtn",
|
||||||
|
"width": 40,
|
||||||
|
"height": 40,
|
||||||
|
"layout": "vertical",
|
||||||
|
"justifyContent": "center",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "icon_font",
|
||||||
|
"id": "l4PfI",
|
||||||
|
"name": "closeIcon",
|
||||||
|
"width": 24,
|
||||||
|
"height": 24,
|
||||||
|
"iconFontName": "x",
|
||||||
|
"iconFontFamily": "lucide",
|
||||||
|
"fill": "#71717A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "8K9GG",
|
||||||
|
"name": "desc",
|
||||||
|
"fill": "#71717A",
|
||||||
|
"textGrowth": "fixed-width",
|
||||||
|
"width": "fill_container",
|
||||||
|
"content": "This action cannot be undone. All your data will be permanently deleted.",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": "normal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "0HXLh",
|
||||||
|
"name": "btnRow",
|
||||||
|
"width": "fill_container",
|
||||||
|
"gap": 12,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "csIsQ",
|
||||||
|
"name": "cancelBtn",
|
||||||
|
"width": "fill_container",
|
||||||
|
"height": 52,
|
||||||
|
"fill": "#F4F4F5",
|
||||||
|
"cornerRadius": 14,
|
||||||
|
"layout": "vertical",
|
||||||
|
"justifyContent": "center",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "yqSEk",
|
||||||
|
"name": "cancelText",
|
||||||
|
"fill": "#18181B",
|
||||||
|
"content": "Cancel",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "600"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "2r5VL",
|
||||||
|
"name": "deleteBtn",
|
||||||
|
"width": "fill_container",
|
||||||
|
"height": 52,
|
||||||
|
"fill": "#DC2626",
|
||||||
|
"cornerRadius": 14,
|
||||||
|
"layout": "vertical",
|
||||||
|
"justifyContent": "center",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "fQlFx",
|
||||||
|
"name": "deleteText",
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"content": "Delete",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "600"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
82
docs/feedback_flow.md
Normal file
82
docs/feedback_flow.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# 举报 / 反馈流程
|
||||||
|
|
||||||
|
本文档说明举报弹窗的提交流程及接口调用顺序。字段映射详见 **petsHeroAI_client_guide.md**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、前置校验
|
||||||
|
|
||||||
|
点击 Submit 时需校验:
|
||||||
|
|
||||||
|
| 校验项 | 要求 | 不满足时 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| 文字输入 | 必填 | 提示用户填写描述 |
|
||||||
|
| 图片选择 | 必选 | 提示用户上传图片 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、接口调用顺序
|
||||||
|
|
||||||
|
### 2.1 获取上传 URL
|
||||||
|
|
||||||
|
| 接口 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/v1/feedback/upload-presigned-url` | POST | 获取图片上传地址 |
|
||||||
|
|
||||||
|
**请求体(映射后)**:
|
||||||
|
- `layer` (fileName):文件名
|
||||||
|
|
||||||
|
**响应 data(映射后)**:
|
||||||
|
- `shed` (uploadUrl):上传 URL,用于 PUT 请求
|
||||||
|
- `hunt` (filePath):文件路径,用于 submit 的 fileUrls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 上传图片
|
||||||
|
|
||||||
|
使用 **PUT** 方式将所选图片上传到上一步返回的 `uploadUrl`。
|
||||||
|
|
||||||
|
- 不经过代理,直接请求返回的 URL
|
||||||
|
- 请求体为图片二进制数据
|
||||||
|
- Content-Type 按 HTTP 规范填写(如 `image/jpeg`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 提交反馈
|
||||||
|
|
||||||
|
| 接口 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/v1/feedback/submit` | POST | 提交举报内容 |
|
||||||
|
|
||||||
|
**请求体(映射后)**:
|
||||||
|
- `inventory` (fileUrls):文件路径列表,填入 2.1 返回的 `filePath`
|
||||||
|
- `cloak` (content):用户输入的文字描述
|
||||||
|
- `pauldron` (contentType):内容类型,按 HTTP 格式填写(如 `text/plain`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、流程概览
|
||||||
|
|
||||||
|
```
|
||||||
|
用户填写描述 + 选择图片
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
校验:文字 + 图片均必填
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POST /v1/feedback/upload-presigned-url
|
||||||
|
body: { layer: fileName }
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
获取 uploadUrl、filePath
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
PUT 图片到 uploadUrl
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POST /v1/feedback/submit
|
||||||
|
body: { inventory: [filePath], cloak: content, pauldron: contentType }
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
提交成功,关闭弹窗
|
||||||
|
```
|
||||||
@ -4,8 +4,8 @@ FLUTTER_APPLICATION_PATH=/Users/sven/Project/Workplace/app_client
|
|||||||
COCOAPODS_PARALLEL_CODE_SIGN=true
|
COCOAPODS_PARALLEL_CODE_SIGN=true
|
||||||
FLUTTER_TARGET=lib/main.dart
|
FLUTTER_TARGET=lib/main.dart
|
||||||
FLUTTER_BUILD_DIR=build
|
FLUTTER_BUILD_DIR=build
|
||||||
FLUTTER_BUILD_NAME=1.0.2
|
FLUTTER_BUILD_NAME=1.0.9
|
||||||
FLUTTER_BUILD_NUMBER=3
|
FLUTTER_BUILD_NUMBER=10
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||||
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
|
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
|
||||||
DART_OBFUSCATION=false
|
DART_OBFUSCATION=false
|
||||||
|
|||||||
@ -5,8 +5,8 @@ export "FLUTTER_APPLICATION_PATH=/Users/sven/Project/Workplace/app_client"
|
|||||||
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
||||||
export "FLUTTER_TARGET=lib/main.dart"
|
export "FLUTTER_TARGET=lib/main.dart"
|
||||||
export "FLUTTER_BUILD_DIR=build"
|
export "FLUTTER_BUILD_DIR=build"
|
||||||
export "FLUTTER_BUILD_NAME=1.0.2"
|
export "FLUTTER_BUILD_NAME=1.0.9"
|
||||||
export "FLUTTER_BUILD_NUMBER=3"
|
export "FLUTTER_BUILD_NUMBER=10"
|
||||||
export "DART_OBFUSCATION=false"
|
export "DART_OBFUSCATION=false"
|
||||||
export "TRACK_WIDGET_CREATION=true"
|
export "TRACK_WIDGET_CREATION=true"
|
||||||
export "TREE_SHAKE_ICONS=false"
|
export "TREE_SHAKE_ICONS=false"
|
||||||
|
|||||||
@ -12,8 +12,8 @@ abstract final class ApiConfig {
|
|||||||
static const String packageName = 'com.petsheroai.app';
|
static const String packageName = 'com.petsheroai.app';
|
||||||
|
|
||||||
/// 预发环境域名
|
/// 预发环境域名
|
||||||
static const String preBaseUrl =
|
static const String preBaseUrl = 'https://pre-ai.petsheroai.xyz';
|
||||||
'https://ai.petsheroai.xyz'; //'https://pre-ai.petsheroai.xyz';
|
//'https://ai.petsheroai.xyz'; //'https://pre-ai.petsheroai.xyz';
|
||||||
|
|
||||||
/// 生产环境域名
|
/// 生产环境域名
|
||||||
static const String prodBaseUrl = 'https://ai.petsheroai.xyz';
|
static const String prodBaseUrl = 'https://ai.petsheroai.xyz';
|
||||||
|
|||||||
38
lib/core/api/services/feedback_api.dart
Normal file
38
lib/core/api/services/feedback_api.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import '../api_client.dart';
|
||||||
|
import '../proxy_client.dart';
|
||||||
|
|
||||||
|
/// 举报/反馈相关 API
|
||||||
|
abstract final class FeedbackApi {
|
||||||
|
static final _client = ApiClient.instance.proxy;
|
||||||
|
|
||||||
|
/// 获取反馈图片上传预签名 URL
|
||||||
|
/// body: layer (fileName)
|
||||||
|
/// 返回 data: shed (uploadUrl), hunt (filePath)
|
||||||
|
static Future<ApiResponse> getUploadPresignedUrl({
|
||||||
|
required String fileName,
|
||||||
|
}) async {
|
||||||
|
return _client.request(
|
||||||
|
path: '/v1/feedback/upload-presigned-url',
|
||||||
|
method: 'POST',
|
||||||
|
body: {'layer': fileName},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交反馈
|
||||||
|
/// body: inventory (fileUrls), cloak (content), pauldron (contentType)
|
||||||
|
static Future<ApiResponse> submit({
|
||||||
|
required List<String> fileUrls,
|
||||||
|
required String content,
|
||||||
|
required String contentType,
|
||||||
|
}) async {
|
||||||
|
return _client.request(
|
||||||
|
path: '/v1/feedback/submit',
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
'inventory': fileUrls,
|
||||||
|
'cloak': content,
|
||||||
|
'pauldron': contentType,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -97,4 +97,19 @@ abstract final class UserApi {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 删除账号
|
||||||
|
static Future<ApiResponse> deleteAccount({
|
||||||
|
required String sentinel,
|
||||||
|
String? asset,
|
||||||
|
}) async {
|
||||||
|
return _client.request(
|
||||||
|
path: '/v1/user/delete',
|
||||||
|
method: 'GET',
|
||||||
|
queryParams: {
|
||||||
|
'sentinel': sentinel,
|
||||||
|
if (asset != null) 'asset': asset,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,26 +32,49 @@ class GalleryTaskItem {
|
|||||||
factory GalleryTaskItem.fromJson(Map<String, dynamic> json) {
|
factory GalleryTaskItem.fromJson(Map<String, dynamic> json) {
|
||||||
final downsample = json['downsample'] as List<dynamic>? ?? [];
|
final downsample = json['downsample'] as List<dynamic>? ?? [];
|
||||||
final items = <GalleryMediaItem>[];
|
final items = <GalleryMediaItem>[];
|
||||||
for (final item in downsample) {
|
// 只取downsample的array[0]
|
||||||
if (item is String) {
|
if (downsample.isNotEmpty) {
|
||||||
items.add(GalleryMediaItem(imageUrl: item));
|
final first = downsample[0];
|
||||||
} else if (item is Map<String, dynamic>) {
|
if (first is String) {
|
||||||
final reconfigure = item['reconfigure'] as String?;
|
items.add(GalleryMediaItem(imageUrl: first));
|
||||||
if (reconfigure == null || reconfigure.isEmpty) continue;
|
} else if (first is Map<String, dynamic>) {
|
||||||
// reconnect(imgType): 0=视频,1=图片,其他默认当图片
|
final reconfigure = first['reconfigure'] as String?;
|
||||||
final reconnect = item['reconnect'];
|
if (reconfigure != null && reconfigure.isNotEmpty) {
|
||||||
final imgType = reconnect is int
|
final reconnect = first['reconnect'];
|
||||||
? reconnect
|
final imgType = reconnect is int
|
||||||
: reconnect is num
|
? reconnect
|
||||||
? reconnect.toInt()
|
: reconnect is num
|
||||||
: 1;
|
? reconnect.toInt()
|
||||||
if (imgType == 2) {
|
: 1;
|
||||||
items.add(GalleryMediaItem(videoUrl: reconfigure));
|
if (imgType == 2) {
|
||||||
} else {
|
items.add(GalleryMediaItem(videoUrl: reconfigure));
|
||||||
items.add(GalleryMediaItem(imageUrl: reconfigure));
|
} else {
|
||||||
|
items.add(GalleryMediaItem(imageUrl: reconfigure));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 后续下标均忽略
|
||||||
}
|
}
|
||||||
|
// for (final item in downsample) {
|
||||||
|
// if (item is String) {
|
||||||
|
// items.add(GalleryMediaItem(imageUrl: item));
|
||||||
|
// } else if (item is Map<String, dynamic>) {
|
||||||
|
// final reconfigure = item['reconfigure'] as String?;
|
||||||
|
// if (reconfigure == null || reconfigure.isEmpty) continue;
|
||||||
|
// // reconnect(imgType): 0=视频,1=图片,其他默认当图片
|
||||||
|
// final reconnect = item['reconnect'];
|
||||||
|
// final imgType = reconnect is int
|
||||||
|
// ? reconnect
|
||||||
|
// : reconnect is num
|
||||||
|
// ? reconnect.toInt()
|
||||||
|
// : 1;
|
||||||
|
// if (imgType == 2) {
|
||||||
|
// items.add(GalleryMediaItem(videoUrl: reconfigure));
|
||||||
|
// } else {
|
||||||
|
// items.add(GalleryMediaItem(imageUrl: reconfigure));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
return GalleryTaskItem(
|
return GalleryTaskItem(
|
||||||
taskId: (json['tree'] as num?)?.toInt() ?? 0,
|
taskId: (json['tree'] as num?)?.toInt() ?? 0,
|
||||||
state: json['listing']?.toString() ?? '',
|
state: json['listing']?.toString() ?? '',
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
import '../../core/api/services/feedback_api.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
import '../../core/theme/app_spacing.dart';
|
import '../../core/theme/app_spacing.dart';
|
||||||
import '../../core/theme/app_typography.dart';
|
import '../../core/theme/app_typography.dart';
|
||||||
@ -228,9 +230,7 @@ class _MediaDisplay extends StatelessWidget {
|
|||||||
top: 16,
|
top: 16,
|
||||||
right: 20,
|
right: 20,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () => _ReportDialog.show(context),
|
||||||
// TODO: Report action
|
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 44,
|
width: 44,
|
||||||
height: 44,
|
height: 44,
|
||||||
@ -399,6 +399,292 @@ class _DownloadButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Report dialog - matches Pencil 5qKUB
|
||||||
|
class _ReportDialog extends StatefulWidget {
|
||||||
|
const _ReportDialog({required this.parentContext});
|
||||||
|
|
||||||
|
final BuildContext parentContext;
|
||||||
|
|
||||||
|
static void show(BuildContext context) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierColor: Colors.black54,
|
||||||
|
builder: (_) => _ReportDialog(parentContext: context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ReportDialog> createState() => _ReportDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReportDialogState extends State<_ReportDialog> {
|
||||||
|
final _controller = TextEditingController();
|
||||||
|
File? _pickedImage;
|
||||||
|
final _picker = ImagePicker();
|
||||||
|
bool _submitting = false;
|
||||||
|
String? _errorText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickImage() async {
|
||||||
|
final x = await _picker.pickImage(source: ImageSource.gallery);
|
||||||
|
if (x != null && mounted) {
|
||||||
|
setState(() => _pickedImage = File(x.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
final content = _controller.text.trim();
|
||||||
|
if (content.isEmpty) {
|
||||||
|
setState(() => _errorText = 'Please describe the issue');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_pickedImage == null) {
|
||||||
|
setState(() => _errorText = 'Please upload an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_errorText = null;
|
||||||
|
_submitting = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final file = _pickedImage!;
|
||||||
|
final ext = file.path.split('.').last.toLowerCase();
|
||||||
|
final contentType = ext == 'png'
|
||||||
|
? 'image/png'
|
||||||
|
: ext == 'gif'
|
||||||
|
? 'image/gif'
|
||||||
|
: 'image/jpeg';
|
||||||
|
final fileName =
|
||||||
|
'feedback_${DateTime.now().millisecondsSinceEpoch}.$ext';
|
||||||
|
|
||||||
|
final presignedRes =
|
||||||
|
await FeedbackApi.getUploadPresignedUrl(fileName: fileName);
|
||||||
|
if (!presignedRes.isSuccess || presignedRes.data == null) {
|
||||||
|
throw Exception(
|
||||||
|
presignedRes.msg.isNotEmpty ? presignedRes.msg : 'Failed to get upload URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = presignedRes.data as Map<String, dynamic>;
|
||||||
|
final uploadUrl = data['shed'] as String?;
|
||||||
|
final filePath = data['hunt'] as String?;
|
||||||
|
|
||||||
|
if (uploadUrl == null ||
|
||||||
|
uploadUrl.isEmpty ||
|
||||||
|
filePath == null ||
|
||||||
|
filePath.isEmpty) {
|
||||||
|
throw Exception('Invalid presigned URL response');
|
||||||
|
}
|
||||||
|
|
||||||
|
final bytes = await file.readAsBytes();
|
||||||
|
final uploadResponse = await http.put(
|
||||||
|
Uri.parse(uploadUrl),
|
||||||
|
headers: {'Content-Type': contentType},
|
||||||
|
body: bytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uploadResponse.statusCode < 200 ||
|
||||||
|
uploadResponse.statusCode >= 300) {
|
||||||
|
throw Exception('Upload failed: ${uploadResponse.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final submitRes = await FeedbackApi.submit(
|
||||||
|
fileUrls: [filePath],
|
||||||
|
content: content,
|
||||||
|
contentType: 'text/plain',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!submitRes.isSuccess) {
|
||||||
|
throw Exception(
|
||||||
|
submitRes.msg.isNotEmpty ? submitRes.msg : 'Failed to submit report');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
if (widget.parentContext.mounted) {
|
||||||
|
ScaffoldMessenger.of(widget.parentContext).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Report submitted'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _errorText = e.toString().replaceAll('Exception: ', ''));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _submitting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Container(
|
||||||
|
width: 342,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.15),
|
||||||
|
blurRadius: 24,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Report',
|
||||||
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Navigator.of(context).pop(),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: Icon(
|
||||||
|
LucideIcons.x,
|
||||||
|
size: 24,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Container(
|
||||||
|
height: 120,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.background,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: AppColors.border),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
maxLines: null,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Describe the issue...',
|
||||||
|
hintStyle: TextStyle(color: AppColors.textMuted),
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _pickImage,
|
||||||
|
child: Container(
|
||||||
|
height: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surfaceAlt,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xFFD4D4D8),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _pickedImage != null
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Image.file(
|
||||||
|
_pickedImage!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
LucideIcons.image_plus,
|
||||||
|
size: 32,
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Tap to upload image',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_errorText != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
_errorText!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.accentOrange,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _submitting ? null : _submit,
|
||||||
|
child: Container(
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _submitting
|
||||||
|
? AppColors.textMuted
|
||||||
|
: const Color(0xFF7C3AED),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: _submitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: AppColors.surface,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'Submit',
|
||||||
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
|
color: AppColors.surface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ShareButton extends StatelessWidget {
|
class _ShareButton extends StatelessWidget {
|
||||||
const _ShareButton({required this.onShare});
|
const _ShareButton({required this.onShare});
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
|
||||||
|
import '../../core/api/api_client.dart';
|
||||||
|
import '../../core/api/api_config.dart';
|
||||||
|
import '../../core/api/services/user_api.dart';
|
||||||
import '../../core/user/account_refresh.dart';
|
import '../../core/user/account_refresh.dart';
|
||||||
import '../recharge/payment_webview_screen.dart';
|
import '../recharge/payment_webview_screen.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
@ -81,7 +84,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => Navigator.of(context).push(
|
||||||
MaterialPageRoute<void>(
|
MaterialPageRoute<void>(
|
||||||
builder: (_) => const PaymentWebViewScreen(
|
builder: (_) => const PaymentWebViewScreen(
|
||||||
paymentUrl: 'http://www.petsheroai.xyz/privacy.html',
|
paymentUrl: 'https://www.petsheroai.xyz/privacy.html',
|
||||||
title: 'Privacy Policy',
|
title: 'Privacy Policy',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -93,12 +96,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => Navigator.of(context).push(
|
||||||
MaterialPageRoute<void>(
|
MaterialPageRoute<void>(
|
||||||
builder: (_) => const PaymentWebViewScreen(
|
builder: (_) => const PaymentWebViewScreen(
|
||||||
paymentUrl: 'http://www.petsheroai.xyz/terms.html',
|
paymentUrl: 'https://www.petsheroai.xyz/terms.html',
|
||||||
title: 'User Agreement',
|
title: 'User Agreement',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_MenuItem(
|
||||||
|
title: 'Delete Account',
|
||||||
|
icon: LucideIcons.trash_2,
|
||||||
|
iconColor: const Color(0xFFDC2626),
|
||||||
|
onTap: () => _DeleteAccountDialog.show(context),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -296,11 +305,13 @@ class _MenuItem extends StatelessWidget {
|
|||||||
required this.title,
|
required this.title,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.iconColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final Color? iconColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -328,13 +339,257 @@ class _MenuItem extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: AppTypography.bodyRegular.copyWith(
|
style: AppTypography.bodyRegular.copyWith(
|
||||||
color: AppColors.textPrimary,
|
color: iconColor ?? AppColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Icon(icon, size: 20, color: AppColors.textMuted),
|
Icon(icon, size: 20, color: iconColor ?? AppColors.textMuted),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete Account confirmation dialog - matches Pencil Xp6Qz
|
||||||
|
class _DeleteAccountDialog extends StatefulWidget {
|
||||||
|
const _DeleteAccountDialog({required this.parentContext});
|
||||||
|
|
||||||
|
final BuildContext parentContext;
|
||||||
|
|
||||||
|
static void show(BuildContext context) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierColor: const Color(0x80000000),
|
||||||
|
builder: (_) => _DeleteAccountDialog(parentContext: context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DeleteAccountDialog> createState() => _DeleteAccountDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeleteAccountDialogState extends State<_DeleteAccountDialog> {
|
||||||
|
bool _deleting = false;
|
||||||
|
String? _errorText;
|
||||||
|
final _verifyController = TextEditingController();
|
||||||
|
|
||||||
|
static const _verifyCode = 'DELETE';
|
||||||
|
|
||||||
|
bool get _isVerifyMatch =>
|
||||||
|
_verifyController.text.trim().toUpperCase() == _verifyCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_verifyController.addListener(() => setState(() {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_verifyController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onDelete() async {
|
||||||
|
setState(() {
|
||||||
|
_errorText = null;
|
||||||
|
_deleting = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final res = await UserApi.deleteAccount(
|
||||||
|
sentinel: ApiConfig.appId,
|
||||||
|
asset: UserState.userId.value,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (res.isSuccess) {
|
||||||
|
// Clear user state and token
|
||||||
|
UserState.setCredits(null);
|
||||||
|
UserState.setUserId(null);
|
||||||
|
UserState.setAvatar(null);
|
||||||
|
UserState.setUserName(null);
|
||||||
|
UserState.setNavigate(null);
|
||||||
|
ApiClient.instance.setUserToken(null);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
if (widget.parentContext.mounted) {
|
||||||
|
ScaffoldMessenger.of(widget.parentContext).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Account deleted'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState(() => _errorText = res.msg.isNotEmpty ? res.msg : 'Delete failed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _errorText = e.toString().replaceAll('Exception: ', ''));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _deleting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
width: 342,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x26000000),
|
||||||
|
blurRadius: 24,
|
||||||
|
offset: Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Delete Account?',
|
||||||
|
style: AppTypography.bodyLarge.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _deleting ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: Icon(LucideIcons.x, size: 24, color: AppColors.textMuted),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'This action cannot be undone. All your data will be permanently deleted.',
|
||||||
|
style: AppTypography.bodyRegular.copyWith(
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'Type $_verifyCode to confirm',
|
||||||
|
style: AppTypography.label.copyWith(
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _verifyController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Type $_verifyCode here',
|
||||||
|
hintStyle: AppTypography.bodyRegular.copyWith(
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFFFAFAFA),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE4E4E7)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE4E4E7)),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: AppTypography.bodyRegular.copyWith(
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
),
|
||||||
|
if (_errorText != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
_errorText!,
|
||||||
|
style: AppTypography.caption.copyWith(color: Colors.red),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _deleting ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: Container(
|
||||||
|
height: 52,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF4F4F5),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Cancel',
|
||||||
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: (_deleting || !_isVerifyMatch) ? null : _onDelete,
|
||||||
|
child: Container(
|
||||||
|
height: 52,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _isVerifyMatch
|
||||||
|
? const Color(0xFFDC2626)
|
||||||
|
: Color(0xFFDC2626).withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: _deleting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'Delete',
|
||||||
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ class PaymentWebViewScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> {
|
class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> {
|
||||||
late final WebViewController _controller;
|
late final WebViewController _controller;
|
||||||
|
int _loadingProgress = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -31,8 +32,15 @@ class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> {
|
|||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
..setNavigationDelegate(
|
..setNavigationDelegate(
|
||||||
NavigationDelegate(
|
NavigationDelegate(
|
||||||
onPageStarted: (_) {},
|
onProgress: (progress) {
|
||||||
onPageFinished: (_) {},
|
if (mounted) setState(() => _loadingProgress = progress);
|
||||||
|
},
|
||||||
|
onPageStarted: (_) {
|
||||||
|
if (mounted) setState(() => _loadingProgress = 0);
|
||||||
|
},
|
||||||
|
onPageFinished: (_) {
|
||||||
|
if (mounted) setState(() => _loadingProgress = 100);
|
||||||
|
},
|
||||||
onWebResourceError: (e) {},
|
onWebResourceError: (e) {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -44,11 +52,27 @@ class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.surface,
|
backgroundColor: AppColors.surface,
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(56),
|
preferredSize: const Size.fromHeight(59),
|
||||||
child: TopNavBar(
|
child: Column(
|
||||||
title: widget.title,
|
mainAxisSize: MainAxisSize.min,
|
||||||
showBackButton: true,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
onBack: () => Navigator.of(context).pop(),
|
children: [
|
||||||
|
TopNavBar(
|
||||||
|
title: widget.title,
|
||||||
|
showBackButton: true,
|
||||||
|
onBack: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
if (_loadingProgress < 100)
|
||||||
|
SizedBox(
|
||||||
|
height: 3,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: _loadingProgress / 100,
|
||||||
|
backgroundColor: AppColors.surfaceAlt,
|
||||||
|
valueColor:
|
||||||
|
const AlwaysStoppedAnimation<Color>(AppColors.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
name: pets_hero_ai
|
name: pets_hero_ai
|
||||||
description: PetsHero AI Application.
|
description: PetsHero AI Application.
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.0.5+6
|
version: 1.1.1+12
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@ -31,13 +31,13 @@ dependencies:
|
|||||||
in_app_purchase: ^3.2.0
|
in_app_purchase: ^3.2.0
|
||||||
webview_flutter: ^4.10.0
|
webview_flutter: ^4.10.0
|
||||||
screen_secure: ^1.0.3
|
screen_secure: ^1.0.3
|
||||||
|
flutter_native_splash: ^2.4.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.0
|
flutter_lints: ^3.0.0
|
||||||
flutter_launcher_icons: ^0.14.4
|
flutter_launcher_icons: ^0.14.4
|
||||||
flutter_native_splash: ^2.4.7
|
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: true
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user