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

This commit is contained in:
csw 2026-06-27 14:15:25 +08:00
parent d90d023e85
commit fbcd33ea2b
50 changed files with 4558 additions and 264 deletions

BIN
assets/imgs/heart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
assets/imgs/home_top_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/imgs/splash_page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

View File

@ -0,0 +1,21 @@
import 'package:skywood/global/request/api_const.dart';
import 'package:skywood/global/request/app_request.dart';
import 'package:skywood/utils/models/drama/skywood_home.dart';
class PlayClient {
static Future<List<SkywoodHome>> getHomeData() {
return AppRequest.get(ApiStringPlay.getHomeModules).then((value) {
if (value.isSuccess) {
List? list = value.data['data']['list'];
List<SkywoodHome> homeList = [];
if (list != null) {
homeList = list.map((e) => SkywoodHome.fromJson(e)).toList();
}
return homeList;
}
return [];
});
}
}

View File

@ -0,0 +1,30 @@
import 'package:skywood/global/request/api_const.dart';
import 'package:skywood/global/request/app_request.dart';
import 'package:skywood/global/tool/sky_log.dart';
import 'package:skywood/utils/models/user/register_bean.dart';
class UserClient {
static Future login() {
final body = <String, dynamic>{
"avator": '',
"email": '123456@qq.com',
"family_name": 'test1',
"platform": 'android',
"third_id": '32434',
};
return AppRequest.post(ApiString.login, data: body).then((value) {
SkyLog.print(value.data, tag: 'loginnnn');
});
}
static Future<RegisterBean?> register() {
return AppRequest.post(ApiString.register).then((value) {
if (value.isSuccess) {
return RegisterBean.fromJson(value.data['data']);
}
return null;
});
}
}

View File

@ -1,10 +1,11 @@
import 'dart:ui';
class CommonColor {
// #653CFA
static const Color primacyColor = Color(0xFF653CFA);
static const Color black = Color(0xFF030510);
// static const Color black = Color(0xFF030510);
// 040408
static const Color black = Color(0xFF040408);
// grey
static const Color grey = Color(0xFF8e8e93);
static const Color goldColor = Color(0xFFc5a059);
@ -12,7 +13,6 @@ class CommonColor {
static const Color goldDark = Color(0xFF92400e);
static const Color secondaryColor = Color(0xFF9ca3af);
static const Color fireRed = Color(0xFFf01d1d);
static const Color woodGreen = Color(0xFF56a324);
static const Color earthBrown = Color(0xFFcb8a46);
@ -28,9 +28,9 @@ class CommonColor {
static const Color diamondColor = Color(0xFFB9F2FF);
static Color withValue(String value) {
String _v = value.replaceAll('#', '');
String res = '0xff$_v';
String colorValue = value.replaceAll('#', '');
String res = '0xff$colorValue';
int valueInt = int.parse(res);
return Color(valueInt);
}
}
}

View File

@ -0,0 +1,99 @@
final class ApiConst {
ApiConst._();
static const appName = 'skywood';
static const int pageSize = 12;
static const String apiLink = 'https://api-quickeltv.quickeltv.com/quickeltv';
static String webPrefix = "quickeltv.com";
static String webIndex = "https://$webPrefix";
static String webPrivacy = "https://$webPrefix/private";
static String webAgreement = "https://$webPrefix/user_policy";
static String webmemberAgreement = "https://$webPrefix/member_ship_agreement";
static String webHelpIndex = "https://campaign.$webPrefix/pages/leave/index";
static String webHelpList = "https://campaign.$webPrefix/pages/leave/list";
static String webHelpDetail =
"https://campaign.$webPrefix/pages/leave/detail";
static String iosDownloadUrl = "";
static String androidDownloadUrl = "";
}
class ApiString {
static const String login = "/customer/login";
// /customer/register
static const String register = "/customer/register";
// /customer/info
static const String userInfo = "/customer/info";
// /customer/signout
static const String signOut = "/customer/signout";
// /customer/logoff
static const String logOff = "/customer/logoff";
/// events
// eventCreateHistory POST /createHistory
// eventUserEnterApp POST /customer/enterTheApp App
// eventUserOnline POST /customer/onLine 线 prevToken
// eventUserLeaveApp POST /customer/leaveApp App prevToken
// eventPlaySeconds POST /uploadHistorySeconds
// eventWatchTime POST /watchShortDuration
// eventReport POST /event/add force_update
static const String eventCreateHistory = "/createHistory";
static const String eventUserEnterApp = "/customer/enterTheApp";
static const String eventUserOnline = "/customer/onLine";
static const String eventUserLeaveApp = "/customer/leaveApp";
static const String eventPlaySeconds = "/uploadHistorySeconds";
static const String eventWatchTime = "/watchShortDuration";
static const String eventReport = "/event/add";
}
class ApiStringPlay {
// getRecommandsList GET /getRecommands current_page/page_size revolution
// getVideoDetail GET /getVideoDetails query: short_play_id, video_id
// getDetailsRecommand GET /getDetailsRecommand page/pageSize/data
static const String getRecommandsList = "/getRecommands";
static const String getVideoDetail = "/getVideoDetails";
static const String getDetailsRecommand = "/getDetailsRecommand";
// getHomeModules GET /home/all-modules
// getRankingList POST /homeRanking body type most_trending/top_searched/new_releases
// getCategoriesList GET /getCategories
// getCategoriesDetail GET /videoList / + extraParams
// getWaterflowList POST /newShortPlay + extraParams
static const String getHomeModules = "/home/all-modules";
static const String getRankingList = "/homeRanking";
static const String getCategoriesList = "/getCategories";
static const String getCategoriesDetail = "/videoList";
static const String getWaterflowList = "/newShortPlay";
// getSearchHots GET /search/hots
// getSearch GET /search query: search
// collect POST /collect body: short_play_id, video_id
// cancelCollect POST /cancelCollect body: short_play_id
// getMyCollections GET /myCollections
// getMyHistorys GET /myHistorys
// getNoticeNum POST /noticeNum
// versionControl GET /customer/versionControl
// getLanguges GET /languges languges
// getTranslates GET /translates query: lang_key en
static const String getSearchHots = "/search/hots";
static const String getSearch = "/search";
static const String collect = "/collect";
static const String cancelCollect = "/cancelCollect";
static const String getMyCollections = "/myCollections";
static const String getMyHistorys = "/myHistorys";
static const String getNoticeNum = "/noticeNum";
static const String versionControl = "/customer/versionControl";
static const String getLanguges = "/languges";
static const String getTranslates = "/translates";
}

View File

@ -1,154 +1,629 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:skywood/global/request/api_const.dart';
import 'package:skywood/global/request/network_interceptor.dart';
import 'package:skywood/utils/tools/app_toast.dart';
import 'package:talker_dio_logger/talker_dio_logger.dart';
class AppRequest {
import '../../utils/tools/token_manage.dart';
static const Duration defaultTimeout = Duration(seconds: 15);
static final AppRequest instance = AppRequest();
final Dio dio;
/// HTTP
class AppService {
static AppService? _instance;
late Dio _dio;
AppRequest({
Dio? dio,
String baseUrl = '',
Duration connectTimeout = defaultTimeout,
Duration sendTimeout = defaultTimeout,
Duration receiveTimeout = defaultTimeout,
}) : dio =
dio ??
Dio(
BaseOptions(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
sendTimeout: sendTimeout,
receiveTimeout: receiveTimeout,
responseType: ResponseType.json,
),
);
void setBaseUrl(String baseUrl) {
dio.options.baseUrl = baseUrl;
static AppService get instance {
_instance ??= AppService._internal();
return _instance!;
}
void setHeader(String key, Object? value) {
dio.options.headers[key] = value;
AppService._internal() {
_initDio();
}
void removeHeader(String key) {
dio.options.headers.remove(key);
Dio get dio => _dio;
/// Dio
void _initDio() {
_dio = Dio(BaseOptions(
baseUrl: ApiConst.apiLink,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
sendTimeout: const Duration(seconds: 15),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
responseType: ResponseType.json,
));
//
_dio.interceptors.add(NetworkInterceptor());
//
// if (!kReleaseMode) {
// _dio.interceptors.add(TalkerDioLogger(
// settings: TalkerDioLoggerSettings(
// printRequestHeaders: true,
// printRequestData: true,
// printResponseData: true,
// printResponseHeaders: false,
// ),
// ));
// }
}
void setToken(String token, {String prefix = 'Bearer'}) {
final value = prefix.isEmpty ? token : '$prefix $token';
setHeader('Authorization', value);
}
void clearToken() {
removeHeader('Authorization');
}
Future<T?> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) {
return request<T>(
path,
method: 'GET',
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
}
Future<T?> post<T>(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) {
return request<T>(
path,
method: 'POST',
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
Future<T?> put<T>(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) {
return request<T>(
path,
method: 'PUT',
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
Future<T?> delete<T>(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) {
return request<T>(
path,
method: 'DELETE',
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
Future<T?> request<T>(
String path, {
required String method,
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final response = await dio.request<T>(
path,
data: data,
queryParameters: queryParameters,
options: _withMethod(options, method),
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
return response.data;
}
Options _withMethod(Options? options, String method) {
return options == null
? Options(method: method)
: options.copyWith(method: method);
///
void updateBaseOptions(BaseOptions options) {
_dio.options = options;
}
}
/// ============ ============
class AppRequest {
static final Dio _dio = AppService.instance.dio;
/// GET
static Future<HttpResult<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
int? retry,
}) async {
try {
final response = await _executeRequest(
() => _dio.get(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
),
);
return HttpResult<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
/// POST
static Future<HttpResult<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
int? retry,
}) async {
try {
final response = await _executeRequest(
() => _dio.post(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
),
);
return HttpResult<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
/// PUT
static Future<HttpResult<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
int? retry,
}) async {
try {
final response = await _executeRequest(
() => _dio.put(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
),
);
return HttpResult<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
/// DELETE
static Future<HttpResult<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
int? retry,
}) async {
try {
final response = await _executeRequest(
// retries: retry,
() => _dio.delete(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
),
);
return HttpResult<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
///
static Future<HttpResult<T>> fileUpload<T>(
String path, {
required File file,
String fieldName = 'file',
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
int? retry,
}) async {
try {
final formData = FormData.fromMap({
fieldName: await MultipartFile.fromFile(file.path),
});
final response = await _executeRequest(
() => _dio.post(
path,
data: formData,
queryParameters: queryParameters,
options: options ?? Options(contentType: 'multipart/form-data'),
cancelToken: cancelToken,
onSendProgress: onSendProgress,
),
);
return HttpResult<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
/// ============ fetchPage ============
static Future<List<T>> fetchPage<T>(
String url, {
int? page,
int? pageSize,
Map<String, dynamic>? extraParams,
required T Function(dynamic json) fromJson,
bool error = true,
String method = 'get',
int? retry,
}) async {
try {
final params = {
'current_page': page?.toString() ?? '1',
'page_size': pageSize?.toString() ?? '20',
...?extraParams,
};
Response response;
if (method.toLowerCase() == 'get') {
response = await _executeRequest(
() => _dio.get(url, queryParameters: params),
);
} else {
response = await _executeRequest(
() => _dio.post(url, data: params),
);
}
if (response.statusCode != 200) {
throw Exception("HTTP ${response.statusCode}");
}
final data = response.data;
if (data is Map<String, dynamic>) {
final base = BasePageResponse.fromJson(data);
if (!base.success) {
if (error) {
// Toast
AppToast.showError(base.msg);
}
throw Exception(base.msg);
}
final result = base.data;
if (result is List) {
return result.map<T>((e) => fromJson(e)).toList();
} else if (result is Map && result['list'] is List) {
return (result['list'] as List).map<T>((e) => fromJson(e)).toList();
} else {
throw Exception("data error");
}
} else {
throw Exception("format error");
}
} catch (e) {
// _handleError();
rethrow;
}
}
///
static Future<Response> _executeRequest(
Future<Response> Function() requestFn,
) async {
try {
final response = await requestFn();
// 200
if (response.statusCode != 200) {
throw DioException(
requestOptions: response.requestOptions,
response: response,
type: DioExceptionType.badResponse,
error: 'HTTP ${response.statusCode}',
);
}
return response;
} catch (e) {
//
rethrow;
}
}
// ============ ============
static Future<Response> _retryRequest(
Future<Response> Function() requestFn, {
int? retries,
}) async {
int attempt = 0;
final maxRetries = retries ?? 2;
while (attempt <= maxRetries) {
try {
final response = await requestFn();
if (response.statusCode == 200) {
return response;
}
// 401/402
if (response.statusCode == 401 || response.statusCode == 402) {
if (attempt == maxRetries) {
throw DioException(
requestOptions: response.requestOptions,
response: response,
type: DioExceptionType.badResponse,
error: 'errorCode:${response.statusCode}',
);
}
}
if (attempt == maxRetries) {
throw DioException(
requestOptions: response.requestOptions,
response: response,
type: DioExceptionType.badResponse,
error: 'HTTP ${response.statusCode}',
);
}
} catch (e) {
if (attempt == maxRetries) {
rethrow;
}
}
attempt++;
await Future.delayed(const Duration(milliseconds: 300));
}
throw Exception("retry failed (unreachable)");
}
// ============ ============
static HttpResult<T> _handleError<T>(DioException e) {
final Response? resp = e.response;
//
if (resp != null && resp.data != null) {
try {
final parsed = HttpResult<T>.fromResponse(resp);
if (!parsed.success) {
// 401/402
if (resp.statusCode == 401 || resp.statusCode == 402) {
TokenManager().update();
if (resp.statusCode == 402) {
AppToast.showError('账号已在其他设备登录,请重新登录');
}
}
return parsed;
}
} catch (_) {}
}
String message = _getErrorMessage(e);
return HttpResult<T>.error(message, httpStatus: resp?.statusCode);
}
static String _getErrorMessage(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return '连接超时,请稍后重试';
case DioExceptionType.sendTimeout:
return '发送超时,请稍后重试';
case DioExceptionType.receiveTimeout:
return '接收超时,请稍后重试';
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
if (statusCode == 401) {
TokenManager().update();
return '未授权,请重新登录';
}
if (statusCode == 402) {
TokenManager().update();
return '账号已在其他设备登录,请重新登录';
}
return _handleStatusCode(statusCode);
case DioExceptionType.cancel:
return '请求已取消';
case DioExceptionType.connectionError:
return '网络连接失败,请检查网络设置';
default:
return '网络错误,请稍后重试';
}
}
static String _handleStatusCode(int? statusCode) {
switch (statusCode) {
case 400:
return '请求错误';
case 403:
return '拒绝访问';
case 404:
return '请求资源不存在';
case 500:
return '服务器内部错误';
case 502:
return '网关错误';
case 503:
return '服务不可用';
default:
return '请求失败';
}
}
}
/// [isSuccess]
class HttpResult<T> {
final int? status;
final bool success;
final String? message;
final T? data;
final int? errorCode;
const HttpResult({
this.status,
this.success = false,
this.message,
this.data,
this.errorCode,
});
bool get isSuccess => success;
factory HttpResult.fromResponse(Response response) {
try {
final int httpStatus = response.statusCode ?? 0;
final dynamic raw = response.data;
final Map<String, dynamic>? root = _jsonMap(raw);
// { "success": false, "error": { ... } }
if (root != null && root['success'] == false) {
final payload = ApiErrorPayload.tryParse(root);
return HttpResult<T>(
status: httpStatus,
success: false,
message: payload?.message ?? root['message']?.toString() ?? '请求失败',
errorCode: payload?.code,
data: null,
);
}
// HTTP 2xxbody
if (httpStatus >= 200 && httpStatus < 300) {
return HttpResult<T>(
status: httpStatus,
success: true,
message: null,
data: raw as T?,
errorCode: null,
);
}
// 2xx body error
if (root != null) {
final payload = ApiErrorPayload.tryParse(root);
if (payload != null) {
return HttpResult<T>(
status: httpStatus,
success: false,
message: payload.message,
errorCode: payload.code,
data: null,
);
}
}
return HttpResult<T>(
status: httpStatus,
success: false,
message: _handleStatusCode(httpStatus),
data: null,
);
} catch (e) {
return HttpResult<T>(
status: response.statusCode,
success: false,
message: '数据解析失败: $e',
data: null,
);
}
}
factory HttpResult.error(String message, {int? httpStatus}) {
return HttpResult<T>(
success: false,
status: httpStatus,
message: message,
);
}
factory HttpResult.networkError() {
return HttpResult<T>(
success: false,
status: -1,
message: '网络连接失败,请检查网络后重试',
);
}
// ============ ============
static Map<String, dynamic>? _jsonMap(dynamic value) {
if (value is Map<String, dynamic>) return value;
if (value is Map) {
try {
return value.cast<String, dynamic>();
} catch (_) {
return Map<String, dynamic>.from(value as Map);
}
}
return null;
}
static String _handleStatusCode(int? statusCode) {
switch (statusCode) {
case 400:
return '请求错误';
case 401:
return '未授权,请重新登录';
case 403:
return '拒绝访问';
case 404:
return '请求资源不存在';
case 500:
return '服务器内部错误';
case 502:
return '网关错误';
case 503:
return '服务不可用';
default:
return '请求失败';
}
}
}
/// `{ "error": { "code", "details", "message" }, "success": false }`
class ApiErrorPayload {
final int? code;
final dynamic details;
final String? message;
const ApiErrorPayload({this.code, this.details, this.message});
static ApiErrorPayload? tryParse(Map<String, dynamic>? root) {
if (root == null) return null;
final err = root['error'];
if (err is! Map) return null;
final m = _jsonMap(err);
if (m == null) return null;
return ApiErrorPayload(
code: _parseInt(m['code']),
details: m['details'],
message: m['message']?.toString(),
);
}
static Map<String, dynamic>? _jsonMap(dynamic value) {
if (value is Map<String, dynamic>) return value;
if (value is Map) {
try {
return value.cast<String, dynamic>();
} catch (_) {
return Map<String, dynamic>.from(value as Map);
}
}
return null;
}
static int? _parseInt(dynamic v) {
if (v == null) return null;
if (v is int) return v;
if (v is num) return v.toInt();
return int.tryParse(v.toString());
}
}
/// GlimzoGetxBaseResponse
class BasePageResponse<T> {
final int code;
final String msg;
final T? data;
BasePageResponse({
required this.code,
required this.msg,
this.data,
});
bool get success => code == 200;
factory BasePageResponse.fromJson(Map<String, dynamic> json) {
return BasePageResponse(
code: json['code'] ?? -1,
msg: json['msg'] ?? '',
data: json['data'],
);
}
}
/// Map GlimzoMapRes
class MapRes {
static List<T> getList<T>({
required Map<dynamic, dynamic> mapData,
required String alias,
required T Function(Map<String, dynamic>) fromJson,
}) {
final rawList = mapData[alias];
if (rawList is List) {
return rawList
.whereType<Map<String, dynamic>>()
.map(fromJson)
.toList();
}
return <T>[];
}
static T toModel<T>({
required Map<String, dynamic> json,
required T Function(Map<String, dynamic>) fromJson,
}) {
return fromJson(json);
}
}

View File

@ -0,0 +1,143 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:skywood/global/request/api_const.dart';
import 'package:skywood/utils/tools/sky_device_info.dart';
import 'package:skywood/utils/tools/sky_time_tool.dart';
import 'package:skywood/utils/tools/app_sp.dart';
import 'package:logger/logger.dart';
import '../../utils/tools/token_manage.dart';
import '../tool/sky_encrypt.dart';
///
class NetworkInterceptor extends Interceptor {
final bool enableLog = !kReleaseMode;
final Logger _logger = Logger();
@override
Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
//
await _addRequestHeaders(options);
//
_logInfo("🔶 REQUEST [${options.method.toUpperCase()}] ${options.uri}");
_logDebug("Headers: ${options.headers}");
if (options.data != null) {
_logDebug("Payload: ${options.data}");
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
_logInfo("🟢 RESPONSE [${response.statusCode}] ${response.requestOptions.uri}");
// _logDebug("Body: ${response.data}");
// 401 / 402 Token
if (response.statusCode == 401 || response.statusCode == 402) {
// Token
TokenManager().update();
if (response.statusCode == 402) {
_handleError('errorCode:${response.statusCode}', true);
}
handler.reject(DioException(
requestOptions: response.requestOptions,
response: response,
type: DioExceptionType.badResponse,
error: 'errorCode:${response.statusCode}',
));
return;
}
// Release
if (kReleaseMode && response.data is String) {
try {
final deStr = SkyEncrypt.deStr(response.data);
response.data = jsonDecode(deStr);
} catch (e) {
_logError("解密失败: $e");
}
}
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
_logError("🔴 Error: ${err.message}");
handler.next(err);
}
// ============ ============
Future<void> _addRequestHeaders(RequestOptions options) async {
// security: false
if (!kReleaseMode) {
options.headers['security'] = 'false';
}
// Token
final tokenManager = TokenManager();
final token = tokenManager.get();
// print('查看token: $token');
if (token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
//
String langKey = await AppSp().getString('GLIMZO_STORE_LANGKEY') ?? 'en';
final deviceInfo = SkyDeviceInfo.instance;
options.headers.addAll({
'product-prefix': 'glimzo',
'device-id': deviceInfo.deviceId,
'system-type': deviceInfo.systemType,
'model': deviceInfo.model,
'system-version': deviceInfo.systemVersion,
'brand': deviceInfo.brand,
'app-version': deviceInfo.appVersion,
'app-name': deviceInfo.appName,
'lang-key': langKey,
'time-zone': SkyTimeTool.getTimeZone(),
'idfa': '',
'idfv': deviceInfo.idfv,
'device-gaid': '',
});
// prevToken
await _handleOldToken(options);
}
Future<void> _handleOldToken(RequestOptions options) async {
try {
if (options.data is Map<String, dynamic>) {
final body = options.data as Map<String, dynamic>;
final prevToken = body['prevToken'];
if (prevToken != null && prevToken.toString().isNotEmpty) {
options.headers['Authorization'] = prevToken.toString();
}
}
} catch (_) {
// JSON
}
}
void _logInfo(String message) {
if (enableLog) _logger.i(message);
}
void _logDebug(String message) {
if (enableLog) _logger.d('💡 $message');
}
void _logError(String message) {
if (enableLog) _logger.e(message);
}
void _handleError(dynamic e, bool error) {
_logError("🔴 Error: $e");
//
}
}

View File

@ -0,0 +1,155 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'dart:math';
class SkyEncrypt {
static const int bfSize = 2048;
static const String enStrTag = r'$';
// Generates a random salt of size between 16 and 64 bytes
static Uint8List randSalt() {
final rand = Random.secure();
final size = rand.nextInt(49) + 16; // 16..64
return Uint8List.fromList(List.generate(size, (_) => rand.nextInt(256)));
}
// Encrypts the given data with a random salt
static Uint8List en(Uint8List data) {
if (data.isEmpty) return data;
final salt = randSalt();
final encryptedData = enWithSalt(data, salt);
return Uint8List.fromList([salt.length, ...salt, ...encryptedData]);
}
// Decrypts the given encrypted data
static Uint8List de(Uint8List data) {
if (data.isEmpty) return data;
final saltLen = data[0];
final salt = data.sublist(1, 1 + saltLen);
final encrypted = data.sublist(1 + saltLen);
return deWithSalt(encrypted, salt);
}
// Encrypts data with a specified salt
static Uint8List enWithSalt(Uint8List data, Uint8List salt) {
final mixedData = mixSalt(data, salt);
return cxEd(mixedData);
}
// Decrypts data with a specified salt
static Uint8List deWithSalt(Uint8List data, Uint8List salt) {
final decryptedData = cxEd(data);
return removeSalt(decryptedData, salt);
}
// Simple XOR-based encryption/decryption (bitwise negation)
static Uint8List cxEd(Uint8List data) {
return Uint8List.fromList(data.map((b) => b ^ 0xFF).toList());
}
// Apply salt to the data for encryption
static Uint8List mixSalt(Uint8List data, Uint8List salt) {
if (salt.isEmpty) return data;
return Uint8List.fromList(
List.generate(data.length, (i) {
final s = salt[i % salt.length];
return calSalt(data[i], s);
}),
);
}
// Remove salt from the data after decryption
static Uint8List removeSalt(Uint8List data, Uint8List salt) {
if (salt.isEmpty) return data;
return Uint8List.fromList(
List.generate(data.length, (i) {
final s = salt[i % salt.length];
return calRemoveSalt(data[i], s);
}),
);
}
// Calculate a modified value after mixing salt (for encryption)
static int calSalt(int v, int s) {
final r = v ^ 0xFF;
return s > r ? (s - r - 1) & 0xFF : (v + s) & 0xFF;
}
// Reverse the salt mixing process for decryption
static int calRemoveSalt(int v, int s) {
return v >= s ? (v - s) & 0xFF : (0xFF - (s - v) + 1) & 0xFF;
}
// Encrypt a string and return the hex representation
static String cxEStr(String data) {
return cxEStrAsBytes(
data,
).map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
// Encrypt string and return as bytes
static Uint8List cxEStrAsBytes(String data) {
return cxEd(Uint8List.fromList(utf8.encode(data)));
}
// Decrypt a hex string to a regular string
static String cxDStr(String data) {
final bytes = Uint8List.fromList([
for (int i = 0; i < data.length; i += 2)
int.parse(data.substring(i, i + 2), radix: 16),
]);
return utf8.decode(cxEd(bytes));
}
// Encrypt string data and return in the form of hex string with prefix
static String enStr(String data) {
return enBytesStr(Uint8List.fromList(utf8.encode(data)));
}
// Encrypt bytes and return as hex string with prefix
static String enBytesStr(Uint8List data) {
final encrypted = en(data);
return '$enStrTag${encrypted.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
}
// Decrypt a string with encoded prefix (e.g., "$" symbol)
static String deStr(String data) {
return utf8.decode(deStrBytes(data));
}
// Decrypt the byte array extracted from a string
static Uint8List deStrBytes(String data) {
if (!data.startsWith(enStrTag)) {
throw ArgumentError("Invalid encoded string");
}
final hexData = data.substring(1);
final bytes = Uint8List.fromList([
for (int i = 0; i < hexData.length; i += 2)
int.parse(hexData.substring(i, i + 2), radix: 16),
]);
return de(bytes);
}
// Copy encrypted data from a stream to another stream
static Future<int> copy(
Stream<List<int>> reader,
StreamSink<List<int>> writer,
) async {
int total = 0;
await for (final chunk in reader) {
total += chunk.length;
final encrypted = chunk.map((b) => b ^ 0xFF).toList();
writer.add(encrypted);
}
await writer.close();
return total;
}
// Custom encryption with condition (even and odd byte values handled differently)
static Uint8List cxEd1(Uint8List data) {
return Uint8List.fromList(
data.map((b) => (b % 2 == 1) ? (b ^ 0xFF) : b).toList(),
);
}
}

View File

@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:talker_flutter/talker_flutter.dart';
class SkyLog {
SkyLog._();
static final SkyLog _instance = SkyLog._();
static SkyLog get instance => _instance;
final Talker _talker = TalkerFlutter.init();
factory SkyLog() => _instance;
Talker get tLog => _talker;
/// DevTools /
static const String logName = 'SkyLog';
Future<void> initLogger() async {
/// app
}
/// [kDebugMode] JSON
static void print(
Object? message, {
String? tag,
String name = logName,
}) {
if (!kDebugMode) return;
try {
final text = _stringify(message);
final line = tag != null ? '[debug-tag: $tag] ===== $text' : text;
log(line, name: name);
} catch (e, st) {
log(
'[SkyLog.print failed] $e',
name: name,
error: e,
stackTrace: st,
);
}
}
/// JSON退 [toString]
static String _stringify(Object? value) {
if (value == null) return 'null';
if (value is String) return value;
if (value is num || value is bool) return value.toString();
const encoder = JsonEncoder.withIndent(' ');
if (value is Map) {
try {
return encoder.convert(value);
} catch (_) {
return value.toString();
}
}
if (value is List) {
try {
return encoder.convert(value);
} catch (_) {
return value.toString();
}
}
if (value is Iterable) {
try {
return encoder.convert(value.toList());
} catch (_) {
return value.toString();
}
}
try {
return encoder.convert(value);
} catch (_) {
try {
return '${value.runtimeType}: $value';
} catch (_) {
return '<unprintable ${value.runtimeType}>';
}
}
}
/// Talker Talker Debug [log]
static void error(
Object? message, {
String? tag,
StackTrace? stackTrace,
}) {
final text = message == null ? 'null' : '$message';
try {
_instance._talker.logCustom(
CustomLog(
text,
settings: _instance._talker.settings,
tag: tag,
logLevel: LogLevel.error,
stackTrace: stackTrace,
),
);
} catch (e, st) {
if (kDebugMode) {
log(
'[SkyLog.error fallback] $text | $e',
name: logName,
error: e,
stackTrace: st,
);
}
}
}
}
class CustomLog extends TalkerLog {
final String? tag;
final TalkerSettings settings;
final LogLevel? _logLevel;
final StackTrace? _stackTrace;
CustomLog(
super.message, {
this.tag,
required this.settings,
LogLevel logLevel = LogLevel.debug,
StackTrace? stackTrace,
}) : _logLevel = logLevel,
_stackTrace = stackTrace;
@override
String? get key => tag;
@override
LogLevel? get logLevel => _logLevel;
@override
StackTrace? get stackTrace => _stackTrace;
@override
AnsiPen? get pen =>
settings.colors[TalkerKey.fromLogLevel(logLevel ?? LogLevel.info)];
}

View File

@ -0,0 +1,35 @@
import 'package:get/get.dart';
import 'package:skywood/pages/splash/splash_page.dart';
class SkyRouters {
static const String splash = '/splash';
}
class SkyPages {
static const initial = SkyRouters.splash;
static final List<GetPage> routes = [
routePage(name: SkyRouters.splash, page: ()=> SplashPage())
];
}
GetPage routePage({
required String name,
required GetPageBuilder page,
Bindings? binding,
Transition transition = Transition.rightToLeft,
Duration transitionDuration = const Duration(milliseconds: 300),
CustomTransition? customTransition,
bool popGesture = true, // Allow iOS swipe-back gesture by default
}) {
return GetPage(
name: name,
page: page,
binding: binding,
transition: transition,
transitionDuration: transitionDuration,
customTransition: customTransition,
popGesture: popGesture, // Pass through to GetPage
);
}

View File

@ -2,7 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:skywood/global/request/app_request.dart';
import 'package:skywood/global/tool/sky_routers.dart';
import 'package:skywood/main/main_root.dart';
import 'package:skywood/pages/controllers/user_controller.dart';
import 'package:skywood/utils/tools/sky_device_info.dart';
import 'package:skywood/utils/tools/token_manage.dart';
import 'global/const/common_color.dart';
import 'global/language/app_language.dart';
@ -10,22 +15,21 @@ import 'global/language/app_language.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// await initServices();
await initServices();
Locale? locale = await AppLanguage.getLocale();
runApp(ComApp(
home: MainRoot(),
locale: locale,
));
runApp(ComApp(home: MainRoot(), locale: locale));
}
Future<void> initServices() async {
await SkyDeviceInfo.init();
await TokenManager().init();
await Get.putAsync(() async => UserController());
}
class ComApp extends StatelessWidget {
const ComApp({super.key,
required this.home,
this.locale,
});
const ComApp({super.key, required this.home, this.locale});
final Widget home;
final Locale? locale;
@ -40,26 +44,38 @@ class ComApp extends StatelessWidget {
translations: AppLanguage(),
locale: locale ?? Get.deviceLocale,
fallbackLocale: const Locale('en'),
// initialBinding: AppBindings(),
initialBinding: AppBindings(),
initialRoute: SkyPages.initial,
getPages: SkyPages.routes,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: CommonColor.primacyColor),
colorScheme: ColorScheme.fromSeed(
seedColor: CommonColor.primacyColor,
),
appBarTheme: AppBarTheme(elevation: 0, centerTitle: true),
splashColor: Colors.transparent,
useMaterial3: true,
scaffoldBackgroundColor: Colors.white,
),
home: home,
builder: FlutterSmartDialog.init(builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(1.0)),
child: child!
);
}),
onInit: () {
},
builder: FlutterSmartDialog.init(
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.linear(1.0)),
child: child!,
);
},
),
onInit: () {},
),
);
}
}
class AppBindings extends Bindings {
@override
void dependencies() {
// Get.lazyPut(() => UserController());
}
}

View File

@ -5,4 +5,6 @@ export 'package:skywood/global/const/common_color.dart';
export 'package:skywood/global/const/common_string.dart';
export 'package:skywood/utils/tools/app_screen.dart';
export 'package:skywood/utils/tools/help_img.dart';
export 'package:skywood/utils/tools/help_img.dart';
export 'package:flutter_screenutil/flutter_screenutil.dart';

View File

@ -58,17 +58,9 @@ class _MainRootState extends State<MainRoot> {
minimum: EdgeInsets.only(bottom: 10.h),
child: Stack(
children: [
SizedBox(
height: 49.h, width: 345.w,
),
Positioned(
top: 11.h,
child: _buildTabBar(),
),
Positioned(
top: 0, left: 15.w,
child: _buildImgs(),
)
SizedBox(height: 49.h, width: 345.w),
Positioned(top: 11.h, child: _buildTabBar()),
Positioned(top: 0, left: 15.w, child: _buildImgs()),
],
),
),
@ -76,8 +68,9 @@ class _MainRootState extends State<MainRoot> {
}
Widget _buildImgs() {
return Container(
height: 38.h, width: 345.w,
return SizedBox(
height: 38.h,
width: 345.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(_items.length, (index) {

View File

@ -0,0 +1,36 @@
import 'package:get/get.dart';
import 'package:skywood/global/client/user_client.dart';
import 'package:skywood/main/main_root.dart';
import 'package:skywood/utils/tools/help_nav.dart';
import 'package:skywood/utils/tools/token_manage.dart';
class UserController extends GetxController {
@override
void onInit() {
super.onInit();
// _userRegister();
}
void _userRegister() async {
// token没有的话去注册
final tm = TokenManager();
String token = tm.get();
print('tokkkkkk: $token');
if (token.isNotEmpty) {
//
print('object去主页面');
Future.delayed(const Duration(seconds: 1), () {
HelpNav.off(MainRoot());
});
return;
}
final res = await UserClient.register();
if (res != null) {
tm.save(res.token);
}
}
}

View File

@ -1,8 +1,12 @@
import 'package:flutter/material.dart';
import 'package:skywood/global/client/play_client.dart';
import 'package:skywood/global/client/user_client.dart';
import 'package:skywood/main/lib_file.dart';
import 'package:skywood/pages/home/widgets/daily_quest_widget.dart';
import 'package:skywood/pages/widgets/common_bg_page.dart';
import 'package:skywood/utils/models/drama/skywood_home.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
@ -13,18 +17,107 @@ class HomePage extends StatefulWidget {
class _MainRootState extends State<HomePage> {
List<SkywoodHome> _homeData = [];
@override
void initState() {
super.initState();
_getHomeData();
}
void _getHomeData() {
// home_v3_recommand
// new_recommand
// week_ranking
// week_highest_recommend
// marquee
// home_banner
// get_details_recommand
PlayClient.getHomeData().then((value) {
for (SkywoodHome element in value) {
print('lele: ${element.moduleKey} : ${element.dataList.length}');
}
setState(() {
_homeData = value;
});
});
}
@override
Widget build(BuildContext context) {
return CommonBgPage(
child: CustomScrollView(
slivers: [
child: SafeArea(
child: _homeData.isEmpty ? SizedBox() : CustomScrollView(
slivers: [
_buildTopTwoImgs(),
_buildSpace(),
DailyQuestWidget(
dramaList: _homeData.firstWhere((element) => element.moduleKey == 'home_v3_recommand').dataList,
),
_buildSpace(height: 10),
_buildTitleImg(ConstImg.homeTitleMystery),
_buildSpace(),
// 345 355
_buildTitleImg(
ConstImg.homeOpenNowBox,
padding: EdgeInsets.symmetric(horizontal: 15.w),
height: 355.h,
),
_buildSpace(height: 10),
_buildTitleImg(ConstImg.homeTitleHot),
_buildSpace(height: 300),
_buildTitleImg(ConstImg.homeTitleWeekly),
_buildSpace(height: 300),
_buildTitleImg(ConstImg.homeTitleHotCate),
_buildSpace(height: 300),
],
),
),
);
}
SliverToBoxAdapter _buildTopTwoImgs() {
// 104-76
return SliverToBoxAdapter(
child: Stack(
children: [
SizedBox(width: double.infinity, height: (204 + 104 - 28).h),
Container(
height: 204.h,
padding: EdgeInsets.only(left: 25.w, top: 3.h, right: 25.w),
child: HelpImg.asset(ConstImg.homeTopBg, fit: BoxFit.fill),
),
Positioned(
top: (204 - 28).h,
height: 104.h,
width: AppScreen.screenWidth,
child: HelpImg.asset(ConstImg.homeTitleDaily, fit: BoxFit.fill),
),
],
),
);
}
Widget _buildTitleImg() {
return Container();
SliverToBoxAdapter _buildSpace({double height = 20}) {
return SliverToBoxAdapter(child: SizedBox(height: height.h));
}
}
SliverToBoxAdapter _buildTitleImg(
String imgName, {
double? height,
double? width,
EdgeInsets? padding,
}) {
// 104-76
return SliverToBoxAdapter(
child: Container(
padding: padding ?? EdgeInsets.zero,
width: width ?? double.infinity,
height: height ?? 104.h,
child: HelpImg.asset(imgName, fit: BoxFit.fill),
),
);
}
}

View File

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:skywood/global/const/common_color.dart';
import 'package:skywood/utils/models/drama/skywood_drama.dart';
import 'package:skywood/utils/tools/sky_time_tool.dart';
import 'package:skywood/utils/tools/widgets/sky_common_widget.dart';
import '../../../utils/tools/help_img.dart';
class DailyQuestWidget extends StatelessWidget {
const DailyQuestWidget({super.key,
required this.dramaList,
});
final List<SkywoodDrama> dramaList;
@override
Widget build(BuildContext context) {
double height = 281.h;
double width = 345.w;
return SliverToBoxAdapter(
child: SizedBox(
width: width,
height: height,
child: Stack(
children: [
HelpImg.asset(ConstImg.homeCardBg, height: height, width: width,fit: BoxFit.fill),
Positioned(
top: 20.h, bottom: 19.h, left: 22.5.w,
width: 300.w,
child: Column(
children: [
_buildFirstDrama(),
_buildDramaList()
],
),
),
],
),
),
);
}
Widget _buildDramaList() {
if (dramaList.length < 2) return SizedBox.shrink();
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
spacing: 5.w,
children: List.generate(dramaList.length-1, (index) {
return HelpImg.network(dramaList[index+1].imageUrl, width: 95.w, height: 50.h, fit: BoxFit.cover);
})
),
);
}
Widget _buildFirstDrama() {
if (dramaList.isEmpty) return SizedBox.shrink();
SkywoodDrama drama = dramaList[0];
return Column(
children: [
Container(
decoration: BoxDecoration(
border: Border.all(color: CommonColor.black, width: 1),
),
child: HelpImg.network(drama.imageUrl, width: 300.w, height: 160.h, fit: BoxFit.cover),
),
Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: SkyText(text: drama.name,
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
)),
Row(
children: [
// Icon(Icons.heart_broken, size: 15, color: Colors.redAccent,),
HelpImg.asset(ConstImg.heart, width: 18.w, height: 18.w),
const SizedBox(width: 3),
SkyText(text: SkyTimeTool.formatNum(drama.watchTotal),
fontSize: 12,
fontWeight: FontWeight.w400,
)
],
)
],
)
],
);
}
}

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:skywood/main/lib_file.dart';
import 'package:skywood/pages/widgets/pixel_widgets.dart';
class LikePage extends StatefulWidget {
const LikePage({super.key});
@override
@ -11,11 +12,356 @@ class LikePage extends StatefulWidget {
}
class _MainRootState extends State<LikePage> {
int _tabIndex = 0;
final List<String> _collectionTitles = const [
'Undercover Play',
"Don't Cross Me",
'Two Worlds Apart',
'Rekindled Scorn',
'My Revenge is You',
"Mr. Gu's Sweet Weakness",
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: PixelPageBackground(
child: SafeArea(
bottom: false,
child: Padding(
padding: EdgeInsets.fromLTRB(15.w, 44.h, 15.w, 0),
child: PixelPanel(
padding: EdgeInsets.fromLTRB(10.w, 18.h, 10.w, 22.h),
child: Column(
children: [
_buildTabs(),
SizedBox(height: 20.h),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
child: _tabIndex == 0
? _buildCollection()
: _buildPlayLog(),
),
),
],
),
),
),
),
),
);
}
}
Widget _buildTabs() {
return Row(
children: [
Expanded(
child: _LibraryTab(
title: 'My Collection',
selected: _tabIndex == 0,
onTap: () => setState(() => _tabIndex = 0),
),
),
SizedBox(width: 8.w),
Expanded(
child: _LibraryTab(
title: 'Play Log',
selected: _tabIndex == 1,
onTap: () => setState(() => _tabIndex = 1),
),
),
],
);
}
Widget _buildCollection() {
return GridView.builder(
key: const ValueKey('collection'),
padding: EdgeInsets.only(top: 2.h, bottom: 120.h),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10.h,
crossAxisSpacing: 7.w,
childAspectRatio: 100 / 174,
),
itemCount: _collectionTitles.length,
itemBuilder: (context, index) {
return _CollectionCard(title: _collectionTitles[index]);
},
);
}
Widget _buildPlayLog() {
return ListView(
key: const ValueKey('play-log'),
padding: EdgeInsets.only(bottom: 120.h),
children: [
_ContinueCard(),
SizedBox(height: 18.h),
Text(
'Watch History',
style: TextStyle(
color: const Color(0xFF1C1E1F),
fontSize: 16.sp,
fontWeight: FontWeight.w700,
height: 1,
),
),
SizedBox(height: 10.h),
_HistoryItem(title: 'As A Mafia Boss, I Refuse To Be An Extra'),
SizedBox(height: 10.h),
_HistoryItem(title: 'Fatal Vengeance'),
SizedBox(height: 10.h),
_HistoryItem(title: "Don't Cross Me"),
],
);
}
}
class _LibraryTab extends StatelessWidget {
const _LibraryTab({
required this.title,
required this.selected,
required this.onTap,
});
final String title;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 45.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: selected ? const Color(0xFF16ED1E) : const Color(0xFFF6F6F6),
border: Border.all(
color: selected ? CommonColor.black : const Color(0xFFD6D6D6),
width: 1.5.w,
),
boxShadow: selected
? const [
BoxShadow(color: Color(0xFF1C1E1F), offset: Offset(2, 2)),
]
: null,
),
child: Text(
title,
style: TextStyle(
color: selected ? Colors.white : const Color(0xFFB2B2B2),
fontSize: 14.sp,
fontWeight: FontWeight.w900,
height: 1,
shadows: selected
? const [Shadow(color: Color(0xFF1C1E1F), offset: Offset(1, 1))]
: null,
),
),
),
);
}
}
class _CollectionCard extends StatelessWidget {
const _CollectionCard({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(4.w, 8.h, 4.w, 6.h),
decoration: BoxDecoration(
color: const Color(0xFFF4FFB6),
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
boxShadow: const [
BoxShadow(color: Color(0xFF653CFA), offset: Offset(3, 3)),
],
),
child: Column(
children: [
Row(
children: List.generate(4, (index) {
return Container(
width: 11.w,
height: 6.h,
margin: EdgeInsets.only(right: 5.w),
decoration: BoxDecoration(
color: index == 3
? const Color(0xFFEAEAEA)
: const Color(0xFF96FE9B),
border: Border.all(
color: const Color(0xFF1C1E1F),
width: 0.8.w,
),
),
);
}),
),
SizedBox(height: 8.h),
const Expanded(child: PosterImage()),
SizedBox(height: 8.h),
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(0xFF1C1E1F),
fontSize: 13.sp,
fontWeight: FontWeight.w500,
height: 1,
),
),
],
),
);
}
}
class _ContinueCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 140.h,
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
color: const Color(0xFFEAE6FF),
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
),
child: Row(
children: [
const PosterImage(width: 90, height: 120, borderRadius: 2),
SizedBox(width: 10.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mogul And His Runaway Bride',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(0xFF1B1926),
fontSize: 14.sp,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
SizedBox(height: 18.h),
Text(
'Chapter 10 / 28',
style: TextStyle(
color: CommonColor.primacyColor,
fontSize: 11.sp,
height: 1,
),
),
SizedBox(height: 6.h),
_ProgressLine(value: 0.46),
const Spacer(),
const PixelButton(
label: 'Continue Watch',
width: 145,
height: 29,
),
],
),
),
],
),
);
}
}
class _HistoryItem extends StatelessWidget {
const _HistoryItem({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Container(
height: 100.h,
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
decoration: BoxDecoration(
color: const Color(0xFFEAE6FF),
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
),
child: Row(
children: [
const PosterImage(width: 66, height: 88, borderRadius: 2),
SizedBox(width: 10.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: CommonColor.black,
fontSize: 13.sp,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
Text(
'EP.40/85',
style: TextStyle(
color: CommonColor.primacyColor,
fontSize: 11.sp,
height: 1,
),
),
_ProgressLine(value: 0.46),
],
),
),
],
),
);
}
}
class _ProgressLine extends StatelessWidget {
const _ProgressLine({required this.value});
final double value;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Container(
height: 6.h,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.7),
border: Border.all(color: const Color(0xFF1C1E1F), width: 0.5.w),
borderRadius: BorderRadius.circular(10.r),
),
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: value,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF00EE02),
borderRadius: BorderRadius.circular(10.r),
),
),
),
),
),
SizedBox(width: 4.w),
Text('46%', style: TextStyle(fontSize: 10.sp, height: 1)),
],
);
}
}

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:skywood/main/lib_file.dart';
import 'package:skywood/pages/widgets/pixel_widgets.dart';
class MinePage extends StatefulWidget {
const MinePage({super.key});
@override
@ -11,11 +12,475 @@ class MinePage extends StatefulWidget {
}
class _MainRootState extends State<MinePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: PixelPageBackground(
child: SafeArea(
bottom: false,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(child: SizedBox(height: 12.h)),
SliverToBoxAdapter(child: _buildHeader()),
SliverToBoxAdapter(child: SizedBox(height: 18.h)),
SliverToBoxAdapter(child: _buildPlayerCard()),
SliverToBoxAdapter(child: SizedBox(height: 10.h)),
SliverToBoxAdapter(child: _buildWalletShop()),
SliverToBoxAdapter(child: SizedBox(height: 10.h)),
SliverToBoxAdapter(child: _buildCheckpoint()),
SliverToBoxAdapter(child: SizedBox(height: 10.h)),
SliverToBoxAdapter(child: _buildGameMenu()),
SliverToBoxAdapter(child: SizedBox(height: 120.h)),
],
),
),
),
);
}
}
Widget _buildHeader() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: Column(
children: [
Row(
children: [
_DecorIcon(icon: Icons.forest, color: const Color(0xFFFF5454)),
SizedBox(width: 10.w),
Expanded(
child: Container(
height: 60.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF16ED1E),
border: Border.all(color: CommonColor.black, width: 1.5.w),
boxShadow: const [
BoxShadow(color: Color(0xFF1C1E1F), offset: Offset(3, 3)),
],
),
child: RichText(
text: TextSpan(
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w900,
height: 1,
shadows: const [
Shadow(
color: Color(0xFF1C1E1F),
offset: Offset(2, 2),
),
],
),
children: const [
TextSpan(
text: 'Profile ',
style: TextStyle(color: Color(0xFFFFDC4C)),
),
TextSpan(
text: 'Menu',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
SizedBox(width: 10.w),
_DecorIcon(icon: Icons.live_tv, color: const Color(0xFF8965FB)),
],
),
SizedBox(height: 12.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(7, (index) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 3.w),
child: Icon(
index < 4 ? Icons.favorite : Icons.question_mark,
size: index < 4 ? 18.w : 14.w,
color: index.isEven
? const Color(0xFF1AF23A)
: const Color(0xFF653CFA),
),
);
}),
),
],
),
);
}
Widget _buildPlayerCard() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: PixelPanel(
color: CommonColor.primacyColor,
shadowColor: CommonColor.black,
padding: EdgeInsets.zero,
child: Column(
children: [
const PixelSectionLabel(label: 'Player Card'),
Padding(
padding: EdgeInsets.all(16.w),
child: Row(
children: [
Container(
width: 68.w,
height: 68.w,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: const Color(0xFFE9C7FF),
border: Border.all(color: CommonColor.black, width: 1.w),
),
child: Icon(
Icons.person_pin,
size: 42.w,
color: const Color(0xFF653CFA),
),
),
SizedBox(width: 15.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Vistor',
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
fontWeight: FontWeight.w700,
height: 1,
),
),
SizedBox(height: 12.h),
Text(
'ID: 32432233341',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.72),
fontSize: 10.sp,
height: 1,
),
),
],
),
],
),
),
],
),
),
);
}
Widget _buildWalletShop() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: Row(
children: [
Expanded(
child: _SmallPanel(
label: 'Treasure Wallet',
child: Row(
children: [
Icon(
Icons.inventory_2,
size: 70.w,
color: const Color(0xFFFFDC4C),
),
SizedBox(width: 6.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_WalletValue(label: 'Coins', value: '15,890'),
Divider(height: 8.h, color: Colors.white54),
_WalletValue(label: 'Bonus', value: '20,000'),
],
),
),
],
),
),
),
SizedBox(width: 10.w),
Expanded(
child: _SmallPanel(
label: 'Item Shop',
child: Row(
children: [
Icon(
Icons.storefront,
size: 66.w,
color: const Color(0xFFFF8DB8),
),
SizedBox(width: 7.w),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Get More\nCoins',
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w600,
height: 1.1,
),
),
SizedBox(height: 14.h),
const PixelButton(
label: 'Store',
width: 70,
height: 20,
color: Color(0xFFFF88CF),
),
],
),
),
],
),
),
),
],
),
);
}
Widget _buildCheckpoint() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: PixelPanel(
color: CommonColor.primacyColor,
padding: EdgeInsets.zero,
shadowColor: CommonColor.black,
child: Column(
children: [
const PixelSectionLabel(label: 'Daily Checkpoint'),
Padding(
padding: EdgeInsets.fromLTRB(16.w, 10.h, 10.w, 10.h),
child: Row(
children: [
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(7, (index) {
return _DayBox(day: index + 1, active: index == 0);
}),
),
),
SizedBox(width: 12.w),
const PixelButton(
label: 'Check In',
width: 70,
height: 22,
color: Color(0xFFFF88CF),
),
],
),
),
],
),
),
);
}
Widget _buildGameMenu() {
final items = const [
_MenuItem('Log In', Icons.login),
_MenuItem('Wallet', Icons.account_balance_wallet),
_MenuItem('Language', Icons.public),
_MenuItem('Feedback', Icons.mail),
_MenuItem('Settings', Icons.settings),
_MenuItem('About Us', Icons.info),
];
return Padding(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: PixelPanel(
color: CommonColor.primacyColor,
padding: EdgeInsets.zero,
shadowColor: CommonColor.black,
child: Column(
children: [
const PixelSectionLabel(label: 'Game Menu'),
Padding(
padding: EdgeInsets.all(10.w),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
mainAxisSpacing: 8.h,
crossAxisSpacing: 6.w,
childAspectRatio: 1,
),
itemCount: items.length,
itemBuilder: (context, index) => _MenuTile(item: items[index]),
),
),
],
),
),
);
}
}
class _SmallPanel extends StatelessWidget {
const _SmallPanel({required this.label, required this.child});
final String label;
final Widget child;
@override
Widget build(BuildContext context) {
return PixelPanel(
color: CommonColor.primacyColor,
shadowColor: CommonColor.black,
padding: EdgeInsets.zero,
child: SizedBox(
height: 134.h,
child: Column(
children: [
PixelSectionLabel(label: label),
Expanded(
child: Padding(padding: EdgeInsets.all(8.w), child: child),
),
],
),
),
);
}
}
class _WalletValue extends StatelessWidget {
const _WalletValue({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(color: Colors.white70, fontSize: 9.sp, height: 1),
),
SizedBox(height: 2.h),
Text(
value,
style: TextStyle(
color: Colors.white,
fontSize: 15.sp,
fontWeight: FontWeight.w900,
height: 1,
),
),
],
);
}
}
class _DayBox extends StatelessWidget {
const _DayBox({required this.day, required this.active});
final int day;
final bool active;
@override
Widget build(BuildContext context) {
return Container(
width: 28.w,
height: 34.h,
decoration: BoxDecoration(
color: active ? const Color(0xFF91F473) : Colors.white,
border: Border.all(color: CommonColor.black, width: 1.w),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Day', style: TextStyle(fontSize: 7.sp, height: 1)),
SizedBox(height: 4.h),
Text(
'$day',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w800,
height: 1,
),
),
],
),
);
}
}
class _MenuTile extends StatelessWidget {
const _MenuTile({required this.item});
final _MenuItem item;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(4.w, 4.h, 4.w, 6.h),
decoration: BoxDecoration(
color: const Color(0xFF8D62F7),
border: Border.all(color: CommonColor.black, width: 1.w),
boxShadow: const [
BoxShadow(color: Color(0xFF1C1E1F), offset: Offset(2, 2)),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(item.icon, color: Colors.white, size: 25.w),
SizedBox(height: 5.h),
Text(
item.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 8.sp,
fontWeight: FontWeight.w700,
height: 1,
),
),
],
),
);
}
}
class _DecorIcon extends StatelessWidget {
const _DecorIcon({required this.icon, required this.color});
final IconData icon;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
width: 50.w,
height: 50.w,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: CommonColor.black, width: 1.w),
boxShadow: const [
BoxShadow(color: Color(0xFF1C1E1F), offset: Offset(2, 2)),
],
),
child: Icon(icon, color: color, size: 32.w),
);
}
}
class _MenuItem {
const _MenuItem(this.label, this.icon);
final String label;
final IconData icon;
}

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:skywood/main/lib_file.dart';
import 'package:skywood/pages/widgets/pixel_widgets.dart';
class SearchPage extends StatefulWidget {
const SearchPage({super.key});
@override
@ -11,11 +12,223 @@ class SearchPage extends StatefulWidget {
}
class _MainRootState extends State<SearchPage> {
final List<String> _titles = const [
"Mr. Gu's Sweet Weakness",
'Secret Wife with a Scar',
'A Second Chance at Love',
'Shadows of the Heart',
'Boarding Pass To Love',
'The Love Blueprint',
'Who Dared Touch My Bride',
'Famous Families Fled',
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
bottom: false,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(child: _buildSearchBar()),
SliverToBoxAdapter(child: SizedBox(height: 16.h)),
SliverToBoxAdapter(child: _buildHistory()),
SliverToBoxAdapter(child: SizedBox(height: 30.h)),
SliverToBoxAdapter(child: _buildTitle('Trending Now')),
SliverToBoxAdapter(child: SizedBox(height: 10.h)),
SliverToBoxAdapter(child: _buildTrendingList()),
SliverToBoxAdapter(child: SizedBox(height: 120.h)),
],
),
),
);
}
}
Widget _buildSearchBar() {
return Padding(
padding: EdgeInsets.fromLTRB(15.w, 10.h, 15.w, 0),
child: Row(
children: [
Container(
width: 34.w,
height: 34.w,
decoration: BoxDecoration(
color: const Color(0xFFB6FFA7),
border: Border.all(color: CommonColor.black, width: 1.w),
boxShadow: const [
BoxShadow(color: Color(0xFF91F473), offset: Offset(2, 2)),
],
),
child: Icon(
Icons.arrow_back_ios_new,
size: 16.w,
color: CommonColor.black,
),
),
SizedBox(width: 10.w),
Expanded(
child: Container(
height: 36.h,
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
border: Border.all(color: CommonColor.black, width: 1.w),
borderRadius: BorderRadius.circular(40.r),
),
child: Row(
children: [
Icon(Icons.search, size: 16.w, color: CommonColor.black),
SizedBox(width: 7.w),
Text(
'Search',
style: TextStyle(
color: const Color(0xFF3E3D40),
fontSize: 12.sp,
height: 1,
),
),
],
),
),
),
],
),
);
}
Widget _buildHistory() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(child: _buildTitle('Search History', fontSize: 14)),
Icon(
Icons.delete_outline,
size: 18.w,
color: const Color(0xFF888888),
),
],
),
SizedBox(height: 10.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: const Color(0x0D000000),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
'Love',
style: TextStyle(
color: const Color(0x80000000),
fontSize: 12.sp,
fontWeight: FontWeight.w500,
height: 1,
),
),
),
],
),
);
}
Widget _buildTitle(String title, {double fontSize = 18}) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: Text(
title,
style: TextStyle(
color: CommonColor.black,
fontSize: fontSize.sp,
fontWeight: FontWeight.w700,
height: 1.1,
),
),
);
}
Widget _buildTrendingList() {
return SizedBox(
height: 446.h,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: Wrap(
direction: Axis.vertical,
spacing: 10.h,
runSpacing: 10.w,
children: _titles
.map((title) => _TrendingItem(title: title))
.toList(),
),
),
);
}
}
class _TrendingItem extends StatelessWidget {
const _TrendingItem({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 236.w,
height: 104.h,
child: Row(
children: [
const PosterImage(
width: 80,
height: 104,
borderRadius: 4,
showBorder: false,
),
SizedBox(width: 8.w),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: CommonColor.black,
fontSize: 13.sp,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
Row(
children: [
Icon(
Icons.local_fire_department,
size: 15.w,
color: const Color(0xFFFF6A00),
),
SizedBox(width: 2.w),
Text(
'17.3K',
style: TextStyle(
color: const Color(0x80000000),
fontSize: 10.sp,
height: 1,
),
),
],
),
],
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:skywood/main/lib_file.dart';
import '../../global/client/user_client.dart';
import '../../main/main_root.dart';
import '../../utils/tools/help_nav.dart';
import '../../utils/tools/token_manage.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@override
State<StatefulWidget> createState() {
return _SplashPageState();
}
}
class _SplashPageState extends State<SplashPage> {
@override
void initState() {
super.initState();
_userRegister();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
bottom: false,
child: HelpImg.asset(ConstImg.splashPage, fit: BoxFit.cover)),
);
}
void _userRegister() async {
// token没有的话去注册
final tm = TokenManager();
String token = tm.get();
print('tokkkkkk: $token');
if (token.isNotEmpty) {
//
print('object去主页面');
Future.delayed(const Duration(seconds: 1), () {
HelpNav.off(MainRoot());
});
return;
}
final res = await UserClient.register();
if (res != null) {
tm.save(res.token);
HelpNav.off(MainRoot());
}
}
}

View File

@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:skywood/main/lib_file.dart';
class PixelPageBackground extends StatelessWidget {
const PixelPageBackground({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Stack(
children: [
const Positioned.fill(child: ColoredBox(color: Colors.white)),
const Positioned.fill(child: CustomPaint(painter: _GridPainter())),
Positioned(
left: -18.w,
top: 178.h,
child: Container(
width: 52.w,
height: 132.h,
color: const Color(0xFF08F214),
),
),
Positioned(
right: -20.w,
top: 282.h,
child: Container(
width: 58.w,
height: 286.h,
color: const Color(0xFF08F214),
),
),
child,
],
);
}
}
class PixelPanel extends StatelessWidget {
const PixelPanel({
super.key,
required this.child,
this.padding,
this.color = Colors.white,
this.borderColor = const Color(0xFF1C1E1F),
this.shadowColor = const Color(0xFF653CFA),
});
final Widget child;
final EdgeInsets? padding;
final Color color;
final Color borderColor;
final Color shadowColor;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: color,
border: Border.all(color: borderColor, width: 1.5.w),
boxShadow: [
BoxShadow(color: shadowColor, offset: Offset(4.w, 4.h)),
BoxShadow(color: shadowColor, offset: Offset(-4.w, 4.h)),
],
),
padding: padding ?? EdgeInsets.all(10.w),
child: child,
);
}
}
class PixelSectionLabel extends StatelessWidget {
const PixelSectionLabel({
super.key,
required this.label,
this.width,
this.trailing,
});
final String label;
final double? width;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return Container(
height: 20.h,
width: width?.w,
padding: EdgeInsets.symmetric(horizontal: 8.w),
decoration: BoxDecoration(
color: const Color(0xFF7B54F6),
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
),
child: Row(
children: [
Container(width: 7.w, height: 7.w, color: const Color(0xFF34F04F)),
SizedBox(width: 5.w),
Expanded(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w700,
height: 1,
),
),
),
trailing ?? const SizedBox.shrink(),
],
),
);
}
}
class PixelButton extends StatelessWidget {
const PixelButton({
super.key,
required this.label,
this.onTap,
this.width = 116,
this.height = 28,
this.color = const Color(0xFF8D62F7),
});
final String label;
final VoidCallback? onTap;
final double width;
final double height;
final Color color;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: width.w,
height: height.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color,
border: Border.all(color: const Color(0xFF1C1E1F), width: 1.w),
),
child: Text(
label,
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w800,
height: 1,
shadows: const [
Shadow(color: Color(0xFF1C1E1F), offset: Offset(1, 1)),
],
),
),
),
);
}
}
class PosterImage extends StatelessWidget {
const PosterImage({
super.key,
this.width,
this.height,
this.borderRadius = 3,
this.showBorder = true,
});
final double? width;
final double? height;
final double borderRadius;
final bool showBorder;
@override
Widget build(BuildContext context) {
return Container(
width: width?.w,
height: height?.h,
decoration: BoxDecoration(
border: showBorder
? Border.all(color: const Color(0xFF1C1E1F), width: 0.8.w)
: null,
borderRadius: BorderRadius.circular(borderRadius.r),
),
clipBehavior: Clip.antiAlias,
child: HelpImg.asset(ConstImg.homeCardBg, fit: BoxFit.cover),
);
}
}
class _GridPainter extends CustomPainter {
const _GridPainter();
@override
void paint(Canvas canvas, Size size) {
final gridPaint = Paint()
..color = const Color(0xFFE0D8FF)
..strokeWidth = 0.8;
const step = 10.0;
for (double x = 0; x <= size.width; x += step) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
}
for (double y = 0; y <= size.height; y += step) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
}
final blockPaint = Paint()..color = const Color(0xFFE0D8FF);
canvas.drawRect(Rect.fromLTWH(80, 0, 34, 96), blockPaint);
canvas.drawRect(Rect.fromLTWH(228, 0, 34, 32), blockPaint);
canvas.drawRect(Rect.fromLTWH(278, 112, 34, 34), blockPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
// Widget类以添加额外的布局和样式方法
extension WidgetExtend on Widget {
/// Widget外层添加一个Container
@ -43,7 +42,8 @@ extension WidgetExtend on Widget {
padding: padding,
alignment: alignment,
clipBehavior: clipBehavior,
constraints: constraints ??
constraints:
constraints ??
BoxConstraints(
minWidth: minWidth ?? 0.0,
maxWidth: maxWidth ?? double.infinity,
@ -59,8 +59,9 @@ extension WidgetExtend on Widget {
image: image,
gradient: gradient,
border: border,
borderRadius:
radius != null ? BorderRadius.circular(radius) : borderRadius,
borderRadius: radius != null
? BorderRadius.circular(radius)
: borderRadius,
),
child: this,
).addInkWell(onTap: onTap);
@ -84,10 +85,7 @@ extension WidgetExtend on Widget {
///
///
Widget addAlign({AlignmentGeometry alignment = Alignment.centerLeft}) {
return Align(
alignment: alignment,
child: this,
);
return Align(alignment: alignment, child: this);
}
Widget addCenter() {
@ -118,20 +116,14 @@ extension WidgetExtend on Widget {
///
///
Widget addVisibility({bool visible = true}) {
return Visibility(
visible: visible,
child: this,
);
return Visibility(visible: visible, child: this);
}
/// Widget外层添加一个Transform.rotate
///
///
Widget addRotate({double angle = 0}) {
return Transform.rotate(
angle: angle,
child: this,
);
return Transform.rotate(angle: angle, child: this);
}
/// Widget外层添加一个InkWell
@ -141,44 +133,27 @@ extension WidgetExtend on Widget {
if (onTap == null) {
return this;
}
return InkWell(
onTap: onTap,
child: this,
);
return InkWell(onTap: onTap, child: this);
}
/// Widget外层添加一个SizedBox
///
///
Widget addSize({double? width, double? height}) {
return SizedBox(
width: width,
height: height,
child: this,
);
return SizedBox(width: width, height: height, child: this);
}
/// Widget外层添加一个Opacity
///
///
Widget addOpacity({double opacity = 1}) {
return Opacity(
opacity: opacity,
child: this,
);
return Opacity(opacity: opacity, child: this);
}
}
extension OnRenderObjectWidget on RenderObjectWidget {
SliverPadding addPaddingSliver({EdgeInsets? padding}) {
return SliverPadding(
padding: padding ?? EdgeInsets.zero,
sliver: this,
);
return SliverPadding(padding: padding ?? EdgeInsets.zero, sliver: this);
}
DecoratedSliver addDecoratedSliver({
@ -199,7 +174,7 @@ extension OnRenderObjectWidget on RenderObjectWidget {
gradient: gradient,
shape: shape ?? BoxShape.rectangle,
),
sliver: this.addPaddingSliver(padding: padding),
sliver: addPaddingSliver(padding: padding),
);
}
}

View File

@ -0,0 +1,23 @@
import '../model_parser.dart';
class SkywoodCategory {
final int id;
final String name;
const SkywoodCategory({required this.id, required this.name});
factory SkywoodCategory.fromJson(Map<String, dynamic> json) {
return SkywoodCategory(
id: parseInt(json['id']),
name: parseString(json['name']),
);
}
Map<String, dynamic> toJson() {
return {'id': id, 'name': name};
}
SkywoodCategory copyWith({int? id, String? name}) {
return SkywoodCategory(id: id ?? this.id, name: name ?? this.name);
}
}

View File

@ -0,0 +1,111 @@
import '../model_parser.dart';
import 'skywood_drama.dart';
import 'skywood_episode.dart';
class SkywoodDetail {
final SkywoodDrama? videoInfo;
final SkywoodDrama? shortPlayInfo;
final List<SkywoodEpisode> episodeList;
final String? businessModel;
bool? isCollect;
final bool? showShareCoin;
final int? shareCoin;
final int? revolution;
final String? userLevel;
final int? jumpType;
final int? jumpShortPlayId;
final List<int>? checkPoint;
SkywoodDetail({
required this.videoInfo,
this.shortPlayInfo,
required this.episodeList,
this.businessModel,
this.isCollect,
this.showShareCoin,
this.shareCoin,
this.revolution,
this.userLevel,
this.jumpType,
this.jumpShortPlayId,
this.checkPoint,
});
factory SkywoodDetail.fromJson(Map<String, dynamic> json) {
return SkywoodDetail(
videoInfo: parseMap(json['video_info']) == null
? null
: SkywoodDrama.fromJson(parseMap(json['video_info'])!),
shortPlayInfo: parseMap(json['shortPlayInfo']) == null
? null
: SkywoodDrama.fromJson(parseMap(json['shortPlayInfo'])!),
episodeList: parseModelList(json['episodeList'], SkywoodEpisode.fromJson),
businessModel: json['business_model'] as String?,
isCollect: json['is_collect'] == null
? null
: parseBool(json['is_collect']),
showShareCoin: json['show_share_coin'] == null
? null
: parseBool(json['show_share_coin']),
shareCoin: json['share_coin'] == null
? null
: parseInt(json['share_coin']),
revolution: json['revolution'] == null
? null
: parseInt(json['revolution']),
userLevel: json['user_level'] as String?,
jumpType: json['jump_type'] == null ? null : parseInt(json['jump_type']),
jumpShortPlayId: json['jump_short_play_id'] == null
? null
: parseInt(json['jump_short_play_id']),
checkPoint: parseIntList(json['check_point']),
);
}
Map<String, dynamic> toJson() {
return {
'video_info': videoInfo?.toJson(),
'shortPlayInfo': shortPlayInfo?.toJson(),
'episodeList': episodeList.map((item) => item.toJson()).toList(),
'business_model': businessModel,
'is_collect': isCollect,
'show_share_coin': showShareCoin,
'share_coin': shareCoin,
'revolution': revolution,
'user_level': userLevel,
'jump_type': jumpType,
'jump_short_play_id': jumpShortPlayId,
'check_point': checkPoint,
};
}
SkywoodDetail copyWith({
SkywoodDrama? videoInfo,
SkywoodDrama? shortPlayInfo,
List<SkywoodEpisode>? episodeList,
String? businessModel,
bool? isCollect,
bool? showShareCoin,
int? shareCoin,
int? revolution,
String? userLevel,
int? jumpType,
int? jumpShortPlayId,
List<int>? checkPoint,
}) {
return SkywoodDetail(
videoInfo: videoInfo ?? this.videoInfo,
shortPlayInfo: shortPlayInfo ?? this.shortPlayInfo,
episodeList: episodeList ?? this.episodeList,
businessModel: businessModel ?? this.businessModel,
isCollect: isCollect ?? this.isCollect,
showShareCoin: showShareCoin ?? this.showShareCoin,
shareCoin: shareCoin ?? this.shareCoin,
revolution: revolution ?? this.revolution,
userLevel: userLevel ?? this.userLevel,
jumpType: jumpType ?? this.jumpType,
jumpShortPlayId: jumpShortPlayId ?? this.jumpShortPlayId,
checkPoint: checkPoint ?? this.checkPoint,
);
}
}

View File

@ -0,0 +1,183 @@
import '../model_parser.dart';
import 'skywood_category.dart';
class SkywoodDrama {
final int? id;
final int shortId;
int? shortPlayId;
final String name;
final String description;
final String imageUrl;
final String horizontallyImg;
int collectTotal;
final int watchTotal;
final int episodeTotal;
final String? playSeconds;
bool? isCollect;
final String? videoUrl;
final Map<String, dynamic>? videoInfo;
final List<String>? category;
final List<SkywoodCategory>? categoryList;
final String tagType;
int? episode;
final int? process;
final int? buyType;
final int? searchClickTotal;
final int? shortPlayVideoId;
final int? currentEpisode;
int? historyTime;
SkywoodDrama({
this.id,
required this.shortId,
this.shortPlayId,
required this.name,
required this.description,
required this.imageUrl,
required this.horizontallyImg,
required this.collectTotal,
required this.watchTotal,
required this.episodeTotal,
this.playSeconds,
this.isCollect,
this.videoUrl,
this.videoInfo,
this.category,
this.categoryList,
required this.tagType,
this.episode,
this.process,
this.buyType,
this.searchClickTotal,
this.shortPlayVideoId,
this.currentEpisode,
this.historyTime,
});
factory SkywoodDrama.fromJson(Map<String, dynamic> json) {
return SkywoodDrama(
id: json['id'] == null ? null : parseInt(json['id']),
shortId: parseInt(json['short_id']),
shortPlayId: json['short_play_id'] == null
? null
: parseInt(json['short_play_id']),
name: parseString(json['name']),
description: parseString(json['description']),
imageUrl: parseString(json['image_url']),
horizontallyImg: parseString(json['horizontally_img']),
collectTotal: parseInt(json['collect_total']),
watchTotal: parseInt(json['watch_total']),
episodeTotal: parseInt(json['episode_total']),
playSeconds: json['play_seconds'] as String?,
isCollect: json['is_collect'] == null
? null
: parseBool(json['is_collect']),
videoUrl: json['video_url'] as String?,
videoInfo: parseMap(json['video_info']),
category: parseStringList(json['category']),
categoryList: json['categoryList'] == null
? null
: parseModelList(json['categoryList'], SkywoodCategory.fromJson),
tagType: parseString(json['tag_type']),
episode: json['episode'] == null ? null : parseInt(json['episode']),
process: json['process'] == null ? null : parseInt(json['process']),
buyType: json['buy_type'] == null ? null : parseInt(json['buy_type']),
searchClickTotal: json['search_click_total'] == null
? null
: parseInt(json['search_click_total']),
shortPlayVideoId: json['short_play_video_id'] == null
? null
: parseInt(json['short_play_video_id']),
currentEpisode: json['current_episode'] == null
? null
: parseInt(json['current_episode']),
historyTime: json['history_time'] == null
? null
: parseInt(json['history_time']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'short_id': shortId,
'short_play_id': shortPlayId,
'name': name,
'description': description,
'image_url': imageUrl,
'horizontally_img': horizontallyImg,
'collect_total': collectTotal,
'watch_total': watchTotal,
'episode_total': episodeTotal,
'play_seconds': playSeconds,
'is_collect': isCollect,
'video_url': videoUrl,
'video_info': videoInfo,
'category': category,
'categoryList': categoryList?.map((item) => item.toJson()).toList(),
'tag_type': tagType,
'episode': episode,
'process': process,
'buy_type': buyType,
'search_click_total': searchClickTotal,
'short_play_video_id': shortPlayVideoId,
'current_episode': currentEpisode,
'history_time': historyTime,
};
}
SkywoodDrama copyWith({
int? id,
int? shortId,
int? shortPlayId,
String? name,
String? description,
String? imageUrl,
String? horizontallyImg,
int? collectTotal,
int? watchTotal,
int? episodeTotal,
String? playSeconds,
bool? isCollect,
String? videoUrl,
Map<String, dynamic>? videoInfo,
List<String>? category,
List<SkywoodCategory>? categoryList,
String? tagType,
int? episode,
int? process,
int? buyType,
int? searchClickTotal,
int? shortPlayVideoId,
int? currentEpisode,
int? historyTime,
}) {
return SkywoodDrama(
id: id ?? this.id,
shortId: shortId ?? this.shortId,
shortPlayId: shortPlayId ?? this.shortPlayId,
name: name ?? this.name,
description: description ?? this.description,
imageUrl: imageUrl ?? this.imageUrl,
horizontallyImg: horizontallyImg ?? this.horizontallyImg,
collectTotal: collectTotal ?? this.collectTotal,
watchTotal: watchTotal ?? this.watchTotal,
episodeTotal: episodeTotal ?? this.episodeTotal,
playSeconds: playSeconds ?? this.playSeconds,
isCollect: isCollect ?? this.isCollect,
videoUrl: videoUrl ?? this.videoUrl,
videoInfo: videoInfo ?? this.videoInfo,
category: category ?? this.category,
categoryList: categoryList ?? this.categoryList,
tagType: tagType ?? this.tagType,
episode: episode ?? this.episode,
process: process ?? this.process,
buyType: buyType ?? this.buyType,
searchClickTotal: searchClickTotal ?? this.searchClickTotal,
shortPlayVideoId: shortPlayVideoId ?? this.shortPlayVideoId,
currentEpisode: currentEpisode ?? this.currentEpisode,
historyTime: historyTime ?? this.historyTime,
);
}
}

View File

@ -0,0 +1,92 @@
import '../model_parser.dart';
class SkywoodEpisode {
final int id;
final int shortId;
final int shortPlayId;
final int shortPlayVideoId;
final int episode;
final String videoUrl;
final int? promiseViewAd;
final int iaaPopUp;
bool isLock;
String playSeconds;
int? viewAdCount;
SkywoodEpisode({
required this.id,
required this.shortId,
required this.shortPlayId,
required this.shortPlayVideoId,
required this.episode,
required this.videoUrl,
required this.playSeconds,
required this.isLock,
this.promiseViewAd,
required this.iaaPopUp,
required this.viewAdCount,
});
factory SkywoodEpisode.fromJson(Map<String, dynamic> json) {
return SkywoodEpisode(
id: parseInt(json['id']),
shortId: parseInt(json['short_id']),
shortPlayId: parseInt(json['short_play_id']),
shortPlayVideoId: parseInt(json['short_play_video_id']),
episode: parseInt(json['episode']),
videoUrl: parseString(json['video_url']),
playSeconds: parseString(json['play_seconds']),
isLock: parseBool(json['is_lock']),
promiseViewAd: json['promise_view_ad'] == null
? null
: parseInt(json['promise_view_ad']),
iaaPopUp: parseInt(json['iaa_pop_up']),
viewAdCount: parseInt(json['view_ad_count']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'short_id': shortId,
'short_play_id': shortPlayId,
'short_play_video_id': shortPlayVideoId,
'episode': episode,
'video_url': videoUrl,
'promise_view_ad': promiseViewAd,
'iaa_pop_up': iaaPopUp,
'is_lock': isLock,
'play_seconds': playSeconds,
'view_ad_count': viewAdCount,
};
}
SkywoodEpisode copyWith({
int? id,
int? shortId,
int? shortPlayId,
int? shortPlayVideoId,
int? episode,
String? videoUrl,
bool? isLock,
String? playSeconds,
String? durationSeconds,
int? promiseViewAd,
int? viewAdCount,
int? iaaPopUp,
}) {
return SkywoodEpisode(
id: id ?? this.id,
shortId: shortId ?? this.shortId,
shortPlayId: shortPlayId ?? this.shortPlayId,
shortPlayVideoId: shortPlayVideoId ?? this.shortPlayVideoId,
episode: episode ?? this.episode,
videoUrl: videoUrl ?? this.videoUrl,
isLock: isLock ?? this.isLock,
playSeconds: playSeconds ?? this.playSeconds,
promiseViewAd: promiseViewAd ?? this.promiseViewAd,
viewAdCount: viewAdCount ?? this.viewAdCount,
iaaPopUp: iaaPopUp ?? this.iaaPopUp,
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:skywood/utils/models/drama/skywood_drama.dart';
class SkywoodHome {
final String? moduleKey;
final List<SkywoodDrama> dataList;
const SkywoodHome({required this.moduleKey, required this.dataList});
factory SkywoodHome.fromJson(Map<String, dynamic> json) {
// Map<String, dynamic>? map = json['data'];
// List? list = map!['list'];
// List<SkywoodDrama> datas = [];
// if (list != null) {
// datas = list.map((e) => SkywoodDrama.fromJson(e)).toList();
// }
final data = json['data'];
List<SkywoodDrama> videos = [];
// 🔥 data
if (data is Map<String, dynamic>) {
// 1: data Map list
final list = data['list'];
if (list is List) {
videos = list.map((e) => SkywoodDrama.fromJson(e)).toList();
}
} else if (data is List) {
// 2: data List
videos = data.map((e) => SkywoodDrama.fromJson(e)).toList();
}
return SkywoodHome(
moduleKey: json['module_key'] as String?,
dataList: videos,
);
}
Map<String, dynamic> toJson() {
return {'module_key': moduleKey, 'dataList': dataList};
}
SkywoodHome copyWith({String? moduleKey, dynamic data}) {
return SkywoodHome(
moduleKey: moduleKey ?? this.moduleKey,
dataList: data ?? dataList,
);
}
}

View File

@ -0,0 +1,57 @@
bool parseBool(dynamic value) {
if (value == null) return false;
if (value is bool) return value;
if (value is num) return value != 0;
if (value is String) {
final v = value.trim().toLowerCase();
return v == 'true' || v == '1' || v == 'yes' || v == 'y';
}
return false;
}
int parseInt(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is double) return value.toInt();
if (value is num) return value.toInt();
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
String parseString(dynamic value) {
if (value == null) return '';
return value.toString();
}
List<String>? parseStringList(dynamic value) {
if (value == null) return null;
if (value is List) {
return value.map((item) => parseString(item)).toList();
}
return null;
}
List<int>? parseIntList(dynamic value) {
if (value == null) return null;
if (value is List) {
return value.map((item) => parseInt(item)).toList();
}
return null;
}
Map<String, dynamic>? parseMap(dynamic value) {
if (value is Map<String, dynamic>) return value;
if (value is Map) return Map<String, dynamic>.from(value);
return null;
}
List<T> parseModelList<T>(
dynamic value,
T Function(Map<String, dynamic> json) fromJson,
) {
if (value is! List) return <T>[];
return value
.whereType<Map>()
.map((item) => fromJson(Map<String, dynamic>.from(item)))
.toList();
}

View File

@ -0,0 +1,59 @@
import '../model_parser.dart';
class GlimzoUser {
final String customerId;
final bool isTourist;
final String userLevel;
final String avator;
final String? familyName;
final String? givingName;
const GlimzoUser({
required this.customerId,
required this.isTourist,
required this.userLevel,
required this.avator,
this.familyName,
this.givingName,
});
factory GlimzoUser.fromJson(Map<String, dynamic> json) {
return GlimzoUser(
customerId: parseString(json['customer_id']),
isTourist: parseBool(json['is_tourist']),
userLevel: parseString(json['user_level']),
avator: parseString(json['avator']),
familyName: json['family_name'] as String?,
givingName: parseString(json['giving_name']),
);
}
Map<String, dynamic> toJson() {
return {
'customer_id': customerId,
'is_tourist': isTourist,
'user_level': userLevel,
'avator': avator,
'family_name': familyName,
'giving_name': givingName,
};
}
GlimzoUser copyWith({
String? customerId,
bool? isTourist,
String? userLevel,
String? avator,
String? familyName,
String? givingName,
}) {
return GlimzoUser(
customerId: customerId ?? this.customerId,
isTourist: isTourist ?? this.isTourist,
userLevel: userLevel ?? this.userLevel,
avator: avator ?? this.avator,
familyName: familyName ?? this.familyName,
givingName: givingName ?? this.givingName,
);
}
}

View File

@ -0,0 +1,18 @@
class RegisterBean {
final String token;
final int customerId;
final bool? autoLogin;
final int touristId;
RegisterBean({required this.token, required this.customerId, this.autoLogin, required this.touristId});
factory RegisterBean.fromJson(Map<String, dynamic> json) {
return RegisterBean(
token: json['token'],
customerId: json['customer_id'],
autoLogin: json['auto_login'],
touristId: json['tourist_id'],
);
}
}

View File

@ -4,8 +4,8 @@ import 'package:get/get.dart';
/// AppScreen class
class AppScreen {
static double get screenWidth => Get.width;
static double get screenHeight => Get.height;
static double get screenWidth => _viewSize.width;
static double get screenHeight => _viewSize.height;
// static double get screenWidth => MediaQuery.of(Get.context!).size.width;
// static double get screenHeight => MediaQuery.of(Get.context!).size.height;
@ -13,7 +13,7 @@ class AppScreen {
return MediaQuery.of(context).size;
}
static bool get isDebug=> kDebugMode;
static bool get isDebug => kDebugMode;
static double get statusBarHeight => Get.statusBarHeight / Get.pixelRatio;
@ -33,12 +33,19 @@ class AppScreen {
return GetPlatform.isIOS;
}
static Size get _viewSize {
if (Get.context != null) {
return MediaQuery.of(Get.context!).size;
}
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
static double aRatioWidth({double? h, double aspectRatio = 1}) {
h ??= screenHeight;
return screenHeight * 1 / aspectRatio;
}
static double get keyboardHeight => Get.mediaQuery.viewInsets.bottom;
}

View File

@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
// import 'package:lottie/lottie.dart';
//
typedef LoadBlock<T> = Future<T> Function();
// Toast提示的类
class AppToast {
static const String _defaultLoadingText = "";
static const String _loadingText = "loading...";
/// Toast
static Future<void> showToast(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) async {
await dismiss();
return SmartDialog.showToast(
msg.toString(),
alignment: Alignment.center,
debounce: true,
displayTime: displayTime,
);
}
static Future<void> showError(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) {
return showFail(msg, displayTime: displayTime);
}
///
static Future<void> showFail(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
Function()? onDismiss,
}) async {
await dismiss();
return SmartDialog.showNotify(
msg: msg.toString(),
alignment: Alignment.center,
notifyType: NotifyType.failure,
displayTime: displayTime,
debounce: true,
onDismiss: onDismiss,
);
}
///
static Future<T?> showSuccess<T>(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) async {
await dismiss();
return SmartDialog.showNotify<T>(
msg: msg.toString(),
notifyType: NotifyType.success,
debounce: true,
displayTime: displayTime,
);
}
static Future<T?> showInfo<T>(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) {
return showWarning<T>(msg, displayTime: displayTime);
}
static Future<T?> showWarning<T>(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) async {
await dismiss();
return SmartDialog.showNotify<T>(
msg: msg.toString(),
notifyType: NotifyType.warning,
debounce: true,
displayTime: displayTime,
);
}
/// Lottie
// static Future<T?> showLottieLoading<T>({
// Duration? displayTime,
// }) async {
// await dismiss();
// return SmartDialog.showLoading<T>(
// displayTime: displayTime ?? const Duration(seconds: 5),
// maskColor: Colors.transparent,
// builder: (_) {
// return SizedBox(
// width: 120,
// height: 120,
// child: Lottie.asset(
// 'assets/bagua.json',
// fit: BoxFit.contain,
// repeat: true,
// ),
// );
// },
// );
// }
///
static Future<T?> showLoading<T>({
String text = _loadingText,
Duration displayTime = const Duration(seconds: 10),
}) async {
await dismiss();
return SmartDialog.showLoading<T>(msg: text, displayTime: displayTime);
}
///
static Future<void> dismiss<T>({SmartStatus status = SmartStatus.loading}) {
return SmartDialog.dismiss<T>(status: status);
}
///
static void dismissAll() {
SmartDialog.dismiss();
}
///
static Future<T> loading<T>(
LoadBlock<T> block, {
String? text,
bool isLoading = true,
}) async {
if (isLoading) {
showLoading(text: text ?? _defaultLoadingText);
}
try {
final result = await block();
return result;
} finally {
dismiss();
}
}
}

View File

@ -18,6 +18,22 @@ class ConstImg {
static const String tabMineUnselect = "assets/common/tab_mine_unselect.png";
static const String homeBg = "assets/imgs/home_bg.png";
// home_card_bg
static const String homeCardBg = "assets/imgs/home_card_bg.png";
// home_top_bg
static const String homeTopBg = "assets/imgs/home_top_bg.png";
// home_title_daily/hot/hot_cate/mystery/weekly
static const String homeTitleDaily = "assets/imgs/home_title_daily.png";
static const String homeTitleHot = "assets/imgs/home_title_hot.png";
static const String homeTitleHotCate = "assets/imgs/home_title_hot_cate.png";
static const String homeTitleMystery = "assets/imgs/home_title_mystery.png";
static const String homeTitleWeekly = "assets/imgs/home_title_weekly.png";
static const String homeOpenNowBox = "assets/imgs/home_opennow_box.png";
// splash_page
static const String splashPage = "assets/imgs/splash_page.png";
// heart
static const String heart = "assets/images/heart.png";
// nodata
static const String noData = "assets/images/nodata.png";

View File

@ -0,0 +1,117 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
/// navigator
class HelpNav {
/// clear all route
static Future<T?>? offAllNamed<T>(
String newRouteName, {
Map? arguments,
RoutePredicate? predicate,
String? tag,
Map? parameters,
}) {
return Get.offAllNamed<T>(
newRouteName,
arguments: arguments,
predicate: predicate,
parameters: {...?parameters},
);
}
static Future<T?>? off<T>(Widget page, {arguments, preventDuplicates}) {
return Get.off<T>(
() => page,
arguments: arguments,
preventDuplicates: preventDuplicates ?? false,
);
}
static Future<T?>? offAll<T>(Widget page, {arguments, preventDuplicates}) {
return Get.offAll<T>(() => page, arguments: arguments);
}
static Future<T?>? offNamed<T>(
String newRouteName, {
arguments,
preventDuplicates,
}) {
return Get.offNamed<T>(
newRouteName,
arguments: arguments,
preventDuplicates: preventDuplicates ?? true,
);
}
/// to name
static Future<T?>? toNamed<T>(
String newRouteName, {
arguments,
String? tag,
Map? parameters,
}) {
return Get.toNamed<T>(
newRouteName,
arguments: arguments,
parameters: {...?parameters},
);
}
/// to route
static Future<T?>? to<T>(Widget page, {arguments, binding}) {
return Get.to<T>(
() => page,
arguments: arguments,
binding: binding,
preventDuplicates: false,
);
}
static void _until<T>({String? routeName, T? result}) {
if (result == null) {
Get.back<T>(result: result);
return;
}
// Get.back<T>(result: result);
//
if (routeName == null) {
Get.back<T>(result: result);
return;
}
}
/// back pre
/// [result]
static void back<T>({T? result, String? routeName}) {
_until(routeName: routeName, result: result);
}
/// show dialog
static Future<T?> showDialog<T>(
Widget child, {
bool barrierDismissible = false,
}) {
return Get.dialog<T>(
Center(child: child),
barrierDismissible: barrierDismissible,
);
}
/// show bottom sheet
static Future<T?> showBottom<T>(
Widget child, {
Color? safeAreaColor,
double? borderRadius,
bool isDismissible = false,
bool isScrollControlled = false,
bool? enableDrag,
}) {
return Get.bottomSheet<T>(
child,
isScrollControlled: isScrollControlled,
enableDrag: enableDrag ?? true,
isDismissible: isDismissible,
);
}
}

View File

@ -0,0 +1,177 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:android_id/android_id.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:skywood/global/request/api_const.dart';
@immutable
class SkyDeviceInfo {
final String deviceId;
final String systemType;
final String systemVersion;
final String brand;
final String model;
final String appName;
final String appPackageName;
final String appVersion;
final String idfv;
static SkyDeviceInfo? _instance;
const SkyDeviceInfo._({
required this.deviceId,
required this.systemType,
required this.systemVersion,
required this.brand,
required this.model,
required this.appName,
required this.appPackageName,
required this.appVersion,
required this.idfv,
});
static SkyDeviceInfo get instance {
if (_instance == null) {
throw StateError('error');
}
return _instance!;
}
@visibleForTesting
static void setMockInstance({
String deviceId = 'test-device-id',
String systemType = 'test',
String systemVersion = '1.0',
String brand = 'test',
String model = 'test',
String appName = 'Glimzo',
String appPackageName = 'com.example.skywood',
String appVersion = '1.0.0',
String idfv = 'test-idfv',
}) {
_instance = SkyDeviceInfo._(
deviceId: deviceId,
systemType: systemType,
systemVersion: systemVersion,
brand: brand,
model: model,
appName: appName,
appPackageName: appPackageName,
appVersion: appVersion,
idfv: idfv,
);
}
///
static Future<void> init() async {
if (_instance != null) return;
String deviceId = '',
systemType = '',
systemVersion = '',
brand = '',
model = '',
appName = 'Glimzo',
appPackageName = '',
appVersion = '',
idfv = '';
//
await _initAppInfo()
.then((info) {
appPackageName = info['packageName'] ?? '';
appVersion = info['version'] ?? '';
appName = ApiConst.appName;
})
.catchError((e) {
debugPrint('[sDeviceInfo] getElPackageName: $e');
});
//
if (Platform.isAndroid) {
await _initAndroidInfo()
.then((info) {
deviceId = info['deviceId'] ?? '';
systemType = 'android';
systemVersion = info['systemVersion'] ?? '';
brand = info['brand'] ?? '';
model = info['model'] ?? '';
})
.catchError((e) {
debugPrint('[GlimzoUtilDeviceInfo] Android: $e');
});
} else if (Platform.isIOS) {
await _initIOSInfo()
.then((info) {
deviceId = info['deviceId'] ?? '';
idfv = info['idfv'] ?? '';
systemType = 'ios';
systemVersion = info['systemVersion'] ?? '';
brand = info['brand'] ?? '';
model = info['model'] ?? '';
})
.catchError((e) {
debugPrint('[GlimzoUtilDeviceInfo] iOS: $e');
});
}
_instance = SkyDeviceInfo._(
deviceId: deviceId,
systemType: systemType,
systemVersion: systemVersion,
brand: brand,
model: model,
appName: appName,
appPackageName: appPackageName,
appVersion: appVersion,
idfv: idfv,
);
}
///
static Future<Map<String, String>> _initAppInfo() async {
final packageInfo = await PackageInfo.fromPlatform();
return {
'packageName': packageInfo.packageName,
'version': packageInfo.version,
};
}
/// Android
static Future<Map<String, String?>> _initAndroidInfo() async {
final plugin = DeviceInfoPlugin();
final androidInfo = await plugin.androidInfo;
final androidIdPlugin = AndroidId();
final androidId = await androidIdPlugin.getId();
return {
'deviceId': androidId ?? androidInfo.id,
'systemVersion': androidInfo.version.release,
'brand': androidInfo.brand,
'model': androidInfo.model,
};
}
/// iOS
static Future<Map<String, String?>> _initIOSInfo() async {
final plugin = DeviceInfoPlugin();
final iosInfo = await plugin.iosInfo;
final secureStorage = FlutterSecureStorage();
String? idfvStore = await secureStorage.read(key: 'identifierForVendor');
idfvStore ??= iosInfo.identifierForVendor;
if (idfvStore != null) {
await secureStorage.write(key: 'identifierForVendor', value: idfvStore);
}
return {
'deviceId': idfvStore,
'idfv': iosInfo.identifierForVendor ?? '',
'systemVersion': iosInfo.systemVersion,
'brand': iosInfo.model,
'model': iosInfo.modelName,
};
}
}

View File

@ -0,0 +1,53 @@
class SkyTimeTool {
static String getTimeZone() {
final offset = DateTime.now().timeZoneOffset;
final hours = offset.inHours;
final minutes = offset.inMinutes.remainder(60).abs();
final sign = hours >= 0 ? '+' : '-';
final formatted =
'GMT$sign${hours.abs().toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}';
return formatted;
}
// k / m
static String formatNum(
num value, {
int decimalDigits = 1,
String separator = ',',
}) {
String removeTrailingZeros(String str) {
if (str.contains('.')) {
str = str.replaceAll(RegExp(r'0+$'), ''); // 0
str = str.replaceAll(RegExp(r'\.$'), ''); //
}
return str;
}
String addSeparator(String str) {
final parts = str.split('.');
final intPart = parts[0];
final decPart = parts.length > 1 ? '.${parts[1]}' : '';
final regex = RegExp(r'(\d+)(\d{3})');
var formatted = intPart;
while (regex.hasMatch(formatted)) {
formatted = formatted.replaceAllMapped(
regex,
(m) => '${m[1]}$separator${m[2]}',
);
}
return '$formatted$decPart';
}
if (value < 1000) {
return addSeparator(value.toStringAsFixed(0));
} else if (value < 1000000) {
final formatted = (value / 1000).toStringAsFixed(decimalDigits);
return '${removeTrailingZeros(formatted)}K';
} else {
final formatted = (value / 1000000).toStringAsFixed(decimalDigits);
return '${removeTrailingZeros(formatted)}M';
}
}
}

View File

@ -0,0 +1,234 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:skywood/global/client/user_client.dart';
/// Token
/// GlimzoUtilToken
class TokenManager {
// ============ ============
static final TokenManager _instance = TokenManager._internal();
factory TokenManager() => _instance;
TokenManager._internal();
// ============ ============
static const String _keyToken = 'SKY_STORE_TOKEN';
static const String _keyUserInfo = 'SKY_STORE_USER_INFO';
// ============ ============
String? _cachedToken;
Map<String, dynamic>? _cachedUserInfo;
// ============ ============
/// Token
String get() {
if (_cachedToken != null) {
return _cachedToken!;
}
//
_cachedToken = _getFromStorage(_keyToken);
return _cachedToken ?? '';
}
/// Token
void save(String token) {
if (token.isNotEmpty) {
_cachedToken = token;
_saveToStorage(_keyToken, token);
}
}
/// Token
void clear() {
_cachedToken = null;
_removeFromStorage(_keyToken);
_cachedUserInfo = null;
_removeFromStorage(_keyUserInfo);
}
/// Token Token
/// GlimzoRequestUser
Future<void> update() async {
try {
final res = await UserClient.register();
if (res == null) return;
final newToken = res.token;
if (newToken.isNotEmpty ?? false) {
// Token
save(newToken);
//
// if (res['userInfo'] != null) {
// saveUserInfo(res['userInfo']);
// }
} else {
throw Exception('Invalid token response');
}
} catch (e) {
debugPrint('[TokenManager] Error updating token: $e');
rethrow;
}
}
// ============ 便 ============
/// Token
bool get isEmpty => get().isEmpty;
/// Token
bool get isNotEmpty => !isEmpty;
/// Authorization
String? get authorizationHeader {
final token = get();
if (token.isNotEmpty) {
return 'Bearer $token';
}
return null;
}
// ============ ============
///
void saveUserInfo(Map<String, dynamic> userInfo) {
_cachedUserInfo = userInfo;
_saveToStorage(_keyUserInfo, jsonEncode(userInfo));
}
///
Map<String, dynamic>? getUserInfo() {
if (_cachedUserInfo != null) {
return _cachedUserInfo;
}
final stored = _getFromStorage(_keyUserInfo);
if (stored != null && stored.isNotEmpty) {
try {
_cachedUserInfo = jsonDecode(stored) as Map<String, dynamic>;
return _cachedUserInfo;
} catch (_) {
return null;
}
}
return null;
}
// ============ ============
String? _getFromStorage(String key) {
try {
// 使 SharedPreferences SharedPreferences
// 使
//
return _getFromStorageSync(key);
} catch (e) {
debugPrint('[TokenManager] Error reading from storage: $e');
return null;
}
}
// SharedPreferences
String? _getFromStorageSync(String key) {
// SharedPreferences 使
// main init()
try {
// null
// 使
return null;
} catch (_) {
return null;
}
}
void _saveToStorage(String key, String value) {
//
_saveToStorageAsync(key, value);
}
void _removeFromStorage(String key) {
//
_removeFromStorageAsync(key);
}
// ============ 使 ============
/// Token
Future<void> init() async {
try {
final prefs = await SharedPreferences.getInstance();
_cachedToken = prefs.getString(_keyToken);
final userInfoStr = prefs.getString(_keyUserInfo);
if (userInfoStr != null && userInfoStr.isNotEmpty) {
try {
_cachedUserInfo = jsonDecode(userInfoStr) as Map<String, dynamic>;
} catch (_) {
_cachedUserInfo = null;
}
}
} catch (e) {
debugPrint('[TokenManager] Init error: $e');
}
}
/// Token
Future<String> getAsync() async {
if (_cachedToken != null) {
return _cachedToken!;
}
try {
final prefs = await SharedPreferences.getInstance();
_cachedToken = prefs.getString(_keyToken);
return _cachedToken ?? '';
} catch (e) {
debugPrint('[TokenManager] Get token error: $e');
return '';
}
}
/// Token
Future<void> saveAsync(String token) async {
if (token.isNotEmpty) {
_cachedToken = token;
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyToken, token);
} catch (e) {
debugPrint('[TokenManager] Save token error: $e');
}
}
}
/// Token
Future<void> clearAsync() async {
_cachedToken = null;
_cachedUserInfo = null;
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_keyToken);
await prefs.remove(_keyUserInfo);
} catch (e) {
debugPrint('[TokenManager] Clear token error: $e');
}
}
//
Future<void> _saveToStorageAsync(String key, String value) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
} catch (e) {
debugPrint('[TokenManager] Save to storage error: $e');
}
}
//
Future<void> _removeFromStorageAsync(String key) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
} catch (e) {
debugPrint('[TokenManager] Remove from storage error: $e');
}
}
}

View File

@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
import '../../../global/const/common_color.dart';
import '../../extend/widget_extend.dart';
import '../help_nav.dart';
class SkyText extends StatelessWidget {
const SkyText({
super.key,
required this.text,
this.color = Colors.white,
this.fontSize = 14,
this.shadow,
this.decoration,
this.decorationColor,
this.textAlign,
this.fontWeight,
this.maxLines,
this.lineHeight,
this.letterSpacing,
this.overflow = TextOverflow.ellipsis,
this.softWrap,
this.fontFamily,
});
final String text;
final Color? color;
final double? fontSize;
final TextDecoration? decoration;
final Color? decorationColor;
final TextAlign? textAlign;
final FontWeight? fontWeight;
final int? maxLines;
final Shadow? shadow;
final double? lineHeight;
final double? letterSpacing;
final TextOverflow? overflow;
final bool? softWrap;
final String? fontFamily;
@override
Widget build(BuildContext context) {
final FontWeight resolvedWeight = fontWeight ?? FontWeight.normal;
return Text(text, style: TextStyle(
fontSize: fontSize,
fontWeight: resolvedWeight,
decoration: decoration,
fontFamily: fontFamily ?? 'Inter',
color: color,
shadows: shadow == null ? null : [shadow!],
decorationColor: decorationColor,
height: lineHeight,
letterSpacing: letterSpacing,
), maxLines: maxLines,
overflow: overflow,
softWrap: softWrap,
textAlign: textAlign,
);
}
}
class ComConfirmWidget extends StatelessWidget {
final String? title;
final String? content;
final Widget? contentWidget;
final String? cancelStr, confirmStr;
final double? height;
final Function? onConfirmed;
final Function? onCanceled;
final bool? showCancel;
final bool? clickHideAlert;
const ComConfirmWidget({
super.key,
this.title,
this.content,
this.contentWidget,
this.cancelStr,
this.confirmStr,
this.height,
this.onConfirmed,
this.onCanceled,
this.showCancel = true,
this.clickHideAlert = true,
});
@override
Widget build(BuildContext context) {
return Container(
height: height ?? 160,
margin: EdgeInsets.only(left: 25, right: 25),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Column(
// mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 15,),
title==null ? SizedBox() : Padding(padding: EdgeInsets.only(left:15, right: 15, bottom: 5),
child: SkyText(text: title!, fontSize: 16, color: Colors.black,
)
),
const SizedBox(height: 10),
Expanded(
child: Padding(padding: EdgeInsets.only(left:14, right: 14),
child: contentWidget ?? Center(
child: SkyText(text: content!,
fontSize: 15, color: CommonColor.black,
maxLines: 3),
),
)
),
const SizedBox(height: 10),
Divider(thickness: 0.6, height: 1),
SizedBox(
height: 44,
child: IntrinsicHeight(
child: showCancel! ? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(onPressed: () {
Navigator.pop(context);
onCanceled?.call();
},
child: Center(
child: SkyText(text: cancelStr ?? 'Cancel',
color: Colors.grey, fontSize: 14))
),
VerticalDivider(width: 5, thickness: 0.6,),
TextButton(onPressed: () {
if (clickHideAlert == true) {
Navigator.pop(context);
}
onConfirmed?.call();
},
child: Center(
child:SkyText(text: confirmStr ?? 'Confirm',
color: CommonColor.goldColor, fontSize: 14))
),
],
) : TextButton(onPressed: () {
Navigator.pop(context);
onConfirmed?.call();
},
child: Center(
child:SkyText(text: confirmStr ?? 'Confirm',
color: CommonColor.black, fontSize: 14,))
),
)
)
],
),
);
}
}
class ComSheetWidget extends StatelessWidget {
final Function(int)? onSelected;
final String? title;
final List<String> items;
final TextAlign? textAlign;
const ComSheetWidget({super.key,
this.onSelected,
this.title,
this.textAlign = TextAlign.left,
required this.items,
});
@override
Widget build(BuildContext context) {
return SafeArea(child: _mainView());
}
_mainView() {
return Container(
padding: EdgeInsets.only(top: 10),
height: items.length>5 ? 360 : 40+items.length*47 + 50,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20)
),
),
child: _ifElseWidget(),
);
}
_ifElseWidget() {
if (title != null) {
return Column(
children: [
const SizedBox(height: 6),
Text('$title', style: TextStyle(
color: Colors.black, fontSize: 16
)),
const SizedBox(height: 5),
const Divider(height: 8, thickness: 0.6,),
// Divider(),
const SizedBox(height: 5),
Expanded(child: ListView.separated(itemBuilder: (context, index) {
return _itemCreateer(index);
}, separatorBuilder: (context, index) {
return Divider( indent: 15, endIndent: 15, thickness: 0.6,);
}, itemCount: items.length))
],
);
} else {
return ListView.separated(itemBuilder: (context, index) {
return _itemCreateer(index);
}, separatorBuilder: (context, index) {
return Divider(indent: 15, endIndent: 15, thickness: 0.6,);
}, itemCount: items.length);
}
}
_itemCreateer(int index) {
return SizedBox(
height: 47,
child: Center(
child: Text(items[index], style: TextStyle(
fontSize: 15, color: Colors.black,
)),
).addInkWell(
onTap: () {
HelpNav.back();
onSelected?.call(index);
})
);
}
}

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.1"
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
args:
dependency: transitive
description:
@ -512,6 +520,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
group_button:
dependency: transitive
description:
name: group_button
sha256: "0610fcf28ed122bfb4b410fce161a390f7f2531d55d1d65c5375982001415940"
url: "https://pub.dev"
source: hosted
version: "5.3.4"
hooks:
dependency: transitive
description:
@ -904,6 +920,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
share_plus:
dependency: transitive
description:
name: share_plus
sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa"
url: "https://pub.dev"
source: hosted
version: "12.0.2"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
shared_preferences:
dependency: "direct main"
description:
@ -1069,6 +1101,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.0+1"
talker:
dependency: transitive
description:
name: talker
sha256: f1a14d623f1d1bec42bb3bb77674eb766ffe8d26e5f79af652d85cb097c3e757
url: "https://pub.dev"
source: hosted
version: "5.1.17"
talker_dio_logger:
dependency: "direct dev"
description:
name: talker_dio_logger
sha256: "6dba5c29afb566c6efe1a2c1b676488ea7c727b486bda0654d965a1cfad6ea9b"
url: "https://pub.dev"
source: hosted
version: "5.1.17"
talker_flutter:
dependency: "direct main"
description:
name: talker_flutter
sha256: "54cbbf852101721664faf4a05639fd2fdefdc37178327990abea00390690d4bc"
url: "https://pub.dev"
source: hosted
version: "5.1.16"
talker_logger:
dependency: transitive
description:
name: talker_logger
sha256: "459205c3e571f97ecc6be6e1b1b7e6b97b853e78ea458894650be407596e3216"
url: "https://pub.dev"
source: hosted
version: "5.1.17"
term_glyph:
dependency: transitive
description:

View File

@ -49,6 +49,7 @@ dependencies:
dio: ^5.9.2
shared_preferences: ^2.5.3
flutter_smart_dialog: ^5.1.0
talker_flutter: ^5.0.2
dev_dependencies:
flutter_test:
@ -56,6 +57,8 @@ dev_dependencies:
flutter_lints: ^6.0.0
talker_dio_logger: ^5.0.2
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -1,31 +1,33 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:skywood/main.dart';
import 'package:skywood/main/main_root.dart';
import 'package:skywood/pages/home/home_page.dart';
import 'package:skywood/utils/tools/help_img.dart';
import 'helpers/app_test_harness.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
testWidgets('ComApp renders the configured root tabs', (
WidgetTester tester,
) async {
await prepareAppTestHarness();
tester.view.physicalSize = const Size(375, 812);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(const ComApp(home: MainRoot()));
await tester.pump(const Duration(seconds: 1));
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
expect(find.byType(HomePage, skipOffstage: false), findsOneWidget);
expect(
find.image(const AssetImage(ConstImg.tabHomeSelected)),
findsOneWidget,
);
expect(
find.image(const AssetImage(ConstImg.tabSearchUnselect)),
findsOneWidget,
);
});
}