From 390fa7d58bb427e0968d5c72fc5aaae6fe2f27cc Mon Sep 17 00:00:00 2001 From: niuhuan Date: Mon, 20 Nov 2023 16:17:09 +0800 Subject: [PATCH] :sparkles: check version upgrade --- ci/src/check_asset/main.rs | 3 +- ci/src/check_release/main.rs | 3 +- ci/src/upload_asset/main.rs | 3 +- lib/configs/configs.dart | 6 +- lib/configs/versions.dart | 71 +++++++++++++ lib/screens/about_screen.dart | 155 +++++++++++++++++++++++++++++ lib/screens/components/badged.dart | 79 +++++++++++++++ lib/screens/user_screen.dart | 38 +++++-- native/src/api.rs | 16 +++ 9 files changed, 359 insertions(+), 15 deletions(-) create mode 100644 lib/configs/versions.dart create mode 100644 lib/screens/about_screen.dart create mode 100644 lib/screens/components/badged.dart diff --git a/ci/src/check_asset/main.rs b/ci/src/check_asset/main.rs index 3b62565..d5e5526 100644 --- a/ci/src/check_asset/main.rs +++ b/ci/src/check_asset/main.rs @@ -16,7 +16,8 @@ async fn main() -> Result<()> { let target = std::env::var("TARGET")?; - let vs_code_txt = tokio::fs::read_to_string("version.code.txt").await?; + let vs_code_txt = + tokio::fs::read_to_string("../../../lib/version.txt/version.code.txt").await?; let code = vs_code_txt.trim(); diff --git a/ci/src/check_release/main.rs b/ci/src/check_release/main.rs index 38b1b35..d0ddae7 100644 --- a/ci/src/check_release/main.rs +++ b/ci/src/check_release/main.rs @@ -21,7 +21,8 @@ async fn main() -> Result<()> { panic!("Can't got repo branch"); } - let vs_code_txt = tokio::fs::read_to_string("version.code.txt").await?; + let vs_code_txt = + tokio::fs::read_to_string("../../../lib/version.txt/version.code.txt").await?; let vs_info_txt = tokio::fs::read_to_string("version.info.txt").await?; let code = vs_code_txt.trim(); diff --git a/ci/src/upload_asset/main.rs b/ci/src/upload_asset/main.rs index 1e1f569..21564c5 100644 --- a/ci/src/upload_asset/main.rs +++ b/ci/src/upload_asset/main.rs @@ -21,7 +21,8 @@ async fn main() -> Result<()> { let target = std::env::var("TARGET")?; - let vs_code_txt = tokio::fs::read_to_string("version.code.txt").await?; + let vs_code_txt = + tokio::fs::read_to_string("../../../lib/version.txt/version.code.txt").await?; let code = vs_code_txt.trim(); diff --git a/lib/configs/configs.dart b/lib/configs/configs.dart index 211740d..5a41738 100644 --- a/lib/configs/configs.dart +++ b/lib/configs/configs.dart @@ -1,10 +1,10 @@ - import 'package:kobi/configs/login.dart'; import 'package:kobi/configs/proxy.dart'; import 'package:kobi/configs/reader_controller_type.dart'; import 'package:kobi/configs/reader_direction.dart'; import 'package:kobi/configs/reader_slider_position.dart'; import 'package:kobi/configs/reader_type.dart'; +import 'package:kobi/configs/versions.dart'; import 'cache_time.dart'; @@ -16,4 +16,6 @@ Future initConfigs() async { await initReaderSliderPosition(); await initReaderType(); await initLogin(); -} \ No newline at end of file + await initVersion(); + autoCheckNewVersion(); +} diff --git a/lib/configs/versions.dart b/lib/configs/versions.dart new file mode 100644 index 0000000..ed3763d --- /dev/null +++ b/lib/configs/versions.dart @@ -0,0 +1,71 @@ +import 'dart:async' show Future; +import 'dart:convert'; +import 'package:event/event.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import '../ffi.io.dart'; +import '../screens/components/commons.dart'; + +const _versionUrl = + "https://api.github.com/repos/niuhuan/kobi/releases/latest"; +const _versionAssets = 'lib/assets/version.txt'; +RegExp _versionExp = RegExp(r"^v\d+\.\d+.\d+$"); + +late String _version; +String? _latestVersion; +String? _latestVersionInfo; + +Future initVersion() async { + // 当前版本 + try { + _version = (await rootBundle.loadString(_versionAssets)).trim(); + } catch (e) { + _version = "dirty"; + } +} + +var versionEvent = Event(); + +String currentVersion() { + return _version; +} + +String? get latestVersion => _latestVersion; + +String? latestVersionInfo() { + return _latestVersionInfo; +} + +Future autoCheckNewVersion() { + return _versionCheck(); +} + +Future manualCheckNewVersion(BuildContext context) async { + try { + defaultToast(context, "检查更新中"); + await _versionCheck(); + defaultToast(context, "检查更新成功"); + } catch (e) { + defaultToast(context, "检查更新失败 : $e"); + } +} + +bool dirtyVersion() { + return !_versionExp.hasMatch(_version); +} + +// maybe exception +Future _versionCheck() async { + if (_versionExp.hasMatch(_version)) { + var json = jsonDecode(await api.httpGet(url: _versionUrl)); + if (json["name"] != null) { + String latestVersion = (json["name"]); + if (latestVersion != _version) { + _latestVersion = latestVersion; + _latestVersionInfo = json["body"] ?? ""; + } + } + } // else dirtyVersion + versionEvent.broadcast(); + print("$_latestVersion"); +} diff --git a/lib/screens/about_screen.dart b/lib/screens/about_screen.dart new file mode 100644 index 0000000..61e0e84 --- /dev/null +++ b/lib/screens/about_screen.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import '../configs/login.dart'; +import '../configs/versions.dart'; +import '../cross.dart'; +import 'components/badged.dart'; + +const _releaseUrl = "https://github.com/niuhuan/kobi/releases/"; + +class AboutScreen extends StatefulWidget { + const AboutScreen({Key? key}) : super(key: key); + + @override + State createState() { + return _AboutState(); + } +} + +class _AboutState extends State { + @override + void initState() { + loginEvent.subscribe(_l); + super.initState(); + } + + @override + void dispose() { + loginEvent.unsubscribe(_l); + super.dispose(); + } + + _l(_) { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("关于"), + ), + body: ListView( + children: [ + const Divider(), + _buildLogo(), + const Divider(), + _buildCurrentVersion(), + const Divider(), + _buildNewestVersion(), + const Divider(), + _buildGotoGithub(), + const Divider(), + _buildVersionText(), + const Divider(), + ], + ), + ); + } + + Widget _buildLogo() { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + double? width, height; + if (constraints.maxWidth < constraints.maxHeight) { + width = constraints.maxWidth / 2; + } else { + height = constraints.maxHeight / 2; + } + return Container( + padding: const EdgeInsets.all(10), + child: Center( + child: SizedBox( + width: width, + height: height, + child: Container(), + ), + ), + ); + }, + ); + } + + Widget _buildCurrentVersion() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), + child: Text("当前版本 : ${currentVersion()}"), + ); + } + + Widget _buildNewestVersion() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), + child: Text.rich(TextSpan( + children: [ + const TextSpan(text: "最新版本 : "), + _buildNewestVersionSpan(), + _buildCheckButton(), + ], + )), + ); + } + + InlineSpan _buildNewestVersionSpan() { + return WidgetSpan( + child: Container( + padding: const EdgeInsets.only(right: 20), + child: VersionBadged( + child: Text( + "${latestVersion ?? "没有检测到新版本"} ", + ), + ), + ), + ); + } + + InlineSpan _buildCheckButton() { + return WidgetSpan( + child: GestureDetector( + child: const Text( + "检查更新", + style: TextStyle(height: 1.3, color: Colors.blue), + strutStyle: StrutStyle(height: 1.3), + ), + onTap: () { + manualCheckNewVersion(context); + }, + ), + ); + } + + Widget _buildGotoGithub() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), + child: GestureDetector( + child: const Text( + "去下载地址", + style: TextStyle(color: Colors.blue), + ), + onTap: () { + openUrl(_releaseUrl); + }, + ), + ); + } + + Widget _buildVersionText() { + var info = latestVersionInfo(); + if (info != null) { + return Container( + padding: const EdgeInsets.all(20), + child: SelectableText("更新内容\n\n$info"), + ); + } + return Container(); + } +} diff --git a/lib/screens/components/badged.dart b/lib/screens/components/badged.dart new file mode 100644 index 0000000..d8a1a57 --- /dev/null +++ b/lib/screens/components/badged.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import '../../configs/versions.dart'; + +// 提示信息, 组件右上角的小红点 +class Badged extends StatelessWidget { + final String? badge; + final Widget child; + + const Badged({Key? key, required this.child, this.badge}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (badge == null) { + return child; + } + return Stack( + children: [ + child, + Positioned( + right: 0, + child: Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(6), + ), + constraints: const BoxConstraints( + minWidth: 12, + minHeight: 12, + ), + child: Text( + badge!, + style: const TextStyle( + color: Colors.white, + fontSize: 8, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ); + } +} + +class VersionBadged extends StatefulWidget { + final Widget child; + + const VersionBadged({required this.child, Key? key}) : super(key: key); + + @override + State createState() => _VersionBadgedState(); +} + +class _VersionBadgedState extends State { + @override + void initState() { + versionEvent.subscribe(_onVersion); + super.initState(); + } + + @override + void dispose() { + versionEvent.unsubscribe(_onVersion); + super.dispose(); + } + + void _onVersion(dynamic a) { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Badged( + child: widget.child, + badge: latestVersion == null ? null : "1", + ); + } +} diff --git a/lib/screens/user_screen.dart b/lib/screens/user_screen.dart index 2121315..83e44ea 100644 --- a/lib/screens/user_screen.dart +++ b/lib/screens/user_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:kobi/configs/login.dart'; +import 'package:kobi/screens/about_screen.dart'; import 'package:kobi/screens/collected_comics_account_screen.dart'; +import 'package:kobi/screens/components/badged.dart'; import 'package:kobi/screens/components/commons.dart'; import 'package:kobi/screens/downloads_screen.dart'; import 'package:kobi/screens/histories_screen.dart'; @@ -18,14 +20,30 @@ class UserScreen extends StatelessWidget { appBar: AppBar( title: const Text('偏好'), actions: [ - IconButton( - onPressed: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) { - return const SettingsScreen(); - })); - }, - icon: const Icon(Icons.settings), - ) + VersionBadged( + child: IconButton( + onPressed: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) { + return const AboutScreen(); + })); + }, + icon: const Icon(Icons.info_outline), + ), + ), + Stack( + children: [ + IconButton( + onPressed: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) { + return const SettingsScreen(); + })); + }, + icon: const Icon(Icons.settings), + ), + ], + ), ], ), body: ListView( @@ -45,7 +63,7 @@ class UserScreen extends StatelessWidget { }, title: const Text('历史记录(本地)'), ), - Divider(), + const Divider(), ListTile( onTap: () { Navigator.push( @@ -56,7 +74,7 @@ class UserScreen extends StatelessWidget { }, title: const Text('下载列表(本地)'), ), - Divider(), + const Divider(), ListTile( onTap: () { Navigator.push( diff --git a/native/src/api.rs b/native/src/api.rs index 9504ef2..3c47bc5 100644 --- a/native/src/api.rs +++ b/native/src/api.rs @@ -597,6 +597,22 @@ pub fn download_set_pause(pause: bool) -> Result<()> { Ok(block_on(downloading::download_set_pause(pause))) } +pub fn http_get(url: String) -> Result { + block_on(http_get_inner(url)) +} + +async fn http_get_inner(url: String) -> Result { + Ok(reqwest::ClientBuilder::new() + .user_agent("kobi") + .build()? + .get(url) + .send() + .await? + .error_for_status()? + .text() + .await?) +} + pub fn desktop_root() -> Result { #[cfg(target_os = "windows")] {