打包:版本发布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">
|
||||
<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.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
<application
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||
|
||||
@ -2293,6 +2293,52 @@
|
||||
"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
|
||||
FLUTTER_TARGET=lib/main.dart
|
||||
FLUTTER_BUILD_DIR=build
|
||||
FLUTTER_BUILD_NAME=1.0.2
|
||||
FLUTTER_BUILD_NUMBER=3
|
||||
FLUTTER_BUILD_NAME=1.0.9
|
||||
FLUTTER_BUILD_NUMBER=10
|
||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
|
||||
DART_OBFUSCATION=false
|
||||
|
||||
@ -5,8 +5,8 @@ export "FLUTTER_APPLICATION_PATH=/Users/sven/Project/Workplace/app_client"
|
||||
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
||||
export "FLUTTER_TARGET=lib/main.dart"
|
||||
export "FLUTTER_BUILD_DIR=build"
|
||||
export "FLUTTER_BUILD_NAME=1.0.2"
|
||||
export "FLUTTER_BUILD_NUMBER=3"
|
||||
export "FLUTTER_BUILD_NAME=1.0.9"
|
||||
export "FLUTTER_BUILD_NUMBER=10"
|
||||
export "DART_OBFUSCATION=false"
|
||||
export "TRACK_WIDGET_CREATION=true"
|
||||
export "TREE_SHAKE_ICONS=false"
|
||||
|
||||
@ -12,8 +12,8 @@ abstract final class ApiConfig {
|
||||
static const String packageName = 'com.petsheroai.app';
|
||||
|
||||
/// 预发环境域名
|
||||
static const String preBaseUrl =
|
||||
'https://ai.petsheroai.xyz'; //'https://pre-ai.petsheroai.xyz';
|
||||
static const String preBaseUrl = 'https://pre-ai.petsheroai.xyz';
|
||||
//'https://ai.petsheroai.xyz'; //'https://pre-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) {
|
||||
final downsample = json['downsample'] as List<dynamic>? ?? [];
|
||||
final items = <GalleryMediaItem>[];
|
||||
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));
|
||||
// 只取downsample的array[0]
|
||||
if (downsample.isNotEmpty) {
|
||||
final first = downsample[0];
|
||||
if (first is String) {
|
||||
items.add(GalleryMediaItem(imageUrl: first));
|
||||
} else if (first is Map<String, dynamic>) {
|
||||
final reconfigure = first['reconfigure'] as String?;
|
||||
if (reconfigure != null && reconfigure.isNotEmpty) {
|
||||
final reconnect = first['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));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 后续下标均忽略
|
||||
}
|
||||
// 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(
|
||||
taskId: (json['tree'] as num?)?.toInt() ?? 0,
|
||||
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:gal/gal.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path_provider/path_provider.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_spacing.dart';
|
||||
import '../../core/theme/app_typography.dart';
|
||||
@ -228,9 +230,7 @@ class _MediaDisplay extends StatelessWidget {
|
||||
top: 16,
|
||||
right: 20,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
// TODO: Report action
|
||||
},
|
||||
onTap: () => _ReportDialog.show(context),
|
||||
child: Container(
|
||||
width: 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 {
|
||||
const _ShareButton({required this.onShare});
|
||||
|
||||
|
||||
@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.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 '../recharge/payment_webview_screen.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
@ -81,7 +84,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const PaymentWebViewScreen(
|
||||
paymentUrl: 'http://www.petsheroai.xyz/privacy.html',
|
||||
paymentUrl: 'https://www.petsheroai.xyz/privacy.html',
|
||||
title: 'Privacy Policy',
|
||||
),
|
||||
),
|
||||
@ -93,12 +96,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const PaymentWebViewScreen(
|
||||
paymentUrl: 'http://www.petsheroai.xyz/terms.html',
|
||||
paymentUrl: 'https://www.petsheroai.xyz/terms.html',
|
||||
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.icon,
|
||||
required this.onTap,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final Color? iconColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -328,13 +339,257 @@ class _MenuItem extends StatelessWidget {
|
||||
Text(
|
||||
title,
|
||||
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> {
|
||||
late final WebViewController _controller;
|
||||
int _loadingProgress = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -31,8 +32,15 @@ class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> {
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageStarted: (_) {},
|
||||
onPageFinished: (_) {},
|
||||
onProgress: (progress) {
|
||||
if (mounted) setState(() => _loadingProgress = progress);
|
||||
},
|
||||
onPageStarted: (_) {
|
||||
if (mounted) setState(() => _loadingProgress = 0);
|
||||
},
|
||||
onPageFinished: (_) {
|
||||
if (mounted) setState(() => _loadingProgress = 100);
|
||||
},
|
||||
onWebResourceError: (e) {},
|
||||
),
|
||||
)
|
||||
@ -44,11 +52,27 @@ class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surface,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(56),
|
||||
child: TopNavBar(
|
||||
title: widget.title,
|
||||
showBackButton: true,
|
||||
onBack: () => Navigator.of(context).pop(),
|
||||
preferredSize: const Size.fromHeight(59),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
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(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
name: pets_hero_ai
|
||||
description: PetsHero AI Application.
|
||||
publish_to: 'none'
|
||||
version: 1.0.5+6
|
||||
version: 1.1.1+12
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@ -31,13 +31,13 @@ dependencies:
|
||||
in_app_purchase: ^3.2.0
|
||||
webview_flutter: ^4.10.0
|
||||
screen_secure: ^1.0.3
|
||||
flutter_native_splash: ^2.4.7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^3.0.0
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
flutter_native_splash: ^2.4.7
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user