i18n support

This commit is contained in:
Junyuan Feng
2022-05-05 16:07:55 +08:00
parent f9aa3b1728
commit 29e3ee0156
22 changed files with 1601 additions and 147 deletions

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/home.dart';
@@ -30,6 +32,13 @@ class MyApp extends StatelessWidget {
final primaryColor = Color(value);
final textStyle = TextStyle(color: primaryColor);
return MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
title: BuildData.name,
debugShowCheckedModeBanner: false,
theme: ThemeData(

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:toolbox/core/persistant_store.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/widget/card_dialog.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:toolbox/core/extension/stringx.dart';
@@ -82,3 +83,17 @@ void setTransparentNavigationBar(BuildContext context) {
));
}
}
String tabTitleName(BuildContext context, int i) {
final s = S.of(context);
switch (i) {
case 0:
return s.server;
case 1:
return s.convert;
case 2:
return s.ping;
default:
return '';
}
}

View File

@@ -0,0 +1,3 @@
import 'package:flutter/material.dart';
const TextStyle size18 = TextStyle(fontSize: 18);

View File

@@ -0,0 +1,66 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that looks up messages for specific locales by
// delegating to the appropriate library.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:implementation_imports, file_names, unnecessary_new
// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
// ignore_for_file:argument_type_not_assignable, invalid_assignment
// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
// ignore_for_file:comment_references
import 'dart:async';
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_en.dart' as messages_en;
import 'messages_zh.dart' as messages_zh;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new Future.value(null),
'zh': () => new Future.value(null),
};
MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'en':
return messages_en.messages;
case 'zh':
return messages_zh.messages;
default:
return null;
}
}
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) async {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new Future.value(false);
}
var lib = _deferredLibraries[availableLocale];
await (lib == null ? new Future.value(false) : lib());
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new Future.value(true);
}
bool _messagesExistFor(String locale) {
try {
return _findExact(locale) != null;
} catch (e) {
return false;
}
}
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale =
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
if (actualLocale == null) return null;
return _findExact(actualLocale);
}

View File

@@ -0,0 +1,140 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a en locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en';
static String m0(rainSunMeGithub) =>
"\nThanks ${rainSunMeGithub} for participating in the test.\n\nAll rights reserved.";
static String m1(code) => "request failed, status code: ${code}";
static String m2(myGithub) => "\nMade with ❤️ by ${myGithub}";
static String m3(server) => "Are you sure to delete server [${server}]?";
static String m4(build) => "Found: v1.0.${build}, click to update";
static String m5(build) => "Current: v1.0.${build}";
static String m6(build) => "Current: v1.0.${build}, is up to date";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks": m0,
"addAServer": MessageLookupByLibrary.simpleMessage("add a server"),
"addPrivateKey":
MessageLookupByLibrary.simpleMessage("Add private key"),
"appPrimaryColor":
MessageLookupByLibrary.simpleMessage("App primary color"),
"attention": MessageLookupByLibrary.simpleMessage("Attention"),
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
"choose": MessageLookupByLibrary.simpleMessage("Choose"),
"chooseDestination":
MessageLookupByLibrary.simpleMessage("Choose destination"),
"choosePrivateKey":
MessageLookupByLibrary.simpleMessage("Choose private key"),
"clear": MessageLookupByLibrary.simpleMessage("Clear"),
"close": MessageLookupByLibrary.simpleMessage("Close"),
"convert": MessageLookupByLibrary.simpleMessage("Convert"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"currentMode": MessageLookupByLibrary.simpleMessage("Current Mode"),
"debug": MessageLookupByLibrary.simpleMessage("Debug"),
"decode": MessageLookupByLibrary.simpleMessage("Decode"),
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"encode": MessageLookupByLibrary.simpleMessage("Encode"),
"exampleName": MessageLookupByLibrary.simpleMessage("Example name"),
"export": MessageLookupByLibrary.simpleMessage("Export"),
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage(
"These fields must not be empty."),
"go": MessageLookupByLibrary.simpleMessage("Go"),
"host": MessageLookupByLibrary.simpleMessage("Host"),
"httpFailedWithCode": m1,
"import": MessageLookupByLibrary.simpleMessage("Import"),
"importAndExport":
MessageLookupByLibrary.simpleMessage("Import and Export"),
"keyAuth": MessageLookupByLibrary.simpleMessage("Key Auth"),
"launchPage": MessageLookupByLibrary.simpleMessage("Launch page"),
"license": MessageLookupByLibrary.simpleMessage("License"),
"loss": MessageLookupByLibrary.simpleMessage("Loss"),
"madeWithLove": m2,
"max": MessageLookupByLibrary.simpleMessage("max"),
"min": MessageLookupByLibrary.simpleMessage("min"),
"ms": MessageLookupByLibrary.simpleMessage("ms"),
"name": MessageLookupByLibrary.simpleMessage("Name"),
"noResult": MessageLookupByLibrary.simpleMessage("No result"),
"noSavedPrivateKey":
MessageLookupByLibrary.simpleMessage("No saved private keys."),
"noSavedSnippet":
MessageLookupByLibrary.simpleMessage("No saved snippets."),
"noServerAvailable":
MessageLookupByLibrary.simpleMessage("No server available."),
"ok": MessageLookupByLibrary.simpleMessage("OK"),
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("Avg:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage(
"Please input a target IP/domain."),
"plzEnterHost":
MessageLookupByLibrary.simpleMessage("Please enter host."),
"plzEnterPwd":
MessageLookupByLibrary.simpleMessage("Please enter password."),
"plzSelectKey":
MessageLookupByLibrary.simpleMessage("Please select a key."),
"port": MessageLookupByLibrary.simpleMessage("Port"),
"privateKey": MessageLookupByLibrary.simpleMessage("Private Key"),
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
"result": MessageLookupByLibrary.simpleMessage("Result"),
"run": MessageLookupByLibrary.simpleMessage("Run"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
"second": MessageLookupByLibrary.simpleMessage("s"),
"server": MessageLookupByLibrary.simpleMessage("Server"),
"serverTabConnecting":
MessageLookupByLibrary.simpleMessage("Connecting..."),
"serverTabEmpty": MessageLookupByLibrary.simpleMessage(
"There is no server.\nClick the fab to add one."),
"serverTabFailed": MessageLookupByLibrary.simpleMessage("Failed"),
"serverTabLoading": MessageLookupByLibrary.simpleMessage("Loading..."),
"serverTabPlzSave": MessageLookupByLibrary.simpleMessage(
"Please \'save\' this private key again."),
"serverTabUnkown":
MessageLookupByLibrary.simpleMessage("Unknown state"),
"setting": MessageLookupByLibrary.simpleMessage("Setting"),
"snippet": MessageLookupByLibrary.simpleMessage("Snippet"),
"start": MessageLookupByLibrary.simpleMessage("Start"),
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
"sureToDeleteServer": m3,
"ttl": MessageLookupByLibrary.simpleMessage("TTL"),
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
"unkownConvertMode":
MessageLookupByLibrary.simpleMessage("Unknown convert mode"),
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
"You set to 0, will not update automatically.\nYou can pull to refresh manually."),
"updateServerStatusInterval": MessageLookupByLibrary.simpleMessage(
"Server status update interval"),
"upsideDown": MessageLookupByLibrary.simpleMessage("Upside Down"),
"urlOrJson": MessageLookupByLibrary.simpleMessage("URL or JSON"),
"user": MessageLookupByLibrary.simpleMessage("User"),
"versionHaveUpdate": m4,
"versionUnknownUpdate": m5,
"versionUpdated": m6,
"willTakEeffectImmediately":
MessageLookupByLibrary.simpleMessage("Will take effect immediately")
};
}

View File

@@ -0,0 +1,123 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a zh locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'zh';
static String m0(rainSunMeGithub) =>
"\n感谢 ${rainSunMeGithub} 参与软件测试。\n\n保留所有权利。";
static String m1(code) => "请求失败, 状态码: ${code}";
static String m2(myGithub) => "\n用❤️制作 by ${myGithub}";
static String m3(server) => "你确定要删除服务器 [${server}] 吗?";
static String m4(build) => "找到新版本v1.0.${build}, 点击更新";
static String m5(build) => "当前v1.0.${build}";
static String m6(build) => "当前v1.0.${build}, 已是最新版本";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks": m0,
"addAServer": MessageLookupByLibrary.simpleMessage("添加服务器"),
"addPrivateKey": MessageLookupByLibrary.simpleMessage("添加一个私钥"),
"appPrimaryColor": MessageLookupByLibrary.simpleMessage("App主要色"),
"attention": MessageLookupByLibrary.simpleMessage("注意"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
"choose": MessageLookupByLibrary.simpleMessage("选择"),
"chooseDestination": MessageLookupByLibrary.simpleMessage("选择目标"),
"choosePrivateKey": MessageLookupByLibrary.simpleMessage("选择私钥"),
"clear": MessageLookupByLibrary.simpleMessage("清除"),
"close": MessageLookupByLibrary.simpleMessage("关闭"),
"convert": MessageLookupByLibrary.simpleMessage("转换"),
"copy": MessageLookupByLibrary.simpleMessage("复制到剪切板"),
"currentMode": MessageLookupByLibrary.simpleMessage("当前模式"),
"debug": MessageLookupByLibrary.simpleMessage("调试"),
"decode": MessageLookupByLibrary.simpleMessage("解码"),
"delete": MessageLookupByLibrary.simpleMessage("删除"),
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"encode": MessageLookupByLibrary.simpleMessage("编码"),
"exampleName": MessageLookupByLibrary.simpleMessage("名称示例"),
"export": MessageLookupByLibrary.simpleMessage("导出"),
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage("这些输入框不能为空。"),
"go": MessageLookupByLibrary.simpleMessage("开始"),
"host": MessageLookupByLibrary.simpleMessage("主机"),
"httpFailedWithCode": m1,
"import": MessageLookupByLibrary.simpleMessage("导入"),
"importAndExport": MessageLookupByLibrary.simpleMessage("导入或导出"),
"keyAuth": MessageLookupByLibrary.simpleMessage("公钥认证"),
"launchPage": MessageLookupByLibrary.simpleMessage("启动页"),
"license": MessageLookupByLibrary.simpleMessage("开源证书"),
"loss": MessageLookupByLibrary.simpleMessage("丢包率"),
"madeWithLove": m2,
"max": MessageLookupByLibrary.simpleMessage("最大"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"ms": MessageLookupByLibrary.simpleMessage("毫秒"),
"name": MessageLookupByLibrary.simpleMessage("名称"),
"noResult": MessageLookupByLibrary.simpleMessage("无结果"),
"noSavedPrivateKey": MessageLookupByLibrary.simpleMessage("没有已保存的私钥。"),
"noSavedSnippet": MessageLookupByLibrary.simpleMessage("没有已保存的代码片段。"),
"noServerAvailable": MessageLookupByLibrary.simpleMessage("没有可用的服务器。"),
"ok": MessageLookupByLibrary.simpleMessage(""),
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("平均:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"),
"plzEnterHost": MessageLookupByLibrary.simpleMessage("请输入主机"),
"plzEnterPwd": MessageLookupByLibrary.simpleMessage("请输入密码"),
"plzSelectKey": MessageLookupByLibrary.simpleMessage("请选择私钥"),
"port": MessageLookupByLibrary.simpleMessage("端口"),
"privateKey": MessageLookupByLibrary.simpleMessage("私钥"),
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
"result": MessageLookupByLibrary.simpleMessage("结果"),
"run": MessageLookupByLibrary.simpleMessage("运行"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"second": MessageLookupByLibrary.simpleMessage(""),
"server": MessageLookupByLibrary.simpleMessage("服务器"),
"serverTabConnecting": MessageLookupByLibrary.simpleMessage("连接中..."),
"serverTabEmpty":
MessageLookupByLibrary.simpleMessage("现在没有服务器。\n点击右下方按钮来添加。"),
"serverTabFailed": MessageLookupByLibrary.simpleMessage("失败"),
"serverTabLoading": MessageLookupByLibrary.simpleMessage("加载中..."),
"serverTabPlzSave": MessageLookupByLibrary.simpleMessage("请再次保存该私钥"),
"serverTabUnkown": MessageLookupByLibrary.simpleMessage("未知状态"),
"setting": MessageLookupByLibrary.simpleMessage("设置"),
"snippet": MessageLookupByLibrary.simpleMessage("代码片段"),
"start": MessageLookupByLibrary.simpleMessage("开始"),
"stop": MessageLookupByLibrary.simpleMessage("停止"),
"sureToDeleteServer": m3,
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"unkownConvertMode": MessageLookupByLibrary.simpleMessage("未知转换模式"),
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
"你设置为0服务器状态不会自动刷新。\n你可以手动下拉刷新。"),
"updateServerStatusInterval":
MessageLookupByLibrary.simpleMessage("服务器状态刷新间隔"),
"upsideDown": MessageLookupByLibrary.simpleMessage("上下交换"),
"urlOrJson": MessageLookupByLibrary.simpleMessage("链接或JSON"),
"user": MessageLookupByLibrary.simpleMessage("用户"),
"versionHaveUpdate": m4,
"versionUnknownUpdate": m5,
"versionUpdated": m6,
"willTakEeffectImmediately":
MessageLookupByLibrary.simpleMessage("更改将会立即生效")
};
}

859
lib/generated/l10n.dart Normal file
View File

@@ -0,0 +1,859 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'intl/messages_all.dart';
// **************************************************************************
// Generator: Flutter Intl IDE plugin
// Made by Localizely
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each
// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes
class S {
S();
static S? _current;
static S get current {
assert(_current != null,
'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.');
return _current!;
}
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<S> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
final instance = S();
S._current = instance;
return instance;
});
}
static S of(BuildContext context) {
final instance = S.maybeOf(context);
assert(instance != null,
'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?');
return instance!;
}
static S? maybeOf(BuildContext context) {
return Localizations.of<S>(context, S);
}
/// `Server`
String get server {
return Intl.message(
'Server',
name: 'server',
desc: '',
args: [],
);
}
/// `Convert`
String get convert {
return Intl.message(
'Convert',
name: 'convert',
desc: '',
args: [],
);
}
/// `Ping`
String get ping {
return Intl.message(
'Ping',
name: 'ping',
desc: '',
args: [],
);
}
/// `Debug`
String get debug {
return Intl.message(
'Debug',
name: 'debug',
desc: '',
args: [],
);
}
/// `add a server`
String get addAServer {
return Intl.message(
'add a server',
name: 'addAServer',
desc: '',
args: [],
);
}
/// `Setting`
String get setting {
return Intl.message(
'Setting',
name: 'setting',
desc: '',
args: [],
);
}
/// `License`
String get license {
return Intl.message(
'License',
name: 'license',
desc: '',
args: [],
);
}
/// `Snippet`
String get snippet {
return Intl.message(
'Snippet',
name: 'snippet',
desc: '',
args: [],
);
}
/// `Private Key`
String get privateKey {
return Intl.message(
'Private Key',
name: 'privateKey',
desc: '',
args: [],
);
}
/// `\nMade with ❤️ by {myGithub}`
String madeWithLove(Object myGithub) {
return Intl.message(
'\nMade with ❤️ by $myGithub',
name: 'madeWithLove',
desc: '',
args: [myGithub],
);
}
/// `\nThanks {rainSunMeGithub} for participating in the test.\n\nAll rights reserved.`
String aboutThanks(Object rainSunMeGithub) {
return Intl.message(
'\nThanks $rainSunMeGithub for participating in the test.\n\nAll rights reserved.',
name: 'aboutThanks',
desc: '',
args: [rainSunMeGithub],
);
}
/// `There is no server.\nClick the fab to add one.`
String get serverTabEmpty {
return Intl.message(
'There is no server.\nClick the fab to add one.',
name: 'serverTabEmpty',
desc: '',
args: [],
);
}
/// `Loading...`
String get serverTabLoading {
return Intl.message(
'Loading...',
name: 'serverTabLoading',
desc: '',
args: [],
);
}
/// `Please 'save' this private key again.`
String get serverTabPlzSave {
return Intl.message(
'Please \'save\' this private key again.',
name: 'serverTabPlzSave',
desc: '',
args: [],
);
}
/// `Failed`
String get serverTabFailed {
return Intl.message(
'Failed',
name: 'serverTabFailed',
desc: '',
args: [],
);
}
/// `Unknown state`
String get serverTabUnkown {
return Intl.message(
'Unknown state',
name: 'serverTabUnkown',
desc: '',
args: [],
);
}
/// `Connecting...`
String get serverTabConnecting {
return Intl.message(
'Connecting...',
name: 'serverTabConnecting',
desc: '',
args: [],
);
}
/// `Decode`
String get decode {
return Intl.message(
'Decode',
name: 'decode',
desc: '',
args: [],
);
}
/// `Encode`
String get encode {
return Intl.message(
'Encode',
name: 'encode',
desc: '',
args: [],
);
}
/// `Current Mode`
String get currentMode {
return Intl.message(
'Current Mode',
name: 'currentMode',
desc: '',
args: [],
);
}
/// `Unknown convert mode`
String get unkownConvertMode {
return Intl.message(
'Unknown convert mode',
name: 'unkownConvertMode',
desc: '',
args: [],
);
}
/// `Copy`
String get copy {
return Intl.message(
'Copy',
name: 'copy',
desc: '',
args: [],
);
}
/// `Upside Down`
String get upsideDown {
return Intl.message(
'Upside Down',
name: 'upsideDown',
desc: '',
args: [],
);
}
/// `Avg:`
String get pingAvg {
return Intl.message(
'Avg:',
name: 'pingAvg',
desc: '',
args: [],
);
}
/// `unknown`
String get unknown {
return Intl.message(
'unknown',
name: 'unknown',
desc: '',
args: [],
);
}
/// `min`
String get min {
return Intl.message(
'min',
name: 'min',
desc: '',
args: [],
);
}
/// `max`
String get max {
return Intl.message(
'max',
name: 'max',
desc: '',
args: [],
);
}
/// `ms`
String get ms {
return Intl.message(
'ms',
name: 'ms',
desc: '',
args: [],
);
}
/// `TTL`
String get ttl {
return Intl.message(
'TTL',
name: 'ttl',
desc: '',
args: [],
);
}
/// `Loss`
String get loss {
return Intl.message(
'Loss',
name: 'loss',
desc: '',
args: [],
);
}
/// `No result`
String get noResult {
return Intl.message(
'No result',
name: 'noResult',
desc: '',
args: [],
);
}
/// `Please input a target IP/domain.`
String get pingInputIP {
return Intl.message(
'Please input a target IP/domain.',
name: 'pingInputIP',
desc: '',
args: [],
);
}
/// `Clear`
String get clear {
return Intl.message(
'Clear',
name: 'clear',
desc: '',
args: [],
);
}
/// `Start`
String get start {
return Intl.message(
'Start',
name: 'start',
desc: '',
args: [],
);
}
/// `App primary color`
String get appPrimaryColor {
return Intl.message(
'App primary color',
name: 'appPrimaryColor',
desc: '',
args: [],
);
}
/// `Server status update interval`
String get updateServerStatusInterval {
return Intl.message(
'Server status update interval',
name: 'updateServerStatusInterval',
desc: '',
args: [],
);
}
/// `Will take effect immediately`
String get willTakEeffectImmediately {
return Intl.message(
'Will take effect immediately',
name: 'willTakEeffectImmediately',
desc: '',
args: [],
);
}
/// `Launch page`
String get launchPage {
return Intl.message(
'Launch page',
name: 'launchPage',
desc: '',
args: [],
);
}
/// `Current: v1.0.{build}, is up to date`
String versionUpdated(Object build) {
return Intl.message(
'Current: v1.0.$build, is up to date',
name: 'versionUpdated',
desc: '',
args: [build],
);
}
/// `Current: v1.0.{build}`
String versionUnknownUpdate(Object build) {
return Intl.message(
'Current: v1.0.$build',
name: 'versionUnknownUpdate',
desc: '',
args: [build],
);
}
/// `Found: v1.0.{build}, click to update`
String versionHaveUpdate(Object build) {
return Intl.message(
'Found: v1.0.$build, click to update',
name: 'versionHaveUpdate',
desc: '',
args: [build],
);
}
/// `s`
String get second {
return Intl.message(
's',
name: 'second',
desc: '',
args: [],
);
}
/// `You set to 0, will not update automatically.\nYou can pull to refresh manually.`
String get updateIntervalEqual0 {
return Intl.message(
'You set to 0, will not update automatically.\nYou can pull to refresh manually.',
name: 'updateIntervalEqual0',
desc: '',
args: [],
);
}
/// `Edit`
String get edit {
return Intl.message(
'Edit',
name: 'edit',
desc: '',
args: [],
);
}
/// `No saved private keys.`
String get noSavedPrivateKey {
return Intl.message(
'No saved private keys.',
name: 'noSavedPrivateKey',
desc: '',
args: [],
);
}
/// `Name`
String get name {
return Intl.message(
'Name',
name: 'name',
desc: '',
args: [],
);
}
/// `Password`
String get pwd {
return Intl.message(
'Password',
name: 'pwd',
desc: '',
args: [],
);
}
/// `Save`
String get save {
return Intl.message(
'Save',
name: 'save',
desc: '',
args: [],
);
}
/// `Delete`
String get delete {
return Intl.message(
'Delete',
name: 'delete',
desc: '',
args: [],
);
}
/// `These fields must not be empty.`
String get fieldMustNotEmpty {
return Intl.message(
'These fields must not be empty.',
name: 'fieldMustNotEmpty',
desc: '',
args: [],
);
}
/// `Import and Export`
String get importAndExport {
return Intl.message(
'Import and Export',
name: 'importAndExport',
desc: '',
args: [],
);
}
/// `Choose`
String get choose {
return Intl.message(
'Choose',
name: 'choose',
desc: '',
args: [],
);
}
/// `Import`
String get import {
return Intl.message(
'Import',
name: 'import',
desc: '',
args: [],
);
}
/// `Export`
String get export {
return Intl.message(
'Export',
name: 'export',
desc: '',
args: [],
);
}
/// `OK`
String get ok {
return Intl.message(
'OK',
name: 'ok',
desc: '',
args: [],
);
}
/// `Cancel`
String get cancel {
return Intl.message(
'Cancel',
name: 'cancel',
desc: '',
args: [],
);
}
/// `URL or JSON`
String get urlOrJson {
return Intl.message(
'URL or JSON',
name: 'urlOrJson',
desc: '',
args: [],
);
}
/// `Go`
String get go {
return Intl.message(
'Go',
name: 'go',
desc: '',
args: [],
);
}
/// `request failed, status code: {code}`
String httpFailedWithCode(Object code) {
return Intl.message(
'request failed, status code: $code',
name: 'httpFailedWithCode',
desc: '',
args: [code],
);
}
/// `Run`
String get run {
return Intl.message(
'Run',
name: 'run',
desc: '',
args: [],
);
}
/// `No saved snippets.`
String get noSavedSnippet {
return Intl.message(
'No saved snippets.',
name: 'noSavedSnippet',
desc: '',
args: [],
);
}
/// `Choose destination`
String get chooseDestination {
return Intl.message(
'Choose destination',
name: 'chooseDestination',
desc: '',
args: [],
);
}
/// `No server available.`
String get noServerAvailable {
return Intl.message(
'No server available.',
name: 'noServerAvailable',
desc: '',
args: [],
);
}
/// `Result`
String get result {
return Intl.message(
'Result',
name: 'result',
desc: '',
args: [],
);
}
/// `Close`
String get close {
return Intl.message(
'Close',
name: 'close',
desc: '',
args: [],
);
}
/// `Attention`
String get attention {
return Intl.message(
'Attention',
name: 'attention',
desc: '',
args: [],
);
}
/// `Are you sure to delete server [{server}]?`
String sureToDeleteServer(Object server) {
return Intl.message(
'Are you sure to delete server [$server]?',
name: 'sureToDeleteServer',
desc: '',
args: [server],
);
}
/// `Host`
String get host {
return Intl.message(
'Host',
name: 'host',
desc: '',
args: [],
);
}
/// `Port`
String get port {
return Intl.message(
'Port',
name: 'port',
desc: '',
args: [],
);
}
/// `User`
String get user {
return Intl.message(
'User',
name: 'user',
desc: '',
args: [],
);
}
/// `Key Auth`
String get keyAuth {
return Intl.message(
'Key Auth',
name: 'keyAuth',
desc: '',
args: [],
);
}
/// `Add private key`
String get addPrivateKey {
return Intl.message(
'Add private key',
name: 'addPrivateKey',
desc: '',
args: [],
);
}
/// `Choose private key`
String get choosePrivateKey {
return Intl.message(
'Choose private key',
name: 'choosePrivateKey',
desc: '',
args: [],
);
}
/// `Please enter host.`
String get plzEnterHost {
return Intl.message(
'Please enter host.',
name: 'plzEnterHost',
desc: '',
args: [],
);
}
/// `Please enter password.`
String get plzEnterPwd {
return Intl.message(
'Please enter password.',
name: 'plzEnterPwd',
desc: '',
args: [],
);
}
/// `Please select a key.`
String get plzSelectKey {
return Intl.message(
'Please select a key.',
name: 'plzSelectKey',
desc: '',
args: [],
);
}
/// `Example name`
String get exampleName {
return Intl.message(
'Example name',
name: 'exampleName',
desc: '',
args: [],
);
}
/// `Stop`
String get stop {
return Intl.message(
'Stop',
name: 'stop',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
const AppLocalizationDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'zh'),
];
}
@override
bool isSupported(Locale locale) => _isSupported(locale);
@override
Future<S> load(Locale locale) => S.load(locale);
@override
bool shouldReload(AppLocalizationDelegate old) => false;
bool _isSupported(Locale locale) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return true;
}
}
return false;
}
}

80
lib/l10n/intl_en.arb Normal file
View File

@@ -0,0 +1,80 @@
{
"server": "Server",
"convert": "Convert",
"ping": "Ping",
"debug": "Debug",
"addAServer": "add a server",
"setting": "Setting",
"license": "License",
"snippet": "Snippet",
"privateKey": "Private Key",
"madeWithLove": "\nMade with ❤️ by {myGithub}",
"aboutThanks": "\nThanks {rainSunMeGithub} for participating in the test.\n\nAll rights reserved.",
"serverTabEmpty": "There is no server.\nClick the fab to add one.",
"serverTabLoading": "Loading...",
"serverTabPlzSave": "Please 'save' this private key again.",
"serverTabFailed": "Failed",
"serverTabUnkown": "Unknown state",
"serverTabConnecting": "Connecting...",
"decode": "Decode",
"encode": "Encode",
"currentMode": "Current Mode",
"unkownConvertMode": "Unknown convert mode",
"copy": "Copy",
"upsideDown": "Upside Down",
"pingAvg": "Avg:",
"unknown": "unknown",
"min": "min",
"max": "max",
"ms": "ms",
"ttl": "TTL",
"loss": "Loss",
"noResult": "No result",
"pingInputIP": "Please input a target IP/domain.",
"clear": "Clear",
"start": "Start",
"appPrimaryColor": "App primary color",
"updateServerStatusInterval": "Server status update interval",
"willTakEeffectImmediately": "Will take effect immediately",
"launchPage": "Launch page",
"versionUpdated": "Current: v1.0.{build}, is up to date",
"versionUnknownUpdate": "Current: v1.0.{build}",
"versionHaveUpdate": "Found: v1.0.{build}, click to update",
"second": "s",
"updateIntervalEqual0": "You set to 0, will not update automatically.\nYou can pull to refresh manually.",
"edit": "Edit",
"noSavedPrivateKey": "No saved private keys.",
"name": "Name",
"pwd": "Password",
"save": "Save",
"delete": "Delete",
"fieldMustNotEmpty": "These fields must not be empty.",
"importAndExport": "Import and Export",
"choose": "Choose",
"import": "Import",
"export": "Export",
"ok": "OK",
"cancel": "Cancel",
"urlOrJson": "URL or JSON",
"go": "Go",
"httpFailedWithCode": "request failed, status code: {code}",
"run": "Run",
"noSavedSnippet": "No saved snippets.",
"chooseDestination": "Choose destination",
"noServerAvailable": "No server available.",
"result": "Result",
"close": "Close",
"attention": "Attention",
"sureToDeleteServer": "Are you sure to delete server [{server}]?",
"host": "Host",
"port": "Port",
"user": "User",
"keyAuth": "Key Auth",
"addPrivateKey": "Add private key",
"choosePrivateKey": "Choose private key",
"plzEnterHost": "Please enter host.",
"plzEnterPwd": "Please enter password.",
"plzSelectKey": "Please select a key.",
"exampleName": "Example name",
"stop": "Stop"
}

80
lib/l10n/intl_zh.arb Normal file
View File

@@ -0,0 +1,80 @@
{
"server": "服务器",
"convert": "转换",
"ping": "Ping",
"debug": "调试",
"addAServer": "添加服务器",
"setting": "设置",
"license": "开源证书",
"snippet": "代码片段",
"privateKey": "私钥",
"madeWithLove": "\n用❤制作 by {myGithub}",
"aboutThanks": "\n感谢 {rainSunMeGithub} 参与软件测试。\n\n保留所有权利。",
"serverTabEmpty": "现在没有服务器。\n点击右下方按钮来添加。",
"serverTabLoading": "加载中...",
"serverTabPlzSave": "请再次保存该私钥",
"serverTabFailed": "失败",
"serverTabUnkown": "未知状态",
"serverTabConnecting": "连接中...",
"decode": "解码",
"encode": "编码",
"currentMode": "当前模式",
"unkownConvertMode": "未知转换模式",
"copy": "复制到剪切板",
"upsideDown": "上下交换",
"pingAvg": "平均:",
"unknown": "未知",
"min": "最小",
"max": "最大",
"ms": "毫秒",
"ttl": "缓存时间",
"loss": "丢包率",
"noResult": "无结果",
"pingInputIP": "请输入目标IP或域名",
"clear": "清除",
"start": "开始",
"appPrimaryColor": "App主要色",
"updateServerStatusInterval": "服务器状态刷新间隔",
"willTakEeffectImmediately": "更改将会立即生效",
"launchPage": "启动页",
"versionUpdated": "当前v1.0.{build}, 已是最新版本",
"versionUnknownUpdate": "当前v1.0.{build}",
"versionHaveUpdate": "找到新版本v1.0.{build}, 点击更新",
"second": "秒",
"updateIntervalEqual0": "你设置为0服务器状态不会自动刷新。\n你可以手动下拉刷新。",
"edit": "编辑",
"noSavedPrivateKey": "没有已保存的私钥。",
"name": "名称",
"pwd": "密码",
"save": "保存",
"delete": "删除",
"fieldMustNotEmpty": "这些输入框不能为空。",
"importAndExport": "导入或导出",
"choose": "选择",
"import": "导入",
"export": "导出",
"ok": "好",
"cancel": "取消",
"urlOrJson": "链接或JSON",
"go": "开始",
"httpFailedWithCode": "请求失败, 状态码: {code}",
"run": "运行",
"noSavedSnippet": "没有已保存的代码片段。",
"chooseDestination": "选择目标",
"noServerAvailable": "没有可用的服务器。",
"result": "结果",
"close": "关闭",
"attention": "注意",
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
"host": "主机",
"port": "端口",
"user": "用户",
"keyAuth": "公钥认证",
"addPrivateKey": "添加一个私钥",
"choosePrivateKey": "选择私钥",
"plzEnterHost": "请输入主机",
"plzEnterPwd": "请输入密码",
"plzSelectKey": "请选择私钥",
"exampleName": "名称示例",
"stop": "停止"
}

View File

@@ -4,6 +4,7 @@ import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/widget/input_field.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -20,13 +21,8 @@ class _ConvertPageState extends State<ConvertPage>
late TextEditingController _textEditingControllerResult;
late MediaQueryData _media;
late ThemeData _theme;
late S s;
static const List<String> _typeOption = [
'base64 decode',
'base64 encode',
'URL encode',
'URL decode'
];
int _typeOptionIndex = 0;
@override
@@ -41,6 +37,7 @@ class _ConvertPageState extends State<ConvertPage>
super.didChangeDependencies();
_media = MediaQuery.of(context);
_theme = Theme.of(context);
s = S.of(context);
}
@override
@@ -67,7 +64,7 @@ class _ConvertPageState extends State<ConvertPage>
showSnackBar(context, Text('Error: \n$e'));
}
},
tooltip: 'convert',
tooltip: s.convert,
child: const Icon(Icons.send),
),
);
@@ -85,7 +82,7 @@ class _ConvertPageState extends State<ConvertPage>
case 3:
return Uri.decodeFull(text);
default:
return 'Unknown Convert Method';
return s.unkownConvertMode;
}
}
@@ -97,6 +94,14 @@ class _ConvertPageState extends State<ConvertPage>
}
Widget _buildTypeOption() {
final decode = s.decode;
final encode = s.encode;
final List<String> _typeOption = [
'Base64 $decode',
'Base64 $encode',
'URL $encode',
'URL $decode'
];
return RoundRectCard(
ExpansionTile(
tilePadding: const EdgeInsets.only(left: 7, right: 27),
@@ -106,7 +111,7 @@ class _ConvertPageState extends State<ConvertPage>
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: const Icon(Icons.change_circle),
child: Icon(Icons.change_circle, semanticLabel: s.upsideDown),
onPressed: () {
final temp = _textEditingController.text;
_textEditingController.text = _textEditingControllerResult.text;
@@ -116,7 +121,7 @@ class _ConvertPageState extends State<ConvertPage>
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: const Icon(Icons.copy),
child: Icon(Icons.copy, semanticLabel: s.copy),
onPressed: () => FlutterClipboard.copy(
_textEditingControllerResult.text == ''
? ' '
@@ -137,11 +142,11 @@ class _ConvertPageState extends State<ConvertPage>
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: primaryColor)),
const Text(
'Current Mode',
Text(
s.currentMode,
textScaleFactor: 1.0,
textAlign: TextAlign.right,
style: TextStyle(fontSize: 9.0, color: Colors.grey),
style: const TextStyle(fontSize: 9.0, color: Colors.grey),
)
],
),

View File

@@ -11,10 +11,12 @@ import 'package:toolbox/data/model/app/navigation_item.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/icon/common.dart';
import 'package:toolbox/data/res/tab.dart';
import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/convert.dart';
import 'package:toolbox/view/page/debug.dart';
@@ -44,6 +46,7 @@ class _MyHomePageState extends State<MyHomePage>
late final AdvancedDrawerController _advancedDrawerController;
late int _selectIndex;
late double _width;
late S s;
@override
void initState() {
@@ -58,6 +61,7 @@ class _MyHomePageState extends State<MyHomePage>
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
_width = MediaQuery.of(context).size.width;
}
@@ -115,11 +119,11 @@ class _MyHomePageState extends State<MyHomePage>
drawer: _buildDrawer(),
child: Scaffold(
appBar: AppBar(
title: Text(tabItems[_selectIndex].title),
title: Text(tabTitleName(context, _selectIndex), style: size18),
actions: [
IconButton(
icon: const Icon(Icons.developer_mode, size: 23),
tooltip: 'Debug',
tooltip: s.debug,
onPressed: () =>
AppRoute(const DebugPage(), 'Debug Page').go(context),
),
@@ -171,7 +175,7 @@ class _MyHomePageState extends State<MyHomePage>
borderRadius: const BorderRadius.all(Radius.circular(50))),
child: IconButton(
icon: Icon(item.icon),
tooltip: item.title,
tooltip: tabTitleName(context, idx),
splashRadius: width / 3.3,
padding: const EdgeInsets.only(left: 17, right: 17),
onPressed: () {
@@ -218,36 +222,35 @@ class _MyHomePageState extends State<MyHomePage>
children: [
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Setting'),
title: Text(s.setting),
onTap: () =>
AppRoute(const SettingPage(), 'Setting').go(context),
),
ListTile(
leading: const Icon(Icons.vpn_key),
title: const Text('Private Key'),
title: Text(s.privateKey),
onTap: () => AppRoute(
const StoredPrivateKeysPage(), 'private key list')
.go(context),
),
ListTile(
leading: const Icon(Icons.snippet_folder),
title: const Text('Snippet'),
title: Text(s.snippet),
onTap: () => AppRoute(const SnippetListPage(), 'snippet list')
.go(context),
),
AboutListTile(
icon: const Icon(Icons.text_snippet),
child: const Text('Licences'),
child: Text(s.license),
applicationName: BuildData.name,
applicationVersion: _buildVersionStr(),
applicationIcon: _buildIcon(),
aboutBoxChildren: const [
aboutBoxChildren: [
UrlText(
text: '\nMade with ❤️ by $myGithub',
text: s.madeWithLove(myGithub),
replace: 'LollipopKit'),
UrlText(
text:
'\nThanks $rainSunMeGithub for participating in the test.\n\nAll rights reserved.',
text: s.aboutThanks(rainSunMeGithub),
replace: 'RainSunMe',
),
],

View File

@@ -4,6 +4,7 @@ import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/ping_result.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/input_field.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -20,6 +21,10 @@ class _PingPageState extends State<PingPage>
late TextEditingController _textEditingController;
late MediaQueryData _media;
final List<PingResult> _results = [];
late S s;
static const summaryTextStyle = TextStyle(
fontSize: 12,
);
@override
void initState() {
@@ -31,6 +36,7 @@ class _PingPageState extends State<PingPage>
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
s = S.of(context);
}
@override
@@ -62,37 +68,39 @@ class _PingPageState extends State<PingPage>
}
Widget _buildResultItem(PingResult result) {
final unknown = s.unknown;
final ms = s.ms;
return RoundRectCard(ListTile(
contentPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
title: Text(result.serverName,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: primaryColor)),
subtitle: Text(_buildPingSummary(result)),
subtitle: Text(_buildPingSummary(result, unknown, ms), style: summaryTextStyle,),
trailing: Text(
'Avg: ' +
(result.statistic?.avg?.toStringAsFixed(2) ?? 'unkown') +
' ms',
s.pingAvg +
(result.statistic?.avg?.toStringAsFixed(2) ?? s.unknown) +
' $ms',
style: TextStyle(fontSize: 14, color: primaryColor)),
));
}
String _buildPingSummary(PingResult result) {
final ip = result.ip ?? 'unkown';
String _buildPingSummary(PingResult result, String unknown, String ms) {
final ip = result.ip ?? unknown;
if (result.results == null || result.results!.isEmpty) {
return '$ip - no results';
return '$ip - ${s.noResult}';
}
final ttl = result.results?.first.ttl ?? 'unkown';
final loss = result.statistic?.loss ?? 'unkown';
final min = result.statistic?.min ?? 'unkown';
final max = result.statistic?.max ?? 'unkown';
return '$ip\nttl: $ttl, loss: $loss%\nmin: $min ms, max: $max ms';
final ttl = result.results?.first.ttl ?? unknown;
final loss = result.statistic?.loss ?? unknown;
final min = result.statistic?.min ?? unknown;
final max = result.statistic?.max ?? unknown;
return '$ip\n${s.ttl}: $ttl, ${s.loss}: $loss%\n${s.min}: $min $ms, ${s.max}: $max $ms';
}
Future<void> doPing() async {
_results.clear();
final target = _textEditingController.text.trim();
if (target.isEmpty) {
showSnackBar(context, const Text('Please input a target'));
showSnackBar(context, Text(s.pingInputIP));
return;
}
@@ -119,12 +127,12 @@ class _PingPageState extends State<PingPage>
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: Row(
children: const [
Icon(Icons.delete),
SizedBox(
children: [
const Icon(Icons.delete),
const SizedBox(
width: 7,
),
Text('Clear')
Text(s.clear)
],
),
onPressed: () {
@@ -136,12 +144,12 @@ class _PingPageState extends State<PingPage>
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: Row(
children: const [
Icon(Icons.play_arrow),
SizedBox(
children: [
const Icon(Icons.play_arrow),
const SizedBox(
width: 7,
),
Text('Start')
Text(s.start)
],
),
onPressed: () {

View File

@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/input_decoration.dart';
@@ -25,6 +27,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
late PrivateKeyProvider _provider;
late Widget loading;
late S s;
@override
void initState() {
@@ -33,12 +36,19 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
loading = const SizedBox();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Edit'), actions: [
appBar: AppBar(title: Text(s.edit, style: size18), actions: [
widget.info != null
? IconButton(
tooltip: s.delete,
onPressed: () {
_provider.delInfo(widget.info!);
Navigator.of(context).pop();
@@ -52,7 +62,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
TextField(
controller: nameController,
keyboardType: TextInputType.text,
decoration: buildDecoration('Name', icon: Icons.info),
decoration: buildDecoration(s.name, icon: Icons.info),
),
TextField(
controller: keyController,
@@ -61,14 +71,14 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
maxLines: 10,
keyboardType: TextInputType.text,
enableSuggestions: false,
decoration: buildDecoration('Private Key', icon: Icons.vpn_key),
decoration: buildDecoration(s.privateKey, icon: Icons.vpn_key),
),
TextField(
controller: pwdController,
autocorrect: false,
keyboardType: TextInputType.text,
obscureText: true,
decoration: buildDecoration('Password', icon: Icons.password),
decoration: buildDecoration(s.pwd, icon: Icons.password),
),
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
loading
@@ -76,13 +86,14 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.save),
tooltip: s.save,
onPressed: () async {
final name = nameController.text;
final key = keyController.text;
final pwd = pwdController.text;
if (name.isEmpty || key.isEmpty) {
showSnackBar(
context, const Text('Name and Key must not be empty.'));
context, Text(s.fieldMustNotEmpty));
return;
}
FocusScope.of(context).unfocus();

View File

@@ -3,7 +3,9 @@ import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/page/private_key/edit.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -16,11 +18,19 @@ class StoredPrivateKeysPage extends StatefulWidget {
class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
final _textStyle = TextStyle(color: primaryColor);
late S s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Private Keys'),
title: Text(s.privateKey, style: size18),
),
body: Consumer<PrivateKeyProvider>(
builder: (_, key, __) {
@@ -46,14 +56,14 @@ class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
'private key edit page')
.go(context),
child: Text(
'Edit',
s.edit,
style: _textStyle,
))
],
),
));
})
: const Center(child: Text('No saved private keys.'));
: Center(child: Text(s.noSavedPrivateKey));
},
),
floatingActionButton: FloatingActionButton(

View File

@@ -6,6 +6,7 @@ import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/icon/linux_icons.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -43,7 +44,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildMainPage(ServerInfo si) {
return Scaffold(
appBar: AppBar(
title: Text(si.info.name),
title: Text(si.info.name, style: size18),
),
body: ListView(
padding: const EdgeInsets.all(13),

View File

@@ -8,7 +8,9 @@ import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/private_key/edit.dart';
import 'package:toolbox/view/widget/input_decoration.dart';
@@ -32,6 +34,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
late ServerProvider _serverProvider;
late S s;
bool usePublicKey = false;
int _pubKeyIndex = -1;
@@ -43,10 +47,16 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_serverProvider = locator<ServerProvider>();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Edit'), actions: [
appBar: AppBar(title: Text(s.edit, style: size18), actions: [
widget.spi != null
? IconButton(
onPressed: () {
@@ -54,7 +64,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
context,
'Attention',
Text(
'Are you sure to delete server [${widget.spi!.name}]'),
s.sureToDeleteServer(widget.spi!.name)),
[
TextButton(
onPressed: () {
@@ -62,13 +72,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: const Text(
'Yes',
style: TextStyle(color: Colors.red),
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
)),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('No'))
child: Text(s.cancel))
]);
},
icon: const Icon(Icons.delete))
@@ -84,20 +94,20 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
controller: nameController,
keyboardType: TextInputType.text,
decoration:
buildDecoration('Name', icon: Icons.info, hint: 'Example'),
buildDecoration(s.name, icon: Icons.info, hint: s.exampleName),
),
TextField(
controller: ipController,
keyboardType: TextInputType.text,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration('Host',
decoration: buildDecoration(s.host,
icon: Icons.storage, hint: 'example.com'),
),
TextField(
controller: portController,
keyboardType: TextInputType.number,
decoration: buildDecoration('Port',
decoration: buildDecoration(s.port,
icon: Icons.format_list_numbered, hint: '22'),
),
TextField(
@@ -105,13 +115,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
keyboardType: TextInputType.text,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration('User',
decoration: buildDecoration(s.user,
icon: Icons.account_box, hint: 'root'),
),
const SizedBox(height: 7),
Row(
children: [
const Text('Key Auth'),
Text(s.keyAuth),
Switch(
value: usePublicKey,
onChanged: (val) => setState(() => usePublicKey = val)),
@@ -122,8 +132,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
controller: passwordController,
obscureText: true,
keyboardType: TextInputType.text,
decoration: buildDecoration('Pwd',
icon: Icons.password, hint: 'Password'),
decoration: buildDecoration(s.pwd,
icon: Icons.password, hint: s.pwd),
onSubmitted: (_) => {},
)
: const SizedBox(),
@@ -143,7 +153,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
)
.toList();
tiles.add(ListTile(
title: const Text('Add a Private Key'),
title: Text(s.addPrivateKey),
contentPadding: EdgeInsets.zero,
trailing: IconButton(
icon: const Icon(Icons.add),
@@ -157,9 +167,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
iconColor: primaryColor,
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: const Text(
'Choose Key',
style: TextStyle(fontSize: 14),
title: Text(
s.choosePrivateKey,
style: const TextStyle(fontSize: 14),
),
children: tiles,
);
@@ -172,15 +182,15 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
child: const Icon(Icons.send),
onPressed: () {
if (ipController.text == '') {
showSnackBar(context, const Text('Please enter host.'));
showSnackBar(context, Text(s.plzEnterHost));
return;
}
if (!usePublicKey && passwordController.text == '') {
showSnackBar(context, const Text('Please enter password.'));
showSnackBar(context, Text(s.plzEnterPwd));
return;
}
if (usePublicKey && _pubKeyIndex == -1) {
showSnackBar(context, const Text('Please select a private key.'));
showSnackBar(context, Text(s.plzSelectKey));
return;
}
if (usernameController.text == '') {

View File

@@ -15,6 +15,7 @@ import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/apt.dart';
import 'package:toolbox/view/page/docker.dart';
@@ -39,6 +40,7 @@ class _ServerPageState extends State<ServerPage>
late RefreshController _refreshController;
late ServerProvider _serverProvider;
late S s;
@override
void initState() {
@@ -53,6 +55,7 @@ class _ServerPageState extends State<ServerPage>
_media = MediaQuery.of(context);
_theme = Theme.of(context);
_primaryColor = primaryColor;
s = S.of(context);
}
@override
@@ -62,9 +65,9 @@ class _ServerPageState extends State<ServerPage>
locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
final child = Consumer<ServerProvider>(builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return const Center(
return Center(
child: Text(
'There is no server.\nClick the fab to add one.',
s.serverTabEmpty,
textAlign: TextAlign.center,
),
);
@@ -99,7 +102,7 @@ class _ServerPageState extends State<ServerPage>
onPressed: () =>
AppRoute(const ServerEditPage(), 'Add server info page')
.go(context),
tooltip: 'add a server',
tooltip: s.addAServer,
heroTag: 'server page fab',
child: const Icon(Icons.add),
),
@@ -302,7 +305,7 @@ class _ServerPageState extends State<ServerPage>
case ServerConnectionState.connected:
if (temp == '') {
if (upTime == '') {
return 'Loading...';
return s.serverTabLoading;
} else {
return upTime;
}
@@ -314,17 +317,17 @@ class _ServerPageState extends State<ServerPage>
}
}
case ServerConnectionState.connecting:
return 'Connecting...';
return s.serverTabConnecting;
case ServerConnectionState.failed:
if (failedInfo == null) {
return 'Failed';
return s.serverTabFailed;
}
if (failedInfo.contains('encypted')) {
return 'Please "save" this private key again.';
return s.serverTabPlzSave;
}
return failedInfo;
default:
return 'Unknown State';
return s.serverTabUnkown;
}
}

View File

@@ -2,13 +2,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/update.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/data/res/tab.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -29,6 +32,7 @@ class _SettingPageState extends State<SettingPage> {
late final ServerProvider _serverProvider;
late MediaQueryData _media;
late ThemeData _theme;
late S s;
@override
void didChangeDependencies() {
@@ -36,6 +40,7 @@ class _SettingPageState extends State<SettingPage> {
priColor = primaryColor;
_media = MediaQuery.of(context);
_theme = Theme.of(context);
s = S.of(context);
}
@override
@@ -51,7 +56,7 @@ class _SettingPageState extends State<SettingPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Setting'),
title: Text(s.setting, style: size18),
),
body: ListView(
padding: const EdgeInsets.all(17),
@@ -70,12 +75,12 @@ class _SettingPageState extends State<SettingPage> {
String display;
if (app.newestBuild != null) {
if (app.newestBuild! > BuildData.build) {
display = 'Found: v1.0.${app.newestBuild}, click to update';
display = s.versionHaveUpdate(app.newestBuild!);
} else {
display = 'Current: v1.0.${BuildData.build}is up to date';
display = s.versionUpdated(BuildData.build);
}
} else {
display = 'Current: v1.0.${BuildData.build}';
display = s.versionUnknownUpdate(BuildData.build);
}
return ListTile(
contentPadding: roundRectCardPadding,
@@ -94,16 +99,16 @@ class _SettingPageState extends State<SettingPage> {
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
textColor: priColor,
title: const Text(
'Server status update interval',
title: Text(
s.updateServerStatusInterval,
style: textStyle,
textAlign: TextAlign.start,
),
subtitle: const Text(
'Will take effect immediately.',
style: TextStyle(color: Colors.grey),
subtitle: Text(
s.willTakEeffectImmediately,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
trailing: Text('${_intervalValue.toInt()} s'),
trailing: Text('${_intervalValue.toInt()} ${s.second}'),
children: [
Slider(
thumbColor: priColor,
@@ -120,16 +125,16 @@ class _SettingPageState extends State<SettingPage> {
_store.serverStatusUpdateInterval.put(val.toInt());
_serverProvider.startAutoRefresh();
},
label: '${_intervalValue.toInt()} seconds',
label: '${_intervalValue.toInt()} ${s.second}',
divisions: 10,
),
const SizedBox(
height: 3,
),
_intervalValue == 0.0
? const Text(
'You set to 0, will not update automatically.\nYou can pull to refresh manually.',
style: TextStyle(color: Colors.grey),
? Text(
s.updateIntervalEqual0,
style: const TextStyle(color: Colors.grey, fontSize: 12),
textAlign: TextAlign.center,
)
: const SizedBox(),
@@ -156,8 +161,8 @@ class _SettingPageState extends State<SettingPage> {
width: 27,
),
),
title: const Text(
'App primary color',
title: Text(
s.appPrimaryColor,
style: textStyle,
));
}
@@ -186,14 +191,14 @@ class _SettingPageState extends State<SettingPage> {
textColor: priColor,
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
title: const Text(
'Launch page',
title: Text(
s.launchPage,
style: textStyle,
),
trailing: ConstrainedBox(
constraints: BoxConstraints(maxWidth: _media.size.width * 0.35),
child: Text(
tabs[_launchPageIdx],
tabTitleName(context, _launchPageIdx),
style: textStyle,
textAlign: TextAlign.right,
),
@@ -202,7 +207,7 @@ class _SettingPageState extends State<SettingPage> {
.map((e) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
e,
tabTitleName(context, tabs.indexOf(e)),
style: TextStyle(
fontSize: 14,
color: _theme.textTheme.bodyText2!.color!.withAlpha(177)),

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/input_decoration.dart';
@@ -21,23 +23,31 @@ class _SnippetEditPageState extends State<SnippetEditPage>
final scriptController = TextEditingController();
late SnippetProvider _provider;
late S s;
@override
void initState() {
super.initState();
_provider = locator<SnippetProvider>();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Edit'), actions: [
appBar: AppBar(title: Text(s.edit, style: size18), actions: [
widget.snippet != null
? IconButton(
onPressed: () {
_provider.del(widget.snippet!);
Navigator.of(context).pop();
},
tooltip: s.delete,
icon: const Icon(Icons.delete))
: const SizedBox()
]),
@@ -47,7 +57,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
TextField(
controller: nameController,
keyboardType: TextInputType.text,
decoration: buildDecoration('Name', icon: Icons.info),
decoration: buildDecoration(s.name, icon: Icons.info),
),
TextField(
controller: scriptController,
@@ -56,7 +66,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
maxLines: 10,
keyboardType: TextInputType.text,
enableSuggestions: false,
decoration: buildDecoration('Snippet', icon: Icons.code),
decoration: buildDecoration(s.snippet, icon: Icons.code),
),
],
),
@@ -66,7 +76,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
final name = nameController.text;
final script = scriptController.text;
if (name.isEmpty || script.isEmpty) {
showSnackBar(context, const Text('Two fields must not be empty.'));
showSnackBar(context, Text(s.fieldMustNotEmpty));
return;
}
final snippet = Snippet(name, script);

View File

@@ -8,7 +8,9 @@ import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/snippet/edit.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -28,14 +30,24 @@ class _SnippetListPageState extends State<SnippetListPage> {
final _exportFieldController = TextEditingController();
final _textStyle = TextStyle(color: primaryColor);
late S s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Snippet List'),
title: Text(s.snippet, style: size18),
actions: [
IconButton(
onPressed: () => _showImportExport(),
tooltip: s.importAndExport,
icon: const Icon(Icons.import_export)),
],
),
@@ -51,17 +63,17 @@ class _SnippetListPageState extends State<SnippetListPage> {
Future<void> _showImportExport() async {
await showRoundDialog(
context,
'Choose',
s.choose,
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('Import'),
title: Text(s.import),
leading: const Icon(Icons.download),
onTap: () => _showImportDialog(),
),
ListTile(
title: const Text('Export'),
title: Text(s.export),
leading: const Icon(Icons.file_upload),
onTap: () => _showExportDialog(),
),
@@ -75,7 +87,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
_exportFieldController.text = locator<SnippetProvider>().export;
await showRoundDialog(
context,
'Export',
s.export,
TextField(
decoration: const InputDecoration(
labelText: 'JSON',
@@ -85,7 +97,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
),
[
TextButton(
child: const Text('OK'),
child: Text(s.ok),
onPressed: () => Navigator.pop(context),
),
]);
@@ -95,10 +107,10 @@ class _SnippetListPageState extends State<SnippetListPage> {
Navigator.of(context).pop();
await showRoundDialog(
context,
'Import',
s.import,
TextField(
decoration: const InputDecoration(
labelText: 'Url or JSON',
decoration: InputDecoration(
labelText: s.urlOrJson,
),
maxLines: 2,
controller: _importFieldController,
@@ -106,18 +118,18 @@ class _SnippetListPageState extends State<SnippetListPage> {
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
child: Text(s.cancel)),
TextButton(
onPressed: () async =>
await _import(_importFieldController.text.trim()),
child: const Text('GO'),
child: Text('GO'),
)
]);
}
Future<void> _import(String text) async {
if (text.isEmpty) {
showSnackBar(context, const Text('field can not be empty'));
showSnackBar(context, Text(s.fieldMustNotEmpty));
return;
}
final snippetProvider = locator<SnippetProvider>();
@@ -125,7 +137,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
final resp = await Dio().get(text);
if (resp.statusCode != 200) {
showSnackBar(
context, Text('request failed, status code: ${resp.statusCode}'));
context, Text(s.httpFailedWithCode(resp.statusCode ?? '-1')));
return;
}
for (final snippet in getSnippetList(resp.data)) {
@@ -166,7 +178,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
'snippet edit page')
.go(context),
child: Text(
'Edit',
s.edit,
style: _textStyle,
)),
TextButton(
@@ -179,7 +191,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
run(context, snippet);
},
child: Text(
'Run',
s.run,
style: _textStyle,
))
])
@@ -187,16 +199,16 @@ class _SnippetListPageState extends State<SnippetListPage> {
),
));
})
: const Center(child: Text('No saved snippets.'));
: Center(child: Text(s.noSavedSnippet));
},
);
}
void _showRunDialog(Snippet snippet) {
showRoundDialog(context, 'Choose destination',
showRoundDialog(context, s.chooseDestination,
Consumer<ServerProvider>(builder: (_, provider, __) {
if (provider.servers.isEmpty) {
return const Text('No server available');
return Text(s.noServerAvailable);
}
_selectedIndex = provider.servers.first.info;
return SizedBox(
@@ -235,10 +247,10 @@ class _SnippetListPageState extends State<SnippetListPage> {
}), [
TextButton(
onPressed: () async => run(context, snippet),
child: const Text('Run')),
child: Text(s.run)),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
child: Text(s.cancel)),
]);
}
@@ -246,11 +258,11 @@ class _SnippetListPageState extends State<SnippetListPage> {
final result = await locator<ServerProvider>()
.runSnippet(widget.spi ?? _selectedIndex, snippet);
if (result != null) {
showRoundDialog(context, 'Result',
showRoundDialog(context, s.result,
Text(result, style: const TextStyle(fontSize: 13)), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'))
child: Text(s.close))
]);
}
}