feat(update): проверка новой версии через GitHub Releases
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AppUpdateInfo {
|
||||
final String version;
|
||||
final int? build;
|
||||
final String tag;
|
||||
final String url;
|
||||
final String notes;
|
||||
final Map<String, String> assets;
|
||||
|
||||
const AppUpdateInfo({
|
||||
required this.version,
|
||||
required this.build,
|
||||
required this.tag,
|
||||
required this.url,
|
||||
required this.notes,
|
||||
required this.assets,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class UpdateChecker {
|
||||
static const String _owner = 'KometTeam';
|
||||
static const String _repo = 'Komet';
|
||||
static const String _userAgent = 'KometUpdateChecker';
|
||||
|
||||
static const String _lastCheckKey = 'update_last_check_ms';
|
||||
static const String _skippedTagKey = 'update_skipped_tag';
|
||||
static const Duration _checkInterval = Duration(hours: 6);
|
||||
static const Duration _timeout = Duration(seconds: 15);
|
||||
|
||||
static Future<AppUpdateInfo?> fetchLatest() async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final currentBase = info.version;
|
||||
final currentBuild = _normalizeBuild(int.tryParse(info.buildNumber));
|
||||
|
||||
final release = await _fetchLatestRelease();
|
||||
if (release == null) return null;
|
||||
|
||||
final tag = (release['tag_name'] as String?)?.trim();
|
||||
if (tag == null || tag.isEmpty) return null;
|
||||
|
||||
final remoteBase = _baseVersion(tag);
|
||||
final remoteBuild = _buildNumber(tag);
|
||||
|
||||
if (!_isNewer(
|
||||
currentBase: currentBase,
|
||||
currentBuild: currentBuild,
|
||||
remoteBase: remoteBase,
|
||||
remoteBuild: remoteBuild,
|
||||
)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AppUpdateInfo(
|
||||
version: remoteBase,
|
||||
build: remoteBuild,
|
||||
tag: tag,
|
||||
url: (release['html_url'] as String?) ?? _releasesPage,
|
||||
notes: (release['body'] as String?)?.trim() ?? '',
|
||||
assets: _parseAssets(release['assets']),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<AppUpdateInfo?> check({bool force = false}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (!force) {
|
||||
final last = prefs.getInt(_lastCheckKey);
|
||||
if (last != null) {
|
||||
final elapsed = DateTime.now().millisecondsSinceEpoch - last;
|
||||
if (elapsed >= 0 && elapsed < _checkInterval.inMilliseconds) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppUpdateInfo? update;
|
||||
try {
|
||||
update = await fetchLatest();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await prefs.setInt(_lastCheckKey, DateTime.now().millisecondsSinceEpoch);
|
||||
|
||||
if (update == null) return null;
|
||||
|
||||
if (!force && prefs.getString(_skippedTagKey) == update.tag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
static Future<void> skip(String tag) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_skippedTagKey, tag);
|
||||
}
|
||||
|
||||
static String get _releasesPage =>
|
||||
'https://github.com/$_owner/$_repo/releases';
|
||||
|
||||
static Future<Map<String, dynamic>?> _fetchLatestRelease() async {
|
||||
final uri = Uri.parse(
|
||||
'https://api.github.com/repos/$_owner/$_repo/releases?per_page=10',
|
||||
);
|
||||
final client = HttpClient()..connectionTimeout = _timeout;
|
||||
try {
|
||||
final req = await client.getUrl(uri);
|
||||
req.headers
|
||||
..set(HttpHeaders.userAgentHeader, _userAgent)
|
||||
..set(HttpHeaders.acceptHeader, 'application/vnd.github+json');
|
||||
final resp = await req.close().timeout(_timeout);
|
||||
if (resp.statusCode != HttpStatus.ok) {
|
||||
await resp.drain<void>();
|
||||
return null;
|
||||
}
|
||||
final body = await resp
|
||||
.transform(const Utf8Decoder())
|
||||
.join()
|
||||
.timeout(_timeout);
|
||||
final decoded = jsonDecode(body);
|
||||
if (decoded is! List) return null;
|
||||
for (final entry in decoded) {
|
||||
if (entry is! Map) continue;
|
||||
if (entry['draft'] == true) continue;
|
||||
return entry.cast<String, dynamic>();
|
||||
}
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
} finally {
|
||||
client.close(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, String> _parseAssets(dynamic raw) {
|
||||
final result = <String, String>{};
|
||||
if (raw is! List) return result;
|
||||
for (final entry in raw) {
|
||||
if (entry is! Map) continue;
|
||||
final name = entry['name'] as String?;
|
||||
final url = entry['browser_download_url'] as String?;
|
||||
if (name != null && url != null) result[name] = url;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static String _baseVersion(String tag) {
|
||||
var s = tag.trim();
|
||||
if (s.startsWith('v') || s.startsWith('V')) s = s.substring(1);
|
||||
final dash = s.indexOf('-');
|
||||
if (dash >= 0) s = s.substring(0, dash);
|
||||
final plus = s.indexOf('+');
|
||||
if (plus >= 0) s = s.substring(0, plus);
|
||||
return s;
|
||||
}
|
||||
|
||||
static const int _abiVersionCodeMultiplier = 1000;
|
||||
|
||||
static int? _normalizeBuild(int? build) {
|
||||
if (build == null) return null;
|
||||
if (build >= _abiVersionCodeMultiplier) {
|
||||
return build % _abiVersionCodeMultiplier;
|
||||
}
|
||||
return build;
|
||||
}
|
||||
|
||||
static int? _buildNumber(String tag) {
|
||||
final matches = RegExp(r'\d+').allMatches(tag).toList();
|
||||
if (matches.isEmpty) return null;
|
||||
return int.tryParse(matches.last.group(0)!);
|
||||
}
|
||||
|
||||
static bool _isNewer({
|
||||
required String currentBase,
|
||||
required int? currentBuild,
|
||||
required String remoteBase,
|
||||
required int? remoteBuild,
|
||||
}) {
|
||||
final cmp = _compareSemver(remoteBase, currentBase);
|
||||
if (cmp > 0) return true;
|
||||
if (cmp < 0) return false;
|
||||
if (remoteBuild != null && currentBuild != null) {
|
||||
return remoteBuild > currentBuild;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int _compareSemver(String a, String b) {
|
||||
final pa = _parts(a);
|
||||
final pb = _parts(b);
|
||||
final len = pa.length > pb.length ? pa.length : pb.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
final va = i < pa.length ? pa[i] : 0;
|
||||
final vb = i < pb.length ? pb[i] : 0;
|
||||
if (va != vb) return va > vb ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static List<int> _parts(String v) =>
|
||||
v.split('.').map((p) => int.tryParse(p.trim()) ?? 0).toList();
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'update_checker.dart';
|
||||
|
||||
enum UpdateInstallStatus { done, noAsset, downloadFailed, installFailed }
|
||||
|
||||
class UpdateInstallResult {
|
||||
final UpdateInstallStatus status;
|
||||
final String? error;
|
||||
|
||||
const UpdateInstallResult(this.status, {this.error});
|
||||
|
||||
bool get ok => status == UpdateInstallStatus.done;
|
||||
}
|
||||
|
||||
abstract class UpdateInstaller {
|
||||
static bool get isSupported => Platform.isAndroid;
|
||||
|
||||
static Future<UpdateInstallResult> downloadAndInstall(
|
||||
AppUpdateInfo info, {
|
||||
void Function(double progress)? onProgress,
|
||||
}) async {
|
||||
final url = await resolveApkUrl(info);
|
||||
if (url == null) {
|
||||
return const UpdateInstallResult(UpdateInstallStatus.noAsset);
|
||||
}
|
||||
|
||||
File file;
|
||||
try {
|
||||
file = await _download(url, info.tag, onProgress);
|
||||
} catch (e) {
|
||||
return UpdateInstallResult(
|
||||
UpdateInstallStatus.downloadFailed,
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
final opened = await OpenFilex.open(
|
||||
file.path,
|
||||
type: 'application/vnd.android.package-archive',
|
||||
);
|
||||
if (opened.type != ResultType.done) {
|
||||
return UpdateInstallResult(
|
||||
UpdateInstallStatus.installFailed,
|
||||
error: opened.message,
|
||||
);
|
||||
}
|
||||
return const UpdateInstallResult(UpdateInstallStatus.done);
|
||||
}
|
||||
|
||||
static Future<String?> resolveApkUrl(AppUpdateInfo info) async {
|
||||
if (!Platform.isAndroid || info.assets.isEmpty) return null;
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final flavor = packageInfo.packageName == 'ru.oneme.app' ? 'oneme' : 'komet';
|
||||
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
final abis = androidInfo.supportedAbis;
|
||||
|
||||
for (final abi in abis) {
|
||||
final url = _findAsset(info.assets, '-$flavor-$abi.apk');
|
||||
if (url != null) return url;
|
||||
}
|
||||
return _findAsset(info.assets, '-$flavor-universal.apk');
|
||||
}
|
||||
|
||||
static String? _findAsset(Map<String, String> assets, String suffix) {
|
||||
for (final entry in assets.entries) {
|
||||
if (entry.key.endsWith(suffix)) return entry.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<File> _download(
|
||||
String url,
|
||||
String tag,
|
||||
void Function(double progress)? onProgress,
|
||||
) async {
|
||||
final dir = await getTemporaryDirectory();
|
||||
final safeTag = tag.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_');
|
||||
final file = File('${dir.path}/komet-update-$safeTag.apk');
|
||||
final part = File('${file.path}.part');
|
||||
|
||||
final client = HttpClient();
|
||||
try {
|
||||
final request = await client.getUrl(Uri.parse(url));
|
||||
request.headers.set(HttpHeaders.userAgentHeader, 'KometUpdateInstaller');
|
||||
final response = await request.close();
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
await response.drain<void>();
|
||||
throw HttpException('HTTP ${response.statusCode}', uri: Uri.parse(url));
|
||||
}
|
||||
|
||||
final total = response.contentLength;
|
||||
var received = 0;
|
||||
final sink = part.openWrite();
|
||||
await for (final chunk in response) {
|
||||
received += chunk.length;
|
||||
sink.add(chunk);
|
||||
if (onProgress != null && total > 0) {
|
||||
onProgress(received / total);
|
||||
}
|
||||
}
|
||||
await sink.close();
|
||||
if (await file.exists()) await file.delete();
|
||||
await part.rename(file.path);
|
||||
return file;
|
||||
} catch (e) {
|
||||
if (await part.exists()) {
|
||||
try {
|
||||
await part.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
rethrow;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../core/utils/update_checker.dart';
|
||||
import '../screens/chats/chat_list_screen.dart';
|
||||
import '../screens/chats/chat_screen.dart';
|
||||
import 'update_dialog.dart';
|
||||
|
||||
class AdaptiveShell extends StatefulWidget {
|
||||
const AdaptiveShell({super.key});
|
||||
@@ -46,6 +48,13 @@ class _AdaptiveShellState extends State<AdaptiveShell> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadListWidth();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _maybeCheckUpdate());
|
||||
}
|
||||
|
||||
Future<void> _maybeCheckUpdate() async {
|
||||
final update = await UpdateChecker.check();
|
||||
if (update == null || !mounted) return;
|
||||
await showUpdateDialog(context, update);
|
||||
}
|
||||
|
||||
Future<void> _loadListWidth() async {
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/utils/update_checker.dart';
|
||||
import '../../core/utils/update_installer.dart';
|
||||
import '../../core/utils/link_opener.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'custom_notification.dart';
|
||||
|
||||
Future<void> showUpdateDialog(
|
||||
BuildContext context,
|
||||
AppUpdateInfo info,
|
||||
) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
final cs = Theme.of(dialogContext).colorScheme;
|
||||
final notes = info.notes;
|
||||
return AlertDialog(
|
||||
backgroundColor: cs.surfaceContainerHigh,
|
||||
title: Text(
|
||||
l10n.updateAvailableTitle,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Outfit',
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.updateAvailableBody(info.version),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
if (notes.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.updateWhatsNew,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 180),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
notes,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actionsOverflowButtonSpacing: 4,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
UpdateChecker.skip(info.tag);
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: Text(
|
||||
l10n.updateSkip,
|
||||
style: TextStyle(color: cs.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(
|
||||
l10n.updateLater,
|
||||
style: TextStyle(color: cs.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext);
|
||||
_startUpdate(context, info);
|
||||
},
|
||||
child: Text(l10n.updateAction),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startUpdate(BuildContext context, AppUpdateInfo info) async {
|
||||
if (!UpdateInstaller.isSupported) {
|
||||
await openExternalUrl(context, info.url);
|
||||
return;
|
||||
}
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => _UpdateProgressDialog(info: info),
|
||||
);
|
||||
}
|
||||
|
||||
class _UpdateProgressDialog extends StatefulWidget {
|
||||
final AppUpdateInfo info;
|
||||
|
||||
const _UpdateProgressDialog({required this.info});
|
||||
|
||||
@override
|
||||
State<_UpdateProgressDialog> createState() => _UpdateProgressDialogState();
|
||||
}
|
||||
|
||||
class _UpdateProgressDialogState extends State<_UpdateProgressDialog> {
|
||||
double _progress = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_run();
|
||||
}
|
||||
|
||||
Future<void> _run() async {
|
||||
final result = await UpdateInstaller.downloadAndInstall(
|
||||
widget.info,
|
||||
onProgress: (p) {
|
||||
if (mounted) setState(() => _progress = p.clamp(0.0, 1.0));
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
if (!result.ok) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
showCustomNotification(context, l10n.updateDownloadFailed);
|
||||
await openExternalUrl(context, widget.info.url);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final percent = (_progress * 100).round();
|
||||
return AlertDialog(
|
||||
backgroundColor: cs.surfaceContainerHigh,
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.updateDownloading,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: LinearProgressIndicator(
|
||||
value: _progress > 0 ? _progress : null,
|
||||
minHeight: 6,
|
||||
backgroundColor: cs.surfaceContainerHighest,
|
||||
color: cs.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'$percent%',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -817,5 +817,20 @@
|
||||
"fontSettingsLoading": "Loading…",
|
||||
"fontSettingsSectionFontSize": "Font size",
|
||||
"fontSettingsPreviewLabel": "PREVIEW",
|
||||
"fontSettingsReset": "Reset"
|
||||
"fontSettingsReset": "Reset",
|
||||
"updateAvailableTitle": "Update available",
|
||||
"updateAvailableBody": "Version {version} is out. Update the app?",
|
||||
"@updateAvailableBody": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateWhatsNew": "WHAT'S NEW",
|
||||
"updateAction": "Update",
|
||||
"updateLater": "Later",
|
||||
"updateSkip": "Skip",
|
||||
"updateDownloading": "Downloading update…",
|
||||
"updateDownloadFailed": "Failed to download the update"
|
||||
}
|
||||
|
||||
@@ -3787,6 +3787,54 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset'**
|
||||
String get fontSettingsReset;
|
||||
|
||||
/// No description provided for @updateAvailableTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Update available'**
|
||||
String get updateAvailableTitle;
|
||||
|
||||
/// No description provided for @updateAvailableBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Version {version} is out. Update the app?'**
|
||||
String updateAvailableBody(String version);
|
||||
|
||||
/// No description provided for @updateWhatsNew.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'WHAT\'S NEW'**
|
||||
String get updateWhatsNew;
|
||||
|
||||
/// No description provided for @updateAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Update'**
|
||||
String get updateAction;
|
||||
|
||||
/// No description provided for @updateLater.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Later'**
|
||||
String get updateLater;
|
||||
|
||||
/// No description provided for @updateSkip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Skip'**
|
||||
String get updateSkip;
|
||||
|
||||
/// No description provided for @updateDownloading.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloading update…'**
|
||||
String get updateDownloading;
|
||||
|
||||
/// No description provided for @updateDownloadFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to download the update'**
|
||||
String get updateDownloadFailed;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -1949,4 +1949,30 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get fontSettingsReset => 'Reset';
|
||||
|
||||
@override
|
||||
String get updateAvailableTitle => 'Update available';
|
||||
|
||||
@override
|
||||
String updateAvailableBody(String version) {
|
||||
return 'Version $version is out. Update the app?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get updateWhatsNew => 'WHAT\'S NEW';
|
||||
|
||||
@override
|
||||
String get updateAction => 'Update';
|
||||
|
||||
@override
|
||||
String get updateLater => 'Later';
|
||||
|
||||
@override
|
||||
String get updateSkip => 'Skip';
|
||||
|
||||
@override
|
||||
String get updateDownloading => 'Downloading update…';
|
||||
|
||||
@override
|
||||
String get updateDownloadFailed => 'Failed to download the update';
|
||||
}
|
||||
|
||||
@@ -1959,4 +1959,30 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get fontSettingsReset => 'Сбросить';
|
||||
|
||||
@override
|
||||
String get updateAvailableTitle => 'Доступно обновление';
|
||||
|
||||
@override
|
||||
String updateAvailableBody(String version) {
|
||||
return 'Вышла версия $version. Обновить приложение?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get updateWhatsNew => 'ЧТО НОВОГО';
|
||||
|
||||
@override
|
||||
String get updateAction => 'Обновить';
|
||||
|
||||
@override
|
||||
String get updateLater => 'Позже';
|
||||
|
||||
@override
|
||||
String get updateSkip => 'Пропустить';
|
||||
|
||||
@override
|
||||
String get updateDownloading => 'Загрузка обновления…';
|
||||
|
||||
@override
|
||||
String get updateDownloadFailed => 'Не удалось скачать обновление';
|
||||
}
|
||||
|
||||
+16
-1
@@ -639,5 +639,20 @@
|
||||
"fontSettingsLoading": "Загрузка…",
|
||||
"fontSettingsSectionFontSize": "Размер шрифта",
|
||||
"fontSettingsPreviewLabel": "ПРЕДПРОСМОТР",
|
||||
"fontSettingsReset": "Сбросить"
|
||||
"fontSettingsReset": "Сбросить",
|
||||
"updateAvailableTitle": "Доступно обновление",
|
||||
"updateAvailableBody": "Вышла версия {version}. Обновить приложение?",
|
||||
"@updateAvailableBody": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateWhatsNew": "ЧТО НОВОГО",
|
||||
"updateAction": "Обновить",
|
||||
"updateLater": "Позже",
|
||||
"updateSkip": "Пропустить",
|
||||
"updateDownloading": "Загрузка обновления…",
|
||||
"updateDownloadFailed": "Не удалось скачать обновление"
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 0.5.0+12
|
||||
version: 0.5.0+13
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.4
|
||||
|
||||
Reference in New Issue
Block a user