feat: 工具类提交。
This commit is contained in:
parent
a2b38fa1e8
commit
557f900187
BIN
assets/other/pricary.png
Normal file
BIN
assets/other/pricary.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/other/web_site.png
Normal file
BIN
assets/other/web_site.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
573
lib/common/api/api_request.dart
Normal file
573
lib/common/api/api_request.dart
Normal file
@ -0,0 +1,573 @@
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:novyronst/common/api/api_string.dart';
|
||||
import 'package:novyronst/common/api/ny_network_intercept.dart';
|
||||
import 'package:novyronst/common/help/help_token.dart';
|
||||
import 'package:novyronst/tools/widgets/app_toast_util.dart';
|
||||
|
||||
|
||||
/// HTTP 请求工具类(单例)
|
||||
class AppService {
|
||||
static AppService? _instance;
|
||||
late Dio _dio;
|
||||
|
||||
static AppService get instance {
|
||||
_instance ??= AppService._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
AppService._internal() {
|
||||
_initDio();
|
||||
}
|
||||
|
||||
Dio get dio => _dio;
|
||||
|
||||
/// 初始化 Dio 配置
|
||||
void _initDio() {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: ApiString.baseUrl,
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
sendTimeout: const Duration(seconds: 15),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
responseType: ResponseType.json,
|
||||
));
|
||||
|
||||
// 添加自定义拦截器
|
||||
_dio.interceptors.add(NyNetworkIntercept());
|
||||
|
||||
// 开发环境添加日志
|
||||
// if (!kReleaseMode) {
|
||||
// _dio.interceptors.add(TalkerDioLogger(
|
||||
// settings: TalkerDioLoggerSettings(
|
||||
// printRequestHeaders: true,
|
||||
// printRequestData: true,
|
||||
// printResponseData: true,
|
||||
// printResponseHeaders: false,
|
||||
// ),
|
||||
// ));
|
||||
// }
|
||||
}
|
||||
|
||||
/// 更新配置
|
||||
void updateBaseOptions(BaseOptions options) {
|
||||
_dio.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
/// ============ 请求方法封装 ============
|
||||
|
||||
class ApiRequest {
|
||||
static final Dio _dio = AppService.instance.dio;
|
||||
|
||||
/// GET 请求(带重试)
|
||||
static Future<HttpResult<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
int? retry,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _executeRequest(
|
||||
() => _dio.get(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
),
|
||||
);
|
||||
return HttpResult<T>.fromResponse(response);
|
||||
} on DioException catch (e) {
|
||||
return _handleError<T>(e);
|
||||
} catch (e) {
|
||||
return HttpResult<T>.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// POST 请求(带重试)
|
||||
static Future<HttpResult<T>> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
int? retry,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _executeRequest(
|
||||
() => _dio.post(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
),
|
||||
);
|
||||
return HttpResult<T>.fromResponse(response);
|
||||
} on DioException catch (e) {
|
||||
return _handleError<T>(e);
|
||||
} catch (e) {
|
||||
return HttpResult<T>.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 文件上传(带重试)
|
||||
static Future<HttpResult<T>> fileUpload<T>(
|
||||
String path, {
|
||||
required File file,
|
||||
String fieldName = 'file',
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
int? retry,
|
||||
}) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
fieldName: await MultipartFile.fromFile(file.path),
|
||||
});
|
||||
|
||||
final response = await _executeRequest(
|
||||
() => _dio.post(
|
||||
path,
|
||||
data: formData,
|
||||
queryParameters: queryParameters,
|
||||
options: options ?? Options(contentType: 'multipart/form-data'),
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
),
|
||||
);
|
||||
return HttpResult<T>.fromResponse(response);
|
||||
} on DioException catch (e) {
|
||||
return _handleError<T>(e);
|
||||
} catch (e) {
|
||||
return HttpResult<T>.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// ============ 分页请求(原项目的 fetchPage) ============
|
||||
static Future<List<T>> fetchPage<T>(
|
||||
String url, {
|
||||
int? page,
|
||||
int? pageSize,
|
||||
Map<String, dynamic>? extraParams,
|
||||
required T Function(dynamic json) fromJson,
|
||||
bool error = true,
|
||||
String method = 'get',
|
||||
int? retry,
|
||||
}) async {
|
||||
try {
|
||||
final params = {
|
||||
'current_page': page?.toString() ?? '1',
|
||||
'page_size': pageSize?.toString() ?? '20',
|
||||
...?extraParams,
|
||||
};
|
||||
|
||||
Response response;
|
||||
if (method.toLowerCase() == 'get') {
|
||||
response = await _executeRequest(
|
||||
() => _dio.get(url, queryParameters: params),
|
||||
);
|
||||
} else {
|
||||
response = await _executeRequest(
|
||||
() => _dio.post(url, data: params),
|
||||
);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("HTTP ${response.statusCode}");
|
||||
}
|
||||
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
final base = BasePageResponse.fromJson(data);
|
||||
if (!base.success) {
|
||||
if (error) {
|
||||
// 这里可以触发 Toast
|
||||
AppToastUtil.showError(base.msg);
|
||||
}
|
||||
throw Exception(base.msg);
|
||||
}
|
||||
|
||||
final result = base.data;
|
||||
if (result is List) {
|
||||
return result.map<T>((e) => fromJson(e)).toList();
|
||||
} else if (result is Map && result['list'] is List) {
|
||||
return (result['list'] as List).map<T>((e) => fromJson(e)).toList();
|
||||
} else {
|
||||
throw Exception("data error");
|
||||
}
|
||||
} else {
|
||||
throw Exception("format error");
|
||||
}
|
||||
} catch (e) {
|
||||
// _handleError();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 直接执行请求,不重试
|
||||
static Future<Response> _executeRequest(
|
||||
Future<Response> Function() requestFn,
|
||||
) async {
|
||||
try {
|
||||
final response = await requestFn();
|
||||
|
||||
// 如果状态码不是 200,直接抛出异常
|
||||
if (response.statusCode != 200) {
|
||||
throw DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: 'HTTP ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
// 直接重新抛出异常,不重试
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 重试机制 ============
|
||||
static Future<Response> _retryRequest(
|
||||
Future<Response> Function() requestFn, {
|
||||
int? retries,
|
||||
}) async {
|
||||
int attempt = 0;
|
||||
final maxRetries = retries ?? 2;
|
||||
|
||||
while (attempt <= maxRetries) {
|
||||
try {
|
||||
final response = await requestFn();
|
||||
if (response.statusCode == 200) {
|
||||
return response;
|
||||
}
|
||||
// 401/402 特殊处理,由拦截器处理
|
||||
if (response.statusCode == 401 || response.statusCode == 402) {
|
||||
if (attempt == maxRetries) {
|
||||
throw DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: 'errorCode:${response.statusCode}',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (attempt == maxRetries) {
|
||||
throw DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: 'HTTP ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (attempt == maxRetries) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
attempt++;
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
throw Exception("retry failed (unreachable)");
|
||||
}
|
||||
|
||||
// ============ 错误处理 ============
|
||||
|
||||
static HttpResult<T> _handleError<T>(DioException e) {
|
||||
final Response? resp = e.response;
|
||||
|
||||
// 尝试从响应中解析错误信息
|
||||
if (resp != null && resp.data != null) {
|
||||
try {
|
||||
final parsed = HttpResult<T>.fromResponse(resp);
|
||||
if (!parsed.success) {
|
||||
// 401/402 错误,触发重新登录
|
||||
if (resp.statusCode == 401 || resp.statusCode == 402) {
|
||||
HelpToken().update();
|
||||
if (resp.statusCode == 402) {
|
||||
AppToastUtil.showError('账号已在其他设备登录,请重新登录');
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
String message = _getErrorMessage(e);
|
||||
return HttpResult<T>.error(message, httpStatus: resp?.statusCode);
|
||||
}
|
||||
|
||||
|
||||
static String _getErrorMessage(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
return '连接超时,请稍后重试';
|
||||
case DioExceptionType.sendTimeout:
|
||||
return '发送超时,请稍后重试';
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return '接收超时,请稍后重试';
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = e.response?.statusCode;
|
||||
if (statusCode == 401) {
|
||||
HelpToken().update();
|
||||
return '未授权,请重新登录';
|
||||
}
|
||||
if (statusCode == 402) {
|
||||
HelpToken().update();
|
||||
return '账号已在其他设备登录,请重新登录';
|
||||
}
|
||||
return _handleStatusCode(statusCode);
|
||||
case DioExceptionType.cancel:
|
||||
return '请求已取消';
|
||||
case DioExceptionType.connectionError:
|
||||
return '网络连接失败,请检查网络设置';
|
||||
default:
|
||||
return '网络错误,请稍后重试';
|
||||
}
|
||||
}
|
||||
|
||||
static String _handleStatusCode(int? statusCode) {
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return '请求错误';
|
||||
case 403:
|
||||
return '拒绝访问';
|
||||
case 404:
|
||||
return '请求资源不存在';
|
||||
case 500:
|
||||
return '服务器内部错误';
|
||||
case 502:
|
||||
return '网关错误';
|
||||
case 503:
|
||||
return '服务不可用';
|
||||
default:
|
||||
return '请求失败';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 统一响应:业务成功见 [isSuccess]
|
||||
class HttpResult<T> {
|
||||
final int? status;
|
||||
final bool success;
|
||||
final String? message;
|
||||
final T? data;
|
||||
final int? errorCode;
|
||||
|
||||
const HttpResult({
|
||||
this.status,
|
||||
this.success = false,
|
||||
this.message,
|
||||
this.data,
|
||||
this.errorCode,
|
||||
});
|
||||
|
||||
bool get isSuccess => success;
|
||||
|
||||
factory HttpResult.fromResponse(Response response) {
|
||||
try {
|
||||
final int httpStatus = response.statusCode ?? 0;
|
||||
final dynamic raw = response.data;
|
||||
final Map<String, dynamic>? root = _jsonMap(raw);
|
||||
|
||||
// 明确失败包:{ "success": false, "error": { ... } }
|
||||
if (root != null && root['success'] == false) {
|
||||
final payload = ApiErrorPayload.tryParse(root);
|
||||
return HttpResult<T>(
|
||||
status: httpStatus,
|
||||
success: false,
|
||||
message: payload?.message ?? root['message']?.toString() ?? '请求失败',
|
||||
errorCode: payload?.code,
|
||||
data: null,
|
||||
);
|
||||
}
|
||||
|
||||
// HTTP 2xx:body 即为业务数据
|
||||
if (httpStatus >= 200 && httpStatus < 300) {
|
||||
return HttpResult<T>(
|
||||
status: httpStatus,
|
||||
success: true,
|
||||
message: null,
|
||||
data: raw as T?,
|
||||
errorCode: null,
|
||||
);
|
||||
}
|
||||
|
||||
// 非 2xx:尽量从 body 里拆 error
|
||||
if (root != null) {
|
||||
final payload = ApiErrorPayload.tryParse(root);
|
||||
if (payload != null) {
|
||||
return HttpResult<T>(
|
||||
status: httpStatus,
|
||||
success: false,
|
||||
message: payload.message,
|
||||
errorCode: payload.code,
|
||||
data: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return HttpResult<T>(
|
||||
status: httpStatus,
|
||||
success: false,
|
||||
message: _handleStatusCode(httpStatus),
|
||||
data: null,
|
||||
);
|
||||
} catch (e) {
|
||||
return HttpResult<T>(
|
||||
status: response.statusCode,
|
||||
success: false,
|
||||
message: '数据解析失败: $e',
|
||||
data: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
factory HttpResult.error(String message, {int? httpStatus}) {
|
||||
return HttpResult<T>(
|
||||
success: false,
|
||||
status: httpStatus,
|
||||
message: message,
|
||||
);
|
||||
}
|
||||
|
||||
factory HttpResult.networkError() {
|
||||
return HttpResult<T>(
|
||||
success: false,
|
||||
status: -1,
|
||||
message: '网络连接失败,请检查网络后重试',
|
||||
);
|
||||
}
|
||||
|
||||
// ============ 私有方法 ============
|
||||
static Map<String, dynamic>? _jsonMap(dynamic value) {
|
||||
if (value is Map<String, dynamic>) return value;
|
||||
if (value is Map) {
|
||||
try {
|
||||
return value.cast<String, dynamic>();
|
||||
} catch (_) {
|
||||
return Map<String, dynamic>.from(value as Map);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String _handleStatusCode(int? statusCode) {
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return '请求错误';
|
||||
case 401:
|
||||
return '未授权,请重新登录';
|
||||
case 403:
|
||||
return '拒绝访问';
|
||||
case 404:
|
||||
return '请求资源不存在';
|
||||
case 500:
|
||||
return '服务器内部错误';
|
||||
case 502:
|
||||
return '网关错误';
|
||||
case 503:
|
||||
return '服务不可用';
|
||||
default:
|
||||
return '请求失败';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 后台错误体:`{ "error": { "code", "details", "message" }, "success": false }`
|
||||
class ApiErrorPayload {
|
||||
final int? code;
|
||||
final dynamic details;
|
||||
final String? message;
|
||||
|
||||
const ApiErrorPayload({this.code, this.details, this.message});
|
||||
|
||||
static ApiErrorPayload? tryParse(Map<String, dynamic>? root) {
|
||||
if (root == null) return null;
|
||||
final err = root['error'];
|
||||
if (err is! Map) return null;
|
||||
final m = _jsonMap(err);
|
||||
if (m == null) return null;
|
||||
return ApiErrorPayload(
|
||||
code: _parseInt(m['code']),
|
||||
details: m['details'],
|
||||
message: m['message']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? _jsonMap(dynamic value) {
|
||||
if (value is Map<String, dynamic>) return value;
|
||||
if (value is Map) {
|
||||
try {
|
||||
return value.cast<String, dynamic>();
|
||||
} catch (_) {
|
||||
return Map<String, dynamic>.from(value as Map);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static int? _parseInt(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is int) return v;
|
||||
if (v is num) return v.toInt();
|
||||
return int.tryParse(v.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 分页响应模型(原项目的 GlimzoGetxBaseResponse 改造)
|
||||
class BasePageResponse<T> {
|
||||
final int code;
|
||||
final String msg;
|
||||
final T? data;
|
||||
|
||||
BasePageResponse({
|
||||
required this.code,
|
||||
required this.msg,
|
||||
this.data,
|
||||
});
|
||||
|
||||
bool get success => code == 200;
|
||||
|
||||
factory BasePageResponse.fromJson(Map<String, dynamic> json) {
|
||||
return BasePageResponse(
|
||||
code: json['code'] ?? -1,
|
||||
msg: json['msg'] ?? '',
|
||||
data: json['data'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Map 数据提取工具(原项目的 GlimzoMapRes 改造)
|
||||
class MapRes {
|
||||
static List<T> getList<T>({
|
||||
required Map<dynamic, dynamic> mapData,
|
||||
required String alias,
|
||||
required T Function(Map<String, dynamic>) fromJson,
|
||||
}) {
|
||||
final rawList = mapData[alias];
|
||||
if (rawList is List) {
|
||||
return rawList
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(fromJson)
|
||||
.toList();
|
||||
}
|
||||
return <T>[];
|
||||
}
|
||||
|
||||
static T toModel<T>({
|
||||
required Map<String, dynamic> json,
|
||||
required T Function(Map<String, dynamic>) fromJson,
|
||||
}) {
|
||||
return fromJson(json);
|
||||
}
|
||||
}
|
||||
53
lib/common/api/api_string.dart
Normal file
53
lib/common/api/api_string.dart
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
// https://api-glimzodf.glimzodf.com/glimzodf/
|
||||
|
||||
class ApiString {
|
||||
static const String baseUrl = "https://api-glimzodf.glimzodf.com/";
|
||||
|
||||
static const String appName = 'Novyronst';
|
||||
|
||||
static String webPrefix = "glimzodf.com";
|
||||
|
||||
static String webIndex = "https://$webPrefix";
|
||||
|
||||
static String webPrivacy = "https://$webPrefix/private";
|
||||
|
||||
static String webAgreement = "https://$webPrefix/user_policy";
|
||||
|
||||
static String webmemberAgreement = "https://$webPrefix/member_ship_agreement";
|
||||
}
|
||||
|
||||
class ApiStringUser {
|
||||
static const String login = "/customer/login";
|
||||
// /customer/register
|
||||
static const String register = "/customer/register";
|
||||
// /customer/info
|
||||
static const String userInfo = "/customer/info";
|
||||
|
||||
static const String eventCreateHistory = "/createHistory";
|
||||
static const String eventUserEnterApp = "/customer/enterTheApp";
|
||||
static const String eventUserOnline = "/customer/onLine";
|
||||
static const String eventUserLeaveApp = "/customer/leaveApp";
|
||||
static const String eventPlaySeconds = "/uploadHistorySeconds";
|
||||
static const String eventWatchTime = "/watchShortDuration";
|
||||
static const String eventReport = "/event/add";
|
||||
}
|
||||
|
||||
class ApiStringVideo {
|
||||
|
||||
static const String getRecommandsList = "/getRecommands";
|
||||
static const String getVideoDetail = "/getVideoDetails";
|
||||
|
||||
static const String getHomeModules = "/home/all-modules";
|
||||
static const String getRankingList = "/homeRanking";
|
||||
static const String getCategoriesList = "/getCategories";
|
||||
static const String getCategoriesDetail = "/videoList";
|
||||
static const String getWaterflowList = "/newShortPlay";
|
||||
|
||||
static const String getSearchHots = "/search/hots";
|
||||
static const String getSearch = "/search";
|
||||
static const String collect = "/collect";
|
||||
static const String cancelCollect = "/cancelCollect";
|
||||
static const String getCollections = "/myCollections";
|
||||
static const String getHistories = "/myHistorys";
|
||||
}
|
||||
141
lib/common/api/ny_network_intercept.dart
Normal file
141
lib/common/api/ny_network_intercept.dart
Normal file
@ -0,0 +1,141 @@
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:novyronst/common/help/help_time.dart';
|
||||
import 'package:novyronst/common/help/help_token.dart';
|
||||
|
||||
import '../../tools/util/ny_device_info.dart';
|
||||
import '../help/help_encrypt.dart';
|
||||
|
||||
|
||||
/// 网络请求拦截器
|
||||
class NyNetworkIntercept extends Interceptor {
|
||||
final bool enableLog = !kReleaseMode;
|
||||
final Logger _logger = Logger();
|
||||
|
||||
@override
|
||||
Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
// 添加公共请求头
|
||||
await _addRequestHeaders(options);
|
||||
|
||||
// 打印请求日志
|
||||
_logInfo("🔶 REQUEST [${options.method.toUpperCase()}] ${options.uri}");
|
||||
_logDebug("Headers: ${options.headers}");
|
||||
if (options.data != null) {
|
||||
_logDebug("Payload: ${options.data}");
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
_logInfo("🟢 RESPONSE [${response.statusCode}] ${response.requestOptions.uri}");
|
||||
// _logDebug("Body: ${response.data}");
|
||||
|
||||
// 处理 401 / 402 Token 过期
|
||||
if (response.statusCode == 401 || response.statusCode == 402) {
|
||||
// 触发 Token 刷新
|
||||
HelpToken().update();
|
||||
if (response.statusCode == 402) {
|
||||
_handleError('errorCode:${response.statusCode}', true);
|
||||
}
|
||||
handler.reject(DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: 'errorCode:${response.statusCode}',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Release 模式解密响应体
|
||||
if (kReleaseMode && response.data is String) {
|
||||
try {
|
||||
final deStr = HelpEncrypt.deStr(response.data);
|
||||
response.data = jsonDecode(deStr);
|
||||
} catch (e) {
|
||||
_logError("解密失败: $e");
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
_logError("🔴 Error: ${err.message}");
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
// ============ 私有方法 ============
|
||||
|
||||
Future<void> _addRequestHeaders(RequestOptions options) async {
|
||||
// 开发模式添加 security: false
|
||||
if (!kReleaseMode) {
|
||||
options.headers['security'] = 'false';
|
||||
}
|
||||
|
||||
// 注入 Token
|
||||
final tokenManager = HelpToken();
|
||||
final token = tokenManager.get();
|
||||
// print('查看token: $token');
|
||||
if (token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
// 其他公共参数
|
||||
final deviceInfo = NyDeviceInfo.instance;
|
||||
|
||||
options.headers.addAll({
|
||||
'product-prefix': 'glimzo',
|
||||
'device-id': deviceInfo.deviceId,
|
||||
'system-type': deviceInfo.systemType,
|
||||
'model': deviceInfo.model,
|
||||
'system-version': deviceInfo.systemVersion,
|
||||
'brand': deviceInfo.brand,
|
||||
'app-version': deviceInfo.appVersion,
|
||||
'app-name': deviceInfo.appName,
|
||||
'lang-key': 'en',
|
||||
'time-zone': HelpTime.getTimeZone(),
|
||||
'idfa': '',
|
||||
'idfv': deviceInfo.idfv,
|
||||
'device-gaid': '',
|
||||
});
|
||||
|
||||
// 处理 prevToken(从请求体中提取)
|
||||
await _handleOldToken(options);
|
||||
}
|
||||
|
||||
Future<void> _handleOldToken(RequestOptions options) async {
|
||||
try {
|
||||
if (options.data is Map<String, dynamic>) {
|
||||
final body = options.data as Map<String, dynamic>;
|
||||
final prevToken = body['prevToken'];
|
||||
if (prevToken != null && prevToken.toString().isNotEmpty) {
|
||||
options.headers['Authorization'] = prevToken.toString();
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// 非 JSON 请求跳过
|
||||
}
|
||||
}
|
||||
|
||||
void _logInfo(String message) {
|
||||
if (enableLog) _logger.i(message);
|
||||
}
|
||||
|
||||
void _logDebug(String message) {
|
||||
if (enableLog) _logger.d('💡 $message');
|
||||
}
|
||||
|
||||
void _logError(String message) {
|
||||
if (enableLog) _logger.e(message);
|
||||
}
|
||||
|
||||
void _handleError(dynamic e, bool error) {
|
||||
_logError("🔴 Error: $e");
|
||||
// 这里可以根据需要处理错误提示
|
||||
}
|
||||
}
|
||||
41
lib/common/api/user_request.dart
Normal file
41
lib/common/api/user_request.dart
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
import 'package:novyronst/common/api/api_request.dart';
|
||||
import 'package:novyronst/common/api/api_string.dart';
|
||||
import 'package:novyronst/common/help/help_log.dart';
|
||||
import 'package:novyronst/common/model/novyronst_user.dart';
|
||||
import 'package:novyronst/common/model/register_model.dart';
|
||||
|
||||
class UserRequest {
|
||||
|
||||
static Future login() {
|
||||
final body = <String, dynamic>{
|
||||
"avator": '',
|
||||
"email": '123456@qq.com',
|
||||
"family_name": 'test1',
|
||||
"platform": 'android',
|
||||
"third_id": '32434',
|
||||
};
|
||||
return ApiRequest.post(ApiStringUser.login, data: body).then((value) {
|
||||
HelpLog.print(value.data, tag: 'loginnnn');
|
||||
});
|
||||
}
|
||||
|
||||
static Future<RegisterModel?> register() {
|
||||
return ApiRequest.post(ApiStringUser.register).then((value) {
|
||||
if (value.isSuccess) {
|
||||
return RegisterModel.fromJson(value.data['data']);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<NovyronstUser?> getUserInfo() {
|
||||
return ApiRequest.get(ApiStringUser.userInfo).then((value) {
|
||||
if (value.isSuccess) {
|
||||
HelpLog.print(value.data, tag: 'getUserInfo');
|
||||
return NovyronstUser.fromJson(value.data['data']);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
0
lib/common/api/video_request.dart
Normal file
0
lib/common/api/video_request.dart
Normal file
155
lib/common/help/help_encrypt.dart
Normal file
155
lib/common/help/help_encrypt.dart
Normal file
@ -0,0 +1,155 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:math';
|
||||
|
||||
class HelpEncrypt {
|
||||
static const int bfSize = 2048;
|
||||
static const String enStrTag = r'$';
|
||||
|
||||
// Generates a random salt of size between 16 and 64 bytes
|
||||
static Uint8List randSalt() {
|
||||
final rand = Random.secure();
|
||||
final size = rand.nextInt(49) + 16; // 16..64
|
||||
return Uint8List.fromList(List.generate(size, (_) => rand.nextInt(256)));
|
||||
}
|
||||
|
||||
// Encrypts the given data with a random salt
|
||||
static Uint8List en(Uint8List data) {
|
||||
if (data.isEmpty) return data;
|
||||
final salt = randSalt();
|
||||
final encryptedData = enWithSalt(data, salt);
|
||||
return Uint8List.fromList([salt.length, ...salt, ...encryptedData]);
|
||||
}
|
||||
|
||||
// Decrypts the given encrypted data
|
||||
static Uint8List de(Uint8List data) {
|
||||
if (data.isEmpty) return data;
|
||||
final saltLen = data[0];
|
||||
final salt = data.sublist(1, 1 + saltLen);
|
||||
final encrypted = data.sublist(1 + saltLen);
|
||||
return deWithSalt(encrypted, salt);
|
||||
}
|
||||
|
||||
// Encrypts data with a specified salt
|
||||
static Uint8List enWithSalt(Uint8List data, Uint8List salt) {
|
||||
final mixedData = mixSalt(data, salt);
|
||||
return cxEd(mixedData);
|
||||
}
|
||||
|
||||
// Decrypts data with a specified salt
|
||||
static Uint8List deWithSalt(Uint8List data, Uint8List salt) {
|
||||
final decryptedData = cxEd(data);
|
||||
return removeSalt(decryptedData, salt);
|
||||
}
|
||||
|
||||
// Simple XOR-based encryption/decryption (bitwise negation)
|
||||
static Uint8List cxEd(Uint8List data) {
|
||||
return Uint8List.fromList(data.map((b) => b ^ 0xFF).toList());
|
||||
}
|
||||
|
||||
// Apply salt to the data for encryption
|
||||
static Uint8List mixSalt(Uint8List data, Uint8List salt) {
|
||||
if (salt.isEmpty) return data;
|
||||
return Uint8List.fromList(
|
||||
List.generate(data.length, (i) {
|
||||
final s = salt[i % salt.length];
|
||||
return calSalt(data[i], s);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Remove salt from the data after decryption
|
||||
static Uint8List removeSalt(Uint8List data, Uint8List salt) {
|
||||
if (salt.isEmpty) return data;
|
||||
return Uint8List.fromList(
|
||||
List.generate(data.length, (i) {
|
||||
final s = salt[i % salt.length];
|
||||
return calRemoveSalt(data[i], s);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate a modified value after mixing salt (for encryption)
|
||||
static int calSalt(int v, int s) {
|
||||
final r = v ^ 0xFF;
|
||||
return s > r ? (s - r - 1) & 0xFF : (v + s) & 0xFF;
|
||||
}
|
||||
|
||||
// Reverse the salt mixing process for decryption
|
||||
static int calRemoveSalt(int v, int s) {
|
||||
return v >= s ? (v - s) & 0xFF : (0xFF - (s - v) + 1) & 0xFF;
|
||||
}
|
||||
|
||||
// Encrypt a string and return the hex representation
|
||||
static String cxEStr(String data) {
|
||||
return cxEStrAsBytes(
|
||||
data,
|
||||
).map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
// Encrypt string and return as bytes
|
||||
static Uint8List cxEStrAsBytes(String data) {
|
||||
return cxEd(Uint8List.fromList(utf8.encode(data)));
|
||||
}
|
||||
|
||||
// Decrypt a hex string to a regular string
|
||||
static String cxDStr(String data) {
|
||||
final bytes = Uint8List.fromList([
|
||||
for (int i = 0; i < data.length; i += 2)
|
||||
int.parse(data.substring(i, i + 2), radix: 16),
|
||||
]);
|
||||
return utf8.decode(cxEd(bytes));
|
||||
}
|
||||
|
||||
// Encrypt string data and return in the form of hex string with prefix
|
||||
static String enStr(String data) {
|
||||
return enBytesStr(Uint8List.fromList(utf8.encode(data)));
|
||||
}
|
||||
|
||||
// Encrypt bytes and return as hex string with prefix
|
||||
static String enBytesStr(Uint8List data) {
|
||||
final encrypted = en(data);
|
||||
return '$enStrTag${encrypted.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
|
||||
}
|
||||
|
||||
// Decrypt a string with encoded prefix (e.g., "$" symbol)
|
||||
static String deStr(String data) {
|
||||
return utf8.decode(deStrBytes(data));
|
||||
}
|
||||
|
||||
// Decrypt the byte array extracted from a string
|
||||
static Uint8List deStrBytes(String data) {
|
||||
if (!data.startsWith(enStrTag)) {
|
||||
throw ArgumentError("Invalid encoded string");
|
||||
}
|
||||
final hexData = data.substring(1);
|
||||
final bytes = Uint8List.fromList([
|
||||
for (int i = 0; i < hexData.length; i += 2)
|
||||
int.parse(hexData.substring(i, i + 2), radix: 16),
|
||||
]);
|
||||
return de(bytes);
|
||||
}
|
||||
|
||||
// Copy encrypted data from a stream to another stream
|
||||
static Future<int> copy(
|
||||
Stream<List<int>> reader,
|
||||
StreamSink<List<int>> writer,
|
||||
) async {
|
||||
int total = 0;
|
||||
await for (final chunk in reader) {
|
||||
total += chunk.length;
|
||||
final encrypted = chunk.map((b) => b ^ 0xFF).toList();
|
||||
writer.add(encrypted);
|
||||
}
|
||||
await writer.close();
|
||||
return total;
|
||||
}
|
||||
|
||||
// Custom encryption with condition (even and odd byte values handled differently)
|
||||
static Uint8List cxEd1(Uint8List data) {
|
||||
return Uint8List.fromList(
|
||||
data.map((b) => (b % 2 == 1) ? (b ^ 0xFF) : b).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -45,7 +45,9 @@ class ConstImage {
|
||||
static const String settings = "assets/other/settings.png";
|
||||
// copy
|
||||
static const String copy = "assets/other/copy.png";
|
||||
|
||||
// pricary web_site
|
||||
static const String primary = "assets/other/pricary.png";
|
||||
static const String webSite = "assets/other/web_site.png";
|
||||
|
||||
}
|
||||
|
||||
|
||||
146
lib/common/help/help_log.dart
Normal file
146
lib/common/help/help_log.dart
Normal file
@ -0,0 +1,146 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
|
||||
class HelpLog {
|
||||
HelpLog._();
|
||||
|
||||
static final HelpLog _instance = HelpLog._();
|
||||
|
||||
static HelpLog get instance => _instance;
|
||||
|
||||
final Talker _talker = TalkerFlutter.init();
|
||||
|
||||
factory HelpLog() => _instance;
|
||||
|
||||
Talker get tLog => _talker;
|
||||
|
||||
/// DevTools / 控制台里可按名称过滤。
|
||||
static const String logName = 'HelpLog';
|
||||
|
||||
Future<void> initLogger() async {
|
||||
/// app 信息
|
||||
}
|
||||
|
||||
/// 仅 [kDebugMode] 输出;内容经安全格式化,不会因无法 JSON 编码而中断业务逻辑。
|
||||
static void print(
|
||||
Object? message, {
|
||||
String? tag,
|
||||
String name = logName,
|
||||
}) {
|
||||
if (!kDebugMode) return;
|
||||
try {
|
||||
final text = _stringify(message);
|
||||
final line = tag != null ? '[debug-tag: $tag] ===== $text' : text;
|
||||
log(line, name: name);
|
||||
} catch (e, st) {
|
||||
log(
|
||||
'[SkyLog.print failed] $e',
|
||||
name: name,
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 尽量输出缩进 JSON;无法编码时退回 [toString],不向外抛错。
|
||||
static String _stringify(Object? value) {
|
||||
if (value == null) return 'null';
|
||||
if (value is String) return value;
|
||||
if (value is num || value is bool) return value.toString();
|
||||
|
||||
const encoder = JsonEncoder.withIndent(' ');
|
||||
|
||||
if (value is Map) {
|
||||
try {
|
||||
return encoder.convert(value);
|
||||
} catch (_) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
if (value is List) {
|
||||
try {
|
||||
return encoder.convert(value);
|
||||
} catch (_) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
if (value is Iterable) {
|
||||
try {
|
||||
return encoder.convert(value.toList());
|
||||
} catch (_) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return encoder.convert(value);
|
||||
} catch (_) {
|
||||
try {
|
||||
return '${value.runtimeType}: $value';
|
||||
} catch (_) {
|
||||
return '<unprintable ${value.runtimeType}>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 错误日志走 Talker;若 Talker 抛错,在 Debug 下用 [log] 兜底,仍不向外抛。
|
||||
static void error(
|
||||
Object? message, {
|
||||
String? tag,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
final text = message == null ? 'null' : '$message';
|
||||
try {
|
||||
_instance._talker.logCustom(
|
||||
CustomLog(
|
||||
text,
|
||||
settings: _instance._talker.settings,
|
||||
tag: tag,
|
||||
logLevel: LogLevel.error,
|
||||
stackTrace: stackTrace,
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
if (kDebugMode) {
|
||||
log(
|
||||
'[SkyLog.error fallback] $text | $e',
|
||||
name: logName,
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CustomLog extends TalkerLog {
|
||||
final String? tag;
|
||||
final TalkerSettings settings;
|
||||
final LogLevel? _logLevel;
|
||||
final StackTrace? _stackTrace;
|
||||
|
||||
CustomLog(
|
||||
super.message, {
|
||||
this.tag,
|
||||
required this.settings,
|
||||
LogLevel logLevel = LogLevel.debug,
|
||||
StackTrace? stackTrace,
|
||||
}) : _logLevel = logLevel,
|
||||
_stackTrace = stackTrace;
|
||||
|
||||
@override
|
||||
String? get key => tag;
|
||||
|
||||
@override
|
||||
LogLevel? get logLevel => _logLevel;
|
||||
|
||||
@override
|
||||
StackTrace? get stackTrace => _stackTrace;
|
||||
|
||||
@override
|
||||
AnsiPen? get pen =>
|
||||
settings.colors[TalkerKey.fromLogLevel(logLevel ?? LogLevel.info)];
|
||||
}
|
||||
55
lib/common/help/help_time.dart
Normal file
55
lib/common/help/help_time.dart
Normal file
@ -0,0 +1,55 @@
|
||||
|
||||
class HelpTime {
|
||||
|
||||
static String getTimeZone() {
|
||||
final offset = DateTime.now().timeZoneOffset;
|
||||
final hours = offset.inHours;
|
||||
final minutes = offset.inMinutes.remainder(60).abs();
|
||||
final sign = hours >= 0 ? '+' : '-';
|
||||
final formatted =
|
||||
'GMT$sign${hours.abs().toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}';
|
||||
return formatted;
|
||||
}
|
||||
|
||||
//格式化数字,带单位 k / m,可自定义小数位和千分位分隔符
|
||||
static String formatNum(
|
||||
num value, {
|
||||
int decimalDigits = 1,
|
||||
String separator = ',',
|
||||
String unit = 'k',
|
||||
}) {
|
||||
String removeTrailingZeros(String str) {
|
||||
if (str.contains('.')) {
|
||||
str = str.replaceAll(RegExp(r'0+$'), ''); // 去掉末尾 0
|
||||
str = str.replaceAll(RegExp(r'\.$'), ''); // 去掉末尾小数点
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
String addSeparator(String str) {
|
||||
final parts = str.split('.');
|
||||
final intPart = parts[0];
|
||||
final decPart = parts.length > 1 ? '.${parts[1]}' : '';
|
||||
final regex = RegExp(r'(\d+)(\d{3})');
|
||||
var formatted = intPart;
|
||||
while (regex.hasMatch(formatted)) {
|
||||
formatted = formatted.replaceAllMapped(
|
||||
regex,
|
||||
(m) => '${m[1]}$separator${m[2]}',
|
||||
);
|
||||
}
|
||||
return '$formatted$decPart';
|
||||
}
|
||||
|
||||
if (value < 1000) {
|
||||
return addSeparator(value.toStringAsFixed(0));
|
||||
} else if (value < 1000000) {
|
||||
int mu = unit=='w' ? 10000 : 1000;
|
||||
final formatted = (value / mu).toStringAsFixed(decimalDigits);
|
||||
return '${removeTrailingZeros(formatted)}$unit';
|
||||
} else {
|
||||
final formatted = (value / 1000000).toStringAsFixed(decimalDigits);
|
||||
return '${removeTrailingZeros(formatted)}m';
|
||||
}
|
||||
}
|
||||
}
|
||||
228
lib/common/help/help_token.dart
Normal file
228
lib/common/help/help_token.dart
Normal file
@ -0,0 +1,228 @@
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:novyronst/common/api/user_request.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class HelpToken {
|
||||
static final HelpToken _instance = HelpToken._internal();
|
||||
factory HelpToken() => _instance;
|
||||
HelpToken._internal();
|
||||
|
||||
static const String _keyToken = 'SKY_STORE_TOKEN';
|
||||
static const String _keyUserInfo = 'SKY_STORE_USER_INFO';
|
||||
|
||||
String? _cachedToken;
|
||||
Map<String, dynamic>? _cachedUserInfo;
|
||||
|
||||
|
||||
/// 获取 Token(从内存或存储)
|
||||
String get() {
|
||||
if (_cachedToken != null) {
|
||||
return _cachedToken!;
|
||||
}
|
||||
// 如果内存中没有,从存储中加载
|
||||
_cachedToken = _getFromStorage(_keyToken);
|
||||
return _cachedToken ?? '';
|
||||
}
|
||||
|
||||
/// 保存 Token(到内存和存储)
|
||||
void save(String token) {
|
||||
if (token.isNotEmpty) {
|
||||
_cachedToken = token;
|
||||
_saveToStorage(_keyToken, token);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除 Token(从内存和存储)
|
||||
void clear() {
|
||||
_cachedToken = null;
|
||||
_removeFromStorage(_keyToken);
|
||||
_cachedUserInfo = null;
|
||||
_removeFromStorage(_keyUserInfo);
|
||||
}
|
||||
|
||||
/// 刷新 Token(调用注册接口获取新 Token)
|
||||
/// 注意:这里需要传入一个刷新函数,因为原项目依赖 GlimzoRequestUser
|
||||
Future<void> update() async {
|
||||
try {
|
||||
final res = await UserRequest.register();
|
||||
if (res == null) return;
|
||||
final newToken = res.token;
|
||||
if (newToken.isNotEmpty ?? false) {
|
||||
// 保存新 Token
|
||||
save(newToken);
|
||||
// 如果返回了用户信息,也一并保存
|
||||
// if (res['userInfo'] != null) {
|
||||
// saveUserInfo(res['userInfo']);
|
||||
// }
|
||||
} else {
|
||||
throw Exception('Invalid token response');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[TokenManager] Error updating token: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 便捷属性 ============
|
||||
|
||||
/// 判断 Token 是否为空
|
||||
bool get isEmpty => get().isEmpty;
|
||||
|
||||
/// 判断 Token 是否不为空
|
||||
bool get isNotEmpty => !isEmpty;
|
||||
|
||||
/// 获取完整 Authorization 头
|
||||
String? get authorizationHeader {
|
||||
final token = get();
|
||||
if (token.isNotEmpty) {
|
||||
return 'Bearer $token';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============ 用户信息扩展(原项目没有,但建议加上) ============
|
||||
|
||||
/// 保存用户信息
|
||||
void saveUserInfo(Map<String, dynamic> userInfo) {
|
||||
_cachedUserInfo = userInfo;
|
||||
_saveToStorage(_keyUserInfo, jsonEncode(userInfo));
|
||||
}
|
||||
|
||||
/// 获取用户信息
|
||||
Map<String, dynamic>? getUserInfo() {
|
||||
if (_cachedUserInfo != null) {
|
||||
return _cachedUserInfo;
|
||||
}
|
||||
final stored = _getFromStorage(_keyUserInfo);
|
||||
if (stored != null && stored.isNotEmpty) {
|
||||
try {
|
||||
_cachedUserInfo = jsonDecode(stored) as Map<String, dynamic>;
|
||||
return _cachedUserInfo;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============ 私有存储方法 ============
|
||||
|
||||
String? _getFromStorage(String key) {
|
||||
try {
|
||||
// 使用 SharedPreferences 同步获取(但 SharedPreferences 是异步的)
|
||||
// 为了保持与原项目一致的同步接口,这里使用异步转同步
|
||||
// 但更好的方式是在初始化时预加载
|
||||
return _getFromStorageSync(key);
|
||||
} catch (e) {
|
||||
debugPrint('[TokenManager] Error reading from storage: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 同步读取(注意:SharedPreferences 本身是异步的,这里用同步方式需要谨慎)
|
||||
String? _getFromStorageSync(String key) {
|
||||
// 由于 SharedPreferences 是异步的,我们使用一个全局缓存
|
||||
// 实际项目中建议在 main 中调用 init() 预加载
|
||||
try {
|
||||
// 这里为了简化,直接返回 null,实际应该用异步方式
|
||||
// 建议使用下面的异步版本
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _saveToStorage(String key, String value) {
|
||||
// 异步保存,不等待结果
|
||||
_saveToStorageAsync(key, value);
|
||||
}
|
||||
|
||||
void _removeFromStorage(String key) {
|
||||
// 异步移除,不等待结果
|
||||
_removeFromStorageAsync(key);
|
||||
}
|
||||
|
||||
// ============ 异步存储方法(推荐使用) ============
|
||||
|
||||
/// 初始化:从存储中加载 Token
|
||||
Future<void> init() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_cachedToken = prefs.getString(_keyToken);
|
||||
|
||||
final userInfoStr = prefs.getString(_keyUserInfo);
|
||||
if (userInfoStr != null && userInfoStr.isNotEmpty) {
|
||||
try {
|
||||
_cachedUserInfo = jsonDecode(userInfoStr) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
_cachedUserInfo = null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[TokenManager] Init error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步获取 Token
|
||||
Future<String> getAsync() async {
|
||||
if (_cachedToken != null) {
|
||||
return _cachedToken!;
|
||||
}
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_cachedToken = prefs.getString(_keyToken);
|
||||
return _cachedToken ?? '';
|
||||
} catch (e) {
|
||||
debugPrint('[TokenManager] Get token error: $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步保存 Token
|
||||
Future<void> saveAsync(String token) async {
|
||||
if (token.isNotEmpty) {
|
||||
_cachedToken = token;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_keyToken, token);
|
||||
} catch (e) {
|
||||
debugPrint('[TokenManager] Save token error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步清除 Token
|
||||
Future<void> clearAsync() async {
|
||||
_cachedToken = null;
|
||||
_cachedUserInfo = null;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_keyToken);
|
||||
await prefs.remove(_keyUserInfo);
|
||||
} catch (e) {
|
||||
debugPrint('[TokenManager] Clear token error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 异步保存到存储
|
||||
Future<void> _saveToStorageAsync(String key, String value) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(key, value);
|
||||
} catch (e) {
|
||||
debugPrint('[TokenManager] Save to storage error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 异步从存储移除
|
||||
Future<void> _removeFromStorageAsync(String key) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(key);
|
||||
} catch (e) {
|
||||
debugPrint('[TokenManager] Remove from storage error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
60
lib/common/model/novyronst_user.dart
Normal file
60
lib/common/model/novyronst_user.dart
Normal file
@ -0,0 +1,60 @@
|
||||
import 'package:novyronst/common/model/parse_model.dart';
|
||||
|
||||
|
||||
class NovyronstUser {
|
||||
final String customerId;
|
||||
final bool isTourist;
|
||||
final String userLevel;
|
||||
final String avator;
|
||||
final String? familyName;
|
||||
final String? givingName;
|
||||
|
||||
const NovyronstUser({
|
||||
required this.customerId,
|
||||
required this.isTourist,
|
||||
required this.userLevel,
|
||||
required this.avator,
|
||||
this.familyName,
|
||||
this.givingName,
|
||||
});
|
||||
|
||||
factory NovyronstUser.fromJson(Map<String, dynamic> json) {
|
||||
return NovyronstUser(
|
||||
customerId: parseString(json['customer_id']),
|
||||
isTourist: parseBool(json['is_tourist']),
|
||||
userLevel: parseString(json['user_level']),
|
||||
avator: parseString(json['avator']),
|
||||
familyName: json['family_name'] as String?,
|
||||
givingName: parseString(json['giving_name']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'customer_id': customerId,
|
||||
'is_tourist': isTourist,
|
||||
'user_level': userLevel,
|
||||
'avator': avator,
|
||||
'family_name': familyName,
|
||||
'giving_name': givingName,
|
||||
};
|
||||
}
|
||||
|
||||
NovyronstUser copyWith({
|
||||
String? customerId,
|
||||
bool? isTourist,
|
||||
String? userLevel,
|
||||
String? avator,
|
||||
String? familyName,
|
||||
String? givingName,
|
||||
}) {
|
||||
return NovyronstUser(
|
||||
customerId: customerId ?? this.customerId,
|
||||
isTourist: isTourist ?? this.isTourist,
|
||||
userLevel: userLevel ?? this.userLevel,
|
||||
avator: avator ?? this.avator,
|
||||
familyName: familyName ?? this.familyName,
|
||||
givingName: givingName ?? this.givingName,
|
||||
);
|
||||
}
|
||||
}
|
||||
57
lib/common/model/parse_model.dart
Normal file
57
lib/common/model/parse_model.dart
Normal file
@ -0,0 +1,57 @@
|
||||
bool parseBool(dynamic value) {
|
||||
if (value == null) return false;
|
||||
if (value is bool) return value;
|
||||
if (value is num) return value != 0;
|
||||
if (value is String) {
|
||||
final v = value.trim().toLowerCase();
|
||||
return v == 'true' || v == '1' || v == 'yes' || v == 'y';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int parseInt(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is double) return value.toInt();
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
String parseString(dynamic value) {
|
||||
if (value == null) return '';
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
List<String>? parseStringList(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is List) {
|
||||
return value.map((item) => parseString(item)).toList();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<int>? parseIntList(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is List) {
|
||||
return value.map((item) => parseInt(item)).toList();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? parseMap(dynamic value) {
|
||||
if (value is Map<String, dynamic>) return value;
|
||||
if (value is Map) return Map<String, dynamic>.from(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
List<T> parseModelList<T>(
|
||||
dynamic value,
|
||||
T Function(Map<String, dynamic> json) fromJson,
|
||||
) {
|
||||
if (value is! List) return <T>[];
|
||||
return value
|
||||
.whereType<Map>()
|
||||
.map((item) => fromJson(Map<String, dynamic>.from(item)))
|
||||
.toList();
|
||||
}
|
||||
18
lib/common/model/register_model.dart
Normal file
18
lib/common/model/register_model.dart
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
class RegisterModel {
|
||||
final String token;
|
||||
final int customerId;
|
||||
final bool? autoLogin;
|
||||
final int touristId;
|
||||
|
||||
RegisterModel({required this.token, required this.customerId, this.autoLogin, required this.touristId});
|
||||
|
||||
factory RegisterModel.fromJson(Map<String, dynamic> json) {
|
||||
return RegisterModel(
|
||||
token: json['token'],
|
||||
customerId: json['customer_id'],
|
||||
autoLogin: json['auto_login'],
|
||||
touristId: json['tourist_id'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import 'package:novyronst/common/help/help_image.dart';
|
||||
import 'package:novyronst/pages/my/widgets/ny_dash_line.dart';
|
||||
import 'package:novyronst/root/lib_export.dart';
|
||||
import 'package:novyronst/tools/widgets/app_bg_page.dart';
|
||||
import 'package:novyronst/tools/widgets/extend_widget.dart';
|
||||
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
|
||||
|
||||
class MyPage extends StatefulWidget {
|
||||
@ -16,6 +17,10 @@ class MyPage extends StatefulWidget {
|
||||
|
||||
class _RootPageState extends State<MyPage> {
|
||||
|
||||
List<String> _titles = [
|
||||
'History', 'Settings', 'About', 'Privacy Policy', 'User Agreement'
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBgPage(
|
||||
@ -26,13 +31,65 @@ class _RootPageState extends State<MyPage> {
|
||||
children: [
|
||||
SizedBox(height: AppScreen.statusBarHeight + 48),
|
||||
_buildUserInfo(),
|
||||
NyDashLine()
|
||||
const SizedBox(height: 26),
|
||||
...List.generate(_titles.length, (index) {
|
||||
return _buildSectionView(_titles[index], index, onTap: () {
|
||||
if (index == 0) {
|
||||
|
||||
} else if (index == 1) {
|
||||
|
||||
} else if (index == 2) {
|
||||
|
||||
} else if (index == 3) {
|
||||
|
||||
} else if (index == 4) {
|
||||
|
||||
}
|
||||
});
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionView(String title, int index, {Function()? onTap}) {
|
||||
String imgName = ConstImage.history;
|
||||
if (index == 1) {
|
||||
imgName = ConstImage.settings;
|
||||
} else if (index == 2) {
|
||||
imgName = ConstImage.about;
|
||||
} else if (index == 3) {
|
||||
imgName = ConstImage.primary;
|
||||
} else if (index == 4) {
|
||||
imgName = ConstImage.webSite;
|
||||
}
|
||||
return Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
HelpImage.asset(imgName, width: 36, height: 36),
|
||||
const SizedBox(width: 10),
|
||||
NovyText(title,
|
||||
fontSize: 14,
|
||||
)
|
||||
],
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios, size: 17, color: Colors.white)
|
||||
],
|
||||
)),
|
||||
if (index < 4) NyDashLine()
|
||||
],
|
||||
),
|
||||
).addInkWell(onTap: onTap);
|
||||
}
|
||||
|
||||
Widget _buildUserInfo() {
|
||||
return Row(
|
||||
children: [
|
||||
|
||||
177
lib/tools/util/ny_device_info.dart
Normal file
177
lib/tools/util/ny_device_info.dart
Normal file
@ -0,0 +1,177 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:android_id/android_id.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:novyronst/common/api/api_string.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
@immutable
|
||||
class NyDeviceInfo {
|
||||
final String deviceId;
|
||||
final String systemType;
|
||||
final String systemVersion;
|
||||
final String brand;
|
||||
final String model;
|
||||
|
||||
final String appName;
|
||||
final String appPackageName;
|
||||
final String appVersion;
|
||||
final String idfv;
|
||||
|
||||
static NyDeviceInfo? _instance;
|
||||
|
||||
const NyDeviceInfo._({
|
||||
required this.deviceId,
|
||||
required this.systemType,
|
||||
required this.systemVersion,
|
||||
required this.brand,
|
||||
required this.model,
|
||||
required this.appName,
|
||||
required this.appPackageName,
|
||||
required this.appVersion,
|
||||
required this.idfv,
|
||||
});
|
||||
|
||||
static NyDeviceInfo get instance {
|
||||
if (_instance == null) {
|
||||
throw StateError('error');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void setMockInstance({
|
||||
String deviceId = 'test-device-id',
|
||||
String systemType = 'test',
|
||||
String systemVersion = '1.0',
|
||||
String brand = 'test',
|
||||
String model = 'test',
|
||||
String appName = 'Glimzo',
|
||||
String appPackageName = 'com.example.skywood',
|
||||
String appVersion = '1.0.0',
|
||||
String idfv = 'test-idfv',
|
||||
}) {
|
||||
_instance = NyDeviceInfo._(
|
||||
deviceId: deviceId,
|
||||
systemType: systemType,
|
||||
systemVersion: systemVersion,
|
||||
brand: brand,
|
||||
model: model,
|
||||
appName: appName,
|
||||
appPackageName: appPackageName,
|
||||
appVersion: appVersion,
|
||||
idfv: idfv,
|
||||
);
|
||||
}
|
||||
|
||||
/// 初始化设备信息
|
||||
static Future<void> init() async {
|
||||
if (_instance != null) return;
|
||||
|
||||
String deviceId = '',
|
||||
systemType = '',
|
||||
systemVersion = '',
|
||||
brand = '',
|
||||
model = '',
|
||||
appName = 'Glimzo',
|
||||
appPackageName = '',
|
||||
appVersion = '',
|
||||
idfv = '';
|
||||
|
||||
// 获取应用信息
|
||||
await _initAppInfo()
|
||||
.then((info) {
|
||||
appPackageName = info['packageName'] ?? '';
|
||||
appVersion = info['version'] ?? '';
|
||||
appName = ApiString.appName;
|
||||
})
|
||||
.catchError((e) {
|
||||
debugPrint('[sDeviceInfo] getElPackageName: $e');
|
||||
});
|
||||
|
||||
// 获取设备信息
|
||||
if (Platform.isAndroid) {
|
||||
await _initAndroidInfo()
|
||||
.then((info) {
|
||||
deviceId = info['deviceId'] ?? '';
|
||||
systemType = 'android';
|
||||
systemVersion = info['systemVersion'] ?? '';
|
||||
brand = info['brand'] ?? '';
|
||||
model = info['model'] ?? '';
|
||||
})
|
||||
.catchError((e) {
|
||||
debugPrint('[GlimzoUtilDeviceInfo] Android: $e');
|
||||
});
|
||||
} else if (Platform.isIOS) {
|
||||
await _initIOSInfo()
|
||||
.then((info) {
|
||||
deviceId = info['deviceId'] ?? '';
|
||||
idfv = info['idfv'] ?? '';
|
||||
systemType = 'ios';
|
||||
systemVersion = info['systemVersion'] ?? '';
|
||||
brand = info['brand'] ?? '';
|
||||
model = info['model'] ?? '';
|
||||
})
|
||||
.catchError((e) {
|
||||
debugPrint('[GlimzoUtilDeviceInfo] iOS: $e');
|
||||
});
|
||||
}
|
||||
|
||||
_instance = NyDeviceInfo._(
|
||||
deviceId: deviceId,
|
||||
systemType: systemType,
|
||||
systemVersion: systemVersion,
|
||||
brand: brand,
|
||||
model: model,
|
||||
appName: appName,
|
||||
appPackageName: appPackageName,
|
||||
appVersion: appVersion,
|
||||
idfv: idfv,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取应用信息
|
||||
static Future<Map<String, String>> _initAppInfo() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
return {
|
||||
'packageName': packageInfo.packageName,
|
||||
'version': packageInfo.version,
|
||||
};
|
||||
}
|
||||
|
||||
/// 获取 Android 设备信息
|
||||
static Future<Map<String, String?>> _initAndroidInfo() async {
|
||||
final plugin = DeviceInfoPlugin();
|
||||
final androidInfo = await plugin.androidInfo;
|
||||
final androidIdPlugin = AndroidId();
|
||||
final androidId = await androidIdPlugin.getId();
|
||||
return {
|
||||
'deviceId': androidId ?? androidInfo.id,
|
||||
'systemVersion': androidInfo.version.release,
|
||||
'brand': androidInfo.brand,
|
||||
'model': androidInfo.model,
|
||||
};
|
||||
}
|
||||
|
||||
/// 获取 iOS 设备信息
|
||||
static Future<Map<String, String?>> _initIOSInfo() async {
|
||||
final plugin = DeviceInfoPlugin();
|
||||
final iosInfo = await plugin.iosInfo;
|
||||
final secureStorage = FlutterSecureStorage();
|
||||
String? idfvStore = await secureStorage.read(key: 'identifierForVendor');
|
||||
idfvStore ??= iosInfo.identifierForVendor;
|
||||
|
||||
if (idfvStore != null) {
|
||||
await secureStorage.write(key: 'identifierForVendor', value: idfvStore);
|
||||
}
|
||||
|
||||
return {
|
||||
'deviceId': idfvStore,
|
||||
'idfv': iosInfo.identifierForVendor ?? '',
|
||||
'systemVersion': iosInfo.systemVersion,
|
||||
'brand': iosInfo.model,
|
||||
'model': iosInfo.modelName,
|
||||
};
|
||||
}
|
||||
}
|
||||
139
lib/tools/widgets/app_toast_util.dart
Normal file
139
lib/tools/widgets/app_toast_util.dart
Normal file
@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:novyronst/common/help/help_image.dart';
|
||||
|
||||
// 加载数据的函数类型
|
||||
typedef LoadBlock<T> = Future<T> Function();
|
||||
|
||||
// 弹出Toast提示的类
|
||||
class AppToastUtil {
|
||||
static const String _defaultLoadingText = "";
|
||||
static const String _loadingText = "loading...";
|
||||
|
||||
/// 显示普通 Toast 提示
|
||||
static Future<void> showToast(
|
||||
Object msg, {
|
||||
Duration displayTime = const Duration(seconds: 2),
|
||||
}) async {
|
||||
await dismiss();
|
||||
return SmartDialog.showToast(
|
||||
msg.toString(),
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
displayTime: displayTime,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> showError(
|
||||
Object msg, {
|
||||
Duration displayTime = const Duration(seconds: 2),
|
||||
}) {
|
||||
return showFail(msg, displayTime: displayTime);
|
||||
}
|
||||
|
||||
/// 显示失败提示
|
||||
static Future<void> showFail(
|
||||
Object msg, {
|
||||
Duration displayTime = const Duration(seconds: 2),
|
||||
Function()? onDismiss,
|
||||
}) async {
|
||||
await dismiss();
|
||||
return SmartDialog.showNotify(
|
||||
msg: msg.toString(),
|
||||
alignment: Alignment.center,
|
||||
notifyType: NotifyType.failure,
|
||||
displayTime: displayTime,
|
||||
debounce: true,
|
||||
onDismiss: onDismiss,
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示成功提示
|
||||
static Future<T?> showSuccess<T>(
|
||||
Object msg, {
|
||||
Duration displayTime = const Duration(seconds: 2),
|
||||
}) async {
|
||||
await dismiss();
|
||||
return SmartDialog.showNotify<T>(
|
||||
msg: msg.toString(),
|
||||
notifyType: NotifyType.success,
|
||||
debounce: true,
|
||||
displayTime: displayTime,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<T?> showInfo<T>(
|
||||
Object msg, {
|
||||
Duration displayTime = const Duration(seconds: 2),
|
||||
}) {
|
||||
return showWarning<T>(msg, displayTime: displayTime);
|
||||
}
|
||||
|
||||
static Future<T?> showWarning<T>(
|
||||
Object msg, {
|
||||
Duration displayTime = const Duration(seconds: 2),
|
||||
}) async {
|
||||
await dismiss();
|
||||
return SmartDialog.showNotify<T>(
|
||||
msg: msg.toString(),
|
||||
notifyType: NotifyType.warning,
|
||||
debounce: true,
|
||||
displayTime: displayTime,
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示 Lottie 加载提示
|
||||
static Future<T?> showLottieLoading<T>({
|
||||
Duration? displayTime,
|
||||
}) async {
|
||||
await dismiss();
|
||||
return SmartDialog.showLoading<T>(
|
||||
displayTime: displayTime ?? const Duration(seconds: 5),
|
||||
maskColor: Colors.transparent,
|
||||
builder: (_) {
|
||||
return SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
// child: Image.asset(ConstImage.loading, fit: BoxFit.fill),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示加载提示
|
||||
static Future<T?> showLoading<T>({
|
||||
String text = _loadingText,
|
||||
Duration displayTime = const Duration(seconds: 10),
|
||||
}) async {
|
||||
await dismiss();
|
||||
return SmartDialog.showLoading<T>(msg: text, displayTime: displayTime);
|
||||
}
|
||||
|
||||
/// 关闭特定状态的弹窗
|
||||
static Future<void> dismiss<T>({SmartStatus status = SmartStatus.loading}) {
|
||||
return SmartDialog.dismiss<T>(status: status);
|
||||
}
|
||||
|
||||
/// 关闭所有弹窗(占位)
|
||||
static void dismissAll() {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
|
||||
/// 自动显示加载框并在完成后关闭
|
||||
static Future<T> loading<T>(
|
||||
LoadBlock<T> block, {
|
||||
String? text,
|
||||
bool isLoading = true,
|
||||
}) async {
|
||||
if (isLoading) {
|
||||
showLoading(text: text ?? _defaultLoadingText);
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await block();
|
||||
return result;
|
||||
} finally {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
120
pubspec.lock
120
pubspec.lock
@ -9,6 +9,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -65,6 +73,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+4"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -137,6 +153,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -240,6 +264,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
group_button:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: group_button
|
||||
sha256: "0610fcf28ed122bfb4b410fce161a390f7f2531d55d1d65c5375982001415940"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.4"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -320,6 +352,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -480,6 +520,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
share_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.2"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -573,6 +629,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
talker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: talker
|
||||
sha256: f1a14d623f1d1bec42bb3bb77674eb766ffe8d26e5f79af652d85cb097c3e757
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.17"
|
||||
talker_flutter:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: talker_flutter
|
||||
sha256: "54cbbf852101721664faf4a05639fd2fdefdc37178327990abea00390690d4bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.16"
|
||||
talker_logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: talker_logger
|
||||
sha256: "459205c3e571f97ecc6be6e1b1b7e6b97b853e78ea458894650be407596e3216"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.17"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -597,6 +677,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -29,6 +29,7 @@ dependencies:
|
||||
flutter_smart_dialog: ^5.1.0
|
||||
flutter_secure_storage: ^9.2.4
|
||||
android_id: ^0.5.1
|
||||
logger: ^2.5.0
|
||||
|
||||
|
||||
|
||||
@ -36,8 +37,8 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
flutter_lints: ^6.0.0
|
||||
talker_flutter: ^5.0.2
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user