v1.0.1
This commit is contained in:
41
lib/app/app_color.dart
Normal file
41
lib/app/app_color.dart
Normal 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
27
lib/app/app_constant.dart
Normal 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
13
lib/app/app_error.dart
Normal 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
167
lib/app/app_style.dart
Normal 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;
|
||||
}
|
||||
148
lib/app/controller/base_controller.dart
Normal file
148
lib/app/controller/base_controller.dart
Normal 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
269
lib/app/dialog_utils.dart
Normal 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
42
lib/app/event_bus.dart
Normal 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
12
lib/app/keys.dart
Normal 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
39
lib/app/log.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
lib/app/platform_utils.dart
Normal file
51
lib/app/platform_utils.dart
Normal 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
276
lib/app/utils.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user