feat: 整体常用控件封装,常用工具类。首页数据对接
BIN
assets/imgs/heart.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/imgs/home_card_bg.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/imgs/home_opennow_box.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/imgs/home_title_daily.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/imgs/home_title_hot.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/imgs/home_title_hot_cate.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/imgs/home_title_mystery.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
assets/imgs/home_title_weekly.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
assets/imgs/home_top_bg.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/imgs/splash_page.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
21
lib/global/client/play_client.dart
Normal file
@ -0,0 +1,21 @@
|
||||
|
||||
import 'package:skywood/global/request/api_const.dart';
|
||||
import 'package:skywood/global/request/app_request.dart';
|
||||
import 'package:skywood/utils/models/drama/skywood_home.dart';
|
||||
|
||||
class PlayClient {
|
||||
|
||||
static Future<List<SkywoodHome>> getHomeData() {
|
||||
return AppRequest.get(ApiStringPlay.getHomeModules).then((value) {
|
||||
if (value.isSuccess) {
|
||||
List? list = value.data['data']['list'];
|
||||
List<SkywoodHome> homeList = [];
|
||||
if (list != null) {
|
||||
homeList = list.map((e) => SkywoodHome.fromJson(e)).toList();
|
||||
}
|
||||
return homeList;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
30
lib/global/client/user_client.dart
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
import 'package:skywood/global/request/api_const.dart';
|
||||
import 'package:skywood/global/request/app_request.dart';
|
||||
import 'package:skywood/global/tool/sky_log.dart';
|
||||
import 'package:skywood/utils/models/user/register_bean.dart';
|
||||
|
||||
class UserClient {
|
||||
|
||||
static Future login() {
|
||||
final body = <String, dynamic>{
|
||||
"avator": '',
|
||||
"email": '123456@qq.com',
|
||||
"family_name": 'test1',
|
||||
"platform": 'android',
|
||||
"third_id": '32434',
|
||||
};
|
||||
return AppRequest.post(ApiString.login, data: body).then((value) {
|
||||
SkyLog.print(value.data, tag: 'loginnnn');
|
||||
});
|
||||
}
|
||||
|
||||
static Future<RegisterBean?> register() {
|
||||
return AppRequest.post(ApiString.register).then((value) {
|
||||
if (value.isSuccess) {
|
||||
return RegisterBean.fromJson(value.data['data']);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
import 'dart:ui';
|
||||
|
||||
class CommonColor {
|
||||
|
||||
// 主题色 #653CFA
|
||||
static const Color primacyColor = Color(0xFF653CFA);
|
||||
static const Color black = Color(0xFF030510);
|
||||
// static const Color black = Color(0xFF030510);
|
||||
// 040408
|
||||
static const Color black = Color(0xFF040408);
|
||||
// grey
|
||||
static const Color grey = Color(0xFF8e8e93);
|
||||
static const Color goldColor = Color(0xFFc5a059);
|
||||
@ -12,7 +13,6 @@ class CommonColor {
|
||||
static const Color goldDark = Color(0xFF92400e);
|
||||
static const Color secondaryColor = Color(0xFF9ca3af);
|
||||
|
||||
|
||||
static const Color fireRed = Color(0xFFf01d1d);
|
||||
static const Color woodGreen = Color(0xFF56a324);
|
||||
static const Color earthBrown = Color(0xFFcb8a46);
|
||||
@ -28,9 +28,9 @@ class CommonColor {
|
||||
static const Color diamondColor = Color(0xFFB9F2FF);
|
||||
|
||||
static Color withValue(String value) {
|
||||
String _v = value.replaceAll('#', '');
|
||||
String res = '0xff$_v';
|
||||
String colorValue = value.replaceAll('#', '');
|
||||
String res = '0xff$colorValue';
|
||||
int valueInt = int.parse(res);
|
||||
return Color(valueInt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
99
lib/global/request/api_const.dart
Normal file
@ -0,0 +1,99 @@
|
||||
final class ApiConst {
|
||||
ApiConst._();
|
||||
|
||||
static const appName = 'skywood';
|
||||
static const int pageSize = 12;
|
||||
static const String apiLink = 'https://api-quickeltv.quickeltv.com/quickeltv';
|
||||
|
||||
static String webPrefix = "quickeltv.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";
|
||||
|
||||
static String webHelpIndex = "https://campaign.$webPrefix/pages/leave/index";
|
||||
|
||||
static String webHelpList = "https://campaign.$webPrefix/pages/leave/list";
|
||||
|
||||
static String webHelpDetail =
|
||||
"https://campaign.$webPrefix/pages/leave/detail";
|
||||
|
||||
static String iosDownloadUrl = "";
|
||||
|
||||
static String androidDownloadUrl = "";
|
||||
}
|
||||
|
||||
class ApiString {
|
||||
static const String login = "/customer/login";
|
||||
// /customer/register
|
||||
static const String register = "/customer/register";
|
||||
// /customer/info
|
||||
static const String userInfo = "/customer/info";
|
||||
// /customer/signout
|
||||
static const String signOut = "/customer/signout";
|
||||
// /customer/logoff
|
||||
static const String logOff = "/customer/logoff";
|
||||
|
||||
/// events
|
||||
// eventCreateHistory POST /createHistory 上报播放历史
|
||||
// eventUserEnterApp POST /customer/enterTheApp 用户进入 App
|
||||
// eventUserOnline POST /customer/onLine 用户在线,可带 prevToken
|
||||
// eventUserLeaveApp POST /customer/leaveApp 用户离开 App,可带 prevToken
|
||||
// eventPlaySeconds POST /uploadHistorySeconds 上报单集播放秒数
|
||||
// eventWatchTime POST /watchShortDuration 上报短剧观看时长
|
||||
// eventReport POST /event/add 通用事件上报,目前枚举只有 force_update
|
||||
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 ApiStringPlay {
|
||||
// getRecommandsList GET /getRecommands 推荐列表,分页参数 current_page/page_size,可带 revolution
|
||||
// getVideoDetail GET /getVideoDetails 获取剧详情,query: short_play_id, video_id
|
||||
// getDetailsRecommand GET /getDetailsRecommand 详情页推荐列表;注意方法参数 page/pageSize/data 当前没有传入请求
|
||||
static const String getRecommandsList = "/getRecommands";
|
||||
static const String getVideoDetail = "/getVideoDetails";
|
||||
static const String getDetailsRecommand = "/getDetailsRecommand";
|
||||
|
||||
// getHomeModules GET /home/all-modules 首页全部模块数据
|
||||
// getRankingList POST /homeRanking 首页榜单,body 带 type,支持 most_trending/top_searched/new_releases
|
||||
// getCategoriesList GET /getCategories 分类列表
|
||||
// getCategoriesDetail GET /videoList 分类详情/剧列表,分页 + extraParams
|
||||
// getWaterflowList POST /newShortPlay 首页瀑布流,分页 + extraParams
|
||||
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";
|
||||
|
||||
// getSearchHots GET /search/hots 搜索热门列表
|
||||
// getSearch GET /search 关键词搜索,query: search
|
||||
// collect POST /collect 收藏,body: short_play_id, video_id
|
||||
// cancelCollect POST /cancelCollect 取消收藏,body: short_play_id
|
||||
// getMyCollections GET /myCollections 我的收藏列表,分页
|
||||
// getMyHistorys GET /myHistorys 播放历史列表,分页
|
||||
// getNoticeNum POST /noticeNum 获取通知数量
|
||||
// versionControl GET /customer/versionControl 版本检查
|
||||
// getLanguges GET /languges 获取语言列表,接口名拼写就是 languges
|
||||
// getTranslates GET /translates 获取语言包,query: lang_key,默认 en
|
||||
static const String getSearchHots = "/search/hots";
|
||||
static const String getSearch = "/search";
|
||||
static const String collect = "/collect";
|
||||
static const String cancelCollect = "/cancelCollect";
|
||||
static const String getMyCollections = "/myCollections";
|
||||
static const String getMyHistorys = "/myHistorys";
|
||||
static const String getNoticeNum = "/noticeNum";
|
||||
static const String versionControl = "/customer/versionControl";
|
||||
static const String getLanguges = "/languges";
|
||||
static const String getTranslates = "/translates";
|
||||
}
|
||||
|
||||
|
||||
@ -1,154 +1,629 @@
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:skywood/global/request/api_const.dart';
|
||||
import 'package:skywood/global/request/network_interceptor.dart';
|
||||
import 'package:skywood/utils/tools/app_toast.dart';
|
||||
import 'package:talker_dio_logger/talker_dio_logger.dart';
|
||||
|
||||
class AppRequest {
|
||||
import '../../utils/tools/token_manage.dart';
|
||||
|
||||
static const Duration defaultTimeout = Duration(seconds: 15);
|
||||
static final AppRequest instance = AppRequest();
|
||||
final Dio dio;
|
||||
/// HTTP 请求工具类(单例)
|
||||
class AppService {
|
||||
static AppService? _instance;
|
||||
late Dio _dio;
|
||||
|
||||
AppRequest({
|
||||
Dio? dio,
|
||||
String baseUrl = '',
|
||||
Duration connectTimeout = defaultTimeout,
|
||||
Duration sendTimeout = defaultTimeout,
|
||||
Duration receiveTimeout = defaultTimeout,
|
||||
}) : dio =
|
||||
dio ??
|
||||
Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: connectTimeout,
|
||||
sendTimeout: sendTimeout,
|
||||
receiveTimeout: receiveTimeout,
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
|
||||
void setBaseUrl(String baseUrl) {
|
||||
dio.options.baseUrl = baseUrl;
|
||||
static AppService get instance {
|
||||
_instance ??= AppService._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
void setHeader(String key, Object? value) {
|
||||
dio.options.headers[key] = value;
|
||||
AppService._internal() {
|
||||
_initDio();
|
||||
}
|
||||
|
||||
void removeHeader(String key) {
|
||||
dio.options.headers.remove(key);
|
||||
Dio get dio => _dio;
|
||||
|
||||
/// 初始化 Dio 配置
|
||||
void _initDio() {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: ApiConst.apiLink,
|
||||
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(NetworkInterceptor());
|
||||
|
||||
// 开发环境添加日志
|
||||
// if (!kReleaseMode) {
|
||||
// _dio.interceptors.add(TalkerDioLogger(
|
||||
// settings: TalkerDioLoggerSettings(
|
||||
// printRequestHeaders: true,
|
||||
// printRequestData: true,
|
||||
// printResponseData: true,
|
||||
// printResponseHeaders: false,
|
||||
// ),
|
||||
// ));
|
||||
// }
|
||||
}
|
||||
|
||||
void setToken(String token, {String prefix = 'Bearer'}) {
|
||||
final value = prefix.isEmpty ? token : '$prefix $token';
|
||||
setHeader('Authorization', value);
|
||||
}
|
||||
|
||||
void clearToken() {
|
||||
removeHeader('Authorization');
|
||||
}
|
||||
|
||||
Future<T?> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: 'GET',
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> post<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> put<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: 'PUT',
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> delete<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: 'DELETE',
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> request<T>(
|
||||
String path, {
|
||||
required String method,
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
final response = await dio.request<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: _withMethod(options, method),
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Options _withMethod(Options? options, String method) {
|
||||
return options == null
|
||||
? Options(method: method)
|
||||
: options.copyWith(method: method);
|
||||
/// 更新配置
|
||||
void updateBaseOptions(BaseOptions options) {
|
||||
_dio.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
/// ============ 请求方法封装 ============
|
||||
|
||||
class AppRequest {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/// PUT 请求(带重试)
|
||||
static Future<HttpResult<T>> put<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
int? retry,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _executeRequest(
|
||||
() => _dio.put(
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/// DELETE 请求(带重试)
|
||||
static Future<HttpResult<T>> delete<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
int? retry,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _executeRequest(
|
||||
// retries: retry,
|
||||
() => _dio.delete(
|
||||
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
|
||||
AppToast.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) {
|
||||
TokenManager().update();
|
||||
if (resp.statusCode == 402) {
|
||||
AppToast.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) {
|
||||
TokenManager().update();
|
||||
return '未授权,请重新登录';
|
||||
}
|
||||
if (statusCode == 402) {
|
||||
TokenManager().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);
|
||||
}
|
||||
}
|
||||
143
lib/global/request/network_interceptor.dart
Normal file
@ -0,0 +1,143 @@
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:skywood/global/request/api_const.dart';
|
||||
import 'package:skywood/utils/tools/sky_device_info.dart';
|
||||
import 'package:skywood/utils/tools/sky_time_tool.dart';
|
||||
import 'package:skywood/utils/tools/app_sp.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../../utils/tools/token_manage.dart';
|
||||
import '../tool/sky_encrypt.dart';
|
||||
|
||||
/// 网络请求拦截器
|
||||
class NetworkInterceptor 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 刷新
|
||||
TokenManager().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 = SkyEncrypt.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 = TokenManager();
|
||||
final token = tokenManager.get();
|
||||
// print('查看token: $token');
|
||||
if (token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
// 其他公共参数
|
||||
String langKey = await AppSp().getString('GLIMZO_STORE_LANGKEY') ?? 'en';
|
||||
final deviceInfo = SkyDeviceInfo.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': langKey,
|
||||
'time-zone': SkyTimeTool.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");
|
||||
// 这里可以根据需要处理错误提示
|
||||
}
|
||||
}
|
||||
155
lib/global/tool/sky_encrypt.dart
Normal file
@ -0,0 +1,155 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:math';
|
||||
|
||||
class SkyEncrypt {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
146
lib/global/tool/sky_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 SkyLog {
|
||||
SkyLog._();
|
||||
|
||||
static final SkyLog _instance = SkyLog._();
|
||||
|
||||
static SkyLog get instance => _instance;
|
||||
|
||||
final Talker _talker = TalkerFlutter.init();
|
||||
|
||||
factory SkyLog() => _instance;
|
||||
|
||||
Talker get tLog => _talker;
|
||||
|
||||
/// DevTools / 控制台里可按名称过滤。
|
||||
static const String logName = 'SkyLog';
|
||||
|
||||
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)];
|
||||
}
|
||||
35
lib/global/tool/sky_routers.dart
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:skywood/pages/splash/splash_page.dart';
|
||||
|
||||
class SkyRouters {
|
||||
static const String splash = '/splash';
|
||||
}
|
||||
|
||||
class SkyPages {
|
||||
static const initial = SkyRouters.splash;
|
||||
|
||||
static final List<GetPage> routes = [
|
||||
routePage(name: SkyRouters.splash, page: ()=> SplashPage())
|
||||
];
|
||||
}
|
||||
|
||||
GetPage routePage({
|
||||
required String name,
|
||||
required GetPageBuilder page,
|
||||
Bindings? binding,
|
||||
Transition transition = Transition.rightToLeft,
|
||||
Duration transitionDuration = const Duration(milliseconds: 300),
|
||||
CustomTransition? customTransition,
|
||||
bool popGesture = true, // Allow iOS swipe-back gesture by default
|
||||
}) {
|
||||
return GetPage(
|
||||
name: name,
|
||||
page: page,
|
||||
binding: binding,
|
||||
transition: transition,
|
||||
transitionDuration: transitionDuration,
|
||||
customTransition: customTransition,
|
||||
popGesture: popGesture, // Pass through to GetPage
|
||||
);
|
||||
}
|
||||
@ -2,7 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:skywood/global/request/app_request.dart';
|
||||
import 'package:skywood/global/tool/sky_routers.dart';
|
||||
import 'package:skywood/main/main_root.dart';
|
||||
import 'package:skywood/pages/controllers/user_controller.dart';
|
||||
import 'package:skywood/utils/tools/sky_device_info.dart';
|
||||
import 'package:skywood/utils/tools/token_manage.dart';
|
||||
|
||||
import 'global/const/common_color.dart';
|
||||
import 'global/language/app_language.dart';
|
||||
@ -10,22 +15,21 @@ import 'global/language/app_language.dart';
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// await initServices();
|
||||
await initServices();
|
||||
|
||||
Locale? locale = await AppLanguage.getLocale();
|
||||
runApp(ComApp(
|
||||
home: MainRoot(),
|
||||
locale: locale,
|
||||
));
|
||||
runApp(ComApp(home: MainRoot(), locale: locale));
|
||||
}
|
||||
|
||||
Future<void> initServices() async {
|
||||
|
||||
await SkyDeviceInfo.init();
|
||||
await TokenManager().init();
|
||||
await Get.putAsync(() async => UserController());
|
||||
}
|
||||
|
||||
class ComApp extends StatelessWidget {
|
||||
const ComApp({super.key,
|
||||
required this.home,
|
||||
this.locale,
|
||||
});
|
||||
const ComApp({super.key, required this.home, this.locale});
|
||||
|
||||
final Widget home;
|
||||
final Locale? locale;
|
||||
@ -40,26 +44,38 @@ class ComApp extends StatelessWidget {
|
||||
translations: AppLanguage(),
|
||||
locale: locale ?? Get.deviceLocale,
|
||||
fallbackLocale: const Locale('en'),
|
||||
// initialBinding: AppBindings(),
|
||||
initialBinding: AppBindings(),
|
||||
initialRoute: SkyPages.initial,
|
||||
getPages: SkyPages.routes,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: CommonColor.primacyColor),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: CommonColor.primacyColor,
|
||||
),
|
||||
appBarTheme: AppBarTheme(elevation: 0, centerTitle: true),
|
||||
splashColor: Colors.transparent,
|
||||
useMaterial3: true,
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
),
|
||||
home: home,
|
||||
builder: FlutterSmartDialog.init(builder: (context, child) {
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(1.0)),
|
||||
child: child!
|
||||
);
|
||||
}),
|
||||
onInit: () {
|
||||
|
||||
},
|
||||
builder: FlutterSmartDialog.init(
|
||||
builder: (context, child) {
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(
|
||||
context,
|
||||
).copyWith(textScaler: TextScaler.linear(1.0)),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
),
|
||||
onInit: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AppBindings extends Bindings {
|
||||
|
||||
@override
|
||||
void dependencies() {
|
||||
// Get.lazyPut(() => UserController());
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,4 +5,6 @@ export 'package:skywood/global/const/common_color.dart';
|
||||
export 'package:skywood/global/const/common_string.dart';
|
||||
|
||||
export 'package:skywood/utils/tools/app_screen.dart';
|
||||
export 'package:skywood/utils/tools/help_img.dart';
|
||||
export 'package:skywood/utils/tools/help_img.dart';
|
||||
|
||||
export 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
@ -58,17 +58,9 @@ class _MainRootState extends State<MainRoot> {
|
||||
minimum: EdgeInsets.only(bottom: 10.h),
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 49.h, width: 345.w,
|
||||
),
|
||||
Positioned(
|
||||
top: 11.h,
|
||||
child: _buildTabBar(),
|
||||
),
|
||||
Positioned(
|
||||
top: 0, left: 15.w,
|
||||
child: _buildImgs(),
|
||||
)
|
||||
SizedBox(height: 49.h, width: 345.w),
|
||||
Positioned(top: 11.h, child: _buildTabBar()),
|
||||
Positioned(top: 0, left: 15.w, child: _buildImgs()),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -76,8 +68,9 @@ class _MainRootState extends State<MainRoot> {
|
||||
}
|
||||
|
||||
Widget _buildImgs() {
|
||||
return Container(
|
||||
height: 38.h, width: 345.w,
|
||||
return SizedBox(
|
||||
height: 38.h,
|
||||
width: 345.w,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: List.generate(_items.length, (index) {
|
||||
|
||||
36
lib/pages/controllers/user_controller.dart
Normal file
@ -0,0 +1,36 @@
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:skywood/global/client/user_client.dart';
|
||||
import 'package:skywood/main/main_root.dart';
|
||||
import 'package:skywood/utils/tools/help_nav.dart';
|
||||
import 'package:skywood/utils/tools/token_manage.dart';
|
||||
|
||||
class UserController extends GetxController {
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// _userRegister();
|
||||
}
|
||||
|
||||
void _userRegister() async {
|
||||
// 先拿token没有的话去注册
|
||||
final tm = TokenManager();
|
||||
String token = tm.get();
|
||||
print('tokkkkkk: $token');
|
||||
if (token.isNotEmpty) {
|
||||
// 成功后去到主页面
|
||||
print('object去主页面');
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
HelpNav.off(MainRoot());
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final res = await UserClient.register();
|
||||
if (res != null) {
|
||||
tm.save(res.token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:skywood/global/client/play_client.dart';
|
||||
import 'package:skywood/global/client/user_client.dart';
|
||||
import 'package:skywood/main/lib_file.dart';
|
||||
import 'package:skywood/pages/home/widgets/daily_quest_widget.dart';
|
||||
import 'package:skywood/pages/widgets/common_bg_page.dart';
|
||||
import 'package:skywood/utils/models/drama/skywood_home.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
@ -13,18 +17,107 @@ class HomePage extends StatefulWidget {
|
||||
|
||||
class _MainRootState extends State<HomePage> {
|
||||
|
||||
List<SkywoodHome> _homeData = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_getHomeData();
|
||||
}
|
||||
|
||||
void _getHomeData() {
|
||||
// home_v3_recommand
|
||||
// new_recommand
|
||||
// week_ranking
|
||||
// week_highest_recommend
|
||||
// marquee
|
||||
// home_banner
|
||||
// get_details_recommand
|
||||
PlayClient.getHomeData().then((value) {
|
||||
for (SkywoodHome element in value) {
|
||||
print('lele: ${element.moduleKey} : ${element.dataList.length}');
|
||||
}
|
||||
setState(() {
|
||||
_homeData = value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonBgPage(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
child: SafeArea(
|
||||
child: _homeData.isEmpty ? SizedBox() : CustomScrollView(
|
||||
slivers: [
|
||||
_buildTopTwoImgs(),
|
||||
_buildSpace(),
|
||||
DailyQuestWidget(
|
||||
dramaList: _homeData.firstWhere((element) => element.moduleKey == 'home_v3_recommand').dataList,
|
||||
),
|
||||
_buildSpace(height: 10),
|
||||
_buildTitleImg(ConstImg.homeTitleMystery),
|
||||
_buildSpace(),
|
||||
// 345 355
|
||||
_buildTitleImg(
|
||||
ConstImg.homeOpenNowBox,
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
height: 355.h,
|
||||
),
|
||||
_buildSpace(height: 10),
|
||||
_buildTitleImg(ConstImg.homeTitleHot),
|
||||
_buildSpace(height: 300),
|
||||
_buildTitleImg(ConstImg.homeTitleWeekly),
|
||||
_buildSpace(height: 300),
|
||||
_buildTitleImg(ConstImg.homeTitleHotCate),
|
||||
_buildSpace(height: 300),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SliverToBoxAdapter _buildTopTwoImgs() {
|
||||
// 104-76
|
||||
return SliverToBoxAdapter(
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(width: double.infinity, height: (204 + 104 - 28).h),
|
||||
Container(
|
||||
height: 204.h,
|
||||
padding: EdgeInsets.only(left: 25.w, top: 3.h, right: 25.w),
|
||||
child: HelpImg.asset(ConstImg.homeTopBg, fit: BoxFit.fill),
|
||||
),
|
||||
Positioned(
|
||||
top: (204 - 28).h,
|
||||
height: 104.h,
|
||||
width: AppScreen.screenWidth,
|
||||
child: HelpImg.asset(ConstImg.homeTitleDaily, fit: BoxFit.fill),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitleImg() {
|
||||
return Container();
|
||||
SliverToBoxAdapter _buildSpace({double height = 20}) {
|
||||
return SliverToBoxAdapter(child: SizedBox(height: height.h));
|
||||
}
|
||||
}
|
||||
|
||||
SliverToBoxAdapter _buildTitleImg(
|
||||
String imgName, {
|
||||
double? height,
|
||||
double? width,
|
||||
EdgeInsets? padding,
|
||||
}) {
|
||||
// 104-76
|
||||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
width: width ?? double.infinity,
|
||||
height: height ?? 104.h,
|
||||
child: HelpImg.asset(imgName, fit: BoxFit.fill),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
93
lib/pages/home/widgets/daily_quest_widget.dart
Normal file
@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:skywood/global/const/common_color.dart';
|
||||
import 'package:skywood/utils/models/drama/skywood_drama.dart';
|
||||
import 'package:skywood/utils/tools/sky_time_tool.dart';
|
||||
import 'package:skywood/utils/tools/widgets/sky_common_widget.dart';
|
||||
|
||||
import '../../../utils/tools/help_img.dart';
|
||||
|
||||
class DailyQuestWidget extends StatelessWidget {
|
||||
|
||||
const DailyQuestWidget({super.key,
|
||||
required this.dramaList,
|
||||
});
|
||||
|
||||
final List<SkywoodDrama> dramaList;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double height = 281.h;
|
||||
double width = 345.w;
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
HelpImg.asset(ConstImg.homeCardBg, height: height, width: width,fit: BoxFit.fill),
|
||||
Positioned(
|
||||
top: 20.h, bottom: 19.h, left: 22.5.w,
|
||||
width: 300.w,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildFirstDrama(),
|
||||
_buildDramaList()
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDramaList() {
|
||||
if (dramaList.length < 2) return SizedBox.shrink();
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
spacing: 5.w,
|
||||
children: List.generate(dramaList.length-1, (index) {
|
||||
return HelpImg.network(dramaList[index+1].imageUrl, width: 95.w, height: 50.h, fit: BoxFit.cover);
|
||||
})
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFirstDrama() {
|
||||
if (dramaList.isEmpty) return SizedBox.shrink();
|
||||
SkywoodDrama drama = dramaList[0];
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: CommonColor.black, width: 1),
|
||||
),
|
||||
child: HelpImg.network(drama.imageUrl, width: 300.w, height: 160.h, fit: BoxFit.cover),
|
||||
),
|
||||
Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(child: SkyText(text: drama.name,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
)),
|
||||
Row(
|
||||
children: [
|
||||
// Icon(Icons.heart_broken, size: 15, color: Colors.redAccent,),
|
||||
HelpImg.asset(ConstImg.heart, width: 18.w, height: 18.w),
|
||||
const SizedBox(width: 3),
|
||||
SkyText(text: SkyTimeTool.formatNum(drama.watchTotal),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:skywood/main/lib_file.dart';
|
||||
import 'package:skywood/pages/widgets/pixel_widgets.dart';
|
||||
|
||||
class LikePage extends StatefulWidget {
|
||||
|
||||
const LikePage({super.key});
|
||||
|
||||
@override
|
||||
@ -11,11 +12,356 @@ class LikePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MainRootState extends State<LikePage> {
|
||||
int _tabIndex = 0;
|
||||
|
||||
final List<String> _collectionTitles = const [
|
||||
'Undercover Play',
|
||||
"Don't Cross Me",
|
||||
'Two Worlds Apart',
|
||||
'Rekindled Scorn',
|
||||
'My Revenge is You',
|
||||
"Mr. Gu's Sweet Weakness",
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
body: PixelPageBackground(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(15.w, 44.h, 15.w, 0),
|
||||
child: PixelPanel(
|
||||
padding: EdgeInsets.fromLTRB(10.w, 18.h, 10.w, 22.h),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTabs(),
|
||||
SizedBox(height: 20.h),
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
child: _tabIndex == 0
|
||||
? _buildCollection()
|
||||
: _buildPlayLog(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTabs() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _LibraryTab(
|
||||
title: 'My Collection',
|
||||
selected: _tabIndex == 0,
|
||||
onTap: () => setState(() => _tabIndex = 0),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: _LibraryTab(
|
||||
title: 'Play Log',
|
||||
selected: _tabIndex == 1,
|
||||
onTap: () => setState(() => _tabIndex = 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCollection() {
|
||||
return GridView.builder(
|
||||
key: const ValueKey('collection'),
|
||||
padding: EdgeInsets.only(top: 2.h, bottom: 120.h),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 10.h,
|
||||
crossAxisSpacing: 7.w,
|
||||
childAspectRatio: 100 / 174,
|
||||
),
|
||||
itemCount: _collectionTitles.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _CollectionCard(title: _collectionTitles[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlayLog() {
|
||||
return ListView(
|
||||
key: const ValueKey('play-log'),
|
||||
padding: EdgeInsets.only(bottom: 120.h),
|
||||
children: [
|
||||
_ContinueCard(),
|
||||
SizedBox(height: 18.h),
|
||||
Text(
|
||||
'Watch History',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF1C1E1F),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
_HistoryItem(title: 'As A Mafia Boss, I Refuse To Be An Extra'),
|
||||
SizedBox(height: 10.h),
|
||||
_HistoryItem(title: 'Fatal Vengeance'),
|
||||
SizedBox(height: 10.h),
|
||||
_HistoryItem(title: "Don't Cross Me"),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LibraryTab extends StatelessWidget {
|
||||
const _LibraryTab({
|
||||
required this.title,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 45.h,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? const Color(0xFF16ED1E) : const Color(0xFFF6F6F6),
|
||||
border: Border.all(
|
||||
color: selected ? CommonColor.black : const Color(0xFFD6D6D6),
|
||||
width: 1.5.w,
|
||||
),
|
||||
boxShadow: selected
|
||||
? const [
|
||||
BoxShadow(color: Color(0xFF1C1E1F), offset: Offset(2, 2)),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: selected ? Colors.white : const Color(0xFFB2B2B2),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1,
|
||||
shadows: selected
|
||||
? const [Shadow(color: Color(0xFF1C1E1F), offset: Offset(1, 1))]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CollectionCard extends StatelessWidget {
|
||||
const _CollectionCard({required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(4.w, 8.h, 4.w, 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF4FFB6),
|
||||
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0xFF653CFA), offset: Offset(3, 3)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: List.generate(4, (index) {
|
||||
return Container(
|
||||
width: 11.w,
|
||||
height: 6.h,
|
||||
margin: EdgeInsets.only(right: 5.w),
|
||||
decoration: BoxDecoration(
|
||||
color: index == 3
|
||||
? const Color(0xFFEAEAEA)
|
||||
: const Color(0xFF96FE9B),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF1C1E1F),
|
||||
width: 0.8.w,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
const Expanded(child: PosterImage()),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF1C1E1F),
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContinueCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 140.h,
|
||||
padding: EdgeInsets.all(10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEAE6FF),
|
||||
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const PosterImage(width: 90, height: 120, borderRadius: 2),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Mogul And His Runaway Bride',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF1B1926),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18.h),
|
||||
Text(
|
||||
'Chapter 10 / 28',
|
||||
style: TextStyle(
|
||||
color: CommonColor.primacyColor,
|
||||
fontSize: 11.sp,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
_ProgressLine(value: 0.46),
|
||||
const Spacer(),
|
||||
const PixelButton(
|
||||
label: 'Continue Watch',
|
||||
width: 145,
|
||||
height: 29,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryItem extends StatelessWidget {
|
||||
const _HistoryItem({required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 100.h,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEAE6FF),
|
||||
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const PosterImage(width: 66, height: 88, borderRadius: 2),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: CommonColor.black,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'EP.40/85',
|
||||
style: TextStyle(
|
||||
color: CommonColor.primacyColor,
|
||||
fontSize: 11.sp,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
_ProgressLine(value: 0.46),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressLine extends StatelessWidget {
|
||||
const _ProgressLine({required this.value});
|
||||
|
||||
final double value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 6.h,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
border: Border.all(color: const Color(0xFF1C1E1F), width: 0.5.w),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: value,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00EE02),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Text('46%', style: TextStyle(fontSize: 10.sp, height: 1)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:skywood/main/lib_file.dart';
|
||||
import 'package:skywood/pages/widgets/pixel_widgets.dart';
|
||||
|
||||
class MinePage extends StatefulWidget {
|
||||
|
||||
const MinePage({super.key});
|
||||
|
||||
@override
|
||||
@ -11,11 +12,475 @@ class MinePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MainRootState extends State<MinePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
body: PixelPageBackground(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: SizedBox(height: 12.h)),
|
||||
SliverToBoxAdapter(child: _buildHeader()),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 18.h)),
|
||||
SliverToBoxAdapter(child: _buildPlayerCard()),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 10.h)),
|
||||
SliverToBoxAdapter(child: _buildWalletShop()),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 10.h)),
|
||||
SliverToBoxAdapter(child: _buildCheckpoint()),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 10.h)),
|
||||
SliverToBoxAdapter(child: _buildGameMenu()),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 120.h)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_DecorIcon(icon: Icons.forest, color: const Color(0xFFFF5454)),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 60.h,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF16ED1E),
|
||||
border: Border.all(color: CommonColor.black, width: 1.5.w),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0xFF1C1E1F), offset: Offset(3, 3)),
|
||||
],
|
||||
),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1,
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: Color(0xFF1C1E1F),
|
||||
offset: Offset(2, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: const [
|
||||
TextSpan(
|
||||
text: 'Profile ',
|
||||
style: TextStyle(color: Color(0xFFFFDC4C)),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'Menu',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_DecorIcon(icon: Icons.live_tv, color: const Color(0xFF8965FB)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(7, (index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 3.w),
|
||||
child: Icon(
|
||||
index < 4 ? Icons.favorite : Icons.question_mark,
|
||||
size: index < 4 ? 18.w : 14.w,
|
||||
color: index.isEven
|
||||
? const Color(0xFF1AF23A)
|
||||
: const Color(0xFF653CFA),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlayerCard() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
child: PixelPanel(
|
||||
color: CommonColor.primacyColor,
|
||||
shadowColor: CommonColor.black,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
const PixelSectionLabel(label: 'Player Card'),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 68.w,
|
||||
height: 68.w,
|
||||
padding: EdgeInsets.all(4.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE9C7FF),
|
||||
border: Border.all(color: CommonColor.black, width: 1.w),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person_pin,
|
||||
size: 42.w,
|
||||
color: const Color(0xFF653CFA),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 15.w),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Vistor',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
Text(
|
||||
'ID: 32432233341',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 10.sp,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWalletShop() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _SmallPanel(
|
||||
label: 'Treasure Wallet',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2,
|
||||
size: 70.w,
|
||||
color: const Color(0xFFFFDC4C),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_WalletValue(label: 'Coins', value: '15,890'),
|
||||
Divider(height: 8.h, color: Colors.white54),
|
||||
_WalletValue(label: 'Bonus', value: '20,000'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: _SmallPanel(
|
||||
label: 'Item Shop',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.storefront,
|
||||
size: 66.w,
|
||||
color: const Color(0xFFFF8DB8),
|
||||
),
|
||||
SizedBox(width: 7.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Get More\nCoins',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 14.h),
|
||||
const PixelButton(
|
||||
label: 'Store',
|
||||
width: 70,
|
||||
height: 20,
|
||||
color: Color(0xFFFF88CF),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCheckpoint() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
child: PixelPanel(
|
||||
color: CommonColor.primacyColor,
|
||||
padding: EdgeInsets.zero,
|
||||
shadowColor: CommonColor.black,
|
||||
child: Column(
|
||||
children: [
|
||||
const PixelSectionLabel(label: 'Daily Checkpoint'),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 10.h, 10.w, 10.h),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(7, (index) {
|
||||
return _DayBox(day: index + 1, active: index == 0);
|
||||
}),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
const PixelButton(
|
||||
label: 'Check In',
|
||||
width: 70,
|
||||
height: 22,
|
||||
color: Color(0xFFFF88CF),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGameMenu() {
|
||||
final items = const [
|
||||
_MenuItem('Log In', Icons.login),
|
||||
_MenuItem('Wallet', Icons.account_balance_wallet),
|
||||
_MenuItem('Language', Icons.public),
|
||||
_MenuItem('Feedback', Icons.mail),
|
||||
_MenuItem('Settings', Icons.settings),
|
||||
_MenuItem('About Us', Icons.info),
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
child: PixelPanel(
|
||||
color: CommonColor.primacyColor,
|
||||
padding: EdgeInsets.zero,
|
||||
shadowColor: CommonColor.black,
|
||||
child: Column(
|
||||
children: [
|
||||
const PixelSectionLabel(label: 'Game Menu'),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10.w),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
mainAxisSpacing: 8.h,
|
||||
crossAxisSpacing: 6.w,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => _MenuTile(item: items[index]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SmallPanel extends StatelessWidget {
|
||||
const _SmallPanel({required this.label, required this.child});
|
||||
|
||||
final String label;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PixelPanel(
|
||||
color: CommonColor.primacyColor,
|
||||
shadowColor: CommonColor.black,
|
||||
padding: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 134.h,
|
||||
child: Column(
|
||||
children: [
|
||||
PixelSectionLabel(label: label),
|
||||
Expanded(
|
||||
child: Padding(padding: EdgeInsets.all(8.w), child: child),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletValue extends StatelessWidget {
|
||||
const _WalletValue({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(color: Colors.white70, fontSize: 9.sp, height: 1),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DayBox extends StatelessWidget {
|
||||
const _DayBox({required this.day, required this.active});
|
||||
|
||||
final int day;
|
||||
final bool active;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 28.w,
|
||||
height: 34.h,
|
||||
decoration: BoxDecoration(
|
||||
color: active ? const Color(0xFF91F473) : Colors.white,
|
||||
border: Border.all(color: CommonColor.black, width: 1.w),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Day', style: TextStyle(fontSize: 7.sp, height: 1)),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
'$day',
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuTile extends StatelessWidget {
|
||||
const _MenuTile({required this.item});
|
||||
|
||||
final _MenuItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(4.w, 4.h, 4.w, 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF8D62F7),
|
||||
border: Border.all(color: CommonColor.black, width: 1.w),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0xFF1C1E1F), offset: Offset(2, 2)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(item.icon, color: Colors.white, size: 25.w),
|
||||
SizedBox(height: 5.h),
|
||||
Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DecorIcon extends StatelessWidget {
|
||||
const _DecorIcon({required this.icon, required this.color});
|
||||
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 50.w,
|
||||
height: 50.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: CommonColor.black, width: 1.w),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0xFF1C1E1F), offset: Offset(2, 2)),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: color, size: 32.w),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuItem {
|
||||
const _MenuItem(this.label, this.icon);
|
||||
|
||||
final String label;
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:skywood/main/lib_file.dart';
|
||||
import 'package:skywood/pages/widgets/pixel_widgets.dart';
|
||||
|
||||
class SearchPage extends StatefulWidget {
|
||||
|
||||
const SearchPage({super.key});
|
||||
|
||||
@override
|
||||
@ -11,11 +12,223 @@ class SearchPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MainRootState extends State<SearchPage> {
|
||||
final List<String> _titles = const [
|
||||
"Mr. Gu's Sweet Weakness",
|
||||
'Secret Wife with a Scar',
|
||||
'A Second Chance at Love',
|
||||
'Shadows of the Heart',
|
||||
'Boarding Pass To Love',
|
||||
'The Love Blueprint',
|
||||
'Who Dared Touch My Bride',
|
||||
'Famous Families Fled',
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: _buildSearchBar()),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 16.h)),
|
||||
SliverToBoxAdapter(child: _buildHistory()),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 30.h)),
|
||||
SliverToBoxAdapter(child: _buildTitle('Trending Now')),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 10.h)),
|
||||
SliverToBoxAdapter(child: _buildTrendingList()),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 120.h)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(15.w, 10.h, 15.w, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 34.w,
|
||||
height: 34.w,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFB6FFA7),
|
||||
border: Border.all(color: CommonColor.black, width: 1.w),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0xFF91F473), offset: Offset(2, 2)),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_back_ios_new,
|
||||
size: 16.w,
|
||||
color: CommonColor.black,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 36.h,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: CommonColor.black, width: 1.w),
|
||||
borderRadius: BorderRadius.circular(40.r),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, size: 16.w, color: CommonColor.black),
|
||||
SizedBox(width: 7.w),
|
||||
Text(
|
||||
'Search',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF3E3D40),
|
||||
fontSize: 12.sp,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistory() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTitle('Search History', fontSize: 14)),
|
||||
Icon(
|
||||
Icons.delete_outline,
|
||||
size: 18.w,
|
||||
color: const Color(0xFF888888),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x0D000000),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Text(
|
||||
'Love',
|
||||
style: TextStyle(
|
||||
color: const Color(0x80000000),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle(String title, {double fontSize = 18}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: CommonColor.black,
|
||||
fontSize: fontSize.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrendingList() {
|
||||
return SizedBox(
|
||||
height: 446.h,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
child: Wrap(
|
||||
direction: Axis.vertical,
|
||||
spacing: 10.h,
|
||||
runSpacing: 10.w,
|
||||
children: _titles
|
||||
.map((title) => _TrendingItem(title: title))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TrendingItem extends StatelessWidget {
|
||||
const _TrendingItem({required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 236.w,
|
||||
height: 104.h,
|
||||
child: Row(
|
||||
children: [
|
||||
const PosterImage(
|
||||
width: 80,
|
||||
height: 104,
|
||||
borderRadius: 4,
|
||||
showBorder: false,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: CommonColor.black,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_fire_department,
|
||||
size: 15.w,
|
||||
color: const Color(0xFFFF6A00),
|
||||
),
|
||||
SizedBox(width: 2.w),
|
||||
Text(
|
||||
'17.3K',
|
||||
style: TextStyle(
|
||||
color: const Color(0x80000000),
|
||||
fontSize: 10.sp,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
57
lib/pages/splash/splash_page.dart
Normal file
@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:skywood/main/lib_file.dart';
|
||||
|
||||
import '../../global/client/user_client.dart';
|
||||
import '../../main/main_root.dart';
|
||||
import '../../utils/tools/help_nav.dart';
|
||||
import '../../utils/tools/token_manage.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
|
||||
const SplashPage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _SplashPageState();
|
||||
}
|
||||
}
|
||||
|
||||
class _SplashPageState extends State<SplashPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_userRegister();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: HelpImg.asset(ConstImg.splashPage, fit: BoxFit.cover)),
|
||||
);
|
||||
}
|
||||
|
||||
void _userRegister() async {
|
||||
// 先拿token没有的话去注册
|
||||
final tm = TokenManager();
|
||||
String token = tm.get();
|
||||
print('tokkkkkk: $token');
|
||||
if (token.isNotEmpty) {
|
||||
// 成功后去到主页面
|
||||
print('object去主页面');
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
HelpNav.off(MainRoot());
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final res = await UserClient.register();
|
||||
if (res != null) {
|
||||
tm.save(res.token);
|
||||
HelpNav.off(MainRoot());
|
||||
}
|
||||
}
|
||||
}
|
||||
218
lib/pages/widgets/pixel_widgets.dart
Normal file
@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:skywood/main/lib_file.dart';
|
||||
|
||||
class PixelPageBackground extends StatelessWidget {
|
||||
const PixelPageBackground({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
const Positioned.fill(child: ColoredBox(color: Colors.white)),
|
||||
const Positioned.fill(child: CustomPaint(painter: _GridPainter())),
|
||||
Positioned(
|
||||
left: -18.w,
|
||||
top: 178.h,
|
||||
child: Container(
|
||||
width: 52.w,
|
||||
height: 132.h,
|
||||
color: const Color(0xFF08F214),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: -20.w,
|
||||
top: 282.h,
|
||||
child: Container(
|
||||
width: 58.w,
|
||||
height: 286.h,
|
||||
color: const Color(0xFF08F214),
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PixelPanel extends StatelessWidget {
|
||||
const PixelPanel({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.color = Colors.white,
|
||||
this.borderColor = const Color(0xFF1C1E1F),
|
||||
this.shadowColor = const Color(0xFF653CFA),
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final EdgeInsets? padding;
|
||||
final Color color;
|
||||
final Color borderColor;
|
||||
final Color shadowColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: Border.all(color: borderColor, width: 1.5.w),
|
||||
boxShadow: [
|
||||
BoxShadow(color: shadowColor, offset: Offset(4.w, 4.h)),
|
||||
BoxShadow(color: shadowColor, offset: Offset(-4.w, 4.h)),
|
||||
],
|
||||
),
|
||||
padding: padding ?? EdgeInsets.all(10.w),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PixelSectionLabel extends StatelessWidget {
|
||||
const PixelSectionLabel({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.width,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final double? width;
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 20.h,
|
||||
width: width?.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF7B54F6),
|
||||
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(width: 7.w, height: 7.w, color: const Color(0xFF34F04F)),
|
||||
SizedBox(width: 5.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing ?? const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PixelButton extends StatelessWidget {
|
||||
const PixelButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.onTap,
|
||||
this.width = 116,
|
||||
this.height = 28,
|
||||
this.color = const Color(0xFF8D62F7),
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
final double width;
|
||||
final double height;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: width.w,
|
||||
height: height.h,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
shadows: const [
|
||||
Shadow(color: Color(0xFF1C1E1F), offset: Offset(1, 1)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PosterImage extends StatelessWidget {
|
||||
const PosterImage({
|
||||
super.key,
|
||||
this.width,
|
||||
this.height,
|
||||
this.borderRadius = 3,
|
||||
this.showBorder = true,
|
||||
});
|
||||
|
||||
final double? width;
|
||||
final double? height;
|
||||
final double borderRadius;
|
||||
final bool showBorder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width?.w,
|
||||
height: height?.h,
|
||||
decoration: BoxDecoration(
|
||||
border: showBorder
|
||||
? Border.all(color: const Color(0xFF1C1E1F), width: 0.8.w)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(borderRadius.r),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: HelpImg.asset(ConstImg.homeCardBg, fit: BoxFit.cover),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridPainter extends CustomPainter {
|
||||
const _GridPainter();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final gridPaint = Paint()
|
||||
..color = const Color(0xFFE0D8FF)
|
||||
..strokeWidth = 0.8;
|
||||
const step = 10.0;
|
||||
for (double x = 0; x <= size.width; x += step) {
|
||||
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
|
||||
}
|
||||
for (double y = 0; y <= size.height; y += step) {
|
||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
|
||||
}
|
||||
|
||||
final blockPaint = Paint()..color = const Color(0xFFE0D8FF);
|
||||
canvas.drawRect(Rect.fromLTWH(80, 0, 34, 96), blockPaint);
|
||||
canvas.drawRect(Rect.fromLTWH(228, 0, 34, 32), blockPaint);
|
||||
canvas.drawRect(Rect.fromLTWH(278, 112, 34, 34), blockPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
// 扩展Widget类以添加额外的布局和样式方法
|
||||
extension WidgetExtend on Widget {
|
||||
/// 在当前Widget外层添加一个Container
|
||||
@ -43,7 +42,8 @@ extension WidgetExtend on Widget {
|
||||
padding: padding,
|
||||
alignment: alignment,
|
||||
clipBehavior: clipBehavior,
|
||||
constraints: constraints ??
|
||||
constraints:
|
||||
constraints ??
|
||||
BoxConstraints(
|
||||
minWidth: minWidth ?? 0.0,
|
||||
maxWidth: maxWidth ?? double.infinity,
|
||||
@ -59,8 +59,9 @@ extension WidgetExtend on Widget {
|
||||
image: image,
|
||||
gradient: gradient,
|
||||
border: border,
|
||||
borderRadius:
|
||||
radius != null ? BorderRadius.circular(radius) : borderRadius,
|
||||
borderRadius: radius != null
|
||||
? BorderRadius.circular(radius)
|
||||
: borderRadius,
|
||||
),
|
||||
child: this,
|
||||
).addInkWell(onTap: onTap);
|
||||
@ -84,10 +85,7 @@ extension WidgetExtend on Widget {
|
||||
///
|
||||
/// 通过此方法可以设置控件的对齐方式
|
||||
Widget addAlign({AlignmentGeometry alignment = Alignment.centerLeft}) {
|
||||
return Align(
|
||||
alignment: alignment,
|
||||
child: this,
|
||||
);
|
||||
return Align(alignment: alignment, child: this);
|
||||
}
|
||||
|
||||
Widget addCenter() {
|
||||
@ -118,20 +116,14 @@ extension WidgetExtend on Widget {
|
||||
///
|
||||
/// 通过此方法可以控制控件的显示和隐藏
|
||||
Widget addVisibility({bool visible = true}) {
|
||||
return Visibility(
|
||||
visible: visible,
|
||||
child: this,
|
||||
);
|
||||
return Visibility(visible: visible, child: this);
|
||||
}
|
||||
|
||||
/// 在当前Widget外层添加一个Transform.rotate
|
||||
///
|
||||
/// 通过此方法可以设置控件的旋转角度
|
||||
Widget addRotate({double angle = 0}) {
|
||||
return Transform.rotate(
|
||||
angle: angle,
|
||||
child: this,
|
||||
);
|
||||
return Transform.rotate(angle: angle, child: this);
|
||||
}
|
||||
|
||||
/// 在当前Widget外层添加一个InkWell
|
||||
@ -141,44 +133,27 @@ extension WidgetExtend on Widget {
|
||||
if (onTap == null) {
|
||||
return this;
|
||||
}
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: this,
|
||||
);
|
||||
return InkWell(onTap: onTap, child: this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 在当前Widget外层添加一个SizedBox
|
||||
///
|
||||
/// 通过此方法可以设置控件的宽高
|
||||
Widget addSize({double? width, double? height}) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: this,
|
||||
);
|
||||
return SizedBox(width: width, height: height, child: this);
|
||||
}
|
||||
|
||||
/// 在当前Widget外层添加一个Opacity
|
||||
///
|
||||
/// 通过此方法可以设置控件的透明度
|
||||
Widget addOpacity({double opacity = 1}) {
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: this,
|
||||
);
|
||||
return Opacity(opacity: opacity, child: this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
extension OnRenderObjectWidget on RenderObjectWidget {
|
||||
SliverPadding addPaddingSliver({EdgeInsets? padding}) {
|
||||
return SliverPadding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
sliver: this,
|
||||
);
|
||||
return SliverPadding(padding: padding ?? EdgeInsets.zero, sliver: this);
|
||||
}
|
||||
|
||||
DecoratedSliver addDecoratedSliver({
|
||||
@ -199,7 +174,7 @@ extension OnRenderObjectWidget on RenderObjectWidget {
|
||||
gradient: gradient,
|
||||
shape: shape ?? BoxShape.rectangle,
|
||||
),
|
||||
sliver: this.addPaddingSliver(padding: padding),
|
||||
sliver: addPaddingSliver(padding: padding),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
23
lib/utils/models/drama/skywood_category.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import '../model_parser.dart';
|
||||
|
||||
class SkywoodCategory {
|
||||
final int id;
|
||||
final String name;
|
||||
|
||||
const SkywoodCategory({required this.id, required this.name});
|
||||
|
||||
factory SkywoodCategory.fromJson(Map<String, dynamic> json) {
|
||||
return SkywoodCategory(
|
||||
id: parseInt(json['id']),
|
||||
name: parseString(json['name']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'id': id, 'name': name};
|
||||
}
|
||||
|
||||
SkywoodCategory copyWith({int? id, String? name}) {
|
||||
return SkywoodCategory(id: id ?? this.id, name: name ?? this.name);
|
||||
}
|
||||
}
|
||||
111
lib/utils/models/drama/skywood_detail.dart
Normal file
@ -0,0 +1,111 @@
|
||||
import '../model_parser.dart';
|
||||
import 'skywood_drama.dart';
|
||||
import 'skywood_episode.dart';
|
||||
|
||||
class SkywoodDetail {
|
||||
final SkywoodDrama? videoInfo;
|
||||
final SkywoodDrama? shortPlayInfo;
|
||||
final List<SkywoodEpisode> episodeList;
|
||||
final String? businessModel;
|
||||
bool? isCollect;
|
||||
final bool? showShareCoin;
|
||||
final int? shareCoin;
|
||||
final int? revolution;
|
||||
final String? userLevel;
|
||||
final int? jumpType;
|
||||
final int? jumpShortPlayId;
|
||||
final List<int>? checkPoint;
|
||||
|
||||
SkywoodDetail({
|
||||
required this.videoInfo,
|
||||
this.shortPlayInfo,
|
||||
required this.episodeList,
|
||||
this.businessModel,
|
||||
this.isCollect,
|
||||
this.showShareCoin,
|
||||
this.shareCoin,
|
||||
this.revolution,
|
||||
this.userLevel,
|
||||
this.jumpType,
|
||||
this.jumpShortPlayId,
|
||||
this.checkPoint,
|
||||
});
|
||||
|
||||
factory SkywoodDetail.fromJson(Map<String, dynamic> json) {
|
||||
return SkywoodDetail(
|
||||
videoInfo: parseMap(json['video_info']) == null
|
||||
? null
|
||||
: SkywoodDrama.fromJson(parseMap(json['video_info'])!),
|
||||
shortPlayInfo: parseMap(json['shortPlayInfo']) == null
|
||||
? null
|
||||
: SkywoodDrama.fromJson(parseMap(json['shortPlayInfo'])!),
|
||||
episodeList: parseModelList(json['episodeList'], SkywoodEpisode.fromJson),
|
||||
businessModel: json['business_model'] as String?,
|
||||
isCollect: json['is_collect'] == null
|
||||
? null
|
||||
: parseBool(json['is_collect']),
|
||||
showShareCoin: json['show_share_coin'] == null
|
||||
? null
|
||||
: parseBool(json['show_share_coin']),
|
||||
shareCoin: json['share_coin'] == null
|
||||
? null
|
||||
: parseInt(json['share_coin']),
|
||||
revolution: json['revolution'] == null
|
||||
? null
|
||||
: parseInt(json['revolution']),
|
||||
userLevel: json['user_level'] as String?,
|
||||
jumpType: json['jump_type'] == null ? null : parseInt(json['jump_type']),
|
||||
jumpShortPlayId: json['jump_short_play_id'] == null
|
||||
? null
|
||||
: parseInt(json['jump_short_play_id']),
|
||||
checkPoint: parseIntList(json['check_point']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'video_info': videoInfo?.toJson(),
|
||||
'shortPlayInfo': shortPlayInfo?.toJson(),
|
||||
'episodeList': episodeList.map((item) => item.toJson()).toList(),
|
||||
'business_model': businessModel,
|
||||
'is_collect': isCollect,
|
||||
'show_share_coin': showShareCoin,
|
||||
'share_coin': shareCoin,
|
||||
'revolution': revolution,
|
||||
'user_level': userLevel,
|
||||
'jump_type': jumpType,
|
||||
'jump_short_play_id': jumpShortPlayId,
|
||||
'check_point': checkPoint,
|
||||
};
|
||||
}
|
||||
|
||||
SkywoodDetail copyWith({
|
||||
SkywoodDrama? videoInfo,
|
||||
SkywoodDrama? shortPlayInfo,
|
||||
List<SkywoodEpisode>? episodeList,
|
||||
String? businessModel,
|
||||
bool? isCollect,
|
||||
bool? showShareCoin,
|
||||
int? shareCoin,
|
||||
int? revolution,
|
||||
String? userLevel,
|
||||
int? jumpType,
|
||||
int? jumpShortPlayId,
|
||||
List<int>? checkPoint,
|
||||
}) {
|
||||
return SkywoodDetail(
|
||||
videoInfo: videoInfo ?? this.videoInfo,
|
||||
shortPlayInfo: shortPlayInfo ?? this.shortPlayInfo,
|
||||
episodeList: episodeList ?? this.episodeList,
|
||||
businessModel: businessModel ?? this.businessModel,
|
||||
isCollect: isCollect ?? this.isCollect,
|
||||
showShareCoin: showShareCoin ?? this.showShareCoin,
|
||||
shareCoin: shareCoin ?? this.shareCoin,
|
||||
revolution: revolution ?? this.revolution,
|
||||
userLevel: userLevel ?? this.userLevel,
|
||||
jumpType: jumpType ?? this.jumpType,
|
||||
jumpShortPlayId: jumpShortPlayId ?? this.jumpShortPlayId,
|
||||
checkPoint: checkPoint ?? this.checkPoint,
|
||||
);
|
||||
}
|
||||
}
|
||||
183
lib/utils/models/drama/skywood_drama.dart
Normal file
@ -0,0 +1,183 @@
|
||||
import '../model_parser.dart';
|
||||
import 'skywood_category.dart';
|
||||
|
||||
class SkywoodDrama {
|
||||
final int? id;
|
||||
final int shortId;
|
||||
int? shortPlayId;
|
||||
final String name;
|
||||
final String description;
|
||||
final String imageUrl;
|
||||
final String horizontallyImg;
|
||||
int collectTotal;
|
||||
final int watchTotal;
|
||||
final int episodeTotal;
|
||||
final String? playSeconds;
|
||||
bool? isCollect;
|
||||
final String? videoUrl;
|
||||
final Map<String, dynamic>? videoInfo;
|
||||
final List<String>? category;
|
||||
final List<SkywoodCategory>? categoryList;
|
||||
final String tagType;
|
||||
int? episode;
|
||||
final int? process;
|
||||
final int? buyType;
|
||||
final int? searchClickTotal;
|
||||
final int? shortPlayVideoId;
|
||||
final int? currentEpisode;
|
||||
int? historyTime;
|
||||
|
||||
|
||||
SkywoodDrama({
|
||||
this.id,
|
||||
required this.shortId,
|
||||
this.shortPlayId,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.imageUrl,
|
||||
required this.horizontallyImg,
|
||||
required this.collectTotal,
|
||||
required this.watchTotal,
|
||||
required this.episodeTotal,
|
||||
this.playSeconds,
|
||||
this.isCollect,
|
||||
this.videoUrl,
|
||||
this.videoInfo,
|
||||
this.category,
|
||||
this.categoryList,
|
||||
required this.tagType,
|
||||
this.episode,
|
||||
this.process,
|
||||
this.buyType,
|
||||
this.searchClickTotal,
|
||||
this.shortPlayVideoId,
|
||||
this.currentEpisode,
|
||||
this.historyTime,
|
||||
});
|
||||
|
||||
factory SkywoodDrama.fromJson(Map<String, dynamic> json) {
|
||||
return SkywoodDrama(
|
||||
id: json['id'] == null ? null : parseInt(json['id']),
|
||||
shortId: parseInt(json['short_id']),
|
||||
shortPlayId: json['short_play_id'] == null
|
||||
? null
|
||||
: parseInt(json['short_play_id']),
|
||||
name: parseString(json['name']),
|
||||
description: parseString(json['description']),
|
||||
imageUrl: parseString(json['image_url']),
|
||||
horizontallyImg: parseString(json['horizontally_img']),
|
||||
collectTotal: parseInt(json['collect_total']),
|
||||
watchTotal: parseInt(json['watch_total']),
|
||||
episodeTotal: parseInt(json['episode_total']),
|
||||
playSeconds: json['play_seconds'] as String?,
|
||||
isCollect: json['is_collect'] == null
|
||||
? null
|
||||
: parseBool(json['is_collect']),
|
||||
videoUrl: json['video_url'] as String?,
|
||||
videoInfo: parseMap(json['video_info']),
|
||||
category: parseStringList(json['category']),
|
||||
categoryList: json['categoryList'] == null
|
||||
? null
|
||||
: parseModelList(json['categoryList'], SkywoodCategory.fromJson),
|
||||
tagType: parseString(json['tag_type']),
|
||||
episode: json['episode'] == null ? null : parseInt(json['episode']),
|
||||
process: json['process'] == null ? null : parseInt(json['process']),
|
||||
buyType: json['buy_type'] == null ? null : parseInt(json['buy_type']),
|
||||
searchClickTotal: json['search_click_total'] == null
|
||||
? null
|
||||
: parseInt(json['search_click_total']),
|
||||
shortPlayVideoId: json['short_play_video_id'] == null
|
||||
? null
|
||||
: parseInt(json['short_play_video_id']),
|
||||
currentEpisode: json['current_episode'] == null
|
||||
? null
|
||||
: parseInt(json['current_episode']),
|
||||
historyTime: json['history_time'] == null
|
||||
? null
|
||||
: parseInt(json['history_time']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'short_id': shortId,
|
||||
'short_play_id': shortPlayId,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'image_url': imageUrl,
|
||||
'horizontally_img': horizontallyImg,
|
||||
'collect_total': collectTotal,
|
||||
'watch_total': watchTotal,
|
||||
'episode_total': episodeTotal,
|
||||
'play_seconds': playSeconds,
|
||||
'is_collect': isCollect,
|
||||
'video_url': videoUrl,
|
||||
'video_info': videoInfo,
|
||||
'category': category,
|
||||
'categoryList': categoryList?.map((item) => item.toJson()).toList(),
|
||||
'tag_type': tagType,
|
||||
'episode': episode,
|
||||
'process': process,
|
||||
'buy_type': buyType,
|
||||
'search_click_total': searchClickTotal,
|
||||
'short_play_video_id': shortPlayVideoId,
|
||||
'current_episode': currentEpisode,
|
||||
'history_time': historyTime,
|
||||
};
|
||||
}
|
||||
|
||||
SkywoodDrama copyWith({
|
||||
int? id,
|
||||
int? shortId,
|
||||
int? shortPlayId,
|
||||
String? name,
|
||||
String? description,
|
||||
String? imageUrl,
|
||||
String? horizontallyImg,
|
||||
int? collectTotal,
|
||||
int? watchTotal,
|
||||
int? episodeTotal,
|
||||
String? playSeconds,
|
||||
bool? isCollect,
|
||||
String? videoUrl,
|
||||
Map<String, dynamic>? videoInfo,
|
||||
List<String>? category,
|
||||
List<SkywoodCategory>? categoryList,
|
||||
String? tagType,
|
||||
int? episode,
|
||||
int? process,
|
||||
int? buyType,
|
||||
int? searchClickTotal,
|
||||
int? shortPlayVideoId,
|
||||
int? currentEpisode,
|
||||
int? historyTime,
|
||||
}) {
|
||||
return SkywoodDrama(
|
||||
id: id ?? this.id,
|
||||
shortId: shortId ?? this.shortId,
|
||||
shortPlayId: shortPlayId ?? this.shortPlayId,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
horizontallyImg: horizontallyImg ?? this.horizontallyImg,
|
||||
collectTotal: collectTotal ?? this.collectTotal,
|
||||
watchTotal: watchTotal ?? this.watchTotal,
|
||||
episodeTotal: episodeTotal ?? this.episodeTotal,
|
||||
playSeconds: playSeconds ?? this.playSeconds,
|
||||
isCollect: isCollect ?? this.isCollect,
|
||||
videoUrl: videoUrl ?? this.videoUrl,
|
||||
videoInfo: videoInfo ?? this.videoInfo,
|
||||
category: category ?? this.category,
|
||||
categoryList: categoryList ?? this.categoryList,
|
||||
tagType: tagType ?? this.tagType,
|
||||
episode: episode ?? this.episode,
|
||||
process: process ?? this.process,
|
||||
buyType: buyType ?? this.buyType,
|
||||
searchClickTotal: searchClickTotal ?? this.searchClickTotal,
|
||||
shortPlayVideoId: shortPlayVideoId ?? this.shortPlayVideoId,
|
||||
currentEpisode: currentEpisode ?? this.currentEpisode,
|
||||
historyTime: historyTime ?? this.historyTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
92
lib/utils/models/drama/skywood_episode.dart
Normal file
@ -0,0 +1,92 @@
|
||||
import '../model_parser.dart';
|
||||
|
||||
class SkywoodEpisode {
|
||||
final int id;
|
||||
final int shortId;
|
||||
final int shortPlayId;
|
||||
final int shortPlayVideoId;
|
||||
final int episode;
|
||||
final String videoUrl;
|
||||
final int? promiseViewAd;
|
||||
final int iaaPopUp;
|
||||
bool isLock;
|
||||
String playSeconds;
|
||||
int? viewAdCount;
|
||||
|
||||
SkywoodEpisode({
|
||||
required this.id,
|
||||
required this.shortId,
|
||||
required this.shortPlayId,
|
||||
required this.shortPlayVideoId,
|
||||
required this.episode,
|
||||
required this.videoUrl,
|
||||
required this.playSeconds,
|
||||
required this.isLock,
|
||||
this.promiseViewAd,
|
||||
required this.iaaPopUp,
|
||||
required this.viewAdCount,
|
||||
});
|
||||
|
||||
factory SkywoodEpisode.fromJson(Map<String, dynamic> json) {
|
||||
return SkywoodEpisode(
|
||||
id: parseInt(json['id']),
|
||||
shortId: parseInt(json['short_id']),
|
||||
shortPlayId: parseInt(json['short_play_id']),
|
||||
shortPlayVideoId: parseInt(json['short_play_video_id']),
|
||||
episode: parseInt(json['episode']),
|
||||
videoUrl: parseString(json['video_url']),
|
||||
playSeconds: parseString(json['play_seconds']),
|
||||
isLock: parseBool(json['is_lock']),
|
||||
promiseViewAd: json['promise_view_ad'] == null
|
||||
? null
|
||||
: parseInt(json['promise_view_ad']),
|
||||
iaaPopUp: parseInt(json['iaa_pop_up']),
|
||||
viewAdCount: parseInt(json['view_ad_count']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'short_id': shortId,
|
||||
'short_play_id': shortPlayId,
|
||||
'short_play_video_id': shortPlayVideoId,
|
||||
'episode': episode,
|
||||
'video_url': videoUrl,
|
||||
'promise_view_ad': promiseViewAd,
|
||||
'iaa_pop_up': iaaPopUp,
|
||||
'is_lock': isLock,
|
||||
'play_seconds': playSeconds,
|
||||
'view_ad_count': viewAdCount,
|
||||
};
|
||||
}
|
||||
|
||||
SkywoodEpisode copyWith({
|
||||
int? id,
|
||||
int? shortId,
|
||||
int? shortPlayId,
|
||||
int? shortPlayVideoId,
|
||||
int? episode,
|
||||
String? videoUrl,
|
||||
bool? isLock,
|
||||
String? playSeconds,
|
||||
String? durationSeconds,
|
||||
int? promiseViewAd,
|
||||
int? viewAdCount,
|
||||
int? iaaPopUp,
|
||||
}) {
|
||||
return SkywoodEpisode(
|
||||
id: id ?? this.id,
|
||||
shortId: shortId ?? this.shortId,
|
||||
shortPlayId: shortPlayId ?? this.shortPlayId,
|
||||
shortPlayVideoId: shortPlayVideoId ?? this.shortPlayVideoId,
|
||||
episode: episode ?? this.episode,
|
||||
videoUrl: videoUrl ?? this.videoUrl,
|
||||
isLock: isLock ?? this.isLock,
|
||||
playSeconds: playSeconds ?? this.playSeconds,
|
||||
promiseViewAd: promiseViewAd ?? this.promiseViewAd,
|
||||
viewAdCount: viewAdCount ?? this.viewAdCount,
|
||||
iaaPopUp: iaaPopUp ?? this.iaaPopUp,
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/utils/models/drama/skywood_home.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'package:skywood/utils/models/drama/skywood_drama.dart';
|
||||
|
||||
class SkywoodHome {
|
||||
final String? moduleKey;
|
||||
final List<SkywoodDrama> dataList;
|
||||
|
||||
const SkywoodHome({required this.moduleKey, required this.dataList});
|
||||
|
||||
factory SkywoodHome.fromJson(Map<String, dynamic> json) {
|
||||
// Map<String, dynamic>? map = json['data'];
|
||||
// List? list = map!['list'];
|
||||
// List<SkywoodDrama> datas = [];
|
||||
// if (list != null) {
|
||||
// datas = list.map((e) => SkywoodDrama.fromJson(e)).toList();
|
||||
// }
|
||||
final data = json['data'];
|
||||
List<SkywoodDrama> videos = [];
|
||||
|
||||
// 🔥 判断 data 的类型
|
||||
if (data is Map<String, dynamic>) {
|
||||
// 情况1: data 是 Map,从中取 list
|
||||
final list = data['list'];
|
||||
if (list is List) {
|
||||
videos = list.map((e) => SkywoodDrama.fromJson(e)).toList();
|
||||
}
|
||||
} else if (data is List) {
|
||||
// 情况2: data 直接就是 List
|
||||
videos = data.map((e) => SkywoodDrama.fromJson(e)).toList();
|
||||
}
|
||||
return SkywoodHome(
|
||||
moduleKey: json['module_key'] as String?,
|
||||
dataList: videos,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'module_key': moduleKey, 'dataList': dataList};
|
||||
}
|
||||
|
||||
SkywoodHome copyWith({String? moduleKey, dynamic data}) {
|
||||
return SkywoodHome(
|
||||
moduleKey: moduleKey ?? this.moduleKey,
|
||||
dataList: data ?? dataList,
|
||||
);
|
||||
}
|
||||
}
|
||||
57
lib/utils/models/model_parser.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();
|
||||
}
|
||||
59
lib/utils/models/user/glimzo_user.dart
Normal file
@ -0,0 +1,59 @@
|
||||
import '../model_parser.dart';
|
||||
|
||||
class GlimzoUser {
|
||||
final String customerId;
|
||||
final bool isTourist;
|
||||
final String userLevel;
|
||||
final String avator;
|
||||
final String? familyName;
|
||||
final String? givingName;
|
||||
|
||||
const GlimzoUser({
|
||||
required this.customerId,
|
||||
required this.isTourist,
|
||||
required this.userLevel,
|
||||
required this.avator,
|
||||
this.familyName,
|
||||
this.givingName,
|
||||
});
|
||||
|
||||
factory GlimzoUser.fromJson(Map<String, dynamic> json) {
|
||||
return GlimzoUser(
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
GlimzoUser copyWith({
|
||||
String? customerId,
|
||||
bool? isTourist,
|
||||
String? userLevel,
|
||||
String? avator,
|
||||
String? familyName,
|
||||
String? givingName,
|
||||
}) {
|
||||
return GlimzoUser(
|
||||
customerId: customerId ?? this.customerId,
|
||||
isTourist: isTourist ?? this.isTourist,
|
||||
userLevel: userLevel ?? this.userLevel,
|
||||
avator: avator ?? this.avator,
|
||||
familyName: familyName ?? this.familyName,
|
||||
givingName: givingName ?? this.givingName,
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/utils/models/user/register_bean.dart
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
class RegisterBean {
|
||||
final String token;
|
||||
final int customerId;
|
||||
final bool? autoLogin;
|
||||
final int touristId;
|
||||
|
||||
RegisterBean({required this.token, required this.customerId, this.autoLogin, required this.touristId});
|
||||
|
||||
factory RegisterBean.fromJson(Map<String, dynamic> json) {
|
||||
return RegisterBean(
|
||||
token: json['token'],
|
||||
customerId: json['customer_id'],
|
||||
autoLogin: json['auto_login'],
|
||||
touristId: json['tourist_id'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,8 @@ import 'package:get/get.dart';
|
||||
|
||||
/// AppScreen class
|
||||
class AppScreen {
|
||||
static double get screenWidth => Get.width;
|
||||
static double get screenHeight => Get.height;
|
||||
static double get screenWidth => _viewSize.width;
|
||||
static double get screenHeight => _viewSize.height;
|
||||
// static double get screenWidth => MediaQuery.of(Get.context!).size.width;
|
||||
// static double get screenHeight => MediaQuery.of(Get.context!).size.height;
|
||||
|
||||
@ -13,7 +13,7 @@ class AppScreen {
|
||||
return MediaQuery.of(context).size;
|
||||
}
|
||||
|
||||
static bool get isDebug=> kDebugMode;
|
||||
static bool get isDebug => kDebugMode;
|
||||
|
||||
static double get statusBarHeight => Get.statusBarHeight / Get.pixelRatio;
|
||||
|
||||
@ -33,12 +33,19 @@ class AppScreen {
|
||||
return GetPlatform.isIOS;
|
||||
}
|
||||
|
||||
static Size get _viewSize {
|
||||
if (Get.context != null) {
|
||||
return MediaQuery.of(Get.context!).size;
|
||||
}
|
||||
|
||||
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
return view.physicalSize / view.devicePixelRatio;
|
||||
}
|
||||
|
||||
static double aRatioWidth({double? h, double aspectRatio = 1}) {
|
||||
h ??= screenHeight;
|
||||
return screenHeight * 1 / aspectRatio;
|
||||
}
|
||||
|
||||
|
||||
static double get keyboardHeight => Get.mediaQuery.viewInsets.bottom;
|
||||
|
||||
}
|
||||
|
||||
143
lib/utils/tools/app_toast.dart
Normal file
@ -0,0 +1,143 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
// import 'package:lottie/lottie.dart';
|
||||
|
||||
// 加载数据的函数类型
|
||||
typedef LoadBlock<T> = Future<T> Function();
|
||||
|
||||
// 弹出Toast提示的类
|
||||
class AppToast {
|
||||
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: Lottie.asset(
|
||||
// 'assets/bagua.json',
|
||||
// fit: BoxFit.contain,
|
||||
// repeat: true,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
/// 显示加载提示
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,22 @@ class ConstImg {
|
||||
static const String tabMineUnselect = "assets/common/tab_mine_unselect.png";
|
||||
static const String homeBg = "assets/imgs/home_bg.png";
|
||||
|
||||
// home_card_bg
|
||||
static const String homeCardBg = "assets/imgs/home_card_bg.png";
|
||||
// home_top_bg
|
||||
static const String homeTopBg = "assets/imgs/home_top_bg.png";
|
||||
// home_title_daily/hot/hot_cate/mystery/weekly
|
||||
static const String homeTitleDaily = "assets/imgs/home_title_daily.png";
|
||||
static const String homeTitleHot = "assets/imgs/home_title_hot.png";
|
||||
static const String homeTitleHotCate = "assets/imgs/home_title_hot_cate.png";
|
||||
static const String homeTitleMystery = "assets/imgs/home_title_mystery.png";
|
||||
static const String homeTitleWeekly = "assets/imgs/home_title_weekly.png";
|
||||
static const String homeOpenNowBox = "assets/imgs/home_opennow_box.png";
|
||||
// splash_page
|
||||
static const String splashPage = "assets/imgs/splash_page.png";
|
||||
// heart
|
||||
static const String heart = "assets/images/heart.png";
|
||||
|
||||
// nodata
|
||||
static const String noData = "assets/images/nodata.png";
|
||||
|
||||
|
||||
117
lib/utils/tools/help_nav.dart
Normal file
@ -0,0 +1,117 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// navigator
|
||||
class HelpNav {
|
||||
/// clear all route
|
||||
static Future<T?>? offAllNamed<T>(
|
||||
String newRouteName, {
|
||||
Map? arguments,
|
||||
RoutePredicate? predicate,
|
||||
String? tag,
|
||||
Map? parameters,
|
||||
}) {
|
||||
return Get.offAllNamed<T>(
|
||||
newRouteName,
|
||||
arguments: arguments,
|
||||
predicate: predicate,
|
||||
parameters: {...?parameters},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<T?>? off<T>(Widget page, {arguments, preventDuplicates}) {
|
||||
return Get.off<T>(
|
||||
() => page,
|
||||
arguments: arguments,
|
||||
preventDuplicates: preventDuplicates ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<T?>? offAll<T>(Widget page, {arguments, preventDuplicates}) {
|
||||
return Get.offAll<T>(() => page, arguments: arguments);
|
||||
}
|
||||
|
||||
static Future<T?>? offNamed<T>(
|
||||
String newRouteName, {
|
||||
arguments,
|
||||
preventDuplicates,
|
||||
}) {
|
||||
return Get.offNamed<T>(
|
||||
newRouteName,
|
||||
arguments: arguments,
|
||||
preventDuplicates: preventDuplicates ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
/// to name
|
||||
static Future<T?>? toNamed<T>(
|
||||
String newRouteName, {
|
||||
arguments,
|
||||
String? tag,
|
||||
Map? parameters,
|
||||
}) {
|
||||
return Get.toNamed<T>(
|
||||
newRouteName,
|
||||
arguments: arguments,
|
||||
parameters: {...?parameters},
|
||||
);
|
||||
}
|
||||
|
||||
/// to route
|
||||
static Future<T?>? to<T>(Widget page, {arguments, binding}) {
|
||||
return Get.to<T>(
|
||||
() => page,
|
||||
arguments: arguments,
|
||||
binding: binding,
|
||||
preventDuplicates: false,
|
||||
);
|
||||
}
|
||||
|
||||
static void _until<T>({String? routeName, T? result}) {
|
||||
if (result == null) {
|
||||
Get.back<T>(result: result);
|
||||
return;
|
||||
}
|
||||
// Get.back<T>(result: result);
|
||||
//
|
||||
if (routeName == null) {
|
||||
Get.back<T>(result: result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// back pre
|
||||
/// [result]
|
||||
static void back<T>({T? result, String? routeName}) {
|
||||
_until(routeName: routeName, result: result);
|
||||
}
|
||||
|
||||
/// show dialog
|
||||
static Future<T?> showDialog<T>(
|
||||
Widget child, {
|
||||
bool barrierDismissible = false,
|
||||
}) {
|
||||
return Get.dialog<T>(
|
||||
Center(child: child),
|
||||
barrierDismissible: barrierDismissible,
|
||||
);
|
||||
}
|
||||
|
||||
/// show bottom sheet
|
||||
static Future<T?> showBottom<T>(
|
||||
Widget child, {
|
||||
Color? safeAreaColor,
|
||||
|
||||
double? borderRadius,
|
||||
bool isDismissible = false,
|
||||
bool isScrollControlled = false,
|
||||
bool? enableDrag,
|
||||
}) {
|
||||
return Get.bottomSheet<T>(
|
||||
child,
|
||||
isScrollControlled: isScrollControlled,
|
||||
enableDrag: enableDrag ?? true,
|
||||
isDismissible: isDismissible,
|
||||
);
|
||||
}
|
||||
}
|
||||
177
lib/utils/tools/sky_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:package_info_plus/package_info_plus.dart';
|
||||
import 'package:skywood/global/request/api_const.dart';
|
||||
|
||||
@immutable
|
||||
class SkyDeviceInfo {
|
||||
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 SkyDeviceInfo? _instance;
|
||||
|
||||
const SkyDeviceInfo._({
|
||||
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 SkyDeviceInfo 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 = SkyDeviceInfo._(
|
||||
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 = ApiConst.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 = SkyDeviceInfo._(
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
53
lib/utils/tools/sky_time_tool.dart
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
class SkyTimeTool {
|
||||
|
||||
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 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) {
|
||||
final formatted = (value / 1000).toStringAsFixed(decimalDigits);
|
||||
return '${removeTrailingZeros(formatted)}K';
|
||||
} else {
|
||||
final formatted = (value / 1000000).toStringAsFixed(decimalDigits);
|
||||
return '${removeTrailingZeros(formatted)}M';
|
||||
}
|
||||
}
|
||||
}
|
||||
234
lib/utils/tools/token_manage.dart
Normal file
@ -0,0 +1,234 @@
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:skywood/global/client/user_client.dart';
|
||||
|
||||
/// Token 管理单例类
|
||||
/// 对应原项目的 GlimzoUtilToken
|
||||
class TokenManager {
|
||||
// ============ 单例模式 ============
|
||||
static final TokenManager _instance = TokenManager._internal();
|
||||
factory TokenManager() => _instance;
|
||||
TokenManager._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 UserClient.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');
|
||||
}
|
||||
}
|
||||
}
|
||||
278
lib/utils/tools/widgets/sky_common_widget.dart
Normal file
@ -0,0 +1,278 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../global/const/common_color.dart';
|
||||
import '../../extend/widget_extend.dart';
|
||||
import '../help_nav.dart';
|
||||
|
||||
|
||||
class SkyText extends StatelessWidget {
|
||||
const SkyText({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.color = Colors.white,
|
||||
this.fontSize = 14,
|
||||
this.shadow,
|
||||
this.decoration,
|
||||
this.decorationColor,
|
||||
this.textAlign,
|
||||
this.fontWeight,
|
||||
this.maxLines,
|
||||
this.lineHeight,
|
||||
this.letterSpacing,
|
||||
this.overflow = TextOverflow.ellipsis,
|
||||
this.softWrap,
|
||||
this.fontFamily,
|
||||
});
|
||||
|
||||
|
||||
final String text;
|
||||
final Color? color;
|
||||
final double? fontSize;
|
||||
final TextDecoration? decoration;
|
||||
final Color? decorationColor;
|
||||
final TextAlign? textAlign;
|
||||
final FontWeight? fontWeight;
|
||||
final int? maxLines;
|
||||
final Shadow? shadow;
|
||||
final double? lineHeight;
|
||||
final double? letterSpacing;
|
||||
final TextOverflow? overflow;
|
||||
final bool? softWrap;
|
||||
final String? fontFamily;
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final FontWeight resolvedWeight = fontWeight ?? FontWeight.normal;
|
||||
|
||||
return Text(text, style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: resolvedWeight,
|
||||
decoration: decoration,
|
||||
fontFamily: fontFamily ?? 'Inter',
|
||||
color: color,
|
||||
shadows: shadow == null ? null : [shadow!],
|
||||
decorationColor: decorationColor,
|
||||
height: lineHeight,
|
||||
letterSpacing: letterSpacing,
|
||||
), maxLines: maxLines,
|
||||
overflow: overflow,
|
||||
softWrap: softWrap,
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ComConfirmWidget extends StatelessWidget {
|
||||
|
||||
final String? title;
|
||||
final String? content;
|
||||
final Widget? contentWidget;
|
||||
final String? cancelStr, confirmStr;
|
||||
final double? height;
|
||||
final Function? onConfirmed;
|
||||
final Function? onCanceled;
|
||||
final bool? showCancel;
|
||||
final bool? clickHideAlert;
|
||||
|
||||
const ComConfirmWidget({
|
||||
super.key,
|
||||
this.title,
|
||||
this.content,
|
||||
this.contentWidget,
|
||||
this.cancelStr,
|
||||
this.confirmStr,
|
||||
this.height,
|
||||
this.onConfirmed,
|
||||
this.onCanceled,
|
||||
this.showCancel = true,
|
||||
this.clickHideAlert = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: height ?? 160,
|
||||
margin: EdgeInsets.only(left: 25, right: 25),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 15,),
|
||||
|
||||
title==null ? SizedBox() : Padding(padding: EdgeInsets.only(left:15, right: 15, bottom: 5),
|
||||
child: SkyText(text: title!, fontSize: 16, color: Colors.black,
|
||||
)
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: Padding(padding: EdgeInsets.only(left:14, right: 14),
|
||||
child: contentWidget ?? Center(
|
||||
child: SkyText(text: content!,
|
||||
fontSize: 15, color: CommonColor.black,
|
||||
maxLines: 3),
|
||||
),
|
||||
)
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
Divider(thickness: 0.6, height: 1),
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: IntrinsicHeight(
|
||||
child: showCancel! ? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
|
||||
TextButton(onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onCanceled?.call();
|
||||
},
|
||||
child: Center(
|
||||
child: SkyText(text: cancelStr ?? 'Cancel',
|
||||
color: Colors.grey, fontSize: 14))
|
||||
),
|
||||
|
||||
VerticalDivider(width: 5, thickness: 0.6,),
|
||||
|
||||
TextButton(onPressed: () {
|
||||
|
||||
if (clickHideAlert == true) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
onConfirmed?.call();
|
||||
},
|
||||
child: Center(
|
||||
child:SkyText(text: confirmStr ?? 'Confirm',
|
||||
color: CommonColor.goldColor, fontSize: 14))
|
||||
),
|
||||
],
|
||||
) : TextButton(onPressed: () {
|
||||
|
||||
Navigator.pop(context);
|
||||
onConfirmed?.call();
|
||||
},
|
||||
child: Center(
|
||||
child:SkyText(text: confirmStr ?? 'Confirm',
|
||||
color: CommonColor.black, fontSize: 14,))
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ComSheetWidget extends StatelessWidget {
|
||||
|
||||
final Function(int)? onSelected;
|
||||
final String? title;
|
||||
final List<String> items;
|
||||
final TextAlign? textAlign;
|
||||
|
||||
const ComSheetWidget({super.key,
|
||||
this.onSelected,
|
||||
this.title,
|
||||
this.textAlign = TextAlign.left,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(child: _mainView());
|
||||
}
|
||||
|
||||
_mainView() {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
height: items.length>5 ? 360 : 40+items.length*47 + 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20)
|
||||
),
|
||||
),
|
||||
child: _ifElseWidget(),
|
||||
);
|
||||
}
|
||||
|
||||
_ifElseWidget() {
|
||||
if (title != null) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
Text('$title', style: TextStyle(
|
||||
color: Colors.black, fontSize: 16
|
||||
)),
|
||||
const SizedBox(height: 5),
|
||||
const Divider(height: 8, thickness: 0.6,),
|
||||
// Divider(),
|
||||
const SizedBox(height: 5),
|
||||
Expanded(child: ListView.separated(itemBuilder: (context, index) {
|
||||
return _itemCreateer(index);
|
||||
|
||||
}, separatorBuilder: (context, index) {
|
||||
return Divider( indent: 15, endIndent: 15, thickness: 0.6,);
|
||||
}, itemCount: items.length))
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return ListView.separated(itemBuilder: (context, index) {
|
||||
return _itemCreateer(index);
|
||||
|
||||
}, separatorBuilder: (context, index) {
|
||||
return Divider(indent: 15, endIndent: 15, thickness: 0.6,);
|
||||
}, itemCount: items.length);
|
||||
}
|
||||
}
|
||||
|
||||
_itemCreateer(int index) {
|
||||
return SizedBox(
|
||||
height: 47,
|
||||
child: Center(
|
||||
child: Text(items[index], style: TextStyle(
|
||||
fontSize: 15, color: Colors.black,
|
||||
)),
|
||||
).addInkWell(
|
||||
onTap: () {
|
||||
|
||||
HelpNav.back();
|
||||
onSelected?.call(index);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
64
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:
|
||||
@ -512,6 +520,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:
|
||||
@ -904,6 +920,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.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:
|
||||
@ -1069,6 +1101,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0+1"
|
||||
talker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: talker
|
||||
sha256: f1a14d623f1d1bec42bb3bb77674eb766ffe8d26e5f79af652d85cb097c3e757
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.17"
|
||||
talker_dio_logger:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: talker_dio_logger
|
||||
sha256: "6dba5c29afb566c6efe1a2c1b676488ea7c727b486bda0654d965a1cfad6ea9b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.17"
|
||||
talker_flutter:
|
||||
dependency: "direct main"
|
||||
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:
|
||||
|
||||
@ -49,6 +49,7 @@ dependencies:
|
||||
dio: ^5.9.2
|
||||
shared_preferences: ^2.5.3
|
||||
flutter_smart_dialog: ^5.1.0
|
||||
talker_flutter: ^5.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -56,6 +57,8 @@ dev_dependencies:
|
||||
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
talker_dio_logger: ^5.0.2
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
|
||||
@ -1,31 +1,33 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:skywood/main.dart';
|
||||
import 'package:skywood/main/main_root.dart';
|
||||
import 'package:skywood/pages/home/home_page.dart';
|
||||
import 'package:skywood/utils/tools/help_img.dart';
|
||||
|
||||
import 'helpers/app_test_harness.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
testWidgets('ComApp renders the configured root tabs', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await prepareAppTestHarness();
|
||||
tester.view.physicalSize = const Size(375, 812);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
await tester.pumpWidget(const ComApp(home: MainRoot()));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(find.byType(HomePage, skipOffstage: false), findsOneWidget);
|
||||
expect(
|
||||
find.image(const AssetImage(ConstImg.tabHomeSelected)),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.image(const AssetImage(ConstImg.tabSearchUnselect)),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||