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

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

No files matched your search

+13 -38
View File
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
// 扩展Widget类以添加额外的布局和样式方法
extension WidgetExtend on Widget {
/// 在当前Widget外层添加一个Container
@@ -43,7 +42,8 @@ extension WidgetExtend on Widget {
padding: padding,
alignment: alignment,
clipBehavior: clipBehavior,
constraints: constraints ??
constraints:
constraints ??
BoxConstraints(
minWidth: minWidth ?? 0.0,
maxWidth: maxWidth ?? double.infinity,
@@ -59,8 +59,9 @@ extension WidgetExtend on Widget {
image: image,
gradient: gradient,
border: border,
borderRadius:
radius != null ? BorderRadius.circular(radius) : borderRadius,
borderRadius: radius != null
? BorderRadius.circular(radius)
: borderRadius,
),
child: this,
).addInkWell(onTap: onTap);
@@ -84,10 +85,7 @@ extension WidgetExtend on Widget {
///
/// 通过此方法可以设置控件的对齐方式
Widget addAlign({AlignmentGeometry alignment = Alignment.centerLeft}) {
return Align(
alignment: alignment,
child: this,
);
return Align(alignment: alignment, child: this);
}
Widget addCenter() {
@@ -118,20 +116,14 @@ extension WidgetExtend on Widget {
///
/// 通过此方法可以控制控件的显示和隐藏
Widget addVisibility({bool visible = true}) {
return Visibility(
visible: visible,
child: this,
);
return Visibility(visible: visible, child: this);
}
/// 在当前Widget外层添加一个Transform.rotate
///
/// 通过此方法可以设置控件的旋转角度
Widget addRotate({double angle = 0}) {
return Transform.rotate(
angle: angle,
child: this,
);
return Transform.rotate(angle: angle, child: this);
}
/// 在当前Widget外层添加一个InkWell
@@ -141,44 +133,27 @@ extension WidgetExtend on Widget {
if (onTap == null) {
return this;
}
return InkWell(
onTap: onTap,
child: this,
);
return InkWell(onTap: onTap, child: this);
}
/// 在当前Widget外层添加一个SizedBox
///
/// 通过此方法可以设置控件的宽高
Widget addSize({double? width, double? height}) {
return SizedBox(
width: width,
height: height,
child: this,
);
return SizedBox(width: width, height: height, child: this);
}
/// 在当前Widget外层添加一个Opacity
///
/// 通过此方法可以设置控件的透明度
Widget addOpacity({double opacity = 1}) {
return Opacity(
opacity: opacity,
child: this,
);
return Opacity(opacity: opacity, child: this);
}
}
extension OnRenderObjectWidget on RenderObjectWidget {
SliverPadding addPaddingSliver({EdgeInsets? padding}) {
return SliverPadding(
padding: padding ?? EdgeInsets.zero,
sliver: this,
);
return SliverPadding(padding: padding ?? EdgeInsets.zero, sliver: this);
}
DecoratedSliver addDecoratedSliver({
@@ -199,7 +174,7 @@ extension OnRenderObjectWidget on RenderObjectWidget {
gradient: gradient,
shape: shape ?? BoxShape.rectangle,
),
sliver: this.addPaddingSliver(padding: padding),
sliver: addPaddingSliver(padding: padding),
);
}
}
@@ -0,0 +1,23 @@
import '../model_parser.dart';
class SkywoodCategory {
final int id;
final String name;
const SkywoodCategory({required this.id, required this.name});
factory SkywoodCategory.fromJson(Map<String, dynamic> json) {
return SkywoodCategory(
id: parseInt(json['id']),
name: parseString(json['name']),
);
}
Map<String, dynamic> toJson() {
return {'id': id, 'name': name};
}
SkywoodCategory copyWith({int? id, String? name}) {
return SkywoodCategory(id: id ?? this.id, name: name ?? this.name);
}
}
+111
View File
@@ -0,0 +1,111 @@
import '../model_parser.dart';
import 'skywood_drama.dart';
import 'skywood_episode.dart';
class SkywoodDetail {
final SkywoodDrama? videoInfo;
final SkywoodDrama? shortPlayInfo;
final List<SkywoodEpisode> episodeList;
final String? businessModel;
bool? isCollect;
final bool? showShareCoin;
final int? shareCoin;
final int? revolution;
final String? userLevel;
final int? jumpType;
final int? jumpShortPlayId;
final List<int>? checkPoint;
SkywoodDetail({
required this.videoInfo,
this.shortPlayInfo,
required this.episodeList,
this.businessModel,
this.isCollect,
this.showShareCoin,
this.shareCoin,
this.revolution,
this.userLevel,
this.jumpType,
this.jumpShortPlayId,
this.checkPoint,
});
factory SkywoodDetail.fromJson(Map<String, dynamic> json) {
return SkywoodDetail(
videoInfo: parseMap(json['video_info']) == null
? null
: SkywoodDrama.fromJson(parseMap(json['video_info'])!),
shortPlayInfo: parseMap(json['shortPlayInfo']) == null
? null
: SkywoodDrama.fromJson(parseMap(json['shortPlayInfo'])!),
episodeList: parseModelList(json['episodeList'], SkywoodEpisode.fromJson),
businessModel: json['business_model'] as String?,
isCollect: json['is_collect'] == null
? null
: parseBool(json['is_collect']),
showShareCoin: json['show_share_coin'] == null
? null
: parseBool(json['show_share_coin']),
shareCoin: json['share_coin'] == null
? null
: parseInt(json['share_coin']),
revolution: json['revolution'] == null
? null
: parseInt(json['revolution']),
userLevel: json['user_level'] as String?,
jumpType: json['jump_type'] == null ? null : parseInt(json['jump_type']),
jumpShortPlayId: json['jump_short_play_id'] == null
? null
: parseInt(json['jump_short_play_id']),
checkPoint: parseIntList(json['check_point']),
);
}
Map<String, dynamic> toJson() {
return {
'video_info': videoInfo?.toJson(),
'shortPlayInfo': shortPlayInfo?.toJson(),
'episodeList': episodeList.map((item) => item.toJson()).toList(),
'business_model': businessModel,
'is_collect': isCollect,
'show_share_coin': showShareCoin,
'share_coin': shareCoin,
'revolution': revolution,
'user_level': userLevel,
'jump_type': jumpType,
'jump_short_play_id': jumpShortPlayId,
'check_point': checkPoint,
};
}
SkywoodDetail copyWith({
SkywoodDrama? videoInfo,
SkywoodDrama? shortPlayInfo,
List<SkywoodEpisode>? episodeList,
String? businessModel,
bool? isCollect,
bool? showShareCoin,
int? shareCoin,
int? revolution,
String? userLevel,
int? jumpType,
int? jumpShortPlayId,
List<int>? checkPoint,
}) {
return SkywoodDetail(
videoInfo: videoInfo ?? this.videoInfo,
shortPlayInfo: shortPlayInfo ?? this.shortPlayInfo,
episodeList: episodeList ?? this.episodeList,
businessModel: businessModel ?? this.businessModel,
isCollect: isCollect ?? this.isCollect,
showShareCoin: showShareCoin ?? this.showShareCoin,
shareCoin: shareCoin ?? this.shareCoin,
revolution: revolution ?? this.revolution,
userLevel: userLevel ?? this.userLevel,
jumpType: jumpType ?? this.jumpType,
jumpShortPlayId: jumpShortPlayId ?? this.jumpShortPlayId,
checkPoint: checkPoint ?? this.checkPoint,
);
}
}
+183
View File
@@ -0,0 +1,183 @@
import '../model_parser.dart';
import 'skywood_category.dart';
class SkywoodDrama {
final int? id;
final int shortId;
int? shortPlayId;
final String name;
final String description;
final String imageUrl;
final String horizontallyImg;
int collectTotal;
final int watchTotal;
final int episodeTotal;
final String? playSeconds;
bool? isCollect;
final String? videoUrl;
final Map<String, dynamic>? videoInfo;
final List<String>? category;
final List<SkywoodCategory>? categoryList;
final String tagType;
int? episode;
final int? process;
final int? buyType;
final int? searchClickTotal;
final int? shortPlayVideoId;
final int? currentEpisode;
int? historyTime;
SkywoodDrama({
this.id,
required this.shortId,
this.shortPlayId,
required this.name,
required this.description,
required this.imageUrl,
required this.horizontallyImg,
required this.collectTotal,
required this.watchTotal,
required this.episodeTotal,
this.playSeconds,
this.isCollect,
this.videoUrl,
this.videoInfo,
this.category,
this.categoryList,
required this.tagType,
this.episode,
this.process,
this.buyType,
this.searchClickTotal,
this.shortPlayVideoId,
this.currentEpisode,
this.historyTime,
});
factory SkywoodDrama.fromJson(Map<String, dynamic> json) {
return SkywoodDrama(
id: json['id'] == null ? null : parseInt(json['id']),
shortId: parseInt(json['short_id']),
shortPlayId: json['short_play_id'] == null
? null
: parseInt(json['short_play_id']),
name: parseString(json['name']),
description: parseString(json['description']),
imageUrl: parseString(json['image_url']),
horizontallyImg: parseString(json['horizontally_img']),
collectTotal: parseInt(json['collect_total']),
watchTotal: parseInt(json['watch_total']),
episodeTotal: parseInt(json['episode_total']),
playSeconds: json['play_seconds'] as String?,
isCollect: json['is_collect'] == null
? null
: parseBool(json['is_collect']),
videoUrl: json['video_url'] as String?,
videoInfo: parseMap(json['video_info']),
category: parseStringList(json['category']),
categoryList: json['categoryList'] == null
? null
: parseModelList(json['categoryList'], SkywoodCategory.fromJson),
tagType: parseString(json['tag_type']),
episode: json['episode'] == null ? null : parseInt(json['episode']),
process: json['process'] == null ? null : parseInt(json['process']),
buyType: json['buy_type'] == null ? null : parseInt(json['buy_type']),
searchClickTotal: json['search_click_total'] == null
? null
: parseInt(json['search_click_total']),
shortPlayVideoId: json['short_play_video_id'] == null
? null
: parseInt(json['short_play_video_id']),
currentEpisode: json['current_episode'] == null
? null
: parseInt(json['current_episode']),
historyTime: json['history_time'] == null
? null
: parseInt(json['history_time']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'short_id': shortId,
'short_play_id': shortPlayId,
'name': name,
'description': description,
'image_url': imageUrl,
'horizontally_img': horizontallyImg,
'collect_total': collectTotal,
'watch_total': watchTotal,
'episode_total': episodeTotal,
'play_seconds': playSeconds,
'is_collect': isCollect,
'video_url': videoUrl,
'video_info': videoInfo,
'category': category,
'categoryList': categoryList?.map((item) => item.toJson()).toList(),
'tag_type': tagType,
'episode': episode,
'process': process,
'buy_type': buyType,
'search_click_total': searchClickTotal,
'short_play_video_id': shortPlayVideoId,
'current_episode': currentEpisode,
'history_time': historyTime,
};
}
SkywoodDrama copyWith({
int? id,
int? shortId,
int? shortPlayId,
String? name,
String? description,
String? imageUrl,
String? horizontallyImg,
int? collectTotal,
int? watchTotal,
int? episodeTotal,
String? playSeconds,
bool? isCollect,
String? videoUrl,
Map<String, dynamic>? videoInfo,
List<String>? category,
List<SkywoodCategory>? categoryList,
String? tagType,
int? episode,
int? process,
int? buyType,
int? searchClickTotal,
int? shortPlayVideoId,
int? currentEpisode,
int? historyTime,
}) {
return SkywoodDrama(
id: id ?? this.id,
shortId: shortId ?? this.shortId,
shortPlayId: shortPlayId ?? this.shortPlayId,
name: name ?? this.name,
description: description ?? this.description,
imageUrl: imageUrl ?? this.imageUrl,
horizontallyImg: horizontallyImg ?? this.horizontallyImg,
collectTotal: collectTotal ?? this.collectTotal,
watchTotal: watchTotal ?? this.watchTotal,
episodeTotal: episodeTotal ?? this.episodeTotal,
playSeconds: playSeconds ?? this.playSeconds,
isCollect: isCollect ?? this.isCollect,
videoUrl: videoUrl ?? this.videoUrl,
videoInfo: videoInfo ?? this.videoInfo,
category: category ?? this.category,
categoryList: categoryList ?? this.categoryList,
tagType: tagType ?? this.tagType,
episode: episode ?? this.episode,
process: process ?? this.process,
buyType: buyType ?? this.buyType,
searchClickTotal: searchClickTotal ?? this.searchClickTotal,
shortPlayVideoId: shortPlayVideoId ?? this.shortPlayVideoId,
currentEpisode: currentEpisode ?? this.currentEpisode,
historyTime: historyTime ?? this.historyTime,
);
}
}
@@ -0,0 +1,92 @@
import '../model_parser.dart';
class SkywoodEpisode {
final int id;
final int shortId;
final int shortPlayId;
final int shortPlayVideoId;
final int episode;
final String videoUrl;
final int? promiseViewAd;
final int iaaPopUp;
bool isLock;
String playSeconds;
int? viewAdCount;
SkywoodEpisode({
required this.id,
required this.shortId,
required this.shortPlayId,
required this.shortPlayVideoId,
required this.episode,
required this.videoUrl,
required this.playSeconds,
required this.isLock,
this.promiseViewAd,
required this.iaaPopUp,
required this.viewAdCount,
});
factory SkywoodEpisode.fromJson(Map<String, dynamic> json) {
return SkywoodEpisode(
id: parseInt(json['id']),
shortId: parseInt(json['short_id']),
shortPlayId: parseInt(json['short_play_id']),
shortPlayVideoId: parseInt(json['short_play_video_id']),
episode: parseInt(json['episode']),
videoUrl: parseString(json['video_url']),
playSeconds: parseString(json['play_seconds']),
isLock: parseBool(json['is_lock']),
promiseViewAd: json['promise_view_ad'] == null
? null
: parseInt(json['promise_view_ad']),
iaaPopUp: parseInt(json['iaa_pop_up']),
viewAdCount: parseInt(json['view_ad_count']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'short_id': shortId,
'short_play_id': shortPlayId,
'short_play_video_id': shortPlayVideoId,
'episode': episode,
'video_url': videoUrl,
'promise_view_ad': promiseViewAd,
'iaa_pop_up': iaaPopUp,
'is_lock': isLock,
'play_seconds': playSeconds,
'view_ad_count': viewAdCount,
};
}
SkywoodEpisode copyWith({
int? id,
int? shortId,
int? shortPlayId,
int? shortPlayVideoId,
int? episode,
String? videoUrl,
bool? isLock,
String? playSeconds,
String? durationSeconds,
int? promiseViewAd,
int? viewAdCount,
int? iaaPopUp,
}) {
return SkywoodEpisode(
id: id ?? this.id,
shortId: shortId ?? this.shortId,
shortPlayId: shortPlayId ?? this.shortPlayId,
shortPlayVideoId: shortPlayVideoId ?? this.shortPlayVideoId,
episode: episode ?? this.episode,
videoUrl: videoUrl ?? this.videoUrl,
isLock: isLock ?? this.isLock,
playSeconds: playSeconds ?? this.playSeconds,
promiseViewAd: promiseViewAd ?? this.promiseViewAd,
viewAdCount: viewAdCount ?? this.viewAdCount,
iaaPopUp: iaaPopUp ?? this.iaaPopUp,
);
}
}
+46
View File
@@ -0,0 +1,46 @@
import 'package:skywood/utils/models/drama/skywood_drama.dart';
class SkywoodHome {
final String? moduleKey;
final List<SkywoodDrama> dataList;
const SkywoodHome({required this.moduleKey, required this.dataList});
factory SkywoodHome.fromJson(Map<String, dynamic> json) {
// Map<String, dynamic>? map = json['data'];
// List? list = map!['list'];
// List<SkywoodDrama> datas = [];
// if (list != null) {
// datas = list.map((e) => SkywoodDrama.fromJson(e)).toList();
// }
final data = json['data'];
List<SkywoodDrama> videos = [];
// 🔥 判断 data 的类型
if (data is Map<String, dynamic>) {
// 情况1: data 是 Map,从中取 list
final list = data['list'];
if (list is List) {
videos = list.map((e) => SkywoodDrama.fromJson(e)).toList();
}
} else if (data is List) {
// 情况2: data 直接就是 List
videos = data.map((e) => SkywoodDrama.fromJson(e)).toList();
}
return SkywoodHome(
moduleKey: json['module_key'] as String?,
dataList: videos,
);
}
Map<String, dynamic> toJson() {
return {'module_key': moduleKey, 'dataList': dataList};
}
SkywoodHome copyWith({String? moduleKey, dynamic data}) {
return SkywoodHome(
moduleKey: moduleKey ?? this.moduleKey,
dataList: data ?? dataList,
);
}
}
+57
View File
@@ -0,0 +1,57 @@
bool parseBool(dynamic value) {
if (value == null) return false;
if (value is bool) return value;
if (value is num) return value != 0;
if (value is String) {
final v = value.trim().toLowerCase();
return v == 'true' || v == '1' || v == 'yes' || v == 'y';
}
return false;
}
int parseInt(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is double) return value.toInt();
if (value is num) return value.toInt();
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
String parseString(dynamic value) {
if (value == null) return '';
return value.toString();
}
List<String>? parseStringList(dynamic value) {
if (value == null) return null;
if (value is List) {
return value.map((item) => parseString(item)).toList();
}
return null;
}
List<int>? parseIntList(dynamic value) {
if (value == null) return null;
if (value is List) {
return value.map((item) => parseInt(item)).toList();
}
return null;
}
Map<String, dynamic>? parseMap(dynamic value) {
if (value is Map<String, dynamic>) return value;
if (value is Map) return Map<String, dynamic>.from(value);
return null;
}
List<T> parseModelList<T>(
dynamic value,
T Function(Map<String, dynamic> json) fromJson,
) {
if (value is! List) return <T>[];
return value
.whereType<Map>()
.map((item) => fromJson(Map<String, dynamic>.from(item)))
.toList();
}
+59
View File
@@ -0,0 +1,59 @@
import '../model_parser.dart';
class GlimzoUser {
final String customerId;
final bool isTourist;
final String userLevel;
final String avator;
final String? familyName;
final String? givingName;
const GlimzoUser({
required this.customerId,
required this.isTourist,
required this.userLevel,
required this.avator,
this.familyName,
this.givingName,
});
factory GlimzoUser.fromJson(Map<String, dynamic> json) {
return GlimzoUser(
customerId: parseString(json['customer_id']),
isTourist: parseBool(json['is_tourist']),
userLevel: parseString(json['user_level']),
avator: parseString(json['avator']),
familyName: json['family_name'] as String?,
givingName: parseString(json['giving_name']),
);
}
Map<String, dynamic> toJson() {
return {
'customer_id': customerId,
'is_tourist': isTourist,
'user_level': userLevel,
'avator': avator,
'family_name': familyName,
'giving_name': givingName,
};
}
GlimzoUser copyWith({
String? customerId,
bool? isTourist,
String? userLevel,
String? avator,
String? familyName,
String? givingName,
}) {
return GlimzoUser(
customerId: customerId ?? this.customerId,
isTourist: isTourist ?? this.isTourist,
userLevel: userLevel ?? this.userLevel,
avator: avator ?? this.avator,
familyName: familyName ?? this.familyName,
givingName: givingName ?? this.givingName,
);
}
}
+18
View File
@@ -0,0 +1,18 @@
class RegisterBean {
final String token;
final int customerId;
final bool? autoLogin;
final int touristId;
RegisterBean({required this.token, required this.customerId, this.autoLogin, required this.touristId});
factory RegisterBean.fromJson(Map<String, dynamic> json) {
return RegisterBean(
token: json['token'],
customerId: json['customer_id'],
autoLogin: json['auto_login'],
touristId: json['tourist_id'],
);
}
}
+12 -5
View File
@@ -4,8 +4,8 @@ import 'package:get/get.dart';
/// AppScreen class
class AppScreen {
static double get screenWidth => Get.width;
static double get screenHeight => Get.height;
static double get screenWidth => _viewSize.width;
static double get screenHeight => _viewSize.height;
// static double get screenWidth => MediaQuery.of(Get.context!).size.width;
// static double get screenHeight => MediaQuery.of(Get.context!).size.height;
@@ -13,7 +13,7 @@ class AppScreen {
return MediaQuery.of(context).size;
}
static bool get isDebug=> kDebugMode;
static bool get isDebug => kDebugMode;
static double get statusBarHeight => Get.statusBarHeight / Get.pixelRatio;
@@ -33,12 +33,19 @@ class AppScreen {
return GetPlatform.isIOS;
}
static Size get _viewSize {
if (Get.context != null) {
return MediaQuery.of(Get.context!).size;
}
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
static double aRatioWidth({double? h, double aspectRatio = 1}) {
h ??= screenHeight;
return screenHeight * 1 / aspectRatio;
}
static double get keyboardHeight => Get.mediaQuery.viewInsets.bottom;
}
+143
View File
@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
// import 'package:lottie/lottie.dart';
// 加载数据的函数类型
typedef LoadBlock<T> = Future<T> Function();
// 弹出Toast提示的类
class AppToast {
static const String _defaultLoadingText = "";
static const String _loadingText = "loading...";
/// 显示普通 Toast 提示
static Future<void> showToast(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) async {
await dismiss();
return SmartDialog.showToast(
msg.toString(),
alignment: Alignment.center,
debounce: true,
displayTime: displayTime,
);
}
static Future<void> showError(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) {
return showFail(msg, displayTime: displayTime);
}
/// 显示失败提示
static Future<void> showFail(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
Function()? onDismiss,
}) async {
await dismiss();
return SmartDialog.showNotify(
msg: msg.toString(),
alignment: Alignment.center,
notifyType: NotifyType.failure,
displayTime: displayTime,
debounce: true,
onDismiss: onDismiss,
);
}
/// 显示成功提示
static Future<T?> showSuccess<T>(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) async {
await dismiss();
return SmartDialog.showNotify<T>(
msg: msg.toString(),
notifyType: NotifyType.success,
debounce: true,
displayTime: displayTime,
);
}
static Future<T?> showInfo<T>(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) {
return showWarning<T>(msg, displayTime: displayTime);
}
static Future<T?> showWarning<T>(
Object msg, {
Duration displayTime = const Duration(seconds: 2),
}) async {
await dismiss();
return SmartDialog.showNotify<T>(
msg: msg.toString(),
notifyType: NotifyType.warning,
debounce: true,
displayTime: displayTime,
);
}
/// 显示 Lottie 加载提示
// static Future<T?> showLottieLoading<T>({
// Duration? displayTime,
// }) async {
// await dismiss();
// return SmartDialog.showLoading<T>(
// displayTime: displayTime ?? const Duration(seconds: 5),
// maskColor: Colors.transparent,
// builder: (_) {
// return SizedBox(
// width: 120,
// height: 120,
// child: Lottie.asset(
// 'assets/bagua.json',
// fit: BoxFit.contain,
// repeat: true,
// ),
// );
// },
// );
// }
/// 显示加载提示
static Future<T?> showLoading<T>({
String text = _loadingText,
Duration displayTime = const Duration(seconds: 10),
}) async {
await dismiss();
return SmartDialog.showLoading<T>(msg: text, displayTime: displayTime);
}
/// 关闭特定状态的弹窗
static Future<void> dismiss<T>({SmartStatus status = SmartStatus.loading}) {
return SmartDialog.dismiss<T>(status: status);
}
/// 关闭所有弹窗(占位)
static void dismissAll() {
SmartDialog.dismiss();
}
/// 自动显示加载框并在完成后关闭
static Future<T> loading<T>(
LoadBlock<T> block, {
String? text,
bool isLoading = true,
}) async {
if (isLoading) {
showLoading(text: text ?? _defaultLoadingText);
}
try {
final result = await block();
return result;
} finally {
dismiss();
}
}
}
+16
View File
@@ -18,6 +18,22 @@ class ConstImg {
static const String tabMineUnselect = "assets/common/tab_mine_unselect.png";
static const String homeBg = "assets/imgs/home_bg.png";
// home_card_bg
static const String homeCardBg = "assets/imgs/home_card_bg.png";
// home_top_bg
static const String homeTopBg = "assets/imgs/home_top_bg.png";
// home_title_daily/hot/hot_cate/mystery/weekly
static const String homeTitleDaily = "assets/imgs/home_title_daily.png";
static const String homeTitleHot = "assets/imgs/home_title_hot.png";
static const String homeTitleHotCate = "assets/imgs/home_title_hot_cate.png";
static const String homeTitleMystery = "assets/imgs/home_title_mystery.png";
static const String homeTitleWeekly = "assets/imgs/home_title_weekly.png";
static const String homeOpenNowBox = "assets/imgs/home_opennow_box.png";
// splash_page
static const String splashPage = "assets/imgs/splash_page.png";
// heart
static const String heart = "assets/images/heart.png";
// nodata
static const String noData = "assets/images/nodata.png";
+117
View File
@@ -0,0 +1,117 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
/// navigator
class HelpNav {
/// clear all route
static Future<T?>? offAllNamed<T>(
String newRouteName, {
Map? arguments,
RoutePredicate? predicate,
String? tag,
Map? parameters,
}) {
return Get.offAllNamed<T>(
newRouteName,
arguments: arguments,
predicate: predicate,
parameters: {...?parameters},
);
}
static Future<T?>? off<T>(Widget page, {arguments, preventDuplicates}) {
return Get.off<T>(
() => page,
arguments: arguments,
preventDuplicates: preventDuplicates ?? false,
);
}
static Future<T?>? offAll<T>(Widget page, {arguments, preventDuplicates}) {
return Get.offAll<T>(() => page, arguments: arguments);
}
static Future<T?>? offNamed<T>(
String newRouteName, {
arguments,
preventDuplicates,
}) {
return Get.offNamed<T>(
newRouteName,
arguments: arguments,
preventDuplicates: preventDuplicates ?? true,
);
}
/// to name
static Future<T?>? toNamed<T>(
String newRouteName, {
arguments,
String? tag,
Map? parameters,
}) {
return Get.toNamed<T>(
newRouteName,
arguments: arguments,
parameters: {...?parameters},
);
}
/// to route
static Future<T?>? to<T>(Widget page, {arguments, binding}) {
return Get.to<T>(
() => page,
arguments: arguments,
binding: binding,
preventDuplicates: false,
);
}
static void _until<T>({String? routeName, T? result}) {
if (result == null) {
Get.back<T>(result: result);
return;
}
// Get.back<T>(result: result);
//
if (routeName == null) {
Get.back<T>(result: result);
return;
}
}
/// back pre
/// [result]
static void back<T>({T? result, String? routeName}) {
_until(routeName: routeName, result: result);
}
/// show dialog
static Future<T?> showDialog<T>(
Widget child, {
bool barrierDismissible = false,
}) {
return Get.dialog<T>(
Center(child: child),
barrierDismissible: barrierDismissible,
);
}
/// show bottom sheet
static Future<T?> showBottom<T>(
Widget child, {
Color? safeAreaColor,
double? borderRadius,
bool isDismissible = false,
bool isScrollControlled = false,
bool? enableDrag,
}) {
return Get.bottomSheet<T>(
child,
isScrollControlled: isScrollControlled,
enableDrag: enableDrag ?? true,
isDismissible: isDismissible,
);
}
}
+177
View File
@@ -0,0 +1,177 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:android_id/android_id.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:skywood/global/request/api_const.dart';
@immutable
class SkyDeviceInfo {
final String deviceId;
final String systemType;
final String systemVersion;
final String brand;
final String model;
final String appName;
final String appPackageName;
final String appVersion;
final String idfv;
static SkyDeviceInfo? _instance;
const SkyDeviceInfo._({
required this.deviceId,
required this.systemType,
required this.systemVersion,
required this.brand,
required this.model,
required this.appName,
required this.appPackageName,
required this.appVersion,
required this.idfv,
});
static SkyDeviceInfo get instance {
if (_instance == null) {
throw StateError('error');
}
return _instance!;
}
@visibleForTesting
static void setMockInstance({
String deviceId = 'test-device-id',
String systemType = 'test',
String systemVersion = '1.0',
String brand = 'test',
String model = 'test',
String appName = 'Glimzo',
String appPackageName = 'com.example.skywood',
String appVersion = '1.0.0',
String idfv = 'test-idfv',
}) {
_instance = SkyDeviceInfo._(
deviceId: deviceId,
systemType: systemType,
systemVersion: systemVersion,
brand: brand,
model: model,
appName: appName,
appPackageName: appPackageName,
appVersion: appVersion,
idfv: idfv,
);
}
/// 初始化设备信息
static Future<void> init() async {
if (_instance != null) return;
String deviceId = '',
systemType = '',
systemVersion = '',
brand = '',
model = '',
appName = 'Glimzo',
appPackageName = '',
appVersion = '',
idfv = '';
// 获取应用信息
await _initAppInfo()
.then((info) {
appPackageName = info['packageName'] ?? '';
appVersion = info['version'] ?? '';
appName = ApiConst.appName;
})
.catchError((e) {
debugPrint('[sDeviceInfo] getElPackageName: $e');
});
// 获取设备信息
if (Platform.isAndroid) {
await _initAndroidInfo()
.then((info) {
deviceId = info['deviceId'] ?? '';
systemType = 'android';
systemVersion = info['systemVersion'] ?? '';
brand = info['brand'] ?? '';
model = info['model'] ?? '';
})
.catchError((e) {
debugPrint('[GlimzoUtilDeviceInfo] Android: $e');
});
} else if (Platform.isIOS) {
await _initIOSInfo()
.then((info) {
deviceId = info['deviceId'] ?? '';
idfv = info['idfv'] ?? '';
systemType = 'ios';
systemVersion = info['systemVersion'] ?? '';
brand = info['brand'] ?? '';
model = info['model'] ?? '';
})
.catchError((e) {
debugPrint('[GlimzoUtilDeviceInfo] iOS: $e');
});
}
_instance = SkyDeviceInfo._(
deviceId: deviceId,
systemType: systemType,
systemVersion: systemVersion,
brand: brand,
model: model,
appName: appName,
appPackageName: appPackageName,
appVersion: appVersion,
idfv: idfv,
);
}
/// 获取应用信息
static Future<Map<String, String>> _initAppInfo() async {
final packageInfo = await PackageInfo.fromPlatform();
return {
'packageName': packageInfo.packageName,
'version': packageInfo.version,
};
}
/// 获取 Android 设备信息
static Future<Map<String, String?>> _initAndroidInfo() async {
final plugin = DeviceInfoPlugin();
final androidInfo = await plugin.androidInfo;
final androidIdPlugin = AndroidId();
final androidId = await androidIdPlugin.getId();
return {
'deviceId': androidId ?? androidInfo.id,
'systemVersion': androidInfo.version.release,
'brand': androidInfo.brand,
'model': androidInfo.model,
};
}
/// 获取 iOS 设备信息
static Future<Map<String, String?>> _initIOSInfo() async {
final plugin = DeviceInfoPlugin();
final iosInfo = await plugin.iosInfo;
final secureStorage = FlutterSecureStorage();
String? idfvStore = await secureStorage.read(key: 'identifierForVendor');
idfvStore ??= iosInfo.identifierForVendor;
if (idfvStore != null) {
await secureStorage.write(key: 'identifierForVendor', value: idfvStore);
}
return {
'deviceId': idfvStore,
'idfv': iosInfo.identifierForVendor ?? '',
'systemVersion': iosInfo.systemVersion,
'brand': iosInfo.model,
'model': iosInfo.modelName,
};
}
}
+53
View File
@@ -0,0 +1,53 @@
class SkyTimeTool {
static String getTimeZone() {
final offset = DateTime.now().timeZoneOffset;
final hours = offset.inHours;
final minutes = offset.inMinutes.remainder(60).abs();
final sign = hours >= 0 ? '+' : '-';
final formatted =
'GMT$sign${hours.abs().toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}';
return formatted;
}
//格式化数字,带单位 k / m,可自定义小数位和千分位分隔符
static String formatNum(
num value, {
int decimalDigits = 1,
String separator = ',',
}) {
String removeTrailingZeros(String str) {
if (str.contains('.')) {
str = str.replaceAll(RegExp(r'0+$'), ''); // 去掉末尾 0
str = str.replaceAll(RegExp(r'\.$'), ''); // 去掉末尾小数点
}
return str;
}
String addSeparator(String str) {
final parts = str.split('.');
final intPart = parts[0];
final decPart = parts.length > 1 ? '.${parts[1]}' : '';
final regex = RegExp(r'(\d+)(\d{3})');
var formatted = intPart;
while (regex.hasMatch(formatted)) {
formatted = formatted.replaceAllMapped(
regex,
(m) => '${m[1]}$separator${m[2]}',
);
}
return '$formatted$decPart';
}
if (value < 1000) {
return addSeparator(value.toStringAsFixed(0));
} else if (value < 1000000) {
final formatted = (value / 1000).toStringAsFixed(decimalDigits);
return '${removeTrailingZeros(formatted)}K';
} else {
final formatted = (value / 1000000).toStringAsFixed(decimalDigits);
return '${removeTrailingZeros(formatted)}M';
}
}
}
+234
View File
@@ -0,0 +1,234 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:skywood/global/client/user_client.dart';
/// Token 管理单例类
/// 对应原项目的 GlimzoUtilToken
class TokenManager {
// ============ 单例模式 ============
static final TokenManager _instance = TokenManager._internal();
factory TokenManager() => _instance;
TokenManager._internal();
// ============ 常量定义 ============
static const String _keyToken = 'SKY_STORE_TOKEN';
static const String _keyUserInfo = 'SKY_STORE_USER_INFO';
// ============ 内存缓存 ============
String? _cachedToken;
Map<String, dynamic>? _cachedUserInfo;
// ============ 核心方法 ============
/// 获取 Token(从内存或存储)
String get() {
if (_cachedToken != null) {
return _cachedToken!;
}
// 如果内存中没有,从存储中加载
_cachedToken = _getFromStorage(_keyToken);
return _cachedToken ?? '';
}
/// 保存 Token(到内存和存储)
void save(String token) {
if (token.isNotEmpty) {
_cachedToken = token;
_saveToStorage(_keyToken, token);
}
}
/// 清除 Token(从内存和存储)
void clear() {
_cachedToken = null;
_removeFromStorage(_keyToken);
_cachedUserInfo = null;
_removeFromStorage(_keyUserInfo);
}
/// 刷新 Token(调用注册接口获取新 Token)
/// 注意:这里需要传入一个刷新函数,因为原项目依赖 GlimzoRequestUser
Future<void> update() async {
try {
final res = await UserClient.register();
if (res == null) return;
final newToken = res.token;
if (newToken.isNotEmpty ?? false) {
// 保存新 Token
save(newToken);
// 如果返回了用户信息,也一并保存
// if (res['userInfo'] != null) {
// saveUserInfo(res['userInfo']);
// }
} else {
throw Exception('Invalid token response');
}
} catch (e) {
debugPrint('[TokenManager] Error updating token: $e');
rethrow;
}
}
// ============ 便捷属性 ============
/// 判断 Token 是否为空
bool get isEmpty => get().isEmpty;
/// 判断 Token 是否不为空
bool get isNotEmpty => !isEmpty;
/// 获取完整 Authorization 头
String? get authorizationHeader {
final token = get();
if (token.isNotEmpty) {
return 'Bearer $token';
}
return null;
}
// ============ 用户信息扩展(原项目没有,但建议加上) ============
/// 保存用户信息
void saveUserInfo(Map<String, dynamic> userInfo) {
_cachedUserInfo = userInfo;
_saveToStorage(_keyUserInfo, jsonEncode(userInfo));
}
/// 获取用户信息
Map<String, dynamic>? getUserInfo() {
if (_cachedUserInfo != null) {
return _cachedUserInfo;
}
final stored = _getFromStorage(_keyUserInfo);
if (stored != null && stored.isNotEmpty) {
try {
_cachedUserInfo = jsonDecode(stored) as Map<String, dynamic>;
return _cachedUserInfo;
} catch (_) {
return null;
}
}
return null;
}
// ============ 私有存储方法 ============
String? _getFromStorage(String key) {
try {
// 使用 SharedPreferences 同步获取(但 SharedPreferences 是异步的)
// 为了保持与原项目一致的同步接口,这里使用异步转同步
// 但更好的方式是在初始化时预加载
return _getFromStorageSync(key);
} catch (e) {
debugPrint('[TokenManager] Error reading from storage: $e');
return null;
}
}
// 同步读取(注意:SharedPreferences 本身是异步的,这里用同步方式需要谨慎)
String? _getFromStorageSync(String key) {
// 由于 SharedPreferences 是异步的,我们使用一个全局缓存
// 实际项目中建议在 main 中调用 init() 预加载
try {
// 这里为了简化,直接返回 null,实际应该用异步方式
// 建议使用下面的异步版本
return null;
} catch (_) {
return null;
}
}
void _saveToStorage(String key, String value) {
// 异步保存,不等待结果
_saveToStorageAsync(key, value);
}
void _removeFromStorage(String key) {
// 异步移除,不等待结果
_removeFromStorageAsync(key);
}
// ============ 异步存储方法(推荐使用) ============
/// 初始化:从存储中加载 Token
Future<void> init() async {
try {
final prefs = await SharedPreferences.getInstance();
_cachedToken = prefs.getString(_keyToken);
final userInfoStr = prefs.getString(_keyUserInfo);
if (userInfoStr != null && userInfoStr.isNotEmpty) {
try {
_cachedUserInfo = jsonDecode(userInfoStr) as Map<String, dynamic>;
} catch (_) {
_cachedUserInfo = null;
}
}
} catch (e) {
debugPrint('[TokenManager] Init error: $e');
}
}
/// 异步获取 Token
Future<String> getAsync() async {
if (_cachedToken != null) {
return _cachedToken!;
}
try {
final prefs = await SharedPreferences.getInstance();
_cachedToken = prefs.getString(_keyToken);
return _cachedToken ?? '';
} catch (e) {
debugPrint('[TokenManager] Get token error: $e');
return '';
}
}
/// 异步保存 Token
Future<void> saveAsync(String token) async {
if (token.isNotEmpty) {
_cachedToken = token;
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyToken, token);
} catch (e) {
debugPrint('[TokenManager] Save token error: $e');
}
}
}
/// 异步清除 Token
Future<void> clearAsync() async {
_cachedToken = null;
_cachedUserInfo = null;
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_keyToken);
await prefs.remove(_keyUserInfo);
} catch (e) {
debugPrint('[TokenManager] Clear token error: $e');
}
}
// 异步保存到存储
Future<void> _saveToStorageAsync(String key, String value) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
} catch (e) {
debugPrint('[TokenManager] Save to storage error: $e');
}
}
// 异步从存储移除
Future<void> _removeFromStorageAsync(String key) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
} catch (e) {
debugPrint('[TokenManager] Remove from storage error: $e');
}
}
}
@@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
import '../../../global/const/common_color.dart';
import '../../extend/widget_extend.dart';
import '../help_nav.dart';
class SkyText extends StatelessWidget {
const SkyText({
super.key,
required this.text,
this.color = Colors.white,
this.fontSize = 14,
this.shadow,
this.decoration,
this.decorationColor,
this.textAlign,
this.fontWeight,
this.maxLines,
this.lineHeight,
this.letterSpacing,
this.overflow = TextOverflow.ellipsis,
this.softWrap,
this.fontFamily,
});
final String text;
final Color? color;
final double? fontSize;
final TextDecoration? decoration;
final Color? decorationColor;
final TextAlign? textAlign;
final FontWeight? fontWeight;
final int? maxLines;
final Shadow? shadow;
final double? lineHeight;
final double? letterSpacing;
final TextOverflow? overflow;
final bool? softWrap;
final String? fontFamily;
@override
Widget build(BuildContext context) {
final FontWeight resolvedWeight = fontWeight ?? FontWeight.normal;
return Text(text, style: TextStyle(
fontSize: fontSize,
fontWeight: resolvedWeight,
decoration: decoration,
fontFamily: fontFamily ?? 'Inter',
color: color,
shadows: shadow == null ? null : [shadow!],
decorationColor: decorationColor,
height: lineHeight,
letterSpacing: letterSpacing,
), maxLines: maxLines,
overflow: overflow,
softWrap: softWrap,
textAlign: textAlign,
);
}
}
class ComConfirmWidget extends StatelessWidget {
final String? title;
final String? content;
final Widget? contentWidget;
final String? cancelStr, confirmStr;
final double? height;
final Function? onConfirmed;
final Function? onCanceled;
final bool? showCancel;
final bool? clickHideAlert;
const ComConfirmWidget({
super.key,
this.title,
this.content,
this.contentWidget,
this.cancelStr,
this.confirmStr,
this.height,
this.onConfirmed,
this.onCanceled,
this.showCancel = true,
this.clickHideAlert = true,
});
@override
Widget build(BuildContext context) {
return Container(
height: height ?? 160,
margin: EdgeInsets.only(left: 25, right: 25),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Column(
// mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 15,),
title==null ? SizedBox() : Padding(padding: EdgeInsets.only(left:15, right: 15, bottom: 5),
child: SkyText(text: title!, fontSize: 16, color: Colors.black,
)
),
const SizedBox(height: 10),
Expanded(
child: Padding(padding: EdgeInsets.only(left:14, right: 14),
child: contentWidget ?? Center(
child: SkyText(text: content!,
fontSize: 15, color: CommonColor.black,
maxLines: 3),
),
)
),
const SizedBox(height: 10),
Divider(thickness: 0.6, height: 1),
SizedBox(
height: 44,
child: IntrinsicHeight(
child: showCancel! ? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(onPressed: () {
Navigator.pop(context);
onCanceled?.call();
},
child: Center(
child: SkyText(text: cancelStr ?? 'Cancel',
color: Colors.grey, fontSize: 14))
),
VerticalDivider(width: 5, thickness: 0.6,),
TextButton(onPressed: () {
if (clickHideAlert == true) {
Navigator.pop(context);
}
onConfirmed?.call();
},
child: Center(
child:SkyText(text: confirmStr ?? 'Confirm',
color: CommonColor.goldColor, fontSize: 14))
),
],
) : TextButton(onPressed: () {
Navigator.pop(context);
onConfirmed?.call();
},
child: Center(
child:SkyText(text: confirmStr ?? 'Confirm',
color: CommonColor.black, fontSize: 14,))
),
)
)
],
),
);
}
}
class ComSheetWidget extends StatelessWidget {
final Function(int)? onSelected;
final String? title;
final List<String> items;
final TextAlign? textAlign;
const ComSheetWidget({super.key,
this.onSelected,
this.title,
this.textAlign = TextAlign.left,
required this.items,
});
@override
Widget build(BuildContext context) {
return SafeArea(child: _mainView());
}
_mainView() {
return Container(
padding: EdgeInsets.only(top: 10),
height: items.length>5 ? 360 : 40+items.length*47 + 50,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20)
),
),
child: _ifElseWidget(),
);
}
_ifElseWidget() {
if (title != null) {
return Column(
children: [
const SizedBox(height: 6),
Text('$title', style: TextStyle(
color: Colors.black, fontSize: 16
)),
const SizedBox(height: 5),
const Divider(height: 8, thickness: 0.6,),
// Divider(),
const SizedBox(height: 5),
Expanded(child: ListView.separated(itemBuilder: (context, index) {
return _itemCreateer(index);
}, separatorBuilder: (context, index) {
return Divider( indent: 15, endIndent: 15, thickness: 0.6,);
}, itemCount: items.length))
],
);
} else {
return ListView.separated(itemBuilder: (context, index) {
return _itemCreateer(index);
}, separatorBuilder: (context, index) {
return Divider(indent: 15, endIndent: 15, thickness: 0.6,);
}, itemCount: items.length);
}
}
_itemCreateer(int index) {
return SizedBox(
height: 47,
child: Center(
child: Text(items[index], style: TextStyle(
fontSize: 15, color: Colors.black,
)),
).addInkWell(
onTap: () {
HelpNav.back();
onSelected?.call(index);
})
);
}
}