Compare commits

...

55 Commits

Author SHA1 Message Date
lollipopkit
9e3afe98a9 fix: ThemeMode not works under copied data 2023-03-21 15:36:35 +08:00
lollipopkit
7732ce5dbb make.dart: auto kill java 2023-03-21 15:03:03 +08:00
lollipopkit
f1ae924724 fix: countly double init due to rebuild app 2023-03-21 15:02:43 +08:00
lollipopkit
4cc72328a7 manually restart app 2023-03-21 14:46:30 +08:00
lollipopkit
43e32775a3 apply font to ssh term 2023-03-21 14:12:44 +08:00
lollipopkit
27e7653587 #15 support: select font 2023-03-21 13:15:36 +08:00
lollipopkit
89050daf4e opt: only display token on iOS 2023-03-18 17:52:25 +08:00
lollipopkit
2cc52fcee6 #11 Update README.md 2023-03-18 00:16:01 +08:00
lollipopkit
3f3240040a opt. 2023-03-18 00:09:24 +08:00
lollipopkit
350d248776 Add issue translate bot 2023-03-16 12:35:27 +08:00
lollipopkit
bb15f34615 #17 ios production env no token 2023-03-16 11:47:06 +08:00
lollipopkit
58b950f814 #14 add ssh log 2023-03-15 13:37:19 +08:00
lollipopkit
c2e822f49d new: PlatformType 2023-03-13 12:02:48 +08:00
lollipopkit
4c4153ef98 new: support msg push 2023-03-12 16:06:51 +08:00
lollipopkit
35b5d1ccb5 Add l10n files 2023-03-11 19:02:40 +08:00
lollipopkit
065fd15429 decrease res files 2023-03-11 19:02:22 +08:00
lollipopkit
5666a23e00 #12 new: custom theme mode 2023-02-25 19:58:37 +08:00
lollipopkit
69fae4dd21 fix: android sftp downloaded files share failed 2023-02-21 17:32:22 +08:00
lollipopkit
2986f80f89 opt: auto rm pwd in key page for safe 2023-02-21 17:12:01 +08:00
lollipopkit
e423e56152 try solve file_picker 2023-02-18 13:18:35 +08:00
lollipopkit
9c00dc8a54 try to solve file_picker on ios 2023-02-17 18:57:16 +08:00
lollipopkit
558721fa79 opt.
opt: check `private key` size
opt: expand key list in default
2023-02-17 16:49:41 +08:00
lollipopkit
0c198c23fc opt: check private key size 2023-02-17 16:29:46 +08:00
lollipopkit
99aa0fc1f5 new: pull to refresh on server tab 2023-02-17 15:55:34 +08:00
lollipopkit
1aac166c43 new: pick from file to add key #9 2023-02-16 12:55:23 +08:00
lollipopkit
28a6067033 new: swap view for #10 2023-02-16 12:19:38 +08:00
lollipopkit
9c3b822311 new & opt
new: Flutter 3.7.3
opt: for `ping` page
2023-02-13 17:51:36 +08:00
lollipopkit
ba44649ce1 fix: #8 2023-02-13 14:49:02 +08:00
lollipopkit
e7b1773e5c opt: settings page 2023-02-06 15:32:36 +08:00
lollipopkit
3feef3936c new & opt
new: support set maxRetryCount of server reconnection
opt: server detail UI
opt: server provider
opt: `ssh` page on Android
2023-02-03 13:12:39 +08:00
lollipopkit
7837fa4339 fix: ssh use SafeArea 2023-02-02 18:47:07 +08:00
lollipopkit
82a201d3dc new: support pick ssh term theme 2023-02-02 16:52:30 +08:00
lollipopkit
c479d18714 new & opt
new: `net` total in & out bytes
opt: i18n for `ssh`
opt: disk path ignore
2023-02-02 13:11:21 +08:00
lollipopkit
469b9fe8cd fix: netspeed bytes too large 2023-02-02 12:04:01 +08:00
lollipopkit
c47e24ac5b ssh: long press menu bar 2023-02-01 23:36:21 +08:00
lollipopkit
1063916474 ssh: support copy/paste, fix ios backspace 2023-02-01 21:34:16 +08:00
lollipopkit
a63e240ce0 fix & opt
fix: whether display docker edit host
opt: docker funcs
2023-02-01 18:24:56 +08:00
lollipopkit
1a8d572fbd fix: btn theme color 2023-02-01 17:48:20 +08:00
lollipopkit
21ac323ed1 fix & opt
fix: cant ping when launch page is ping
fix: button text color not primaryColor
opt: getting primaryColor
2023-02-01 17:18:46 +08:00
lollipopkit
2faea10d61 server connect: max try 7 times 2023-02-01 13:34:45 +08:00
lollipopkit
04cf5b65ce opt. proj struct 2023-02-01 13:00:02 +08:00
lollipopkit
4d741ac82a fix & opt
fix: android in-app upgrade
fmt: proj struct
opt: fetch primaryColor
2023-02-01 12:52:40 +08:00
lollipopkit
068089d207 fix cpu temp padding 2023-02-01 10:37:06 +08:00
lollipopkit
5ebb4e6b3e ssh: more tip 2023-01-29 22:23:18 +08:00
lollipopkit
19e0b283ae ssh page opt: performance & auto exit 2023-01-29 22:03:25 +08:00
lollipopkit
7e8600ab6d fix typo 2023-01-29 17:51:23 +08:00
LollipopKit
7c0e01d0d5 fix & opt.
fix: net iface parse
opt: `ssh` page auto unpress `ctrl or alt` after call
opt: enable translation for menu
opt: add confirm to `docker` page
2023-01-29 17:39:27 +08:00
LollipopKit
f3c670d82c opt. proj struct 2023-01-29 16:56:40 +08:00
lollipopkit
49f9b0b179 ssh page: rm appbar 2023-01-29 15:11:01 +08:00
lollipopkit
47861b1e0b rm: ssh term size 2023-01-29 13:56:39 +08:00
lollipopkit
923667d57c opt. for ssh 2023-01-29 00:09:20 +08:00
lollipopkit
e6458a1d7f opt. for ssh page 2023-01-28 23:39:03 +08:00
lollipopkit
f109aca484 ssh: more virtual keys 2023-01-28 23:10:59 +08:00
lollipopkit
a518dca0ca opt. 2023-01-28 21:16:53 +08:00
lollipopkit
be1a162632 fix & opt. 2023-01-28 15:35:19 +08:00
106 changed files with 6183 additions and 4345 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,572 @@
import 'l10n.dart';
/// The translations for English (`en`).
class SEn extends S {
SEn([String locale = 'en']) : super(locale);
@override
String get about => 'About';
@override
String get aboutThanks => '\nThanks to the following people who participated in the test.';
@override
String get addAServer => 'add a server';
@override
String get addOne => 'Add one';
@override
String get addPrivateKey => 'Add private key';
@override
String get alreadyLastDir => 'Already in last directory.';
@override
String get appPrimaryColor => 'App primary color';
@override
String get attention => 'Attention';
@override
String get auto => 'Auto';
@override
String get backDir => 'Back';
@override
String get backup => 'Backup';
@override
String get backupTip => 'The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).';
@override
String get backupVersionNotMatch => 'Backup version is not match.';
@override
String get cancel => 'Cancel';
@override
String get choose => 'Choose';
@override
String get chooseDestination => 'Choose destination';
@override
String get chooseFontFile => 'Choose a font file';
@override
String get choosePrivateKey => 'Choose private key';
@override
String get clear => 'Clear';
@override
String get clickSee => 'Click here';
@override
String get close => 'Close';
@override
String get cmd => 'Command';
@override
String get containerStatus => 'Container status';
@override
String get convert => 'Convert';
@override
String get copy => 'Copy';
@override
String get copyPath => 'Copy path';
@override
String get createFile => 'Create file';
@override
String get createFolder => 'Create folder';
@override
String get currentMode => 'Current Mode';
@override
String get dark => 'Dark';
@override
String get debug => 'Debug';
@override
String get decode => 'Decode';
@override
String get delete => 'Delete';
@override
String get disconnected => 'Disconnected';
@override
String dl2Local(Object fileName) {
return 'Download [$fileName] to local?';
}
@override
String get dockerContainerName => 'Container name';
@override
String get dockerEditHost => 'Edit DOCKER_HOST';
@override
String get dockerEmptyRunningItems => 'No running container. \nIt may be that the env DOCKER_HOST is not read correctly. You can found it by running `echo \$DOCKER_HOST` in terminal.';
@override
String get dockerImage => 'Image';
@override
String dockerImagesFmt(Object count) {
return '$count images';
}
@override
String get dockerNotInstalled => 'Docker not installed';
@override
String dockerStatusRunningAndStoppedFmt(Object runningCount, Object stoppedCount) {
return '$runningCount running, $stoppedCount container stopped.';
}
@override
String dockerStatusRunningFmt(Object count) {
return '$count container running.';
}
@override
String get download => 'Download';
@override
String get downloadFinished => 'Download finished';
@override
String downloadStatus(Object percent, Object size) {
return '$percent% of $size';
}
@override
String get edit => 'Edit';
@override
String get encode => 'Encode';
@override
String get error => 'Error';
@override
String get exampleName => 'Example name';
@override
String get experimentalFeature => 'Experimental feature';
@override
String get export => 'Export';
@override
String get extraArgs => 'Extra args';
@override
String get failed => 'Failed';
@override
String get feedback => 'Feedback';
@override
String get feedbackOnGithub => 'If you have any questions, please feedback on Github.';
@override
String get fieldMustNotEmpty => 'These fields must not be empty.';
@override
String fileNotExist(Object file) {
return '$file not exist';
}
@override
String fileTooLarge(Object file, Object size, Object sizeMax) {
return 'File \'$file\' too large $size, max $sizeMax';
}
@override
String get files => 'Files';
@override
String foundNUpdate(Object count) {
return 'Found $count update';
}
@override
String get getPushTokenFailed => 'Can\'t fetch push token';
@override
String get gettingToken => 'Getting token...';
@override
String get go => 'Go';
@override
String get goto => 'Go to';
@override
String get host => 'Host';
@override
String httpFailedWithCode(Object code) {
return 'request failed, status code: $code';
}
@override
String get imagesList => 'Images list';
@override
String get import => 'Import';
@override
String get importAndExport => 'Import and Export';
@override
String get inputDomainHere => 'Input Domain here';
@override
String get install => 'install';
@override
String get installDockerWithUrl => 'Please https://docs.docker.com/engine/install docker first.';
@override
String get invalidJson => 'Invalid JSON';
@override
String get invalidVersion => 'Invalid version';
@override
String invalidVersionHelp(Object url) {
return 'Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don\'t have the above issues, please submit an issue on $url.';
}
@override
String get isBusy => 'Is busy now';
@override
String get keepForeground => 'Keep app foreground!';
@override
String get keyAuth => 'Key Auth';
@override
String get lastTry => 'Last try!';
@override
String get launchPage => 'Launch page';
@override
String get license => 'License';
@override
String get light => 'Light';
@override
String get loadingFiles => 'Loading files...';
@override
String get loss => 'loss';
@override
String madeWithLove(Object myGithub) {
return '\nMade with ❤️ by $myGithub';
}
@override
String get max => 'max';
@override
String get maxRetryCount => 'Number of server reconnection';
@override
String get maxRetryCountEqual0 => 'Will retry again and again.';
@override
String get min => 'min';
@override
String get ms => 'ms';
@override
String get name => 'Name';
@override
String get needRestart => 'Need to restart app';
@override
String get newContainer => 'New container';
@override
String get noClient => 'No client';
@override
String get noInterface => 'No interface';
@override
String get noResult => 'No result';
@override
String get noSavedPrivateKey => 'No saved private keys.';
@override
String get noSavedSnippet => 'No saved snippets.';
@override
String get noServerAvailable => 'No server available.';
@override
String get noUpdateAvailable => 'No update available';
@override
String get notSelected => 'Not selected';
@override
String get nullToken => 'Null token';
@override
String get ok => 'OK';
@override
String get onServerDetailPage => 'On server detail page';
@override
String get onlyIOS => 'Only valid on iOS';
@override
String get open => 'Open';
@override
String get path => 'Path';
@override
String get pickFile => 'Pick file';
@override
String get ping => 'Ping';
@override
String get pingAvg => 'Avg:';
@override
String get pingInputIP => 'Please input a target IP/domain.';
@override
String get pingNoServer => 'No server to ping.\nPlease add a server in server tab.';
@override
String get pkg => 'Pkg';
@override
String get platformNotSupportUpdate => 'Current platform does not support in app update.\nPlease build from source and install it.';
@override
String get plzEnterHost => 'Please enter host.';
@override
String get plzSelectKey => 'Please select a key.';
@override
String get port => 'Port';
@override
String get preview => 'Preview';
@override
String get privateKey => 'Private Key';
@override
String get pushToken => 'Push token';
@override
String get pwd => 'Password';
@override
String get rename => 'Rename';
@override
String reportBugsOnGithubIssue(Object url) {
return 'Please report bugs on $url';
}
@override
String get restart => 'Restart';
@override
String get restore => 'Restore';
@override
String get restoreSuccess => 'Restore success. Restart app to apply.';
@override
String restoreSureWithDate(Object date) {
return 'Are you sure to restore from $date ?';
}
@override
String get result => 'Result';
@override
String get run => 'Run';
@override
String get save => 'Save';
@override
String get second => 's';
@override
String get server => 'Server';
@override
String get serverTabConnecting => 'Connecting...';
@override
String get serverTabEmpty => 'There is no server.\nClick the fab to add one.';
@override
String get serverTabFailed => 'Failed';
@override
String get serverTabLoading => 'Loading...';
@override
String get serverTabPlzSave => 'Please \'save\' this private key again.';
@override
String get serverTabUnkown => 'Unknown state';
@override
String get setting => 'Setting';
@override
String get sftpDlPrepare => 'Preparing to connect...';
@override
String get sftpNoDownloadTask => 'No download task.';
@override
String get sftpSSHConnected => 'SFTP Connected';
@override
String get showDistLogo => 'Show distribution logo';
@override
String get snippet => 'Snippet';
@override
String spentTime(Object time) {
return 'Spent time: $time';
}
@override
String sshTip(Object url) {
return 'This function is now in the experimental stage.\n\nPlease report bugs on $url or join our development.';
}
@override
String get start => 'Start';
@override
String get stop => 'Stop';
@override
String get success => 'Success';
@override
String sureDelete(Object name) {
return 'Are you sure to delete [$name]?';
}
@override
String get sureNoPwd => 'Are you sure to use no password?';
@override
String sureToDeleteServer(Object server) {
return 'Are you sure to delete server [$server]?';
}
@override
String get termTheme => 'Terminal theme';
@override
String get themeMode => 'Theme mode';
@override
String get times => 'Times';
@override
String get ttl => 'ttl';
@override
String get unknown => 'unknown';
@override
String get unknownError => 'Unknown error';
@override
String get unkownConvertMode => 'Unknown convert mode';
@override
String get update => 'Update';
@override
String get updateAll => 'Update all';
@override
String get updateIntervalEqual0 => 'You set to 0, will not update automatically.\nCan\'t calculate CPU status.';
@override
String get updateServerStatusInterval => 'Server status update interval';
@override
String updateTip(Object newest) {
return 'Update: v1.0.$newest';
}
@override
String updateTipTooLow(Object newest) {
return 'Current version is too low, please update to v1.0.$newest';
}
@override
String get upsideDown => 'Upside Down';
@override
String get urlOrJson => 'URL or JSON';
@override
String get user => 'User';
@override
String versionHaveUpdate(Object build) {
return 'Found: v1.0.$build, click to update';
}
@override
String versionUnknownUpdate(Object build) {
return 'Current: v1.0.$build';
}
@override
String versionUpdated(Object build) {
return 'Current: v1.0.$build, is up to date';
}
@override
String get waitConnection => 'Please wait for the connection to be established.';
@override
String get willTakEeffectImmediately => 'Will take effect immediately';
}

View File

@@ -0,0 +1,572 @@
import 'l10n.dart';
/// The translations for Chinese (`zh`).
class SZh extends S {
SZh([String locale = 'zh']) : super(locale);
@override
String get about => '关于';
@override
String get aboutThanks => '\n感谢以下参与软件测试的各位。';
@override
String get addAServer => '添加服务器';
@override
String get addOne => '前去新增';
@override
String get addPrivateKey => '添加一个私钥';
@override
String get alreadyLastDir => '已经是最上层目录了';
@override
String get appPrimaryColor => 'App主要色';
@override
String get attention => '注意';
@override
String get auto => '自动';
@override
String get backDir => '返回上一级';
@override
String get backup => '备份';
@override
String get backupTip => '导出的数据仅进行了简单加密,请妥善保管。\n除了设置项,恢复的数据不会覆盖现有数据。';
@override
String get backupVersionNotMatch => '备份版本不匹配,无法恢复';
@override
String get cancel => '取消';
@override
String get choose => '选择';
@override
String get chooseDestination => '选择目标';
@override
String get chooseFontFile => '选择字体文件';
@override
String get choosePrivateKey => '选择私钥';
@override
String get clear => '清除';
@override
String get clickSee => '点击查看';
@override
String get close => '关闭';
@override
String get cmd => '命令';
@override
String get containerStatus => '容器状态';
@override
String get convert => '转换';
@override
String get copy => '复制';
@override
String get copyPath => '复制路径';
@override
String get createFile => '创建文件';
@override
String get createFolder => '创建文件夹';
@override
String get currentMode => '当前模式';
@override
String get dark => '';
@override
String get debug => '调试';
@override
String get decode => '解码';
@override
String get delete => '删除';
@override
String get disconnected => '连接断开';
@override
String dl2Local(Object fileName) {
return '下载 [$fileName] 到本地?';
}
@override
String get dockerContainerName => '容器名';
@override
String get dockerEditHost => '编辑 DOCKER_HOST';
@override
String get dockerEmptyRunningItems => '没有正在运行的容器。\n这可能是因为环境变量 DOCKER_HOST 没有被正确读取。你可以通过在终端内运行 `echo \$DOCKER_HOST` 来获取。';
@override
String get dockerImage => '镜像';
@override
String dockerImagesFmt(Object count) {
return '$count 个镜像';
}
@override
String get dockerNotInstalled => 'Docker未安装';
@override
String dockerStatusRunningAndStoppedFmt(Object runningCount, Object stoppedCount) {
return '$runningCount个正在运行, $stoppedCount个已停止';
}
@override
String dockerStatusRunningFmt(Object count) {
return '$count个容器正在运行';
}
@override
String get download => '下载';
@override
String get downloadFinished => '下载完成!';
@override
String downloadStatus(Object percent, Object size) {
return '$size$percent%';
}
@override
String get edit => '编辑';
@override
String get encode => '编码';
@override
String get error => '出错了';
@override
String get exampleName => '名称示例';
@override
String get experimentalFeature => '实验性功能';
@override
String get export => '导出';
@override
String get extraArgs => '额外参数';
@override
String get failed => '失败';
@override
String get feedback => '反馈';
@override
String get feedbackOnGithub => '如果你有任何问题请在GitHub反馈';
@override
String get fieldMustNotEmpty => '这些输入框不能为空。';
@override
String fileNotExist(Object file) {
return '$file 不存在';
}
@override
String fileTooLarge(Object file, Object size, Object sizeMax) {
return '文件 \'$file\' 过大 \'$size\',超过了 $sizeMax';
}
@override
String get files => '文件';
@override
String foundNUpdate(Object count) {
return '找到 $count 个更新';
}
@override
String get getPushTokenFailed => '未能获取到推送token';
@override
String get gettingToken => '正在获取Token...';
@override
String get go => '开始';
@override
String get goto => '前往';
@override
String get host => '主机';
@override
String httpFailedWithCode(Object code) {
return '请求失败, 状态码: $code';
}
@override
String get imagesList => '镜像列表';
@override
String get import => '导入';
@override
String get importAndExport => '导入或导出';
@override
String get inputDomainHere => '在这里输入域名';
@override
String get install => '安装';
@override
String get installDockerWithUrl => '请先 https://docs.docker.com/engine/install docker';
@override
String get invalidJson => '无效的json存在格式问题';
@override
String get invalidVersion => '不支持的版本';
@override
String invalidVersionHelp(Object url) {
return '请确保正确安装了docker或者使用的非自编译版本。如果没有以上问题请在 $url 提交问题。';
}
@override
String get isBusy => '当前正忙';
@override
String get keepForeground => '请保持应用处于前台!';
@override
String get keyAuth => '公钥认证';
@override
String get lastTry => '最后尝试';
@override
String get launchPage => '启动页';
@override
String get license => '开源证书';
@override
String get light => '';
@override
String get loadingFiles => '正在加载目录。。。';
@override
String get loss => '丢包率';
@override
String madeWithLove(Object myGithub) {
return '\n用❤️制作 by $myGithub';
}
@override
String get max => '最大';
@override
String get maxRetryCount => '服务器尝试重连次数';
@override
String get maxRetryCountEqual0 => '会无限重试';
@override
String get min => '最小';
@override
String get ms => '毫秒';
@override
String get name => '名称';
@override
String get needRestart => '需要重启 App';
@override
String get newContainer => '新建容器';
@override
String get noClient => '没有SSH连接';
@override
String get noInterface => '没有可用的接口';
@override
String get noResult => '无结果';
@override
String get noSavedPrivateKey => '没有已保存的私钥。';
@override
String get noSavedSnippet => '没有已保存的代码片段。';
@override
String get noServerAvailable => '没有可用的服务器。';
@override
String get noUpdateAvailable => '没有可用更新';
@override
String get notSelected => '未选择';
@override
String get nullToken => '无Token';
@override
String get ok => '';
@override
String get onServerDetailPage => '在服务器详情页';
@override
String get onlyIOS => '仅在iOS上有效';
@override
String get open => '打开';
@override
String get path => '路径';
@override
String get pickFile => '选择文件';
@override
String get ping => 'Ping';
@override
String get pingAvg => '平均:';
@override
String get pingInputIP => '请输入目标IP或域名';
@override
String get pingNoServer => '没有服务器可用于Ping\n请在服务器tab添加服务器后再试';
@override
String get pkg => '包管理';
@override
String get platformNotSupportUpdate => '当前平台不支持更新,请编译最新源码后手动安装';
@override
String get plzEnterHost => '请输入主机';
@override
String get plzSelectKey => '请选择私钥';
@override
String get port => '端口';
@override
String get preview => '预览';
@override
String get privateKey => '私钥';
@override
String get pushToken => '消息推送 Token';
@override
String get pwd => '密码';
@override
String get rename => '重命名';
@override
String reportBugsOnGithubIssue(Object url) {
return '请到 $url 提交问题';
}
@override
String get restart => '重启';
@override
String get restore => '恢复';
@override
String get restoreSuccess => '恢复成功需要重启App来应用更改';
@override
String restoreSureWithDate(Object date) {
return '确定恢复 $date 的备份吗?';
}
@override
String get result => '结果';
@override
String get run => '运行';
@override
String get save => '保存';
@override
String get second => '';
@override
String get server => '服务器';
@override
String get serverTabConnecting => '连接中...';
@override
String get serverTabEmpty => '现在没有服务器。\n点击右下方按钮来添加。';
@override
String get serverTabFailed => '失败';
@override
String get serverTabLoading => '加载中...';
@override
String get serverTabPlzSave => '请再次保存该私钥';
@override
String get serverTabUnkown => '未知状态';
@override
String get setting => '设置';
@override
String get sftpDlPrepare => '准备连接至服务器...';
@override
String get sftpNoDownloadTask => '没有下载任务';
@override
String get sftpSSHConnected => 'SFTP 已连接,即将开始下载...';
@override
String get showDistLogo => '显示发行版 Logo';
@override
String get snippet => '代码片段';
@override
String spentTime(Object time) {
return '耗时: $time';
}
@override
String sshTip(Object url) {
return '该功能目前处于测试阶段。\n\n请在 $url 反馈问题,或者加入我们开发。';
}
@override
String get start => '开始';
@override
String get stop => '停止';
@override
String get success => '成功';
@override
String sureDelete(Object name) {
return '确定删除[$name]';
}
@override
String get sureNoPwd => '确认使用无密码?';
@override
String sureToDeleteServer(Object server) {
return '你确定要删除服务器 [$server] 吗?';
}
@override
String get termTheme => '终端主题';
@override
String get themeMode => '主题模式';
@override
String get times => '';
@override
String get ttl => '缓存时间';
@override
String get unknown => '未知';
@override
String get unknownError => '未知错误';
@override
String get unkownConvertMode => '未知转换模式';
@override
String get update => '更新';
@override
String get updateAll => '更新全部';
@override
String get updateIntervalEqual0 => '你设置为0服务器状态不会自动刷新。\n且不能计算CPU使用情况。';
@override
String get updateServerStatusInterval => '服务器状态刷新间隔';
@override
String updateTip(Object newest) {
return '新版本: v1.0.$newest';
}
@override
String updateTipTooLow(Object newest) {
return '当前版本过低,请升级至 v1.0.$newest';
}
@override
String get upsideDown => '上下交换';
@override
String get urlOrJson => '链接或JSON';
@override
String get user => '用户';
@override
String versionHaveUpdate(Object build) {
return '找到新版本v1.0.$build, 点击更新';
}
@override
String versionUnknownUpdate(Object build) {
return '当前v1.0.$build';
}
@override
String versionUpdated(Object build) {
return '当前v1.0.$build, 已是最新版本';
}
@override
String get waitConnection => '请等待连接建立';
@override
String get willTakEeffectImmediately => '更改将会立即生效';
}

View File

@@ -0,0 +1,3 @@
# Generated by the flutter tool
name: synthetic_package
description: The Flutter application's synthetic package.

20
.github/workflows/issue-translator.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: 'issue-translator'
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
# not require, default false.
# Decide whether to modify the issue title.
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
IS_MODIFY_TITLE: false
# not require.
# Customize the translation robot prefix message.
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿

8
.gitignore vendored
View File

@@ -23,7 +23,7 @@
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
# .dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
@@ -48,4 +48,8 @@ app.*.map.json
/android/app/fjy.androidstudio.key
/release
test.dart
.fvm
.fvm
# Keep generated l10n files
/.dart_tool/*
!/.dart_tool/flutter_gen

View File

@@ -4,7 +4,7 @@
# This file should be version controlled.
version:
revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
revision: 9944297138845a94256f1cf37beb88ff9a8e811a
channel: stable
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
base_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
- platform: macos
create_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
base_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
# User provided section

View File

@@ -3,6 +3,9 @@
"files.watcherExclude": {
"**/.fvm": true
},
"git.ignoredRepositories": [
".fvm"
],
"search.exclude": {
"**/.fvm": true
}

View File

@@ -1,3 +1,4 @@
English | [简体中文](README_zh.md)
<!-- Title-->
<p align="center">
<h1 align="center">Server Box</h1>
@@ -13,6 +14,12 @@
</a>
</p>
<p align="center">
<a href="https://count.ly/f/badge" rel="nofollow">
<img style="height: 37px" src="https://count.ly/badges/light.svg">
</a>
</p>
<p align="center">
A Flutter project which provide charts to display server status and tools to manage server.
<br>
@@ -21,11 +28,21 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
## 🔖 Feature
- [x] 📊 Status chart view
- [x] ⌨️ `SSH` terminal
- [x] ⚙️ `Docker & Pkg` Manager, `SFTP`, `Snippet` ~~market~~, `Ping` and etc.
- [x] 📚 i18n (English, Chinese), **welcome contribution** :)
- [x] 🖥️ Desktop support
- [x] Functions
- [x] `SSH` Terminal, `SFTP`
- [x] `Docker & Pkg` Manager
- [x] Status charts
- [x] `Ping` and etc.
- [x] i18n (English, Chinese)
- **Welcome contribution** :)
- [How to contribute?](#l10n-guide)
- [x] Desktop support
## 📩 Push
You need to install [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor) on your servers.
And config iOS / Webhook to push server status to your portable device without using ServerBox app.
## 📱 ScreenShots
<table>
@@ -34,11 +51,14 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
<img width="200px" src="screenshots/server.jpg">
</td>
<td>
<img width="200px" src="screenshots/server_detail.png">
<img width="200px" src="screenshots/detail.jpg">
</td>
<td>
<img width="200px" src="screenshots/ssh.jpg">
</td>
<td>
<img width="200px" src="screenshots/apt.png">
</td>
</tr>
</table>
<table>
@@ -52,9 +72,13 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
<td>
<img width="200px" src="screenshots/docker.jpg">
</td>
<td>
<img width="200px" src="screenshots/convert.png">
</td>
</tr>
</table>
## 🖥 Platform
Status|Platform
--- | ---
@@ -62,5 +86,15 @@ Full Support|Android/iOS
Support, but not tested|macOS/Windows/Linux
## l10n guide
1. Fork this repo and clone it to your local machine.
2. Create `arb` file in `lib/l10n/` directory
- File name should be `intl_XX.arb`, where `XX` is the language code. Such as `intl_en.arb` for English and `intl_zh.arb` for Chinese.
3. Add content to the file. You can refer to `intl_en.arb` and `intl_zh.arb` for the format.
4. Run `flutter gen-l10n` to generate files.
5. Pull commit to your forked repo.
6. Request a pull request on my repo.
## 📝 License
`GPL v3. lollipopkit 2023`

99
README_zh.md Normal file
View File

@@ -0,0 +1,99 @@
简体中文 | [English](README.md)
<!-- Title-->
<p align="center">
<h1 align="center">Server Box</h1>
</p>
<!-- Badges-->
<p align="center">
<a href="https://apps.apple.com/app/id1586449703">
<img style="height: 37px" src="screenshots/appstore.svg">
</a>
<a href="https://github.com/lollipopkit/flutter_server_box/releases/latest">
<img style="height: 37px" src="screenshots/dl-android.svg">
</a>
</p>
<p align="center">
<a href="https://count.ly/f/badge" rel="nofollow">
<img style="height: 37px" src="https://count.ly/badges/light.svg">
</a>
</p>
<p align="center">
使用Flutter开发的服务器工具箱提供服务器状态图表和管理工具。
<br>
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
</p>
## 🔖 特点
- [x] 功能
- [x] `SSH` 终端, `SFTP`
- [x] `Docker & 包` 管理器
- [x] 状态图表
- [x] `Ping` 和 更多
- [x] 本地化 (英语, 中文)
- **欢迎贡献** :)
- [怎么贡献?](#l10n)
- [x] 桌面端支持
## 📩 推送
你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor)。
并且配置 iOS / Webhook 推送服务,这样,你可以在不使用 ServerBox app 时获取服务器状态。
## 📱 截屏
<table>
<tr>
<td>
<img width="200px" src="screenshots/server.jpg">
</td>
<td>
<img width="200px" src="screenshots/detail.jpg">
</td>
<td>
<img width="200px" src="screenshots/ssh.jpg">
</td>
<td>
<img width="200px" src="screenshots/apt.png">
</td>
</tr>
</table>
<table>
<tr>
<td>
<img width="200px" src="screenshots/ping.png">
</td>
<td>
<img width="200px" src="screenshots/sftp.jpg">
</td>
<td>
<img width="200px" src="screenshots/docker.jpg">
</td>
<td>
<img width="200px" src="screenshots/convert.png">
</td>
</tr>
</table>
## 🖥 平台
状态|平台
--- | ---
完整支持|Android/iOS
可能支持,未测试|macOS/Windows/Linux
## l10n
1. Fork本项目并Clone你Fork的项目至你的电脑
2.`lib/l10n/` 文件夹内创建 `.arb` 本地化文件
- 文件名应该类似 `intl_XX.arb`, `XX` 是语言标识码。 例如 `intl_en.arb` 是给英语的, `intl_zh.arb` 是给中文的
3.`.arb` 本地化文件添加内容。 你可以查看 `intl_en.arb``intl_zh.arb` 的内容,并理解其含义,来创建新的本地化文件
4. 运行 `flutter gen-l10n` 来生成所需文件
5. Commit 变更到你的 Fork 的 Repo
6. 在我的项目中发起 Pull Request.
## 📝 License
`GPL v3. lollipopkit 2023`

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,10 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup
# Used for flutter lib "file_picker"
Pod::PICKER_MEDIA = false
Pod::PICKER_AUDIO = false
target 'Runner' do
use_frameworks!
use_modular_headers!

View File

@@ -1,12 +1,16 @@
PODS:
- countly_flutter (22.09.0):
- Flutter
- file_picker (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_native_splash (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- plain_notification_token (0.0.1):
- Flutter
- r_upgrade (0.0.1):
- Flutter
- share_plus (0.0.1):
@@ -16,9 +20,11 @@ PODS:
DEPENDENCIES:
- countly_flutter (from `.symlinks/plugins/countly_flutter/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
- plain_notification_token (from `.symlinks/plugins/plain_notification_token/ios`)
- r_upgrade (from `.symlinks/plugins/r_upgrade/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -26,12 +32,16 @@ DEPENDENCIES:
EXTERNAL SOURCES:
countly_flutter:
:path: ".symlinks/plugins/countly_flutter/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/ios"
plain_notification_token:
:path: ".symlinks/plugins/plain_notification_token/ios"
r_upgrade:
:path: ".symlinks/plugins/r_upgrade/ios"
share_plus:
@@ -41,13 +51,15 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
countly_flutter: 135f1a4930f8e26ba223a14201d3f265ea7b4c83
file_picker: 1d63c4949e05e386da864365f8c13e1e64787675
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
PODFILE CHECKSUM: 7fb15c416f8685fca4966867a8da218ec592ec2e
COCOAPODS: 1.11.3

View File

@@ -47,6 +47,7 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9C5314B89F1F73A1900CCAFD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
A775F241DEE026555178AC01 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
E398BF6A29BDB34500FE4FD5 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -104,6 +105,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
E398BF6A29BDB34500FE4FD5 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -356,7 +358,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 174;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 243;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -364,7 +367,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.174;
MARKETING_VERSION = 1.0.243;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -486,7 +489,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 174;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 243;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -494,7 +498,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.174;
MARKETING_VERSION = 1.0.243;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -510,7 +514,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 174;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 243;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -518,7 +523,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.174;
MARKETING_VERSION = 1.0.243;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View File

@@ -1,54 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ServerBox</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>zh</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>zh</string>
</array>
<key>CFBundleName</key>
<string>ServerBox</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>production</string>
</dict>
</plist>

File diff suppressed because one or more lines are too long

4
l10n.yaml Normal file
View File

@@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: l10n.dart
output-class: S

View File

@@ -1,79 +1,97 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:toolbox/core/extension/colorx.dart';
import 'package:toolbox/core/utils.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';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/utils/misc.dart';
import '/core/extension/colorx.dart';
import 'core/utils/ui.dart';
import 'data/res/build_data.dart';
import 'data/res/color.dart';
import 'data/store/setting.dart';
import 'locator.dart';
import 'view/page/home.dart';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
MyApp({Key? key}) : super(key: key);
final _setting = locator<SettingStore>();
@override
Widget build(BuildContext context) {
setTransparentNavigationBar(context);
final fontName = getFileName(_setting.fontPath.fetch());
primaryColor = Color(_setting.primaryColor.fetch()!);
final textStyle = TextStyle(color: primaryColor);
final materialColor = primaryColor.materialStateColor;
final materialColorAlpha = primaryColor.withOpacity(0.7).materialStateColor;
final fabTheme =
FloatingActionButtonThemeData(backgroundColor: primaryColor);
final switchTheme = SwitchThemeData(
thumbColor: materialColor,
trackColor: materialColorAlpha,
);
final appBarTheme = AppBarTheme(backgroundColor: primaryColor);
final iconTheme = IconThemeData(color: primaryColor);
final inputDecorationTheme = InputDecorationTheme(
labelStyle: textStyle,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor),
),
);
final radioTheme = RadioThemeData(
fillColor: materialColor,
);
final primarySwatch = primaryColor.materialColor;
return ValueListenableBuilder<int>(
valueListenable: locator<SettingStore>().primaryColor.listenable(),
builder: (_, value, __) {
final primaryColor = Color(value);
final textStyle = TextStyle(color: primaryColor);
final materialColor = primaryColor.materialStateColor;
final materialColorAlpha =
primaryColor.withOpacity(0.7).materialStateColor;
return MaterialApp(
localizationsDelegates: const [
S.delegate,
...GlobalMaterialLocalizations.delegates,
],
supportedLocales: S.delegate.supportedLocales,
title: BuildData.name,
theme: ThemeData(
primaryColor: primaryColor,
appBarTheme: AppBarTheme(backgroundColor: primaryColor),
floatingActionButtonTheme:
FloatingActionButtonThemeData(backgroundColor: primaryColor),
iconTheme: IconThemeData(color: primaryColor),
primaryIconTheme: IconThemeData(color: primaryColor),
switchTheme: SwitchThemeData(
thumbColor: materialColor,
trackColor: materialColorAlpha,
),
buttonTheme: ButtonThemeData(splashColor: primaryColor),
inputDecorationTheme: InputDecorationTheme(
labelStyle: textStyle,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor),
),
),
radioTheme: RadioThemeData(
fillColor: materialColor,
),
),
darkTheme: ThemeData.dark().copyWith(
primaryColor: primaryColor,
floatingActionButtonTheme:
FloatingActionButtonThemeData(backgroundColor: primaryColor),
iconTheme: IconThemeData(color: primaryColor),
primaryIconTheme: IconThemeData(color: primaryColor),
switchTheme: SwitchThemeData(
thumbColor: materialColor,
trackColor: materialColorAlpha,
),
buttonTheme: ButtonThemeData(splashColor: primaryColor),
inputDecorationTheme: InputDecorationTheme(
labelStyle: textStyle,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor),
),
),
radioTheme: RadioThemeData(
fillColor: materialColor,
),
),
home: MyHomePage(primaryColor: primaryColor),
);
});
valueListenable: _setting.themeMode.listenable(),
builder: (_, tMode, __) {
final ok = tMode >= 0 && tMode <= ThemeMode.values.length - 1;
final themeMode = ok ? ThemeMode.values[tMode] : ThemeMode.system;
final theme = ThemeData(
useMaterial3: false,
fontFamily: fontName,
primaryColor: primaryColor,
primarySwatch: primarySwatch,
appBarTheme: appBarTheme,
floatingActionButtonTheme: fabTheme,
iconTheme: iconTheme,
primaryIconTheme: iconTheme,
switchTheme: switchTheme,
inputDecorationTheme: inputDecorationTheme,
radioTheme: radioTheme,
);
final darkTheme = ThemeData(
useMaterial3: false,
fontFamily: fontName,
primaryColor: primaryColor,
primarySwatch: primarySwatch,
floatingActionButtonTheme: fabTheme,
iconTheme: iconTheme,
primaryIconTheme: iconTheme,
switchTheme: switchTheme,
inputDecorationTheme: inputDecorationTheme,
radioTheme: radioTheme,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSwatch(
primarySwatch: primaryColor.materialColor,
brightness: Brightness.dark,
accentColor: primaryColor,
),
);
return MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: S.localizationsDelegates,
supportedLocales: S.supportedLocales,
title: BuildData.name,
themeMode: themeMode,
theme: theme,
darkTheme: darkTheme,
home: const MyHomePage(),
);
},
);
}
}

View File

@@ -6,18 +6,20 @@ import 'package:countly_flutter/countly_flutter.dart';
import 'package:logging/logging.dart';
import 'package:toolbox/core/build_mode.dart';
import 'utils/platform.dart';
class Analysis {
static const _url = 'https://countly.xuty.cc';
static const _key = '80372a2a66424b32d0ac8991bfa1ef058bd36b1f';
static bool _enabled = false;
static bool enabled = false;
static Future<void> init() async {
if (!BuildMode.isRelease) {
return;
}
if (Platform.isAndroid || Platform.isIOS) {
_enabled = true;
if (isAndroid || isIOS) {
enabled = true;
final config = CountlyConfig(_url, _key)
.setLoggingEnabled(false)
.enableCrashReporting();
@@ -31,13 +33,13 @@ class Analysis {
}
static void recordView(String view) {
if (_enabled) {
if (enabled) {
Countly.recordView(view);
}
}
static void recordException(Object exception, [bool fatal = false]) {
if (_enabled) {
if (enabled) {
Countly.logException(exception.toString(), !fatal, null);
}
}

View File

@@ -24,4 +24,17 @@ extension ColorX on Color {
return null;
});
}
MaterialColor get materialColor => MaterialColor(value, {
50: withOpacity(0.05),
100: withOpacity(0.1),
200: withOpacity(0.2),
300: withOpacity(0.3),
400: withOpacity(0.4),
500: withOpacity(0.5),
600: withOpacity(0.6),
700: withOpacity(0.7),
800: withOpacity(0.8),
900: withOpacity(0.9),
});
}

View File

@@ -3,6 +3,7 @@ import 'dart:typed_data';
extension FutureUint8ListX on Future<Uint8List> {
Future<String> get string async => utf8.decode(await this);
Future<ByteData> get byteData async => (await this).buffer.asByteData();
}
extension Uint8ListX on Uint8List {

View File

@@ -1,15 +1,16 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:logging/logging.dart';
import 'package:r_upgrade/r_upgrade.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/service/app.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import '../data/model/app/update.dart';
import '../data/provider/app.dart';
import '../data/res/build_data.dart';
import '../data/service/app.dart';
import '../locator.dart';
import 'utils/platform.dart';
import 'utils/ui.dart';
final _logger = Logger('UPDATE');
@@ -29,11 +30,11 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
locator<AppProvider>().setNewestBuild(update.newest);
final newest = () {
if (Platform.isAndroid) {
if (isAndroid) {
return update.androidbuild;
} else if (Platform.isIOS) {
} else if (isIOS) {
return update.iosbuild;
} else if (Platform.isMacOS) {
} else if (isMacOS) {
return update.macbuild;
}
return update.newest;
@@ -46,30 +47,51 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
}
_logger.info('Update available: $newest');
if (Platform.isAndroid && !await isFileAvailable(update.android)) {
if (isAndroid && !await isFileAvailable(update.android)) {
_logger.warning('Android update file not available');
return;
}
final s = S.of(context);
final s = S.of(context)!;
if (update.min > BuildData.build) {
showRoundDialog(
context,
s.attention,
Text(s.updateTipTooLow(newest)),
[
TextButton(
onPressed: () => _doUpdate(update, context, s),
child: Text(s.ok),
)
],
);
return;
}
showSnackBarWithAction(
context,
update.min > BuildData.build
? 'Your version is too old. \nPlease update to v1.0.$newest.'
: 'Update: v1.0.$newest available. \n${update.changelog}',
s.update, () async {
if (Platform.isAndroid) {
await RUpgrade.upgrade(update.android,
fileName: update.android.split('/').last, isAutoRequestInstall: true);
} else if (Platform.isIOS) {
await RUpgrade.upgradeFromAppStore('1586449703');
} else if (Platform.isMacOS) {
await RUpgrade.upgradeFromUrl(update.mac);
} else {
showRoundDialog(context, s.attention, Text(s.platformNotSupportUpdate), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok))
]);
}
});
context,
'${s.updateTip(newest)} \n${update.changelog}',
s.update,
() => _doUpdate(update, context, s),
);
}
Future<void> _doUpdate(AppUpdate update, BuildContext context, S s) async {
if (isAndroid) {
await RUpgrade.upgrade(
update.android,
fileName: update.android.split('/').last,
isAutoRequestInstall: true,
);
} else if (isIOS) {
await RUpgrade.upgradeFromAppStore('1586449703');
} else {
showRoundDialog(context, s.attention, Text(s.platformNotSupportUpdate), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok),
)
]);
}
}

61
lib/core/utils/misc.dart Normal file
View File

@@ -0,0 +1,61 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:plain_notification_token/plain_notification_token.dart';
import 'package:share_plus/share_plus.dart';
import '../../view/widget/rebuild.dart';
import 'platform.dart';
Future<bool> shareFiles(BuildContext context, List<String> filePaths) async {
for (final filePath in filePaths) {
if (!await File(filePath).exists()) {
return false;
}
}
var text = '';
if (filePaths.length == 1) {
text = filePaths.first.split('/').last;
} else {
text = '${filePaths.length} ${S.of(context)!.files}';
}
final xfiles = filePaths.map((e) => XFile(e)).toList();
await Share.shareXFiles(xfiles, text: 'ServerBox -> $text');
return filePaths.isNotEmpty;
}
void copy(String text) {
Clipboard.setData(ClipboardData(text: text));
}
Future<String?> pickOneFile() async {
final result = await FilePicker.platform.pickFiles(type: FileType.any);
return result?.files.single.path;
}
Future<String?> getToken() async {
if (isIOS) {
final plainNotificationToken = PlainNotificationToken();
plainNotificationToken.requestPermission();
// If you want to wait until Permission dialog close,
// you need wait changing setting registered.
await plainNotificationToken.onIosSettingsRegistered.first;
return await plainNotificationToken.getToken();
}
return null;
}
String? getFileName(String? path) {
if (path == null) {
return null;
}
return path.split('/').last;
}
void rebuildAll(BuildContext context) {
RebuildWidget.restartApp(context);
}

View File

@@ -0,0 +1,43 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
enum PlatformType {
android,
ios,
linux,
macos,
windows,
web,
}
final _p = () {
if (kIsWeb) {
return PlatformType.web;
}
if (Platform.isAndroid) {
return PlatformType.android;
}
if (Platform.isIOS) {
return PlatformType.ios;
}
if (Platform.isLinux) {
return PlatformType.linux;
}
if (Platform.isMacOS) {
return PlatformType.macos;
}
if (Platform.isWindows) {
return PlatformType.windows;
}
return PlatformType.web;
}();
PlatformType get platform => _p;
bool get isAndroid => _p == PlatformType.android;
bool get isIOS => _p == PlatformType.ios;
bool get isLinux => _p == PlatformType.linux;
bool get isMacOS => _p == PlatformType.macos;
bool get isWindows => _p == PlatformType.windows;
bool get isWeb => _p == PlatformType.web;

View File

@@ -0,0 +1,57 @@
import 'dart:async';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/store/private_key.dart';
import '../../locator.dart';
/// Must put this func out of any Class.
///
/// Because of this function is called by [compute] in [ServerProvider.genClient].
///
/// https://stackoverflow.com/questions/51998995/invalid-arguments-illegal-argument-in-isolate-message-object-is-a-closure
List<SSHKeyPair> loadIndentity(String key) {
return SSHKeyPair.fromPem(key);
}
/// [args] : [key, pwd]
String decyptPem(List<String> args) {
/// skip when the key is not encrypted, or will throw exception
if (!SSHKeyPair.isEncryptedPem(args[0])) return args[0];
final sshKey = SSHKeyPair.fromPem(args[0], args[1]);
return sshKey.first.toPem();
}
enum GenSSHClientStatus {
socket,
key,
pwd,
}
Future<SSHClient> genClient(ServerPrivateInfo spi,
{void Function(GenSSHClientStatus)? onStatus}) async {
final onStatus_ = onStatus ?? (_) {};
onStatus_(GenSSHClientStatus.socket);
final socket = await SSHSocket.connect(
spi.ip,
spi.port,
timeout: const Duration(seconds: 5),
);
if (spi.pubKeyId == null) {
onStatus_(GenSSHClientStatus.pwd);
return SSHClient(
socket,
username: spi.user,
onPasswordRequest: () => spi.pwd,
);
}
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
onStatus_(GenSSHClientStatus.key);
return SSHClient(
socket,
username: spi.user,
identities: await compute(loadIndentity, key.privateKey),
);
}

View File

@@ -1,14 +1,16 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import 'package:toolbox/core/persistant_store.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/widget/card_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/utils/misc.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:toolbox/core/extension/stringx.dart';
import '../../view/widget/card_dialog.dart';
import '../persistant_store.dart';
import 'platform.dart';
import '../extension/stringx.dart';
import '../extension/uint8list.dart';
bool isDarkMode(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark;
@@ -63,7 +65,7 @@ Widget buildSwitch(BuildContext context, StoreProperty<bool> prop,
}
void setTransparentNavigationBar(BuildContext context) {
if (Platform.isAndroid) {
if (isAndroid) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
@@ -71,37 +73,6 @@ 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 '';
}
}
Future<bool> shareFiles(BuildContext context, List<String> filePaths) async {
for (final filePath in filePaths) {
if (!await File(filePath).exists()) {
return false;
}
}
var text = '';
if (filePaths.length == 1) {
text = filePaths.first.split('/').last;
} else {
text = '${filePaths.length} ${S.of(context).files}';
}
final xfiles = filePaths.map((e) => XFile(e)).toList();
await Share.shareXFiles(xfiles, text: 'ServerBox -> $text');
return filePaths.isNotEmpty;
}
Widget buildPopuopMenu(
{required List<PopupMenuEntry> items,
required Function(dynamic) onSelected}) {
@@ -119,3 +90,25 @@ Widget buildPopuopMenu(
),
);
}
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 '';
}
}
Future<void> loadFontFile(String? localPath) async {
if (localPath == null) return;
final name = getFileName(localPath)!;
var fontLoader = FontLoader(name);
fontLoader.addFont(File(localPath).readAsBytes().byteData);
await fontLoader.load();
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/widgets.dart';
import '../../../core/utils/ui.dart';
class DynamicColor {
/// 白天模式显示的颜色
Color light;
/// 暗黑模式显示的颜色
Color dark;
DynamicColor(this.light, this.dark);
Color resolve(BuildContext context) => isDarkMode(context) ? dark : light;
}

View File

@@ -1,56 +0,0 @@
import 'package:toolbox/data/model/server/cpu_status.dart';
class Cpu2Status {
List<CpuStatus> _pre;
List<CpuStatus> _now;
String temp;
Cpu2Status(this._pre, this._now, this.temp);
double usedPercent({int coreIdx = 0}) {
if (_now.length != _pre.length) return 0;
final idleDelta = _now[coreIdx].idle - _pre[coreIdx].idle;
final totalDelta = _now[coreIdx].total - _pre[coreIdx].total;
final used = idleDelta / totalDelta;
return used.isNaN ? 0 : 100 - used * 100;
}
void update(List<CpuStatus> newStatus, String newTemp) {
_pre = _now;
_now = newStatus;
temp = newTemp;
}
int get coresCount => _now.length;
int get totalDelta => _now[0].total - _pre[0].total;
double get user {
if (_now.length != _pre.length) return 0;
final delta = _now[0].user - _pre[0].user;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get sys {
if (_now.length != _pre.length) return 0;
final delta = _now[0].sys - _pre[0].sys;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get nice {
if (_now.length != _pre.length) return 0;
final delta = _now[0].nice - _pre[0].nice;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get iowait {
if (_now.length != _pre.length) return 0;
final delta = _now[0].iowait - _pre[0].iowait;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get idle => 100 - usedPercent();
}

View File

@@ -1,7 +1,62 @@
class CpuStatus {
List<OneTimeCpuStatus> _pre;
List<OneTimeCpuStatus> _now;
String temp;
CpuStatus(this._pre, this._now, this.temp);
double usedPercent({int coreIdx = 0}) {
if (_now.length != _pre.length) return 0;
final idleDelta = _now[coreIdx].idle - _pre[coreIdx].idle;
final totalDelta = _now[coreIdx].total - _pre[coreIdx].total;
final used = idleDelta / totalDelta;
return used.isNaN ? 0 : 100 - used * 100;
}
void update(List<OneTimeCpuStatus> newStatus, String newTemp) {
_pre = _now;
_now = newStatus;
temp = newTemp;
}
int get coresCount => _now.length;
int get totalDelta => _now[0].total - _pre[0].total;
double get user {
if (_now.length != _pre.length) return 0;
final delta = _now[0].user - _pre[0].user;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get sys {
if (_now.length != _pre.length) return 0;
final delta = _now[0].sys - _pre[0].sys;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get nice {
if (_now.length != _pre.length) return 0;
final delta = _now[0].nice - _pre[0].nice;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get iowait {
if (_now.length != _pre.length) return 0;
final delta = _now[0].iowait - _pre[0].iowait;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get idle => 100 - usedPercent();
}
///
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
///
class CpuStatus {
class OneTimeCpuStatus {
/*
{
"user": 0,
@@ -23,7 +78,7 @@ class CpuStatus {
late int irq;
late int softirq;
CpuStatus(
OneTimeCpuStatus(
this.id,
this.user,
this.sys,
@@ -36,3 +91,51 @@ class CpuStatus {
int get total => user + sys + nice + idle + iowait + irq + softirq;
}
List<OneTimeCpuStatus> parseCPU(String raw) {
final List<OneTimeCpuStatus> cpus = [];
for (var item in raw.split('\n')) {
if (item == '') break;
final id = item.split(' ').first;
final matches = item.replaceFirst(id, '').trim().split(' ');
cpus.add(OneTimeCpuStatus(
id,
int.parse(matches[0]),
int.parse(matches[1]),
int.parse(matches[2]),
int.parse(matches[3]),
int.parse(matches[4]),
int.parse(matches[5]),
int.parse(matches[6])));
}
return cpus;
}
final cpuTempReg = RegExp(r'(x86_pkg_temp|cpu_thermal)');
String parseCPUTemp(List<String> segments) {
const noMatch = "/sys/class/thermal/thermal_zone*/type";
final type = segments[0];
final value = segments[1];
// Not support to get CPU temperature
if (value.contains(noMatch) ||
type.contains(noMatch) ||
value.isEmpty ||
type.isEmpty) {
return '';
}
final split = type.split('\n');
int idx = 0;
for (var item in split) {
if (item.contains(cpuTempReg)) {
break;
}
idx++;
}
final valueSplited = value.split('\n');
if (idx >= valueSplited.length) return '';
final temp = int.tryParse(valueSplited[idx].trim());
if (temp == null) return '';
return '${(temp / 1000).toStringAsFixed(1)}°C';
}

View File

@@ -1,3 +1,5 @@
import '../../res/misc.dart';
class DiskInfo {
/*
{
@@ -10,19 +12,40 @@ class DiskInfo {
}
*/
late String mountPath;
late String mountLocation;
late String path;
late String loc;
late int usedPercent;
late String used;
late String size;
late String avail;
DiskInfo(
this.mountPath,
this.mountLocation,
this.path,
this.loc,
this.usedPercent,
this.used,
this.size,
this.avail,
);
}
List<DiskInfo> parseDisk(String raw) {
final list = <DiskInfo>[];
final items = raw.split('\n');
items.removeAt(0);
for (var item in items) {
if (item.isEmpty) {
continue;
}
final vals = item.split(numReg);
list.add(DiskInfo(
vals[0],
vals[5],
int.parse(vals[4].replaceFirst('%', '')),
vals[2],
vals[1],
vals[3],
));
}
return list;
}

View File

@@ -11,3 +11,70 @@ class Memory {
required this.cache,
required this.avail});
}
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
Memory parseMem(String raw) {
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
final total = int.parse(
items.firstWhere((e) => e?.group(1) == 'MemTotal:')?.group(2) ?? '1',
);
final free = int.parse(
items.firstWhere((e) => e?.group(1) == 'MemFree:')?.group(2) ?? '0',
);
final cached = int.parse(
items.firstWhere((e) => e?.group(1) == 'Cached:')?.group(2) ?? '0',
);
final available = int.parse(
items.firstWhere((e) => e?.group(1) == 'MemAvailable:')?.group(2) ?? '0',
);
return Memory(
total: total,
used: total - available,
free: free,
cache: cached,
avail: available,
);
}
class Swap {
final int total;
final int used;
final int free;
final int cached;
Swap({
required this.total,
required this.used,
required this.free,
required this.cached,
});
@override
String toString() {
return 'Swap{total: $total, used: $used, free: $free, cached: $cached}';
}
}
Swap parseSwap(String raw) {
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
final total = int.parse(
items.firstWhere((e) => e?.group(1) == 'SwapTotal:')?.group(2) ?? '1',
);
final free = int.parse(
items.firstWhere((e) => e?.group(1) == 'SwapFree:')?.group(2) ?? '0',
);
final cached = int.parse(
items.firstWhere((e) => e?.group(1) == 'SwapCached:')?.group(2) ?? '0',
);
return Swap(
total: total,
used: total - free,
free: free,
cached: cached,
);
}

View File

@@ -2,9 +2,9 @@ import 'package:toolbox/core/extension/numx.dart';
class NetSpeedPart {
String device;
int bytesIn;
int bytesOut;
int time;
BigInt bytesIn;
BigInt bytesOut;
BigInt time;
NetSpeedPart(this.device, this.bytesIn, this.bytesOut, this.time);
}
@@ -26,7 +26,7 @@ class NetSpeed {
_now = newOne;
}
int get timeDiff => _now[0].time - _old[0].time;
BigInt get timeDiff => _now[0].time - _old[0].time;
String speedIn({String? device}) {
if (_old[0].device == '' || _now[0].device == '') return '0kb/s';
@@ -36,6 +36,13 @@ class NetSpeed {
return buildStandardOutput(speedInBytesPerSecond);
}
String totalIn({String? device}) {
if (_old[0].device == '' || _now[0].device == '') return '0kb';
final idx = deviceIdx(device);
final totalInBytes = _now[idx].bytesIn;
return totalInBytes.toInt().convertBytes;
}
String speedOut({String? device}) {
if (_old[0].device == '' || _now[0].device == '') return '0kb/s';
final idx = deviceIdx(device);
@@ -44,6 +51,13 @@ class NetSpeed {
return buildStandardOutput(speedOutBytesPerSecond);
}
String totalOut({String? device}) {
if (_old[0].device == '' || _now[0].device == '') return '0kb';
final idx = deviceIdx(device);
final totalOutBytes = _now[idx].bytesOut;
return totalOutBytes.toInt().convertBytes;
}
int deviceIdx(String? device) {
if (device != null) {
for (var item in _now) {
@@ -55,6 +69,31 @@ class NetSpeed {
return 0;
}
String buildStandardOutput(double speed) =>
'${speed.convertBytes.toLowerCase()}/s';
String buildStandardOutput(double speed) => '${speed.convertBytes}/s';
}
/// [raw] example:
/// Inter-| Receive | Transmit
/// face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
/// 1635752901
List<NetSpeedPart> parseNetSpeed(String raw) {
final split = raw.split('\n');
if (split.length < 4) {
return [];
}
final time = BigInt.parse(split[split.length - 1]);
final results = <NetSpeedPart>[];
for (final item in split.sublist(2, split.length - 1)) {
final data = item.trim().split(':');
final device = data.first;
final bytes = data.last.trim().split(' ');
bytes.removeWhere((element) => element == '');
final bytesIn = BigInt.parse(bytes.first);
final bytesOut = BigInt.parse(bytes[8]);
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
}
return results;
}

View File

@@ -2,13 +2,21 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart';
class ServerInfo {
ServerPrivateInfo info;
class Server {
ServerPrivateInfo spi;
ServerStatus status;
SSHClient? client;
ServerConnectionState connectionState;
ServerState cs;
ServerInfo(this.info, this.status, this.client, this.connectionState);
Server(this.spi, this.status, this.client, this.cs);
}
enum ServerConnectionState { disconnected, connecting, connected, failed }
enum ServerState {
disconnected,
connecting,
connected,
failed;
bool get shouldConnect =>
this == ServerState.disconnected || this == ServerState.failed;
}

View File

@@ -31,15 +31,17 @@ class ServerPrivateInfo {
@HiveField(5)
String? pubKeyId;
String get id => '$user@$ip:$port';
late String id;
ServerPrivateInfo({
required this.name,
required this.ip,
required this.port,
required this.user,
required this.pwd,
this.pubKeyId,
}) : id = '$user@$ip:$port';
ServerPrivateInfo(
{required this.name,
required this.ip,
required this.port,
required this.user,
required this.pwd,
this.pubKeyId});
ServerPrivateInfo.fromJson(Map<String, dynamic> json) {
name = json["name"].toString();
ip = json["ip"].toString();
@@ -47,7 +49,9 @@ class ServerPrivateInfo {
user = json["user"].toString();
pwd = json["authorization"].toString();
pubKeyId = json["pubKeyId"]?.toString();
id = '$user@$ip:$port';
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["name"] = name;

View File

@@ -1,4 +1,4 @@
import 'package:toolbox/data/model/server/cpu_2_status.dart';
import 'package:toolbox/data/model/server/cpu_status.dart';
import 'package:toolbox/data/model/server/disk_info.dart';
import 'package:toolbox/data/model/server/memory.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
@@ -30,8 +30,9 @@ class ServerStatus {
}
*/
Cpu2Status cpu2Status;
Memory memory;
CpuStatus cpu;
Memory mem;
Swap swap;
String sysVer;
String uptime;
List<DiskInfo> disk;
@@ -39,7 +40,15 @@ class ServerStatus {
NetSpeed netSpeed;
String? failedInfo;
ServerStatus(this.cpu2Status, this.memory, this.sysVer, this.uptime,
this.disk, this.tcp, this.netSpeed,
{this.failedInfo});
ServerStatus({
required this.cpu,
required this.mem,
required this.sysVer,
required this.uptime,
required this.disk,
required this.tcp,
required this.netSpeed,
required this.swap,
this.failedInfo,
});
}

View File

@@ -1,3 +1,6 @@
import '../../../core/extension/stringx.dart';
import '../../res/misc.dart';
///
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
///
@@ -39,3 +42,14 @@ class TcpStatus {
return data;
}
}
TcpStatus? parseTcp(String raw) {
final lines = raw.split('\n');
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
orElse: () => '');
if (idx != '') {
final vals = idx.split(numReg);
return TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
}
return null;
}

View File

@@ -0,0 +1,16 @@
import '../server/server_private_info.dart';
class DownloadItem {
DownloadItem(this.spi, this.remotePath, this.localPath);
final ServerPrivateInfo spi;
final String remotePath;
final String localPath;
}
class DownloadItemEvent {
DownloadItemEvent(this.item, this.privateKey);
final DownloadItem item;
final String? privateKey;
}

View File

@@ -1,5 +1,7 @@
import 'package:toolbox/data/model/sftp/download_worker.dart';
import 'download_item.dart';
class SftpDownloadStatus {
final int id;
final DownloadItem item;

View File

@@ -4,16 +4,9 @@ import 'dart:isolate';
import 'package:dartssh2/dartssh2.dart';
import 'package:easy_isolate/easy_isolate.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/sftp/download_status.dart';
class DownloadItem {
DownloadItem(this.spi, this.remotePath, this.localPath);
final ServerPrivateInfo spi;
final String remotePath;
final String localPath;
}
import 'download_item.dart';
import 'download_status.dart';
class SftpDownloadWorker {
SftpDownloadWorker(
@@ -101,10 +94,3 @@ class SftpDownloadWorker {
}
}
}
class DownloadItemEvent {
DownloadItemEvent(this.item, this.privateKey);
final DownloadItem item;
final String? privateKey;
}

View File

@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:xterm/ui.dart';
import '../../res/terminal.dart';
class TerminalUITheme {
final Color cursor;
final Color selection;
final Color foreground;
final Color background;
final Color searchHitBackground;
final Color searchHitBackgroundCurrent;
final Color searchHitForeground;
const TerminalUITheme({
required this.cursor,
required this.selection,
required this.foreground,
required this.background,
required this.searchHitBackground,
required this.searchHitBackgroundCurrent,
required this.searchHitForeground,
});
TerminalTheme toTerminalTheme(TerminalColors termColor) {
return TerminalTheme(
cursor: cursor,
selection: selection,
foreground: foreground,
background: background,
black: termColor.black,
red: termColor.red,
green: termColor.green,
yellow: termColor.yellow,
blue: termColor.blue,
magenta: termColor.magenta,
cyan: termColor.cyan,
white: termColor.white,
brightBlack: termColor.brightBlack,
brightRed: termColor.brightRed,
brightGreen: termColor.brightGreen,
brightYellow: termColor.brightYellow,
brightBlue: termColor.brightBlue,
brightMagenta: termColor.brightMagenta,
brightCyan: termColor.brightCyan,
brightWhite: termColor.brightWhite,
searchHitBackground: searchHitBackground,
searchHitBackgroundCurrent: searchHitBackgroundCurrent,
searchHitForeground: searchHitForeground,
);
}
}
abstract class TerminalColors {
final TerminalColorsPlatform platform;
final Color black = Colors.black;
final Color red;
final Color green;
final Color yellow;
final Color blue;
// 品红
final Color magenta;
// 青
final Color cyan;
final Color white;
/// Also called grey
final Color brightBlack;
final Color brightRed;
final Color brightGreen;
final Color brightYellow;
final Color brightBlue;
final Color brightMagenta;
final Color brightCyan;
final Color brightWhite;
TerminalColors(
this.platform,
this.red,
this.green,
this.yellow,
this.blue,
this.magenta,
this.cyan,
this.white,
this.brightBlack,
this.brightRed,
this.brightGreen,
this.brightYellow,
this.brightBlue,
this.brightMagenta,
this.brightCyan, {
this.brightWhite = Colors.white,
});
}
enum TerminalColorsPlatform {
macOS,
vga,
cmd,
putty,
xterm,
ubuntu,
;
String get name {
switch (this) {
case TerminalColorsPlatform.vga:
return 'VGA';
case TerminalColorsPlatform.cmd:
return 'CMD';
case TerminalColorsPlatform.macOS:
return 'macOS';
case TerminalColorsPlatform.putty:
return 'PuTTY';
case TerminalColorsPlatform.xterm:
return 'XTerm';
case TerminalColorsPlatform.ubuntu:
return 'Ubuntu';
default:
return 'Unknown';
}
}
TerminalColors get colors {
switch (this) {
case TerminalColorsPlatform.vga:
return VGATerminalColor();
case TerminalColorsPlatform.cmd:
return CMDTerminalColor();
case TerminalColorsPlatform.macOS:
return MacOSTerminalColor();
case TerminalColorsPlatform.putty:
return PuttyTerminalColor();
case TerminalColorsPlatform.xterm:
return XTermTerminalColor();
case TerminalColorsPlatform.ubuntu:
return UbuntuTerminalColor();
default:
return MacOSTerminalColor();
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
import 'package:xterm/core.dart';
class VirtualKey {
final String text;
final bool toggleable;
final TerminalKey? key;
final IconData? icon;
final VirtualKeyFunc? func;
VirtualKey(this.text,
{this.key, this.toggleable = false, this.icon, this.func});
}
enum VirtualKeyFunc { toggleIME, backspace, copy, paste }

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import '../../data/res/misc.dart';
/// format: [NAME][LEVEL]: MESSAGE
final _headReg = RegExp(r'(\[[A-Za-z]+\])(\[[A-Z]+\]): (.*)');
const _level2Color = {
@@ -14,7 +16,7 @@ class DebugProvider extends ChangeNotifier {
final match = _headReg.allMatches(text);
if (match.isNotEmpty) {
addWidget(Text.rich(TextSpan(
_addWidget(Text.rich(TextSpan(
children: [
TextSpan(
text: match.first.group(1),
@@ -30,31 +32,11 @@ class DebugProvider extends ChangeNotifier {
],
)));
} else {
_addText(text);
_addWidget(Text(text));
}
notifyListeners();
}
void _addText(String text) {
_addWidget(Text(text));
}
void addError(Object error) {
_addError(error);
notifyListeners();
}
void _addError(Object error) {
_addMultiline(error, Colors.red);
}
void addMultiline(Object data, [Color color = Colors.blue]) {
_addMultiline(data, color);
notifyListeners();
}
void _addMultiline(Object data, [Color color = Colors.blue]) {
final widget = Text(
'$data',
style: TextStyle(
@@ -67,14 +49,13 @@ class DebugProvider extends ChangeNotifier {
));
}
void addWidget(Widget widget) {
_addWidget(widget);
notifyListeners();
}
void _addWidget(Widget widget) {
widgets.add(widget);
widgets.add(const SizedBox(height: 13));
if (widgets.length > maxDebugLogLines) {
widgets.removeRange(0, widgets.length - maxDebugLogLines);
}
notifyListeners();
}
void clear() {

View File

@@ -53,6 +53,7 @@ class DockerProvider extends BusyProvider {
}
Future<void> refresh() async {
if (isBusy) return;
final verRaw = await client!.run('docker version'.withLangExport).string;
if (verRaw.contains(_dockerNotFound)) {
error = DockerErr(type: DockerErrType.notInstalled);

View File

@@ -1,111 +1,58 @@
import 'dart:async';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/server/cpu_2_status.dart';
import 'package:toolbox/data/model/server/cpu_status.dart';
import 'package:toolbox/data/model/server/memory.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
import 'package:toolbox/data/model/server/disk_info.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/model/server/tcp_status.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart';
/// Must put this func out of any Class.
/// Because of this function is called by [compute] in [ServerProvider.genClient].
/// https://stackoverflow.com/questions/51998995/invalid-arguments-illegal-argument-in-isolate-message-object-is-a-closure
List<SSHKeyPair> loadIndentity(String key) {
return SSHKeyPair.fromPem(key);
}
const seperator = 'A====A';
const shellCmd = "export LANG=en_US.utf-8 \necho '$seperator' \n"
"cat /proc/net/dev && date +%s \necho $seperator \n "
"cat /etc/os-release | grep PRETTY_NAME \necho $seperator \n"
"cat /proc/stat | grep cpu \necho $seperator \n"
"uptime \necho $seperator \n"
"cat /proc/net/snmp \necho $seperator \n"
"df -h \necho $seperator \n"
"cat /proc/meminfo \necho $seperator \n"
"cat /sys/class/thermal/thermal_zone*/type \necho $seperator \n"
"cat /sys/class/thermal/thermal_zone*/temp";
const shellPath = '.serverbox.sh';
final cpuTempReg = RegExp(r'(x86_pkg_temp|cpu_thermal)');
final numReg = RegExp(r'\s{1,}');
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
import '../../core/extension/uint8list.dart';
import '../../core/provider_base.dart';
import '../../core/utils/server.dart';
import '../../locator.dart';
import '../model/server/cpu_status.dart';
import '../model/server/disk_info.dart';
import '../model/server/memory.dart';
import '../model/server/net_speed.dart';
import '../model/server/server.dart';
import '../model/server/server_private_info.dart';
import '../model/server/snippet.dart';
import '../model/server/tcp_status.dart';
import '../res/server_cmd.dart';
import '../res/status.dart';
import '../store/server.dart';
import '../store/setting.dart';
class ServerProvider extends BusyProvider {
List<ServerInfo> _servers = [];
List<ServerInfo> get servers => _servers;
List<Server> _servers = [];
List<Server> get servers => _servers;
final _TryLimiter _limiter = _TryLimiter();
Timer? _timer;
final logger = Logger('SERVER');
Memory get emptyMemory =>
Memory(total: 1, used: 0, free: 1, cache: 0, avail: 1);
NetSpeedPart get emptyNetSpeedPart => NetSpeedPart('', 0, 0, 0);
NetSpeed get emptyNetSpeed =>
NetSpeed([emptyNetSpeedPart], [emptyNetSpeedPart]);
CpuStatus get emptyCpuStatus => CpuStatus('cpu', 0, 0, 0, 0, 0, 0, 0);
Cpu2Status get emptyCpu2Status =>
Cpu2Status([emptyCpuStatus], [emptyCpuStatus], '');
ServerStatus get emptyStatus => ServerStatus(
emptyCpu2Status,
emptyMemory,
'Loading...',
'',
[DiskInfo('/', '/', 0, '0', '0', '0')],
TcpStatus(0, 0, 0, 0),
emptyNetSpeed);
final _logger = Logger('SERVER');
Future<void> loadLocalData() async {
setBusyState(true);
final infos = locator<ServerStore>().fetch();
_servers = List.generate(infos.length, (index) => genInfo(infos[index]));
_servers = List.generate(infos.length, (index) => genServer(infos[index]));
setBusyState(false);
notifyListeners();
}
ServerInfo genInfo(ServerPrivateInfo spi) {
return ServerInfo(
spi, emptyStatus, null, ServerConnectionState.disconnected);
Server genServer(ServerPrivateInfo spi) {
return Server(spi, initStatus, null, ServerState.disconnected);
}
Future<SSHClient> genClient(ServerPrivateInfo spi) async {
final socket = await SSHSocket.connect(spi.ip, spi.port);
if (spi.pubKeyId == null) {
return SSHClient(socket,
username: spi.user, onPasswordRequest: () => spi.pwd);
}
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
return SSHClient(socket,
username: spi.user,
identities: await compute(loadIndentity, key.privateKey));
}
Future<void> refreshData({ServerPrivateInfo? spi}) async {
Future<void> refreshData(
{ServerPrivateInfo? spi, bool onlyFailed = false}) async {
if (spi != null) {
_getData(spi);
await _getData(spi);
return;
}
await Future.wait(_servers.map((s) async {
await _getData(s.info);
if (onlyFailed) {
if (s.cs != ServerState.failed) return;
_limiter.resetTryTimes(s.spi.id);
}
await _getData(s.spi);
}));
}
@@ -128,8 +75,10 @@ class ServerProvider extends BusyProvider {
void setDisconnected() {
for (var i = 0; i < _servers.length; i++) {
_servers[i].connectionState = ServerConnectionState.disconnected;
_servers[i].cs = ServerState.disconnected;
}
_limiter.clear();
notifyListeners();
}
void closeServer({ServerPrivateInfo? spi}) {
@@ -140,170 +89,167 @@ class ServerProvider extends BusyProvider {
}
return;
}
final idx = _servers.indexWhere((e) => e.info == spi);
if (idx < 0) {
throw RangeError.index(idx, _servers);
}
final idx = getServerIdx(spi.id);
_servers[idx].client?.close();
_servers[idx].client = null;
}
void addServer(ServerPrivateInfo spi) {
_servers.add(genInfo(spi));
locator<ServerStore>().put(spi);
_servers.add(genServer(spi));
notifyListeners();
locator<ServerStore>().put(spi);
refreshData(spi: spi);
}
void delServer(ServerPrivateInfo info) {
final idx = _servers.indexWhere((s) => s.info == info);
if (idx == -1) return;
void delServer(String id) {
final idx = getServerIdx(id);
_servers[idx].client?.close();
_servers.removeAt(idx);
notifyListeners();
locator<ServerStore>().delete(info);
locator<ServerStore>().delete(id);
}
Future<void> updateServer(
ServerPrivateInfo old, ServerPrivateInfo newSpi) async {
final idx = _servers.indexWhere((e) => e.info == old);
final idx = _servers.indexWhere((e) => e.spi.id == old.id);
if (idx < 0) {
throw RangeError.index(idx, _servers);
}
_servers[idx].info = newSpi;
_servers[idx].spi = newSpi;
locator<ServerStore>().update(old, newSpi);
_servers[idx].client = await genClient(newSpi);
notifyListeners();
refreshData(spi: newSpi);
}
int getServerIdx(String id) {
final idx = _servers.indexWhere((s) => s.spi.id == id);
if (idx < 0) {
throw Exception('Server not found: $id');
}
return idx;
}
Server getServer(String id) => _servers[getServerIdx(id)];
Future<void> _getData(ServerPrivateInfo spi) async {
final s = _servers.firstWhere((element) => element.info == spi);
final state = s.connectionState;
if (state == ServerConnectionState.failed ||
state == ServerConnectionState.disconnected) {
s.connectionState = ServerConnectionState.connecting;
final sid = spi.id;
final s = getServer(sid);
final state = s.cs;
if (state.shouldConnect) {
if (!_limiter.shouldTry(sid)) {
s.cs = ServerState.failed;
notifyListeners();
return;
}
s.cs = ServerState.connecting;
notifyListeners();
final time1 = DateTime.now();
try {
// try to connect
final time1 = DateTime.now();
s.client = await genClient(spi);
final time2 = DateTime.now();
logger.info(
'Connected to [${spi.name}] in [${time2.difference(time1).toString()}].');
s.connectionState = ServerConnectionState.connected;
final writeResult = await s.client!
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath")
.string;
final spentTime = time2.difference(time1).inMilliseconds;
_logger.info('Connected to [$sid] in $spentTime ms.');
// after connected
s.cs = ServerState.connected;
final writeResult = await s.client!.run(installShellCmd).string;
// if write failed
if (writeResult.isNotEmpty) {
throw Exception(writeResult);
}
_limiter.resetTryTimes(sid);
} catch (e) {
s.connectionState = ServerConnectionState.failed;
s.status.failedInfo = '$e ## ';
logger.warning(e);
s.cs = ServerState.failed;
s.status.failedInfo = '$e';
_logger.warning(e);
} finally {
notifyListeners();
}
}
// if client is null, return
if (s.client == null) return;
// run script to get server status
final raw = await s.client!.run("sh $shellPath").string;
final segments = raw.split(seperator).map((e) => e.trim()).toList();
if (raw.isEmpty || segments.length == 1) {
s.connectionState = ServerConnectionState.failed;
s.cs = ServerState.failed;
if (s.status.failedInfo == null || s.status.failedInfo!.isEmpty) {
s.status.failedInfo = 'No data received';
s.status.failedInfo = 'Seperate segments failed, raw:\n$raw';
}
notifyListeners();
return;
}
// remove first empty segment
segments.removeAt(0);
try {
_getCPU(spi, segments[2], segments[7], segments[8]);
_getMem(spi, segments[6]);
_getSysVer(spi, segments[1]);
_getUpTime(spi, segments[3]);
_getDisk(spi, segments[5]);
_getTcp(spi, segments[4]);
_getNetSpeed(spi, segments[0]);
_getCPU(sid, segments[2], segments[7], segments[8]);
_getMem(sid, segments[6]);
_getSysVer(sid, segments[1]);
_getUpTime(sid, segments[3]);
_getDisk(sid, segments[5]);
_getTcp(sid, segments[4]);
_getNetSpeed(sid, segments[0]);
} catch (e) {
s.connectionState = ServerConnectionState.failed;
s.cs = ServerState.failed;
s.status.failedInfo = e.toString();
logger.warning(e);
_logger.warning(e);
rethrow;
} finally {
notifyListeners();
}
}
/// [raw] example:
/// Inter-| Receive | Transmit
/// face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
/// 1635752901
Future<void> _getNetSpeed(ServerPrivateInfo spi, String raw) async {
final info = _servers.firstWhere((e) => e.info == spi);
info.status.netSpeed.update(await compute(_parseNetSpeed, raw));
Future<void> _getNetSpeed(String id, String raw) async {
final net = await compute(parseNetSpeed, raw);
getServer(id).status.netSpeed.update(net);
}
void _getSysVer(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
void _getSysVer(String id, String raw) {
final s = raw.split('=');
if (s.length == 2) {
info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
final ver = s[1].replaceAll('"', '').replaceFirst('\n', '');
getServer(id).status.sysVer = ver;
}
}
Future<void> _getCPU(ServerPrivateInfo spi, String raw, String tempType,
String tempValue) async {
final info = _servers.firstWhere((e) => e.info == spi);
final cpus = await compute(_parseCPU, raw);
Future<void> _getCPU(
String id, String raw, String tempType, String tempValue) async {
final cpus = await compute(parseCPU, raw);
final temp = await compute(parseCPUTemp, [tempType, tempValue]);
if (cpus.isNotEmpty) {
info.status.cpu2Status
.update(cpus, await compute(_getCPUTemp, [tempType, tempValue]));
getServer(id).status.cpu.update(cpus, temp);
}
}
void _getUpTime(ServerPrivateInfo spi, String raw) {
_servers.firstWhere((e) => e.info == spi).status.uptime =
raw.split('up ')[1].split(', ')[0];
void _getUpTime(String id, String raw) {
getServer(id).status.uptime = raw.split('up ')[1].split(', ')[0];
}
Future<void> _getTcp(ServerPrivateInfo spi, String raw) async {
final info = _servers.firstWhere((e) => e.info == spi);
final status = await compute(_parseTcp, raw);
Future<void> _getTcp(String id, String raw) async {
final status = await compute(parseTcp, raw);
if (status != null) {
info.status.tcp = status;
getServer(id).status.tcp = status;
}
}
void _getDisk(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
final list = <DiskInfo>[];
final items = raw.split('\n');
for (var item in items) {
if (items.indexOf(item) == 0 || item.isEmpty) {
continue;
}
final vals = item.split(numReg);
list.add(DiskInfo(vals[0], vals[5],
int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3]));
}
info.status.disk = list;
Future<void> _getDisk(String id, String raw) async {
getServer(id).status.disk = await compute(parseDisk, raw);
}
Future<void> _getMem(ServerPrivateInfo spi, String raw) async {
final info = _servers.firstWhere((e) => e.info == spi);
final mem = await compute(_parseMem, raw);
info.status.memory = mem;
Future<void> _getMem(String id, String raw) async {
final s = getServer(id);
s.status.mem = await compute(parseMem, raw);
s.status.swap = await compute(parseSwap, raw);
}
Future<String?> runSnippet(String id, Snippet snippet) async {
final client =
_servers.firstWhere((element) => element.info.id == id).client;
final client = getServer(id).client;
if (client == null) {
return null;
}
@@ -311,95 +257,27 @@ class ServerProvider extends BusyProvider {
}
}
Memory _parseMem(String raw) {
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
final total = int.parse(
items.firstWhere((e) => e?.group(1) == 'MemTotal:')?.group(2) ?? '1');
final free = int.parse(
items.firstWhere((e) => e?.group(1) == 'MemFree:')?.group(2) ?? '0');
final cached = int.parse(
items.firstWhere((e) => e?.group(1) == 'Cached:')?.group(2) ?? '0');
final available = int.parse(
items.firstWhere((e) => e?.group(1) == 'MemAvailable:')?.group(2) ?? '0');
return Memory(
total: total,
used: total - available,
free: free,
cache: cached,
avail: available);
}
class _TryLimiter {
final Map<String, int> _triedTimes = {};
TcpStatus? _parseTcp(String raw) {
final lines = raw.split('\n');
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
orElse: () => '');
if (idx != '') {
final vals = idx.split(numReg);
return TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
}
return null;
}
List<CpuStatus> _parseCPU(String raw) {
final List<CpuStatus> cpus = [];
for (var item in raw.split('\n')) {
if (item == '') break;
final id = item.split(' ').first;
final matches = item.replaceFirst(id, '').trim().split(' ');
cpus.add(CpuStatus(
id,
int.parse(matches[0]),
int.parse(matches[1]),
int.parse(matches[2]),
int.parse(matches[3]),
int.parse(matches[4]),
int.parse(matches[5]),
int.parse(matches[6])));
}
return cpus;
}
String _getCPUTemp(List<String> segments) {
const noMatch = "/sys/class/thermal/thermal_zone*/type";
final type = segments[0];
final value = segments[1];
// Not support to get CPU temperature
if (value.contains(noMatch) ||
type.contains(noMatch) ||
value.isEmpty ||
type.isEmpty) {
return '';
}
final split = type.split('\n');
int idx = 0;
for (var item in split) {
if (item.contains(cpuTempReg)) {
break;
bool shouldTry(String id) {
final maxCount = locator<SettingStore>().maxRetryCount.fetch()!;
if (maxCount <= 0) {
return true;
}
idx++;
final times = _triedTimes[id] ?? 0;
if (times >= maxCount) {
return false;
}
_triedTimes[id] = times + 1;
return true;
}
final valueSplited = value.split('\n');
if (idx >= valueSplited.length) return '';
final temp = int.tryParse(valueSplited[idx].trim());
if (temp == null) return '';
return '${(temp / 1000).toStringAsFixed(1)}°C';
}
List<NetSpeedPart> _parseNetSpeed(String raw) {
final split = raw.split('\n');
final deviceCount = split.length - 3;
if (deviceCount < 1) return [];
final time = int.parse(split[split.length - 1]);
final results = <NetSpeedPart>[];
for (int idx = 2; idx < deviceCount; idx++) {
final data = split[idx].trim().split(':');
final device = data.first;
final bytes = data.last.trim().split(' ');
bytes.removeWhere((element) => element == '');
final bytesIn = int.parse(bytes.first);
final bytesOut = int.parse(bytes[8]);
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
void resetTryTimes(String id) {
_triedTimes[id] = 0;
}
void clear() {
_triedTimes.clear();
}
return results;
}

View File

@@ -1,8 +1,9 @@
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/sftp/download_status.dart';
import 'package:toolbox/data/model/sftp/download_worker.dart';
class SftpDownloadProvider extends ProviderBase {
import '../model/sftp/download_item.dart';
import '../model/sftp/download_status.dart';
class SftpProvider extends ProviderBase {
final List<SftpDownloadStatus> _status = [];
List<SftpDownloadStatus> get status => _status;

View File

@@ -0,0 +1,29 @@
import 'package:flutter/widgets.dart';
import 'package:xterm/core.dart';
class VirtualKeyboard extends TerminalInputHandler with ChangeNotifier {
VirtualKeyboard();
bool ctrl = false;
bool alt = false;
void reset(TerminalKeyboardEvent e) {
if (e.ctrl) {
ctrl = false;
}
if (e.alt) {
alt = false;
}
notifyListeners();
}
@override
String? call(TerminalKeyboardEvent event) {
final e = event.copyWith(
ctrl: event.ctrl || ctrl,
alt: event.alt || alt,
);
reset(e);
return defaultInputHandler.call(e);
}
}

View File

@@ -2,9 +2,8 @@
class BuildData {
static const String name = "ServerBox";
static const int build = 187;
static const String engine =
"Flutter 3.7.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision b06b8b2710 (4 days ago) • 2023-01-23 16:55:55 -0800\nEngine • revision b24591ed32\nTools • Dart 2.19.0 • DevTools 2.20.1\n";
static const String buildAt = "2023-01-28 13:54:17.985459";
static const int modifications = 13;
static const int build = 243;
static const String engine = "Flutter 3.7.7 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 2ad6cd72c0 (13 days ago) • 2023-03-08 09:41:59 -0800\nEngine • revision 1837b5be5f\nTools • Dart 2.19.4 • DevTools 2.20.1\n";
static const String buildAt = "2023-03-21 15:18:42.802605";
static const int modifications = 5;
}

View File

@@ -1,21 +1,11 @@
import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart';
Color get primaryColor => Color(locator<SettingStore>().primaryColor.fetch()!);
import '../model/app/dynamic_color.dart';
class DynamicColor {
/// 白天模式显示的颜色
Color light;
Color primaryColor = Color(locator<SettingStore>().primaryColor.fetch()!);
/// 暗黑模式显示的颜色
Color dark;
DynamicColor(this.light, this.dark);
resolve(BuildContext context) => isDarkMode(context) ? dark : light;
}
final mainColor = DynamicColor(Colors.black87, Colors.white70);
final contentColor = DynamicColor(Colors.black87, Colors.white70);
final bgColor = DynamicColor(Colors.white, Colors.black);
final progressColor = DynamicColor(Colors.grey.shade100, Colors.white10);

View File

@@ -1,3 +0,0 @@
import 'package:flutter/widgets.dart';
final appIcon = Image.asset('assets/app_icon.png');

View File

@@ -1,27 +1,7 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class DropdownBtnItem {
final String text;
final IconData icon;
const DropdownBtnItem({
required this.text,
required this.icon,
});
Widget get build => Row(
children: [
Icon(icon, color: primaryColor),
const SizedBox(
width: 10,
),
Text(
text,
),
],
);
}
import '../../view/widget/dropdown_menu.dart';
class ServerTabMenuItems {
static const List<DropdownBtnItem> firstItems = [sftp, snippet, pkg, docker];
@@ -42,3 +22,22 @@ class DockerMenuItems {
static const start = DropdownBtnItem(text: 'Start', icon: Icons.play_arrow);
static const stop = DropdownBtnItem(text: 'Stop', icon: Icons.stop);
}
String getDropdownBtnText(S s, String text) {
switch (text) {
case 'Snippet':
return s.snippet;
case 'Pkg':
return s.pkg;
case 'Remove':
return s.delete;
case 'Start':
return s.start;
case 'Stop':
return s.stop;
case 'Edit':
return s.edit;
default:
return text;
}
}

8
lib/data/res/misc.dart Normal file
View File

@@ -0,0 +1,8 @@
/// RegExp for number
final numReg = RegExp(r'\s{1,}');
/// Private Key max allowed size is 20kb
const privateKeyMaxSize = 20 * 1024;
/// Max debug log lines
const maxDebugLogLines = 100;

View File

@@ -1,3 +0,0 @@
import 'package:flutter/material.dart';
const roundRectCardPadding = EdgeInsets.symmetric(horizontal: 17);

View File

@@ -8,3 +8,8 @@ Future<Directory> get sftpDownloadDir async {
final dir = Directory('${(await docDir).path}/sftp');
return dir.create(recursive: true);
}
Future<Directory> get fontDir async {
final dir = Directory('${(await docDir).path}/font');
return dir.create(recursive: true);
}

View File

@@ -0,0 +1,32 @@
import 'build_data.dart';
const seperator = 'SrvBox';
const serverBoxDir = r'$HOME/.config/server_box';
const shellPath = '$serverBoxDir/mobile_app.sh';
const installShellCmd =
"mkdir -p $serverBoxDir && echo '$shellCmd' > $shellPath && chmod +x $shellPath";
const shellCmd = """
# Script for app `${BuildData.name}`
# Delete this file while app is running will cause app crash
export LANG=en_US.utf-8
echo $seperator
cat /proc/net/dev && date +%s
echo $seperator
cat /etc/os-release | grep PRETTY_NAME
echo $seperator
cat /proc/stat | grep cpu
echo $seperator
uptime
echo $seperator
cat /proc/net/snmp
echo $seperator
df -h
echo $seperator
cat /proc/meminfo
echo $seperator
cat /sys/class/thermal/thermal_zone*/type
echo $seperator
cat /sys/class/thermal/thermal_zone*/temp
""";

55
lib/data/res/status.dart Normal file
View File

@@ -0,0 +1,55 @@
import '../model/server/cpu_status.dart';
import '../model/server/disk_info.dart';
import '../model/server/memory.dart';
import '../model/server/net_speed.dart';
import '../model/server/server_status.dart';
import '../model/server/tcp_status.dart';
Memory get _initMemory => Memory(
total: 1,
used: 0,
free: 1,
cache: 0,
avail: 1,
);
OneTimeCpuStatus get _initOneTimeCpuStatus => OneTimeCpuStatus(
'cpu',
0,
0,
0,
0,
0,
0,
0,
);
CpuStatus get initCpuStatus => CpuStatus(
[_initOneTimeCpuStatus],
[_initOneTimeCpuStatus],
'',
);
NetSpeedPart get _initNetSpeedPart => NetSpeedPart(
'',
BigInt.zero,
BigInt.zero,
BigInt.zero,
);
NetSpeed get initNetSpeed => NetSpeed(
[_initNetSpeedPart],
[_initNetSpeedPart],
);
Swap get _initSwap => Swap(
total: 1,
used: 0,
free: 1,
cached: 0,
);
ServerStatus get initStatus => ServerStatus(
cpu: initCpuStatus,
mem: _initMemory,
sysVer: 'Loading...',
uptime: '',
disk: [DiskInfo('/', '/', 0, '0', '0', '0')],
tcp: TcpStatus(0, 0, 0, 0),
netSpeed: initNetSpeed,
swap: _initSwap,
);

148
lib/data/res/terminal.dart Normal file
View File

@@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/model/ssh/terminal_color.dart';
const termDarkTheme = TerminalUITheme(
cursor: Color(0XAAAEAFAD),
selection: Color(0XAAAEAFAD),
foreground: Color(0XFFCCCCCC),
background: Colors.black,
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),
);
const termLightTheme = TerminalUITheme(
cursor: Color(0XFFAEAFAD),
selection: Color.fromARGB(102, 174, 175, 173),
foreground: Color(0XFF000000),
background: Color(0XFFFFFFFF),
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),
);
class VGATerminalColor extends TerminalColors {
VGATerminalColor()
: super(
TerminalColorsPlatform.vga,
const Color.fromARGB(255, 170, 0, 0),
const Color.fromARGB(255, 0, 170, 0),
const Color.fromARGB(255, 170, 85, 0),
const Color.fromARGB(255, 0, 0, 170),
const Color.fromARGB(255, 170, 0, 170),
const Color.fromARGB(255, 0, 170, 170),
const Color.fromARGB(255, 170, 170, 170),
const Color.fromARGB(255, 85, 85, 85),
const Color.fromARGB(255, 255, 85, 85),
const Color.fromARGB(255, 85, 255, 85),
const Color.fromARGB(255, 255, 255, 85),
const Color.fromARGB(255, 85, 85, 255),
const Color.fromARGB(255, 255, 85, 255),
const Color.fromARGB(255, 85, 255, 255),
);
}
class CMDTerminalColor extends TerminalColors {
CMDTerminalColor()
: super(
TerminalColorsPlatform.cmd,
const Color.fromARGB(255, 128, 0, 0),
const Color.fromARGB(255, 0, 128, 0),
const Color.fromARGB(255, 128, 128, 0),
const Color.fromARGB(255, 0, 0, 128),
const Color.fromARGB(255, 128, 0, 128),
const Color.fromARGB(255, 0, 128, 128),
const Color.fromARGB(255, 192, 192, 192),
const Color.fromARGB(255, 128, 128, 128),
const Color.fromARGB(255, 255, 0, 0),
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 255, 255, 0),
const Color.fromARGB(255, 0, 0, 255),
const Color.fromARGB(255, 255, 0, 255),
const Color.fromARGB(255, 0, 255, 255),
);
}
class MacOSTerminalColor extends TerminalColors {
MacOSTerminalColor()
: super(
TerminalColorsPlatform.macOS,
const Color.fromARGB(255, 194, 54, 33),
const Color.fromARGB(255, 37, 188, 36),
const Color.fromARGB(255, 173, 173, 39),
const Color.fromARGB(255, 73, 46, 225),
const Color.fromARGB(255, 211, 56, 211),
const Color.fromARGB(255, 51, 187, 200),
const Color.fromARGB(255, 203, 204, 205),
const Color.fromARGB(255, 129, 131, 131),
const Color.fromARGB(255, 252, 57, 31),
const Color.fromARGB(255, 49, 231, 34),
const Color.fromARGB(255, 234, 236, 35),
const Color.fromARGB(255, 88, 51, 255),
const Color.fromARGB(255, 249, 53, 248),
const Color.fromARGB(255, 20, 240, 240),
brightWhite: const Color.fromARGB(255, 233, 235, 235));
}
class PuttyTerminalColor extends TerminalColors {
PuttyTerminalColor()
: super(
TerminalColorsPlatform.putty,
const Color.fromARGB(255, 187, 0, 0),
const Color.fromARGB(255, 0, 187, 0),
const Color.fromARGB(255, 187, 187, 0),
const Color.fromARGB(255, 0, 0, 187),
const Color.fromARGB(255, 187, 0, 187),
const Color.fromARGB(255, 0, 187, 187),
const Color.fromARGB(255, 187, 187, 187),
const Color.fromARGB(255, 85, 85, 85),
const Color.fromARGB(255, 255, 85, 85),
const Color.fromARGB(255, 85, 255, 85),
const Color.fromARGB(255, 255, 255, 85),
const Color.fromARGB(255, 85, 85, 255),
const Color.fromARGB(255, 255, 85, 255),
const Color.fromARGB(255, 85, 255, 255),
);
}
class XTermTerminalColor extends TerminalColors {
XTermTerminalColor()
: super(
TerminalColorsPlatform.xterm,
const Color.fromARGB(255, 205, 0, 0),
const Color.fromARGB(255, 0, 205, 0),
const Color.fromARGB(255, 205, 205, 0),
const Color.fromARGB(255, 0, 0, 238),
const Color.fromARGB(255, 205, 0, 205),
const Color.fromARGB(255, 0, 205, 205),
const Color.fromARGB(255, 229, 229, 229),
const Color.fromARGB(255, 127, 127, 127),
const Color.fromARGB(255, 255, 0, 0),
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 255, 255, 0),
const Color.fromARGB(255, 92, 92, 255),
const Color.fromARGB(255, 255, 0, 255),
const Color.fromARGB(255, 0, 255, 255),
);
}
class UbuntuTerminalColor extends TerminalColors {
UbuntuTerminalColor()
: super(
TerminalColorsPlatform.ubuntu,
const Color.fromARGB(255, 222, 56, 43),
const Color.fromARGB(255, 57, 181, 74),
const Color.fromARGB(255, 255, 199, 6),
const Color.fromARGB(255, 0, 111, 184),
const Color.fromARGB(255, 118, 38, 113),
const Color.fromARGB(255, 44, 181, 233),
const Color.fromARGB(255, 204, 204, 204),
const Color.fromARGB(255, 128, 128, 128),
const Color.fromARGB(255, 255, 0, 0),
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 255, 255, 0),
const Color.fromARGB(255, 0, 0, 255),
const Color.fromARGB(255, 255, 0, 255),
const Color.fromARGB(255, 0, 255, 255),
);
}

View File

@@ -1,54 +0,0 @@
import 'package:flutter/material.dart';
import 'package:xterm/ui.dart';
const termDarkTheme = TerminalTheme(
cursor: Color(0XAAAEAFAD),
selection: Color(0XAAAEAFAD),
foreground: Color(0XFFCCCCCC),
background: Colors.black,
black: Color(0XFF000000),
red: Color(0XFFCD3131),
green: Color(0XFF0DBC79),
yellow: Color(0XFFE5E510),
blue: Color(0XFF2472C8),
magenta: Color(0XFFBC3FBC),
cyan: Color(0XFF11A8CD),
white: Color(0XFFE5E5E5),
brightBlack: Color(0XFF666666),
brightRed: Color(0XFFF14C4C),
brightGreen: Color(0XFF23D18B),
brightYellow: Color(0XFFF5F543),
brightBlue: Color(0XFF3B8EEA),
brightMagenta: Color(0XFFD670D6),
brightCyan: Color(0XFF29B8DB),
brightWhite: Color(0XFFFFFFFF),
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),
);
const termLightTheme = TerminalTheme(
cursor: Color(0XFFAEAFAD),
selection: Color(0XFFAEAFAD),
foreground: Color(0XFF000000),
background: Color(0XFFFFFFFF),
black: Color(0XFF000000),
red: Color(0XFFCD3131),
green: Color(0XFF0DBC79),
yellow: Color(0XFFE5E510),
blue: Color(0XFF2472C8),
magenta: Color(0XFFBC3FBC),
cyan: Color(0XFF11A8CD),
white: Color(0XFFE5E5E5),
brightBlack: Color(0XFF666666),
brightRed: Color(0XFFF14C4C),
brightGreen: Color(0XFF23D18B),
brightYellow: Color(0XFFF5F543),
brightBlue: Color(0XFF3B8EEA),
brightMagenta: Color(0XFFD670D6),
brightCyan: Color(0XFF29B8DB),
brightWhite: Color(0XFFFFFFFF),
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),
);

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
/// Font style
const textSize18 = TextStyle(fontSize: 18);
const textSize11 = TextStyle(fontSize: 11);
const textSize13 = TextStyle(fontSize: 13);
@@ -7,3 +9,17 @@ const textSize13Grey = TextStyle(color: Colors.grey, fontSize: 13);
const textSize27 = TextStyle(fontSize: 27);
const grey = TextStyle(color: Colors.grey);
/// Icon
final appIcon = Image.asset('assets/app_icon.png');
/// Padding
const roundRectCardPadding = EdgeInsets.symmetric(horizontal: 17, vertical: 13);
/// SizedBox
const height13 = SizedBox(height: 13);
const width13 = SizedBox(width: 13);
const width7 = SizedBox(width: 7);

View File

@@ -2,6 +2,13 @@ const backendUrl = 'https://res.lolli.tech';
const baseUrl = '$backendUrl/toolbox';
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
const myGithub = 'https://github.com/lollipopkit';
const rainSunMeGithub = 'https://github.com/RainSunMe';
const fectureGithub = 'https://github.com/fecture';
const issueUrl = '$myGithub/flutter_server_box/issues';
// Thanks
const thanksMap = {
'RainSunMe': 'https://github.com/RainSunMe',
'fecture': 'https://github.com/fecture',
'Tao173': 'https://github.com/Tao173',
'QingAnLe': 'https://github.com/QingAnLe',
'wxdjs': 'https://github.com/wxdjs',
};

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:xterm/core.dart';
import '../model/ssh/virtual_key.dart';
var virtualKeys = [
VirtualKey('Esc', key: TerminalKey.escape),
VirtualKey('Alt', key: TerminalKey.alt, toggleable: true),
VirtualKey('Home', key: TerminalKey.home),
VirtualKey('Up', key: TerminalKey.arrowUp, icon: Icons.arrow_upward),
VirtualKey('End', key: TerminalKey.end),
// VirtualKey(
// 'Del',
// key: TerminalKey.delete,
// ),
VirtualKey('Paste', func: VirtualKeyFunc.paste, icon: Icons.paste),
VirtualKey('Tab', key: TerminalKey.tab),
VirtualKey('Ctrl', key: TerminalKey.control, toggleable: true),
VirtualKey('Left', key: TerminalKey.arrowLeft, icon: Icons.arrow_back),
VirtualKey('Down', key: TerminalKey.arrowDown, icon: Icons.arrow_downward),
VirtualKey('Right', key: TerminalKey.arrowRight, icon: Icons.arrow_forward),
VirtualKey(
'IME',
func: VirtualKeyFunc.toggleIME,
icon: Icons.keyboard_hide,
),
];

View File

@@ -18,14 +18,15 @@ class ServerStore extends PersistentStore {
return ss;
}
void delete(ServerPrivateInfo s) {
box.delete(s.id);
void delete(String id) {
box.delete(id);
}
void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) {
if (!have(old)) {
throw Exception('Old ServerPrivateInfo not found');
}
delete(old.id);
put(newInfo);
}

View File

@@ -6,7 +6,7 @@ class SettingStore extends PersistentStore {
property('primaryColor', defaultValue: Colors.deepPurpleAccent.value);
StoreProperty<int> get serverStatusUpdateInterval =>
property('serverStatusUpdateInterval', defaultValue: 5);
property('serverStatusUpdateInterval', defaultValue: 3);
/// Lanch page idx
StoreProperty<int> get launchPage => property('launchPage', defaultValue: 0);
@@ -18,4 +18,21 @@ class SettingStore extends PersistentStore {
/// Show logo on server detail page
StoreProperty<bool> get showDistLogo =>
property('showDistLogo', defaultValue: true);
/// First time to use SSH term
StoreProperty<bool> get firstTimeUseSshTerm =>
property('firstTimeUseSshTerm', defaultValue: true);
StoreProperty<int> get termColorIdx =>
property('termColorIdx', defaultValue: 0);
/// Max retry count when connect to server
StoreProperty<int> get maxRetryCount =>
property('maxRetryCount', defaultValue: 7);
/// Night mode: 0 -> auto, 1 -> light, 2 -> dark
StoreProperty<int> get themeMode => property('themeMode', defaultValue: 0);
/// Font file path
StoreProperty<String> get fontPath => property('fontPath');
}

View File

@@ -1,67 +0,0 @@
// 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:flutter/foundation.dart';
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 SynchronousFuture(null),
'zh': () => new SynchronousFuture(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) {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new SynchronousFuture(false);
}
var lib = _deferredLibraries[availableLocale];
lib == null ? new SynchronousFuture(false) : lib();
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new SynchronousFuture(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

@@ -1,263 +0,0 @@
// 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(fileName) => "Download [${fileName}] to local?";
static String m1(count) => "${count} images";
static String m2(runningCount, stoppedCount) =>
"${runningCount} running, ${stoppedCount} container stopped.";
static String m3(count) => "${count} container running.";
static String m4(percent, size) => "${percent}% of ${size}";
static String m5(count) => "Found ${count} update";
static String m6(code) => "request failed, status code: ${code}";
static String m7(url) =>
"Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don\'t have the above issues, please submit an issue on ${url}.";
static String m8(myGithub) => "\nMade with ❤️ by ${myGithub}";
static String m9(url) => "Please report bugs on ${url}";
static String m10(date) => "Are you sure to restore from ${date} ?";
static String m11(time) => "Spent time: ${time}";
static String m12(url) =>
"This function is now in the experimental stage. \nPlease report bugs on ${url} or join our development.";
static String m13(name) => "Are you sure to delete [${name}]?";
static String m14(server) => "Are you sure to delete server [${server}]?";
static String m15(build) => "Found: v1.0.${build}, click to update";
static String m16(build) => "Current: v1.0.${build}";
static String m17(build) => "Current: v1.0.${build}, is up to date";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks": MessageLookupByLibrary.simpleMessage(
"\nAll rights reserved.\n\nThanks to the following people who participated in the test."),
"addAServer": MessageLookupByLibrary.simpleMessage("add a server"),
"addOne": MessageLookupByLibrary.simpleMessage("Add one"),
"addPrivateKey":
MessageLookupByLibrary.simpleMessage("Add private key"),
"alreadyLastDir":
MessageLookupByLibrary.simpleMessage("Already in last directory."),
"appPrimaryColor":
MessageLookupByLibrary.simpleMessage("App primary color"),
"attention": MessageLookupByLibrary.simpleMessage("Attention"),
"backDir": MessageLookupByLibrary.simpleMessage("Back"),
"backup": MessageLookupByLibrary.simpleMessage("Backup"),
"backupTip": MessageLookupByLibrary.simpleMessage(
"The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting)."),
"backupVersionNotMatch": MessageLookupByLibrary.simpleMessage(
"Backup version is not match."),
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
"choose": MessageLookupByLibrary.simpleMessage("Choose"),
"chooseDestination":
MessageLookupByLibrary.simpleMessage("Choose destination"),
"choosePrivateKey":
MessageLookupByLibrary.simpleMessage("Choose private key"),
"clear": MessageLookupByLibrary.simpleMessage("Clear"),
"clickSee": MessageLookupByLibrary.simpleMessage("Click here"),
"close": MessageLookupByLibrary.simpleMessage("Close"),
"cmd": MessageLookupByLibrary.simpleMessage("Command"),
"containerStatus":
MessageLookupByLibrary.simpleMessage("Container status"),
"convert": MessageLookupByLibrary.simpleMessage("Convert"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"copyPath": MessageLookupByLibrary.simpleMessage("Copy path"),
"createFile": MessageLookupByLibrary.simpleMessage("Create file"),
"createFolder": MessageLookupByLibrary.simpleMessage("Create folder"),
"currentMode": MessageLookupByLibrary.simpleMessage("Current Mode"),
"debug": MessageLookupByLibrary.simpleMessage("Debug"),
"decode": MessageLookupByLibrary.simpleMessage("Decode"),
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
"disconnected": MessageLookupByLibrary.simpleMessage("Disconnected"),
"dl2Local": m0,
"dockerContainerName":
MessageLookupByLibrary.simpleMessage("Container name"),
"dockerEditHost":
MessageLookupByLibrary.simpleMessage("Edit DOCKER_HOST"),
"dockerEmptyRunningItems": MessageLookupByLibrary.simpleMessage(
"No running container. \nIt may be that the env DOCKER_HOST is not read correctly. You can found it by running `echo \$DOCKER_HOST` in terminal."),
"dockerImage": MessageLookupByLibrary.simpleMessage("Image"),
"dockerImagesFmt": m1,
"dockerNotInstalled":
MessageLookupByLibrary.simpleMessage("Docker not installed"),
"dockerStatusRunningAndStoppedFmt": m2,
"dockerStatusRunningFmt": m3,
"download": MessageLookupByLibrary.simpleMessage("Download"),
"downloadFinished":
MessageLookupByLibrary.simpleMessage("Download finished"),
"downloadStatus": m4,
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"encode": MessageLookupByLibrary.simpleMessage("Encode"),
"error": MessageLookupByLibrary.simpleMessage("Error"),
"exampleName": MessageLookupByLibrary.simpleMessage("Example name"),
"experimentalFeature":
MessageLookupByLibrary.simpleMessage("Experimental feature"),
"export": MessageLookupByLibrary.simpleMessage("Export"),
"extraArgs": MessageLookupByLibrary.simpleMessage("Extra args"),
"feedback": MessageLookupByLibrary.simpleMessage("Feedback"),
"feedbackOnGithub": MessageLookupByLibrary.simpleMessage(
"If you have any questions, please feedback on Github."),
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage(
"These fields must not be empty."),
"files": MessageLookupByLibrary.simpleMessage("Files"),
"foundNUpdate": m5,
"go": MessageLookupByLibrary.simpleMessage("Go"),
"goSftpDlPage":
MessageLookupByLibrary.simpleMessage("Go to SFTP download page?"),
"goto": MessageLookupByLibrary.simpleMessage("Go to"),
"host": MessageLookupByLibrary.simpleMessage("Host"),
"httpFailedWithCode": m6,
"imagesList": MessageLookupByLibrary.simpleMessage("Images list"),
"import": MessageLookupByLibrary.simpleMessage("Import"),
"importAndExport":
MessageLookupByLibrary.simpleMessage("Import and Export"),
"inputDomainHere":
MessageLookupByLibrary.simpleMessage("Input Domain here"),
"install": MessageLookupByLibrary.simpleMessage("install"),
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
"Please https://docs.docker.com/engine/install docker first."),
"invalidJson": MessageLookupByLibrary.simpleMessage("Invalid JSON"),
"invalidVersion":
MessageLookupByLibrary.simpleMessage("Invalid version"),
"invalidVersionHelp": m7,
"isBusy": MessageLookupByLibrary.simpleMessage("Is busy now"),
"keepForeground":
MessageLookupByLibrary.simpleMessage("Keep app foreground!"),
"keyAuth": MessageLookupByLibrary.simpleMessage("Key Auth"),
"lastTry": MessageLookupByLibrary.simpleMessage("Last try!"),
"launchPage": MessageLookupByLibrary.simpleMessage("Launch page"),
"license": MessageLookupByLibrary.simpleMessage("License"),
"loadingFiles":
MessageLookupByLibrary.simpleMessage("Loading files..."),
"loss": MessageLookupByLibrary.simpleMessage("loss"),
"madeWithLove": m8,
"max": MessageLookupByLibrary.simpleMessage("max"),
"min": MessageLookupByLibrary.simpleMessage("min"),
"ms": MessageLookupByLibrary.simpleMessage("ms"),
"name": MessageLookupByLibrary.simpleMessage("Name"),
"newContainer": MessageLookupByLibrary.simpleMessage("New container"),
"noClient": MessageLookupByLibrary.simpleMessage("No client"),
"noInterface": MessageLookupByLibrary.simpleMessage("No interface"),
"noResult": MessageLookupByLibrary.simpleMessage("No result"),
"noSavedPrivateKey":
MessageLookupByLibrary.simpleMessage("No saved private keys."),
"noSavedSnippet":
MessageLookupByLibrary.simpleMessage("No saved snippets."),
"noServerAvailable":
MessageLookupByLibrary.simpleMessage("No server available."),
"noUpdateAvailable":
MessageLookupByLibrary.simpleMessage("No update available"),
"ok": MessageLookupByLibrary.simpleMessage("OK"),
"onServerDetailPage":
MessageLookupByLibrary.simpleMessage("On server detail page"),
"open": MessageLookupByLibrary.simpleMessage("Open"),
"path": MessageLookupByLibrary.simpleMessage("Path"),
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("Avg:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage(
"Please input a target IP/domain."),
"pingNoServer": MessageLookupByLibrary.simpleMessage(
"No server to ping.\nPlease add a server in server tab."),
"platformNotSupportUpdate": MessageLookupByLibrary.simpleMessage(
"Current platform does not support in app update.\nPlease build from source and install it."),
"plzEnterHost":
MessageLookupByLibrary.simpleMessage("Please enter host."),
"plzSelectKey":
MessageLookupByLibrary.simpleMessage("Please select a key."),
"port": MessageLookupByLibrary.simpleMessage("Port"),
"preview": MessageLookupByLibrary.simpleMessage("Preview"),
"privateKey": MessageLookupByLibrary.simpleMessage("Private Key"),
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
"rename": MessageLookupByLibrary.simpleMessage("Rename"),
"reportBugsOnGithubIssue": m9,
"restore": MessageLookupByLibrary.simpleMessage("Restore"),
"restoreSuccess": MessageLookupByLibrary.simpleMessage(
"Restore success. Restart app to apply."),
"restoreSureWithDate": m10,
"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"),
"sftpDlPrepare":
MessageLookupByLibrary.simpleMessage("Preparing to connect..."),
"sftpNoDownloadTask":
MessageLookupByLibrary.simpleMessage("No download task."),
"sftpSSHConnected":
MessageLookupByLibrary.simpleMessage("SFTP Connected"),
"showDistLogo":
MessageLookupByLibrary.simpleMessage("Show distribution logo"),
"snippet": MessageLookupByLibrary.simpleMessage("Snippet"),
"spentTime": m11,
"sshTip": m12,
"start": MessageLookupByLibrary.simpleMessage("Start"),
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
"sureDelete": m13,
"sureNoPwd": MessageLookupByLibrary.simpleMessage(
"Are you sure to use no password?"),
"sureToDeleteServer": m14,
"ttl": MessageLookupByLibrary.simpleMessage("ttl"),
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
"unkownConvertMode":
MessageLookupByLibrary.simpleMessage("Unknown convert mode"),
"update": MessageLookupByLibrary.simpleMessage("Update"),
"updateAll": MessageLookupByLibrary.simpleMessage("Update all"),
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
"You set to 0, will not update automatically.\nCan\'t calculate CPU status."),
"updateServerStatusInterval": MessageLookupByLibrary.simpleMessage(
"Server status update interval"),
"upsideDown": MessageLookupByLibrary.simpleMessage("Upside Down"),
"urlOrJson": MessageLookupByLibrary.simpleMessage("URL or JSON"),
"user": MessageLookupByLibrary.simpleMessage("User"),
"versionHaveUpdate": m15,
"versionUnknownUpdate": m16,
"versionUpdated": m17,
"waitConnection": MessageLookupByLibrary.simpleMessage(
"Please wait for the connection to be established."),
"willTakEeffectImmediately":
MessageLookupByLibrary.simpleMessage("Will take effect immediately")
};
}

View File

@@ -1,228 +0,0 @@
// 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(fileName) => "下载 [${fileName}] 到本地?";
static String m1(count) => "${count} 个镜像";
static String m2(runningCount, stoppedCount) =>
"${runningCount}个正在运行, ${stoppedCount}个已停止";
static String m3(count) => "${count}个容器正在运行";
static String m4(percent, size) => "${size}${percent}%";
static String m5(count) => "找到 ${count} 个更新";
static String m6(code) => "请求失败, 状态码: ${code}";
static String m7(url) =>
"请确保正确安装了docker或者使用的非自编译版本。如果没有以上问题请在 ${url} 提交问题。";
static String m8(myGithub) => "\n用❤️制作 by ${myGithub}";
static String m9(url) => "请到 ${url} 提交问题";
static String m10(date) => "确定恢复 ${date} 的备份吗?";
static String m11(time) => "耗时: ${time}";
static String m12(url) => "该功能目前处于测试阶段,请在 ${url} 反馈问题,或者加入我们开发。";
static String m13(name) => "确定删除[${name}]";
static String m14(server) => "你确定要删除服务器 [${server}] 吗?";
static String m15(build) => "找到新版本v1.0.${build}, 点击更新";
static String m16(build) => "当前v1.0.${build}";
static String m17(build) => "当前v1.0.${build}, 已是最新版本";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks":
MessageLookupByLibrary.simpleMessage("\n保留所有权利。\n\n感谢以下参与软件测试的各位。"),
"addAServer": MessageLookupByLibrary.simpleMessage("添加服务器"),
"addOne": MessageLookupByLibrary.simpleMessage("前去新增"),
"addPrivateKey": MessageLookupByLibrary.simpleMessage("添加一个私钥"),
"alreadyLastDir": MessageLookupByLibrary.simpleMessage("已经是最上层目录了"),
"appPrimaryColor": MessageLookupByLibrary.simpleMessage("App主要色"),
"attention": MessageLookupByLibrary.simpleMessage("注意"),
"backDir": MessageLookupByLibrary.simpleMessage("返回上一级"),
"backup": MessageLookupByLibrary.simpleMessage("备份"),
"backupTip": MessageLookupByLibrary.simpleMessage(
"导出的数据仅进行了简单加密,请妥善保管。\n除了设置项,恢复的数据不会覆盖现有数据。"),
"backupVersionNotMatch":
MessageLookupByLibrary.simpleMessage("备份版本不匹配,无法恢复"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
"choose": MessageLookupByLibrary.simpleMessage("选择"),
"chooseDestination": MessageLookupByLibrary.simpleMessage("选择目标"),
"choosePrivateKey": MessageLookupByLibrary.simpleMessage("选择私钥"),
"clear": MessageLookupByLibrary.simpleMessage("清除"),
"clickSee": MessageLookupByLibrary.simpleMessage("点击查看"),
"close": MessageLookupByLibrary.simpleMessage("关闭"),
"cmd": MessageLookupByLibrary.simpleMessage("命令"),
"containerStatus": MessageLookupByLibrary.simpleMessage("容器状态"),
"convert": MessageLookupByLibrary.simpleMessage("转换"),
"copy": MessageLookupByLibrary.simpleMessage("复制到剪切板"),
"copyPath": MessageLookupByLibrary.simpleMessage("复制路径"),
"createFile": MessageLookupByLibrary.simpleMessage("创建文件"),
"createFolder": MessageLookupByLibrary.simpleMessage("创建文件夹"),
"currentMode": MessageLookupByLibrary.simpleMessage("当前模式"),
"debug": MessageLookupByLibrary.simpleMessage("调试"),
"decode": MessageLookupByLibrary.simpleMessage("解码"),
"delete": MessageLookupByLibrary.simpleMessage("删除"),
"disconnected": MessageLookupByLibrary.simpleMessage("连接断开"),
"dl2Local": m0,
"dockerContainerName": MessageLookupByLibrary.simpleMessage("容器名"),
"dockerEditHost":
MessageLookupByLibrary.simpleMessage("编辑 DOCKER_HOST"),
"dockerEmptyRunningItems": MessageLookupByLibrary.simpleMessage(
"没有正在运行的容器。\n这可能是因为环境变量 DOCKER_HOST 没有被正确读取。你可以通过在终端内运行 `echo \$DOCKER_HOST` 来获取。"),
"dockerImage": MessageLookupByLibrary.simpleMessage("镜像"),
"dockerImagesFmt": m1,
"dockerNotInstalled": MessageLookupByLibrary.simpleMessage("Docker未安装"),
"dockerStatusRunningAndStoppedFmt": m2,
"dockerStatusRunningFmt": m3,
"download": MessageLookupByLibrary.simpleMessage("下载"),
"downloadFinished": MessageLookupByLibrary.simpleMessage("下载完成!"),
"downloadStatus": m4,
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"encode": MessageLookupByLibrary.simpleMessage("编码"),
"error": MessageLookupByLibrary.simpleMessage("出错了"),
"exampleName": MessageLookupByLibrary.simpleMessage("名称示例"),
"experimentalFeature": MessageLookupByLibrary.simpleMessage("实验性功能"),
"export": MessageLookupByLibrary.simpleMessage("导出"),
"extraArgs": MessageLookupByLibrary.simpleMessage("额外参数"),
"feedback": MessageLookupByLibrary.simpleMessage("反馈"),
"feedbackOnGithub":
MessageLookupByLibrary.simpleMessage("如果你有任何问题请在GitHub反馈"),
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage("这些输入框不能为空。"),
"files": MessageLookupByLibrary.simpleMessage("文件"),
"foundNUpdate": m5,
"go": MessageLookupByLibrary.simpleMessage("开始"),
"goSftpDlPage": MessageLookupByLibrary.simpleMessage("前往下载页?"),
"goto": MessageLookupByLibrary.simpleMessage("前往"),
"host": MessageLookupByLibrary.simpleMessage("主机"),
"httpFailedWithCode": m6,
"imagesList": MessageLookupByLibrary.simpleMessage("镜像列表"),
"import": MessageLookupByLibrary.simpleMessage("导入"),
"importAndExport": MessageLookupByLibrary.simpleMessage("导入或导出"),
"inputDomainHere": MessageLookupByLibrary.simpleMessage("在这里输入域名"),
"install": MessageLookupByLibrary.simpleMessage("安装"),
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
"请先 https://docs.docker.com/engine/install docker"),
"invalidJson": MessageLookupByLibrary.simpleMessage("无效的json存在格式问题"),
"invalidVersion": MessageLookupByLibrary.simpleMessage("不支持的版本"),
"invalidVersionHelp": m7,
"isBusy": MessageLookupByLibrary.simpleMessage("当前正忙"),
"keepForeground": MessageLookupByLibrary.simpleMessage("请保持应用处于前台!"),
"keyAuth": MessageLookupByLibrary.simpleMessage("公钥认证"),
"lastTry": MessageLookupByLibrary.simpleMessage("最后尝试"),
"launchPage": MessageLookupByLibrary.simpleMessage("启动页"),
"license": MessageLookupByLibrary.simpleMessage("开源证书"),
"loadingFiles": MessageLookupByLibrary.simpleMessage("正在加载目录。。。"),
"loss": MessageLookupByLibrary.simpleMessage("丢包率"),
"madeWithLove": m8,
"max": MessageLookupByLibrary.simpleMessage("最大"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"ms": MessageLookupByLibrary.simpleMessage("毫秒"),
"name": MessageLookupByLibrary.simpleMessage("名称"),
"newContainer": MessageLookupByLibrary.simpleMessage("新建容器"),
"noClient": MessageLookupByLibrary.simpleMessage("没有SSH连接"),
"noInterface": MessageLookupByLibrary.simpleMessage("没有可用的接口"),
"noResult": MessageLookupByLibrary.simpleMessage("无结果"),
"noSavedPrivateKey": MessageLookupByLibrary.simpleMessage("没有已保存的私钥。"),
"noSavedSnippet": MessageLookupByLibrary.simpleMessage("没有已保存的代码片段。"),
"noServerAvailable": MessageLookupByLibrary.simpleMessage("没有可用的服务器。"),
"noUpdateAvailable": MessageLookupByLibrary.simpleMessage("没有可用更新"),
"ok": MessageLookupByLibrary.simpleMessage(""),
"onServerDetailPage": MessageLookupByLibrary.simpleMessage("在服务器详情页"),
"open": MessageLookupByLibrary.simpleMessage("打开"),
"path": MessageLookupByLibrary.simpleMessage("路径"),
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("平均:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"),
"pingNoServer": MessageLookupByLibrary.simpleMessage(
"没有服务器可用于Ping\n请在服务器tab添加服务器后再试"),
"platformNotSupportUpdate":
MessageLookupByLibrary.simpleMessage("当前平台不支持更新,请编译最新源码后手动安装"),
"plzEnterHost": MessageLookupByLibrary.simpleMessage("请输入主机"),
"plzSelectKey": MessageLookupByLibrary.simpleMessage("请选择私钥"),
"port": MessageLookupByLibrary.simpleMessage("端口"),
"preview": MessageLookupByLibrary.simpleMessage("预览"),
"privateKey": MessageLookupByLibrary.simpleMessage("私钥"),
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
"rename": MessageLookupByLibrary.simpleMessage("重命名"),
"reportBugsOnGithubIssue": m9,
"restore": MessageLookupByLibrary.simpleMessage("恢复"),
"restoreSuccess":
MessageLookupByLibrary.simpleMessage("恢复成功需要重启App来应用更改"),
"restoreSureWithDate": m10,
"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("设置"),
"sftpDlPrepare": MessageLookupByLibrary.simpleMessage("准备连接至服务器..."),
"sftpNoDownloadTask": MessageLookupByLibrary.simpleMessage("没有下载任务"),
"sftpSSHConnected":
MessageLookupByLibrary.simpleMessage("SFTP 已连接,即将开始下载..."),
"showDistLogo": MessageLookupByLibrary.simpleMessage("显示发行版 Logo"),
"snippet": MessageLookupByLibrary.simpleMessage("代码片段"),
"spentTime": m11,
"sshTip": m12,
"start": MessageLookupByLibrary.simpleMessage("开始"),
"stop": MessageLookupByLibrary.simpleMessage("停止"),
"sureDelete": m13,
"sureNoPwd": MessageLookupByLibrary.simpleMessage("确认使用无密码?"),
"sureToDeleteServer": m14,
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
"unkownConvertMode": MessageLookupByLibrary.simpleMessage("未知转换模式"),
"update": MessageLookupByLibrary.simpleMessage("更新"),
"updateAll": MessageLookupByLibrary.simpleMessage("更新全部"),
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
"你设置为0服务器状态不会自动刷新。\n且不能计算CPU使用情况。"),
"updateServerStatusInterval":
MessageLookupByLibrary.simpleMessage("服务器状态刷新间隔"),
"upsideDown": MessageLookupByLibrary.simpleMessage("上下交换"),
"urlOrJson": MessageLookupByLibrary.simpleMessage("链接或JSON"),
"user": MessageLookupByLibrary.simpleMessage("用户"),
"versionHaveUpdate": m15,
"versionUnknownUpdate": m16,
"versionUpdated": m17,
"waitConnection": MessageLookupByLibrary.simpleMessage("请等待连接建立"),
"willTakEeffectImmediately":
MessageLookupByLibrary.simpleMessage("更改将会立即生效")
};
}

File diff suppressed because it is too large Load Diff

176
lib/l10n/app_en.arb Normal file
View File

@@ -0,0 +1,176 @@
{
"about": "About",
"aboutThanks": "\nThanks to the following people who participated in the test.",
"addAServer": "add a server",
"addOne": "Add one",
"addPrivateKey": "Add private key",
"alreadyLastDir": "Already in last directory.",
"appPrimaryColor": "App primary color",
"attention": "Attention",
"auto": "Auto",
"backDir": "Back",
"backup": "Backup",
"backupTip": "The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).",
"backupVersionNotMatch": "Backup version is not match.",
"cancel": "Cancel",
"choose": "Choose",
"chooseDestination": "Choose destination",
"chooseFontFile": "Choose a font file",
"choosePrivateKey": "Choose private key",
"clear": "Clear",
"clickSee": "Click here",
"close": "Close",
"cmd": "Command",
"containerStatus": "Container status",
"convert": "Convert",
"copy": "Copy",
"copyPath": "Copy path",
"createFile": "Create file",
"createFolder": "Create folder",
"currentMode": "Current Mode",
"dark": "Dark",
"debug": "Debug",
"decode": "Decode",
"delete": "Delete",
"disconnected": "Disconnected",
"dl2Local": "Download [{fileName}] to local?",
"dockerContainerName": "Container name",
"dockerEditHost": "Edit DOCKER_HOST",
"dockerEmptyRunningItems": "No running container. \nIt may be that the env DOCKER_HOST is not read correctly. You can found it by running `echo $DOCKER_HOST` in terminal.",
"dockerImage": "Image",
"dockerImagesFmt": "{count} images",
"dockerNotInstalled": "Docker not installed",
"dockerStatusRunningAndStoppedFmt": "{runningCount} running, {stoppedCount} container stopped.",
"dockerStatusRunningFmt": "{count} container running.",
"download": "Download",
"downloadFinished": "Download finished",
"downloadStatus": "{percent}% of {size}",
"edit": "Edit",
"encode": "Encode",
"error": "Error",
"exampleName": "Example name",
"experimentalFeature": "Experimental feature",
"export": "Export",
"extraArgs": "Extra args",
"failed": "Failed",
"feedback": "Feedback",
"feedbackOnGithub": "If you have any questions, please feedback on Github.",
"fieldMustNotEmpty": "These fields must not be empty.",
"fileNotExist": "{file} not exist",
"fileTooLarge": "File '{file}' too large {size}, max {sizeMax}",
"files": "Files",
"foundNUpdate": "Found {count} update",
"getPushTokenFailed": "Can't fetch push token",
"gettingToken": "Getting token...",
"go": "Go",
"goto": "Go to",
"host": "Host",
"httpFailedWithCode": "request failed, status code: {code}",
"imagesList": "Images list",
"import": "Import",
"importAndExport": "Import and Export",
"inputDomainHere": "Input Domain here",
"install": "install",
"installDockerWithUrl": "Please https://docs.docker.com/engine/install docker first.",
"invalidJson": "Invalid JSON",
"invalidVersion": "Invalid version",
"invalidVersionHelp": "Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don't have the above issues, please submit an issue on {url}.",
"isBusy": "Is busy now",
"keepForeground": "Keep app foreground!",
"keyAuth": "Key Auth",
"lastTry": "Last try!",
"launchPage": "Launch page",
"license": "License",
"light": "Light",
"loadingFiles": "Loading files...",
"loss": "loss",
"madeWithLove": "\nMade with ❤️ by {myGithub}",
"max": "max",
"maxRetryCount": "Number of server reconnection",
"maxRetryCountEqual0": "Will retry again and again.",
"min": "min",
"ms": "ms",
"name": "Name",
"needRestart": "Need to restart app",
"newContainer": "New container",
"noClient": "No client",
"noInterface": "No interface",
"noResult": "No result",
"noSavedPrivateKey": "No saved private keys.",
"noSavedSnippet": "No saved snippets.",
"noServerAvailable": "No server available.",
"noUpdateAvailable": "No update available",
"notSelected": "Not selected",
"nullToken": "Null token",
"ok": "OK",
"onServerDetailPage": "On server detail page",
"onlyIOS": "Only valid on iOS",
"open": "Open",
"path": "Path",
"pickFile": "Pick file",
"ping": "Ping",
"pingAvg": "Avg:",
"pingInputIP": "Please input a target IP/domain.",
"pingNoServer": "No server to ping.\nPlease add a server in server tab.",
"pkg": "Pkg",
"platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it.",
"plzEnterHost": "Please enter host.",
"plzSelectKey": "Please select a key.",
"port": "Port",
"preview": "Preview",
"privateKey": "Private Key",
"pushToken": "Push token",
"pwd": "Password",
"rename": "Rename",
"reportBugsOnGithubIssue": "Please report bugs on {url}",
"restart": "Restart",
"restore": "Restore",
"restoreSuccess": "Restore success. Restart app to apply.",
"restoreSureWithDate": "Are you sure to restore from {date} ?",
"result": "Result",
"run": "Run",
"save": "Save",
"second": "s",
"server": "Server",
"serverTabConnecting": "Connecting...",
"serverTabEmpty": "There is no server.\nClick the fab to add one.",
"serverTabFailed": "Failed",
"serverTabLoading": "Loading...",
"serverTabPlzSave": "Please 'save' this private key again.",
"serverTabUnkown": "Unknown state",
"setting": "Setting",
"sftpDlPrepare": "Preparing to connect...",
"sftpNoDownloadTask": "No download task.",
"sftpSSHConnected": "SFTP Connected",
"showDistLogo": "Show distribution logo",
"snippet": "Snippet",
"spentTime": "Spent time: {time}",
"sshTip": "This function is now in the experimental stage.\n\nPlease report bugs on {url} or join our development.",
"start": "Start",
"stop": "Stop",
"success": "Success",
"sureDelete": "Are you sure to delete [{name}]?",
"sureNoPwd": "Are you sure to use no password?",
"sureToDeleteServer": "Are you sure to delete server [{server}]?",
"termTheme": "Terminal theme",
"themeMode": "Theme mode",
"times": "Times",
"ttl": "ttl",
"unknown": "unknown",
"unknownError": "Unknown error",
"unkownConvertMode": "Unknown convert mode",
"update": "Update",
"updateAll": "Update all",
"updateIntervalEqual0": "You set to 0, will not update automatically.\nCan't calculate CPU status.",
"updateServerStatusInterval": "Server status update interval",
"updateTip": "Update: v1.0.{newest}",
"updateTipTooLow": "Current version is too low, please update to v1.0.{newest}",
"upsideDown": "Upside Down",
"urlOrJson": "URL or JSON",
"user": "User",
"versionHaveUpdate": "Found: v1.0.{build}, click to update",
"versionUnknownUpdate": "Current: v1.0.{build}",
"versionUpdated": "Current: v1.0.{build}, is up to date",
"waitConnection": "Please wait for the connection to be established.",
"willTakEeffectImmediately": "Will take effect immediately"
}

176
lib/l10n/app_zh.arb Normal file
View File

@@ -0,0 +1,176 @@
{
"about": "关于",
"aboutThanks": "\n感谢以下参与软件测试的各位。",
"addAServer": "添加服务器",
"addOne": "前去新增",
"addPrivateKey": "添加一个私钥",
"alreadyLastDir": "已经是最上层目录了",
"appPrimaryColor": "App主要色",
"attention": "注意",
"auto": "自动",
"backDir": "返回上一级",
"backup": "备份",
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。\n除了设置项恢复的数据不会覆盖现有数据。",
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
"cancel": "取消",
"choose": "选择",
"chooseDestination": "选择目标",
"chooseFontFile": "选择字体文件",
"choosePrivateKey": "选择私钥",
"clear": "清除",
"clickSee": "点击查看",
"close": "关闭",
"cmd": "命令",
"containerStatus": "容器状态",
"convert": "转换",
"copy": "复制",
"copyPath": "复制路径",
"createFile": "创建文件",
"createFolder": "创建文件夹",
"currentMode": "当前模式",
"dark": "暗",
"debug": "调试",
"decode": "解码",
"delete": "删除",
"disconnected": "连接断开",
"dl2Local": "下载 [{fileName}] 到本地?",
"dockerContainerName": "容器名",
"dockerEditHost": "编辑 DOCKER_HOST",
"dockerEmptyRunningItems": "没有正在运行的容器。\n这可能是因为环境变量 DOCKER_HOST 没有被正确读取。你可以通过在终端内运行 `echo $DOCKER_HOST` 来获取。",
"dockerImage": "镜像",
"dockerImagesFmt": "共 {count} 个镜像",
"dockerNotInstalled": "Docker未安装",
"dockerStatusRunningAndStoppedFmt": "{runningCount}个正在运行, {stoppedCount}个已停止",
"dockerStatusRunningFmt": "{count}个容器正在运行",
"download": "下载",
"downloadFinished": "下载完成!",
"downloadStatus": "{size} 的 {percent}%",
"edit": "编辑",
"encode": "编码",
"error": "出错了",
"exampleName": "名称示例",
"experimentalFeature": "实验性功能",
"export": "导出",
"extraArgs": "额外参数",
"failed": "失败",
"feedback": "反馈",
"feedbackOnGithub": "如果你有任何问题请在GitHub反馈",
"fieldMustNotEmpty": "这些输入框不能为空。",
"fileNotExist": "{file} 不存在",
"fileTooLarge": "文件 '{file}' 过大 '{size}',超过了 {sizeMax}",
"files": "文件",
"foundNUpdate": "找到 {count} 个更新",
"getPushTokenFailed": "未能获取到推送token",
"gettingToken": "正在获取Token...",
"go": "开始",
"goto": "前往",
"host": "主机",
"httpFailedWithCode": "请求失败, 状态码: {code}",
"imagesList": "镜像列表",
"import": "导入",
"importAndExport": "导入或导出",
"inputDomainHere": "在这里输入域名",
"install": "安装",
"installDockerWithUrl": "请先 https://docs.docker.com/engine/install docker",
"invalidJson": "无效的json存在格式问题",
"invalidVersion": "不支持的版本",
"invalidVersionHelp": "请确保正确安装了docker或者使用的非自编译版本。如果没有以上问题请在 {url} 提交问题。",
"isBusy": "当前正忙",
"keepForeground": "请保持应用处于前台!",
"keyAuth": "公钥认证",
"lastTry": "最后尝试",
"launchPage": "启动页",
"license": "开源证书",
"light": "亮",
"loadingFiles": "正在加载目录。。。",
"loss": "丢包率",
"madeWithLove": "\n用❤制作 by {myGithub}",
"max": "最大",
"maxRetryCount": "服务器尝试重连次数",
"maxRetryCountEqual0": "会无限重试",
"min": "最小",
"ms": "毫秒",
"name": "名称",
"needRestart": "需要重启 App",
"newContainer": "新建容器",
"noClient": "没有SSH连接",
"noInterface": "没有可用的接口",
"noResult": "无结果",
"noSavedPrivateKey": "没有已保存的私钥。",
"noSavedSnippet": "没有已保存的代码片段。",
"noServerAvailable": "没有可用的服务器。",
"noUpdateAvailable": "没有可用更新",
"notSelected": "未选择",
"nullToken": "无Token",
"ok": "好",
"onServerDetailPage": "在服务器详情页",
"onlyIOS": "仅在iOS上有效",
"open": "打开",
"path": "路径",
"pickFile": "选择文件",
"ping": "Ping",
"pingAvg": "平均:",
"pingInputIP": "请输入目标IP或域名",
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试",
"pkg": "包管理",
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装",
"plzEnterHost": "请输入主机",
"plzSelectKey": "请选择私钥",
"port": "端口",
"preview": "预览",
"privateKey": "私钥",
"pushToken": "消息推送 Token",
"pwd": "密码",
"rename": "重命名",
"reportBugsOnGithubIssue": "请到 {url} 提交问题",
"restart": "重启",
"restore": "恢复",
"restoreSuccess": "恢复成功需要重启App来应用更改",
"restoreSureWithDate": "确定恢复 {date} 的备份吗?",
"result": "结果",
"run": "运行",
"save": "保存",
"second": "秒",
"server": "服务器",
"serverTabConnecting": "连接中...",
"serverTabEmpty": "现在没有服务器。\n点击右下方按钮来添加。",
"serverTabFailed": "失败",
"serverTabLoading": "加载中...",
"serverTabPlzSave": "请再次保存该私钥",
"serverTabUnkown": "未知状态",
"setting": "设置",
"sftpDlPrepare": "准备连接至服务器...",
"sftpNoDownloadTask": "没有下载任务",
"sftpSSHConnected": "SFTP 已连接,即将开始下载...",
"showDistLogo": "显示发行版 Logo",
"snippet": "代码片段",
"spentTime": "耗时: {time}",
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
"start": "开始",
"stop": "停止",
"success": "成功",
"sureDelete": "确定删除[{name}]",
"sureNoPwd": "确认使用无密码?",
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
"termTheme": "终端主题",
"themeMode": "主题模式",
"times": "次",
"ttl": "缓存时间",
"unknown": "未知",
"unknownError": "未知错误",
"unkownConvertMode": "未知转换模式",
"update": "更新",
"updateAll": "更新全部",
"updateIntervalEqual0": "你设置为0服务器状态不会自动刷新。\n且不能计算CPU使用情况。",
"updateServerStatusInterval": "服务器状态刷新间隔",
"updateTip": "新版本: v1.0.{newest}",
"updateTipTooLow": "当前版本过低,请升级至 v1.0.{newest}",
"upsideDown": "上下交换",
"urlOrJson": "链接或JSON",
"user": "用户",
"versionHaveUpdate": "找到新版本v1.0.{build}, 点击更新",
"versionUnknownUpdate": "当前v1.0.{build}",
"versionUpdated": "当前v1.0.{build}, 已是最新版本",
"waitConnection": "请等待连接建立",
"willTakEeffectImmediately": "更改将会立即生效"
}

View File

@@ -1,151 +0,0 @@
{
"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": "\nAll rights reserved.\n\nThanks to the following people who participated in the test.",
"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.\nCan't calculate CPU status.",
"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.",
"sureNoPwd": "Are you sure to use no password?",
"plzSelectKey": "Please select a key.",
"exampleName": "Example name",
"stop": "Stop",
"download": "Download",
"copyPath": "Copy path",
"keepForeground": "Keep app foreground!",
"downloadFinished": "Download finished",
"downloadStatus": "{percent}% of {size}",
"sftpDlPrepare": "Preparing to connect...",
"sftpSSHConnected": "SFTP Connected",
"spentTime": "Spent time: {time}",
"backDir": "Back",
"alreadyLastDir": "Already in last directory.",
"open": "Open",
"sureDelete": "Are you sure to delete [{name}]?",
"containerStatus": "Container status",
"noClient": "No client",
"installDockerWithUrl": "Please https://docs.docker.com/engine/install docker first.",
"waitConnection": "Please wait for the connection to be established.",
"unknownError": "Unknown error",
"dockerStatusRunningFmt": "{count} container running.",
"dockerStatusRunningAndStoppedFmt": "{runningCount} running, {stoppedCount} container stopped.",
"install": "install",
"loadingFiles": "Loading files...",
"sftpNoDownloadTask": "No download task.",
"goSftpDlPage": "Go to SFTP download page?",
"createFolder": "Create folder",
"createFile": "Create file",
"rename": "Rename",
"dl2Local": "Download [{fileName}] to local?",
"error": "Error",
"disconnected": "Disconnected",
"files": "Files",
"experimentalFeature": "Experimental feature",
"reportBugsOnGithubIssue": "Please report bugs on {url}",
"noUpdateAvailable": "No update available",
"foundNUpdate": "Found {count} update",
"updateAll": "Update all",
"platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it.",
"invalidVersionHelp": "Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don't have the above issues, please submit an issue on {url}.",
"noInterface": "No interface",
"lastTry": "Last try!",
"pingNoServer": "No server to ping.\nPlease add a server in server tab.",
"backupTip": "The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).",
"backup": "Backup",
"restore": "Restore",
"restoreSureWithDate": "Are you sure to restore from {date} ?",
"backupVersionNotMatch": "Backup version is not match.",
"invalidJson": "Invalid JSON",
"restoreSuccess": "Restore success. Restart app to apply.",
"clickSee": "Click here",
"feedback": "Feedback",
"feedbackOnGithub": "If you have any questions, please feedback on Github.",
"update": "Update",
"inputDomainHere": "Input Domain here",
"dockerNotInstalled": "Docker not installed",
"invalidVersion": "Invalid version",
"cmd": "Command",
"dockerEmptyRunningItems": "No running container. \nIt may be that the env DOCKER_HOST is not read correctly. You can found it by running `echo $DOCKER_HOST` in terminal.",
"dockerEditHost": "Edit DOCKER_HOST",
"newContainer": "New container",
"dockerImage": "Image",
"dockerContainerName": "Container name",
"extraArgs": "Extra args",
"preview": "Preview",
"isBusy": "Is busy now",
"imagesList": "Images list",
"dockerImagesFmt": "{count} images",
"path": "Path",
"goto": "Go to",
"showDistLogo": "Show distribution logo",
"onServerDetailPage": "On server detail page",
"addOne": "Add one",
"sshTip": "This function is now in the experimental stage. \nPlease report bugs on {url} or join our development."
}

View File

@@ -1,151 +0,0 @@
{
"server": "服务器",
"convert": "转换",
"ping": "Ping",
"debug": "调试",
"addAServer": "添加服务器",
"setting": "设置",
"license": "开源证书",
"snippet": "代码片段",
"privateKey": "私钥",
"madeWithLove": "\n用❤制作 by {myGithub}",
"aboutThanks": "\n保留所有权利。\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且不能计算CPU使用情况。",
"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": "请输入主机",
"sureNoPwd": "确认使用无密码?",
"plzSelectKey": "请选择私钥",
"exampleName": "名称示例",
"stop": "停止",
"download": "下载",
"copyPath": "复制路径",
"keepForeground": "请保持应用处于前台!",
"downloadFinished": "下载完成!",
"downloadStatus": "{size} 的 {percent}%",
"sftpDlPrepare": "准备连接至服务器...",
"sftpSSHConnected": "SFTP 已连接,即将开始下载...",
"spentTime": "耗时: {time}",
"backDir": "返回上一级",
"alreadyLastDir": "已经是最上层目录了",
"open": "打开",
"sureDelete": "确定删除[{name}]",
"containerStatus": "容器状态",
"noClient": "没有SSH连接",
"installDockerWithUrl": "请先 https://docs.docker.com/engine/install docker",
"waitConnection": "请等待连接建立",
"unknownError": "未知错误",
"dockerStatusRunningFmt": "{count}个容器正在运行",
"dockerStatusRunningAndStoppedFmt": "{runningCount}个正在运行, {stoppedCount}个已停止",
"install": "安装",
"loadingFiles": "正在加载目录。。。",
"sftpNoDownloadTask": "没有下载任务",
"goSftpDlPage": "前往下载页?",
"createFolder": "创建文件夹",
"createFile": "创建文件",
"rename": "重命名",
"dl2Local": "下载 [{fileName}] 到本地?",
"error": "出错了",
"disconnected": "连接断开",
"files": "文件",
"experimentalFeature": "实验性功能",
"reportBugsOnGithubIssue": "请到 {url} 提交问题",
"noUpdateAvailable": "没有可用更新",
"foundNUpdate": "找到 {count} 个更新",
"updateAll": "更新全部",
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装",
"invalidVersionHelp": "请确保正确安装了docker或者使用的非自编译版本。如果没有以上问题请在 {url} 提交问题。",
"noInterface": "没有可用的接口",
"lastTry": "最后尝试",
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试",
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。\n除了设置项恢复的数据不会覆盖现有数据。",
"backup": "备份",
"restore": "恢复",
"restoreSureWithDate": "确定恢复 {date} 的备份吗?",
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
"invalidJson": "无效的json存在格式问题",
"restoreSuccess": "恢复成功需要重启App来应用更改",
"clickSee": "点击查看",
"feedback": "反馈",
"feedbackOnGithub": "如果你有任何问题请在GitHub反馈",
"update": "更新",
"inputDomainHere": "在这里输入域名",
"dockerNotInstalled": "Docker未安装",
"invalidVersion": "不支持的版本",
"cmd": "命令",
"dockerEmptyRunningItems": "没有正在运行的容器。\n这可能是因为环境变量 DOCKER_HOST 没有被正确读取。你可以通过在终端内运行 `echo $DOCKER_HOST` 来获取。",
"dockerEditHost": "编辑 DOCKER_HOST",
"newContainer": "新建容器",
"dockerImage": "镜像",
"dockerContainerName": "容器名",
"extraArgs": "额外参数",
"preview": "预览",
"isBusy": "当前正忙",
"imagesList": "镜像列表",
"dockerImagesFmt": "共 {count} 个镜像",
"path": "路径",
"goto": "前往",
"showDistLogo": "显示发行版 Logo",
"onServerDetailPage": "在服务器详情页",
"addOne": "前去新增",
"sshTip": "该功能目前处于测试阶段,请在 {url} 反馈问题,或者加入我们开发。"
}

View File

@@ -1,18 +1,20 @@
import 'package:get_it/get_it.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/pkg.dart';
import 'package:toolbox/data/provider/debug.dart';
import 'package:toolbox/data/provider/docker.dart';
import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/sftp_download.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/service/app.dart';
import 'package:toolbox/data/store/docker.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/data/store/snippet.dart';
import 'data/provider/app.dart';
import 'data/provider/debug.dart';
import 'data/provider/docker.dart';
import 'data/provider/pkg.dart';
import 'data/provider/private_key.dart';
import 'data/provider/server.dart';
import 'data/provider/sftp_download.dart';
import 'data/provider/snippet.dart';
import 'data/provider/virtual_keyboard.dart';
import 'data/service/app.dart';
import 'data/store/docker.dart';
import 'data/store/private_key.dart';
import 'data/store/server.dart';
import 'data/store/setting.dart';
import 'data/store/snippet.dart';
GetIt locator = GetIt.instance;
@@ -26,9 +28,10 @@ void setupLocatorForProviders() {
locator.registerSingleton(DebugProvider());
locator.registerSingleton(DockerProvider());
locator.registerSingleton(ServerProvider());
locator.registerSingleton(VirtualKeyboard());
locator.registerSingleton(SnippetProvider());
locator.registerSingleton(PrivateKeyProvider());
locator.registerSingleton(SftpDownloadProvider());
locator.registerSingleton(SftpProvider());
}
Future<void> setupLocatorForStores() async {

View File

@@ -4,34 +4,39 @@ import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/app.dart';
import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/core/persistant_store.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/pkg.dart';
import 'package:toolbox/data/provider/debug.dart';
import 'package:toolbox/data/provider/docker.dart';
import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/sftp_download.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/data/store/snippet.dart';
import 'package:toolbox/locator.dart';
import 'app.dart';
import 'core/analysis.dart';
import 'core/utils/ui.dart';
import 'data/model/server/private_key_info.dart';
import 'data/model/server/server_private_info.dart';
import 'data/model/server/snippet.dart';
import 'data/provider/app.dart';
import 'data/provider/debug.dart';
import 'data/provider/docker.dart';
import 'data/provider/pkg.dart';
import 'data/provider/private_key.dart';
import 'data/provider/server.dart';
import 'data/provider/sftp_download.dart';
import 'data/provider/snippet.dart';
import 'data/provider/virtual_keyboard.dart';
import 'data/store/setting.dart';
import 'locator.dart';
import 'view/widget/rebuild.dart';
late final DebugProvider _debug;
Future<void> initApp() async {
await initHive();
await setupLocator();
await upgradeStore();
_debug = locator<DebugProvider>();
locator<SnippetProvider>().loadData();
locator<PrivateKeyProvider>().loadData();
final settings = locator<SettingStore>();
await loadFontFile(settings.fontPath.fetch());
///设置Logger
Logger.root.level = Level.ALL; // defaults to Level.INFO
Logger.root.onRecord.listen((record) {
@@ -47,20 +52,6 @@ Future<void> initHive() async {
Hive.registerAdapter(ServerPrivateInfoAdapter());
}
Future<void> upgradeStore() async {
final setting = locator<SettingStore>();
final version = setting.storeVersion.fetch();
if (version == 0) {
final snippet = locator<SnippetStore>();
final key = locator<PrivateKeyStore>();
final spi = locator<ServerStore>();
for (final s in <PersistentStore>[snippet, key, spi]) {
await s.box.deleteAll(s.box.keys);
}
setting.storeVersion.put(1);
}
}
void runInZone(dynamic Function() body) {
final zoneSpec = ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
@@ -69,8 +60,7 @@ void runInZone(dynamic Function() body) {
// `setState() or markNeedsBuild() called during build`
// error.
Future.delayed(const Duration(milliseconds: 1), () {
final debugProvider = locator<DebugProvider>();
debugProvider.addText(line);
_debug.addText(line);
});
},
);
@@ -84,9 +74,8 @@ void runInZone(dynamic Function() body) {
void onError(Object obj, StackTrace stack) {
Analysis.recordException(obj);
final debugProvider = locator<DebugProvider>();
debugProvider.addError(obj);
debugProvider.addError(stack);
_debug.addMultiline(obj, Colors.red);
_debug.addMultiline(stack, Colors.white);
}
Future<void> main() async {
@@ -101,11 +90,13 @@ Future<void> main() async {
ChangeNotifierProvider(create: (_) => locator<DockerProvider>()),
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
ChangeNotifierProvider(create: (_) => locator<SnippetProvider>()),
ChangeNotifierProvider(create: (_) => locator<VirtualKeyboard>()),
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
ChangeNotifierProvider(
create: (_) => locator<SftpDownloadProvider>()),
ChangeNotifierProvider(create: (_) => locator<SftpProvider>()),
],
child: const MyApp(),
child: RebuildWidget(
child: MyApp(),
),
),
);
});

View File

@@ -4,17 +4,18 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:toolbox/core/extension/colorx.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/backup.dart';
import 'package:flutter_gen/gen_l10n/l10n.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/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/data/store/snippet.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import '../../core/extension/colorx.dart';
import '../../core/utils/ui.dart';
import '../../data/model/app/backup.dart';
import '../../data/res/ui.dart';
import '../../data/store/private_key.dart';
import '../../data/store/server.dart';
import '../../data/store/setting.dart';
import '../../data/store/snippet.dart';
import '../../locator.dart';
const backupFormatVersion = 1;
@@ -29,7 +30,7 @@ class BackupPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final media = MediaQuery.of(context);
final s = S.of(context);
final s = S.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(s.importAndExport, style: textSize18),
@@ -67,13 +68,12 @@ class BackupPage extends StatelessWidget {
Widget _buildCard(String text, IconData icon, MediaQueryData media,
FutureOr Function() onTap) {
final priColor = primaryColor;
final textColor = priColor.isBrightColor ? Colors.black : Colors.white;
final textColor = primaryColor.isBrightColor ? Colors.black : Colors.white;
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(37), color: priColor),
borderRadius: BorderRadius.circular(37), color: primaryColor),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
child: Row(

View File

@@ -2,11 +2,12 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.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';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../core/utils/ui.dart';
import '../../data/res/color.dart';
import '../widget/input_field.dart';
import '../widget/round_rect_card.dart';
class ConvertPage extends StatefulWidget {
const ConvertPage({Key? key}) : super(key: key);
@@ -37,7 +38,7 @@ class _ConvertPageState extends State<ConvertPage>
super.didChangeDependencies();
_media = MediaQuery.of(context);
_theme = Theme.of(context);
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -51,7 +52,7 @@ class _ConvertPageState extends State<ConvertPage>
children: [
const SizedBox(height: 13),
_buildInputTop(),
_buildTypeOption(),
_buildMiddleBtns(),
_buildResult(),
],
),
@@ -95,7 +96,7 @@ class _ConvertPageState extends State<ConvertPage>
);
}
Widget _buildTypeOption() {
Widget _buildMiddleBtns() {
final decode = _s.decode;
final encode = _s.encode;
final List<String> typeOption = [
@@ -111,8 +112,6 @@ class _ConvertPageState extends State<ConvertPage>
title: Row(
children: [
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: Icon(Icons.change_circle, semanticLabel: _s.upsideDown),
onPressed: () {
final temp = _textEditingController.text;
@@ -121,15 +120,13 @@ class _ConvertPageState extends State<ConvertPage>
},
),
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor),
),
child: Icon(Icons.copy, semanticLabel: _s.copy),
onPressed: () => Clipboard.setData(
ClipboardData(
text: _textEditingControllerResult.text == ''
? ' '
: _textEditingControllerResult.text),
text: _textEditingControllerResult.text == ''
? ' '
: _textEditingControllerResult.text,
),
),
)
],
@@ -145,9 +142,10 @@ class _ConvertPageState extends State<ConvertPage>
textScaleFactor: 1.0,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: primaryColor),
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: primaryColor,
),
),
Text(
_s.currentMode,

View File

@@ -1,21 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/menu_item.dart';
import 'package:toolbox/data/model/docker/ps.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/docker.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/error.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/data/store/docker.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/center_loading.dart';
import 'package:toolbox/view/widget/two_line_text.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:toolbox/view/widget/url_text.dart';
import '../../core/utils/ui.dart';
import '../../data/model/docker/ps.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/provider/docker.dart';
import '../../data/provider/server.dart';
import '../../data/res/error.dart';
import '../../data/res/menu.dart';
import '../../data/res/ui.dart';
import '../../data/res/url.dart';
import '../../data/store/docker.dart';
import '../../locator.dart';
import '../widget/center_loading.dart';
import '../widget/dropdown_menu.dart';
import '../widget/round_rect_card.dart';
import '../widget/two_line_text.dart';
import '../widget/url_text.dart';
class DockerManagePage extends StatefulWidget {
final ServerPrivateInfo spi;
@@ -39,16 +41,13 @@ class _DockerManagePageState extends State<DockerManagePage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
void initState() {
super.initState();
final client = locator<ServerProvider>()
.servers
.firstWhere((element) => element.info == widget.spi)
.client;
final client = locator<ServerProvider>().getServer(widget.spi.id).client;
if (client == null) {
showSnackBar(context, Text(_s.noClient));
Navigator.of(context).pop();
@@ -59,32 +58,32 @@ class _DockerManagePageState extends State<DockerManagePage> {
@override
Widget build(BuildContext context) {
return Consumer<DockerProvider>(builder: (_, docker, __) {
return Consumer<DockerProvider>(builder: (_, ___, __) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: TwoLineText(up: 'Docker', down: widget.spi.name),
actions: [
IconButton(
onPressed: () => docker.refresh(),
onPressed: () => _docker.refresh(),
icon: const Icon(Icons.refresh),
)
],
),
body: _buildMain(docker),
floatingActionButton: _buildFAB(docker),
body: _buildMain(),
floatingActionButton: _buildFAB(),
);
});
}
Widget _buildFAB(DockerProvider docker) {
Widget _buildFAB() {
return FloatingActionButton(
onPressed: () async => await _showAddFAB(docker),
onPressed: () async => await _showAddFAB(),
child: const Icon(Icons.add),
);
}
Future<void> _showAddFAB(DockerProvider docker) async {
Future<void> _showAddFAB() async {
final imageCtrl = TextEditingController();
final nameCtrl = TextEditingController();
final argsCtrl = TextEditingController();
@@ -188,7 +187,9 @@ class _DockerManagePageState extends State<DockerManagePage> {
if (_textController.text == '') {
showRoundDialog(context, _s.attention, Text(_s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(_s.ok)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok),
),
]);
return;
}
@@ -229,9 +230,8 @@ class _DockerManagePageState extends State<DockerManagePage> {
return _textController.text.trim();
}
Widget _buildMain(DockerProvider docker) {
final running = docker.items;
if (docker.error != null && running == null) {
Widget _buildMain() {
if (_docker.error != null && _docker.items == null) {
return SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -242,17 +242,17 @@ class _DockerManagePageState extends State<DockerManagePage> {
size: 37,
),
const SizedBox(height: 27),
_buildErr(docker.error!),
_buildErr(_docker.error!),
const SizedBox(height: 27),
Padding(
padding: const EdgeInsets.all(17),
child: _buildSolution(docker.error!),
child: _buildSolution(_docker.error!),
)
],
),
);
}
if (running == null) {
if (_docker.items == null || _docker.images == null) {
_docker.refresh();
return centerLoading;
}
@@ -260,49 +260,74 @@ class _DockerManagePageState extends State<DockerManagePage> {
return ListView(
padding: const EdgeInsets.all(7),
children: [
_buildLoading(docker),
_buildLoading(),
_buildVersion(
docker.edition ?? _s.unknown, docker.version ?? _s.unknown),
_buildPsItems(running, docker),
_buildImages(docker),
_buildEditHost(running, docker),
_docker.edition ?? _s.unknown, _docker.version ?? _s.unknown),
_buildPsItems(),
_buildImages(),
_buildEditHost(),
].map((e) => RoundRectCard(e)).toList(),
);
}
Widget _buildImages(DockerProvider docker) {
if (docker.images == null) {
Widget _buildImages() {
if (_docker.images == null) {
return const SizedBox();
}
return ExpansionTile(
title: Text(_s.imagesList),
subtitle: Text(
_s.dockerImagesFmt(docker.images!.length),
style: grey,
),
children: docker.images!
.map(
(e) => ListTile(
title: Text(e.repo),
subtitle: Text('${e.tag} - ${e.size}'),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
final result = await _docker.run('docker rmi ${e.id} -f');
if (result != null) {
showSnackBar(
context, Text(getErrMsg(result) ?? _s.unknownError));
}
},
),
title: Text(_s.imagesList),
subtitle: Text(
_s.dockerImagesFmt(_docker.images!.length),
style: grey,
),
children: _docker.images!
.map(
(e) => ListTile(
title: Text(e.repo),
subtitle: Text('${e.tag} - ${e.size}'),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showRoundDialog(
context,
_s.attention,
Text(_s.sureDelete(e.repo)),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final result = await _docker.run(
'docker rmi ${e.id} -f',
);
if (result != null) {
showSnackBar(
context,
Text(getErrMsg(result) ?? _s.unknownError),
);
}
},
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
),
],
);
},
),
)
.toList());
),
)
.toList(),
);
}
Widget _buildLoading(DockerProvider docker) {
if (!docker.isBusy) return const SizedBox();
final haveLog = docker.runLog != null;
Widget _buildLoading() {
if (!_docker.isBusy) return const SizedBox();
final haveLog = _docker.runLog != null;
return Padding(
padding: const EdgeInsets.all(17),
child: Column(
@@ -311,14 +336,16 @@ class _DockerManagePageState extends State<DockerManagePage> {
child: CircularProgressIndicator(),
),
haveLog ? const SizedBox(height: 17) : const SizedBox(),
haveLog ? Text(docker.runLog!) : const SizedBox()
haveLog ? Text(_docker.runLog!) : const SizedBox()
],
),
);
}
Widget _buildEditHost(List<DockerPsItem> running, DockerProvider docker) {
if (running.isNotEmpty) return const SizedBox();
Widget _buildEditHost() {
if (_docker.items!.isNotEmpty || _docker.images!.isNotEmpty) {
return const SizedBox();
}
return Padding(
padding: const EdgeInsets.fromLTRB(17, 17, 17, 0),
child: Column(
@@ -328,7 +355,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
textAlign: TextAlign.center,
),
TextButton(
onPressed: () => _showEditHostDialog(docker),
onPressed: () => _showEditHostDialog(),
child: Text(_s.dockerEditHost),
)
],
@@ -336,7 +363,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
);
}
Future<void> _showEditHostDialog(DockerProvider docker) async {
Future<void> _showEditHostDialog() async {
await showRoundDialog(
context,
_s.dockerEditHost,
@@ -347,7 +374,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
TextEditingController(text: 'unix:///run/user/1000/docker.sock'),
onSubmitted: (value) {
locator<DockerStore>().setDockerHost(widget.spi.id, value.trim());
docker.refresh();
_docker.refresh();
Navigator.of(context).pop();
},
),
@@ -407,34 +434,33 @@ class _DockerManagePageState extends State<DockerManagePage> {
);
}
Widget _buildPsItems(List<DockerPsItem> running, DockerProvider docker) {
Widget _buildPsItems() {
return ExpansionTile(
title: Text(_s.containerStatus),
subtitle: Text(_buildSubtitle(running), style: grey),
children: running.map(
subtitle: Text(_buildSubtitle(_docker.items!), style: grey),
children: _docker.items!.map(
(item) {
return ListTile(
title: Text(item.image),
subtitle: Text(item.status),
trailing:
_buildMoreBtn(item.running, item.containerId, docker.isBusy),
title: Text(item.name),
subtitle: Text('${item.image} - ${item.status}'),
trailing: _buildMoreBtn(item, _docker.isBusy),
);
},
).toList(),
);
}
Widget _buildMoreBtn(bool running, String containerId, bool busy) {
final item = running ? DockerMenuItems.stop : DockerMenuItems.start;
Widget _buildMoreBtn(DockerPsItem dItem, bool busy) {
final item = dItem.running ? DockerMenuItems.stop : DockerMenuItems.start;
return buildPopuopMenu(
items: [
PopupMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
child: item.build(_s),
),
PopupMenuItem<DropdownBtnItem>(
value: DockerMenuItems.rm,
child: DockerMenuItems.rm.build,
child: DockerMenuItems.rm.build(_s),
),
],
onSelected: (value) {
@@ -445,13 +471,26 @@ class _DockerManagePageState extends State<DockerManagePage> {
final item = value as DropdownBtnItem;
switch (item) {
case DockerMenuItems.rm:
_docker.delete(containerId);
showRoundDialog(
context,
_s.attention,
Text(_s.sureDelete(dItem.name)),
[
TextButton(
onPressed: () {
Navigator.of(context).pop();
_docker.delete(dItem.containerId);
},
child: Text(_s.ok),
)
],
);
break;
case DockerMenuItems.start:
_docker.start(containerId);
_docker.start(dItem.containerId);
break;
case DockerMenuItems.stop:
_docker.stop(containerId);
_docker.stop(dItem.containerId);
break;
}
},

View File

@@ -1,36 +1,37 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:get_it/get_it.dart';
import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/update.dart';
import 'package:toolbox/core/utils.dart';
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.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/backup.dart';
import 'package:toolbox/view/page/convert.dart';
import 'package:toolbox/view/page/debug.dart';
import 'package:toolbox/view/page/ping.dart';
import 'package:toolbox/view/page/private_key/list.dart';
import 'package:toolbox/view/page/server/tab.dart';
import 'package:toolbox/view/page/setting.dart';
import 'package:toolbox/view/page/sftp/downloaded.dart';
import 'package:toolbox/view/page/snippet/list.dart';
import 'package:toolbox/view/widget/url_text.dart';
import '../../core/analysis.dart';
import '../../core/route.dart';
import '../../core/update.dart';
import '../../core/utils/ui.dart';
import '../../data/model/app/dynamic_color.dart';
import '../../data/model/app/navigation_item.dart';
import '../../data/provider/server.dart';
import '../../data/res/build_data.dart';
import '../../data/res/tab.dart';
import '../../data/res/ui.dart';
import '../../data/res/url.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
import '../widget/url_text.dart';
import 'backup.dart';
import 'convert.dart';
import 'debug.dart';
import 'ping.dart';
import 'private_key/list.dart';
import 'server/tab.dart';
import 'setting.dart';
import 'sftp/downloaded.dart';
import 'snippet/list.dart';
final _bottomItemOverlayColor =
DynamicColor(Colors.black.withOpacity(0.07), Colors.white12);
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.primaryColor}) : super(key: key);
final Color primaryColor;
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
@@ -59,7 +60,7 @@ class _MyHomePageState extends State<MyHomePage>
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
_width = MediaQuery.of(context).size.width;
}
@@ -112,7 +113,7 @@ class _MyHomePageState extends State<MyHomePage>
);
}
Widget _buildItem(int idx, NavigationItem item, bool isSelected) {
Widget _buildBottomItem(int idx, NavigationItem item, bool isSelected) {
final width = _width / tabItems.length;
return AnimatedContainer(
duration: const Duration(milliseconds: 377),
@@ -121,9 +122,7 @@ class _MyHomePageState extends State<MyHomePage>
width: isSelected ? width : width - 17,
decoration: BoxDecoration(
color: isSelected
? isDarkMode(context)
? Colors.white12
: Colors.black.withOpacity(0.07)
? _bottomItemOverlayColor.resolve(context)
: Colors.transparent,
borderRadius: const BorderRadius.all(
Radius.circular(50),
@@ -156,7 +155,11 @@ class _MyHomePageState extends State<MyHomePage>
children: tabItems.map(
(item) {
int itemIndex = tabItems.indexOf(item);
return _buildItem(itemIndex, item, _selectIndex == itemIndex);
return _buildBottomItem(
itemIndex,
item,
_selectIndex == itemIndex,
);
},
).toList(),
),
@@ -170,8 +173,19 @@ class _MyHomePageState extends State<MyHomePage>
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildIcon(),
const Text(BuildData.name),
Text(_versionStr),
TextButton(
onPressed: () => showRoundDialog(
context,
_versionStr,
const Text(BuildData.buildAt),
[],
),
child: Text(
'${BuildData.name}\n$_versionStr',
textAlign: TextAlign.center,
style: textSize13,
),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.07,
),
@@ -189,8 +203,9 @@ class _MyHomePageState extends State<MyHomePage>
leading: const Icon(Icons.vpn_key),
title: Text(_s.privateKey),
onTap: () => AppRoute(
const StoredPrivateKeysPage(), 'private key list')
.go(context),
const PrivateKeysListPage(),
'private key list',
).go(context),
),
ListTile(
leading: const Icon(Icons.download),
@@ -213,11 +228,6 @@ class _MyHomePageState extends State<MyHomePage>
_s.feedback,
Text(_s.feedbackOnGithub),
[
TextButton(
onPressed: () => Clipboard.setData(
const ClipboardData(text: issueUrl)),
child: Text(_s.copy),
),
TextButton(
onPressed: () => openUrl(issueUrl),
child: Text(_s.feedback),
@@ -237,7 +247,7 @@ class _MyHomePageState extends State<MyHomePage>
),
AboutListTile(
icon: const Icon(Icons.text_snippet),
applicationName: BuildData.name,
applicationName: '\n${BuildData.name}',
applicationVersion: _versionStr,
applicationIcon: _buildIcon(),
aboutBoxChildren: [
@@ -247,16 +257,15 @@ class _MyHomePageState extends State<MyHomePage>
UrlText(
text: _s.aboutThanks,
),
const UrlText(
text: rainSunMeGithub,
replace: 'RainSunMe',
),
const UrlText(
text: fectureGithub,
replace: 'fecture',
// Thanks
...thanksMap.keys.map(
(key) => UrlText(
text: thanksMap[key] ?? '',
replace: key,
),
)
],
child: Text(_s.license),
child: Text(_s.about),
)
],
),
@@ -273,7 +282,7 @@ class _MyHomePageState extends State<MyHomePage>
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 53, maxWidth: 53),
child: Container(
color: primaryColor,
color: Colors.white,
),
),
ConstrainedBox(
@@ -300,6 +309,8 @@ class _MyHomePageState extends State<MyHomePage>
await GetIt.I.allReady();
await locator<ServerProvider>().loadLocalData();
await doUpdate(context);
await Analysis.init();
if (!Analysis.enabled) {
await Analysis.init();
}
}
}

View File

@@ -1,14 +1,18 @@
import 'dart:async';
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/uint8list.dart';
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/data/res/font_style.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';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../core/extension/uint8list.dart';
import '../../core/utils/ui.dart';
import '../../data/model/server/ping_result.dart';
import '../../data/provider/server.dart';
import '../../data/res/color.dart';
import '../../data/res/ui.dart';
import '../../locator.dart';
import '../widget/input_field.dart';
import '../widget/round_rect_card.dart';
final doaminReg =
RegExp(r'^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$');
@@ -24,7 +28,7 @@ class PingPage extends StatefulWidget {
}
class _PingPageState extends State<PingPage>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late TextEditingController _textEditingController;
late MediaQueryData _media;
final List<PingResult> _results = [];
@@ -41,7 +45,7 @@ class _PingPageState extends State<PingPage>
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
s = S.of(context);
s = S.of(context)!;
}
@override
@@ -53,20 +57,24 @@ class _PingPageState extends State<PingPage>
child: Column(
children: [
const SizedBox(height: 13),
buildInput(context, _textEditingController,
hint: s.inputDomainHere,
maxLines: 1,
onSubmitted: (_) => doPing()),
buildInput(
context,
_textEditingController,
hint: s.inputDomainHere,
maxLines: 1,
onSubmitted: (_) => doPing(),
),
SizedBox(
width: double.infinity,
height: _media.size.height * 0.6,
child: ListView.builder(
controller: ScrollController(),
itemCount: _results.length,
itemBuilder: (context, index) {
final result = _results[index];
return _buildResultItem(result);
}),
controller: ScrollController(),
itemCount: _results.length,
itemBuilder: (context, index) {
final result = _results[index];
return _buildResultItem(result);
},
),
),
],
),
@@ -153,7 +161,7 @@ class _PingPageState extends State<PingPage>
return;
}
final result = await e.client!.run('ping -c 3 $target').string;
_results.add(PingResult.parse(e.info.name, result));
_results.add(PingResult.parse(e.spi.name, result));
setState(() {});
}));
} catch (e) {
@@ -163,4 +171,12 @@ class _PingPageState extends State<PingPage>
@override
bool get wantKeepAlive => true;
@override
Future<FutureOr<void>> afterFirstLayout(BuildContext context) async {
if (_serverProvider.servers.isEmpty) {
await _serverProvider.loadLocalData();
await _serverProvider.refreshData();
}
}
}

View File

@@ -1,19 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/pkg/upgrade_info.dart';
import 'package:toolbox/data/model/server/dist.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/pkg.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/center_loading.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:toolbox/view/widget/two_line_text.dart';
import 'package:toolbox/view/widget/url_text.dart';
import '../../data/model/pkg/upgrade_info.dart';
import '../../data/model/server/dist.dart';
import '../../core/utils/ui.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/provider/pkg.dart';
import '../../data/provider/server.dart';
import '../../data/res/ui.dart';
import '../../locator.dart';
import '../widget/center_loading.dart';
import '../widget/round_rect_card.dart';
import '../widget/two_line_text.dart';
class PkgManagePage extends StatefulWidget {
const PkgManagePage(this.spi, {Key? key}) : super(key: key);
@@ -37,7 +36,7 @@ class _PkgManagePageState extends State<PkgManagePage>
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -49,9 +48,7 @@ class _PkgManagePageState extends State<PkgManagePage>
@override
void initState() {
super.initState();
final si = locator<ServerProvider>()
.servers
.firstWhere((e) => e.info == widget.spi);
final si = locator<ServerProvider>().getServer(widget.spi.id);
if (si.client == null) {
showSnackBar(context, Text(_s.waitConnection));
Navigator.of(context).pop();
@@ -79,7 +76,9 @@ class _PkgManagePageState extends State<PkgManagePage>
Text(_s.fieldMustNotEmpty),
[
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(_s.ok)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok),
),
],
);
return;
@@ -109,11 +108,12 @@ class _PkgManagePageState extends State<PkgManagePage>
},
child: Text(_s.cancel)),
TextButton(
onPressed: () => onSubmitted(),
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
)),
onPressed: () => onSubmitted(),
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
),
],
);
return _textController.text.trim();
@@ -124,7 +124,7 @@ class _PkgManagePageState extends State<PkgManagePage>
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: TwoLineText(up: 'Apt', down: widget.spi.name),
title: TwoLineText(up: _s.pkg, down: widget.spi.name),
),
body: Consumer<PkgProvider>(builder: (_, apt, __) {
if (apt.error != null) {
@@ -139,8 +139,10 @@ class _PkgManagePageState extends State<PkgManagePage>
const SizedBox(
height: 37,
),
SizedBox(
height: _media.size.height * 0.4,
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: _media.size.height * 0.3,
minWidth: _media.size.width),
child: Padding(
padding: const EdgeInsets.all(17),
child: RoundRectCard(
@@ -148,7 +150,6 @@ class _PkgManagePageState extends State<PkgManagePage>
padding: const EdgeInsets.all(17),
child: Text(
apt.error!,
textAlign: TextAlign.center,
),
),
),
@@ -177,16 +178,7 @@ class _PkgManagePageState extends State<PkgManagePage>
return ListView(
padding: const EdgeInsets.all(13),
children: [
Padding(
padding: const EdgeInsets.all(17),
child: UrlText(
text:
'${_s.experimentalFeature}\n${_s.reportBugsOnGithubIssue(issueUrl)}',
replace: 'Github Issue',
textAlign: TextAlign.center,
),
),
_buildUpdatePanel(apt)
_buildUpdatePanel(apt),
].map((e) => RoundRectCard(e)).toList(),
);
}),
@@ -203,49 +195,31 @@ class _PkgManagePageState extends State<PkgManagePage>
subtitle: const Text('>_<', textAlign: TextAlign.center),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ExpansionTile(
title: Text(_s.foundNUpdate(apt.upgradeable!.length)),
subtitle: Text(
apt.upgradeable!.map((e) => e.package).join(', '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: grey,
),
children: apt.upgradeLog == null
? [
TextButton(
child: Text(_s.updateAll),
onPressed: () {
apt.upgrade();
}),
SizedBox(
height: _media.size.height * 0.73,
child: ListView(
controller: _scrollController,
children: apt.upgradeable!
.map((e) => _buildUpdateItem(e, apt))
.toList(),
),
)
]
: [
SizedBox(
height: _media.size.height * 0.7,
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: SingleChildScrollView(
padding: const EdgeInsets.all(18),
controller: _scrollController,
child: Text(apt.upgradeLog!),
),
),
)
],
)
],
return ExpansionTile(
title: Text(_s.foundNUpdate(apt.upgradeable!.length)),
subtitle: Text(
apt.upgradeable!.map((e) => e.package).join(', '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: grey,
),
children: apt.upgradeLog == null
? [
TextButton(
child: Text(_s.updateAll),
onPressed: () {
apt.upgrade();
},
),
...apt.upgradeable!.map((e) => _buildUpdateItem(e, apt)).toList()
]
: [
SingleChildScrollView(
padding: const EdgeInsets.all(18),
controller: _scrollController,
child: Text(apt.upgradeLog!),
)
],
);
}

View File

@@ -1,15 +1,21 @@
import 'dart:io';
import 'package:after_layout/after_layout.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/utils/misc.dart';
import 'package:toolbox/data/res/misc.dart';
import '../../../core/utils/server.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/private_key_info.dart';
import '../../../data/provider/private_key.dart';
import '../../../data/res/ui.dart';
import '../../../locator.dart';
import '../../widget/input_decoration.dart';
const _format = 'text/plain';
@@ -47,7 +53,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
_focusScope = FocusScope.of(context);
}
@@ -89,6 +95,38 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
enableSuggestions: false,
decoration: buildDecoration(_s.privateKey, icon: Icons.vpn_key),
),
TextButton(
onPressed: () async {
final path = await pickOneFile();
if (path == null) {
showSnackBar(context, const Text('path is null'));
return;
}
final file = File(path);
if (!file.existsSync()) {
showSnackBar(context, Text(_s.fileNotExist(path)));
return;
}
final size = (await file.stat()).size;
if (size > privateKeyMaxSize) {
showSnackBar(
context,
Text(
_s.fileTooLarge(
path,
size.convertBytes,
privateKeyMaxSize.convertBytes,
),
),
);
return;
}
_keyController.text = await file.readAsString();
},
child: Text(_s.pickFile),
),
TextField(
controller: _pwdController,
autocorrect: false,
@@ -120,7 +158,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
),
);
});
final info = PrivateKeyInfo(name, key, pwd);
final info = PrivateKeyInfo(name, key, '');
bool haveErr = false;
try {
info.privateKey = await compute(decyptPem, [key, pwd]);
@@ -159,11 +197,3 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
}
}
}
/// [args] : [key, pwd]
String decyptPem(List<String> args) {
/// skip when the key is not encrypted, or will throw exception
if (!SSHKeyPair.isEncryptedPem(args[0])) return args[0];
final sshKey = SSHKeyPair.fromPem(args[0], args[1]);
return sshKey.first.toPem();
}

View File

@@ -1,27 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/provider/private_key.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';
class StoredPrivateKeysPage extends StatefulWidget {
const StoredPrivateKeysPage({Key? key}) : super(key: key);
import '../../../core/route.dart';
import '../../../data/provider/private_key.dart';
import '../../../data/res/ui.dart';
import 'edit.dart';
import '../../../view/widget/round_rect_card.dart';
class PrivateKeysListPage extends StatefulWidget {
const PrivateKeysListPage({Key? key}) : super(key: key);
@override
_PrivateKeyListState createState() => _PrivateKeyListState();
}
class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
class _PrivateKeyListState extends State<PrivateKeysListPage> {
late S _s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -32,36 +32,31 @@ class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
),
body: Consumer<PrivateKeyProvider>(
builder: (_, key, __) {
return key.infos.isNotEmpty
? ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: key.infos.length,
itemExtent: 57,
itemBuilder: (context, idx) {
return RoundRectCard(Padding(
padding: roundRectCardPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
key.infos[idx].id,
textAlign: TextAlign.center,
),
TextButton(
onPressed: () => AppRoute(
PrivateKeyEditPage(info: key.infos[idx]),
'private key edit page')
.go(context),
child: Text(_s.edit),
)
],
),
));
})
: Center(
child: Text(_s.noSavedPrivateKey),
);
if (key.infos.isEmpty) {
return Center(
child: Text(_s.noSavedPrivateKey),
);
}
return ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: key.infos.length,
itemBuilder: (context, idx) {
return RoundRectCard(
ListTile(
title: Text(
key.infos[idx].id,
),
trailing: TextButton(
onPressed: () => AppRoute(
PrivateKeyEditPage(info: key.infos[idx]),
'private key edit page',
).go(context),
child: Text(_s.edit),
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(

View File

@@ -1,18 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/data/model/server/dist.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
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/padding.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';
import '../../../core/extension/numx.dart';
import '../../../data/model/server/dist.dart';
import '../../../data/model/server/net_speed.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_status.dart';
import '../../../data/provider/server.dart';
import '../../../data/res/color.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/round_rect_card.dart';
class ServerDetailPage extends StatefulWidget {
const ServerDetailPage(this.id, {Key? key}) : super(key: key);
@@ -27,33 +27,29 @@ class _ServerDetailPageState extends State<ServerDetailPage>
with SingleTickerProviderStateMixin {
late MediaQueryData _media;
late S _s;
late Color pColor;
bool _showDistLogo = true;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_s = S.of(context);
_s = S.of(context)!;
_showDistLogo = locator<SettingStore>().showDistLogo.fetch()!;
pColor = primaryColor;
}
@override
Widget build(BuildContext context) {
return Consumer<ServerProvider>(builder: (_, provider, __) {
return _buildMainPage(
provider.servers.firstWhere(
(e) => '${e.info.ip}:${e.info.port}' == widget.id,
),
provider.getServer(widget.id),
);
});
}
Widget _buildMainPage(ServerInfo si) {
Widget _buildMainPage(Server si) {
return Scaffold(
appBar: AppBar(
title: Text(si.info.name, style: textSize18),
title: Text(si.spi.name, style: textSize18),
),
body: ListView(
padding: const EdgeInsets.all(13),
@@ -62,6 +58,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
_buildUpTimeAndSys(si.status),
_buildCPUView(si.status),
_buildMemView(si.status),
_buildSwapView(si.status),
_buildDiskView(si.status),
_buildNetView(si.status.netSpeed),
// avoid the hieght of navigation bar
@@ -92,45 +89,46 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildCPUView(ServerStatus ss) {
final tempWidget = ss.cpu.temp.isEmpty
? const SizedBox()
: Text(
ss.cpu.temp,
style: textSize13Grey,
);
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: SizedBox(
height: 12 * ss.cpu2Status.coresCount + 63,
child: Column(children: [
SizedBox(
height: _media.size.height * 0.02,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${ss.cpu2Status.usedPercent(coreIdx: 0).toInt()}%',
style: textSize27,
textScaleFactor: 1.0,
),
Row(
children: [
_buildDetailPercent(ss.cpu2Status.user, 'user'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildDetailPercent(ss.cpu2Status.sys, 'sys'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildDetailPercent(ss.cpu2Status.iowait, 'io'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildDetailPercent(ss.cpu2Status.idle, 'idle')
],
)
],
),
_buildCPUProgress(ss)
]),
),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'${ss.cpu.usedPercent(coreIdx: 0).toInt()}%',
style: textSize27,
textScaleFactor: 1.0,
),
width7,
tempWidget
],
),
Row(
children: [
_buildDetailPercent(ss.cpu.user, 'user'),
width13,
_buildDetailPercent(ss.cpu.sys, 'sys'),
width13,
_buildDetailPercent(ss.cpu.iowait, 'io'),
width13,
_buildDetailPercent(ss.cpu.idle, 'idle')
],
)
],
),
height13,
_buildCPUProgress(ss)
]),
),
);
}
@@ -156,21 +154,17 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildCPUProgress(ServerStatus ss) {
return SizedBox(
height: 12.0 * ss.cpu2Status.coresCount,
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 13),
itemBuilder: (ctx, idx) {
if (idx == 0) return const SizedBox();
return Padding(
padding: const EdgeInsets.all(2),
child: _buildProgress(ss.cpu2Status.usedPercent(coreIdx: idx)),
);
},
itemCount: ss.cpu2Status.coresCount,
),
);
final children = <Widget>[];
for (var i = 0; i < ss.cpu.coresCount; i++) {
if (i == 0) continue;
children.add(
Padding(
padding: const EdgeInsets.all(2),
child: _buildProgress(ss.cpu.usedPercent(coreIdx: i)),
),
);
}
return Column(children: children);
}
Widget _buildProgress(double percent) {
@@ -180,36 +174,37 @@ class _ServerDetailPageState extends State<ServerDetailPage>
value: percentWithinOne,
minHeight: 7,
backgroundColor: progressColor.resolve(context),
color: pColor.withOpacity(0.5 + percentWithinOne / 2),
color: primaryColor,
);
}
Widget _buildUpTimeAndSys(ServerStatus ss) {
return RoundRectCard(Padding(
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(ss.sysVer, style: textSize11, textScaleFactor: 1.0),
Text(
ss.uptime,
style: textSize11,
textScaleFactor: 1.0,
),
],
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(ss.sysVer, style: textSize11, textScaleFactor: 1.0),
Text(
ss.uptime,
style: textSize11,
textScaleFactor: 1.0,
),
],
),
),
));
);
}
Widget _buildMemView(ServerStatus ss) {
final used = ss.memory.used / ss.memory.total * 100;
final free = ss.memory.free / ss.memory.total * 100;
final avail = ss.memory.avail / ss.memory.total * 100;
final used = ss.mem.used / ss.mem.total * 100;
final free = ss.mem.free / ss.mem.total * 100;
final avail = ss.mem.avail / ss.mem.total * 100;
return RoundRectCard(Padding(
padding: roundRectCardPadding,
child: SizedBox(
height: 70,
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
@@ -220,50 +215,73 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Row(
children: [
Text('${used.toStringAsFixed(0)}%', style: textSize27),
const SizedBox(width: 7),
Text('of ${(ss.memory.total * 1024).convertBytes}',
width7,
Text('of ${(ss.mem.total * 1024).convertBytes}',
style: textSize13Grey)
],
),
Row(
children: [
_buildDetailPercent(free, 'free'),
SizedBox(
width: _media.size.width * 0.03,
),
width13,
_buildDetailPercent(avail, 'avail'),
],
),
],
),
const SizedBox(
height: 11,
),
height13,
_buildProgress(used)
],
),
),
));
);
}
Widget _buildSwapView(ServerStatus ss) {
if (ss.swap.total == 0) return const SizedBox();
final used = ss.swap.used / ss.swap.total * 100;
final cached = ss.swap.cached / ss.swap.total * 100;
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text('${used.toStringAsFixed(0)}%', style: textSize27),
width7,
Text('of ${(ss.swap.total * 1024).convertBytes} ',
style: textSize13Grey)
],
),
_buildDetailPercent(cached, 'cached'),
],
),
height13,
_buildProgress(used)
],
),
),
);
}
Widget _buildDiskView(ServerStatus ss) {
final clone = ss.disk.toList();
for (var item in ss.disk) {
if (ignorePath.any((ele) => item.mountLocation.contains(ele))) {
if (_ignorePath.any((ele) => item.path.startsWith(ele))) {
clone.remove(item);
}
}
return RoundRectCard(SizedBox(
height: 27 * clone.length + 25,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
physics: const NeverScrollableScrollPhysics(),
itemCount: clone.length,
itemBuilder: (_, idx) {
final disk = clone[idx];
return Padding(
padding: const EdgeInsets.all(3),
final children = clone
.map((disk) => Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -273,16 +291,23 @@ class _ServerDetailPageState extends State<ServerDetailPage>
style: textSize11,
textScaleFactor: 1.0,
),
Text(disk.mountPath,
style: textSize11, textScaleFactor: 1.0)
Text(disk.path, style: textSize11, textScaleFactor: 1.0)
],
),
_buildProgress(disk.usedPercent.toDouble())
],
),
);
}),
));
))
.toList();
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
),
);
}
Widget _buildNetView(NetSpeed ns) {
@@ -303,24 +328,23 @@ class _ServerDetailPageState extends State<ServerDetailPage>
children.addAll(ns.devices.map((e) => _buildNetSpeedItem(ns, e)));
}
return RoundRectCard(Padding(
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
child: Column(
children: children,
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Column(
children: children,
),
),
));
);
}
Widget _buildNetSpeedTop() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
padding: const EdgeInsets.only(bottom: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(
Icons.device_hub,
size: 17,
),
Icon(Icons.device_hub, size: 17),
Icon(Icons.arrow_downward, size: 17),
Icon(Icons.arrow_upward, size: 17),
],
@@ -329,39 +353,42 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildNetSpeedItem(NetSpeed ns, String device) {
final width = (_media.size.width - 34 - 34) / 3;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: _media.size.width / 4,
child: Text(device, style: textSize11, textScaleFactor: 1.0)),
SizedBox(
width: _media.size.width / 4,
child: Text(ns.speedIn(device: device),
style: textSize11,
textAlign: TextAlign.center,
textScaleFactor: 1.0),
width: width,
child: Text(
device,
style: textSize11,
textScaleFactor: 1.0,
),
),
SizedBox(
width: _media.size.width / 4,
child: Text(ns.speedOut(device: device),
style: textSize11,
textAlign: TextAlign.right,
textScaleFactor: 1.0),
width: width,
child: Text(
'${ns.speedIn(device: device)} | ${ns.totalIn(device: device)}',
style: textSize11,
textAlign: TextAlign.center,
textScaleFactor: 0.9,
),
),
SizedBox(
width: width,
child: Text(
'${ns.speedOut(device: device)} | ${ns.totalOut(device: device)}',
style: textSize11,
textAlign: TextAlign.right,
textScaleFactor: 0.9,
),
)
],
),
);
}
static const ignorePath = [
'/run',
'/sys',
'/dev/shm',
'/snap',
'/var/lib/docker',
'/dev/tty'
];
static const _ignorePath = ['udev', 'tmpfs', 'devtmpfs'];
}

View File

@@ -1,19 +1,20 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
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';
import '../../../core/route.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/private_key_info.dart';
import '../../../data/model/server/server_private_info.dart';
import '../../../data/provider/private_key.dart';
import '../../../data/provider/server.dart';
import '../../../data/res/color.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/private_key.dart';
import '../../../locator.dart';
import '../../widget/input_decoration.dart';
import '../private_key/edit.dart';
class ServerEditPage extends StatefulWidget {
const ServerEditPage({Key? key, this.spi}) : super(key: key);
@@ -52,7 +53,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
_focusScope = FocusScope.of(context);
}
@@ -72,7 +73,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
[
TextButton(
onPressed: () {
_serverProvider.delServer(widget.spi!);
_serverProvider.delServer(widget.spi!.id);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
@@ -104,8 +105,11 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
keyboardType: TextInputType.text,
focusNode: _nameFocus,
onSubmitted: (_) => _focusScope.requestFocus(_ipFocus),
decoration: buildDecoration(_s.name,
icon: Icons.info, hint: _s.exampleName),
decoration: buildDecoration(
_s.name,
icon: Icons.info,
hint: _s.exampleName,
),
),
TextField(
controller: _ipController,
@@ -114,16 +118,22 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
focusNode: _ipFocus,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration(_s.host,
icon: Icons.storage, hint: 'example.com'),
decoration: buildDecoration(
_s.host,
icon: Icons.storage,
hint: 'example.com',
),
),
TextField(
controller: _portController,
keyboardType: TextInputType.number,
focusNode: _portFocus,
onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),
decoration: buildDecoration(_s.port,
icon: Icons.format_list_numbered, hint: '22'),
decoration: buildDecoration(
_s.port,
icon: Icons.format_list_numbered,
hint: '22',
),
),
TextField(
controller: _usernameController,
@@ -131,16 +141,20 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
focusNode: _usernameFocus,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration(_s.user,
icon: Icons.account_box, hint: 'root'),
decoration: buildDecoration(
_s.user,
icon: Icons.account_box,
hint: 'root',
),
),
const SizedBox(height: 7),
Row(
children: [
Text(_s.keyAuth),
Switch(
value: usePublicKey,
onChanged: (val) => setState(() => usePublicKey = val)),
value: usePublicKey,
onChanged: (val) => setState(() => usePublicKey = val),
),
],
),
!usePublicKey
@@ -148,8 +162,11 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
controller: _passwordController,
obscureText: true,
keyboardType: TextInputType.text,
decoration: buildDecoration(_s.pwd,
icon: Icons.password, hint: _s.pwd),
decoration: buildDecoration(
_s.pwd,
icon: Icons.password,
hint: _s.pwd,
),
onSubmitted: (_) => {},
)
: const SizedBox(),
@@ -164,9 +181,10 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final tiles = key.infos
.map(
(e) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(e.id, textAlign: TextAlign.start),
trailing: _buildRadio(key.infos.indexOf(e), e)),
contentPadding: EdgeInsets.zero,
title: Text(e.id, textAlign: TextAlign.start),
trailing: _buildRadio(key.infos.indexOf(e), e),
),
)
.toList();
tiles.add(
@@ -176,9 +194,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () => AppRoute(
const PrivateKeyEditPage(),
'private key edit page')
.go(context),
const PrivateKeyEditPage(),
'private key edit page',
).go(context),
),
),
);
@@ -187,6 +205,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
iconColor: primaryColor,
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
initiallyExpanded: true,
title: Text(
_s.choosePrivateKey,
style: const TextStyle(fontSize: 14),
@@ -213,11 +232,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
Text(_s.sureNoPwd),
[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(_s.ok)),
onPressed: () => Navigator.of(context).pop(false),
child: Text(_s.ok),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(_s.cancel))
onPressed: () => Navigator.of(context).pop(true),
child: Text(_s.cancel),
)
],
barrierDismiss: false,
);

View File

@@ -1,32 +1,34 @@
import 'package:after_layout/after_layout.dart';
import 'package:circle_chart/circle_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:get_it/get_it.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/menu_item.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.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/url.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/pkg.dart';
import 'package:toolbox/view/page/docker.dart';
import 'package:toolbox/view/page/server/detail.dart';
import 'package:toolbox/view/page/server/edit.dart';
import 'package:toolbox/view/page/sftp/view.dart';
import 'package:toolbox/view/page/snippet/edit.dart';
import 'package:toolbox/view/page/ssh.dart';
import 'package:toolbox/view/widget/picker.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../../core/route.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_private_info.dart';
import '../../../data/model/server/server_status.dart';
import '../../../data/provider/server.dart';
import '../../../data/provider/snippet.dart';
import '../../../data/res/color.dart';
import '../../../data/res/menu.dart';
import '../../../data/res/ui.dart';
import '../../../data/res/url.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/dropdown_menu.dart';
import '../../widget/picker.dart';
import '../../widget/round_rect_card.dart';
import '../../widget/url_text.dart';
import '../docker.dart';
import '../pkg.dart';
import '../sftp/view.dart';
import '../snippet/edit.dart';
import '../ssh.dart';
import 'detail.dart';
import 'edit.dart';
class ServerPage extends StatefulWidget {
const ServerPage({Key? key}) : super(key: key);
@@ -39,14 +41,15 @@ class _ServerPageState extends State<ServerPage>
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late MediaQueryData _media;
late ThemeData _theme;
late Color _primaryColor;
late ServerProvider _serverProvider;
late SettingStore _settingStore;
late S _s;
@override
void initState() {
super.initState();
_serverProvider = locator<ServerProvider>();
_settingStore = locator<SettingStore>();
}
@override
@@ -54,45 +57,49 @@ class _ServerPageState extends State<ServerPage>
super.didChangeDependencies();
_media = MediaQuery.of(context);
_theme = Theme.of(context);
_primaryColor = primaryColor;
_s = S.of(context);
_s = S.of(context)!;
}
@override
Widget build(BuildContext context) {
super.build(context);
final child = Consumer<ServerProvider>(
builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return Center(
child: Text(
_s.serverTabEmpty,
textAlign: TextAlign.center,
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(7),
controller: ScrollController(),
itemBuilder: (ctx, idx) {
if (idx == pro.servers.length) {
return SizedBox(height: _media.padding.bottom);
}
return _buildEachServerCard(pro.servers[idx]);
},
itemCount: pro.servers.length + 1,
separatorBuilder: (_, __) => const SizedBox(
height: 3,
),
);
},
);
return Scaffold(
body: child,
body: RefreshIndicator(
onRefresh: () async =>
await _serverProvider.refreshData(onlyFailed: true),
child: Consumer<ServerProvider>(
builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return Center(
child: Text(
_s.serverTabEmpty,
textAlign: TextAlign.center,
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(7),
controller: ScrollController(),
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (ctx, idx) {
if (idx == pro.servers.length) {
return SizedBox(height: _media.padding.bottom);
}
return _buildEachServerCard(pro.servers[idx]);
},
itemCount: pro.servers.length + 1,
separatorBuilder: (_, __) => const SizedBox(
height: 3,
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
AppRoute(const ServerEditPage(), 'Add server info page')
.go(context),
onPressed: () => AppRoute(
const ServerEditPage(),
'Add server info page',
).go(context),
tooltip: _s.addAServer,
heroTag: 'server page fab',
child: const Icon(Icons.add),
@@ -100,36 +107,32 @@ class _ServerPageState extends State<ServerPage>
);
}
Widget _buildEachServerCard(ServerInfo si) {
Widget _buildEachServerCard(Server si) {
return RoundRectCard(
InkWell(
onLongPress: () => AppRoute(
ServerEditPage(
spi: si.info,
spi: si.spi,
),
'Edit server info page')
.go(context),
child: Padding(
padding: const EdgeInsets.all(13),
child: _buildRealServerCard(
si.status, si.info.name, si.connectionState, si.info),
child: _buildRealServerCard(si.status, si.spi.name, si.cs, si.spi),
),
onTap: () => AppRoute(ServerDetailPage('${si.info.ip}:${si.info.port}'),
'server detail page')
onTap: () => AppRoute(ServerDetailPage(si.spi.id), 'server detail page')
.go(context),
),
);
}
Widget _buildRealServerCard(ServerStatus ss, String serverName,
ServerConnectionState cs, ServerPrivateInfo spi) {
final rootDisk =
ss.disk.firstWhere((element) => element.mountLocation == '/');
ServerState cs, ServerPrivateInfo spi) {
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/');
final topRightStr =
getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime, ss.failedInfo);
final hasError =
cs == ServerConnectionState.failed && ss.failedInfo != null;
getTopRightStr(cs, ss.cpu.temp, ss.uptime, ss.failedInfo);
final hasError = cs == ServerState.failed && ss.failedInfo != null;
final style = TextStyle(
color: _theme.textTheme.bodyLarge!.color!.withAlpha(100), fontSize: 11);
@@ -162,7 +165,11 @@ class _ServerPageState extends State<ServerPage>
? GestureDetector(
onTap: () => showRoundDialog(
context, _s.error, Text(ss.failedInfo ?? ''), []),
child: Text(_s.clickSee, style: style))
child: Text(
_s.clickSee,
style: style,
textScaleFactor: 1.0,
))
: Text(topRightStr, style: style, textScaleFactor: 1.0),
const SizedBox(width: 9),
_buildSSHBtn(spi),
@@ -178,8 +185,8 @@ class _ServerPageState extends State<ServerPage>
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildPercentCircle(ss.cpu2Status.usedPercent()),
_buildPercentCircle(ss.memory.used / ss.memory.total * 100),
_buildPercentCircle(ss.cpu.usedPercent()),
_buildPercentCircle(ss.mem.used / ss.mem.total * 100),
_buildIOData('Conn:\n${ss.tcp.maxConn}', 'Fail:\n${ss.tcp.fail}'),
_buildIOData(
'Total:\n${rootDisk.size}', 'Used:\n${rootDisk.usedPercent}%')
@@ -206,22 +213,30 @@ class _ServerPageState extends State<ServerPage>
Icons.terminal,
size: 21,
),
onTap: () => showRoundDialog(
context,
_s.attention,
UrlText(
text: _s.sshTip(issueUrl),
replace: 'Github Issue',
),
[
TextButton(
onPressed: () {
Navigator.of(context).pop();
AppRoute(SSHPage(spi: spi), 'ssh page').go(context);
},
child: Text(_s.ok),
)
]),
onTap: () async {
if (_settingStore.firstTimeUseSshTerm.fetch()!) {
await showRoundDialog(
context,
_s.attention,
UrlText(
text: _s.sshTip(issueUrl),
replace: 'Github Issue',
),
[
TextButton(
onPressed: () {
Navigator.of(context).pop();
AppRoute(SSHPage(spi: spi), 'ssh page').go(context);
},
child: Text(_s.ok),
)
],
);
_settingStore.firstTimeUseSshTerm.put(false);
} else {
AppRoute(SSHPage(spi: spi), 'ssh page').go(context);
}
},
);
}
@@ -231,14 +246,14 @@ class _ServerPageState extends State<ServerPage>
...ServerTabMenuItems.firstItems.map(
(item) => PopupMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
child: item.build(_s),
),
),
const PopupMenuDivider(height: 1),
...ServerTabMenuItems.secondItems.map(
(item) => PopupMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
child: item.build(_s),
),
),
],
@@ -276,12 +291,12 @@ class _ServerPageState extends State<ServerPage>
);
}
String getTopRightStr(ServerConnectionState cs, String temp, String upTime,
String? failedInfo) {
String getTopRightStr(
ServerState cs, String temp, String upTime, String? failedInfo) {
switch (cs) {
case ServerConnectionState.disconnected:
case ServerState.disconnected:
return _s.disconnected;
case ServerConnectionState.connected:
case ServerState.connected:
if (temp == '') {
if (upTime == '') {
return _s.serverTabLoading;
@@ -295,9 +310,9 @@ class _ServerPageState extends State<ServerPage>
return '$temp | $upTime';
}
}
case ServerConnectionState.connecting:
case ServerState.connecting:
return _s.serverTabConnecting;
case ServerConnectionState.failed:
case ServerState.failed:
if (failedInfo == null) {
return _s.serverTabFailed;
}
@@ -345,7 +360,7 @@ class _ServerPageState extends State<ServerPage>
children: [
Center(
child: CircleChart(
progressColor: _primaryColor,
progressColor: primaryColor,
progressNumber: percent,
maxNumber: 100,
width: 53,
@@ -380,8 +395,10 @@ class _ServerPageState extends State<ServerPage>
child: Text(_s.ok),
),
TextButton(
onPressed: () =>
AppRoute(const SnippetEditPage(), 'edit snippet').go(context),
onPressed: () {
Navigator.of(context).pop();
AppRoute(const SnippetEditPage(), 'edit snippet').go(context);
},
child: Text(_s.addOne),
)
],
@@ -393,22 +410,24 @@ class _ServerPageState extends State<ServerPage>
showRoundDialog(
context,
_s.choose,
buildPicker(provider.snippets.map((e) => Text(e.name)).toList(),
(idx) => snippet = provider.snippets[idx]),
buildPicker(
provider.snippets.map((e) => Text(e.name)).toList(),
(idx) => snippet = provider.snippets[idx],
),
[
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final result =
await locator<ServerProvider>().runSnippet(id, snippet);
final result = await _serverProvider.runSnippet(id, snippet);
showRoundDialog(
context,
_s.result,
Text(result ?? _s.error, style: textSize13),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok))
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok),
)
],
);
},
@@ -424,8 +443,9 @@ class _ServerPageState extends State<ServerPage>
@override
Future<void> afterFirstLayout(BuildContext context) async {
await GetIt.I.allReady();
await _serverProvider.loadLocalData();
await _serverProvider.refreshData();
if (_serverProvider.servers.isEmpty) {
await _serverProvider.loadLocalData();
}
_serverProvider.startAutoRefresh();
}
}

View File

@@ -1,19 +1,26 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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';
import 'package:toolbox/data/res/path.dart';
import '../../core/utils/misc.dart';
import '../../core/utils/platform.dart';
import '../../data/model/ssh/terminal_color.dart';
import '../../core/update.dart';
import '../../core/utils/ui.dart';
import '../../data/provider/app.dart';
import '../../data/provider/server.dart';
import '../../data/res/build_data.dart';
import '../../data/res/color.dart';
import '../../data/res/tab.dart';
import '../../data/res/ui.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
import '../widget/future_widget.dart';
import '../widget/round_rect_card.dart';
class SettingPage extends StatefulWidget {
const SettingPage({Key? key}) : super(key: key);
@@ -24,23 +31,24 @@ class SettingPage extends StatefulWidget {
class _SettingPageState extends State<SettingPage> {
late final SettingStore _setting;
late int _selectedColorValue;
late int _launchPageIdx;
late Color priColor;
late final ServerProvider _serverProvider;
late MediaQueryData _media;
late ThemeData _theme;
late S _s;
var _updateInterval = 5.0;
late int _selectedColorValue;
late int _launchPageIdx;
late int _termThemeIdx;
late int _nightMode;
late double _maxRetryCount;
late double _updateInterval;
String? _pushToken;
@override
void didChangeDependencies() {
super.didChangeDependencies();
priColor = primaryColor;
_media = MediaQuery.of(context);
_theme = Theme.of(context);
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -49,7 +57,10 @@ class _SettingPageState extends State<SettingPage> {
_serverProvider = locator<ServerProvider>();
_setting = locator<SettingStore>();
_launchPageIdx = _setting.launchPage.fetch()!;
_termThemeIdx = _setting.termColorIdx.fetch()!;
_nightMode = _setting.themeMode.fetch()!;
_updateInterval = _setting.serverStatusUpdateInterval.fetch()!.toDouble();
_maxRetryCount = _setting.maxRetryCount.fetch()!.toDouble();
}
@override
@@ -59,27 +70,67 @@ class _SettingPageState extends State<SettingPage> {
title: Text(_s.setting),
),
body: ListView(
padding: const EdgeInsets.all(17),
padding: const EdgeInsets.symmetric(horizontal: 17),
children: [
_buildAppColorPreview(),
_buildUpdateInterval(),
_buildCheckUpdate(),
_buildLaunchPage(),
_buildDistLogoSwitch(),
].map((e) => RoundRectCard(e)).toList(),
// App
_buildTitle('App'),
_buildApp(),
// Server
_buildTitle(_s.server),
_buildServer(),
const SizedBox(height: 37),
],
),
);
}
Widget _buildTitle(String text) {
return Padding(
padding: const EdgeInsets.only(top: 23, bottom: 17),
child: Center(
child: Text(
text,
style: grey,
),
),
);
}
Widget _buildApp() {
final children = [
_buildThemeMode(),
_buildAppColorPreview(),
_buildLaunchPage(),
_buildCheckUpdate(),
_buildFont(),
];
if (isIOS) {
children.add(_buildPushToken());
}
return Column(
children: children.map((e) => RoundRectCard(e)).toList(),
);
}
Widget _buildServer() {
return Column(
children: [
_buildDistLogoSwitch(),
_buildUpdateInterval(),
_buildTermTheme(),
_buildMaxRetry(),
].map((e) => RoundRectCard(e)).toList(),
);
}
Widget _buildDistLogoSwitch() {
return ListTile(
title: Text(
_s.showDistLogo,
style: textSize13,
),
subtitle: Text(
_s.onServerDetailPage,
style: textSize13Grey,
style: grey,
),
trailing: buildSwitch(context, _setting.showDistLogo),
);
@@ -99,12 +150,9 @@ class _SettingPageState extends State<SettingPage> {
display = _s.versionUnknownUpdate(BuildData.build);
}
return ListTile(
contentPadding: roundRectCardPadding,
trailing: const Icon(Icons.keyboard_arrow_right),
title: Text(
display,
style: textSize13,
textAlign: TextAlign.start,
),
onTap: () => doUpdate(context, force: true),
);
@@ -114,23 +162,21 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildUpdateInterval() {
return ExpansionTile(
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
textColor: priColor,
textColor: primaryColor,
title: Text(
_s.updateServerStatusInterval,
style: textSize13,
textAlign: TextAlign.start,
),
subtitle: Text(
_s.willTakEeffectImmediately,
style: textSize13Grey,
style: grey,
),
trailing: Text(
'${_updateInterval.toInt()} ${_s.second}',
),
trailing: Text('${_updateInterval.toInt()} ${_s.second}'),
children: [
Slider(
thumbColor: priColor,
activeColor: priColor.withOpacity(0.7),
thumbColor: primaryColor,
activeColor: primaryColor.withOpacity(0.7),
min: 0,
max: 10,
value: _updateInterval,
@@ -152,7 +198,7 @@ class _SettingPageState extends State<SettingPage> {
_updateInterval == 0.0
? Text(
_s.updateIntervalEqual0,
style: const TextStyle(color: Colors.grey, fontSize: 12),
style: grey,
textAlign: TextAlign.center,
)
: const SizedBox(),
@@ -165,31 +211,29 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildAppColorPreview() {
return ExpansionTile(
textColor: priColor,
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
textColor: primaryColor,
trailing: ClipOval(
child: Container(
color: priColor,
color: primaryColor,
height: 27,
width: 27,
),
),
title: Text(
_s.appPrimaryColor,
style: textSize13,
),
children: [_buildAppColorPicker(priColor), _buildColorPickerConfirmBtn()],
children: [_buildAppColorPicker(), _buildColorPickerConfirmBtn()],
);
}
Widget _buildAppColorPicker(Color selected) {
Widget _buildAppColorPicker() {
return MaterialColorPicker(
shrinkWrap: true,
onColorChange: (Color color) {
_selectedColorValue = color.value;
},
selectedColor: selected);
shrinkWrap: true,
onColorChange: (Color color) {
_selectedColorValue = color.value;
},
selectedColor: primaryColor,
);
}
Widget _buildColorPickerConfirmBtn() {
@@ -197,25 +241,22 @@ class _SettingPageState extends State<SettingPage> {
icon: const Icon(Icons.save),
onPressed: (() {
_setting.primaryColor.put(_selectedColorValue);
setState(() {});
_showRestartSnackbar();
}),
);
}
Widget _buildLaunchPage() {
return ExpansionTile(
textColor: priColor,
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
childrenPadding: const EdgeInsets.only(left: 17, right: 7),
textColor: primaryColor,
title: Text(
_s.launchPage,
style: textSize13,
),
trailing: ConstrainedBox(
constraints: BoxConstraints(maxWidth: _media.size.width * 0.35),
child: Text(
tabTitleName(context, _launchPageIdx),
style: textSize13,
textAlign: TextAlign.right,
),
),
@@ -225,9 +266,6 @@ class _SettingPageState extends State<SettingPage> {
contentPadding: EdgeInsets.zero,
title: Text(
tabTitleName(context, tabs.indexOf(e)),
style: TextStyle(
fontSize: 14,
color: _theme.textTheme.bodyMedium!.color!.withAlpha(177)),
),
trailing: _buildRadio(tabs.indexOf(e)),
),
@@ -248,4 +286,216 @@ class _SettingPageState extends State<SettingPage> {
},
);
}
Widget _buildTermTheme() {
return ExpansionTile(
textColor: primaryColor,
childrenPadding: const EdgeInsets.only(left: 17),
title: Text(
_s.termTheme,
),
trailing: Text(
TerminalColorsPlatform.values[_termThemeIdx].name,
),
children: _buildTermThemeRadioList(),
);
}
List<Widget> _buildTermThemeRadioList() {
return TerminalColorsPlatform.values
.map(
(e) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
e.name,
),
trailing: _buildTermThemeRadio(e),
),
)
.toList();
}
Radio _buildTermThemeRadio(TerminalColorsPlatform platform) {
return Radio<int>(
value: platform.index,
groupValue: _termThemeIdx,
onChanged: (int? value) {
setState(() {
value ??= 0;
_termThemeIdx = value!;
_setting.termColorIdx.put(value!);
});
},
);
}
Widget _buildMaxRetry() {
return ExpansionTile(
textColor: primaryColor,
title: Text(
_s.maxRetryCount,
textAlign: TextAlign.start,
),
trailing: Text(
'${_maxRetryCount.toInt()} ${_s.times}',
),
children: [
Slider(
thumbColor: primaryColor,
activeColor: primaryColor.withOpacity(0.7),
min: 0,
max: 10,
value: _maxRetryCount,
onChanged: (newValue) {
setState(() {
_maxRetryCount = newValue;
});
},
onChangeEnd: (val) {
_setting.maxRetryCount.put(val.toInt());
},
label: '${_maxRetryCount.toInt()} ${_s.times}',
divisions: 10,
),
const SizedBox(
height: 3,
),
_maxRetryCount == 0.0
? Text(
_s.maxRetryCountEqual0,
style: grey,
textAlign: TextAlign.center,
)
: const SizedBox(),
const SizedBox(
height: 13,
)
],
);
}
Widget _buildThemeMode() {
return ExpansionTile(
textColor: primaryColor,
title: Text(
_s.themeMode,
),
trailing: Text(
_buildThemeModeStr(_nightMode),
),
children: [
Slider(
thumbColor: primaryColor,
activeColor: primaryColor.withOpacity(0.7),
min: 0,
max: 2,
value: _nightMode.toDouble(),
onChanged: (newValue) {
setState(() {
_nightMode = newValue.toInt();
});
},
onChangeEnd: (val) {
_setting.themeMode.put(val.toInt());
},
label: _buildThemeModeStr(_nightMode),
divisions: 2,
),
],
);
}
String _buildThemeModeStr(int n) {
switch (n) {
case 1:
return _s.light;
case 2:
return _s.dark;
default:
return _s.auto;
}
}
Widget _buildPushToken() {
return ListTile(
title: Text(
_s.pushToken,
),
trailing: TextButton(
child: Text(_s.copy),
onPressed: () {
if (_pushToken != null) {
copy(_pushToken!);
showSnackBar(context, Text(_s.success));
} else {
showSnackBar(context, Text(_s.getPushTokenFailed));
}
},
),
subtitle: FutureWidget<String?>(
future: getToken(),
loading: Text(_s.gettingToken),
error: (error, trace) => Text('${_s.error}: $error'),
noData: Text(_s.nullToken),
success: (text) {
_pushToken = text;
return Text(
text ?? _s.nullToken,
style: grey,
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
},
),
);
}
Widget _buildFont() {
return ExpansionTile(
title: Text(_s.chooseFontFile),
trailing: Text(getFileName(_setting.fontPath.fetch()) ?? _s.notSelected),
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
onPressed: () async => _pickFontFile(),
child: Text(_s.pickFile),
),
TextButton(
onPressed: () => setState(() {
_setting.fontPath.delete();
_showRestartSnackbar();
}),
child: Text(_s.clear),
)
],
)
],
);
}
Future<void> _pickFontFile() async {
final path = await pickOneFile();
if (path != null) {
final fontDir_ = await fontDir;
final fontFile = File(path);
final newPath = '${fontDir_.path}/${path.split('/').last}';
await fontFile.copy(newPath);
_setting.fontPath.put(newPath);
setState(() {});
_showRestartSnackbar();
return;
}
showSnackBar(context, Text(_s.failed));
}
void _showRestartSnackbar() {
showSnackBarWithAction(
context,
'${_s.success}\n${_s.needRestart}',
_s.restart,
() => rebuildAll(context),
);
}
}

View File

@@ -1,16 +1,18 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/path_with_prefix.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/path.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/page/sftp/downloading.dart';
import 'package:toolbox/view/widget/fade_in.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../core/extension/numx.dart';
import '../../../core/extension/stringx.dart';
import '../../../core/route.dart';
import '../../../core/utils/misc.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/app/path_with_prefix.dart';
import '../../../data/res/path.dart';
import '../../../data/res/ui.dart';
import '../../widget/fade_in.dart';
import 'downloading.dart';
class SFTPDownloadedPage extends StatefulWidget {
const SFTPDownloadedPage({Key? key}) : super(key: key);
@@ -37,7 +39,7 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -149,8 +151,9 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
const SizedBox(),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () {
file.deleteSync();
@@ -174,8 +177,9 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
),
[
TextButton(
onPressed: (() => Navigator.of(context).pop()),
child: Text(_s.close))
onPressed: (() => Navigator.of(context).pop()),
child: Text(_s.close),
)
],
);
}

View File

@@ -1,13 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/sftp/download_status.dart';
import 'package:toolbox/data/provider/sftp_download.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/widget/center_loading.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../../core/extension/numx.dart';
import '../../../core/utils/misc.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/sftp/download_status.dart';
import '../../../data/provider/sftp_download.dart';
import '../../../data/res/ui.dart';
import '../../widget/center_loading.dart';
import '../../widget/round_rect_card.dart';
class SFTPDownloadingPage extends StatefulWidget {
const SFTPDownloadingPage({Key? key}) : super(key: key);
@@ -22,7 +24,7 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -39,7 +41,7 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
}
Widget _buildBody() {
return Consumer<SftpDownloadProvider>(builder: (__, pro, _) {
return Consumer<SftpProvider>(builder: (__, pro, _) {
if (pro.status.isEmpty) {
return Center(
child: Text(_s.sftpNoDownloadTask),
@@ -75,6 +77,7 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
Widget _buildItem(SftpDownloadStatus status) {
if (status.error != null) {
showSnackBar(context, Text(status.error.toString()));
status.error = null;
}
switch (status.status) {
case SftpWorkerStatus.finished:

View File

@@ -2,24 +2,25 @@ import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/sftp/absolute_path.dart';
import 'package:toolbox/data/model/sftp/download_worker.dart';
import 'package:toolbox/data/model/sftp/browser_status.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/sftp_download.dart';
import 'package:toolbox/data/res/path.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/sftp/downloading.dart';
import 'package:toolbox/view/widget/fade_in.dart';
import 'package:toolbox/view/widget/two_line_text.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../core/extension/numx.dart';
import '../../../core/extension/stringx.dart';
import '../../../core/route.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_private_info.dart';
import '../../../data/model/sftp/absolute_path.dart';
import '../../../data/model/sftp/browser_status.dart';
import '../../../data/model/sftp/download_item.dart';
import '../../../data/provider/server.dart';
import '../../../data/provider/sftp_download.dart';
import '../../../data/res/path.dart';
import '../../../data/store/private_key.dart';
import '../../../locator.dart';
import '../../widget/fade_in.dart';
import '../../widget/two_line_text.dart';
import 'downloading.dart';
class SFTPPage extends StatefulWidget {
final ServerPrivateInfo spi;
@@ -36,21 +37,21 @@ class _SFTPPageState extends State<SFTPPage> {
late MediaQueryData _media;
late S _s;
ServerInfo? _si;
Server? _si;
SSHClient? _client;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_s = S.of(context);
_s = S.of(context)!;
}
@override
void initState() {
super.initState();
final serverProvider = locator<ServerProvider>();
_si = serverProvider.servers.firstWhere((s) => s.info == widget.spi);
_si = serverProvider.getServer(widget.spi.id);
_client = _si?.client;
}
@@ -62,38 +63,19 @@ class _SFTPPageState extends State<SFTPPage> {
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
actions: [
IconButton(
onPressed: (() => showRoundDialog(
context,
_s.choose,
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.folder),
title: Text(_s.createFolder),
onTap: () => mkdir(context)),
ListTile(
leading: const Icon(Icons.insert_drive_file),
title: Text(_s.createFile),
onTap: () => newFile(context)),
],
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.close))
],
)),
icon: const Icon(Icons.add),
)
icon: const Icon(Icons.downloading),
onPressed: () =>
AppRoute(const SFTPDownloadingPage(), 'sftp downloading')
.go(context),
),
],
),
body: _buildFileView(),
bottomNavigationBar: _buildPath(),
bottomNavigationBar: SafeArea(child: _buildBottom()),
);
}
Widget _buildPath() {
Widget _buildBottom() {
return SafeArea(
child: Container(
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
@@ -112,6 +94,32 @@ class _SFTPPageState extends State<SFTPPage> {
},
icon: const Icon(Icons.arrow_back),
),
IconButton(
onPressed: (() => showRoundDialog(
context,
_s.choose,
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.folder),
title: Text(_s.createFolder),
onTap: () => mkdir(context)),
ListTile(
leading: const Icon(Icons.insert_drive_file),
title: Text(_s.createFile),
onTap: () => newFile(context)),
],
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.close),
)
],
)),
icon: const Icon(Icons.add),
),
IconButton(
padding: const EdgeInsets.all(0),
onPressed: () async {
@@ -133,8 +141,9 @@ class _SFTPPageState extends State<SFTPPage> {
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel))
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
)
],
);
@@ -168,8 +177,7 @@ class _SFTPPageState extends State<SFTPPage> {
);
Widget _buildFileView() {
if (_client == null ||
_si?.connectionState != ServerConnectionState.connected) {
if (_client == null || _si?.cs != ServerState.connected) {
return centerCircleLoading;
}
@@ -271,8 +279,9 @@ class _SFTPPageState extends State<SFTPPage> {
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel))
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
)
],
);
}
@@ -284,8 +293,9 @@ class _SFTPPageState extends State<SFTPPage> {
Text('${_s.dl2Local(name.filename)}\n${_s.keepForeground}'),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
@@ -295,7 +305,7 @@ class _SFTPPageState extends State<SFTPPage> {
final local = '${(await sftpDownloadDir).path}$remotePath';
final pubKeyId = widget.spi.pubKeyId;
locator<SftpDownloadProvider>().add(
locator<SftpProvider>().add(
DownloadItem(
widget.spi,
remotePath,
@@ -307,25 +317,6 @@ class _SFTPPageState extends State<SFTPPage> {
);
Navigator.of(context).pop();
showRoundDialog(
context,
_s.goSftpDlPage,
const SizedBox(),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
AppRoute(const SFTPDownloadingPage(), 'sftp downloading')
.go(context);
},
child: Text(_s.ok),
)
],
);
},
child: Text(_s.download),
)
@@ -341,8 +332,9 @@ class _SFTPPageState extends State<SFTPPage> {
Text(_s.sureDelete(file.filename)),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
_status.client!.remove(file.filename);
@@ -384,8 +376,9 @@ class _SFTPPageState extends State<SFTPPage> {
Text(_s.fieldMustNotEmpty),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok),
),
],
);
return;

View File

@@ -1,12 +1,13 @@
import 'package:after_layout/after_layout.dart';
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';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/snippet.dart';
import '../../../data/provider/snippet.dart';
import '../../../data/res/ui.dart';
import '../../../locator.dart';
import '../../widget/input_decoration.dart';
class SnippetEditPage extends StatefulWidget {
const SnippetEditPage({Key? key, this.snippet}) : super(key: key);
@@ -35,7 +36,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override

View File

@@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.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/view/page/snippet/edit.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../../data/res/ui.dart';
import '/core/route.dart';
import '/data/provider/snippet.dart';
import 'edit.dart';
import '/view/widget/round_rect_card.dart';
class SnippetListPage extends StatefulWidget {
const SnippetListPage({Key? key}) : super(key: key);
@@ -17,14 +16,12 @@ class SnippetListPage extends StatefulWidget {
}
class _SnippetListPageState extends State<SnippetListPage> {
final _textStyle = TextStyle(color: primaryColor);
late S _s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -54,30 +51,18 @@ class _SnippetListPageState extends State<SnippetListPage> {
return ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: key.snippets.length,
itemExtent: 57,
itemBuilder: (context, idx) {
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
key.snippets[idx].name,
textAlign: TextAlign.center,
),
TextButton(
onPressed: () => AppRoute(
SnippetEditPage(snippet: key.snippets[idx]),
'snippet edit page')
.go(context),
child: Text(
_s.edit,
style: _textStyle,
),
),
],
ListTile(
title: Text(
key.snippets[idx].name,
),
trailing: TextButton(
onPressed: () => AppRoute(
SnippetEditPage(snippet: key.snippets[idx]),
'snippet edit page')
.go(context),
child: Text(_s.edit),
),
),
);

View File

@@ -1,17 +1,26 @@
import 'dart:convert';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:xterm/xterm.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:xterm/xterm.dart' hide TerminalColors;
import '../../core/utils.dart';
import '../../core/utils/platform.dart';
import '../../data/model/ssh/terminal_color.dart';
import '../../core/utils/misc.dart';
import '../../core/utils/ui.dart';
import '../../core/utils/server.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/provider/server.dart';
import '../../data/res/terminal_theme.dart';
import '../../data/model/ssh/virtual_key.dart';
import '../../data/provider/virtual_keyboard.dart';
import '../../data/res/color.dart';
import '../../data/res/terminal.dart';
import '../../data/res/virtual_key.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
import '../widget/virtual_keyboard.dart';
class SSHPage extends StatefulWidget {
final ServerPrivateInfo spi;
@@ -22,98 +31,319 @@ class SSHPage extends StatefulWidget {
}
class _SSHPageState extends State<SSHPage> {
late final terminal = Terminal(inputHandler: keyboard);
late final SSHSession session;
final keyboard = VirtualKeyboard(defaultInputHandler);
late final _terminal = Terminal(inputHandler: _keyboard);
SSHClient? _client;
final _keyboard = locator<VirtualKeyboard>();
final _setting = locator<SettingStore>();
late MediaQueryData _media;
final _virtualKeyboardHeight = 57.0;
final TerminalController _terminalController = TerminalController();
final ContextMenuController _menuController = ContextMenuController();
late TextStyle _menuTextStyle;
late TerminalColors _termColors;
late S _s;
var title = '';
var isDark = false;
var _isDark = false;
@override
void initState() {
super.initState();
final termColorIdx = _setting.termColorIdx.fetch()!;
_termColors = TerminalColorsPlatform.values[termColorIdx].colors;
initTerminal();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
isDark = isDarkMode(context);
_isDark = isDarkMode(context);
_media = MediaQuery.of(context);
_menuTextStyle = TextStyle(color: contentColor.resolve(context));
_s = S.of(context)!;
}
@override
void dispose() {
session.close();
_client?.close();
super.dispose();
}
void _write(String p0) {
_terminal.write('$p0\r\n');
}
Future<void> initTerminal() async {
terminal.write('Connecting...\r\n');
_write('Connecting...\r\n');
final client = locator<ServerProvider>()
.servers
.where((e) => e.info.id == widget.spi.id)
.first
.client;
if (client == null) {
terminal.write('Failed to connect\r\n');
return;
}
_client = await genClient(
widget.spi,
onStatus: (p0) {
switch (p0) {
case GenSSHClientStatus.socket:
_write('Destination: ${widget.spi.id}');
return _write('Establishing socket...');
case GenSSHClientStatus.key:
return _write('Using private key to connect...');
case GenSSHClientStatus.pwd:
return _write('Sending password to auth...');
}
},
);
_write('Connected\r\n');
_write('Terminal size: ${_terminal.viewWidth}x${_terminal.viewHeight}\r\n');
_write('Starting shell...\r\n');
terminal.write('Connected\r\n');
session = await client.shell(
final session = await _client!.shell(
pty: SSHPtyConfig(
width: terminal.viewWidth,
height: terminal.viewHeight,
width: _terminal.viewWidth,
height: _terminal.viewHeight,
),
);
terminal.buffer.clear();
terminal.buffer.setCursor(0, 0);
_terminal.buffer.clear();
_terminal.buffer.setCursor(0, 0);
terminal.onTitleChange = (title) {
setState(() => this.title = title);
};
terminal.onResize = (width, height, pixelWidth, pixelHeight) {
session.resizeTerminal(width, height, pixelWidth, pixelHeight);
};
terminal.onOutput = (data) {
_terminal.onOutput = (data) {
session.write(utf8.encode(data) as Uint8List);
};
session.stdout
.cast<List<int>>()
.transform(const Utf8Decoder())
.listen(terminal.write);
_listen(session.stdout);
_listen(session.stderr);
session.stderr
await session.done;
if (mounted) {
Navigator.of(context).pop();
}
}
void _listen(Stream<Uint8List> stream) {
stream
.cast<List<int>>()
.transform(const Utf8Decoder())
.listen(terminal.write);
.listen(_terminal.write);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title, style: textSize18),
),
body: Column(
children: [
Expanded(
child: TerminalView(
terminal,
keyboardType: TextInputType.visiblePassword,
theme: isDark ? termDarkTheme : termLightTheme,
keyboardAppearance: isDark ? Brightness.dark : Brightness.light,
),
),
VirtualKeyboardView(keyboard),
],
final termTheme = _isDark ? termDarkTheme : termLightTheme;
Widget child = Scaffold(
backgroundColor: termTheme.background,
body: _buildBody(termTheme.toTerminalTheme(_termColors)),
bottomNavigationBar: _buildBottom(termTheme.background),
);
if (isIOS) {
child = AnnotatedRegion(
value: _isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark,
child: child,
);
}
return child;
}
Widget _buildBody(TerminalTheme termTheme) {
return SizedBox(
height: _media.size.height -
_virtualKeyboardHeight -
_media.padding.bottom -
_media.padding.top,
child: TerminalView(
_terminal,
controller: _terminalController,
keyboardType: TextInputType.visiblePassword,
textStyle: TerminalStyle.fromTextStyle(
TextStyle(fontFamily: getFileName(_setting.fontPath.fetch()))),
theme: termTheme,
deleteDetection: isIOS,
onTapUp: _onTapUp,
autoFocus: true,
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
),
);
}
Widget _buildBottom(Color bgColor) {
return SafeArea(
child: AnimatedPadding(
padding: _media.viewInsets,
duration: const Duration(milliseconds: 23),
curve: Curves.fastOutSlowIn,
child: Container(
color: bgColor,
height: _virtualKeyboardHeight,
child: Consumer<VirtualKeyboard>(
builder: (_, __, ___) => _buildVirtualKey(),
),
),
),
);
}
Widget _buildVirtualKey() {
final half = virtualKeys.length ~/ 2;
final top = virtualKeys.sublist(0, half);
final bottom = virtualKeys.sublist(half);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: top.map((e) => _buildVirtualKeyItem(e)).toList(),
),
Row(
children: bottom.map((e) => _buildVirtualKeyItem(e)).toList(),
)
],
);
}
Widget _buildVirtualKeyItem(VirtualKey item) {
var selected = false;
switch (item.key) {
case TerminalKey.control:
selected = _keyboard.ctrl;
break;
case TerminalKey.alt:
selected = _keyboard.alt;
break;
default:
break;
}
final child = item.icon != null
? Icon(
item.icon,
color: _isDark ? Colors.white : Colors.black,
size: 17,
)
: Text(
item.text,
style: TextStyle(
color: selected ? primaryColor : null,
fontSize: 17,
),
);
return InkWell(
onTap: () => _doVirtualKey(item),
child: SizedBox(
width: _media.size.width / (virtualKeys.length / 2),
height: _virtualKeyboardHeight / 2,
child: Center(
child: child,
),
),
);
}
void _doVirtualKey(VirtualKey item) {
if (item.func != null) {
_doVirtualKeyFunc(item.func!);
return;
}
if (item.key != null) {
_doVirtualKeyInput(item.key!);
}
}
void _doVirtualKeyInput(TerminalKey key) {
switch (key) {
case TerminalKey.control:
_keyboard.ctrl = !_keyboard.ctrl;
setState(() {});
break;
case TerminalKey.alt:
_keyboard.alt = !_keyboard.alt;
setState(() {});
break;
default:
_terminal.keyInput(key);
break;
}
}
void _doVirtualKeyFunc(VirtualKeyFunc type) {
switch (type) {
case VirtualKeyFunc.toggleIME:
FocusScope.of(context).requestFocus(FocusNode());
break;
case VirtualKeyFunc.backspace:
_terminal.keyInput(TerminalKey.backspace);
break;
case VirtualKeyFunc.paste:
_paste();
break;
case VirtualKeyFunc.copy:
copy(terminalSelected);
break;
}
}
void _paste() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
if (value != null) {
_terminal.textInput(value.text!);
}
});
}
String get terminalSelected {
final range = _terminalController.selection;
if (range == null) {
return '';
}
return _terminal.buffer.getText(range);
}
void _onTapUp(TapUpDetails details, CellOffset offset) {
if (_menuController.isShown) {
_menuController.remove();
return;
}
final selected = terminalSelected;
if (selected.trim().isEmpty) {
// _menuController.show(
// context: context,
// contextMenuBuilder: (context) {
// return TextSelectionToolbar(
// anchorAbove: detail.globalPosition,
// anchorBelow: detail.globalPosition,
// children: [
// TextButton(
// child: Text(
// 'Paste',
// style: _menuTextStyle,
// ),
// onPressed: () async {
// _paste();
// _menuController.remove();
// },
// )
// ],
// );
// },
// );
return;
}
_menuController.show(
context: context,
contextMenuBuilder: (context) {
return TextSelectionToolbar(
anchorAbove: details.globalPosition,
anchorBelow: details.globalPosition,
children: [
TextButton(
child: Text(
_s.copy,
style: _menuTextStyle,
),
onPressed: () {
_terminalController.setSelection(null);
copy(selected);
_menuController.remove();
},
),
],
);
},
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../data/res/menu.dart';
class DropdownBtnItem {
final String text;
final IconData icon;
const DropdownBtnItem({
required this.text,
required this.icon,
});
Widget build(S s) => Row(
children: [
Icon(icon),
const SizedBox(
width: 10,
),
Text(
getDropdownBtnText(s, text),
),
],
);
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
class FutureWidget<T> extends StatelessWidget {
final Future future;
final Widget loading;
final Widget Function(Object? error, StackTrace? trace) error;
final Widget Function(T data) success;
final Widget noData;
final Widget Function(AsyncSnapshot<Object?> snapshot)? active;
const FutureWidget({
super.key,
required this.future,
required this.loading,
required this.error,
required this.success,
required this.noData,
this.active,
});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
if (snapshot.hasError) {
return error(snapshot.error, snapshot.stackTrace);
}
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return loading;
case ConnectionState.active:
if (active != null) {
return active!(snapshot);
}
return loading;
case ConnectionState.done:
if (snapshot.hasData) {
return success(snapshot.data as T);
}
return noData;
}
},
);
}
}

View File

@@ -1,14 +1,16 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/color.dart';
import '../../data/res/color.dart';
InputDecoration buildDecoration(String label,
{TextStyle? textStyle, IconData? icon, String? hint}) {
return InputDecoration(
labelText: label,
labelStyle: textStyle,
hintText: hint,
icon: Icon(
icon,
color: primaryColor,
));
labelText: label,
labelStyle: textStyle,
hintText: hint,
icon: Icon(
icon,
color: primaryColor,
),
);
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
class RebuildWidget extends StatefulWidget {
const RebuildWidget({super.key, required this.child});
final Widget child;
static void restartApp(BuildContext context) {
context.findAncestorStateOfType<_RebuildWidgetState>()?.restartApp();
}
@override
_RebuildWidgetState createState() => _RebuildWidgetState();
}
class _RebuildWidgetState extends State<RebuildWidget> {
Key key = UniqueKey();
void restartApp() {
setState(() {
key = UniqueKey();
});
}
@override
Widget build(BuildContext context) {
return KeyedSubtree(
key: key,
child: widget.child,
);
}
}

View File

@@ -1,9 +1,13 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/res/color.dart';
const regUrl =
r"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]*";
import '../../core/utils/ui.dart';
final _reg = RegExp(
r"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]*");
const _textStyle = TextStyle();
class UrlText extends StatelessWidget {
final String text;
@@ -11,18 +15,17 @@ class UrlText extends StatelessWidget {
final TextAlign? textAlign;
final TextStyle style;
const UrlText(
{Key? key,
required this.text,
this.replace,
this.textAlign,
this.style = const TextStyle()})
: super(key: key);
const UrlText({
Key? key,
required this.text,
this.replace,
this.textAlign,
this.style = _textStyle,
}) : super(key: key);
List<InlineSpan> _getTextSpans(bool isDarkMode) {
List<InlineSpan> _getTextSpans(Color c) {
List<InlineSpan> widgets = <InlineSpan>[];
final reg = RegExp(regUrl);
Iterable<Match> matches = reg.allMatches(text);
Iterable<Match> matches = _reg.allMatches(text);
List<_ResultMatch> resultMatches = <_ResultMatch>[];
int start = 0;
@@ -53,16 +56,22 @@ class UrlText extends StatelessWidget {
for (var result in resultMatches) {
if (result.isUrl) {
widgets.add(_LinkTextSpan(
widgets.add(
_LinkTextSpan(
replace: replace ?? result.text,
text: result.text,
style: style.copyWith(color: Colors.blue)));
style: style.copyWith(color: primaryColor),
),
);
} else {
widgets.add(TextSpan(
widgets.add(
TextSpan(
text: result.text,
style: style.copyWith(
color: isDarkMode ? Colors.white : Colors.black,
)));
color: c,
),
),
);
}
}
return widgets;
@@ -72,7 +81,7 @@ class UrlText extends StatelessWidget {
Widget build(BuildContext context) {
return RichText(
textAlign: textAlign ?? TextAlign.start,
text: TextSpan(children: _getTextSpans(isDarkMode(context))),
text: TextSpan(children: _getTextSpans(contentColor.resolve(context))),
);
}
}
@@ -80,12 +89,13 @@ class UrlText extends StatelessWidget {
class _LinkTextSpan extends TextSpan {
_LinkTextSpan({TextStyle? style, required String text, String? replace})
: super(
style: style,
text: replace,
recognizer: TapGestureRecognizer()
..onTap = () {
openUrl(text);
});
style: style,
text: replace,
recognizer: TapGestureRecognizer()
..onTap = () {
openUrl(text);
},
);
}
class _ResultMatch {

View File

@@ -1,81 +0,0 @@
import 'package:flutter/material.dart';
import 'package:xterm/xterm.dart';
class VirtualKeyboardView extends StatelessWidget {
const VirtualKeyboardView(this.keyboard, {super.key});
final VirtualKeyboard keyboard;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: keyboard,
builder: (context, child) => ToggleButtons(
renderBorder: false,
isSelected: [keyboard.ctrl, keyboard.alt, keyboard.shift],
onPressed: (index) {
switch (index) {
case 0:
keyboard.ctrl = !keyboard.ctrl;
break;
case 1:
keyboard.alt = !keyboard.alt;
break;
case 2:
keyboard.shift = !keyboard.shift;
break;
}
},
children: const [Text('Ctrl'), Text('Alt'), Text('Shift')],
),
);
}
}
class VirtualKeyboard extends TerminalInputHandler with ChangeNotifier {
final TerminalInputHandler _inputHandler;
VirtualKeyboard(this._inputHandler);
bool _ctrl = false;
bool get ctrl => _ctrl;
set ctrl(bool value) {
if (_ctrl != value) {
_ctrl = value;
notifyListeners();
}
}
bool _shift = false;
bool get shift => _shift;
set shift(bool value) {
if (_shift != value) {
_shift = value;
notifyListeners();
}
}
bool _alt = false;
bool get alt => _alt;
set alt(bool value) {
if (_alt != value) {
_alt = value;
notifyListeners();
}
}
@override
String? call(TerminalKeyboardEvent event) {
return _inputHandler.call(event.copyWith(
ctrl: event.ctrl || _ctrl,
shift: event.shift || _shift,
alt: event.alt || _alt,
));
}
}

View File

@@ -26,9 +26,9 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2
url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7

View File

@@ -26,7 +26,7 @@
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
E3D733E2A8794200D26EFCCF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ADDB81DD1CCC4A9ED73177B /* Pods_Runner.framework */; };
CC3C9C24336DBCBB13B1DD4E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051B6E43AB66C836E65690B6 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -53,6 +53,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
051B6E43AB66C836E65690B6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1B64E26251C2132C1FD3DE5F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* server_box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = server_box.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -67,12 +69,10 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
6B35024F2A8A5A7961F90167 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
6F4CBCB4E20C50200E1C67AD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
7ADDB81DD1CCC4A9ED73177B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3EBAA16736A04B8FA45DF33F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
A3006D048053A6426855B015 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
E8FDB0F1B04D4A1C2983795D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -80,7 +80,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E3D733E2A8794200D26EFCCF /* Pods_Runner.framework in Frameworks */,
CC3C9C24336DBCBB13B1DD4E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -105,7 +105,7 @@
33CEB47122A05771004F2AC0 /* Flutter */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
B9C356C33EBFDC4109524AEE /* Pods */,
51D4AEDD40A46AA3AF8AF7AE /* Pods */,
);
sourceTree = "<group>";
};
@@ -152,12 +152,12 @@
path = Runner;
sourceTree = "<group>";
};
B9C356C33EBFDC4109524AEE /* Pods */ = {
51D4AEDD40A46AA3AF8AF7AE /* Pods */ = {
isa = PBXGroup;
children = (
6B35024F2A8A5A7961F90167 /* Pods-Runner.debug.xcconfig */,
A3006D048053A6426855B015 /* Pods-Runner.release.xcconfig */,
6F4CBCB4E20C50200E1C67AD /* Pods-Runner.profile.xcconfig */,
1B64E26251C2132C1FD3DE5F /* Pods-Runner.debug.xcconfig */,
E8FDB0F1B04D4A1C2983795D /* Pods-Runner.release.xcconfig */,
3EBAA16736A04B8FA45DF33F /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
@@ -166,7 +166,7 @@
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
7ADDB81DD1CCC4A9ED73177B /* Pods_Runner.framework */,
051B6E43AB66C836E65690B6 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -178,13 +178,13 @@
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
6380F17CF8505331723EA2D3 /* [CP] Check Pods Manifest.lock */,
33DCC8E03FB9B8627035A7B7 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
3A1CB36BB218FE124CA9BC01 /* [CP] Embed Pods Frameworks */,
DE29B08A1EBC54F140840A8F /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -292,24 +292,7 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
3A1CB36BB218FE124CA9BC01 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
6380F17CF8505331723EA2D3 /* [CP] Check Pods Manifest.lock */ = {
33DCC8E03FB9B8627035A7B7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -331,6 +314,23 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
DE29B08A1EBC54F140840A8F /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

View File

@@ -6,16 +6,20 @@ import 'dart:io';
const appName = 'ServerBox';
const buildDataFilePath = 'lib/data/res/build_data.dart';
const apkPath = 'build/app/outputs/flutter-apk/app-release.apk';
const xcarchivePath = 'build/ios/archive/Runner.xcarchive';
var regiOSProjectVer = RegExp(r'CURRENT_PROJECT_VERSION = .+;');
var regiOSMarketVer = RegExp(r'MARKETING_VERSION = .+');
const iOSInfoPlistPath = 'ios/Runner.xcodeproj/project.pbxproj';
const appleXCConfigPath = '/Runner.xcodeproj/project.pbxproj';
var regAppleProjectVer = RegExp(r'CURRENT_PROJECT_VERSION = .+;');
var regAppleMarketVer = RegExp(r'MARKETING_VERSION = .+');
const skslFileSuffix = '.sksl.json';
const buildFuncs = {
'ios': flutterBuildIOS,
'android': flutterBuildAndroid,
'macos': flutterBuildMacOS,
};
int? build;
@@ -24,9 +28,9 @@ Future<ProcessResult> fvmRun(List<String> args) async {
return await Process.run('fvm', args, runInShell: true);
}
Future<int> getGitCommitCount() async {
Future<void> getGitCommitCount() async {
final result = await Process.run('git', ['log', '--oneline']);
return (result.stdout as String)
build = (result.stdout as String)
.split('\n')
.where((line) => line.isNotEmpty)
.length;
@@ -86,7 +90,7 @@ Future<void> updateBuildData() async {
Future<void> dartFormat() async {
final result = await fvmRun(['dart', 'format', '.']);
print('\n${result.stdout}');
print(result.stdout);
if (result.exitCode != 0) {
print(result.stderr);
exit(1);
@@ -119,7 +123,7 @@ Future<void> flutterBuild(
'--build-name=1.0.$build',
]);
}
print('[$buildType]\nBuilding with args: ${args.join(' ')}');
print('\n[$buildType]\nBuilding with args: ${args.join(' ')}');
final buildResult = await fvmRun(['flutter', ...args]);
final exitCode = buildResult.exitCode;
@@ -135,8 +139,6 @@ Future<void> flutterBuild(
exit(1);
}
}
print('Done.\n');
} else {
print(buildResult.stdout);
print(buildResult.stderr);
@@ -146,26 +148,43 @@ Future<void> flutterBuild(
}
Future<void> flutterBuildIOS() async {
await changeInfoPlistVersion();
await flutterBuild(
xcarchivePath, './release/${appName}_ios_build.xcarchive', 'ipa');
}
Future<void> flutterBuildAndroid() async {
await flutterBuild(apkPath, './release/${appName}_build_Arm64.apk', 'apk');
Future<void> flutterBuildMacOS() async {
await flutterBuild(
xcarchivePath, './release/${appName}_macos_build.xcarchive', 'macos');
}
Future<void> changeInfoPlistVersion() async {
for (final path in [iOSInfoPlistPath]) {
final file = File(path);
Future<void> flutterBuildAndroid() async {
await flutterBuild(apkPath, './release/${appName}_build_Arm64.apk', 'apk');
await killJava();
}
Future<void> changeAppleVersion() async {
for (final path in ['ios', 'macos']) {
final file = File(path + appleXCConfigPath);
final contents = await file.readAsString();
final newContents = contents
.replaceAll(regiOSMarketVer, 'MARKETING_VERSION = 1.0.$build;')
.replaceAll(regiOSProjectVer, 'CURRENT_PROJECT_VERSION = $build;');
.replaceAll(regAppleMarketVer, 'MARKETING_VERSION = 1.0.$build;')
.replaceAll(regAppleProjectVer, 'CURRENT_PROJECT_VERSION = $build;');
await file.writeAsString(newContents);
}
}
Future<void> killJava() async {
final result = await Process.run('ps', ['-A']);
final lines = (result.stdout as String).split('\n');
for (final line in lines) {
if (line.contains('java')) {
final pid = line.split(' ')[0];
print('Killing java process: $pid');
await Process.run('kill', [pid]);
}
}
}
void main(List<String> args) async {
if (args.isEmpty) {
print('No action. Exit.');
@@ -175,26 +194,31 @@ void main(List<String> args) async {
final command = args[0];
switch (command) {
case 'run':
return flutterRun(args.length == 2 ? args[1] : null);
case 'build':
final stopwatch = Stopwatch()..start();
build = await getGitCommitCount();
await updateBuildData();
await dartFormat();
await getGitCommitCount();
await changeAppleVersion();
await updateBuildData();
if (args.length > 1) {
final platform = args[1];
if (buildFuncs.keys.contains(platform)) {
await buildFuncs[platform]!();
} else {
print('Unknown platform: $platform');
final platforms = args[1];
for (final platform in platforms.split(',')) {
if (buildFuncs.keys.contains(platform)) {
await buildFuncs[platform]!();
print('Build finished in [${stopwatch.elapsed}]');
stopwatch.reset();
stopwatch.start();
} else {
print('Unknown platform: $platform');
}
}
return;
}
for (final func in buildFuncs.values) {
await func();
}
print('Build finished in ${stopwatch.elapsed}');
print('Build finished in ${stopwatch.elapsed}\n');
return;
default:
print('Unsupported command: $command');

Some files were not shown because too many files have changed in this diff Show More