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.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
formatter:
|
||||
page_width: 120
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
|
||||
@@ -315,5 +315,15 @@
|
||||
"time": "DateTime",
|
||||
"nowTimestamp": "Now timestamp",
|
||||
"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": "时间",
|
||||
"nowTimestamp": "当前时间戳(秒)",
|
||||
"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 {
|
||||
static const String version = "1.1.8";
|
||||
|
||||
ValueNotifier<bool> globalChange = ValueNotifier(false);
|
||||
|
||||
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/utils/listenable_list.dart';
|
||||
|
||||
import '../app_update/app_update_repository.dart';
|
||||
import '../component/split_view.dart';
|
||||
|
||||
/// @author wanghongen
|
||||
@@ -93,6 +94,8 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
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/ui/component/multi_window.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/hosts.dart';
|
||||
import 'package:proxypin/ui/desktop/toolbar/setting/request_block.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'filter.dart';
|
||||
|
||||
@@ -56,7 +56,6 @@ class _SettingState extends State<Setting> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return MenuAnchor(
|
||||
builder: (context, controller, child) {
|
||||
return IconButton(
|
||||
@@ -94,49 +93,7 @@ class _SettingState extends State<Setting> {
|
||||
}
|
||||
|
||||
showAbout() {
|
||||
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
|
||||
|
||||
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")))
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
showDialog(context: context, builder: (context) => DesktopAbout());
|
||||
}
|
||||
|
||||
///设置外部代理地址
|
||||
|
||||
@@ -13,6 +13,15 @@ extension ListFirstWhere<T> on Iterable<T> {
|
||||
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 {
|
||||
@@ -102,6 +111,14 @@ class Strings {
|
||||
/// 这样会导致,换行时上一行可能会留很大的空白区域
|
||||
/// 把每个字符插入一个0宽的字符, \u{200B}
|
||||
extension StringEnhance on String {
|
||||
|
||||
String removePrefix(String prefix) {
|
||||
if (startsWith(prefix)) {
|
||||
return substring(prefix.length, length);
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
String fixAutoLines() {
|
||||
return Characters(this).join('\u{200B}');
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ dependencies:
|
||||
shared_preferences: ^2.5.3
|
||||
image_pickers: ^2.0.6
|
||||
url_launcher: ^6.3.1
|
||||
toastification: ^3.0.2
|
||||
|
||||
qr_flutter: ^4.1.0
|
||||
flutter_qr_reader_copy: ^1.0.9
|
||||
|
||||
Reference in New Issue
Block a user