添加版本更新检测功能

This commit is contained in:
jyuesong
2022-01-21 13:39:00 +08:00
parent 6cc810b9b1
commit ae7368ecbc
8 changed files with 439 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -76,4 +76,6 @@ class Url {
}
return false;
}
static String checkUpdateUrl = "https://raw.githubusercontent.com/qinglong-app/qinglong_app/main/version";
}

View File

@@ -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<HomePage> {
initTitles();
_title = titles[0].title;
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
update();
});
}
@override
@@ -185,6 +189,16 @@ class _HomePageState extends ConsumerState<HomePage> {
),
);
}
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 {

View File

@@ -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<LoginPage> {
}
getIt<UserInfoViewModel>().updateToken("");
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
update();
if (useSecretLogin) {
cardKey.currentState?.toggleCard();
}
@@ -483,4 +485,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}
});
}
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();
}
}
}

View File

@@ -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<AboutPage> {
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

View File

@@ -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(),

395
lib/utils/update_utils.dart Normal file
View File

@@ -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<String?> 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<bool> show() {
try {
if (isShowing()) {
return Future<bool>.value(false);
}
showDialog<bool>(
context: _context,
barrierDismissible: false,
builder: (BuildContext context) {
return WillPopScope(onWillPop: () => Future<bool>.value(false), child: _widget);
});
_isShowing = true;
return Future<bool>.value(true);
} catch (err) {
_isShowing = false;
return Future<bool>.value(false);
}
}
/// 隐藏弹窗
Future<bool> dismiss() {
try {
if (_isShowing) {
_isShowing = false;
Navigator.pop(_context);
return Future<bool>.value(true);
} else {
return Future<bool>.value(false);
}
} catch (err) {
return Future<bool>.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<UpdateWidget> {
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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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;
}
}