feat: 工具类提交。
This commit is contained in:
1 parent
a2b38fa1e8
commit
557f900187
20 files changed
+2026
-3
No files matched your search
@@ -0,0 +1,155 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:math';
|
||||
|
||||
class HelpEncrypt {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,9 @@ class ConstImage {
|
||||
static const String settings = "assets/other/settings.png";
|
||||
// copy
|
||||
static const String copy = "assets/other/copy.png";
|
||||
|
||||
// pricary web_site
|
||||
static const String primary = "assets/other/pricary.png";
|
||||
static const String webSite = "assets/other/web_site.png";
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
|
||||
class HelpLog {
|
||||
HelpLog._();
|
||||
|
||||
static final HelpLog _instance = HelpLog._();
|
||||
|
||||
static HelpLog get instance => _instance;
|
||||
|
||||
final Talker _talker = TalkerFlutter.init();
|
||||
|
||||
factory HelpLog() => _instance;
|
||||
|
||||
Talker get tLog => _talker;
|
||||
|
||||
/// DevTools / 控制台里可按名称过滤。
|
||||
static const String logName = 'HelpLog';
|
||||
|
||||
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)];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
class HelpTime {
|
||||
|
||||
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 unit = 'k',
|
||||
}) {
|
||||
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) {
|
||||
int mu = unit=='w' ? 10000 : 1000;
|
||||
final formatted = (value / mu).toStringAsFixed(decimalDigits);
|
||||
return '${removeTrailingZeros(formatted)}$unit';
|
||||
} else {
|
||||
final formatted = (value / 1000000).toStringAsFixed(decimalDigits);
|
||||
return '${removeTrailingZeros(formatted)}m';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:novyronst/common/api/user_request.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class HelpToken {
|
||||
static final HelpToken _instance = HelpToken._internal();
|
||||
factory HelpToken() => _instance;
|
||||
HelpToken._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 UserRequest.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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user