This commit is contained in:
2026-03-07 17:24:59 +08:00
parent 4418ebecac
commit b0ec8ab4bd
417 changed files with 42546 additions and 2 deletions

41
lib/app/app_color.dart Normal file
View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
class AppColor {
static const Color primaryColor = Color(0xff4196f9);
static ColorScheme colorSchemeLight = ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.light,
);
static ColorScheme colorSchemeDark = ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark,
);
static const Color backgroundColor = Color(0xfffafafa);
static const Color backgroundColorDark = Color(0xff212121);
static const Color black333 = Color(0xff333333);
static const Color greyf0f0f0 = Color(0xfff0f0f0);
static Map<int, List<Color>> novelThemes = {
0: [
const Color.fromRGBO(245, 239, 217, 1),
const Color(0xff301e1b),
],
1: [
const Color.fromRGBO(248, 247, 252, 1),
black333,
],
2: [
const Color.fromRGBO(192, 237, 198, 1),
Colors.black,
],
3: [
const Color(0xff3b3a39),
const Color.fromRGBO(230, 230, 230, 1),
],
4: [
Colors.black,
const Color.fromRGBO(200, 200, 200, 1),
],
};
}

27
lib/app/app_constant.dart Normal file
View File

@@ -0,0 +1,27 @@
class AppConstant {
/// 定义平板宽度当大于此宽度时APP进入双栏模式
static const double kTabletWidth = 1000;
/// 类型ID-漫画
static const int kTypeComic = 4;
/// 类型ID-新闻
static const int kTypeNews = 6;
/// 类型ID-专题
static const int kTypeSpecial = 2;
/// 类型ID-轻小说
static const int kTypeNovel = 1;
}
class ReaderDirection {
/// 左右 0
static const int kLeftToRight = 0;
/// 上下 1
static const int kUpToDown = 1;
/// 右左 2
static const int kRightToLeft = 2;
}

13
lib/app/app_error.dart Normal file
View File

@@ -0,0 +1,13 @@
class AppError implements Exception {
final int code;
final String message;
AppError(
this.message, {
this.code = 0,
});
@override
String toString() {
return message;
}
}

167
lib/app/app_style.dart Normal file
View File

@@ -0,0 +1,167 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dmzj/app/app_color.dart';
import 'package:get/get.dart';
class AppStyle {
static ThemeData lightTheme = getLightTheme();
static ThemeData getLightTheme({ColorScheme? colorScheme}) {
final scheme = colorScheme ?? AppColor.colorSchemeLight;
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
brightness: Brightness.light,
).copyWith(
scaffoldBackgroundColor: colorScheme != null ? null : Colors.white,
cardColor: colorScheme != null ? null : Colors.white,
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: scheme.onSurface,
centerTitle: false,
shape: Border(
bottom: BorderSide(
color: Colors.grey.withOpacity(.2),
width: 1,
),
),
iconTheme: IconThemeData(
color: scheme.onSurface,
),
titleTextStyle: TextStyle(
fontSize: 16,
color: scheme.onSurface,
),
systemOverlayStyle: SystemUiOverlayStyle.dark.copyWith(
systemNavigationBarColor: Colors.transparent,
),
),
);
}
static ThemeData darkTheme = getDarkTheme();
static ThemeData getDarkTheme({ColorScheme? colorScheme}) {
final scheme = colorScheme ?? AppColor.colorSchemeDark;
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
brightness: Brightness.dark,
).copyWith(
primaryColor: scheme.primary,
cardColor: const Color(0xff424242),
scaffoldBackgroundColor: Colors.black,
tabBarTheme: TabBarThemeData(
indicatorColor: scheme.primary,
),
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
centerTitle: false,
shape: Border(
bottom: BorderSide(
color: Colors.grey.withOpacity(.2),
width: 1,
),
),
titleTextStyle: const TextStyle(
fontSize: 16,
color: Colors.white,
),
iconTheme: const IconThemeData(
color: Colors.white,
),
systemOverlayStyle: SystemUiOverlayStyle.light.copyWith(
systemNavigationBarColor: Colors.transparent,
),
),
);
}
static const vGap4 = SizedBox(
height: 4,
);
static const vGap8 = SizedBox(
height: 8,
);
static const vGap12 = SizedBox(
height: 12,
);
static const vGap24 = SizedBox(
height: 24,
);
static const vGap32 = SizedBox(
height: 32,
);
static const hGap4 = SizedBox(
width: 4,
);
static const hGap8 = SizedBox(
width: 8,
);
static const hGap12 = SizedBox(
width: 12,
);
static const hGap16 = SizedBox(
width: 16,
);
static const hGap24 = SizedBox(
width: 24,
);
static const hGap32 = SizedBox(
width: 32,
);
static const edgeInsetsH4 = EdgeInsets.symmetric(horizontal: 4);
static const edgeInsetsH8 = EdgeInsets.symmetric(horizontal: 8);
static const edgeInsetsH12 = EdgeInsets.symmetric(horizontal: 12);
static const edgeInsetsH16 = EdgeInsets.symmetric(horizontal: 16);
static const edgeInsetsH20 = EdgeInsets.symmetric(horizontal: 20);
static const edgeInsetsH24 = EdgeInsets.symmetric(horizontal: 24);
static const edgeInsetsV4 = EdgeInsets.symmetric(vertical: 4);
static const edgeInsetsV8 = EdgeInsets.symmetric(vertical: 8);
static const edgeInsetsV12 = EdgeInsets.symmetric(vertical: 12);
static const edgeInsetsV24 = EdgeInsets.symmetric(vertical: 24);
static const edgeInsetsA4 = EdgeInsets.all(4);
static const edgeInsetsA8 = EdgeInsets.all(8);
static const edgeInsetsA12 = EdgeInsets.all(12);
static const edgeInsetsA24 = EdgeInsets.all(24);
static const edgeInsetsR4 = EdgeInsets.only(right: 4);
static const edgeInsetsR8 = EdgeInsets.only(right: 8);
static const edgeInsetsR12 = EdgeInsets.only(right: 12);
static const edgeInsetsR20 = EdgeInsets.only(right: 20);
static const edgeInsetsR24 = EdgeInsets.only(right: 24);
static const edgeInsetsL4 = EdgeInsets.only(left: 4);
static const edgeInsetsL8 = EdgeInsets.only(left: 8);
static const edgeInsetsL12 = EdgeInsets.only(left: 12);
static const edgeInsetsL24 = EdgeInsets.only(left: 24);
static const edgeInsetsT4 = EdgeInsets.only(top: 4);
static const edgeInsetsT8 = EdgeInsets.only(top: 8);
static const edgeInsetsT12 = EdgeInsets.only(top: 12);
static const edgeInsetsT24 = EdgeInsets.only(top: 24);
static const edgeInsetsB4 = EdgeInsets.only(bottom: 4);
static const edgeInsetsB8 = EdgeInsets.only(bottom: 8);
static const edgeInsetsB12 = EdgeInsets.only(bottom: 12);
static const edgeInsetsB24 = EdgeInsets.only(bottom: 24);
static BorderRadius radius4 = BorderRadius.circular(4);
static BorderRadius radius8 = BorderRadius.circular(8);
static BorderRadius radius12 = BorderRadius.circular(12);
static BorderRadius radius24 = BorderRadius.circular(24);
static BorderRadius radius32 = BorderRadius.circular(32);
static BorderRadius radius48 = BorderRadius.circular(48);
/// 顶部状态栏的高度
static double get statusBarHeight => MediaQuery.of(Get.context!).padding.top;
/// 底部导航条的高度
static double get bottomBarHeight =>
MediaQuery.of(Get.context!).padding.bottom;
}

View File

@@ -0,0 +1,148 @@
import 'dart:async';
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class BaseController extends GetxController {
/// 加载中,更新页面
var pageLoadding = false.obs;
/// 加载中,不会更新页面
var loadding = false;
/// 空白页面
var pageEmpty = false.obs;
/// 页面错误
var pageError = false.obs;
/// 未登录
var notLogin = false.obs;
/// 错误信息
var errorMsg = "".obs;
Error? error;
/// 显示错误
/// * [msg] 错误信息
/// * [showPageError] 显示页面错误
/// * 只在第一页加载错误时showPageError=true后续页加载错误时使用Toast弹出通知
void handleError(Object exception, {bool showPageError = false}) {
Log.logPrint(exception);
var msg = exceptionToString(exception);
if (exception is Error) {
error = exception;
}
if (showPageError) {
pageError.value = true;
errorMsg.value = msg;
} else {
SmartDialog.showToast(exceptionToString(msg));
}
}
String exceptionToString(Object exception) {
return exception.toString().replaceAll("Exception:", "");
}
void onLogin() {}
void onLogout() {}
}
class BaseDataController<T> extends BaseController {
T? data;
Future loadData() async {
try {
if (loadding) return;
loadding = true;
pageError.value = false;
pageLoadding.value = true;
error = null;
var result = await getData();
data = result;
} catch (e) {
handleError(exceptionToString(e), showPageError: true);
} finally {
loadding = false;
pageLoadding.value = false;
}
}
Future<T?> getData() async {
return null;
}
}
class BasePageController<T> extends BaseController {
final ScrollController scrollController = ScrollController();
final EasyRefreshController easyRefreshController = EasyRefreshController();
int currentPage = 1;
int count = 0;
int maxPage = 0;
int pageSize = 24;
var canLoadMore = false.obs;
var list = <T>[].obs;
Future refreshData() async {
currentPage = 1;
list.clear();
await loadData();
}
Future loadData() async {
try {
if (loadding) return;
loadding = true;
pageError.value = false;
pageEmpty.value = false;
notLogin.value = false;
error = null;
pageLoadding.value = currentPage == 1;
var result = await getData(currentPage, pageSize);
//是否可以加载更多
if (result.isNotEmpty) {
currentPage++;
canLoadMore.value = true;
pageEmpty.value = false;
} else {
canLoadMore.value = false;
if (currentPage == 1) {
pageEmpty.value = true;
}
}
// 赋值数据
if (currentPage == 1) {
list.value = result;
} else {
list.addAll(result);
}
} catch (e) {
handleError(e, showPageError: currentPage == 1);
} finally {
loadding = false;
pageLoadding.value = false;
}
}
Future<List<T>> getData(int page, int pageSize) async {
return [];
}
void scrollToTopOrRefresh() {
if (scrollController.offset > 0) {
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 200),
curve: Curves.linear,
);
} else {
easyRefreshController.callRefresh();
}
}
}

269
lib/app/dialog_utils.dart Normal file
View File

@@ -0,0 +1,269 @@
import 'dart:io';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/utils.dart';
import 'package:get/get.dart';
import 'package:photo_view/photo_view_gallery.dart';
class DialogUtils {
/// 提示弹窗
/// - `content` 内容
/// - `title` 弹窗标题
/// - `confirm` 确认按钮内容,留空为确定
/// - `cancel` 取消按钮内容,留空为取消
static Future<bool> showAlertDialog(
String content, {
String title = '',
String confirm = '',
String cancel = '',
bool selectable = false,
bool barrierDismissible = true,
List<Widget>? actions,
}) async {
var result = await Get.dialog(
AlertDialog(
title: Text(title),
content: Container(
constraints: const BoxConstraints(
maxHeight: 400,
maxWidth: 500,
),
child: SingleChildScrollView(
child: Padding(
padding: AppStyle.edgeInsetsV12,
child: selectable ? SelectableText(content) : Text(content),
),
),
),
actions: [
TextButton(
onPressed: (() => Get.back(result: false)),
child: Text(cancel.isEmpty ? "取消" : cancel),
),
TextButton(
onPressed: (() => Get.back(result: true)),
child: Text(confirm.isEmpty ? "确定" : confirm),
),
...?actions,
],
),
barrierDismissible: barrierDismissible,
);
return result ?? false;
}
/// 提示弹窗
/// - `content` 内容
/// - `title` 弹窗标题
/// - `confirm` 确认按钮内容,留空为确定
static Future<bool> showMessageDialog(String content,
{String title = '', String confirm = '', bool selectable = false}) async {
var result = await Get.dialog(
AlertDialog(
title: Text(title),
content: Padding(
padding: AppStyle.edgeInsetsV12,
child: selectable ? SelectableText(content) : Text(content),
),
actions: [
TextButton(
onPressed: (() => Get.back(result: true)),
child: Text(confirm.isEmpty ? "确定" : confirm),
),
],
),
);
return result ?? false;
}
/// 文本编辑的弹窗
/// - `content` 编辑框默认的内容
/// - `title` 弹窗标题
/// - `confirm` 确认按钮内容
/// - `cancel` 取消按钮内容
static Future<String?> showEditTextDialog(String content,
{String title = '',
String? hintText,
String confirm = '',
String cancel = ''}) async {
final TextEditingController textEditingController =
TextEditingController(text: content);
var result = await Get.dialog(
AlertDialog(
title: Text(title),
content: Padding(
padding: AppStyle.edgeInsetsT12,
child: TextField(
controller: textEditingController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
//prefixText: title,
contentPadding: AppStyle.edgeInsetsA12,
hintText: hintText ?? title,
),
// style: TextStyle(
// height: 1.0,
// color: Get.isDarkMode ? Colors.white : Colors.black),
autofocus: true,
),
),
actions: [
TextButton(
onPressed: Get.back,
child: const Text("取消"),
),
TextButton(
onPressed: () {
Get.back(result: textEditingController.text);
},
child: const Text("确定"),
),
],
),
// barrierColor:
// Get.isDarkMode ? Colors.grey.withOpacity(.3) : Colors.black38,
);
return result;
}
static Future<T?> showOptionDialog<T>(
List<T> contents,
T value, {
String title = '',
}) async {
var result = await Get.dialog(
SimpleDialog(
title: Text(title),
children: contents
.map(
(e) => RadioListTile<T>(
title: Text(e.toString()),
value: e,
groupValue: value,
onChanged: (e) {
Get.back(result: e);
},
),
)
.toList(),
),
);
return result;
}
static void showStatement() async {
var text = await rootBundle.loadString("assets/statement.txt");
showAlertDialog(
text,
selectable: true,
title: "免责声明",
confirm: "已阅读并同意",
cancel: "退出",
barrierDismissible: false,
).then((value) {
if (!value) {
exit(0);
}
});
}
static Future<T?> showMapOptionDialog<T>(
Map<T, String> contents,
T value, {
String title = '',
}) async {
var result = await Get.dialog(
SimpleDialog(
title: Text(title),
children: contents.keys
.map(
(e) => RadioListTile<T>(
title: Text((contents[e] ?? '-').tr),
value: e,
groupValue: value,
onChanged: (e) {
Get.back(result: e);
},
),
)
.toList(),
),
);
return result;
}
static void showImageViewer(int initIndex, List<String> images) {
var index = initIndex.obs;
Get.dialog(
Scaffold(
backgroundColor: Colors.black87,
body: Stack(
children: [
PhotoViewGallery.builder(
itemCount: images.length,
builder: (_, i) {
if (images[i].startsWith("http")) {
return PhotoViewGalleryPageOptions(
filterQuality: FilterQuality.high,
imageProvider: ExtendedNetworkImageProvider(
images[i],
cache: true,
),
onTapUp: ((context, details, controllerValue) =>
Get.back()),
);
} else {
return PhotoViewGalleryPageOptions(
filterQuality: FilterQuality.high,
imageProvider: ExtendedMemoryImageProvider(
File(images[i]).readAsBytesSync(),
),
onTapUp: ((context, details, controllerValue) =>
Get.back()),
);
}
},
loadingBuilder: (context, event) => const Center(
child: CircularProgressIndicator(),
),
pageController: PageController(
initialPage: index.value,
),
onPageChanged: ((i) {
index.value = i;
}),
),
Container(
alignment: Alignment.bottomCenter,
margin: AppStyle.edgeInsetsA24
.copyWith(bottom: 24 + AppStyle.bottomBarHeight),
child: Obx(
() => Text(
"${index.value + 1}/${images.length}",
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white),
),
),
),
Positioned(
right: 12 + AppStyle.bottomBarHeight,
bottom: 12,
child: TextButton.icon(
onPressed: () {
Utils.saveImage(images[index.value]);
},
icon: const Icon(Icons.save),
label: const Text("保存"),
),
),
],
),
),
);
}
}

42
lib/app/event_bus.dart Normal file
View File

@@ -0,0 +1,42 @@
import 'dart:async';
import 'package:flutter_dmzj/app/log.dart';
/// 全局事件
class EventBus {
/// 点击了底部导航
static const String kBottomNavigationBarClicked =
"BottomNavigationBarClicked";
/// 更新了漫画记录
static const String kUpdatedComicHistory = "UpdateComicHistory";
/// 更新了小说记录
static const String kUpdatedNovelHistory = "UpdateNovelHistory";
static EventBus? _instance;
static EventBus get instance {
_instance ??= EventBus();
return _instance!;
}
final Map<String, StreamController> _streams = {};
/// 触发事件
void emit<T>(String name, T data) {
if (!_streams.containsKey(name)) {
_streams.addAll({name: StreamController.broadcast()});
}
Log.d("Emit Event$name\r\n$data");
_streams[name]!.add(data);
}
/// 监听事件
StreamSubscription<dynamic> listen(String name, Function(dynamic)? onData) {
if (!_streams.containsKey(name)) {
_streams.addAll({name: StreamController.broadcast()});
}
return _streams[name]!.stream.listen(onData);
}
}

12
lib/app/keys.dart Normal file
View File

@@ -0,0 +1,12 @@
// ignore_for_file: constant_identifier_names
class Keys {
/// APP相关设置的Hive Box名称
static const SETTINGS_BOX_NAME = "DmzjSettings";
/// 主题模式
static const SETTINGS_THEME_MODE = "ThemeMode";
/// 主题颜色
static const SETTINGS_THEME_COLOR = "ThemeColor";
}

39
lib/app/log.dart Normal file
View File

@@ -0,0 +1,39 @@
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
class Log {
static Logger logger = Logger(
printer: PrettyPrinter(
methodCount: 0,
errorMethodCount: 8,
lineLength: 120,
colors: true,
printEmojis: true,
printTime: false,
),
);
static d(String message) {
logger.d("${DateTime.now().toString()}\n$message");
}
static i(String message) {
logger.i("${DateTime.now().toString()}\n$message");
}
static e(String message, StackTrace stackTrace) {
logger.e("${DateTime.now().toString()}\n$message", stackTrace: stackTrace);
}
static w(String message) {
logger.w("${DateTime.now().toString()}\n$message");
}
static void logPrint(dynamic obj) {
if (obj is Error) {
Log.e(obj.toString(), obj.stackTrace ?? StackTrace.current);
} else if (kDebugMode) {
print(obj);
}
}
}

View File

@@ -0,0 +1,51 @@
import 'dart:io' show Platform;
import 'package:fluent_ui/fluent_ui.dart' as fluent;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
/// Windows平台检测与Fluent UI主题工具
class PlatformUtils {
/// Web平台不支持dart:io需先排除
static bool get isWindows => !kIsWeb && Platform.isWindows;
/// 根据当前Material主题生成对应的FluentThemeData
static fluent.FluentThemeData getFluentTheme(BuildContext context) {
final materialTheme = Theme.of(context);
final brightness = materialTheme.brightness;
final primary = materialTheme.colorScheme.primary;
// 根据primary color构建AccentColor渐变色阶
final accent = fluent.AccentColor.swatch({
'darkest': _darken(primary, 0.4),
'darker': _darken(primary, 0.2),
'dark': _darken(primary, 0.1),
'normal': primary,
'light': _lighten(primary, 0.15),
'lighter': _lighten(primary, 0.3),
'lightest': _lighten(primary, 0.5),
});
return fluent.FluentThemeData(
brightness: brightness,
accentColor: accent,
scaffoldBackgroundColor: brightness == Brightness.dark
? const Color(0xff202020)
: const Color(0xfff3f3f3),
);
}
static Color _darken(Color color, double amount) {
final hsl = HSLColor.fromColor(color);
return hsl
.withLightness((hsl.lightness - amount).clamp(0.0, 1.0))
.toColor();
}
static Color _lighten(Color color, double amount) {
final hsl = HSLColor.fromColor(color);
return hsl
.withLightness((hsl.lightness + amount).clamp(0.0, 1.0))
.toColor();
}
}

276
lib/app/utils.dart Normal file
View File

@@ -0,0 +1,276 @@
import 'dart:io';
import 'package:extended_image/extended_image.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/requests/common_request.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
// ignore: depend_on_referenced_packages
import 'package:path/path.dart' as p;
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher_string.dart';
class Utils {
static late PackageInfo packageInfo;
static DateFormat dateFormat = DateFormat("yyyy-MM-dd");
static DateFormat dateTimeFormat = DateFormat("MM-dd HH:mm");
static DateFormat dateTimeFormatWithYear = DateFormat("yyyy-MM-dd HH:mm");
/// 版本号解析
static int parseVersion(String version) {
var sp = version.split('.');
var num = "";
for (var item in sp) {
num = num + item.padLeft(2, '0');
}
return int.parse(num);
}
/// 时间戳格式化-秒
static String formatTimestamp(int ts) {
if (ts == 0) {
return "----";
}
return formatTimestampMS(ts * 1000);
}
static String formatTimestampToDate(int ts) {
if (ts == 0) {
return "----";
}
var dt = DateTime.fromMillisecondsSinceEpoch(ts * 1000);
return dateFormat.format(dt);
}
/// 时间戳格式化-毫秒
static String formatTimestampMS(int ts) {
var dt = DateTime.fromMillisecondsSinceEpoch(ts);
var dtNow = DateTime.now();
if (dt.year == dtNow.year &&
dt.month == dtNow.month &&
dt.day == dtNow.day) {
return "今天${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}";
}
if (dt.year == dtNow.year &&
dt.month == dtNow.month &&
dt.day == dtNow.day - 1) {
return "昨天${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}";
}
if (dt.year == dtNow.year) {
return dateTimeFormat.format(dt);
}
return dateTimeFormatWithYear.format(dt);
}
/// 检查相册权限
static Future<bool> checkPhotoPermission() async {
try {
var status = await Permission.photos.status;
if (status == PermissionStatus.granted) {
return true;
}
status = await Permission.photos.request();
if (status.isGranted) {
return true;
} else {
SmartDialog.showToast("请授予相册权限");
return false;
}
} catch (e) {
return false;
}
}
/// 保存图片
static void saveImage(String url) async {
if (Platform.isIOS && !await Utils.checkPhotoPermission()) {
return;
}
try {
Uint8List? data;
if (url.startsWith("http")) {
var provider = ExtendedNetworkImageProvider(url, cache: true);
data = await provider.getNetworkImageData();
} else {
data = await File(url).readAsBytes();
}
if (data == null) {
SmartDialog.showToast("图片保存失败");
return;
}
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) {
saveImageDetktop(p.basename(url), data);
} else {
var cacheDir = await getTemporaryDirectory();
var file = File(p.join(cacheDir.path, p.basename(url)));
await file.writeAsBytes(data);
final result = await ImageGallerySaverPlus.saveFile(
file.path,
name: p.basename(url),
isReturnPathOfIOS: true,
);
Log.d(result.toString());
SmartDialog.showToast("保存成功");
}
} catch (e) {
SmartDialog.showToast("保存失败");
}
}
/// 保存图片-桌面平台
static void saveImageDetktop(String fileName, Uint8List list) async {
final FileSaveLocation? location =
await getSaveLocation(suggestedName: fileName);
if (location == null) {
return;
}
final XFile file = XFile.fromData(list, name: fileName);
await file.saveTo(location.path);
}
/// 分享
static void share(String url, {String content = ""}) {
showModalBottomSheet(
context: Get.context!,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
constraints: const BoxConstraints(
maxWidth: 500,
),
useSafeArea: true,
backgroundColor: Get.theme.cardColor,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.copy),
title: const Text("复制链接"),
onTap: () {
Get.back();
Utils.copyText(url);
},
),
Visibility(
visible: content.isNotEmpty,
child: ListTile(
leading: const Icon(Icons.copy),
title: const Text("复制标题与链接"),
onTap: () {
Get.back();
Utils.copyText("$content\n$url");
},
),
),
ListTile(
leading: const Icon(Icons.public),
title: const Text("浏览器打开"),
onTap: () {
Get.back();
launchUrlString(url, mode: LaunchMode.externalApplication);
},
),
ListTile(
leading: const Icon(Icons.share),
title: const Text("系统分享"),
onTap: () {
Get.back();
Share.share(content.isEmpty ? url : "$content\n$url");
},
),
],
),
);
}
/// 检查更新
static void checkUpdate({bool showMsg = false}) async {
try {
int currentVer = Utils.parseVersion(packageInfo.version);
CommonRequest request = CommonRequest();
var versionInfo = await request.checkUpdate();
if (versionInfo.versionNum > currentVer) {
Get.dialog(
AlertDialog(
title: Text(
"发现新版本 ${versionInfo.version}",
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
content: Text(
versionInfo.versionDesc,
style: const TextStyle(fontSize: 14, height: 1.4),
),
actionsPadding: AppStyle.edgeInsetsH12,
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextButton(
onPressed: () {
Get.back();
},
child: const Text("取消"),
),
),
AppStyle.hGap12,
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
),
onPressed: () {
launchUrlString(
versionInfo.downloadUrl,
mode: LaunchMode.externalApplication,
);
},
child: const Text("更新"),
),
),
],
),
],
),
);
} else {
if (showMsg) {
SmartDialog.showToast("当前已经是最新版本了");
}
}
} catch (e) {
Log.logPrint(e);
if (showMsg) {
SmartDialog.showToast("检查更新失败");
}
}
}
/// 复制文本
static void copyText(String text) async {
try {
await Clipboard.setData(ClipboardData(text: text));
SmartDialog.showToast("已复制到剪切板");
} catch (e) {
SmartDialog.showToast(e.toString());
}
}
}