diff --git a/assets/imgs/heart.png b/assets/imgs/heart.png new file mode 100644 index 0000000..df4f792 Binary files /dev/null and b/assets/imgs/heart.png differ diff --git a/assets/imgs/home_card_bg.png b/assets/imgs/home_card_bg.png new file mode 100644 index 0000000..d0465f5 Binary files /dev/null and b/assets/imgs/home_card_bg.png differ diff --git a/assets/imgs/home_opennow_box.png b/assets/imgs/home_opennow_box.png new file mode 100644 index 0000000..cfb01cd Binary files /dev/null and b/assets/imgs/home_opennow_box.png differ diff --git a/assets/imgs/home_title_daily.png b/assets/imgs/home_title_daily.png new file mode 100644 index 0000000..feaf4fa Binary files /dev/null and b/assets/imgs/home_title_daily.png differ diff --git a/assets/imgs/home_title_hot.png b/assets/imgs/home_title_hot.png new file mode 100644 index 0000000..68c3e4c Binary files /dev/null and b/assets/imgs/home_title_hot.png differ diff --git a/assets/imgs/home_title_hot_cate.png b/assets/imgs/home_title_hot_cate.png new file mode 100644 index 0000000..6417ae3 Binary files /dev/null and b/assets/imgs/home_title_hot_cate.png differ diff --git a/assets/imgs/home_title_mystery.png b/assets/imgs/home_title_mystery.png new file mode 100644 index 0000000..7bf3fb2 Binary files /dev/null and b/assets/imgs/home_title_mystery.png differ diff --git a/assets/imgs/home_title_weekly.png b/assets/imgs/home_title_weekly.png new file mode 100644 index 0000000..6007405 Binary files /dev/null and b/assets/imgs/home_title_weekly.png differ diff --git a/assets/imgs/home_top_bg.png b/assets/imgs/home_top_bg.png new file mode 100644 index 0000000..d074975 Binary files /dev/null and b/assets/imgs/home_top_bg.png differ diff --git a/assets/imgs/splash_page.png b/assets/imgs/splash_page.png new file mode 100644 index 0000000..18a9cbe Binary files /dev/null and b/assets/imgs/splash_page.png differ diff --git a/lib/global/client/play_client.dart b/lib/global/client/play_client.dart new file mode 100644 index 0000000..1856f15 --- /dev/null +++ b/lib/global/client/play_client.dart @@ -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> getHomeData() { + return AppRequest.get(ApiStringPlay.getHomeModules).then((value) { + if (value.isSuccess) { + List? list = value.data['data']['list']; + List homeList = []; + if (list != null) { + homeList = list.map((e) => SkywoodHome.fromJson(e)).toList(); + } + return homeList; + } + return []; + }); + } +} \ No newline at end of file diff --git a/lib/global/client/user_client.dart b/lib/global/client/user_client.dart new file mode 100644 index 0000000..a1712ea --- /dev/null +++ b/lib/global/client/user_client.dart @@ -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 = { + "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 register() { + return AppRequest.post(ApiString.register).then((value) { + if (value.isSuccess) { + return RegisterBean.fromJson(value.data['data']); + } + return null; + }); + } +} \ No newline at end of file diff --git a/lib/global/const/common_color.dart b/lib/global/const/common_color.dart index 32d9298..f64c019 100644 --- a/lib/global/const/common_color.dart +++ b/lib/global/const/common_color.dart @@ -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); } -} \ No newline at end of file +} diff --git a/lib/global/request/api_const.dart b/lib/global/request/api_const.dart new file mode 100644 index 0000000..cf89b7a --- /dev/null +++ b/lib/global/request/api_const.dart @@ -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"; +} + + diff --git a/lib/global/request/app_request.dart b/lib/global/request/app_request.dart index e631688..2369553 100644 --- a/lib/global/request/app_request.dart +++ b/lib/global/request/app_request.dart @@ -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 get( - String path, { - Map? queryParameters, - Options? options, - CancelToken? cancelToken, - ProgressCallback? onReceiveProgress, - }) { - return request( - path, - method: 'GET', - queryParameters: queryParameters, - options: options, - cancelToken: cancelToken, - onReceiveProgress: onReceiveProgress, - ); - } - - Future post( - String path, { - Object? data, - Map? queryParameters, - Options? options, - CancelToken? cancelToken, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) { - return request( - path, - method: 'POST', - data: data, - queryParameters: queryParameters, - options: options, - cancelToken: cancelToken, - onSendProgress: onSendProgress, - onReceiveProgress: onReceiveProgress, - ); - } - - Future put( - String path, { - Object? data, - Map? queryParameters, - Options? options, - CancelToken? cancelToken, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) { - return request( - path, - method: 'PUT', - data: data, - queryParameters: queryParameters, - options: options, - cancelToken: cancelToken, - onSendProgress: onSendProgress, - onReceiveProgress: onReceiveProgress, - ); - } - - Future delete( - String path, { - Object? data, - Map? queryParameters, - Options? options, - CancelToken? cancelToken, - }) { - return request( - path, - method: 'DELETE', - data: data, - queryParameters: queryParameters, - options: options, - cancelToken: cancelToken, - ); - } - - Future request( - String path, { - required String method, - Object? data, - Map? queryParameters, - Options? options, - CancelToken? cancelToken, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) async { - final response = await dio.request( - 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> get( + String path, { + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + int? retry, + }) async { + try { + final response = await _executeRequest( + () => _dio.get( + path, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ), + ); + return HttpResult.fromResponse(response); + } on DioException catch (e) { + return _handleError(e); + } catch (e) { + return HttpResult.error(e.toString()); + } + } + + /// POST 请求(带重试) + static Future> post( + String path, { + dynamic data, + Map? 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.fromResponse(response); + } on DioException catch (e) { + return _handleError(e); + } catch (e) { + return HttpResult.error(e.toString()); + } + } + + /// PUT 请求(带重试) + static Future> put( + String path, { + dynamic data, + Map? 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.fromResponse(response); + } on DioException catch (e) { + return _handleError(e); + } catch (e) { + return HttpResult.error(e.toString()); + } + } + + /// DELETE 请求(带重试) + static Future> delete( + String path, { + dynamic data, + Map? 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.fromResponse(response); + } on DioException catch (e) { + return _handleError(e); + } catch (e) { + return HttpResult.error(e.toString()); + } + } + + /// 文件上传(带重试) + static Future> fileUpload( + String path, { + required File file, + String fieldName = 'file', + Map? 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.fromResponse(response); + } on DioException catch (e) { + return _handleError(e); + } catch (e) { + return HttpResult.error(e.toString()); + } + } + + /// ============ 分页请求(原项目的 fetchPage) ============ + static Future> fetchPage( + String url, { + int? page, + int? pageSize, + Map? 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) { + 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((e) => fromJson(e)).toList(); + } else if (result is Map && result['list'] is List) { + return (result['list'] as List).map((e) => fromJson(e)).toList(); + } else { + throw Exception("data error"); + } + } else { + throw Exception("format error"); + } + } catch (e) { + // _handleError(); + rethrow; + } + } + + /// 直接执行请求,不重试 + static Future _executeRequest( + Future 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 _retryRequest( + Future 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 _handleError(DioException e) { + final Response? resp = e.response; + + // 尝试从响应中解析错误信息 + if (resp != null && resp.data != null) { + try { + final parsed = HttpResult.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.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 { + 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? root = _jsonMap(raw); + + // 明确失败包:{ "success": false, "error": { ... } } + if (root != null && root['success'] == false) { + final payload = ApiErrorPayload.tryParse(root); + return HttpResult( + 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( + 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( + status: httpStatus, + success: false, + message: payload.message, + errorCode: payload.code, + data: null, + ); + } + } + + return HttpResult( + status: httpStatus, + success: false, + message: _handleStatusCode(httpStatus), + data: null, + ); + } catch (e) { + return HttpResult( + status: response.statusCode, + success: false, + message: '数据解析失败: $e', + data: null, + ); + } + } + + factory HttpResult.error(String message, {int? httpStatus}) { + return HttpResult( + success: false, + status: httpStatus, + message: message, + ); + } + + factory HttpResult.networkError() { + return HttpResult( + success: false, + status: -1, + message: '网络连接失败,请检查网络后重试', + ); + } + + // ============ 私有方法 ============ + static Map? _jsonMap(dynamic value) { + if (value is Map) return value; + if (value is Map) { + try { + return value.cast(); + } catch (_) { + return Map.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? 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? _jsonMap(dynamic value) { + if (value is Map) return value; + if (value is Map) { + try { + return value.cast(); + } catch (_) { + return Map.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 { + 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 json) { + return BasePageResponse( + code: json['code'] ?? -1, + msg: json['msg'] ?? '', + data: json['data'], + ); + } +} + +/// Map 数据提取工具(原项目的 GlimzoMapRes 改造) +class MapRes { + static List getList({ + required Map mapData, + required String alias, + required T Function(Map) fromJson, + }) { + final rawList = mapData[alias]; + if (rawList is List) { + return rawList + .whereType>() + .map(fromJson) + .toList(); + } + return []; + } + + static T toModel({ + required Map json, + required T Function(Map) fromJson, + }) { + return fromJson(json); + } +} \ No newline at end of file diff --git a/lib/global/request/network_interceptor.dart b/lib/global/request/network_interceptor.dart new file mode 100644 index 0000000..3f4483e --- /dev/null +++ b/lib/global/request/network_interceptor.dart @@ -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 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 _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 _handleOldToken(RequestOptions options) async { + try { + if (options.data is Map) { + final body = options.data as Map; + 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"); + // 这里可以根据需要处理错误提示 + } +} \ No newline at end of file diff --git a/lib/global/tool/sky_encrypt.dart b/lib/global/tool/sky_encrypt.dart new file mode 100644 index 0000000..e6f5f5c --- /dev/null +++ b/lib/global/tool/sky_encrypt.dart @@ -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 copy( + Stream> reader, + StreamSink> 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(), + ); + } +} diff --git a/lib/global/tool/sky_log.dart b/lib/global/tool/sky_log.dart new file mode 100644 index 0000000..47d0c1f --- /dev/null +++ b/lib/global/tool/sky_log.dart @@ -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 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 ''; + } + } + } + + /// 错误日志走 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)]; +} diff --git a/lib/global/tool/sky_routers.dart b/lib/global/tool/sky_routers.dart new file mode 100644 index 0000000..e84f937 --- /dev/null +++ b/lib/global/tool/sky_routers.dart @@ -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 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 + ); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index ec883e3..964189b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 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()); + } } diff --git a/lib/main/lib_file.dart b/lib/main/lib_file.dart index 7255fa0..8de49a1 100644 --- a/lib/main/lib_file.dart +++ b/lib/main/lib_file.dart @@ -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'; \ No newline at end of file +export 'package:skywood/utils/tools/help_img.dart'; + +export 'package:flutter_screenutil/flutter_screenutil.dart'; diff --git a/lib/main/main_root.dart b/lib/main/main_root.dart index eb07827..093515f 100644 --- a/lib/main/main_root.dart +++ b/lib/main/main_root.dart @@ -58,17 +58,9 @@ class _MainRootState extends State { 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 { } 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) { diff --git a/lib/pages/controllers/user_controller.dart b/lib/pages/controllers/user_controller.dart new file mode 100644 index 0000000..5c4a2dc --- /dev/null +++ b/lib/pages/controllers/user_controller.dart @@ -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); + } + } +} \ No newline at end of file diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 539a0ca..882e4a7 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -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 { + List _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)); } -} \ No newline at end of file + + 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), + ), + ); + } +} diff --git a/lib/pages/home/widgets/daily_quest_widget.dart b/lib/pages/home/widgets/daily_quest_widget.dart new file mode 100644 index 0000000..b5ff244 --- /dev/null +++ b/lib/pages/home/widgets/daily_quest_widget.dart @@ -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 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, + ) + ], + ) + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/pages/like/like_page.dart b/lib/pages/like/like_page.dart index e193650..1e1bd5b 100644 --- a/lib/pages/like/like_page.dart +++ b/lib/pages/like/like_page.dart @@ -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 { + int _tabIndex = 0; + + final List _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(), + ), + ), + ], + ), + ), + ), + ), + ), ); } -} \ No newline at end of file + + 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)), + ], + ); + } +} diff --git a/lib/pages/mine/mine_page.dart b/lib/pages/mine/mine_page.dart index b6d8936..0daef54 100644 --- a/lib/pages/mine/mine_page.dart +++ b/lib/pages/mine/mine_page.dart @@ -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 { - @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)), + ], + ), + ), + ), ); } -} \ No newline at end of file + + 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; +} diff --git a/lib/pages/search/search_page.dart b/lib/pages/search/search_page.dart index 5e7d835..af6f9f9 100644 --- a/lib/pages/search/search_page.dart +++ b/lib/pages/search/search_page.dart @@ -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 { + final List _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)), + ], + ), + ), ); } -} \ No newline at end of file + + 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, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/splash/splash_page.dart b/lib/pages/splash/splash_page.dart new file mode 100644 index 0000000..9b17e5f --- /dev/null +++ b/lib/pages/splash/splash_page.dart @@ -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 createState() { + return _SplashPageState(); + } +} + +class _SplashPageState extends State { + + @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()); + } + } +} \ No newline at end of file diff --git a/lib/pages/widgets/pixel_widgets.dart b/lib/pages/widgets/pixel_widgets.dart new file mode 100644 index 0000000..ca9bb0e --- /dev/null +++ b/lib/pages/widgets/pixel_widgets.dart @@ -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; +} diff --git a/lib/utils/extend/widget_extend.dart b/lib/utils/extend/widget_extend.dart index 7c4008d..cc9e07e 100644 --- a/lib/utils/extend/widget_extend.dart +++ b/lib/utils/extend/widget_extend.dart @@ -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), ); } } diff --git a/lib/utils/models/drama/skywood_category.dart b/lib/utils/models/drama/skywood_category.dart new file mode 100644 index 0000000..d7c19b8 --- /dev/null +++ b/lib/utils/models/drama/skywood_category.dart @@ -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 json) { + return SkywoodCategory( + id: parseInt(json['id']), + name: parseString(json['name']), + ); + } + + Map toJson() { + return {'id': id, 'name': name}; + } + + SkywoodCategory copyWith({int? id, String? name}) { + return SkywoodCategory(id: id ?? this.id, name: name ?? this.name); + } +} diff --git a/lib/utils/models/drama/skywood_detail.dart b/lib/utils/models/drama/skywood_detail.dart new file mode 100644 index 0000000..f26cdd9 --- /dev/null +++ b/lib/utils/models/drama/skywood_detail.dart @@ -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 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? 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 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 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? episodeList, + String? businessModel, + bool? isCollect, + bool? showShareCoin, + int? shareCoin, + int? revolution, + String? userLevel, + int? jumpType, + int? jumpShortPlayId, + List? 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, + ); + } +} diff --git a/lib/utils/models/drama/skywood_drama.dart b/lib/utils/models/drama/skywood_drama.dart new file mode 100644 index 0000000..c4e0f36 --- /dev/null +++ b/lib/utils/models/drama/skywood_drama.dart @@ -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? videoInfo; + final List? category; + final List? 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 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 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? videoInfo, + List? category, + List? 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, + ); + } +} diff --git a/lib/utils/models/drama/skywood_episode.dart b/lib/utils/models/drama/skywood_episode.dart new file mode 100644 index 0000000..c52cdf7 --- /dev/null +++ b/lib/utils/models/drama/skywood_episode.dart @@ -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 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 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, + ); + } +} diff --git a/lib/utils/models/drama/skywood_home.dart b/lib/utils/models/drama/skywood_home.dart new file mode 100644 index 0000000..9a88133 --- /dev/null +++ b/lib/utils/models/drama/skywood_home.dart @@ -0,0 +1,46 @@ +import 'package:skywood/utils/models/drama/skywood_drama.dart'; + +class SkywoodHome { + final String? moduleKey; + final List dataList; + + const SkywoodHome({required this.moduleKey, required this.dataList}); + + factory SkywoodHome.fromJson(Map json) { + // Map? map = json['data']; + // List? list = map!['list']; + // List datas = []; + // if (list != null) { + // datas = list.map((e) => SkywoodDrama.fromJson(e)).toList(); + // } + final data = json['data']; + List videos = []; + + // 🔥 判断 data 的类型 + if (data is Map) { + // 情况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 toJson() { + return {'module_key': moduleKey, 'dataList': dataList}; + } + + SkywoodHome copyWith({String? moduleKey, dynamic data}) { + return SkywoodHome( + moduleKey: moduleKey ?? this.moduleKey, + dataList: data ?? dataList, + ); + } +} diff --git a/lib/utils/models/model_parser.dart b/lib/utils/models/model_parser.dart new file mode 100644 index 0000000..26b5946 --- /dev/null +++ b/lib/utils/models/model_parser.dart @@ -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? parseStringList(dynamic value) { + if (value == null) return null; + if (value is List) { + return value.map((item) => parseString(item)).toList(); + } + return null; +} + +List? parseIntList(dynamic value) { + if (value == null) return null; + if (value is List) { + return value.map((item) => parseInt(item)).toList(); + } + return null; +} + +Map? parseMap(dynamic value) { + if (value is Map) return value; + if (value is Map) return Map.from(value); + return null; +} + +List parseModelList( + dynamic value, + T Function(Map json) fromJson, +) { + if (value is! List) return []; + return value + .whereType() + .map((item) => fromJson(Map.from(item))) + .toList(); +} diff --git a/lib/utils/models/user/glimzo_user.dart b/lib/utils/models/user/glimzo_user.dart new file mode 100644 index 0000000..4b72edc --- /dev/null +++ b/lib/utils/models/user/glimzo_user.dart @@ -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 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 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, + ); + } +} diff --git a/lib/utils/models/user/register_bean.dart b/lib/utils/models/user/register_bean.dart new file mode 100644 index 0000000..22e2c93 --- /dev/null +++ b/lib/utils/models/user/register_bean.dart @@ -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 json) { + return RegisterBean( + token: json['token'], + customerId: json['customer_id'], + autoLogin: json['auto_login'], + touristId: json['tourist_id'], + ); + } +} \ No newline at end of file diff --git a/lib/utils/tools/app_screen.dart b/lib/utils/tools/app_screen.dart index dd9e5e6..fdd62d5 100644 --- a/lib/utils/tools/app_screen.dart +++ b/lib/utils/tools/app_screen.dart @@ -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; - } diff --git a/lib/utils/tools/app_toast.dart b/lib/utils/tools/app_toast.dart new file mode 100644 index 0000000..09547c6 --- /dev/null +++ b/lib/utils/tools/app_toast.dart @@ -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 = Future Function(); + +// 弹出Toast提示的类 +class AppToast { + static const String _defaultLoadingText = ""; + static const String _loadingText = "loading..."; + + /// 显示普通 Toast 提示 + static Future 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 showError( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + }) { + return showFail(msg, displayTime: displayTime); + } + + /// 显示失败提示 + static Future 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 showSuccess( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + }) async { + await dismiss(); + return SmartDialog.showNotify( + msg: msg.toString(), + notifyType: NotifyType.success, + debounce: true, + displayTime: displayTime, + ); + } + + static Future showInfo( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + }) { + return showWarning(msg, displayTime: displayTime); + } + + static Future showWarning( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + }) async { + await dismiss(); + return SmartDialog.showNotify( + msg: msg.toString(), + notifyType: NotifyType.warning, + debounce: true, + displayTime: displayTime, + ); + } + + /// 显示 Lottie 加载提示 + // static Future showLottieLoading({ + // Duration? displayTime, + // }) async { + // await dismiss(); + // return SmartDialog.showLoading( + // 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 showLoading({ + String text = _loadingText, + Duration displayTime = const Duration(seconds: 10), + }) async { + await dismiss(); + return SmartDialog.showLoading(msg: text, displayTime: displayTime); + } + + /// 关闭特定状态的弹窗 + static Future dismiss({SmartStatus status = SmartStatus.loading}) { + return SmartDialog.dismiss(status: status); + } + + /// 关闭所有弹窗(占位) + static void dismissAll() { + SmartDialog.dismiss(); + } + + /// 自动显示加载框并在完成后关闭 + static Future loading( + LoadBlock block, { + String? text, + bool isLoading = true, + }) async { + if (isLoading) { + showLoading(text: text ?? _defaultLoadingText); + } + + try { + final result = await block(); + return result; + } finally { + dismiss(); + } + } +} diff --git a/lib/utils/tools/help_img.dart b/lib/utils/tools/help_img.dart index 4f0846c..8936843 100644 --- a/lib/utils/tools/help_img.dart +++ b/lib/utils/tools/help_img.dart @@ -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"; diff --git a/lib/utils/tools/help_nav.dart b/lib/utils/tools/help_nav.dart new file mode 100644 index 0000000..85e8146 --- /dev/null +++ b/lib/utils/tools/help_nav.dart @@ -0,0 +1,117 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; + +/// navigator +class HelpNav { + /// clear all route + static Future? offAllNamed( + String newRouteName, { + Map? arguments, + RoutePredicate? predicate, + String? tag, + Map? parameters, + }) { + return Get.offAllNamed( + newRouteName, + arguments: arguments, + predicate: predicate, + parameters: {...?parameters}, + ); + } + + static Future? off(Widget page, {arguments, preventDuplicates}) { + return Get.off( + () => page, + arguments: arguments, + preventDuplicates: preventDuplicates ?? false, + ); + } + + static Future? offAll(Widget page, {arguments, preventDuplicates}) { + return Get.offAll(() => page, arguments: arguments); + } + + static Future? offNamed( + String newRouteName, { + arguments, + preventDuplicates, + }) { + return Get.offNamed( + newRouteName, + arguments: arguments, + preventDuplicates: preventDuplicates ?? true, + ); + } + + /// to name + static Future? toNamed( + String newRouteName, { + arguments, + String? tag, + Map? parameters, + }) { + return Get.toNamed( + newRouteName, + arguments: arguments, + parameters: {...?parameters}, + ); + } + + /// to route + static Future? to(Widget page, {arguments, binding}) { + return Get.to( + () => page, + arguments: arguments, + binding: binding, + preventDuplicates: false, + ); + } + + static void _until({String? routeName, T? result}) { + if (result == null) { + Get.back(result: result); + return; + } + // Get.back(result: result); + // + if (routeName == null) { + Get.back(result: result); + return; + } + } + + /// back pre + /// [result] + static void back({T? result, String? routeName}) { + _until(routeName: routeName, result: result); + } + + /// show dialog + static Future showDialog( + Widget child, { + bool barrierDismissible = false, + }) { + return Get.dialog( + Center(child: child), + barrierDismissible: barrierDismissible, + ); + } + + /// show bottom sheet + static Future showBottom( + Widget child, { + Color? safeAreaColor, + + double? borderRadius, + bool isDismissible = false, + bool isScrollControlled = false, + bool? enableDrag, + }) { + return Get.bottomSheet( + child, + isScrollControlled: isScrollControlled, + enableDrag: enableDrag ?? true, + isDismissible: isDismissible, + ); + } +} diff --git a/lib/utils/tools/sky_device_info.dart b/lib/utils/tools/sky_device_info.dart new file mode 100644 index 0000000..f09d35f --- /dev/null +++ b/lib/utils/tools/sky_device_info.dart @@ -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 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> _initAppInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); + return { + 'packageName': packageInfo.packageName, + 'version': packageInfo.version, + }; + } + + /// 获取 Android 设备信息 + static Future> _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> _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, + }; + } +} diff --git a/lib/utils/tools/sky_time_tool.dart b/lib/utils/tools/sky_time_tool.dart new file mode 100644 index 0000000..a93e7e5 --- /dev/null +++ b/lib/utils/tools/sky_time_tool.dart @@ -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'; + } + } +} \ No newline at end of file diff --git a/lib/utils/tools/token_manage.dart b/lib/utils/tools/token_manage.dart new file mode 100644 index 0000000..ef3bacf --- /dev/null +++ b/lib/utils/tools/token_manage.dart @@ -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? _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 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 userInfo) { + _cachedUserInfo = userInfo; + _saveToStorage(_keyUserInfo, jsonEncode(userInfo)); + } + + /// 获取用户信息 + Map? getUserInfo() { + if (_cachedUserInfo != null) { + return _cachedUserInfo; + } + final stored = _getFromStorage(_keyUserInfo); + if (stored != null && stored.isNotEmpty) { + try { + _cachedUserInfo = jsonDecode(stored) as Map; + 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 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; + } catch (_) { + _cachedUserInfo = null; + } + } + } catch (e) { + debugPrint('[TokenManager] Init error: $e'); + } + } + + /// 异步获取 Token + Future 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 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 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 _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 _removeFromStorageAsync(String key) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(key); + } catch (e) { + debugPrint('[TokenManager] Remove from storage error: $e'); + } + } +} \ No newline at end of file diff --git a/lib/utils/tools/widgets/sky_common_widget.dart b/lib/utils/tools/widgets/sky_common_widget.dart new file mode 100644 index 0000000..847bcbe --- /dev/null +++ b/lib/utils/tools/widgets/sky_common_widget.dart @@ -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 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); + }) + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index aeabfe8..1ab9f11 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" args: dependency: transitive description: @@ -512,6 +520,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + group_button: + dependency: transitive + description: + name: group_button + sha256: "0610fcf28ed122bfb4b410fce161a390f7f2531d55d1d65c5375982001415940" + url: "https://pub.dev" + source: hosted + version: "5.3.4" hooks: dependency: transitive description: @@ -904,6 +920,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + share_plus: + dependency: transitive + description: + name: share_plus + sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa" + url: "https://pub.dev" + source: hosted + version: "12.0.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" shared_preferences: dependency: "direct main" description: @@ -1069,6 +1101,38 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.0+1" + talker: + dependency: transitive + description: + name: talker + sha256: f1a14d623f1d1bec42bb3bb77674eb766ffe8d26e5f79af652d85cb097c3e757 + url: "https://pub.dev" + source: hosted + version: "5.1.17" + talker_dio_logger: + dependency: "direct dev" + description: + name: talker_dio_logger + sha256: "6dba5c29afb566c6efe1a2c1b676488ea7c727b486bda0654d965a1cfad6ea9b" + url: "https://pub.dev" + source: hosted + version: "5.1.17" + talker_flutter: + dependency: "direct main" + description: + name: talker_flutter + sha256: "54cbbf852101721664faf4a05639fd2fdefdc37178327990abea00390690d4bc" + url: "https://pub.dev" + source: hosted + version: "5.1.16" + talker_logger: + dependency: transitive + description: + name: talker_logger + sha256: "459205c3e571f97ecc6be6e1b1b7e6b97b853e78ea458894650be407596e3216" + url: "https://pub.dev" + source: hosted + version: "5.1.17" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4c3d390..51e0a05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/widget_test.dart b/test/widget_test.dart index eaeb005..e5fbc68 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -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, + ); }); }