mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 18:10:29 +00:00
feat: auto updater (#933)
This commit is contained in:
30 files changed
+1209
-245
No files matched your search
@@ -69,5 +69,7 @@ windows/flutter/generated_plugins.cmake
|
||||
database.hive
|
||||
database.lock
|
||||
|
||||
assets/host/host
|
||||
assets/host/host.exe
|
||||
assets/exec/host
|
||||
assets/exec/host.exe
|
||||
assets/exec/updater
|
||||
assets/exec/updater.exe
|
||||
File renamed without changes.
@@ -24,9 +24,11 @@ import '../../../../database/database.dart';
|
||||
import '../../../../database/entity.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../../util/github_mirror.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
import '../../../../util/log_util.dart';
|
||||
import '../../../../util/package_info.dart';
|
||||
import '../../../../util/updater.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../create/dto/create_router_params.dart';
|
||||
@@ -35,47 +37,22 @@ import '../../redirect/views/redirect_view.dart';
|
||||
const unixSocketPath = 'gopeed.sock';
|
||||
|
||||
const allTrackerSubscribeUrls = [
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_http.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_https.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ip.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_udp.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ws.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best_ip.txt',
|
||||
'https://github.com/XIU2/TrackersListCollection/raw/master/all.txt',
|
||||
'https://github.com/XIU2/TrackersListCollection/raw/master/best.txt',
|
||||
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_http.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_https.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_udp.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ws.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt',
|
||||
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/all.txt',
|
||||
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/best.txt',
|
||||
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/http.txt',
|
||||
];
|
||||
const allTrackerCdns = [
|
||||
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||
["https://fastly.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
|
||||
// ghproxy: https://ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
[
|
||||
"https://ghproxy.com/https://raw.githubusercontent.com",
|
||||
r".*github.com(/.*)/raw(/.*)"
|
||||
],
|
||||
// mirror.ghproxy.com: https://mirror.ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
[
|
||||
"https://mirror.ghproxy.com/https://raw.githubusercontent.com",
|
||||
r".*github.com(/.*)/raw(/.*)"
|
||||
],
|
||||
];
|
||||
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
||||
key: (v) => v as String,
|
||||
value: (v) {
|
||||
final ret = [v as String];
|
||||
for (final cdn in allTrackerCdns) {
|
||||
final reg = RegExp(cdn[1]);
|
||||
final match = reg.firstMatch(v.toString());
|
||||
var matchStr = "";
|
||||
for (var i = 1; i <= match!.groupCount; i++) {
|
||||
matchStr += match.group(i)!;
|
||||
}
|
||||
ret.add("${cdn[0]}$matchStr");
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
final allTrackerSubscribeUrlCdns = {
|
||||
for (var v in allTrackerSubscribeUrls)
|
||||
v: githubMirrorUrls(v, MirrorType.githubSource)
|
||||
};
|
||||
|
||||
class AppController extends GetxController with WindowListener, TrayListener {
|
||||
static StartConfig? _defaultStartConfig;
|
||||
@@ -109,6 +86,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
_initLaunchAtStartup().onError((error, stackTrace) =>
|
||||
logger.w("initLaunchAtStartup error", error, stackTrace));
|
||||
|
||||
_initCheckUpdate().onError((error, stackTrace) =>
|
||||
logger.w("initCheckUpdate error", error, stackTrace));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -525,6 +505,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
autoStartup.value = await launchAtStartup.isEnabled();
|
||||
}
|
||||
|
||||
Future<void> _initCheckUpdate() async {
|
||||
final versionInfo = await checkUpdate();
|
||||
if (versionInfo != null) {
|
||||
await showUpdateDialog(Get.context!, versionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveConfig() async {
|
||||
Database.instance.saveStartConfig(StartConfigEntity(
|
||||
network: startConfig.value.network,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/api.dart';
|
||||
|
||||
import '../../../../util/updater.dart';
|
||||
|
||||
class SettingController extends GetxController {
|
||||
final tapStatues = <String, bool>{}.obs;
|
||||
final latestVersion = "".obs;
|
||||
final latestVersion = Rxn<VersionInfo>();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@@ -26,23 +25,6 @@ class SettingController extends GetxController {
|
||||
|
||||
// fetch latest version
|
||||
void fetchLatestVersion() async {
|
||||
String? releaseDataStr;
|
||||
try {
|
||||
releaseDataStr = (await proxyRequest(
|
||||
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
|
||||
.data;
|
||||
} catch (e) {
|
||||
releaseDataStr =
|
||||
(await proxyRequest("https://gopeed.com/api/release")).data;
|
||||
}
|
||||
if (releaseDataStr == null) {
|
||||
return;
|
||||
}
|
||||
final releaseData = jsonDecode(releaseDataStr);
|
||||
final tagName = releaseData["tag_name"];
|
||||
if (tagName == null) {
|
||||
return;
|
||||
}
|
||||
latestVersion.value = releaseData["tag_name"].substring(1);
|
||||
latestVersion.value = await checkUpdate();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import '../../../../util/log_util.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/package_info.dart';
|
||||
import '../../../../util/scheme_register/scheme_register.dart';
|
||||
import '../../../../util/updater.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../views/check_list_view.dart';
|
||||
import '../../../views/directory_selector.dart';
|
||||
@@ -514,29 +515,7 @@ class SettingView extends GetView<SettingController> {
|
||||
}
|
||||
|
||||
buildVersion() {
|
||||
bool isNewVersion(String current, String latest) {
|
||||
if (latest == "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
final v1Parts = current.split('.');
|
||||
final v2Parts = latest.split('.');
|
||||
|
||||
for (var i = 0; i < 3; i++) {
|
||||
final v1Part = int.parse(v1Parts[i]);
|
||||
final v2Part = int.parse(v2Parts[i]);
|
||||
|
||||
if (v1Part < v2Part) {
|
||||
return true;
|
||||
} else if (v1Part > v2Part) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasNewVersion =
|
||||
isNewVersion(packageInfo.version, controller.latestVersion.value);
|
||||
var hasNewVersion = controller.latestVersion.value != null;
|
||||
return ListTile(
|
||||
title: hasNewVersion
|
||||
? badges.Badge(
|
||||
@@ -546,27 +525,7 @@ class SettingView extends GetView<SettingController> {
|
||||
subtitle: Text(packageInfo.version),
|
||||
onTap: () {
|
||||
if (hasNewVersion) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: Text('newVersionTitle'.trParams(
|
||||
{'version': controller.latestVersion.value})),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Text('newVersionLater'.tr),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
launchUrl(Uri.parse('https://gopeed.com'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
},
|
||||
child: Text('newVersionUpdate'.tr),
|
||||
),
|
||||
],
|
||||
));
|
||||
showUpdateDialog(context, controller.latestVersion.value!);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
File renamed without changes.
@@ -15,6 +15,7 @@ import 'langs/zh_cn.dart';
|
||||
import 'langs/zh_tw.dart';
|
||||
import 'langs/es_es.dart';
|
||||
import 'langs/uk_ua.dart';
|
||||
import 'langs/hu_hu.dart';
|
||||
|
||||
final messages = _Messages();
|
||||
|
||||
@@ -37,5 +38,6 @@ class _Messages extends Translations {
|
||||
...frFR,
|
||||
...esES,
|
||||
...ukUA,
|
||||
...huHU,
|
||||
};
|
||||
}
|
||||
+27
-18
@@ -16,6 +16,7 @@ import 'util/locale_manager.dart';
|
||||
import 'util/log_util.dart';
|
||||
import 'util/package_info.dart';
|
||||
import 'util/scheme_register/scheme_register.dart';
|
||||
import 'util/updater.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
class Args {
|
||||
@@ -93,26 +94,34 @@ Future<void> init(Args args) async {
|
||||
}
|
||||
|
||||
() async {
|
||||
try {
|
||||
registerUrlScheme("gopeed");
|
||||
if (controller.downloaderConfig.value.extra.defaultBtClient) {
|
||||
registerDefaultTorrentClient();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("register scheme fail", e);
|
||||
}
|
||||
|
||||
try {
|
||||
await installHost();
|
||||
} catch (e) {
|
||||
logger.e("browser extension host binary install fail", e);
|
||||
}
|
||||
for (final browser in Browser.values) {
|
||||
if (Util.isDesktop()) {
|
||||
try {
|
||||
await installManifest(browser);
|
||||
registerUrlScheme("gopeed");
|
||||
if (controller.downloaderConfig.value.extra.defaultBtClient) {
|
||||
registerDefaultTorrentClient();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e(
|
||||
"browser [${browser.name}] extension host integration fail", e);
|
||||
logger.e("register scheme fail", e);
|
||||
}
|
||||
|
||||
try {
|
||||
await installHost();
|
||||
} catch (e) {
|
||||
logger.e("browser extension host binary install fail", e);
|
||||
}
|
||||
for (final browser in Browser.values) {
|
||||
try {
|
||||
await installManifest(browser);
|
||||
} catch (e) {
|
||||
logger.e(
|
||||
"browser [${browser.name}] extension host integration fail", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await installUpdater();
|
||||
} catch (e) {
|
||||
logger.e("updater install fail", e);
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'arch_stub.dart'
|
||||
if (dart.library.io) 'entry/arch_native.dart'
|
||||
if (dart.library.html) 'entry/arch_web.dart';
|
||||
|
||||
// Copy from pkg/sky_engine/lib/ffi/abi.dart
|
||||
enum Architecture {
|
||||
arm,
|
||||
arm64,
|
||||
ia32,
|
||||
x64,
|
||||
riscv32,
|
||||
riscv64,
|
||||
}
|
||||
|
||||
Architecture getArch() => doGetArch();
|
||||
@@ -0,0 +1,3 @@
|
||||
import 'arch.dart';
|
||||
|
||||
Architecture doGetArch() => throw UnimplementedError();
|
||||
@@ -0,0 +1,13 @@
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:ffi';
|
||||
|
||||
import '../arch.dart';
|
||||
|
||||
Architecture doGetArch() {
|
||||
final currentAbi = Abi.current().toString();
|
||||
final archName = currentAbi.split("_")[1];
|
||||
final arch = Architecture.values.firstWhere(
|
||||
(element) => element.name == archName,
|
||||
orElse: () => Architecture.x64);
|
||||
return arch;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import '../arch.dart';
|
||||
|
||||
Architecture doGetArch() {
|
||||
return Architecture.x64;
|
||||
}
|
||||
+6
-35
@@ -1,10 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:win32_registry/win32_registry.dart';
|
||||
|
||||
import '../../util.dart';
|
||||
import '../../win32.dart';
|
||||
import '../browser_extension_host.dart';
|
||||
|
||||
@@ -26,32 +26,9 @@ const _firefoxNativeHostsKey = r'Software\Mozilla\NativeMessagingHosts';
|
||||
|
||||
/// Install host binary for browser extension
|
||||
Future<void> doInstallHost() async {
|
||||
final hostPath = _joinExePath(_hostExecName);
|
||||
|
||||
Future<List<int>> getHostAssetData() async {
|
||||
final hostAsset = await rootBundle.load('assets/host/$_hostExecName');
|
||||
return hostAsset.buffer
|
||||
.asUint8List(hostAsset.offsetInBytes, hostAsset.lengthInBytes);
|
||||
}
|
||||
|
||||
// Check if host binary is not installed
|
||||
if (!await File(hostPath).exists()) {
|
||||
final hostData = await getHostAssetData();
|
||||
final file = File(hostPath);
|
||||
await file.writeAsBytes(hostData);
|
||||
// Add execute permission
|
||||
if (!Platform.isWindows) {
|
||||
await Process.run('chmod', ['+x', hostPath]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Check if host binary is outdated
|
||||
final hostAssetData = await getHostAssetData();
|
||||
final hostFileData = await File(hostPath).readAsBytes();
|
||||
if (hostAssetData.length != hostFileData.length) {
|
||||
await File(hostPath).writeAsBytes(hostAssetData);
|
||||
return;
|
||||
}
|
||||
final hostPath = Util.homePathJoin(_hostExecName);
|
||||
await Util.installAsset('assets/exec/$_hostExecName', hostPath,
|
||||
executable: true);
|
||||
}
|
||||
|
||||
/// Check if specified browser is installed
|
||||
@@ -236,7 +213,7 @@ String? _getManifestPath(Browser browser) {
|
||||
final manifestName =
|
||||
browser == Browser.firefox ? '$_hostName.moz.json' : '$_hostName.json';
|
||||
if (Platform.isWindows) {
|
||||
return _joinExePath(manifestName);
|
||||
return Util.homePathJoin(manifestName);
|
||||
}
|
||||
|
||||
final home =
|
||||
@@ -282,7 +259,7 @@ Future<bool> _checkWindowsRegistry(String keyPath) async {
|
||||
}
|
||||
|
||||
Future<String> _getManifestContent(Browser browser) async {
|
||||
final hostPath = _joinExePath(_hostExecName);
|
||||
final hostPath = Util.homePathJoin(_hostExecName);
|
||||
final manifest = {
|
||||
'name': _hostName,
|
||||
'description': 'Gopeed browser extension host',
|
||||
@@ -309,9 +286,3 @@ String _getWindowsRegistryKey(Browser browser) {
|
||||
return _firefoxNativeHostsKey;
|
||||
}
|
||||
}
|
||||
|
||||
String _joinExePath(String fileName) {
|
||||
final execPath = Platform.resolvedExecutable;
|
||||
final execDir = path.dirname(execPath);
|
||||
return path.join(execDir, fileName);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
// List of mirrors for GitHub
|
||||
const _sourceMirror = [
|
||||
// ghfast.top: https://ghfast.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://ghfast.top/", r"(.*)"],
|
||||
// github.moeyy.xyz: https://github.moeyy.xyz/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://github.moeyy.xyz/", r"(.*)"],
|
||||
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||
[
|
||||
"https://fastly.jsdelivr.net/gh",
|
||||
r".*raw.githubusercontent.com(/.*)(/.*)/(?:master|main)(/.*)"
|
||||
],
|
||||
];
|
||||
|
||||
// List of mirrors for GitHub release
|
||||
const _assertMirror = [
|
||||
// ghfast.top: https://ghfast.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://ghfast.top/", r"(.*)"],
|
||||
// github.moeyy.xyz: https://github.moeyy.xyz/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://github.moeyy.xyz/", r"(.*)"],
|
||||
];
|
||||
|
||||
enum MirrorType {
|
||||
githubSource,
|
||||
githubRelease,
|
||||
}
|
||||
|
||||
/// Auto detect the best mirror for the given [rawUrl] and [type]
|
||||
Future<String> githubAutoMirror(String rawUrl, MirrorType type) async {
|
||||
final mirrorUrls = githubMirrorUrls(rawUrl, type);
|
||||
|
||||
// Ping all mirrors and get the fastest one
|
||||
final pingResult = await Future.wait(mirrorUrls.map((e) async {
|
||||
final client = Dio()
|
||||
..options.sendTimeout = const Duration(seconds: 3)
|
||||
..options.connectTimeout = const Duration(seconds: 3);
|
||||
var time = DateTime.now().millisecondsSinceEpoch;
|
||||
try {
|
||||
final response = await client.head(e);
|
||||
if (response.statusCode == 200) {
|
||||
time = DateTime.now().millisecondsSinceEpoch - time;
|
||||
return (e, time);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return (e, -1);
|
||||
}));
|
||||
|
||||
var list = pingResult.where((e) => e.$2 != -1).toList()
|
||||
..sort((a, b) => a.$2.compareTo(b.$2));
|
||||
if (list.isNotEmpty) {
|
||||
return list.first.$1;
|
||||
}
|
||||
|
||||
return rawUrl;
|
||||
}
|
||||
|
||||
List<String> githubMirrorUrls(String rawUrl, MirrorType type) {
|
||||
final mirrors = switch (type) {
|
||||
MirrorType.githubSource => _sourceMirror,
|
||||
MirrorType.githubRelease => _assertMirror,
|
||||
};
|
||||
|
||||
final ret = <String>[];
|
||||
for (final cdn in mirrors) {
|
||||
final reg = RegExp(cdn[1]);
|
||||
final match = reg.firstMatch(rawUrl.toString());
|
||||
var matchStr = "";
|
||||
for (var i = 1; i <= match!.groupCount; i++) {
|
||||
matchStr += match.group(i)!;
|
||||
}
|
||||
ret.add("${cdn[0]}$matchStr");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:install_plugin/install_plugin.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../api/api.dart';
|
||||
import '../app/views/outlined_button_loading.dart';
|
||||
import 'arch/arch.dart';
|
||||
import 'github_mirror.dart';
|
||||
import 'log_util.dart';
|
||||
import 'message.dart';
|
||||
import 'package_info.dart';
|
||||
import 'util.dart';
|
||||
|
||||
enum Channel {
|
||||
windowsInstaller,
|
||||
windowsPortable,
|
||||
macosDmg,
|
||||
linuxFlathub,
|
||||
linuxSnap,
|
||||
linuxDeb,
|
||||
linuxAppImage,
|
||||
androidApk,
|
||||
iosIpa,
|
||||
docker,
|
||||
}
|
||||
|
||||
const _channelEnv = String.fromEnvironment("UPDATE_CHANNEL");
|
||||
final _channel =
|
||||
Channel.values.where((e) => e.name == _channelEnv).firstOrNull ??
|
||||
() {
|
||||
if (Util.isWindows()) {
|
||||
return Channel.windowsPortable;
|
||||
} else if (Util.isMacos()) {
|
||||
return Channel.macosDmg;
|
||||
} else if (Util.isAndroid()) {
|
||||
return Channel.androidApk;
|
||||
} else if (Util.isIOS()) {
|
||||
return Channel.iosIpa;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}();
|
||||
final _updaterBin = "updater${Util.isWindows() ? ".exe" : ""}";
|
||||
|
||||
Future<void> installUpdater() async {
|
||||
await Util.installAsset(
|
||||
'assets/exec/$_updaterBin', Util.homePathJoin(_updaterBin),
|
||||
executable: true);
|
||||
}
|
||||
|
||||
class VersionInfo {
|
||||
final String version;
|
||||
final String changeLog;
|
||||
|
||||
VersionInfo(this.version, this.changeLog);
|
||||
}
|
||||
|
||||
Future<VersionInfo?> checkUpdate() async {
|
||||
String? releaseDataStr;
|
||||
try {
|
||||
releaseDataStr = (await proxyRequest(
|
||||
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
|
||||
.data;
|
||||
} catch (e) {
|
||||
releaseDataStr =
|
||||
(await proxyRequest("https://gopeed.com/api/release")).data;
|
||||
}
|
||||
if (releaseDataStr == null) {
|
||||
return null;
|
||||
}
|
||||
final releaseData = jsonDecode(releaseDataStr);
|
||||
final tagName = releaseData["tag_name"];
|
||||
if (tagName == null) {
|
||||
return null;
|
||||
}
|
||||
final latestVersion = releaseData["tag_name"].substring(1);
|
||||
|
||||
// compare version x.y.z to x.y.z
|
||||
final currentVersion = packageInfo.version;
|
||||
var isNewVersion = false;
|
||||
if (latestVersion != currentVersion) {
|
||||
final currentVersionList = currentVersion.split(".");
|
||||
final latestVersionList = latestVersion.split(".");
|
||||
for (var i = 0; i < currentVersionList.length; i++) {
|
||||
if (int.parse(latestVersionList[i]) > int.parse(currentVersionList[i])) {
|
||||
isNewVersion = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNewVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return VersionInfo(latestVersion, releaseData["body"]);
|
||||
}
|
||||
|
||||
Future<void> showUpdateDialog(
|
||||
BuildContext context, VersionInfo versionInfo) async {
|
||||
final fullChangeLog = versionInfo.changeLog;
|
||||
final isZh = Get.locale?.languageCode == "zh";
|
||||
final changeLogRegex = isZh
|
||||
? RegExp(r"(#\s+更新日志.*)", multiLine: true, dotAll: true)
|
||||
: RegExp(r"(# Release notes.*)#\s+更新日志", multiLine: true, dotAll: true);
|
||||
final changeLog = changeLogRegex.firstMatch(fullChangeLog)?.group(1) ?? "";
|
||||
await showDialog(
|
||||
context: Get.context!,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
bool isDownloading = false;
|
||||
double progress = 0;
|
||||
int total = 0;
|
||||
final buttonController = OutlinedButtonLoadingController();
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final dialogWidth =
|
||||
screenSize.width < 500 ? screenSize.width * 0.9 : 500.0;
|
||||
final dialogHeight =
|
||||
screenSize.height < 600 ? screenSize.height * 0.8 : 400.0;
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: dialogWidth,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'newVersionTitle'
|
||||
.trParams({'version': versionInfo.version}),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
height: dialogHeight * 0.5,
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
scrollbars: true,
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final controller = ScrollController();
|
||||
return Scrollbar(
|
||||
controller: controller,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children:
|
||||
_parseMarkdown(changeLog, context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isDownloading) ...[
|
||||
const SizedBox(height: 16),
|
||||
LinearProgressIndicator(
|
||||
value: progress,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${(progress * 100).toStringAsFixed(1)}%',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
Text(
|
||||
total == 0
|
||||
? ''
|
||||
: '${Util.fmtByte((total * progress).toInt())} / ${Util.fmtByte(total)}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: isDownloading ? null : () => Get.back(),
|
||||
child: Text(
|
||||
'newVersionLater'.tr,
|
||||
style: TextStyle(
|
||||
color: isDownloading
|
||||
? Theme.of(context).disabledColor
|
||||
: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButtonLoading(
|
||||
controller: buttonController,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
isDownloading = true;
|
||||
});
|
||||
buttonController.start();
|
||||
try {
|
||||
await _update(versionInfo.version,
|
||||
(received, fileTotal) {
|
||||
setState(() {
|
||||
total = fileTotal;
|
||||
progress = received / fileTotal;
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
} finally {
|
||||
setState(() {
|
||||
isDownloading = false;
|
||||
});
|
||||
buttonController.stop();
|
||||
}
|
||||
},
|
||||
child: Text('newVersionUpdate'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _parseMarkdown(String markdown, BuildContext context) {
|
||||
final List<Widget> widgets = [];
|
||||
final lines = markdown.split('\n');
|
||||
|
||||
for (final line in lines) {
|
||||
if (line.trim().isEmpty) continue;
|
||||
if (line.startsWith('# ')) {
|
||||
// H1 header
|
||||
widgets.add(Text(
|
||||
line.substring(2),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
));
|
||||
} else if (line.startsWith('## ')) {
|
||||
// H2 header
|
||||
widgets.add(Text(
|
||||
line.substring(3),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
));
|
||||
} else if (line.trim().startsWith('- ')) {
|
||||
// List item
|
||||
widgets.add(Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ', style: TextStyle(fontSize: 14)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
line.substring(line.indexOf('-') + 1).trim().replaceFirst(
|
||||
RegExp(r'@[^\s]*\s\(#\d+\)'),
|
||||
''), // Remove contributor and pr number
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
} else {
|
||||
// Normal text
|
||||
widgets.add(Text(
|
||||
line,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
));
|
||||
}
|
||||
|
||||
// Add spacing between elements
|
||||
widgets.add(const SizedBox(height: 8));
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
Future<void> _update(String version, Function(int, int) onProgress) async {
|
||||
var newVersionAssetPath = "";
|
||||
final newVersionAssetName = _getAssetName(version);
|
||||
|
||||
// Need to download the asset
|
||||
if (newVersionAssetName.isNotEmpty) {
|
||||
final downloadUrl =
|
||||
'https://github.com/GopeedLab/gopeed/releases/download/v$version/$newVersionAssetName';
|
||||
newVersionAssetPath = await _getAssetPath(version);
|
||||
|
||||
if (downloadUrl.isNotEmpty) {
|
||||
final fastDownloadUrl =
|
||||
await githubAutoMirror(downloadUrl, MirrorType.githubRelease);
|
||||
final downloadClient = Dio();
|
||||
await downloadClient.download(fastDownloadUrl, newVersionAssetPath,
|
||||
onReceiveProgress: onProgress);
|
||||
}
|
||||
}
|
||||
|
||||
switch (_channel) {
|
||||
case Channel.windowsInstaller:
|
||||
case Channel.windowsPortable:
|
||||
case Channel.macosDmg:
|
||||
case Channel.linuxFlathub:
|
||||
case Channel.linuxSnap:
|
||||
case Channel.linuxDeb:
|
||||
final updaterPath = Util.homePathJoin(_updaterBin);
|
||||
// Check the updater binary is exists
|
||||
if (!await File(updaterPath).exists()) {
|
||||
await launchUrl(
|
||||
Uri.parse(
|
||||
'https://github.com/GopeedLab/gopeed/releases/tag/v$version'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
break;
|
||||
}
|
||||
/**
|
||||
*Usage of updater command:
|
||||
-pid int
|
||||
PID of the process to update
|
||||
-channel string
|
||||
Update channel
|
||||
-asset string
|
||||
Path to the package asset
|
||||
-log string
|
||||
Log file path
|
||||
*/
|
||||
await Process.run(updaterPath, [
|
||||
"-pid",
|
||||
pid.toString(),
|
||||
"-channel",
|
||||
_channel!.name,
|
||||
"-asset",
|
||||
newVersionAssetPath,
|
||||
"-log",
|
||||
path.join(logsDir(), "updater.log")
|
||||
]);
|
||||
break;
|
||||
case Channel.androidApk:
|
||||
await InstallPlugin.installApk(newVersionAssetPath);
|
||||
break;
|
||||
default:
|
||||
await launchUrl(
|
||||
Uri.parse(
|
||||
'https://github.com/GopeedLab/gopeed/releases/tag/v$version'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String _getAssetName(String version) {
|
||||
final arch = getArch();
|
||||
|
||||
String commonArchName() {
|
||||
return switch (arch) {
|
||||
Architecture.ia32 => "386",
|
||||
Architecture.x64 => "amd64",
|
||||
_ => arch.name
|
||||
};
|
||||
}
|
||||
|
||||
switch (_channel) {
|
||||
case Channel.windowsInstaller:
|
||||
return 'Gopeed-v$version-windows-${commonArchName()}.zip';
|
||||
case Channel.windowsPortable:
|
||||
return 'Gopeed-v$version-windows-${commonArchName()}-portable.zip';
|
||||
case Channel.macosDmg:
|
||||
return 'Gopeed-v$version-macos.dmg';
|
||||
case Channel.linuxDeb:
|
||||
return 'Gopeed-v$version-linux-${commonArchName()}.deb';
|
||||
case Channel.androidApk:
|
||||
final apkArchName = switch (arch) {
|
||||
Architecture.arm => "armeabi-v7a",
|
||||
Architecture.arm64 => "arm64-v8a",
|
||||
Architecture.x64 => "x86_64",
|
||||
_ => null
|
||||
};
|
||||
var apkNamePrefix = "Gopeed-v$version-android";
|
||||
if (apkArchName != null) {
|
||||
apkNamePrefix += "-$apkArchName";
|
||||
}
|
||||
return "$apkNamePrefix.apk";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getAssetPath(String version) async {
|
||||
return path.join(
|
||||
(await getTemporaryDirectory()).path, _getAssetName(version));
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
@@ -153,4 +155,45 @@ class Util {
|
||||
timer = Timer(Duration(milliseconds: ms), fn);
|
||||
};
|
||||
}
|
||||
|
||||
static String homePathJoin(String fileName) {
|
||||
final execPath = Platform.resolvedExecutable;
|
||||
final execDir = path.dirname(execPath);
|
||||
return path.join(execDir, fileName);
|
||||
}
|
||||
|
||||
static Future<void> installAsset(String assetPath, String targetPath,
|
||||
{bool executable = false}) async {
|
||||
Future<List<int>> getAssetData() async {
|
||||
final asset = await rootBundle.load(assetPath);
|
||||
return asset.buffer.asUint8List(asset.offsetInBytes, asset.lengthInBytes);
|
||||
}
|
||||
|
||||
// Check if target file is not installed
|
||||
if (!await File(targetPath).exists()) {
|
||||
final assetData = await getAssetData();
|
||||
final file = File(targetPath);
|
||||
await file.writeAsBytes(assetData);
|
||||
// Add execute permission when file first created
|
||||
if (executable && !Platform.isWindows) {
|
||||
await Process.run('chmod', ['+x', targetPath]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target file needs to be updated
|
||||
final assetData = await getAssetData();
|
||||
if (_md5(assetData) != await _md5File(File(targetPath))) {
|
||||
await File(targetPath).writeAsBytes(assetData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static String _md5(List<int> data) {
|
||||
return md5.convert(data).toString();
|
||||
}
|
||||
|
||||
static Future<String> _md5File(File file) async {
|
||||
return file.openRead().transform(md5).first.toString();
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -271,7 +271,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
@@ -546,6 +546,15 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
install_plugin:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||
resolved-ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||
url: "https://github.com/hui-z/flutter_install_plugin.git"
|
||||
source: git
|
||||
version: "2.1.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -71,6 +71,11 @@ dependencies:
|
||||
contentsize_tabbarview: ^0.0.2
|
||||
win32_registry: ^1.1.5
|
||||
share_handler: ^0.0.22
|
||||
crypto: ^3.0.6
|
||||
install_plugin:
|
||||
git:
|
||||
url: https://github.com/hui-z/flutter_install_plugin.git
|
||||
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||
dependency_overrides:
|
||||
permission_handler_windows:
|
||||
git:
|
||||
@@ -108,7 +113,7 @@ flutter:
|
||||
- assets/tray_icon/
|
||||
- assets/extension/
|
||||
# Browser extension native host binary, see https://github.com/GopeedLab/gopeed/tree/main/cmd/host
|
||||
- assets/host/
|
||||
- assets/exec/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
Reference in new issue
Block a user