mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-15 04:23:17 +08:00
check app version update
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
formatter:
|
||||||
|
page_width: 120
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
|||||||
@@ -315,5 +315,15 @@
|
|||||||
"time": "DateTime",
|
"time": "DateTime",
|
||||||
"nowTimestamp": "Now timestamp",
|
"nowTimestamp": "Now timestamp",
|
||||||
"hosts": "Hosts",
|
"hosts": "Hosts",
|
||||||
"toAddress": "To Address"
|
"toAddress": "To Address",
|
||||||
|
|
||||||
|
"appUpdateCheckVersion": "Check for Updates",
|
||||||
|
"appUpdateNotAvailableMsg": "Already Using The Latest Version",
|
||||||
|
"appUpdateDialogTitle": "Update Available",
|
||||||
|
"appUpdateUpdateMsg": "A new version of ProxyPin is available. Would you like to update now?",
|
||||||
|
"appUpdateCurrentVersionLbl": "Current Version",
|
||||||
|
"appUpdateNewVersionLbl": "New Version",
|
||||||
|
"appUpdateUpdateNowBtnTxt": "Update Now",
|
||||||
|
"appUpdateLaterBtnTxt": "Later",
|
||||||
|
"appUpdateIgnoreBtnTxt": "Ignore"
|
||||||
}
|
}
|
||||||
@@ -314,5 +314,15 @@
|
|||||||
"time": "时间",
|
"time": "时间",
|
||||||
"nowTimestamp": "当前时间戳(秒)",
|
"nowTimestamp": "当前时间戳(秒)",
|
||||||
"hosts": "Hosts 映射",
|
"hosts": "Hosts 映射",
|
||||||
"toAddress": "映射地址"
|
"toAddress": "映射地址",
|
||||||
|
|
||||||
|
"appUpdateCheckVersion": "检测更新",
|
||||||
|
"appUpdateNotAvailableMsg": "已是最新版本",
|
||||||
|
"appUpdateDialogTitle": "有可用更新",
|
||||||
|
"appUpdateUpdateMsg": "ProxyPin 的新版本现已推出。您想现在更新吗?",
|
||||||
|
"appUpdateCurrentVersionLbl": "当前版本",
|
||||||
|
"appUpdateNewVersionLbl": "新版本",
|
||||||
|
"appUpdateUpdateNowBtnTxt": "现在更新",
|
||||||
|
"appUpdateLaterBtnTxt": "以后再说",
|
||||||
|
"appUpdateIgnoreBtnTxt": "忽略"
|
||||||
}
|
}
|
||||||
107
lib/ui/app_update/app_update_repository.dart
Normal file
107
lib/ui/app_update/app_update_repository.dart
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:proxypin/network/util/logger.dart';
|
||||||
|
import 'package:proxypin/ui/app_update/remote_version_entity.dart';
|
||||||
|
import 'package:proxypin/ui/component/app_dialog.dart';
|
||||||
|
import 'package:proxypin/ui/configuration.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'constants.dart';
|
||||||
|
import 'new_version_dialog.dart';
|
||||||
|
|
||||||
|
class AppUpdateRepository {
|
||||||
|
static final HttpClient httpClient = HttpClient();
|
||||||
|
|
||||||
|
static Future<void> checkUpdate(BuildContext context, {bool canIgnore = true, bool showToast = false}) async {
|
||||||
|
try {
|
||||||
|
var lastVersion = await getLatestVersion();
|
||||||
|
if (lastVersion == null) {
|
||||||
|
logger.w("[AppUpdate] failed to fetch latest version info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
var availableUpdates = compareVersions(AppConfiguration.version, lastVersion.version);
|
||||||
|
if (availableUpdates) {
|
||||||
|
if (canIgnore) {
|
||||||
|
var ignoreVersion = await SharedPreferencesAsync().getString(Constants.ignoreReleaseVersionKey);
|
||||||
|
if (ignoreVersion == lastVersion.version) {
|
||||||
|
logger.d("ignored release [${lastVersion.version}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.d("new version available: $lastVersion");
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
NewVersionDialog(
|
||||||
|
AppConfiguration.version,
|
||||||
|
lastVersion,
|
||||||
|
canIgnore: true,
|
||||||
|
).show(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.i("already using latest version[${AppConfiguration.version}], last: [${lastVersion.version}]");
|
||||||
|
|
||||||
|
if (showToast) {
|
||||||
|
AppLocalizations localizations = AppLocalizations.of(context)!;
|
||||||
|
CustomToast.success(localizations.appUpdateNotAvailableMsg).show(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("Error checking for updates: $e");
|
||||||
|
if (showToast) {
|
||||||
|
AppAlertDialog(message: e.toString()).show(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the latest version information from the GitHub releases API.
|
||||||
|
static Future<RemoteVersionEntity?> getLatestVersion({bool includePreReleases = false}) async {
|
||||||
|
final response = await http.get(Uri.parse(Constants.githubReleasesApiUrl));
|
||||||
|
if (response.statusCode != 200 || response.body.isEmpty) {
|
||||||
|
logger.w("[AppUpdate] failed to fetch latest version info");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = jsonDecode(response.body) as List;
|
||||||
|
final releases = body.map((e) => GithubReleaseParser.parse(e as Map<String, dynamic>));
|
||||||
|
late RemoteVersionEntity latest;
|
||||||
|
if (includePreReleases) {
|
||||||
|
latest = releases.first;
|
||||||
|
} else {
|
||||||
|
latest = releases.firstWhere((e) => e.preRelease == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.d("[AppUpdate] latest version: $latest");
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool compareVersions(String currentVersion, String latestVersion) {
|
||||||
|
String normalizeVersion(String version) {
|
||||||
|
return version.startsWith('v') ? version.substring(1) : version;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> parseVersion(String version) {
|
||||||
|
return normalizeVersion(version).split('.').map(int.parse).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> current = parseVersion(currentVersion);
|
||||||
|
List<int> latest = parseVersion(latestVersion);
|
||||||
|
|
||||||
|
for (int i = 0; i < current.length; i++) {
|
||||||
|
if (i >= latest.length || current[i] > latest[i]) {
|
||||||
|
return false; // 当前版本高于最新版本
|
||||||
|
} else if (current[i] < latest[i]) {
|
||||||
|
return true; // 需要更新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return latest.length > current.length; // 最新版本有更多的子版本号
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/ui/app_update/constants.dart
Normal file
11
lib/ui/app_update/constants.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
abstract class Constants {
|
||||||
|
static const githubUrl = "https://github.com/wanghongenpin/proxypin";
|
||||||
|
static const githubReleasesApiUrl =
|
||||||
|
"https://api.github.com/repos/wanghongenpin/proxypin/releases";
|
||||||
|
static const githubLatestReleaseUrl =
|
||||||
|
"https://github.com/wanghongenpin/proxypin/releases/latest";
|
||||||
|
|
||||||
|
static const String ignoreReleaseVersionKey = "ignored_release_version";
|
||||||
|
}
|
||||||
|
|
||||||
|
const kAnimationDuration = Duration(milliseconds: 250);
|
||||||
94
lib/ui/app_update/new_version_dialog.dart
Normal file
94
lib/ui/app_update/new_version_dialog.dart
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:proxypin/network/util/logger.dart';
|
||||||
|
import 'package:proxypin/ui/app_update/remote_version_entity.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import 'constants.dart';
|
||||||
|
|
||||||
|
class NewVersionDialog extends StatelessWidget {
|
||||||
|
NewVersionDialog(
|
||||||
|
this.currentVersion,
|
||||||
|
this.newVersion, {
|
||||||
|
this.canIgnore = true,
|
||||||
|
}) : super(key: _dialogKey);
|
||||||
|
|
||||||
|
final String currentVersion;
|
||||||
|
final RemoteVersionEntity newVersion;
|
||||||
|
final bool canIgnore;
|
||||||
|
|
||||||
|
static final _dialogKey = GlobalKey(debugLabel: 'new version dialog');
|
||||||
|
|
||||||
|
Future<void> show(BuildContext context) async {
|
||||||
|
if (_dialogKey.currentContext == null) {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder: (context) => this,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.d("new version dialog is already open");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
AppLocalizations localizations = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(localizations.appUpdateDialogTitle),
|
||||||
|
// scrollable: true,
|
||||||
|
content: Container(
|
||||||
|
constraints: BoxConstraints(maxHeight: 230, maxWidth: 500),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(localizations.appUpdateUpdateMsg),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: "${localizations.appUpdateCurrentVersionLbl}: ", style: theme.textTheme.bodySmall),
|
||||||
|
TextSpan(text: currentVersion, style: theme.textTheme.labelMedium),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: "${localizations.appUpdateNewVersionLbl}: ", style: theme.textTheme.bodySmall),
|
||||||
|
TextSpan(text: newVersion.version, style: theme.textTheme.labelMedium),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(newVersion.content ?? '', style: theme.textTheme.labelMedium),
|
||||||
|
],
|
||||||
|
))),
|
||||||
|
actions: [
|
||||||
|
if (canIgnore)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
SharedPreferencesAsync().setString(Constants.ignoreReleaseVersionKey, newVersion.version);
|
||||||
|
logger.i("ignored release [${newVersion.version}]");
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(localizations.appUpdateIgnoreBtnTxt),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(localizations.appUpdateLaterBtnTxt),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await launchUrl(Uri.parse(newVersion.url), mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
child: Text(localizations.appUpdateUpdateNowBtnTxt),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
lib/ui/app_update/remote_version_entity.dart
Normal file
49
lib/ui/app_update/remote_version_entity.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:proxypin/utils/lang.dart';
|
||||||
|
|
||||||
|
class RemoteVersionEntity {
|
||||||
|
final String version;
|
||||||
|
final String buildNumber;
|
||||||
|
final String releaseTag;
|
||||||
|
final bool preRelease;
|
||||||
|
final String url;
|
||||||
|
final String? content;
|
||||||
|
final DateTime publishedAt;
|
||||||
|
|
||||||
|
RemoteVersionEntity({
|
||||||
|
required this.version,
|
||||||
|
required this.buildNumber,
|
||||||
|
required this.releaseTag,
|
||||||
|
required this.preRelease,
|
||||||
|
required this.url,
|
||||||
|
this.content,
|
||||||
|
required this.publishedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'RemoteVersionEntity(version: $version, buildNumber: $buildNumber, releaseTag: $releaseTag, preRelease: $preRelease, url: $url, publishedAt: $publishedAt)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class GithubReleaseParser {
|
||||||
|
static RemoteVersionEntity parse(Map<String, dynamic> json) {
|
||||||
|
final fullTag = json['tag_name'] as String;
|
||||||
|
final fullVersion = fullTag.removePrefix("v").split("-").first.split("+");
|
||||||
|
var version = fullVersion.first;
|
||||||
|
var buildNumber = fullVersion.elementAtOrElse(1, (index) => "");
|
||||||
|
|
||||||
|
final preRelease = json["prerelease"] as bool;
|
||||||
|
final publishedAt = DateTime.parse(json["published_at"] as String);
|
||||||
|
|
||||||
|
var body = json['body']?.toString().split("English: ");
|
||||||
|
return RemoteVersionEntity(
|
||||||
|
version: version,
|
||||||
|
buildNumber: buildNumber,
|
||||||
|
releaseTag: fullTag,
|
||||||
|
preRelease: preRelease,
|
||||||
|
url: json["html_url"] as String,
|
||||||
|
content: body?.last,
|
||||||
|
publishedAt: publishedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
124
lib/ui/component/app_dialog.dart
Normal file
124
lib/ui/component/app_dialog.dart
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:toastification/toastification.dart';
|
||||||
|
|
||||||
|
class AppAlertDialog extends StatelessWidget {
|
||||||
|
const AppAlertDialog({
|
||||||
|
super.key,
|
||||||
|
this.title,
|
||||||
|
required this.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
factory AppAlertDialog.fromErr(({String type, String? message}) err) => AppAlertDialog(
|
||||||
|
title: err.message == null ? null : err.type,
|
||||||
|
message: err.message ?? err.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> show(BuildContext context) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder: (context) => this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final localizations = MaterialLocalizations.of(context);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: title != null ? Text(title!) : null,
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 468,
|
||||||
|
child: Text(message),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(localizations.okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AlertType {
|
||||||
|
info,
|
||||||
|
error,
|
||||||
|
success;
|
||||||
|
|
||||||
|
ToastificationType get _toastificationType => switch (this) {
|
||||||
|
success => ToastificationType.success,
|
||||||
|
error => ToastificationType.error,
|
||||||
|
info => ToastificationType.info,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomToast extends StatelessWidget {
|
||||||
|
const CustomToast(
|
||||||
|
this.message, {
|
||||||
|
super.key,
|
||||||
|
this.type = AlertType.info,
|
||||||
|
this.icon,
|
||||||
|
this.duration = const Duration(seconds: 3),
|
||||||
|
});
|
||||||
|
|
||||||
|
const CustomToast.error(
|
||||||
|
this.message, {
|
||||||
|
super.key,
|
||||||
|
this.duration = const Duration(seconds: 5),
|
||||||
|
}) : type = AlertType.error,
|
||||||
|
icon = Icons.error;
|
||||||
|
|
||||||
|
const CustomToast.success(
|
||||||
|
this.message, {
|
||||||
|
super.key,
|
||||||
|
this.duration = const Duration(seconds: 3),
|
||||||
|
}) : type = AlertType.success,
|
||||||
|
icon = Icons.check_circle;
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
final AlertType type;
|
||||||
|
final IconData? icon;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(child: Text(message)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void show(BuildContext context) {
|
||||||
|
toastification.show(
|
||||||
|
context: context,
|
||||||
|
title: Text(message),
|
||||||
|
icon: icon == null ? null : Icon(icon),
|
||||||
|
type: type._toastificationType,
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
autoCloseDuration: duration,
|
||||||
|
style: ToastificationStyle.fillColored,
|
||||||
|
pauseOnHover: true,
|
||||||
|
showProgressBar: false,
|
||||||
|
dragToClose: true,
|
||||||
|
closeOnClick: true,
|
||||||
|
closeButtonShowType: CloseButtonShowType.onHover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,8 @@ class ThemeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AppConfiguration {
|
class AppConfiguration {
|
||||||
|
static const String version = "1.1.8";
|
||||||
|
|
||||||
ValueNotifier<bool> globalChange = ValueNotifier(false);
|
ValueNotifier<bool> globalChange = ValueNotifier(false);
|
||||||
|
|
||||||
ThemeModel _theme = ThemeModel();
|
ThemeModel _theme = ThemeModel();
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import 'package:proxypin/ui/desktop/request/list.dart';
|
|||||||
import 'package:proxypin/ui/desktop/toolbar/toolbar.dart';
|
import 'package:proxypin/ui/desktop/toolbar/toolbar.dart';
|
||||||
import 'package:proxypin/utils/listenable_list.dart';
|
import 'package:proxypin/utils/listenable_list.dart';
|
||||||
|
|
||||||
|
import '../app_update/app_update_repository.dart';
|
||||||
import '../component/split_view.dart';
|
import '../component/split_view.dart';
|
||||||
|
|
||||||
/// @author wanghongen
|
/// @author wanghongen
|
||||||
@@ -93,6 +94,8 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
showUpgradeNotice();
|
showUpgradeNotice();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
AppUpdateRepository.checkUpdate(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
82
lib/ui/desktop/toolbar/setting/about.dart
Normal file
82
lib/ui/desktop/toolbar/setting/about.dart
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:proxypin/ui/app_update/app_update_repository.dart';
|
||||||
|
import 'package:proxypin/ui/configuration.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
class DesktopAbout extends StatefulWidget {
|
||||||
|
const DesktopAbout({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _AppUpdateStateChecking();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppUpdateStateChecking extends State<DesktopAbout> {
|
||||||
|
bool checkUpdating = false;
|
||||||
|
|
||||||
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
|
||||||
|
String gitHub = "https://github.com/wanghongenpin/proxypin";
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),
|
||||||
|
title: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
const Expanded(child: SizedBox()),
|
||||||
|
Text(localizations.about, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
|
||||||
|
const Expanded(child: SizedBox()),
|
||||||
|
Align(alignment: Alignment.topRight, child: CloseButton())
|
||||||
|
]),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 360,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text("ProxyPin", style: TextStyle(fontSize: 20)),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 10, right: 10),
|
||||||
|
child:
|
||||||
|
Text(isCN ? "全平台开源免费抓包软件" : "Full platform open source free capture HTTP(S) traffic software")),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text("v${AppConfiguration.version}"),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ListTile(
|
||||||
|
title: Text('GitHub'),
|
||||||
|
trailing: const Icon(Icons.open_in_new, size: 22),
|
||||||
|
onTap: () => launchUrl(Uri.parse(gitHub))),
|
||||||
|
ListTile(
|
||||||
|
title: Text(localizations.feedback),
|
||||||
|
trailing: const Icon(Icons.open_in_new, size: 22),
|
||||||
|
onTap: () => launchUrl(Uri.parse("$gitHub/issues"))),
|
||||||
|
ListTile(
|
||||||
|
title: Text(localizations.appUpdateCheckVersion),
|
||||||
|
trailing: checkUpdating
|
||||||
|
? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator())
|
||||||
|
: const Icon(Icons.sync, size: 22),
|
||||||
|
onTap: () async {
|
||||||
|
if (checkUpdating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
checkUpdating = true;
|
||||||
|
});
|
||||||
|
await AppUpdateRepository.checkUpdate(context, canIgnore: false, showToast: true);
|
||||||
|
setState(() {
|
||||||
|
checkUpdating = false;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
ListTile(
|
||||||
|
title: Text(isCN ? "下载地址" : "Download"),
|
||||||
|
trailing: const Icon(Icons.open_in_new, size: 22),
|
||||||
|
onTap: () => launchUrl(
|
||||||
|
Uri.parse(isCN ? "https://gitee.com/wanghongenpin/proxypin/releases" : "$gitHub/releases")))
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,10 +24,10 @@ import 'package:proxypin/network/components/manager/request_block_manager.dart';
|
|||||||
import 'package:proxypin/network/util/system_proxy.dart';
|
import 'package:proxypin/network/util/system_proxy.dart';
|
||||||
import 'package:proxypin/ui/component/multi_window.dart';
|
import 'package:proxypin/ui/component/multi_window.dart';
|
||||||
import 'package:proxypin/ui/component/widgets.dart';
|
import 'package:proxypin/ui/component/widgets.dart';
|
||||||
|
import 'package:proxypin/ui/desktop/toolbar/setting/about.dart';
|
||||||
import 'package:proxypin/ui/desktop/toolbar/setting/external_proxy.dart';
|
import 'package:proxypin/ui/desktop/toolbar/setting/external_proxy.dart';
|
||||||
import 'package:proxypin/ui/desktop/toolbar/setting/hosts.dart';
|
import 'package:proxypin/ui/desktop/toolbar/setting/hosts.dart';
|
||||||
import 'package:proxypin/ui/desktop/toolbar/setting/request_block.dart';
|
import 'package:proxypin/ui/desktop/toolbar/setting/request_block.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
import 'filter.dart';
|
import 'filter.dart';
|
||||||
|
|
||||||
@@ -56,7 +56,6 @@ class _SettingState extends State<Setting> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
return MenuAnchor(
|
return MenuAnchor(
|
||||||
builder: (context, controller, child) {
|
builder: (context, controller, child) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
@@ -94,49 +93,7 @@ class _SettingState extends State<Setting> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showAbout() {
|
showAbout() {
|
||||||
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
|
showDialog(context: context, builder: (context) => DesktopAbout());
|
||||||
|
|
||||||
String gitHub = "https://github.com/wanghongenpin/proxypin";
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),
|
|
||||||
title: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
|
||||||
const Expanded(child: SizedBox()),
|
|
||||||
Text(localizations.about, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
|
|
||||||
const Expanded(child: SizedBox()),
|
|
||||||
Align(alignment: Alignment.topRight, child: CloseButton())
|
|
||||||
]),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Text("ProxyPin", style: TextStyle(fontSize: 20)),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 10, right: 10),
|
|
||||||
child:
|
|
||||||
Text(isCN ? "全平台开源免费抓包软件" : "Full platform open source free capture HTTP(S) traffic software")),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
const Text("V1.1.7"),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
ListTile(
|
|
||||||
title: Text('GitHub', textAlign: TextAlign.center, style: TextStyle(color: Colors.blue)),
|
|
||||||
onTap: () => launchUrl(Uri.parse(gitHub))),
|
|
||||||
ListTile(
|
|
||||||
title:
|
|
||||||
Text(localizations.feedback, textAlign: TextAlign.center, style: TextStyle(color: Colors.blue)),
|
|
||||||
onTap: () => launchUrl(Uri.parse("$gitHub/issues"))),
|
|
||||||
ListTile(
|
|
||||||
title: Text(isCN ? "下载地址" : "Download",
|
|
||||||
textAlign: TextAlign.center, style: TextStyle(color: Colors.blue)),
|
|
||||||
onTap: () => launchUrl(
|
|
||||||
Uri.parse(isCN ? "https://gitee.com/wanghongenpin/proxypin/releases" : "$gitHub/releases")))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///设置外部代理地址
|
///设置外部代理地址
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ extension ListFirstWhere<T> on Iterable<T> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
T elementAtOrElse(int index, T Function(int index) defaultValue) {
|
||||||
|
if (index < 0) return defaultValue(index);
|
||||||
|
var count = 0;
|
||||||
|
for (final element in this) {
|
||||||
|
if (index == count++) return element;
|
||||||
|
}
|
||||||
|
return defaultValue(index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DateTimeFormat on DateTime {
|
extension DateTimeFormat on DateTime {
|
||||||
@@ -102,6 +111,14 @@ class Strings {
|
|||||||
/// 这样会导致,换行时上一行可能会留很大的空白区域
|
/// 这样会导致,换行时上一行可能会留很大的空白区域
|
||||||
/// 把每个字符插入一个0宽的字符, \u{200B}
|
/// 把每个字符插入一个0宽的字符, \u{200B}
|
||||||
extension StringEnhance on String {
|
extension StringEnhance on String {
|
||||||
|
|
||||||
|
String removePrefix(String prefix) {
|
||||||
|
if (startsWith(prefix)) {
|
||||||
|
return substring(prefix.length, length);
|
||||||
|
} else {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
String fixAutoLines() {
|
String fixAutoLines() {
|
||||||
return Characters(this).join('\u{200B}');
|
return Characters(this).join('\u{200B}');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ dependencies:
|
|||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
image_pickers: ^2.0.6
|
image_pickers: ^2.0.6
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
|
toastification: ^3.0.2
|
||||||
|
|
||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
flutter_qr_reader_copy: ^1.0.9
|
flutter_qr_reader_copy: ^1.0.9
|
||||||
|
|||||||
Reference in New Issue
Block a user