feat: 整体常用控件封装,常用工具类。首页数据对接

This commit is contained in:
chengsiwen committed 2026-06-27 14:15:25 +08:00
1 parent d90d023e85
commit fbcd33ea2b
50 files changed
+4558 -264

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

+21
View 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
View 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;
});
}
}
+6 -6
View File
@@ -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
View 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";
}
+617 -142
View File
@@ -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
View 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
View 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
View 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
View 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
);
}
+38 -22
View File
@@ -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());
}
}
+3 -1
View File
@@ -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';
+6 -13
View File
@@ -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) {
@@ -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);
}
}
}
+99 -6
View File
@@ -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),
),
);
}
}
@@ -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,
)
],
)
],
)
],
);
}
}
+349 -3
View File
@@ -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)),
],
);
}
}
+469 -4
View File
@@ -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;
}
+216 -3
View File
@@ -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
View 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
View 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;
}
+13 -38
View File
@@ -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),
);
}
}
@@ -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
View 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
View 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,
);
}
}
@@ -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
View 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
View 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
View 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
View 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'],
);
}
}
+12 -5
View File
@@ -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
View 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();
}
}
}
+16
View File
@@ -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
View 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
View 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
View 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
View 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');
}
}
}
@@ -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
View File
@@ -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:
+3
View File
@@ -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
+23 -21
View File
@@ -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,
);
});
}