初始化
This commit is contained in:
16
lib/core/config/global.dart
Normal file
16
lib/core/config/global.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
class Config {
|
||||
///获取环境
|
||||
static String getEnv() {
|
||||
const env = String.fromEnvironment('ENV', defaultValue: 'dev');
|
||||
return env;
|
||||
}
|
||||
|
||||
///获取接口地址
|
||||
static String baseUrl() {
|
||||
if (getEnv() == 'dev') {
|
||||
return 'https://mindapp.test.tuzuu.com/api';
|
||||
} else {
|
||||
return 'https://mindapp.cells.org.cn/api';
|
||||
}
|
||||
}
|
||||
}
|
||||
0
lib/core/constants/values.dart
Normal file
0
lib/core/constants/values.dart
Normal file
30
lib/core/event/event_bus.dart
Normal file
30
lib/core/event/event_bus.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'global_event.dart';
|
||||
|
||||
/// 全局事件总线
|
||||
class EventBus {
|
||||
EventBus._();
|
||||
|
||||
static final _instance = EventBus._();
|
||||
|
||||
factory EventBus() => _instance;
|
||||
|
||||
// StreamController,广播模式可以让多个地方监听
|
||||
final StreamController<GlobalEvent> _controller = StreamController.broadcast();
|
||||
|
||||
/// 发送事件
|
||||
void publish(GlobalEvent event) {
|
||||
_controller.add(event);
|
||||
}
|
||||
|
||||
/// 监听事件
|
||||
Stream<GlobalEvent> get stream => _controller.stream;
|
||||
|
||||
|
||||
|
||||
/// 关闭流(一般应用生命周期结束时调用)
|
||||
void dispose() {
|
||||
_controller.close();
|
||||
}
|
||||
}
|
||||
16
lib/core/event/global_event.dart
Normal file
16
lib/core/event/global_event.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:app/data/models/common/version_dto.dart';
|
||||
|
||||
///基类
|
||||
abstract class GlobalEvent {
|
||||
const GlobalEvent();
|
||||
}
|
||||
|
||||
|
||||
///重新登录
|
||||
class UnauthorizedEvent extends GlobalEvent {}
|
||||
|
||||
///版本更新
|
||||
class VersionUpdateEvent extends GlobalEvent {
|
||||
final VersionDto version;
|
||||
const VersionUpdateEvent(this.version);
|
||||
}
|
||||
65
lib/core/network/interceptor.dart
Normal file
65
lib/core/network/interceptor.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:app/data/models/api_dto.dart';
|
||||
import 'package:app/data/repository/auto_repo.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
|
||||
import '../event/event_bus.dart';
|
||||
import '../event/global_event.dart';
|
||||
|
||||
///请求拦截器
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
String? token = await AuthRepo.getToken();
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
return handler.next(options);
|
||||
}
|
||||
|
||||
///响应拦截器
|
||||
void onResponse(Response<dynamic> response, ResponseInterceptorHandler handler) async {
|
||||
var apiResponse = ApiDto.fromJson(response.data);
|
||||
if (apiResponse.code == 1) {
|
||||
response.data = apiResponse.data;
|
||||
handler.next(response);
|
||||
} else if (apiResponse.code == 401) {
|
||||
final bus = EventBus();
|
||||
bus.publish(UnauthorizedEvent());
|
||||
handler.next(response);
|
||||
} else {
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
error: {'code': apiResponse.code, 'message': apiResponse.message},
|
||||
),
|
||||
);
|
||||
showError(apiResponse.message);
|
||||
}
|
||||
}
|
||||
|
||||
///错误响应
|
||||
void onError(DioException e, ErrorInterceptorHandler handler) async {
|
||||
var title = "";
|
||||
if (e.type == DioExceptionType.connectionTimeout) {
|
||||
title = "请求超时";
|
||||
} else if (e.type == DioExceptionType.badResponse) {
|
||||
if (e.response?.statusCode == 401) {
|
||||
final bus = EventBus();
|
||||
bus.publish(UnauthorizedEvent());
|
||||
title = "登录信息已失效,请重新登录";
|
||||
} else if (e.response?.statusCode == 404) {
|
||||
title = "接口404不存在";
|
||||
} else {
|
||||
title = "500";
|
||||
}
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
title = "网络连接失败";
|
||||
} else {
|
||||
title = "异常其他错误";
|
||||
}
|
||||
showError(title);
|
||||
handler.next(e);
|
||||
}
|
||||
|
||||
///显示错误信息
|
||||
void showError(String message) {
|
||||
EasyLoading.showError(message);
|
||||
}
|
||||
43
lib/core/network/request.dart
Normal file
43
lib/core/network/request.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../config/global.dart';
|
||||
import 'interceptor.dart';
|
||||
|
||||
class Request {
|
||||
static Dio _dio = Dio();
|
||||
|
||||
//返回单例
|
||||
factory Request() {
|
||||
return Request._instance();
|
||||
}
|
||||
|
||||
//初始化
|
||||
Request._instance() {
|
||||
//创建基本配置
|
||||
final BaseOptions options = BaseOptions(
|
||||
baseUrl: Config.baseUrl(),
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
);
|
||||
|
||||
_dio = Dio(options);
|
||||
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: onRequest,
|
||||
onResponse: onResponse,
|
||||
onError: onError,
|
||||
));
|
||||
}
|
||||
|
||||
///get请求
|
||||
Future<T> get<T>(String path, [Map<String, dynamic>? params]) async {
|
||||
var res = await _dio.get(path, queryParameters: params);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
///post请求
|
||||
Future<T> post<T>(String path, Object? data) async {
|
||||
var res = await _dio.post(path, data: data);
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
37
lib/core/theme/base/app_colors_base.dart
Normal file
37
lib/core/theme/base/app_colors_base.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'dart:ui';
|
||||
|
||||
abstract class AppColorsBase {
|
||||
/// 品牌主色
|
||||
Color get primaryStart;
|
||||
|
||||
Color get primaryEnd;
|
||||
|
||||
// 灰度
|
||||
Color get textPrimary;
|
||||
|
||||
Color get textSecondary;
|
||||
|
||||
Color get textTertiary;
|
||||
|
||||
// 状态颜色
|
||||
Color get success;
|
||||
|
||||
Color get warning;
|
||||
|
||||
Color get info;
|
||||
|
||||
Color get danger;
|
||||
|
||||
|
||||
// 容器色
|
||||
Color get surfaceContainerLowest;
|
||||
|
||||
Color get surfaceContainerLow;
|
||||
|
||||
Color get surfaceContainer;
|
||||
|
||||
Color get surfaceContainerHigh; //白色卡片 / item
|
||||
|
||||
//阴影颜色
|
||||
Color get shadow;
|
||||
}
|
||||
54
lib/core/theme/base/app_text_style.dart
Normal file
54
lib/core/theme/base/app_text_style.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_colors_base.dart'; // 假设你之前定义了 AppColorsBase
|
||||
|
||||
TextTheme buildTextTheme(AppColorsBase colors) {
|
||||
return TextTheme(
|
||||
// 标题层级
|
||||
titleLarge: TextStyle(
|
||||
fontSize: 22, // 比正文大6
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
fontSize: 20, // 比正文大4
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
titleSmall: TextStyle(
|
||||
fontSize: 18, // 比正文大2
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
|
||||
// 正文字体
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: 18, // 稍大正文
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: 16, // 正文标准
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontSize: 14, // 辅助正文
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
|
||||
// 标签/提示文字
|
||||
labelLarge: TextStyle(
|
||||
fontSize: 14, // 比正文小一点
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
56
lib/core/theme/base/app_theme_ext.dart
Normal file
56
lib/core/theme/base/app_theme_ext.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_colors_base.dart';
|
||||
|
||||
@immutable
|
||||
class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
|
||||
final AppColorsBase baseTheme;
|
||||
|
||||
const AppThemeExtension({required this.baseTheme});
|
||||
|
||||
@override
|
||||
ThemeExtension<AppThemeExtension> copyWith({
|
||||
AppColorsBase? baseTheme,
|
||||
}) {
|
||||
return AppThemeExtension(
|
||||
baseTheme: baseTheme ?? this.baseTheme,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AppThemeExtension lerp(AppThemeExtension? other, double t) {
|
||||
if (other is! AppThemeExtension) return this;
|
||||
return t < 0.5 ? this : other; // 或者 this/base 混合逻辑
|
||||
}
|
||||
}
|
||||
|
||||
extension AppThemeExt on BuildContext {
|
||||
AppThemeExtension get themeEx => Theme.of(this).extension<AppThemeExtension>()!;
|
||||
|
||||
Gradient get primaryGradient => LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [themeEx.baseTheme.primaryStart, themeEx.baseTheme.primaryEnd],
|
||||
);
|
||||
|
||||
|
||||
Color get primaryStart => themeEx.baseTheme.primaryStart;
|
||||
|
||||
Color get primaryEnd => themeEx.baseTheme.primaryEnd;
|
||||
|
||||
///主题
|
||||
Color get success => themeEx.baseTheme.success;
|
||||
|
||||
Color get warning => themeEx.baseTheme.warning;
|
||||
|
||||
Color get danger => themeEx.baseTheme.danger;
|
||||
|
||||
Color get info => themeEx.baseTheme.info;
|
||||
|
||||
//字体灰度
|
||||
Color get textSecondary => themeEx.baseTheme.textSecondary;
|
||||
|
||||
Color get textTertiary => themeEx.baseTheme.textTertiary;
|
||||
|
||||
double get pagePadding => 12;
|
||||
}
|
||||
42
lib/core/theme/theme.dart
Normal file
42
lib/core/theme/theme.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'base/app_colors_base.dart';
|
||||
import 'base/app_text_style.dart';
|
||||
import 'base/app_theme_ext.dart';
|
||||
|
||||
export 'themes/light_theme.dart';
|
||||
export 'base/app_theme_ext.dart';
|
||||
|
||||
class AppTheme {
|
||||
static ThemeData createTheme(AppColorsBase themeBase) {
|
||||
final textTheme = buildTextTheme(themeBase);
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: "资源圆体",
|
||||
scaffoldBackgroundColor: themeBase.surfaceContainerHigh,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
primary: themeBase.primaryStart,
|
||||
seedColor: themeBase.primaryStart,
|
||||
brightness: Brightness.light,
|
||||
|
||||
onSurfaceVariant: themeBase.textSecondary,
|
||||
//背景色
|
||||
surfaceContainerHigh: themeBase.surfaceContainerHigh,
|
||||
surfaceContainer: themeBase.surfaceContainer,
|
||||
surfaceContainerLow: themeBase.surfaceContainerLow,
|
||||
surfaceContainerLowest: themeBase.surfaceContainerLowest,
|
||||
//阴影
|
||||
shadow: themeBase.shadow,
|
||||
),
|
||||
textTheme: textTheme,
|
||||
extensions: [AppThemeExtension(baseTheme: themeBase)],
|
||||
// pageTransitionsTheme: const PageTransitionsTheme(),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Colors.white,
|
||||
titleTextStyle: textTheme.titleMedium,
|
||||
scrolledUnderElevation: 0,
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
lib/core/theme/themes/light_theme.dart
Normal file
49
lib/core/theme/themes/light_theme.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../base/app_colors_base.dart';
|
||||
|
||||
class LightTheme extends AppColorsBase {
|
||||
@override
|
||||
Color get primaryStart => const Color(0xff44EAB3);
|
||||
|
||||
@override
|
||||
Color get primaryEnd => const Color(0xff1EDCDA);
|
||||
|
||||
@override
|
||||
Color get textPrimary => const Color(0xff222932);
|
||||
|
||||
@override
|
||||
Color get textSecondary => const Color(0xffA6B0BE);
|
||||
|
||||
@override
|
||||
Color get textTertiary => const Color(0xffC7CDD5);
|
||||
|
||||
@override
|
||||
Color get success => const Color(0xff00D4B5);
|
||||
|
||||
@override
|
||||
Color get warning => const Color(0xffFF9200);
|
||||
|
||||
@override
|
||||
Color get info => const Color(0xffededed);
|
||||
|
||||
@override
|
||||
Color get danger => const Color(0xffFF4900);
|
||||
|
||||
@override
|
||||
Color get surfaceContainerLowest => Color(0xffE0E0E0);
|
||||
|
||||
@override
|
||||
Color get surfaceContainerLow => Color(0xffF0F0F0);
|
||||
|
||||
@override
|
||||
Color get surfaceContainer => Color(0xFFF2F5FA);
|
||||
|
||||
@override
|
||||
Color get surfaceContainerHigh => Color(0xffFFFFFF);
|
||||
|
||||
@override
|
||||
Color get shadow => const Color.fromRGBO(0, 0, 0, 0.1);
|
||||
}
|
||||
32
lib/core/utils/format/time.dart
Normal file
32
lib/core/utils/format/time.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
/// 格式化时间
|
||||
String formatDate(dynamic date, [String format = 'YYYY-MM-DD hh:mm:ss']) {
|
||||
DateTime dateTime;
|
||||
|
||||
if (date is String) {
|
||||
// 如果是字符串类型,尝试将其解析为 DateTime
|
||||
dateTime = DateTime.tryParse(date) ?? DateTime.now();
|
||||
} else if (date is DateTime) {
|
||||
// 如果是 DateTime 类型,直接使用
|
||||
dateTime = date;
|
||||
} else {
|
||||
// 如果不是合法的输入类型,默认使用当前时间
|
||||
dateTime = DateTime.now();
|
||||
}
|
||||
|
||||
final yyyy = dateTime.year.toString();
|
||||
final MM = (dateTime.month).toString().padLeft(2, '0');
|
||||
final dd = (dateTime.day).toString().padLeft(2, '0');
|
||||
final HH = (dateTime.hour).toString().padLeft(2, '0');
|
||||
final mm = (dateTime.minute).toString().padLeft(2, '0');
|
||||
final ss = (dateTime.second).toString().padLeft(2, '0');
|
||||
|
||||
String result = format
|
||||
.replaceFirst(RegExp('YYYY'), '$yyyy')
|
||||
.replaceFirst(RegExp('MM'), MM)
|
||||
.replaceFirst(RegExp('DD'), dd)
|
||||
.replaceFirst(RegExp('hh'), HH)
|
||||
.replaceFirst(RegExp('mm'), mm)
|
||||
.replaceFirst(RegExp('ss'), ss);
|
||||
|
||||
return result;
|
||||
}
|
||||
54
lib/core/utils/storage.dart
Normal file
54
lib/core/utils/storage.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Storage {
|
||||
/// 存储数据
|
||||
static Future<void> set(String key, dynamic value) async {
|
||||
SharedPreferences sp = await SharedPreferences.getInstance();
|
||||
if (value is String) {
|
||||
sp.setString(key, value);
|
||||
} else if (value is int) {
|
||||
sp.setInt(key, value);
|
||||
} else if (value is bool) {
|
||||
sp.setBool(key, value);
|
||||
} else if (value is double) {
|
||||
sp.setDouble(key, value);
|
||||
} else if (value is Map) {
|
||||
String jsonStr = jsonEncode(value);
|
||||
sp.setString(key, jsonStr);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取数据
|
||||
/// - [decoder] 解码器,不传默认返回原数据
|
||||
static Future<T?> get<T>(
|
||||
String key, {
|
||||
T Function(dynamic json)? decoder,
|
||||
}) async {
|
||||
final sp = await SharedPreferences.getInstance();
|
||||
final value = sp.get(key);
|
||||
|
||||
//如果不存在
|
||||
if (value == null) return null;
|
||||
|
||||
// 如果需要反序列化
|
||||
if (decoder != null && value is String) {
|
||||
return decoder(jsonDecode(value));
|
||||
}
|
||||
|
||||
return value as T?;
|
||||
}
|
||||
|
||||
//删除数据
|
||||
static Future<void> remove(key) async {
|
||||
SharedPreferences sp = await SharedPreferences.getInstance();
|
||||
sp.remove(key);
|
||||
}
|
||||
|
||||
//判断键是否存在
|
||||
static Future<bool> hasKey(String key) async {
|
||||
SharedPreferences sp = await SharedPreferences.getInstance();
|
||||
return sp.containsKey(key);
|
||||
}
|
||||
}
|
||||
77
lib/core/utils/system.dart
Normal file
77
lib/core/utils/system.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
///判断是否是安卓
|
||||
bool isAndroid(){
|
||||
return Platform.isAndroid;
|
||||
}
|
||||
|
||||
/// 封装通用权限处理方法
|
||||
/// - [permissions] 需要检查的权限列表
|
||||
/// - [onGranted] 当所有权限都被授予时调用的回调
|
||||
/// - [onDenied] 当有权限被拒绝时调用的回调
|
||||
/// - [onPermanentlyDenied] 当有权限被永久拒绝时调用的回调(可选,默认打开设置页)
|
||||
Future<void> checkAndRequestPermissions({
|
||||
required List<Permission> permissions,
|
||||
required VoidCallback onGranted,
|
||||
VoidCallback? onDenied,
|
||||
VoidCallback? onPermanentlyDenied,
|
||||
}) async {
|
||||
// 判断当前权限状态
|
||||
Map<Permission, PermissionStatus> statuses = {};
|
||||
for (final permission in permissions) {
|
||||
statuses[permission] = await permission.status;
|
||||
}
|
||||
|
||||
// 筛选出未授权的权限
|
||||
final needRequest = statuses.entries.where((entry) => !entry.value.isGranted).map((entry) => entry.key).toList();
|
||||
// 如果全部已有权限
|
||||
if (needRequest.isEmpty) {
|
||||
onGranted();
|
||||
return;
|
||||
}
|
||||
|
||||
// 请求未授权的权限
|
||||
final requestResult = await needRequest.request();
|
||||
|
||||
//是否全部授权
|
||||
bool allGranted = true;
|
||||
//是否有任何一个授权了
|
||||
bool anyPermanentlyDenied = false;
|
||||
|
||||
for (final permission in permissions) {
|
||||
final status = requestResult[permission] ?? await permission.status;
|
||||
|
||||
if (status.isPermanentlyDenied) {
|
||||
anyPermanentlyDenied = true;
|
||||
allGranted = false;
|
||||
break;
|
||||
} else if (!status.isGranted) {
|
||||
allGranted = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (allGranted) {
|
||||
onGranted();
|
||||
} else if (anyPermanentlyDenied) {
|
||||
if (onPermanentlyDenied != null) {
|
||||
onPermanentlyDenied();
|
||||
} else {
|
||||
openAppSettings(); // 可选:默认打开设置页
|
||||
}
|
||||
} else {
|
||||
if (onDenied != null) {
|
||||
onDenied();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///复制文本到剪切板
|
||||
void copyToClipboard(String text) {
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
EasyLoading.showToast('已复制到剪切板');
|
||||
}
|
||||
81
lib/core/utils/transfer/download.dart
Normal file
81
lib/core/utils/transfer/download.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
//下载文件
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class LocalDownload {
|
||||
static Future<String> getLocalFilePath(String url, String path) async {
|
||||
Uri uri = Uri.parse(url);
|
||||
String fileName = uri.pathSegments.last;
|
||||
//获取下载目录
|
||||
Directory dir = await getApplicationCacheDirectory();
|
||||
Directory uploadPath = Directory("${dir.path}$path/");
|
||||
return uploadPath.path + fileName;
|
||||
}
|
||||
|
||||
/// 公用下载方法
|
||||
/// url 下载网络地址
|
||||
/// path 存储地址,如/test
|
||||
/// onProgress 下载回调函数
|
||||
/// onDone 下载完毕回调
|
||||
static downLoadFile({
|
||||
required url,
|
||||
required path,
|
||||
required Function(double) onProgress,
|
||||
required Function(String) onDone,
|
||||
}) async {
|
||||
HttpClient client = HttpClient();
|
||||
Uri uri = Uri.parse(url);
|
||||
//获取本地文件路径
|
||||
String filePath = await getLocalFilePath(url, path);
|
||||
// 发起 get 请求
|
||||
HttpClientRequest request = await client.getUrl(uri);
|
||||
// 响应
|
||||
HttpClientResponse response = await request.close();
|
||||
int contentLength = response.contentLength; // 获取文件总大小
|
||||
int bytesReceived = 0; // 已接收的字节数
|
||||
List<int> chunkList = [];
|
||||
if (response.statusCode == 200) {
|
||||
response.listen(
|
||||
(List<int> chunk) {
|
||||
chunkList.addAll(chunk);
|
||||
bytesReceived += chunk.length; //更新已接受的字节数
|
||||
//进度
|
||||
double progress = bytesReceived * 100 / contentLength * 100;
|
||||
progress = (progress / 100).truncateToDouble();
|
||||
onProgress(progress);
|
||||
},
|
||||
onDone: () async {
|
||||
//下载完毕
|
||||
client.close();
|
||||
File file = File(filePath);
|
||||
if (!file.existsSync()) {
|
||||
file.createSync(recursive: true);
|
||||
await file.writeAsBytes(chunkList);
|
||||
}
|
||||
onDone(file.path);
|
||||
},
|
||||
onError: () {
|
||||
client.close();
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///获取本地地址
|
||||
///
|
||||
static Future<String> getFilePath({
|
||||
required url,
|
||||
required path,
|
||||
}) async {
|
||||
//获取本地文件路径
|
||||
String filePath = await getLocalFilePath(url, path);
|
||||
File file = File(filePath);
|
||||
if (file.existsSync()) {
|
||||
return file.path;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
46
lib/core/utils/transfer/file_type.dart
Normal file
46
lib/core/utils/transfer/file_type.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
enum FileType {
|
||||
image,
|
||||
pdf,
|
||||
other,
|
||||
}
|
||||
|
||||
class FileMeta {
|
||||
final String name; // 不含后缀
|
||||
final String extension; // 不含点
|
||||
final FileType type;
|
||||
|
||||
const FileMeta({
|
||||
required this.name,
|
||||
required this.extension,
|
||||
required this.type,
|
||||
});
|
||||
}
|
||||
|
||||
FileMeta parseFileMeta(String path) {
|
||||
final uri = Uri.parse(path);
|
||||
final fileName = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : '';
|
||||
|
||||
if (!fileName.contains('.')) {
|
||||
return const FileMeta(
|
||||
name: '',
|
||||
extension: '',
|
||||
type: FileType.other,
|
||||
);
|
||||
}
|
||||
|
||||
final index = fileName.lastIndexOf('.');
|
||||
final name = fileName.substring(0, index);
|
||||
final ext = fileName.substring(index + 1).toLowerCase();
|
||||
|
||||
final type = switch (ext) {
|
||||
'jpg' || 'jpeg' || 'png' || 'webp' || 'gif' => FileType.image,
|
||||
'pdf' => FileType.pdf,
|
||||
_ => FileType.other,
|
||||
};
|
||||
|
||||
return FileMeta(
|
||||
name: name,
|
||||
extension: ext,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
70
lib/core/utils/transfer/select_file.dart
Normal file
70
lib/core/utils/transfer/select_file.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'dart:io';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 选择文件
|
||||
/// - [type] 文件类型
|
||||
/// - [allowedExtensions] 允许的扩展名
|
||||
/// - [maxSize] 最大文件大小
|
||||
Future<List<File>> selectFile({
|
||||
required FileType type,
|
||||
List<String>? allowedExtensions,
|
||||
int maxSize = 1024 * 1024 * 20,
|
||||
}) async {
|
||||
List<File> files = [];
|
||||
bool hasOversize = false;
|
||||
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
type: type,
|
||||
allowedExtensions: allowedExtensions,
|
||||
);
|
||||
// 判断空
|
||||
if (result == null) return [];
|
||||
|
||||
//进行文件大小,压缩、等操作
|
||||
for (final e in result.files) {
|
||||
if (e.size > maxSize) {
|
||||
hasOversize = true;
|
||||
continue;
|
||||
}
|
||||
if (e.path == null) continue;
|
||||
|
||||
if (type == FileType.image) {
|
||||
final compressed = await compressImage(File(e.path!));
|
||||
files.add(compressed);
|
||||
} else {
|
||||
files.add(File(e.path!));
|
||||
}
|
||||
}
|
||||
//提示
|
||||
if (hasOversize) {
|
||||
EasyLoading.showInfo('已过滤超过 20MB 的文件');
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/// 压缩图片
|
||||
Future<File> compressImage(File file) async {
|
||||
final dir = await getTemporaryDirectory();
|
||||
final targetPath = p.join(
|
||||
dir.path,
|
||||
'${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||||
);
|
||||
|
||||
final result = await FlutterImageCompress.compressAndGetFile(
|
||||
file.absolute.path,
|
||||
targetPath,
|
||||
quality: 80,
|
||||
format: CompressFormat.jpeg,
|
||||
minWidth: 1080,
|
||||
minHeight: 1080,
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
throw Exception('Image compression failed');
|
||||
}
|
||||
return File(result.path);
|
||||
}
|
||||
59
lib/core/utils/transfer/upload.dart
Normal file
59
lib/core/utils/transfer/upload.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'dart:io';
|
||||
import 'package:app/data/api/common_api.dart';
|
||||
import 'package:app/data/models/common/qiu_token_dto.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
|
||||
import '../../config/global.dart';
|
||||
|
||||
class QinUpload {
|
||||
///获取七牛token
|
||||
static Future<QiuTokenDto> _getQiuToken(File file, String path) async {
|
||||
// 读取文件的字节数据
|
||||
final fileBytes = await file.readAsBytes();
|
||||
String fileMd5 = md5.convert(fileBytes).toString();
|
||||
//前缀
|
||||
var prefix = Config.getEnv() == "dev" ? "test" : "release";
|
||||
var suffix = file.path.split(".").last;
|
||||
|
||||
var res = await getQiuTokenApi(
|
||||
"shangyiapp/$prefix/$path/$fileMd5.$suffix",
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
///上传文件
|
||||
/// - [file] 文件
|
||||
/// - [path] 目标目录,不以/开头和结尾
|
||||
static Future<String?> upload({
|
||||
required File file,
|
||||
required String path,
|
||||
|
||||
}) async {
|
||||
var qiuToken = await _getQiuToken(file, path);
|
||||
//数据
|
||||
FormData formData = FormData.fromMap({
|
||||
"file": await MultipartFile.fromFile(file.path),
|
||||
"token": qiuToken.upToken,
|
||||
"fname": qiuToken.fileKey,
|
||||
"key": qiuToken.fileKey,
|
||||
});
|
||||
try {
|
||||
Dio dio = Dio();
|
||||
Response response = await dio.post(
|
||||
qiuToken.uploadUrl!,
|
||||
data: formData,
|
||||
onSendProgress: (int sent, int total) {
|
||||
// double progress = sent * 100 / total * 100;
|
||||
// progress = (progress / 100).truncateToDouble();
|
||||
},
|
||||
);
|
||||
String key = response.data['key'];
|
||||
return "https://${qiuToken.domain}/$key";
|
||||
} catch (e) {
|
||||
EasyLoading.showError("上传失败");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
lib/data/api/common_api.dart
Normal file
22
lib/data/api/common_api.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
import 'package:app/core/network/request.dart';
|
||||
import '../models/common/qiu_token_dto.dart';
|
||||
import '../models/common/version_dto.dart';
|
||||
|
||||
///获取七牛token
|
||||
/// - [fileKey]: 文件key
|
||||
/// - [isPrivate]: 是否私有
|
||||
Future<QiuTokenDto> getQiuTokenApi(String fileKey, {bool isPrivate = false}) async {
|
||||
var response = await Request().get("/file/get_qiniu_upload_token", {
|
||||
"file_key": fileKey,
|
||||
"is_public_bucket": isPrivate ? 2 : 1,
|
||||
});
|
||||
return QiuTokenDto.fromJson(response);
|
||||
}
|
||||
|
||||
///获取APP最新版本
|
||||
/// - [isIos] 是否获取ios最新版本,默认安卓
|
||||
Future<VersionDto> getAppVersionApi({bool isIos = false}) async {
|
||||
var response = await Request().get("/get_latest_version", {"platform": isIos ? 2 : 1});
|
||||
return VersionDto.fromJson(response);
|
||||
}
|
||||
20
lib/data/models/api_dto.dart
Normal file
20
lib/data/models/api_dto.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
class ApiDto<T> {
|
||||
final int code;
|
||||
final String message;
|
||||
final T data;
|
||||
|
||||
ApiDto({
|
||||
required this.code,
|
||||
required this.message,
|
||||
required this.data
|
||||
});
|
||||
|
||||
|
||||
factory ApiDto.fromJson(Map<String, dynamic> json) {
|
||||
return ApiDto<T>(
|
||||
code: json['code'],
|
||||
message: json['message'],
|
||||
data: json['data'],
|
||||
);
|
||||
}
|
||||
}
|
||||
24
lib/data/models/common/qiu_token_dto.dart
Normal file
24
lib/data/models/common/qiu_token_dto.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
class QiuTokenDto {
|
||||
String? uploadUrl;
|
||||
String? upToken;
|
||||
String? fileKey;
|
||||
String? domain;
|
||||
|
||||
QiuTokenDto({this.uploadUrl, this.upToken, this.fileKey, this.domain});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map["upload_url"] = uploadUrl;
|
||||
map["up_token"] = upToken;
|
||||
map["file_key"] = fileKey;
|
||||
map["domain"] = domain;
|
||||
return map;
|
||||
}
|
||||
|
||||
QiuTokenDto.fromJson(dynamic json){
|
||||
uploadUrl = json["upload_url"] ?? "";
|
||||
upToken = json["up_token"] ?? "";
|
||||
fileKey = json["file_key"] ?? "";
|
||||
domain = json["domain"] ?? "";
|
||||
}
|
||||
}
|
||||
43
lib/data/models/common/version_dto.dart
Normal file
43
lib/data/models/common/version_dto.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
class VersionDto {
|
||||
VersionDto({
|
||||
required this.latestVersion,
|
||||
required this.updatedAt,
|
||||
required this.downloadUrl,
|
||||
required this.updateContent,
|
||||
required this.createdAt,
|
||||
required this.lowVersion,
|
||||
required this.id,
|
||||
required this.downloadSize,
|
||||
});
|
||||
|
||||
String latestVersion;
|
||||
DateTime updatedAt;
|
||||
String downloadUrl;
|
||||
List<String> updateContent;
|
||||
DateTime createdAt;
|
||||
String lowVersion;
|
||||
int id;
|
||||
String downloadSize;
|
||||
|
||||
factory VersionDto.fromJson(Map<dynamic, dynamic> json) => VersionDto(
|
||||
latestVersion: json["latest_version"],
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
downloadUrl: json["download_url"],
|
||||
updateContent: List<String>.from(json["update_content"].map((x) => x)),
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
lowVersion: json["low_version"],
|
||||
id: json["id"],
|
||||
downloadSize: json["download_size"],
|
||||
);
|
||||
|
||||
Map<dynamic, dynamic> toJson() => {
|
||||
"latest_version": latestVersion,
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"download_url": downloadUrl,
|
||||
"update_content": List<dynamic>.from(updateContent.map((x) => x)),
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"low_version": lowVersion,
|
||||
"id": id,
|
||||
"download_size": downloadSize,
|
||||
};
|
||||
}
|
||||
33
lib/data/repository/auto_repo.dart
Normal file
33
lib/data/repository/auto_repo.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import 'package:app/core/utils/storage.dart';
|
||||
|
||||
/// 登录数据仓库
|
||||
class AuthRepo {
|
||||
static final String _key = "login_info";
|
||||
|
||||
///登录储存信息
|
||||
static Future<void> saveLogin(dynamic loginDto) async {
|
||||
await Storage.set(_key, loginDto.toJson());
|
||||
}
|
||||
|
||||
///获取登录信息
|
||||
static Future<dynamic> getLoginInfo() async {
|
||||
// final loginInfo = await Storage.get<dynamic?>(
|
||||
// _key,
|
||||
// decoder: (json) => dynamic.fromJson(json),
|
||||
// );
|
||||
// return loginInfo;
|
||||
return null;
|
||||
}
|
||||
|
||||
///获取token
|
||||
static Future<String?> getToken() async {
|
||||
final loginInfo = await getLoginInfo();
|
||||
return loginInfo?.token;
|
||||
}
|
||||
|
||||
///清除登录信息
|
||||
static Future<void> clearLogin() async {
|
||||
await Storage.remove(_key);
|
||||
}
|
||||
}
|
||||
14
lib/data/stores/user_provider.dart
Normal file
14
lib/data/stores/user_provider.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter_riverpod/legacy.dart';
|
||||
|
||||
final userProvider = StateNotifierProvider<UserNotifier, UserState>((ref) => UserNotifier());
|
||||
|
||||
class UserNotifier extends StateNotifier<UserState> {
|
||||
UserNotifier() : super(UserState());
|
||||
}
|
||||
|
||||
/// 用户数据
|
||||
class UserState {
|
||||
String token;
|
||||
|
||||
UserState({this.token = ""});
|
||||
}
|
||||
146
lib/l10n/app_localizations.dart
Normal file
146
lib/l10n/app_localizations.dart
Normal file
@@ -0,0 +1,146 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_zh.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations? of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('zh'),
|
||||
];
|
||||
|
||||
/// No description provided for @hello.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'你好'**
|
||||
String get hello;
|
||||
|
||||
/// 登陆标题
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'标题'**
|
||||
String get title;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['en', 'zh'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
case 'zh':
|
||||
return AppLocalizationsZh();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
16
lib/l10n/app_localizations_en.dart
Normal file
16
lib/l10n/app_localizations_en.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get hello => 'Hello';
|
||||
|
||||
@override
|
||||
String get title => 'title';
|
||||
}
|
||||
16
lib/l10n/app_localizations_zh.dart
Normal file
16
lib/l10n/app_localizations_zh.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for Chinese (`zh`).
|
||||
class AppLocalizationsZh extends AppLocalizations {
|
||||
AppLocalizationsZh([String locale = 'zh']) : super(locale);
|
||||
|
||||
@override
|
||||
String get hello => '你好';
|
||||
|
||||
@override
|
||||
String get title => '标题';
|
||||
}
|
||||
7
lib/l10n/arb/app_en.arb
Normal file
7
lib/l10n/arb/app_en.arb
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"hello": "Hello",
|
||||
"title": "title",
|
||||
"@title": {
|
||||
"description": "登陆标题"
|
||||
}
|
||||
}
|
||||
7
lib/l10n/arb/app_zh.arb
Normal file
7
lib/l10n/arb/app_zh.arb
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"hello": "你好",
|
||||
"title": "标题",
|
||||
"@title": {
|
||||
"description": "登陆标题"
|
||||
}
|
||||
}
|
||||
22
lib/l10n/arb/i18n.yaml
Normal file
22
lib/l10n/arb/i18n.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
#hello:
|
||||
# zh: 你好
|
||||
# en: Hello
|
||||
#title:
|
||||
# zh: 标题
|
||||
# en: title
|
||||
# description: 登陆标题
|
||||
|
||||
login:
|
||||
hello:
|
||||
zh: 你好
|
||||
en: Hello
|
||||
description: 登陆标题
|
||||
title:
|
||||
zh: 登陆标题
|
||||
en: title
|
||||
description: 登陆标题
|
||||
common:
|
||||
hello:
|
||||
zh: 你好
|
||||
en: Hello
|
||||
description: 登陆标题
|
||||
77
lib/main.dart
Normal file
77
lib/main.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:app/widgets/version/version_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:app/router/app_routes.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import 'core/event/event_bus.dart';
|
||||
import 'core/event/global_event.dart';
|
||||
import 'core/theme/theme.dart';
|
||||
import 'data/repository/auto_repo.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'widgets/version/check_version_update.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
EventBus().stream.listen((event) {
|
||||
// 1. 监听 401
|
||||
if (event is UnauthorizedEvent) {
|
||||
final ctx = navigatorKey.currentState?.context;
|
||||
if (ctx != null) {
|
||||
AuthRepo.clearLogin();
|
||||
// ctx.go(RoutePaths.auth);
|
||||
}
|
||||
}
|
||||
|
||||
//监听版本更新
|
||||
if (event is VersionUpdateEvent) {
|
||||
final ctx = navigatorKey.currentState?.context;
|
||||
if (ctx == null) return;
|
||||
showUpdateDialog(ctx, event.version);
|
||||
}
|
||||
});
|
||||
|
||||
checkUpdate();
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 694),
|
||||
useInheritedMediaQuery: true,
|
||||
child: MaterialApp.router(
|
||||
localizationsDelegates: [
|
||||
// 本地化的代理类
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
AppLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: [
|
||||
Locale('en'),
|
||||
Locale('zh', 'CN'),
|
||||
],
|
||||
routerConfig: goRouter,
|
||||
theme: AppTheme.createTheme(LightTheme()),
|
||||
builder: (context, child) {
|
||||
final easyLoadingBuilder = EasyLoading.init();
|
||||
return easyLoadingBuilder(context, child);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
24
lib/pages/home/home_page.dart
Normal file
24
lib/pages/home/home_page.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.hello),
|
||||
//使用Locale title
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Text("ds22d43"),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
24
lib/pages/system/splash_page.dart
Normal file
24
lib/pages/system/splash_page.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({super.key});
|
||||
|
||||
@override
|
||||
State<SplashPage> createState() => _SplashPageState();
|
||||
}
|
||||
|
||||
class _SplashPageState extends State<SplashPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
init() {
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold();
|
||||
}
|
||||
}
|
||||
27
lib/router/app_routes.dart
Normal file
27
lib/router/app_routes.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/cupertino.dart' hide RouterConfig;
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'config/route_paths.dart';
|
||||
import 'config/router_config.dart';
|
||||
import 'modules/home_routes.dart';
|
||||
|
||||
List<RouterConfig> routeConfigs = [...homeRoutes];
|
||||
|
||||
//for循环遍历
|
||||
List<RouteBase> routes = routeConfigs.map((item) {
|
||||
return GoRoute(
|
||||
path: item.path,
|
||||
builder: (context,state){
|
||||
return item.child(state);
|
||||
}
|
||||
);
|
||||
}).toList();
|
||||
|
||||
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
//变量命名
|
||||
GoRouter goRouter = GoRouter(
|
||||
navigatorKey: navigatorKey,
|
||||
initialLocation: RoutePaths.home,
|
||||
routes: routes,
|
||||
);
|
||||
3
lib/router/config/route_paths.dart
Normal file
3
lib/router/config/route_paths.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class RoutePaths {
|
||||
static const home = "/";
|
||||
}
|
||||
16
lib/router/config/router_config.dart
Normal file
16
lib/router/config/router_config.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class RouterConfig {
|
||||
String path;
|
||||
FutureOr<String?> Function(BuildContext, GoRouterState)? redirect;
|
||||
Widget Function(GoRouterState) child;
|
||||
|
||||
RouterConfig({
|
||||
required this.path,
|
||||
required this.child,
|
||||
this.redirect,
|
||||
});
|
||||
}
|
||||
13
lib/router/modules/home_routes.dart
Normal file
13
lib/router/modules/home_routes.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:app/pages/home/home_page.dart';
|
||||
import 'package:app/router/config/router_config.dart';
|
||||
|
||||
import '../config/route_paths.dart';
|
||||
|
||||
List<RouterConfig> homeRoutes = [
|
||||
RouterConfig(
|
||||
path: RoutePaths.home,
|
||||
child: (state) {
|
||||
return HomePage();
|
||||
},
|
||||
)
|
||||
];
|
||||
83
lib/widgets/base/button/index.dart
Normal file
83
lib/widgets/base/button/index.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:app/core/theme/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../config/config.dart';
|
||||
|
||||
class Button extends StatelessWidget {
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Widget child;
|
||||
final ThemeType type;
|
||||
final BorderRadius radius;
|
||||
final VoidCallback? onPressed;
|
||||
final bool loading;
|
||||
final bool disabled;
|
||||
final Widget? icon;
|
||||
|
||||
const Button({
|
||||
super.key,
|
||||
this.icon,
|
||||
this.width,
|
||||
this.height,
|
||||
this.radius = const BorderRadius.all(Radius.circular(80)),
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.type = ThemeType.primary,
|
||||
this.loading = false,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bgDecoration = switch (type) {
|
||||
ThemeType.primary => BoxDecoration(color: Theme.of(context).primaryColor),
|
||||
ThemeType.success => BoxDecoration(color: context.success),
|
||||
ThemeType.danger => BoxDecoration(color: context.danger),
|
||||
ThemeType.warning => BoxDecoration(color: context.warning),
|
||||
ThemeType.info => BoxDecoration(color: context.info),
|
||||
};
|
||||
|
||||
return Opacity(
|
||||
opacity: disabled || loading ? 0.5 : 1,
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: bgDecoration.copyWith(borderRadius: radius),
|
||||
child: Material(
|
||||
type: MaterialType.transparency, // 让波纹依附在上层容器
|
||||
child: InkWell(
|
||||
borderRadius: radius,
|
||||
onTap: disabled || loading ? null : onPressed,
|
||||
splashColor: Colors.white.withValues(alpha: 0.2),
|
||||
highlightColor: Colors.white.withValues(alpha: 0.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Colors.white
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (loading)
|
||||
const SizedBox(
|
||||
width: 15,
|
||||
height: 15,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
21
lib/widgets/base/config/color.dart
Normal file
21
lib/widgets/base/config/color.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'config.dart';
|
||||
|
||||
class ColorResolver {
|
||||
final Color bg;
|
||||
final Color font;
|
||||
final Color border;
|
||||
|
||||
ColorResolver(this.bg, this.font, this.border);
|
||||
}
|
||||
|
||||
///返回颜色
|
||||
ColorResolver resolveEffectColors(Color color, Effect effect) => switch (effect) {
|
||||
Effect.dark => ColorResolver(color, Colors.white, color),
|
||||
Effect.light => () {
|
||||
final pale = color.withValues(alpha: 0.2);
|
||||
return ColorResolver(pale, color, pale);
|
||||
}(), // 注意这里多了 (),表示立即调用
|
||||
Effect.plain => ColorResolver(Colors.white, color, color),
|
||||
};
|
||||
15
lib/widgets/base/config/config.dart
Normal file
15
lib/widgets/base/config/config.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
///主题风格
|
||||
enum Effect {
|
||||
dark,
|
||||
light,
|
||||
plain,
|
||||
}
|
||||
|
||||
///主题类型
|
||||
enum ThemeType {
|
||||
primary,
|
||||
success,
|
||||
warning,
|
||||
danger,
|
||||
info,
|
||||
}
|
||||
53
lib/widgets/base/empty/index.dart
Normal file
53
lib/widgets/base/empty/index.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum EmptyType {
|
||||
data,
|
||||
}
|
||||
|
||||
class Empty extends StatelessWidget {
|
||||
final EmptyType type;
|
||||
final String? text;
|
||||
final Widget? child;
|
||||
|
||||
const Empty({
|
||||
super.key,
|
||||
this.type = EmptyType.data,
|
||||
this.text,
|
||||
this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var emptyImg = switch (type) {
|
||||
EmptyType.data => Image.asset('assets/image/empty_data.png'),
|
||||
};
|
||||
var emptyText = switch (type) {
|
||||
EmptyType.data => '暂无数据',
|
||||
};
|
||||
return Container(
|
||||
padding: EdgeInsets.all(0),
|
||||
child: Column(
|
||||
children: [
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(bottom: 15),
|
||||
child: emptyImg,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
text ?? emptyText,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
child != null
|
||||
? Container(
|
||||
margin: EdgeInsets.only(top: 15),
|
||||
child: child,
|
||||
)
|
||||
: SizedBox(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/widgets/base/tag/index.dart
Normal file
58
lib/widgets/base/tag/index.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:app/core/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/widgets/base/config/color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../config/config.dart';
|
||||
|
||||
class Tag extends StatelessWidget {
|
||||
final String text;
|
||||
final Color? color;
|
||||
final ThemeType type;
|
||||
final Effect effect;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const Tag({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.color,
|
||||
this.type = ThemeType.primary,
|
||||
this.effect = Effect.dark,
|
||||
this.padding = const EdgeInsets.symmetric(vertical: 3, horizontal: 6),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
///颜色
|
||||
var baseColor = switch (type) {
|
||||
ThemeType.primary => Theme.of(context).primaryColor,
|
||||
ThemeType.success => context.success,
|
||||
ThemeType.warning => context.warning,
|
||||
ThemeType.danger => context.danger,
|
||||
ThemeType.info => Color(0xFF8F9298),
|
||||
};
|
||||
if (color != null) {
|
||||
baseColor = color!;
|
||||
}
|
||||
|
||||
ColorResolver colorResolver = resolveEffectColors(baseColor, effect);
|
||||
|
||||
return Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: colorResolver.bg,
|
||||
border: Border.all(
|
||||
color: colorResolver.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: colorResolver.font,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/widgets/version/check_version_update.dart
Normal file
39
lib/widgets/version/check_version_update.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:app/core/config/global.dart';
|
||||
import 'package:app/core/event/event_bus.dart';
|
||||
import 'package:app/core/event/global_event.dart';
|
||||
import 'package:app/core/utils/system.dart';
|
||||
import 'package:app/data/api/common_api.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
///检查版本更新
|
||||
void checkUpdate() async {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
var versionRes = await getAppVersionApi(isIos: !isAndroid());
|
||||
//判断版本是不是小于最低版本,如果不是,不弹更新
|
||||
bool isMin = _compareVersions(packageInfo.version, versionRes.lowVersion) == -1;
|
||||
if (!isMin) {
|
||||
return;
|
||||
}
|
||||
Future.delayed(const Duration(seconds: 2), () async {
|
||||
EventBus().publish(VersionUpdateEvent(versionRes));
|
||||
});
|
||||
}
|
||||
|
||||
///比较版本号
|
||||
/// 1:表示当前版本号比传入的版本号高
|
||||
/// -1:表示当前版本号比传入的版本号低
|
||||
/// 0 表示版本号相同
|
||||
int _compareVersions(String version1, String version2) {
|
||||
List<String> v1Parts = version1.split('.');
|
||||
List<String> v2Parts = version2.split('.');
|
||||
int length = v1Parts.length > v2Parts.length ? v1Parts.length : v2Parts.length;
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int v1Part = i < v1Parts.length ? int.tryParse(v1Parts[i]) ?? 0 : 0;
|
||||
int v2Part = i < v2Parts.length ? int.tryParse(v2Parts[i]) ?? 0 : 0;
|
||||
|
||||
if (v1Part > v2Part) return 1;
|
||||
if (v1Part < v2Part) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
213
lib/widgets/version/version_ui.dart
Normal file
213
lib/widgets/version/version_ui.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
import 'package:app/core/utils/system.dart';
|
||||
import 'package:app/core/utils/transfer/download.dart';
|
||||
import 'package:app/data/models/common/version_dto.dart';
|
||||
import 'package:app_installer/app_installer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../base/button/index.dart';
|
||||
import '../base/config/config.dart';
|
||||
import '../base/tag/index.dart';
|
||||
|
||||
|
||||
void showUpdateDialog(BuildContext context, VersionDto data){
|
||||
//如果是最新版本
|
||||
showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierLabel: "Update",
|
||||
transitionDuration: const Duration(milliseconds: 300),
|
||||
pageBuilder: (context, animation, secondaryAnimation) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: AppUpdateUi(
|
||||
version: data.latestVersion,
|
||||
updateNotice: data.updateContent,
|
||||
uploadUrl: data.downloadUrl,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
///下载状态枚举
|
||||
enum UploadState {
|
||||
notStarted, //未开始下载
|
||||
downloading, //下载中
|
||||
completed, //下载完毕
|
||||
}
|
||||
|
||||
class AppUpdateUi extends StatefulWidget {
|
||||
final String version;
|
||||
final List<String> updateNotice;
|
||||
final String uploadUrl; //下载地址
|
||||
|
||||
const AppUpdateUi({
|
||||
super.key,
|
||||
required this.version,
|
||||
required this.updateNotice,
|
||||
required this.uploadUrl,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AppUpdateUi> createState() => _UpdateUiState();
|
||||
}
|
||||
|
||||
class _UpdateUiState extends State<AppUpdateUi> {
|
||||
int _uploadProgress = 0; //下载进度
|
||||
UploadState _uploadState = UploadState.notStarted;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getLocalApk();
|
||||
}
|
||||
|
||||
///读取本地是否有下载记录
|
||||
void getLocalApk() async {
|
||||
String url = await LocalDownload.getFilePath(url: widget.uploadUrl, path: '/apk');
|
||||
if (url.isNotEmpty) {
|
||||
setState(() {
|
||||
_uploadState = UploadState.completed;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
///下载apk
|
||||
void _handUploadApk() async {
|
||||
//如果是ios
|
||||
if(!isAndroid()){
|
||||
_launchAppStore();
|
||||
return;
|
||||
}
|
||||
if (_uploadState == UploadState.notStarted) {
|
||||
setState(() {
|
||||
_uploadState = UploadState.downloading;
|
||||
});
|
||||
LocalDownload.downLoadFile(
|
||||
url: widget.uploadUrl,
|
||||
path: "/apk",
|
||||
onProgress: (double double) {
|
||||
setState(() {
|
||||
_uploadProgress = double.toInt();
|
||||
});
|
||||
},
|
||||
onDone: (apk) async {
|
||||
setState(() {
|
||||
_uploadState = UploadState.completed;
|
||||
});
|
||||
AppInstaller.installApk(apk);
|
||||
},
|
||||
);
|
||||
} else if (_uploadState == UploadState.completed) {
|
||||
String url = await LocalDownload.getFilePath(url: widget.uploadUrl, path: '/apk');
|
||||
AppInstaller.installApk(url);
|
||||
}
|
||||
}
|
||||
///跳转到appstore商店
|
||||
void _launchAppStore() async{
|
||||
final appStoreUrl = Uri.parse('itms-apps://itunes.apple.com/app/6757746410');
|
||||
if (await canLaunchUrl(appStoreUrl)) {
|
||||
print("qq");
|
||||
await launchUrl(appStoreUrl);
|
||||
} else {
|
||||
EasyLoading.showToast("无法打开App Store");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String text;
|
||||
if (_uploadState == UploadState.downloading) {
|
||||
text = "$_uploadProgress%";
|
||||
} else if (_uploadState == UploadState.completed) {
|
||||
text = '安装';
|
||||
} else {
|
||||
text = '立即升级';
|
||||
}
|
||||
return IntrinsicHeight(
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
padding: EdgeInsets.symmetric(horizontal: 40),
|
||||
alignment: Alignment.center,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 500,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
Image.asset("assets/image/version_bg.png"),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
child: FractionalTranslation(
|
||||
translation: Offset(0.35, 0.3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("发现新版本"),
|
||||
SizedBox(width: 10),
|
||||
Tag(
|
||||
text: "V ${widget.version}",
|
||||
type: ThemeType.warning,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Material(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(15),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...widget.updateNotice.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final item = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: Text("${index + 1}.$item"),
|
||||
);
|
||||
}),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
height: 40,
|
||||
child: Button(
|
||||
onPressed: _uploadState == UploadState.downloading
|
||||
? null
|
||||
: _handUploadApk,
|
||||
child: Text(text),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user