diff --git a/assets/images/update_bg_app_top.png b/assets/images/update_bg_app_top.png new file mode 100644 index 0000000..d97da82 Binary files /dev/null and b/assets/images/update_bg_app_top.png differ diff --git a/assets/images/update_ic_close.png b/assets/images/update_ic_close.png new file mode 100644 index 0000000..29e55e9 Binary files /dev/null and b/assets/images/update_ic_close.png differ diff --git a/lib/base/http/url.dart b/lib/base/http/url.dart index 4b6345d..6e919ab 100644 --- a/lib/base/http/url.dart +++ b/lib/base/http/url.dart @@ -76,4 +76,6 @@ class Url { } return false; } + + static String checkUpdateUrl = "https://raw.githubusercontent.com/qinglong-app/qinglong_app/main/version"; } diff --git a/lib/module/home/home_page.dart b/lib/module/home/home_page.dart index 4bee986..ca37d09 100644 --- a/lib/module/home/home_page.dart +++ b/lib/module/home/home_page.dart @@ -9,6 +9,7 @@ import 'package:qinglong_app/module/env/env_page.dart'; import 'package:qinglong_app/module/others/other_page.dart'; import 'package:qinglong_app/module/task/task_page.dart'; import 'package:move_to_background/move_to_background.dart'; +import 'package:qinglong_app/utils/update_utils.dart'; class HomePage extends ConsumerStatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -30,6 +31,9 @@ class _HomePageState extends ConsumerState { initTitles(); _title = titles[0].title; super.initState(); + WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { + update(); + }); } @override @@ -185,6 +189,16 @@ class _HomePageState extends ConsumerState { ), ); } + + void update() async { + String? result = await UpdateUtils().checkUpdate(); + if (result != null && result.isNotEmpty) { + UpdateDialog updateDialog = UpdateDialog(context, title: "发现新版本", updateContent: "版本号:v$result", onUpdate: () { + UpdateUtils.launchURL(result); + }); + updateDialog.show(); + } + } } class IndexBean { diff --git a/lib/module/login/login_page.dart b/lib/module/login/login_page.dart index 8b59a60..68f7ab9 100644 --- a/lib/module/login/login_page.dart +++ b/lib/module/login/login_page.dart @@ -9,6 +9,7 @@ import 'package:qinglong_app/base/userinfo_viewmodel.dart'; import 'package:qinglong_app/main.dart'; import 'package:qinglong_app/module/login/user_bean.dart'; import 'package:qinglong_app/utils/extension.dart'; +import 'package:qinglong_app/utils/update_utils.dart'; import 'package:qinglong_app/utils/utils.dart'; import 'package:flip_card/flip_card.dart'; @@ -57,6 +58,7 @@ class _LoginPageState extends ConsumerState { } getIt().updateToken(""); WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { + update(); if (useSecretLogin) { cardKey.currentState?.toggleCard(); } @@ -483,4 +485,14 @@ class _LoginPageState extends ConsumerState { } }); } + + void update() async { + String? result = await UpdateUtils().checkUpdate(); + if (result != null && result.isNotEmpty) { + UpdateDialog updateDialog = UpdateDialog(context, title: "发现新版本", updateContent: "版本号:v${result}", onUpdate: () { + UpdateUtils.launchURL(result); + }); + updateDialog.show(); + } + } } diff --git a/lib/module/others/about_page.dart b/lib/module/others/about_page.dart index 6a21fcd..083fe9c 100644 --- a/lib/module/others/about_page.dart +++ b/lib/module/others/about_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qinglong_app/base/ql_app_bar.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:qinglong_app/base/theme.dart'; +import 'package:qinglong_app/utils/update_utils.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../main.dart'; @@ -23,6 +24,20 @@ class _AboutPageState extends ConsumerState { void initState() { super.initState(); getInfo(); + + WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { + update(); + }); + } + + void update() async { + String? result = await UpdateUtils().checkUpdate(true); + if (result != null && result.isNotEmpty) { + UpdateDialog updateDialog = UpdateDialog(context, title: "发现新版本", updateContent: "版本号:v${result}", onUpdate: () { + UpdateUtils.launchURL(result); + }); + updateDialog.show(); + } } @override diff --git a/lib/module/task/task_page.dart b/lib/module/task/task_page.dart index cf5c532..8ccd229 100644 --- a/lib/module/task/task_page.dart +++ b/lib/module/task/task_page.dart @@ -270,6 +270,7 @@ class TaskItemCell extends StatelessWidget { child: Text( bean.name ?? "", maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( overflow: TextOverflow.ellipsis, color: ref.watch(themeProvider).themeColor.titleColor(), diff --git a/lib/utils/update_utils.dart b/lib/utils/update_utils.dart new file mode 100644 index 0000000..2cd9f98 --- /dev/null +++ b/lib/utils/update_utils.dart @@ -0,0 +1,395 @@ +import 'dart:math'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:qinglong_app/base/http/url.dart'; +import 'package:qinglong_app/utils/extension.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../main.dart'; + +/// @author NewTab + +class UpdateUtils { + Future checkUpdate([bool remind = false]) async { + try { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + String version = packageInfo.version; + String url = Url.checkUpdateUrl; + Dio dio = Dio(BaseOptions( + connectTimeout: 50000, + receiveTimeout: 50000, + sendTimeout: 50000, + )); + var response = await dio.get(url); + if (response.statusCode == 200) { + if (version == response.data as String) { + return null; + } + return response.data as String; + } + } on DioError catch (e) { + if (remind) { + "无法连接到github服务器,版本更新检测失败".toast(); + } + logger.e(e); + return null; + } + } + + static void launchURL(String version) async { + try { + await launch("https://github.com/qinglong-app/qinglong_app/releases/download/v$version/app-release-v$version.apk"); + } catch (e) { + logger.e(e); + } + } +} + +///版本更新加提示框 +class UpdateDialog { + bool _isShowing = false; + late BuildContext _context; + late UpdateWidget _widget; + + UpdateDialog(BuildContext context, + {double width = 0.0, + required String title, + required String updateContent, + required VoidCallback onUpdate, + double titleTextSize = 16.0, + double contentTextSize = 14.0, + double buttonTextSize = 14.0, + double progress = -1.0, + Color progressBackgroundColor = const Color(0xFFFFCDD2), + Image? topImage, + double extraHeight = 5.0, + double radius = 4.0, + Color themeColor = Colors.red, + bool enableIgnore = false, + VoidCallback? onIgnore, + bool isForce = false, + String? updateButtonText, + String? ignoreButtonText, + VoidCallback? onClose}) { + _context = context; + _widget = UpdateWidget( + width: width, + title: title, + updateContent: updateContent, + onUpdate: onUpdate, + titleTextSize: titleTextSize, + contentTextSize: contentTextSize, + buttonTextSize: buttonTextSize, + progress: progress, + topImage: topImage, + extraHeight: extraHeight, + radius: radius, + themeColor: themeColor, + progressBackgroundColor: progressBackgroundColor, + enableIgnore: enableIgnore, + onIgnore: onIgnore, + isForce: isForce, + updateButtonText: updateButtonText ?? '更新', + ignoreButtonText: ignoreButtonText ?? '忽略此版本', + onClose: onClose ?? () => dismiss()); + } + + /// 显示弹窗 + Future show() { + try { + if (isShowing()) { + return Future.value(false); + } + showDialog( + context: _context, + barrierDismissible: false, + builder: (BuildContext context) { + return WillPopScope(onWillPop: () => Future.value(false), child: _widget); + }); + _isShowing = true; + return Future.value(true); + } catch (err) { + _isShowing = false; + return Future.value(false); + } + } + + /// 隐藏弹窗 + Future dismiss() { + try { + if (_isShowing) { + _isShowing = false; + Navigator.pop(_context); + return Future.value(true); + } else { + return Future.value(false); + } + } catch (err) { + return Future.value(false); + } + } + + /// 是否显示 + bool isShowing() { + return _isShowing; + } + + /// 更新进度 + void update(double progress) { + if (isShowing()) { + _widget.update(progress); + } + } + + /// 显示版本更新提示框 + static UpdateDialog showUpdate(BuildContext context, + {double width = 0.0, + required String title, + required String updateContent, + required VoidCallback onUpdate, + double titleTextSize = 16.0, + double contentTextSize = 14.0, + double buttonTextSize = 14.0, + double progress = -1.0, + Color progressBackgroundColor = const Color(0xFFFFCDD2), + Image? topImage, + double extraHeight = 5.0, + double radius = 4.0, + Color themeColor = Colors.red, + bool enableIgnore = false, + VoidCallback? onIgnore, + String? updateButtonText, + String? ignoreButtonText, + bool isForce = false}) { + final UpdateDialog dialog = UpdateDialog(context, + width: width, + title: title, + updateContent: updateContent, + onUpdate: onUpdate, + titleTextSize: titleTextSize, + contentTextSize: contentTextSize, + buttonTextSize: buttonTextSize, + progress: progress, + topImage: topImage, + extraHeight: extraHeight, + radius: radius, + themeColor: themeColor, + progressBackgroundColor: progressBackgroundColor, + enableIgnore: enableIgnore, + isForce: isForce, + updateButtonText: updateButtonText, + ignoreButtonText: ignoreButtonText, + onIgnore: onIgnore); + dialog.show(); + return dialog; + } +} + +// ignore: must_be_immutable +class UpdateWidget extends StatefulWidget { + /// 对话框的宽度 + final double width; + + /// 升级标题 + final String title; + + /// 更新内容 + final String updateContent; + + /// 标题文字的大小 + final double titleTextSize; + + /// 更新文字内容的大小 + final double contentTextSize; + + /// 按钮文字的大小 + final double buttonTextSize; + + /// 顶部图片 + final Widget? topImage; + + /// 拓展高度(适配顶部图片高度不一致的情况) + final double extraHeight; + + /// 边框圆角大小 + final double radius; + + /// 主题颜色 + final Color themeColor; + + /// 更新事件 + final VoidCallback onUpdate; + + /// 可忽略更新 + final bool enableIgnore; + + /// 更新事件 + final VoidCallback? onIgnore; + + double progress; + + /// 进度条的背景颜色 + final Color progressBackgroundColor; + + /// 更新事件 + final VoidCallback? onClose; + + /// 是否是强制更新 + final bool isForce; + + /// 更新按钮内容 + final String updateButtonText; + + /// 忽略按钮内容 + final String ignoreButtonText; + + UpdateWidget( + {Key? key, + this.width = 0.0, + required this.title, + required this.updateContent, + required this.onUpdate, + this.titleTextSize = 16.0, + this.contentTextSize = 14.0, + this.buttonTextSize = 14.0, + this.progress = -1.0, + this.progressBackgroundColor = const Color(0xFFFFCDD2), + this.topImage, + this.extraHeight = 5.0, + this.radius = 4.0, + this.themeColor = Colors.red, + this.enableIgnore = false, + this.onIgnore, + this.isForce = false, + this.updateButtonText = '更新', + this.ignoreButtonText = '忽略此版本', + this.onClose}) + : super(key: key); + + final _UpdateWidgetState _state = _UpdateWidgetState(); + + void update(double progress) { + _state.update(progress); + } + + @override + _UpdateWidgetState createState() => _state; +} + +class _UpdateWidgetState extends State { + void update(double progress) { + if (!mounted) { + return; + } + setState(() { + widget.progress = progress; + }); + } + + @override + Widget build(BuildContext context) { + final double dialogWidth = widget.width <= 0 ? getFitWidth(context) * 0.618 : widget.width; + return Material( + type: MaterialType.transparency, + child: SizedBox( + width: dialogWidth, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: dialogWidth, + child: widget.topImage ?? Image.asset('assets/images/update_bg_app_top.png', fit: BoxFit.fill), + ), + Container( + width: dialogWidth, + alignment: Alignment.center, + padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 8), + decoration: ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(widget.radius), bottomRight: Radius.circular(widget.radius)), + ), + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.only(top: widget.extraHeight), + child: Text(widget.title, style: TextStyle(fontSize: widget.titleTextSize, color: Colors.black)), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text(widget.updateContent, style: TextStyle(fontSize: widget.contentTextSize, color: const Color(0xFF666666))), + ), + Column(children: [ + FractionallySizedBox( + widthFactor: 1, + child: ElevatedButton( + style: ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + textStyle: MaterialStateProperty.all(TextStyle(fontSize: widget.buttonTextSize)), + foregroundColor: MaterialStateProperty.all(Colors.white), + shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))), + elevation: MaterialStateProperty.all(0), + backgroundColor: MaterialStateProperty.all(widget.themeColor), + ), + child: Text(widget.updateButtonText), + onPressed: widget.onUpdate, + ), + ), + if (widget.enableIgnore && widget.onIgnore != null) + FractionallySizedBox( + widthFactor: 1, + child: TextButton( + style: ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + textStyle: MaterialStateProperty.all(TextStyle(fontSize: widget.buttonTextSize)), + foregroundColor: MaterialStateProperty.all(const Color(0xFF666666)), + shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))), + ), + child: Text(widget.ignoreButtonText), + onPressed: widget.onIgnore, + )) + else + const SizedBox() + ]), + ], + )), + ), + if (!widget.isForce) + Column(children: [ + const SizedBox(width: 1.5, height: 50, child: DecoratedBox(decoration: BoxDecoration(color: Colors.white))), + IconButton( + iconSize: 30, + constraints: const BoxConstraints(maxHeight: 30, maxWidth: 30), + padding: EdgeInsets.zero, + icon: Image.asset( + 'assets/images/update_ic_close.png', + ), + onPressed: widget.onClose, + ) + ]) + else + const SizedBox() + ], + ), + )); + } + + double getFitWidth(BuildContext context) { + return min(getScreenHeight(context), getScreenWidth(context)); + } + + double getScreenHeight(BuildContext context) { + return MediaQuery.of(context).size.height; + } + + double getScreenWidth(BuildContext context) { + return MediaQuery.of(context).size.width; + } +}