Compare commits

...

16 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
63 changed files with 3163 additions and 349 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

@@ -1,3 +1,4 @@
English | [简体中文](README_zh.md)
<!-- Title-->
<p align="center">
<h1 align="center">Server Box</h1>
@@ -28,18 +29,21 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
## 🔖 Feature
- [x] Functions
- [x] `SSH` Terminal
- [x] `SSH` Terminal, `SFTP`
- [x] `Docker & Pkg` Manager
- [x] `SFTP`
- [x] `Snippet`
- [x] `Ping`
- [x] Status charts
- [x] etc.
- [x] `Ping` and etc.
- [x] i18n (English, Chinese)
- **Welcome contribution** :)
- [How to contribute?](#i18n-guide)
- [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>
<tr>
@@ -74,19 +78,23 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
</tr>
</table>
## 🖥 Platform
Status|Platform
--- | ---
Full Support|Android/iOS
Support, but not tested|macOS/Windows/Linux
## i18n guide
## 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. Pull commit to your forked repo.
5. Request a pull request on my repo.
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

@@ -9,6 +9,8 @@ PODS:
- 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):
@@ -22,6 +24,7 @@ DEPENDENCIES:
- 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`)
@@ -37,6 +40,8 @@ EXTERNAL SOURCES:
: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:
@@ -50,6 +55,7 @@ SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4

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 = 227;
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.227;
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 = 227;
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.227;
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 = 227;
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.227;
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,62 +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>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
<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

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.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';
@@ -17,19 +18,9 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
setTransparentNavigationBar(context);
return ValueListenableBuilder<int>(
valueListenable: _setting.primaryColor.listenable(),
builder: (_, colorValue, __) {
primaryColor = Color(colorValue);
return ValueListenableBuilder<int>(
valueListenable: _setting.nightMode.listenable(),
builder: (_, mode, __) => _buildApp(mode),
);
},
);
}
final fontName = getFileName(_setting.fontPath.fetch());
primaryColor = Color(_setting.primaryColor.fetch()!);
Widget _buildApp(int nightMode) {
final textStyle = TextStyle(color: primaryColor);
final materialColor = primaryColor.materialStateColor;
final materialColorAlpha = primaryColor.withOpacity(0.7).materialStateColor;
@@ -50,43 +41,57 @@ class MyApp extends StatelessWidget {
final radioTheme = RadioThemeData(
fillColor: materialColor,
);
final ok = nightMode >= 0 && nightMode <= ThemeMode.values.length - 1;
final themeMode = ok ? ThemeMode.values[nightMode] : ThemeMode.system;
final primarySwatch = primaryColor.materialColor;
return MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: S.localizationsDelegates,
supportedLocales: S.supportedLocales,
title: BuildData.name,
themeMode: themeMode,
theme: ThemeData(
useMaterial3: false,
primaryColor: primaryColor,
primarySwatch: primaryColor.materialColor,
appBarTheme: appBarTheme,
floatingActionButtonTheme: fabTheme,
iconTheme: iconTheme,
primaryIconTheme: iconTheme,
switchTheme: switchTheme,
inputDecorationTheme: inputDecorationTheme,
radioTheme: radioTheme,
),
darkTheme: ThemeData.dark().copyWith(
useMaterial3: false,
floatingActionButtonTheme: fabTheme,
iconTheme: iconTheme,
primaryIconTheme: iconTheme,
switchTheme: switchTheme,
inputDecorationTheme: inputDecorationTheme,
radioTheme: radioTheme,
colorScheme: ColorScheme.fromSwatch(
primarySwatch: primaryColor.materialColor,
return ValueListenableBuilder<int>(
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,
accentColor: primaryColor,
),
),
themeAnimationDuration: const Duration(milliseconds: 237),
home: const MyHomePage(),
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

@@ -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,5 +1,3 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@@ -11,6 +9,7 @@ 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');
@@ -31,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;
@@ -48,7 +47,7 @@ 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;
}
@@ -79,13 +78,13 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
}
Future<void> _doUpdate(AppUpdate update, BuildContext context, S s) async {
if (Platform.isAndroid) {
if (isAndroid) {
await RUpgrade.upgrade(
update.android,
fileName: update.android.split('/').last,
isAutoRequestInstall: true,
);
} else if (Platform.isIOS) {
} else if (isIOS) {
await RUpgrade.upgradeFromAppStore('1586449703');
} else {
showRoundDialog(context, s.attention, Text(s.platformNotSupportUpdate), [

View File

@@ -4,8 +4,12 @@ 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()) {
@@ -31,3 +35,27 @@ 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

@@ -24,13 +24,23 @@ String decyptPem(List<String> args) {
return sshKey.first.toPem();
}
Future<SSHClient> genClient(ServerPrivateInfo spi) async {
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,
@@ -38,6 +48,7 @@ Future<SSHClient> genClient(ServerPrivateInfo spi) async {
);
}
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
onStatus_(GenSSHClientStatus.key);
return SSHClient(
socket,
username: spi.user,

View File

@@ -3,11 +3,14 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/utils/misc.dart';
import 'package:url_launcher/url_launcher.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;
@@ -62,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,
@@ -101,3 +104,11 @@ String tabTitleName(BuildContext context, int i) {
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

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:xterm/ui.dart';
import '../../res/terminal_color.dart';
import '../../res/terminal.dart';
class TerminalUITheme {
final Color cursor;

View File

@@ -87,13 +87,11 @@ class ServerProvider extends BusyProvider {
_servers[i].client?.close();
_servers[i].client = null;
}
notifyListeners();
return;
}
final idx = getServerIdx(spi.id);
_servers[idx].client?.close();
_servers[idx].client = null;
notifyListeners();
}
void addServer(ServerPrivateInfo spi) {
@@ -157,9 +155,7 @@ class ServerProvider extends BusyProvider {
// after connected
s.cs = ServerState.connected;
final writeResult = await s.client!
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath")
.string;
final writeResult = await s.client!.run(installShellCmd).string;
// if write failed
if (writeResult.isNotEmpty) {

View File

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

View File

@@ -2,9 +2,8 @@
class BuildData {
static const String name = "ServerBox";
static const int build = 227;
static const String engine =
"Flutter 3.7.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 9944297138 (2 weeks ago) • 2023-02-08 15:46:04 -0800\nEngine • revision 248290d6d5\nTools • Dart 2.19.2 • DevTools 2.20.1\n";
static const String buildAt = "2023-02-25 18:23:00.718232";
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,3 +0,0 @@
import 'package:flutter/widgets.dart';
final appIcon = Image.asset('assets/app_icon.png');

View File

@@ -1,3 +1,4 @@
/// RegExp for number
final numReg = RegExp(r'\s{1,}');
/// Private Key max allowed size is 20kb

View File

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

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

@@ -1,12 +1,32 @@
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';
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
""";

View File

@@ -1,5 +0,0 @@
import 'package:flutter/widgets.dart';
const height13 = SizedBox(height: 13);
const width13 = SizedBox(width: 13);
const width7 = SizedBox(width: 7);

View File

@@ -5,14 +5,14 @@ import '../model/server/net_speed.dart';
import '../model/server/server_status.dart';
import '../model/server/tcp_status.dart';
get _initMemory => Memory(
Memory get _initMemory => Memory(
total: 1,
used: 0,
free: 1,
cache: 0,
avail: 1,
);
get _initOneTimeCpuStatus => OneTimeCpuStatus(
OneTimeCpuStatus get _initOneTimeCpuStatus => OneTimeCpuStatus(
'cpu',
0,
0,
@@ -22,28 +22,28 @@ get _initOneTimeCpuStatus => OneTimeCpuStatus(
0,
0,
);
get initCpuStatus => CpuStatus(
CpuStatus get initCpuStatus => CpuStatus(
[_initOneTimeCpuStatus],
[_initOneTimeCpuStatus],
'',
);
get _initNetSpeedPart => NetSpeedPart(
NetSpeedPart get _initNetSpeedPart => NetSpeedPart(
'',
BigInt.zero,
BigInt.zero,
BigInt.zero,
);
get initNetSpeed => NetSpeed(
NetSpeed get initNetSpeed => NetSpeed(
[_initNetSpeedPart],
[_initNetSpeedPart],
);
get _initSwap => Swap(
Swap get _initSwap => Swap(
total: 1,
used: 0,
free: 1,
cached: 0,
);
get initStatus => ServerStatus(
ServerStatus get initStatus => ServerStatus(
cpu: initCpuStatus,
mem: _initMemory,
sysVer: 'Loading...',

View File

@@ -1,6 +1,26 @@
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(

View File

@@ -1,23 +0,0 @@
import 'package:flutter/material.dart';
import '../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),
);

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

@@ -31,6 +31,8 @@ class SettingStore extends PersistentStore {
property('maxRetryCount', defaultValue: 7);
/// Night mode: 0 -> auto, 1 -> light, 2 -> dark
StoreProperty<int> get nightMode =>
property('nightMode', defaultValue: ThemeMode.system.index);
StoreProperty<int> get themeMode => property('themeMode', defaultValue: 0);
/// Font file path
StoreProperty<String> get fontPath => property('fontPath');
}

View File

@@ -1,6 +1,6 @@
{
"about": "About",
"aboutThanks": "\n\nThanks to the following people who participated in the test.",
"aboutThanks": "\nThanks to the following people who participated in the test.",
"addAServer": "add a server",
"addOne": "Add one",
"addPrivateKey": "Add private key",
@@ -15,6 +15,7 @@
"cancel": "Cancel",
"choose": "Choose",
"chooseDestination": "Choose destination",
"chooseFontFile": "Choose a font file",
"choosePrivateKey": "Choose private key",
"clear": "Clear",
"clickSee": "Click here",
@@ -51,6 +52,7 @@
"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.",
@@ -58,6 +60,8 @@
"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",
@@ -87,6 +91,7 @@
"min": "min",
"ms": "ms",
"name": "Name",
"needRestart": "Need to restart app",
"newContainer": "New container",
"noClient": "No client",
"noInterface": "No interface",
@@ -95,8 +100,11 @@
"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",
@@ -111,9 +119,11 @@
"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} ?",
@@ -138,6 +148,7 @@
"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}]?",

View File

@@ -1,6 +1,6 @@
{
"about": "关于",
"aboutThanks": "\n\n感谢以下参与软件测试的各位。",
"aboutThanks": "\n感谢以下参与软件测试的各位。",
"addAServer": "添加服务器",
"addOne": "前去新增",
"addPrivateKey": "添加一个私钥",
@@ -15,6 +15,7 @@
"cancel": "取消",
"choose": "选择",
"chooseDestination": "选择目标",
"chooseFontFile": "选择字体文件",
"choosePrivateKey": "选择私钥",
"clear": "清除",
"clickSee": "点击查看",
@@ -51,6 +52,7 @@
"experimentalFeature": "实验性功能",
"export": "导出",
"extraArgs": "额外参数",
"failed": "失败",
"feedback": "反馈",
"feedbackOnGithub": "如果你有任何问题请在GitHub反馈",
"fieldMustNotEmpty": "这些输入框不能为空。",
@@ -58,6 +60,8 @@
"fileTooLarge": "文件 '{file}' 过大 '{size}',超过了 {sizeMax}",
"files": "文件",
"foundNUpdate": "找到 {count} 个更新",
"getPushTokenFailed": "未能获取到推送token",
"gettingToken": "正在获取Token...",
"go": "开始",
"goto": "前往",
"host": "主机",
@@ -87,6 +91,7 @@
"min": "最小",
"ms": "毫秒",
"name": "名称",
"needRestart": "需要重启 App",
"newContainer": "新建容器",
"noClient": "没有SSH连接",
"noInterface": "没有可用的接口",
@@ -95,8 +100,11 @@
"noSavedSnippet": "没有已保存的代码片段。",
"noServerAvailable": "没有可用的服务器。",
"noUpdateAvailable": "没有可用更新",
"notSelected": "未选择",
"nullToken": "无Token",
"ok": "好",
"onServerDetailPage": "在服务器详情页",
"onlyIOS": "仅在iOS上有效",
"open": "打开",
"path": "路径",
"pickFile": "选择文件",
@@ -111,9 +119,11 @@
"port": "端口",
"preview": "预览",
"privateKey": "私钥",
"pushToken": "消息推送 Token",
"pwd": "密码",
"rename": "重命名",
"reportBugsOnGithubIssue": "请到 {url} 提交问题",
"restart": "重启",
"restore": "恢复",
"restoreSuccess": "恢复成功需要重启App来应用更改",
"restoreSureWithDate": "确定恢复 {date} 的备份吗?",
@@ -138,6 +148,7 @@
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
"start": "开始",
"stop": "停止",
"success": "成功",
"sureDelete": "确定删除[{name}]",
"sureNoPwd": "确认使用无密码?",
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",

View File

@@ -31,7 +31,7 @@ void setupLocatorForProviders() {
locator.registerSingleton(VirtualKeyboard());
locator.registerSingleton(SnippetProvider());
locator.registerSingleton(PrivateKeyProvider());
locator.registerSingleton(SftpDownloadProvider());
locator.registerSingleton(SftpProvider());
}
Future<void> setupLocatorForStores() async {

View File

@@ -7,6 +7,7 @@ import 'package:provider/provider.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';
@@ -19,7 +20,9 @@ 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;
@@ -31,6 +34,9 @@ Future<void> initApp() async {
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) {
@@ -86,10 +92,11 @@ Future<void> main() async {
ChangeNotifierProvider(create: (_) => locator<SnippetProvider>()),
ChangeNotifierProvider(create: (_) => locator<VirtualKeyboard>()),
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
ChangeNotifierProvider(
create: (_) => locator<SftpDownloadProvider>()),
ChangeNotifierProvider(create: (_) => locator<SftpProvider>()),
],
child: MyApp(),
child: RebuildWidget(
child: MyApp(),
),
),
);
});

View File

@@ -10,7 +10,7 @@ import 'package:toolbox/data/res/color.dart';
import '../../core/extension/colorx.dart';
import '../../core/utils/ui.dart';
import '../../data/model/app/backup.dart';
import '../../data/res/font_style.dart';
import '../../data/res/ui.dart';
import '../../data/store/private_key.dart';
import '../../data/store/server.dart';
import '../../data/store/setting.dart';

View File

@@ -8,8 +8,8 @@ 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/font_style.dart';
import '../../data/res/menu.dart';
import '../../data/res/ui.dart';
import '../../data/res/url.dart';
import '../../data/store/docker.dart';
import '../../locator.dart';
@@ -47,10 +47,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
@override
void initState() {
super.initState();
final client = locator<ServerProvider>()
.servers
.firstWhere((element) => element.spi == widget.spi)
.client;
final client = locator<ServerProvider>().getServer(widget.spi.id).client;
if (client == null) {
showSnackBar(context, Text(_s.noClient));
Navigator.of(context).pop();

View File

@@ -11,9 +11,8 @@ 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/font_style.dart';
import '../../data/res/icon.dart';
import '../../data/res/tab.dart';
import '../../data/res/ui.dart';
import '../../data/res/url.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
@@ -310,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

@@ -9,7 +9,7 @@ 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/font_style.dart';
import '../../data/res/ui.dart';
import '../../locator.dart';
import '../widget/input_field.dart';
import '../widget/round_rect_card.dart';

View File

@@ -8,7 +8,7 @@ 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/font_style.dart';
import '../../data/res/ui.dart';
import '../../locator.dart';
import '../widget/center_loading.dart';
import '../widget/round_rect_card.dart';
@@ -48,9 +48,7 @@ class _PkgManagePageState extends State<PkgManagePage>
@override
void initState() {
super.initState();
final si = locator<ServerProvider>()
.servers
.firstWhere((e) => e.spi == widget.spi);
final si = locator<ServerProvider>().getServer(widget.spi.id);
if (si.client == null) {
showSnackBar(context, Text(_s.waitConnection));
Navigator.of(context).pop();
@@ -179,8 +177,9 @@ class _PkgManagePageState extends State<PkgManagePage>
}
return ListView(
padding: const EdgeInsets.all(13),
children:
[_buildUpdatePanel(apt)].map((e) => RoundRectCard(e)).toList(),
children: [
_buildUpdatePanel(apt),
].map((e) => RoundRectCard(e)).toList(),
);
}),
);

View File

@@ -13,7 +13,7 @@ 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/font_style.dart';
import '../../../data/res/ui.dart';
import '../../../locator.dart';
import '../../widget/input_decoration.dart';

View File

@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
import '../../../core/route.dart';
import '../../../data/provider/private_key.dart';
import '../../../data/res/font_style.dart';
import '../../../data/res/ui.dart';
import 'edit.dart';
import '../../../view/widget/round_rect_card.dart';

View File

@@ -9,9 +9,7 @@ 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/font_style.dart';
import '../../../data/res/padding.dart';
import '../../../data/res/sizedbox.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/round_rect_card.dart';
@@ -43,9 +41,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget build(BuildContext context) {
return Consumer<ServerProvider>(builder: (_, provider, __) {
return _buildMainPage(
provider.servers.firstWhere(
(e) => e.spi.id == widget.id,
),
provider.getServer(widget.id),
);
});
}

View File

@@ -10,7 +10,7 @@ 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/font_style.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/private_key.dart';
import '../../../locator.dart';
import '../../widget/input_decoration.dart';

View File

@@ -13,8 +13,8 @@ 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/font_style.dart';
import '../../../data/res/menu.dart';
import '../../../data/res/ui.dart';
import '../../../data/res/url.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
@@ -443,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,8 +1,13 @@
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/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';
@@ -10,10 +15,11 @@ import '../../data/provider/app.dart';
import '../../data/provider/server.dart';
import '../../data/res/build_data.dart';
import '../../data/res/color.dart';
import '../../data/res/font_style.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 {
@@ -36,6 +42,8 @@ class _SettingPageState extends State<SettingPage> {
late double _maxRetryCount;
late double _updateInterval;
String? _pushToken;
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -50,7 +58,7 @@ class _SettingPageState extends State<SettingPage> {
_setting = locator<SettingStore>();
_launchPageIdx = _setting.launchPage.fetch()!;
_termThemeIdx = _setting.termColorIdx.fetch()!;
_nightMode = _setting.nightMode.fetch()!;
_nightMode = _setting.themeMode.fetch()!;
_updateInterval = _setting.serverStatusUpdateInterval.fetch()!.toDouble();
_maxRetryCount = _setting.maxRetryCount.fetch()!.toDouble();
}
@@ -70,6 +78,7 @@ class _SettingPageState extends State<SettingPage> {
// Server
_buildTitle(_s.server),
_buildServer(),
const SizedBox(height: 37),
],
),
);
@@ -81,20 +90,25 @@ class _SettingPageState extends State<SettingPage> {
child: Center(
child: Text(
text,
style: textSize13,
style: grey,
),
),
);
}
Widget _buildApp() {
final children = [
_buildThemeMode(),
_buildAppColorPreview(),
_buildLaunchPage(),
_buildCheckUpdate(),
_buildFont(),
];
if (isIOS) {
children.add(_buildPushToken());
}
return Column(
children: [
_buildNightMode(),
_buildAppColorPreview(),
_buildLaunchPage(),
_buildCheckUpdate(),
].map((e) => RoundRectCard(e)).toList(),
children: children.map((e) => RoundRectCard(e)).toList(),
);
}
@@ -113,11 +127,10 @@ class _SettingPageState extends State<SettingPage> {
return ListTile(
title: Text(
_s.showDistLogo,
style: textSize13,
),
subtitle: Text(
_s.onServerDetailPage,
style: textSize13Grey,
style: grey,
),
trailing: buildSwitch(context, _setting.showDistLogo),
);
@@ -140,8 +153,6 @@ class _SettingPageState extends State<SettingPage> {
trailing: const Icon(Icons.keyboard_arrow_right),
title: Text(
display,
style: textSize13,
textAlign: TextAlign.start,
),
onTap: () => doUpdate(context, force: true),
);
@@ -154,16 +165,13 @@ class _SettingPageState extends State<SettingPage> {
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}',
style: textSize13,
),
children: [
Slider(
@@ -190,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(),
@@ -213,7 +221,6 @@ class _SettingPageState extends State<SettingPage> {
),
title: Text(
_s.appPrimaryColor,
style: textSize13,
),
children: [_buildAppColorPicker(), _buildColorPickerConfirmBtn()],
);
@@ -234,7 +241,7 @@ class _SettingPageState extends State<SettingPage> {
icon: const Icon(Icons.save),
onPressed: (() {
_setting.primaryColor.put(_selectedColorValue);
setState(() {});
_showRestartSnackbar();
}),
);
}
@@ -245,13 +252,11 @@ class _SettingPageState extends State<SettingPage> {
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,
),
),
@@ -261,7 +266,6 @@ class _SettingPageState extends State<SettingPage> {
contentPadding: EdgeInsets.zero,
title: Text(
tabTitleName(context, tabs.indexOf(e)),
style: textSize13,
),
trailing: _buildRadio(tabs.indexOf(e)),
),
@@ -289,11 +293,9 @@ class _SettingPageState extends State<SettingPage> {
childrenPadding: const EdgeInsets.only(left: 17),
title: Text(
_s.termTheme,
style: textSize13,
),
trailing: Text(
TerminalColorsPlatform.values[_termThemeIdx].name,
style: textSize13,
),
children: _buildTermThemeRadioList(),
);
@@ -306,7 +308,6 @@ class _SettingPageState extends State<SettingPage> {
contentPadding: EdgeInsets.zero,
title: Text(
e.name,
style: textSize13,
),
trailing: _buildTermThemeRadio(e),
),
@@ -333,12 +334,10 @@ class _SettingPageState extends State<SettingPage> {
textColor: primaryColor,
title: Text(
_s.maxRetryCount,
style: textSize13,
textAlign: TextAlign.start,
),
trailing: Text(
'${_maxRetryCount.toInt()} ${_s.times}',
style: textSize13,
),
children: [
Slider(
@@ -364,7 +363,7 @@ class _SettingPageState extends State<SettingPage> {
_maxRetryCount == 0.0
? Text(
_s.maxRetryCountEqual0,
style: const TextStyle(color: Colors.grey, fontSize: 12),
style: grey,
textAlign: TextAlign.center,
)
: const SizedBox(),
@@ -375,17 +374,14 @@ class _SettingPageState extends State<SettingPage> {
);
}
Widget _buildNightMode() {
Widget _buildThemeMode() {
return ExpansionTile(
textColor: primaryColor,
title: Text(
_s.themeMode,
style: textSize13,
textAlign: TextAlign.start,
),
trailing: Text(
_buildNightModeStr(_nightMode),
style: textSize13,
_buildThemeModeStr(_nightMode),
),
children: [
Slider(
@@ -400,16 +396,16 @@ class _SettingPageState extends State<SettingPage> {
});
},
onChangeEnd: (val) {
_setting.nightMode.put(val.toInt());
_setting.themeMode.put(val.toInt());
},
label: _buildNightModeStr(_nightMode),
label: _buildThemeModeStr(_nightMode),
divisions: 2,
),
],
);
}
String _buildNightModeStr(int n) {
String _buildThemeModeStr(int n) {
switch (n) {
case 1:
return _s.light;
@@ -419,4 +415,87 @@ class _SettingPageState extends State<SettingPage> {
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

@@ -9,8 +9,8 @@ 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/font_style.dart';
import '../../../data/res/path.dart';
import '../../../data/res/ui.dart';
import '../../widget/fade_in.dart';
import 'downloading.dart';

View File

@@ -7,7 +7,7 @@ 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/font_style.dart';
import '../../../data/res/ui.dart';
import '../../widget/center_loading.dart';
import '../../widget/round_rect_card.dart';
@@ -41,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),

View File

@@ -51,7 +51,7 @@ class _SFTPPageState extends State<SFTPPage> {
void initState() {
super.initState();
final serverProvider = locator<ServerProvider>();
_si = serverProvider.servers.firstWhere((s) => s.spi == widget.spi);
_si = serverProvider.getServer(widget.spi.id);
_client = _si?.client;
}
@@ -305,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,

View File

@@ -5,7 +5,7 @@ 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/font_style.dart';
import '../../../data/res/ui.dart';
import '../../../locator.dart';
import '../../widget/input_decoration.dart';

View File

@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import '../../../data/res/ui.dart';
import '/core/route.dart';
import '/data/provider/snippet.dart';
import '/data/res/font_style.dart';
import 'edit.dart';
import '/view/widget/round_rect_card.dart';

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
@@ -9,6 +8,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:xterm/xterm.dart' hide TerminalColors;
import '../../core/utils/platform.dart';
import '../../data/model/ssh/terminal_color.dart';
import '../../core/utils/misc.dart';
import '../../core/utils/ui.dart';
@@ -17,7 +17,7 @@ import '../../data/model/server/server_private_info.dart';
import '../../data/model/ssh/virtual_key.dart';
import '../../data/provider/virtual_keyboard.dart';
import '../../data/res/color.dart';
import '../../data/res/terminal_theme.dart';
import '../../data/res/terminal.dart';
import '../../data/res/virtual_key.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
@@ -34,6 +34,7 @@ class _SSHPageState extends State<SSHPage> {
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();
@@ -47,7 +48,7 @@ class _SSHPageState extends State<SSHPage> {
@override
void initState() {
super.initState();
final termColorIdx = locator<SettingStore>().termColorIdx.fetch()!;
final termColorIdx = _setting.termColorIdx.fetch()!;
_termColors = TerminalColorsPlatform.values[termColorIdx].colors;
initTerminal();
}
@@ -67,11 +68,30 @@ class _SSHPageState extends State<SSHPage> {
super.dispose();
}
Future<void> initTerminal() async {
_terminal.write('Connecting...\r\n');
void _write(String p0) {
_terminal.write('$p0\r\n');
}
_client = await genClient(widget.spi);
_terminal.write('Connected\r\n');
Future<void> initTerminal() async {
_write('Connecting...\r\n');
_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');
final session = await _client!.shell(
pty: SSHPtyConfig(
@@ -111,7 +131,7 @@ class _SSHPageState extends State<SSHPage> {
body: _buildBody(termTheme.toTerminalTheme(_termColors)),
bottomNavigationBar: _buildBottom(termTheme.background),
);
if (Platform.isIOS) {
if (isIOS) {
child = AnnotatedRegion(
value: _isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark,
child: child,
@@ -130,8 +150,10 @@ class _SSHPageState extends State<SSHPage> {
_terminal,
controller: _terminalController,
keyboardType: TextInputType.visiblePassword,
textStyle: TerminalStyle.fromTextStyle(
TextStyle(fontFamily: getFileName(_setting.fontPath.fetch()))),
theme: termTheme,
deleteDetection: Platform.isIOS,
deleteDetection: isIOS,
onTapUp: _onTapUp,
autoFocus: true,
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
@@ -272,58 +294,56 @@ class _SSHPageState extends State<SSHPage> {
}
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();
},
),
],
);
},
);
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,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

@@ -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

@@ -159,6 +159,7 @@ Future<void> flutterBuildMacOS() async {
Future<void> flutterBuildAndroid() async {
await flutterBuild(apkPath, './release/${appName}_build_Arm64.apk', 'apk');
await killJava();
}
Future<void> changeAppleVersion() async {
@@ -172,6 +173,18 @@ Future<void> changeAppleVersion() async {
}
}
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.');

View File

@@ -230,10 +230,10 @@ packages:
dependency: "direct main"
description:
name: dartssh2
sha256: "4c44ad7bb6a12cd56f775fc72b64e81fcc3f7971dc731c48359f2aa551d6f6a7"
sha256: "53a230c7dd6f487b704ceef1b29323ad64d19be89e786ccbc81e157a70417a56"
url: "https://pub.dev"
source: hosted
version: "2.7.3"
version: "2.8.2"
dio:
dependency: "direct main"
description:
@@ -646,6 +646,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.1"
plain_notification_token:
dependency: "direct main"
description:
name: plain_notification_token
sha256: "32481c1c6ad76613ed38822ffe819ea5d5534f9e07e8ff1d44aafe069f45ea24"
url: "https://pub.dev"
source: hosted
version: "0.0.4"
platform:
dependency: transitive
description:

View File

@@ -39,7 +39,7 @@ dependencies:
extended_image: ^6.0.3
url_launcher: ^6.1.8
countly_flutter: ^22.2.0
dartssh2: ^2.7.1
dartssh2: ^2.8.2
logging: ^1.0.2
flutter_material_color_picker: ^1.1.0+2
circle_chart:
@@ -57,6 +57,7 @@ dependencies:
xterm:
path: ../xterm.dart
file_picker: ^5.2.5
plain_notification_token: ^0.0.4
dev_dependencies:
flutter_native_splash: ^2.1.6