Compare commits

..

86 Commits

Author SHA1 Message Date
lollipopkit
e3f2b211a9 new & opt.
opt.: input field auto focus
opt.: snippet page top padding
new: setting of editor font size
2023-08-21 13:54:09 +08:00
lollipopkit
6a2191ff92 opt.: cntering btn text 2023-08-21 13:04:07 +08:00
lollipopkit
8111a83703 opt. 2023-08-20 23:30:44 +08:00
lollipopkit
e643378249 update: README 2023-08-20 22:55:32 +08:00
lollipopkit
d663106f9f opt.: UI 2023-08-20 22:40:39 +08:00
lollipopkit
d5f8cf6cf0 new: animation of circle_chart 2023-08-20 21:59:28 +08:00
lollipopkit
a0287a9f36 #139 fix 2023-08-20 21:34:32 +08:00
lollipopkit
9c8ed3dfa8 opt.: TagBtn 2023-08-20 21:33:27 +08:00
lollipopkit
f02cca1981 opt.: move logic of reorder to settings page 2023-08-20 20:14:02 +08:00
lollipopkit
46cc363413 #94 new: option of moving out server func btns 2023-08-20 19:07:20 +08:00
lollipopkit
a59286473f opt.
- move out btns from more vert btn (docker, sftp & etc.)
- redesigned routes
- confirmation before deleting private key
2023-08-20 18:30:08 +08:00
lollipopkit
f88f5c3bda Merge pull request #141 from lollipopkit/lollipopkit/issue140
#140 feat: double columns of servers tab
2023-08-20 14:05:17 +08:00
lollipopkit
b5d8b8771e #140 feat: double columns of servers tab 2023-08-20 14:01:41 +08:00
lollipopkit
35e9ecedd0 Merge pull request #137 from lollipopkit/lollipopkit/issue136
Lollipopkit/issue136
2023-08-18 20:08:30 +08:00
lollipopkit
b5c705a1fe feat: no titlebar on macOS
Fixes #136
2023-08-18 20:01:18 +08:00
lollipopkit
fe51669369 chore: update l10n 2023-08-18 16:33:16 +08:00
lollipopkit
46cffb836c new & opt.
- new: ssh discontinuity test
- opt.: server cmds
- opt.: check ssh client status before exec cmds
- new: #124 notify on discontinuity
2023-08-17 21:36:00 +08:00
lollipopkit
b78949cf0c opt.: only display virt keys on mobile devices 2023-08-17 18:43:41 +08:00
lollipopkit
1be87d0ec0 fix: ssh session not inited 2023-08-17 14:59:23 +08:00
lollipopkit
329922a836 opt.: settings 2023-08-16 15:11:22 +08:00
lollipopkit
c62c8e2c43 #135 opt. 2023-08-15 23:05:51 +08:00
lollipopkit
cfca40b7be #130 opt. 2023-08-14 11:07:05 +08:00
lollipopkit
8057c24947 opt. 2023-08-13 22:19:08 +08:00
lollipopkit
1af7271a06 fix: alterUrl will change spi's props 2023-08-13 21:19:16 +08:00
calvinweb
7ce03c18b2 Merge pull request #126 from lollipopkit/fix_system_private_prompt
FIX:Fix system private prompt
2023-08-12 09:28:27 +08:00
calvin
ab8fdf3106 FIX:Fix system private prompt 2023-08-12 09:27:55 +08:00
lollipopkit
1d1b186d1e Merge branch 'main' of https://github.com/lollipopkit/flutter_server_box 2023-08-11 23:48:51 +08:00
lollipopkit
fb1f868c42 Merge branch 'main' of https://github.com/lollipopkit/flutter_server_box 2023-08-11 23:48:35 +08:00
lollipopkit
e08fa188ec Merge pull request #123 from lollipopkit/remove_header_bar
FEAT:Remove headerbar for linux so it will look better
2023-08-11 23:37:55 +08:00
lollipopkit
e30bf47f0d new: AGP 7.4.2 2023-08-11 23:35:29 +08:00
calvin
253ab40e5c FEAT:Remove headerbar for linux so it will look better 2023-08-11 13:48:31 +08:00
lollipopkit
58a08757f5 #118 fix 2023-08-11 11:45:22 +08:00
lollipopkit
9ca096094f add: refresh btn on desktop 2023-08-10 18:39:58 +08:00
lollipopkit
4788f1dddc #116 fix 2023-08-10 18:05:24 +08:00
lollipopkit
cf1c9643b9 opt.: check client before route pushed 2023-08-10 00:37:51 +08:00
lollipopkit
c512a6a274 opt. 2023-08-09 23:58:38 +08:00
lollipopkit
58fbd62779 #86 fix: docker loading forever 2023-08-08 20:31:35 +08:00
lollipopkit
173b7f6362 fix: typo 2023-08-08 18:43:17 +08:00
lollipopkit
9fb738eda1 #114 fix 2023-08-08 18:38:56 +08:00
lollipopkit
d35d106ad4 opt. 2023-08-08 17:46:04 +08:00
lollipopkit
159942de95 #112 new: check hash during upgrade 2023-08-08 16:31:28 +08:00
lollipopkit
693eef8f7e new: animation for process page 2023-08-08 15:21:22 +08:00
lollipopkit
2887d23381 new: animation 2023-08-08 14:55:13 +08:00
lollipopkit
096d41088f rm: fvm 2023-08-08 12:52:28 +08:00
lollipopkit
bd84eeca0b Merge pull request #111 from lollipopkit/lollipopkit/issue95
#95 fix
2023-08-07 20:05:48 +08:00
lollipopkit
b804f43d5a opt. 2023-08-07 20:03:48 +08:00
lollipopkit
36b24bedb4 #95 fix 2023-08-07 20:02:09 +08:00
lollipopkit
c1b3ff7bfd Merge pull request #110 from lollipopkit/lollipopkit/issue109
Lollipopkit/issue109
2023-08-07 18:04:55 +08:00
lollipopkit
20c859b0a1 #109 fix: path sep on win 2023-08-07 17:41:35 +08:00
lollipopkit
c4925ee2c7 opt.: delete key after use 2023-08-07 16:51:47 +08:00
lollipopkit
d37a1fbea7 feat: use native ssh terminal on desktop
Implement #109
2023-08-07 16:27:00 +08:00
lollipopkit
2142ae3e1c new: kill process 2023-08-07 15:01:58 +08:00
lollipopkit
e686df45c9 new: actions analysis 2023-08-07 14:57:21 +08:00
calvinweb
ed9ed905ed Merge pull request #108 from calvinweb/call_system_terminal
FEAT:Support call system terminal(Macos&Linux)
2023-08-07 14:31:46 +08:00
lollipopkit
98e77b9d0f #105 new: switch of auto check update 2023-08-07 13:30:53 +08:00
lollipopkit
879a347f23 update: README 2023-08-07 13:10:24 +08:00
lollipopkit
cab58c30a7 Merge branch 'main' of https://github.com/lollipopkit/flutter_server_box 2023-08-07 12:55:04 +08:00
lollipopkit
75b9a3eeb0 Merge branch 'main' of https://github.com/lollipopkit/flutter_server_box 2023-08-07 12:54:56 +08:00
lollipopkit
00bf34965a Merge pull request #102 from gaelthas/main
Update: FileDescription in Windows
2023-08-07 12:51:07 +08:00
lollipopkit
81ab841fa5 #100 fix 2023-08-07 12:46:22 +08:00
lollipopkit
df313adf39 Update README.md 2023-08-07 12:41:49 +08:00
Galois
c991c20cc1 Update: FileDescription in Windows
Signed-off-by: Galois <fv10015@outlook.com>
2023-08-06 20:02:47 +08:00
lollipopkit
0e54be8f66 Merge pull request #101 from gaelthas/main
Update the title in Windows and Linux.
2023-08-06 20:01:55 +08:00
Galois
140a3de5ed Update: linux BINARY_NAME
Signed-off-by: Galois <fv10015@outlook.com>
2023-08-06 19:54:17 +08:00
Galois
ae97012456 Update: Windows title
Signed-off-by: Galois <fv10015@outlook.com>
2023-08-06 19:48:39 +08:00
Galois
20d81e4353 Fix: readme_zh 上述
Signed-off-by: Galois <fv10015@outlook.com>
2023-08-06 11:35:18 +08:00
lollipopkit
8abdcf15d4 #96 opt. 2023-08-05 22:09:54 +08:00
lollipopkit
7f35ddfe30 #96 opt. 2023-08-05 21:19:19 +08:00
lollipopkit
7431de094f #97 fix 2023-08-05 21:15:29 +08:00
lollipopkit
4b7397de46 opt. 2023-08-05 17:48:56 +08:00
lollipopkit
a716254557 Merge branch 'main' of https://github.com/lollipopkit/flutter_server_box 2023-08-05 15:20:05 +08:00
lollipopkit
c406d92b82 opt.: refactor AppShellFunc 2023-08-05 15:19:37 +08:00
lollipopkit
432e3b1824 add: more contributors 2023-08-05 14:32:21 +08:00
calvin
73611dacf1 FEAT:Support call system terminal(Macos&Linux) 2023-08-05 14:12:08 +08:00
lollipopkit
8f4f141a64 new: import local file & rm local dir 2023-08-05 13:34:55 +08:00
lollipopkit
51af3c63f1 Merge pull request #93 from Liloupar/Liloupar-patch-1
typo
2023-08-05 13:07:08 +08:00
Liloupar
b3c35b385b typo 2023-08-05 13:02:21 +08:00
lollipopkit
5b2ed02428 opt.: move docker stats to more btn 2023-08-05 12:40:45 +08:00
lollipopkit
3405172d76 #90 #91 fix 2023-08-05 12:23:29 +08:00
lollipopkit
d88a078cd6 rm: field password of PrivateKeyInfo 2023-08-04 23:47:22 +08:00
lollipopkit
ee3e30d9b5 #88 #74 fix: firstWhere 2023-08-04 22:53:26 +08:00
lollipopkit
0250589be2 Merge pull request #89 from lollipopkit/lollipopkit/issue88
#88 #74 fix: rootDisk if not found
2023-08-04 22:01:33 +08:00
lollipopkit
a66204f672 #88 #74 fix: rootDisk if not found 2023-08-04 21:58:48 +08:00
lollipopkit
91967e6ce3 #87 new: auto ask add system key (~/.ssh/id_rsa) 2023-08-04 21:46:44 +08:00
lollipopkit
60507ea4bc Merge pull request #87 from calvinweb/system_privatekey
DRAFT:Add support of reading system privatekey (.ssh/id_rsa)
2023-08-04 18:50:52 +08:00
calvin
486b920d6b DRAFT:Add support of reading system privatekey (.ssh/id_rsa) 2023-08-04 18:41:04 +08:00
120 changed files with 3776 additions and 2272 deletions

View File

@@ -128,6 +128,12 @@ abstract class S {
/// **'Add private key'**
String get addPrivateKey;
/// No description provided for @addSystemPrivateKeyTip.
///
/// In en, this message translates to:
/// **'Currently don\'t have any private key, do you add the one that comes with the system (~/.ssh/id_rsa)?'**
String get addSystemPrivateKeyTip;
/// No description provided for @added2List.
///
/// In en, this message translates to:
@@ -164,6 +170,12 @@ abstract class S {
/// **'Auto'**
String get auto;
/// No description provided for @autoCheckUpdate.
///
/// In en, this message translates to:
/// **'Auto check update'**
String get autoCheckUpdate;
/// No description provided for @autoUpdateHomeWidget.
///
/// In en, this message translates to:
@@ -254,6 +266,12 @@ abstract class S {
/// **'Connection'**
String get conn;
/// No description provided for @connected.
///
/// In en, this message translates to:
/// **'Connected'**
String get connected;
/// No description provided for @containerName.
///
/// In en, this message translates to:
@@ -548,6 +566,12 @@ abstract class S {
/// **'Getting token...'**
String get gettingToken;
/// No description provided for @goBackQ.
///
/// In en, this message translates to:
/// **'Go back?'**
String get goBackQ;
/// No description provided for @goto.
///
/// In en, this message translates to:
@@ -752,6 +776,18 @@ abstract class S {
/// **'Mission'**
String get mission;
/// No description provided for @moveOutServerFuncBtns.
///
/// In en, this message translates to:
/// **'Server function button location'**
String get moveOutServerFuncBtns;
/// No description provided for @moveOutServerFuncBtnsHelp.
///
/// In en, this message translates to:
/// **'On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.'**
String get moveOutServerFuncBtnsHelp;
/// No description provided for @ms.
///
/// In en, this message translates to:
@@ -824,6 +860,12 @@ abstract class S {
/// **'No server available.'**
String get noServerAvailable;
/// No description provided for @noTask.
///
/// In en, this message translates to:
/// **'No task'**
String get noTask;
/// No description provided for @noUpdateAvailable.
///
/// In en, this message translates to:
@@ -932,11 +974,11 @@ abstract class S {
/// **'Preview'**
String get preview;
/// No description provided for @primaryColor.
/// No description provided for @primaryColorSeed.
///
/// In en, this message translates to:
/// **'Primary color'**
String get primaryColor;
/// **'Primary color seed'**
String get primaryColorSeed;
/// No description provided for @privateKey.
///
@@ -1046,6 +1088,18 @@ abstract class S {
/// **'Server'**
String get server;
/// No description provided for @serverDetailOrder.
///
/// In en, this message translates to:
/// **'Detail page widget order'**
String get serverDetailOrder;
/// No description provided for @serverOrder.
///
/// In en, this message translates to:
/// **'Server order'**
String get serverOrder;
/// No description provided for @serverTabConnecting.
///
/// In en, this message translates to:
@@ -1094,12 +1148,6 @@ abstract class S {
/// **'Preparing to connect...'**
String get sftpDlPrepare;
/// No description provided for @sftpNoDownloadTask.
///
/// In en, this message translates to:
/// **'No download task.'**
String get sftpNoDownloadTask;
/// No description provided for @sftpSSHConnected.
///
/// In en, this message translates to:
@@ -1148,6 +1196,12 @@ abstract class S {
/// **'Start'**
String get start;
/// No description provided for @stats.
///
/// In en, this message translates to:
/// **'Stats'**
String get stats;
/// No description provided for @stop.
///
/// In en, this message translates to:
@@ -1178,6 +1232,12 @@ abstract class S {
/// **'Are you sure to use no password?'**
String get sureNoPwd;
/// No description provided for @sureStop.
///
/// In en, this message translates to:
/// **'Sure to stop [{item}] ?'**
String sureStop(Object item);
/// No description provided for @sureToDeleteServer.
///
/// In en, this message translates to:
@@ -1319,7 +1379,7 @@ abstract class S {
/// No description provided for @versionUnknownUpdate.
///
/// In en, this message translates to:
/// **'Current: v1.0.{build}'**
/// **'Current: v1.0.{build}, click to check updates'**
String versionUnknownUpdate(Object build);
/// No description provided for @versionUpdated.

View File

@@ -19,6 +19,9 @@ class SDe extends S {
@override
String get addPrivateKey => 'Private key hinzufügen';
@override
String get addSystemPrivateKeyTip => 'Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?';
@override
String get added2List => 'Zur Aufgabenliste hinzugefügt';
@@ -37,6 +40,9 @@ class SDe extends S {
@override
String get auto => 'System folgen';
@override
String get autoCheckUpdate => 'Aktualisierung automatisch prüfen';
@override
String get autoUpdateHomeWidget => 'Home-Widget automatisch aktualisieren';
@@ -82,6 +88,9 @@ class SDe extends S {
@override
String get conn => 'Verbindung';
@override
String get connected => 'in Verbindung gebracht';
@override
String get containerName => 'Container Name';
@@ -245,6 +254,9 @@ class SDe extends S {
@override
String get gettingToken => 'Getting token...';
@override
String get goBackQ => 'Zurückkommen?';
@override
String get goto => 'Pfad öffnen';
@@ -353,6 +365,12 @@ class SDe extends S {
@override
String get mission => 'Mission';
@override
String get moveOutServerFuncBtns => 'Position der Server-Funktionsschaltfläche';
@override
String get moveOutServerFuncBtnsHelp => 'Ein: kann unter jeder Karte auf der Registerkarte \"Server\" angezeigt werden. Aus: kann oben auf der Seite \"Serverdetails\" angezeigt werden.';
@override
String get ms => 'ms';
@@ -389,6 +407,9 @@ class SDe extends S {
@override
String get noServerAvailable => 'Kein Server verfügbar.';
@override
String get noTask => 'Nicht fragen';
@override
String get noUpdateAvailable => 'Kein Update verfügbar';
@@ -444,7 +465,7 @@ class SDe extends S {
String get preview => 'Vorschau';
@override
String get primaryColor => 'Farbschema';
String get primaryColorSeed => 'Farbschema';
@override
String get privateKey => 'Private Key';
@@ -504,6 +525,12 @@ class SDe extends S {
@override
String get server => 'Server';
@override
String get serverDetailOrder => 'Reihenfolge der Widgets auf der Detailseite';
@override
String get serverOrder => 'Server-Bestellung';
@override
String get serverTabConnecting => 'Verbinden...';
@@ -528,9 +555,6 @@ class SDe extends S {
@override
String get sftpDlPrepare => 'Verbindung vorbereiten...';
@override
String get sftpNoDownloadTask => 'Keine aktiven Downloads.';
@override
String get sftpSSHConnected => 'SFTP Verbunden';
@@ -559,6 +583,9 @@ class SDe extends S {
@override
String get start => 'Start';
@override
String get stats => 'Statistik';
@override
String get stop => 'Stop';
@@ -576,6 +603,11 @@ class SDe extends S {
@override
String get sureNoPwd => 'Bist du sicher, dass du kein Passwort verwenden willst?';
@override
String sureStop(Object item) {
return 'Sind Sie sicher, dass Sie [$item] stoppen möchten?';
}
@override
String sureToDeleteServer(Object server) {
return 'Bist du sicher, dass du [$server] löschen willst?';
@@ -655,7 +687,7 @@ class SDe extends S {
@override
String versionUnknownUpdate(Object build) {
return 'Aktuell: v1.0.$build';
return 'Aktuell: v1.0.$build. Klicken Sie hier, um nach Updates zu suchen';
}
@override

View File

@@ -19,6 +19,9 @@ class SEn extends S {
@override
String get addPrivateKey => 'Add private key';
@override
String get addSystemPrivateKeyTip => 'Currently don\'t have any private key, do you add the one that comes with the system (~/.ssh/id_rsa)?';
@override
String get added2List => 'Added to task list';
@@ -37,6 +40,9 @@ class SEn extends S {
@override
String get auto => 'Auto';
@override
String get autoCheckUpdate => 'Auto check update';
@override
String get autoUpdateHomeWidget => 'Auto update home widget';
@@ -82,6 +88,9 @@ class SEn extends S {
@override
String get conn => 'Connection';
@override
String get connected => 'Connected';
@override
String get containerName => 'Container name';
@@ -245,6 +254,9 @@ class SEn extends S {
@override
String get gettingToken => 'Getting token...';
@override
String get goBackQ => 'Go back?';
@override
String get goto => 'Go to';
@@ -353,6 +365,12 @@ class SEn extends S {
@override
String get mission => 'Mission';
@override
String get moveOutServerFuncBtns => 'Server function button location';
@override
String get moveOutServerFuncBtnsHelp => 'On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.';
@override
String get ms => 'ms';
@@ -389,6 +407,9 @@ class SEn extends S {
@override
String get noServerAvailable => 'No server available.';
@override
String get noTask => 'No task';
@override
String get noUpdateAvailable => 'No update available';
@@ -444,7 +465,7 @@ class SEn extends S {
String get preview => 'Preview';
@override
String get primaryColor => 'Primary color';
String get primaryColorSeed => 'Primary color seed';
@override
String get privateKey => 'Private Key';
@@ -504,6 +525,12 @@ class SEn extends S {
@override
String get server => 'Server';
@override
String get serverDetailOrder => 'Detail page widget order';
@override
String get serverOrder => 'Server order';
@override
String get serverTabConnecting => 'Connecting...';
@@ -528,9 +555,6 @@ class SEn extends S {
@override
String get sftpDlPrepare => 'Preparing to connect...';
@override
String get sftpNoDownloadTask => 'No download task.';
@override
String get sftpSSHConnected => 'SFTP Connected';
@@ -559,6 +583,9 @@ class SEn extends S {
@override
String get start => 'Start';
@override
String get stats => 'Stats';
@override
String get stop => 'Stop';
@@ -576,6 +603,11 @@ class SEn extends S {
@override
String get sureNoPwd => 'Are you sure to use no password?';
@override
String sureStop(Object item) {
return 'Sure to stop [$item] ?';
}
@override
String sureToDeleteServer(Object server) {
return 'Are you sure to delete server [$server]?';
@@ -655,7 +687,7 @@ class SEn extends S {
@override
String versionUnknownUpdate(Object build) {
return 'Current: v1.0.$build';
return 'Current: v1.0.$build, click to check updates';
}
@override

View File

@@ -19,6 +19,9 @@ class SId extends S {
@override
String get addPrivateKey => 'Tambahkan kunci pribadi';
@override
String get addSystemPrivateKeyTip => 'Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?';
@override
String get added2List => 'Ditambahkan ke Daftar Tugas';
@@ -37,6 +40,9 @@ class SId extends S {
@override
String get auto => 'Auto';
@override
String get autoCheckUpdate => 'Periksa pembaruan otomatis';
@override
String get autoUpdateHomeWidget => 'Widget Rumah Pembaruan Otomatis';
@@ -82,6 +88,9 @@ class SId extends S {
@override
String get conn => 'Koneksi';
@override
String get connected => 'Terhubung';
@override
String get containerName => 'Nama kontainer';
@@ -245,6 +254,9 @@ class SId extends S {
@override
String get gettingToken => 'Mendapatkan token ...';
@override
String get goBackQ => 'Datang kembali?';
@override
String get goto => 'Pergi ke';
@@ -353,6 +365,12 @@ class SId extends S {
@override
String get mission => 'Misi';
@override
String get moveOutServerFuncBtns => 'Lokasi tombol fungsi server';
@override
String get moveOutServerFuncBtnsHelp => 'Aktif: dapat ditampilkan di bawah setiap kartu pada halaman Tab Server. Nonaktif: dapat ditampilkan di bagian atas halaman Rincian Server.';
@override
String get ms => 'MS';
@@ -389,6 +407,9 @@ class SId extends S {
@override
String get noServerAvailable => 'Tidak ada server yang tersedia.';
@override
String get noTask => 'Tidak bertanya';
@override
String get noUpdateAvailable => 'Tidak ada pembaruan yang tersedia';
@@ -444,7 +465,7 @@ class SId extends S {
String get preview => 'Pratinjau';
@override
String get primaryColor => 'Warna utama';
String get primaryColorSeed => 'Warna utama';
@override
String get privateKey => 'Kunci Pribadi';
@@ -504,6 +525,12 @@ class SId extends S {
@override
String get server => 'Server';
@override
String get serverDetailOrder => 'Detail pesanan widget halaman';
@override
String get serverOrder => 'Pesanan server';
@override
String get serverTabConnecting => 'Menghubungkan ...';
@@ -528,9 +555,6 @@ class SId extends S {
@override
String get sftpDlPrepare => 'Bersiap untuk terhubung ...';
@override
String get sftpNoDownloadTask => 'Tidak ada tugas unduhan.';
@override
String get sftpSSHConnected => 'Sftp terhubung';
@@ -559,6 +583,9 @@ class SId extends S {
@override
String get start => 'Awal';
@override
String get stats => 'Statistik';
@override
String get stop => 'Berhenti';
@@ -576,6 +603,11 @@ class SId extends S {
@override
String get sureNoPwd => 'Apakah Anda pasti tidak menggunakan kata sandi?';
@override
String sureStop(Object item) {
return 'Anda yakin ingin menghentikan [$item]?';
}
@override
String sureToDeleteServer(Object server) {
return 'Apakah Anda pasti akan menghapus server [$server]?';
@@ -655,7 +687,7 @@ class SId extends S {
@override
String versionUnknownUpdate(Object build) {
return 'Saat ini: v1.0.$build';
return 'Saat ini: v1.0.$build. Klik untuk memeriksa pembaruan.';
}
@override

View File

@@ -19,6 +19,9 @@ class SZh extends S {
@override
String get addPrivateKey => '添加一个私钥';
@override
String get addSystemPrivateKeyTip => '当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa';
@override
String get added2List => '已添加至任务列表';
@@ -37,6 +40,9 @@ class SZh extends S {
@override
String get auto => '自动';
@override
String get autoCheckUpdate => '自动检查更新';
@override
String get autoUpdateHomeWidget => '自动更新桌面小部件';
@@ -82,6 +88,9 @@ class SZh extends S {
@override
String get conn => '连接';
@override
String get connected => '已连接';
@override
String get containerName => '容器名';
@@ -245,6 +254,9 @@ class SZh extends S {
@override
String get gettingToken => '正在获取Token...';
@override
String get goBackQ => '返回?';
@override
String get goto => '前往';
@@ -353,6 +365,12 @@ class SZh extends S {
@override
String get mission => '任务';
@override
String get moveOutServerFuncBtns => '服务器功能按钮位置';
@override
String get moveOutServerFuncBtnsHelp => '开启:可以在服务器 Tab 页的每个卡片下方显示。关闭:在服务器详情页顶部显示。';
@override
String get ms => '毫秒';
@@ -389,6 +407,9 @@ class SZh extends S {
@override
String get noServerAvailable => '没有可用的服务器。';
@override
String get noTask => '没有任务';
@override
String get noUpdateAvailable => '没有可用更新';
@@ -444,7 +465,7 @@ class SZh extends S {
String get preview => '预览';
@override
String get primaryColor => '主题色';
String get primaryColorSeed => '主题色种子';
@override
String get privateKey => '私钥';
@@ -504,6 +525,12 @@ class SZh extends S {
@override
String get server => '服务器';
@override
String get serverDetailOrder => '详情页部件顺序';
@override
String get serverOrder => '服务器顺序';
@override
String get serverTabConnecting => '连接中...';
@@ -529,10 +556,7 @@ class SZh extends S {
String get sftpDlPrepare => '准备连接至服务器...';
@override
String get sftpNoDownloadTask => '没有下载任务';
@override
String get sftpSSHConnected => 'SFTP 已连接,即将开始下载...';
String get sftpSSHConnected => 'SFTP 已连接...';
@override
String get showDistLogo => '显示发行版 Logo';
@@ -559,6 +583,9 @@ class SZh extends S {
@override
String get start => '开始';
@override
String get stats => '统计';
@override
String get stop => '停止';
@@ -576,6 +603,11 @@ class SZh extends S {
@override
String get sureNoPwd => '确认使用无密码?';
@override
String sureStop(Object item) {
return '确定要停止 [$item] 吗?';
}
@override
String sureToDeleteServer(Object server) {
return '你确定要删除服务器 [$server] 吗?';
@@ -655,7 +687,7 @@ class SZh extends S {
@override
String versionUnknownUpdate(Object build) {
return '当前v1.0.$build';
return '当前v1.0.$build,点击检查更新';
}
@override
@@ -701,6 +733,9 @@ class SZhTw extends SZh {
@override
String get addPrivateKey => '新增一個私鑰';
@override
String get addSystemPrivateKeyTip => '當前沒有任何私鑰,是否添加系統自帶的(~/.ssh/id_rsa';
@override
String get added2List => '已添加至任務列表';
@@ -719,6 +754,9 @@ class SZhTw extends SZh {
@override
String get auto => '自動';
@override
String get autoCheckUpdate => '自動檢查更新';
@override
String get autoUpdateHomeWidget => '自動更新桌面小部件';
@@ -764,6 +802,9 @@ class SZhTw extends SZh {
@override
String get conn => '連接';
@override
String get connected => '已連接';
@override
String get containerName => '容器名稱';
@@ -927,6 +968,9 @@ class SZhTw extends SZh {
@override
String get gettingToken => '正在獲取Token...';
@override
String get goBackQ => '返回?';
@override
String get goto => '前往';
@@ -1035,6 +1079,12 @@ class SZhTw extends SZh {
@override
String get mission => '任務';
@override
String get moveOutServerFuncBtns => '服務器功能按鈕位置';
@override
String get moveOutServerFuncBtnsHelp => '開啟:可以在服務器 Tab 頁的每個卡片下方顯示。關閉:在服務器詳情頁頂部顯示。';
@override
String get ms => '毫秒';
@@ -1071,6 +1121,9 @@ class SZhTw extends SZh {
@override
String get noServerAvailable => '沒有可用的服務器。';
@override
String get noTask => '沒有任務';
@override
String get noUpdateAvailable => '沒有可用更新';
@@ -1126,7 +1179,7 @@ class SZhTw extends SZh {
String get preview => '預覽';
@override
String get primaryColor => '主要色調';
String get primaryColorSeed => '主要色調種子';
@override
String get privateKey => '私鑰';
@@ -1186,6 +1239,12 @@ class SZhTw extends SZh {
@override
String get server => '服務器';
@override
String get serverDetailOrder => '詳情頁部件順序';
@override
String get serverOrder => '服務器順序';
@override
String get serverTabConnecting => '連接中...';
@@ -1211,10 +1270,7 @@ class SZhTw extends SZh {
String get sftpDlPrepare => '準備連接至服務器...';
@override
String get sftpNoDownloadTask => '沒有下載任務';
@override
String get sftpSSHConnected => 'SFTP 已連接,即將開始下載...';
String get sftpSSHConnected => 'SFTP 已連接...';
@override
String get showDistLogo => '顯示發行版 Logo';
@@ -1241,6 +1297,9 @@ class SZhTw extends SZh {
@override
String get start => '開始';
@override
String get stats => '統計';
@override
String get stop => '停止';
@@ -1258,6 +1317,11 @@ class SZhTw extends SZh {
@override
String get sureNoPwd => '確認使用無密碼?';
@override
String sureStop(Object item) {
return '確定要停止 [$item] 嗎?';
}
@override
String sureToDeleteServer(Object server) {
return '你確定要刪除服務器 [$server] 嗎?';
@@ -1337,7 +1401,7 @@ class SZhTw extends SZh {
@override
String versionUnknownUpdate(Object build) {
return '當前v1.0.$build';
return '當前v1.0.$build,點擊檢查更新';
}
@override

39
.github/workflows/analysis.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: flutter analysis
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
channel: 'stable' # or: 'beta', 'dev' or 'master'
- name: Install dependencies
run: flutter pub get
# Uncomment this step to verify the use of 'dart format' on each commit.
- name: Verify formatting
run: dart format --output=none .
# Consider passing '--fatal-infos' for slightly stricter analysis.
- name: Analyze project source
run: dart analyze
# Your project will need to have tests in test/ and a dependency on
# package:test for this step to succeed. Note that Flutter projects will
# want to change this to 'flutter test'.
- name: Run tests
run: flutter test

1
.gitignore vendored
View File

@@ -48,7 +48,6 @@ app.*.map.json
/android/app/fjy.androidstudio.key
/release
test.dart
.fvm
# Keep generated l10n files
/.dart_tool/*

12
.vscode/settings.json vendored
View File

@@ -1,12 +0,0 @@
{
"dart.flutterSdkPath": ".fvm",
"files.watcherExclude": {
"**/.fvm": true
},
"git.ignoredRepositories": [
".fvm"
],
"search.exclude": {
"**/.fvm": true
}
}

View File

@@ -44,20 +44,20 @@ If you have any question or feature request, please open a [discussion](https://
If ServerBox app has any bug, please open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
## 📱 ScreenShots
## 🏙️ ScreenShots
<table>
<tr>
<td>
<img width="200px" src="imgs/server.jpeg">
<img width="200px" src="imgs/server.png">
</td>
<td>
<img width="200px" src="imgs/detail.jpg">
<img width="200px" src="imgs/detail.png">
</td>
<td>
<img width="200px" src="imgs/ssh.jpg">
<img width="200px" src="imgs/sftp.png">
</td>
<td>
<img width="200px" src="imgs/editor.jpg">
<img width="200px" src="imgs/editor.png">
</td>
</tr>
</table>
@@ -67,7 +67,7 @@ If ServerBox app has any bug, please open an [issue](https://github.com/lollipop
<img width="200px" src="imgs/ping.png">
</td>
<td>
<img width="200px" src="imgs/sftp.jpeg">
<img width="200px" src="imgs/ssh.jpg">
</td>
<td>
<img width="200px" src="imgs/docker.jpeg">
@@ -83,12 +83,12 @@ If ServerBox app has any bug, please open an [issue](https://github.com/lollipop
Status|Platform
--- | ---
Full Support| Android / iOS / macOS
Support, but not tested| Windows / Linux
Not tested| Windows / Linux
## 🧱 Contribution
**Any positive contribution is welcome**.
10 iOS app redemption codes will be given away for the first time you participate in the contribution. This is the only thing I can do to thank you. :)
10 iOS app redemption codes will be given away for the first time you participate in the contribution. :)
### l10n guide
1. Fork this repo and clone forked repo to your local machine.
2. Create `arb` file in `lib/l10n/` directory
@@ -100,5 +100,9 @@ Support, but not tested| Windows / Linux
## 📝 License
1. You can package it for personal use, but you can't distribute it. (For example: You can teach others how to package it to avoid spending money to buy App, but you can't directly distribute the App you packaged.)
2. Except for the above, apply the `GPLv3` license.
- You can package it for personal use, but you can't distribute it.
- For example: You can teach others how to package it to avoid spending money to buy App, but you can't directly distribute the App you packaged.
- Why do I have to do this?
- Security: If anyone inject malicious code into the source code and distribute it, it will cause a lot of trouble.
- Income: Apple developer account = $99 per year. As a freshly graduated independent developer, I need income.
- Except for the above, apply the `GPLv3` license.

View File

@@ -27,7 +27,7 @@
## 🔖 特点
- [x] 功能
- [x] `SSH` 终端, `SFTP`
- [x] `Docker & 包` 管理器
- [x] `Docker & 包 & 进程` 管理器
- [x] 状态图表
- [x] 代码编辑器
- [x] `Ping` 和 更多
@@ -44,20 +44,20 @@
如果 ServerBox app 有任何 bug请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
## 📱 截屏
## 🏙️ 截屏
<table>
<tr>
<td>
<img width="200px" src="imgs/server.jpeg">
<img width="200px" src="imgs/server.png">
</td>
<td>
<img width="200px" src="imgs/detail.jpg">
<img width="200px" src="imgs/detail.png">
</td>
<td>
<img width="200px" src="imgs/ssh.jpg">
<img width="200px" src="imgs/sftp.png">
</td>
<td>
<img width="200px" src="imgs/editor.jpg">
<img width="200px" src="imgs/editor.png">
</td>
</tr>
</table>
@@ -67,7 +67,7 @@
<img width="200px" src="imgs/ping.png">
</td>
<td>
<img width="200px" src="imgs/sftp.jpeg">
<img width="200px" src="imgs/ssh.jpg">
</td>
<td>
<img width="200px" src="imgs/docker.jpeg">
@@ -83,11 +83,11 @@
状态|平台
--- | ---
完整支持 | Android / iOS / macOS
可能支持,未测试 | Windows / Linux
未测试 | Windows / Linux
## 🧱 贡献
**任何正面的贡献都欢迎**.
第一次参与贡献,会赠送 10 份 iOS App 兑换码。这是我唯一能送的。你可以来送给其他人。:)
**任何正面的贡献都欢迎**
第一次参与贡献,会赠送 10 份 iOS App 兑换码。如果没有 iOS 设备,你可以来送给其他人。:)
### l10n
1. Fork 本项目,并 Clone 你 Fork 的项目至你的电脑
@@ -95,10 +95,14 @@
- 文件名应该类似 `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.
5. Commit 变更到你 Fork 的 Repo
6. 在我的项目中发起 Pull Request
## 📝 License
1. 允许打包自用,但不允许分发举例你可以教别人如何打包避免花钱购买App但不能与他人分享你打包的App
2. 除去上诉情形:遵循 `GPLv3`
## 📝 开源协议
- 允许打包自用,但不允许分发
- 举例你可以教别人如何打包避免花钱购买App但不能与他人分享你打包的App
- 之所以这样做:
1. 安全性:可能会有有心之人植入后门并分发
2. 回血:苹果开发者 **99刀/年**,并且作为刚毕业的独立开发者,我需要收入
- 除去上述情形:遵循 `GPLv3`

View File

@@ -79,6 +79,7 @@ android {
applicationIdSuffix '.debug'
}
}
namespace 'tech.lolli.toolbox'
}
flutter {

View File

@@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="tech.lolli.toolbox">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -56,5 +55,7 @@
android:name="android.appwidget.provider"
android:resource="@xml/home_widget" />
</receiver>
<service android:name=".KeepAliveService"/>
</application>
</manifest>

View File

@@ -0,0 +1,18 @@
package tech.lolli.toolbox
import android.app.Service
import android.content.Intent
import android.os.IBinder
import org.jetbrains.annotations.Nullable
class KeepAliveService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
@Nullable
override fun onBind(intent: Intent?): IBinder? {
return null
}
}

View File

@@ -1,5 +1,6 @@
package tech.lolli.toolbox
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
@@ -11,11 +12,18 @@ class MainActivity : FlutterActivity() {
MethodChannel(binaryMessenger, "tech.lolli.toolbox/app_retain").apply {
setMethodCallHandler { method, result ->
if (method.method == "sendToBackground") {
moveTaskToBack(true)
result.success(null)
} else {
result.notImplemented()
when (method.method) {
"sendToBackground" -> {
moveTaskToBack(true)
result.success(null)
}
"startService" -> {
val intent = Intent(this@MainActivity, KeepAliveService::class.java)
startService(intent)
}
else -> {
result.notImplemented()
}
}
}
}

View File

@@ -15,6 +15,7 @@
android:textColor="@color/widgetText"
android:textSize="23sp"
android:textStyle="bold"
android:maxLines="1"
tools:text="Server Name" />
<RelativeLayout

View File

@@ -6,7 +6,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

BIN
imgs/detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 KiB

BIN
imgs/editor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

BIN
imgs/server.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

BIN
imgs/sftp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -60,7 +60,7 @@ SPEC CHECKSUMS:
file_picker: 1d63c4949e05e386da864365f8c13e1e64787675
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028

View File

@@ -470,7 +470,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 406;
CURRENT_PROJECT_VERSION = 491;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -478,7 +478,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.406;
MARKETING_VERSION = 1.0.491;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -602,7 +602,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 406;
CURRENT_PROJECT_VERSION = 491;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -610,7 +610,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.406;
MARKETING_VERSION = 1.0.491;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -628,7 +628,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 406;
CURRENT_PROJECT_VERSION = 491;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -636,7 +636,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.406;
MARKETING_VERSION = 1.0.491;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -657,7 +657,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 406;
CURRENT_PROJECT_VERSION = 491;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -670,7 +670,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.406;
MARKETING_VERSION = 1.0.491;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
@@ -696,7 +696,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 406;
CURRENT_PROJECT_VERSION = 491;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -709,7 +709,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.406;
MARKETING_VERSION = 1.0.491;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -732,7 +732,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 406;
CURRENT_PROJECT_VERSION = 491;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -745,7 +745,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.406;
MARKETING_VERSION = 1.0.491;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -28,8 +28,7 @@ class MyApp extends StatelessWidget {
// Issue #57
// if not [ok] -> [AMOLED] mode, use [ThemeMode.dark]
final themeMode = isAMOLED ? ThemeMode.values[tMode] : ThemeMode.dark;
final localeStr = _setting.locale.fetch();
final locale = localeStr?.toLocale;
final locale = _setting.locale.fetch()?.toLocale;
final darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
@@ -47,39 +46,25 @@ class MyApp extends StatelessWidget {
useMaterial3: true,
colorSchemeSeed: primaryColor,
),
darkTheme: isAMOLED
? darkTheme
: darkTheme.copyWith(
scaffoldBackgroundColor: Colors.black,
dialogBackgroundColor: Colors.black,
drawerTheme: const DrawerThemeData(
backgroundColor: Colors.black,
),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.black,
),
dialogTheme: const DialogTheme(
backgroundColor: Colors.black,
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.black,
),
listTileTheme: const ListTileThemeData(
tileColor: Colors.black12,
),
cardTheme: const CardTheme(
color: Colors.black12,
),
navigationBarTheme: const NavigationBarThemeData(
backgroundColor: Colors.black,
),
popupMenuTheme: const PopupMenuThemeData(
color: Colors.black,
),
),
darkTheme: isAMOLED ? darkTheme : _getAmoledTheme(darkTheme),
home: fullScreen ? const FullScreenPage() : const HomePage(),
);
},
);
}
}
ThemeData _getAmoledTheme(ThemeData darkTheme) => darkTheme.copyWith(
scaffoldBackgroundColor: Colors.black,
dialogBackgroundColor: Colors.black,
drawerTheme: const DrawerThemeData(backgroundColor: Colors.black),
appBarTheme: const AppBarTheme(backgroundColor: Colors.black),
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
bottomSheetTheme:
const BottomSheetThemeData(backgroundColor: Colors.black),
listTileTheme: const ListTileThemeData(tileColor: Colors.black12),
cardTheme: const CardTheme(color: Colors.black12),
navigationBarTheme:
const NavigationBarThemeData(backgroundColor: Colors.black),
popupMenuTheme: const PopupMenuThemeData(color: Colors.black),
);

View File

@@ -8,6 +8,13 @@ const _interactiveStates = <MaterialState>{
};
extension ColorX on Color {
String get toHex {
final redStr = red.toRadixString(16).padLeft(2, '0');
final greenStr = green.toRadixString(16).padLeft(2, '0');
final blueStr = blue.toRadixString(16).padLeft(2, '0');
return '#$redStr$greenStr$blueStr';
}
bool get isBrightColor {
return getBrightnessFromColor == Brightness.light;
}

View File

@@ -0,0 +1,5 @@
import 'package:flutter/widgets.dart';
extension MideaQueryX on MediaQueryData {
bool get useDoubleColumn => size.width > 639;
}

View File

@@ -4,6 +4,16 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
extension StringX on String {
/// Format: `#8b2252` or `8b2252`
Color? get hexToColor {
final hexCode = replaceAll('#', '');
final val = int.tryParse('FF$hexCode', radix: 16);
if (val == null) {
return null;
}
return Color(val);
}
int get i => int.parse(this);
Uri get uri {

View File

@@ -1,31 +0,0 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
class ProviderBase with ChangeNotifier {
void setState(void Function() callback) {
callback();
notifyListeners();
}
}
class BusyProvider extends ProviderBase {
bool _isBusy = false;
bool get isBusy => _isBusy;
setBusyState([bool isBusy = true]) {
_isBusy = isBusy;
notifyListeners();
}
FutureOr<T> busyRun<T>(FutureOr<T> Function() func) async {
setBusyState(true);
try {
return await Future.sync(func);
} catch (e) {
rethrow;
} finally {
setBusyState(false);
}
}
}

View File

@@ -1,5 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/analysis.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/server.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/backup.dart';
import 'package:toolbox/view/page/docker.dart';
import 'package:toolbox/view/page/home.dart';
import 'package:toolbox/view/page/ping.dart';
import 'package:toolbox/view/page/private_key/edit.dart';
import 'package:toolbox/view/page/private_key/list.dart';
import 'package:toolbox/view/page/server/detail.dart';
import 'package:toolbox/view/page/ssh_term.dart';
import 'package:toolbox/view/page/setting/virt_key.dart';
import 'package:toolbox/view/page/storage/local.dart';
import '../data/model/server/snippet.dart';
import '../view/page/convert.dart';
import '../view/page/debug.dart';
import '../view/page/editor.dart';
import '../view/page/full_screen.dart';
import '../view/page/pkg.dart';
import '../view/page/process.dart';
import '../view/page/server/edit.dart';
import '../view/page/server/tab.dart';
import '../view/page/setting/entry.dart';
import '../view/page/setting/srv_detail_seq.dart';
import '../view/page/setting/srv_seq.dart';
import '../view/page/snippet/edit.dart';
import '../view/page/snippet/list.dart';
import '../view/page/storage/sftp.dart';
import '../view/page/storage/sftp_mission.dart';
import 'utils/ui.dart';
class AppRoute {
final Widget page;
@@ -14,4 +47,148 @@ class AppRoute {
MaterialPageRoute(builder: (context) => page),
);
}
Future<T?> checkClientAndGo<T>({
required BuildContext context,
required S s,
required String id,
}) {
final server = locator<ServerProvider>().servers[id];
if (server == null || server.client == null) {
showSnackBar(context, Text(s.waitConnection));
return Future.value(null);
}
return go(context);
}
static AppRoute serverDetail({Key? key, required ServerPrivateInfo spi}) {
return AppRoute(ServerDetailPage(key: key, spi: spi), 'server_detail');
}
static AppRoute serverTab({Key? key}) {
return AppRoute(ServerPage(key: key), 'server_tab');
}
static AppRoute serverEdit({Key? key, ServerPrivateInfo? spi}) {
return AppRoute(
ServerEditPage(spi: spi),
'server_${spi == null ? 'add' : 'edit'}',
);
}
static AppRoute keyEdit({Key? key, PrivateKeyInfo? pki}) {
return AppRoute(
PrivateKeyEditPage(pki: pki),
'key_${pki == null ? 'add' : 'edit'}',
);
}
static AppRoute keyList({Key? key}) {
return AppRoute(PrivateKeysListPage(key: key), 'key_detail');
}
static AppRoute snippetEdit({Key? key, Snippet? snippet}) {
return AppRoute(
SnippetEditPage(snippet: snippet),
'snippet_${snippet == null ? 'add' : 'edit'}',
);
}
static AppRoute snippetList({Key? key}) {
return AppRoute(SnippetListPage(key: key), 'snippet_detail');
}
static AppRoute ssh({
Key? key,
required ServerPrivateInfo spi,
String? initCmd,
}) {
return AppRoute(
SSHPage(
key: key,
spi: spi,
initCmd: initCmd,
),
'ssh_term',
);
}
static AppRoute sshVirtKeySetting({Key? key}) {
return AppRoute(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
}
static AppRoute localStorage({Key? key}) {
return AppRoute(LocalStoragePage(key: key), 'local_storage');
}
static AppRoute sftpMission({Key? key}) {
return AppRoute(SftpMissionPage(key: key), 'sftp_mission');
}
static AppRoute sftp(
{Key? key,
required ServerPrivateInfo spi,
String? initPath,
bool isSelect = false}) {
return AppRoute(
SftpPage(
key: key,
spi: spi,
initPath: initPath,
selectPath: isSelect,
),
'sftp');
}
static AppRoute backup({Key? key}) {
return AppRoute(BackupPage(key: key), 'backup');
}
static AppRoute convert({Key? key}) {
return AppRoute(ConvertPage(key: key), 'convert');
}
static AppRoute debug({Key? key}) {
return AppRoute(DebugPage(key: key), 'debug');
}
static AppRoute docker({Key? key, required ServerPrivateInfo spi}) {
return AppRoute(DockerManagePage(key: key, spi: spi), 'docker');
}
static AppRoute editor({Key? key, required String path}) {
return AppRoute(EditorPage(key: key, path: path), 'editor');
}
static AppRoute fullscreen({Key? key}) {
return AppRoute(FullScreenPage(key: key), 'fullscreen');
}
static AppRoute home({Key? key}) {
return AppRoute(HomePage(key: key), 'home');
}
static AppRoute ping({Key? key}) {
return AppRoute(PingPage(key: key), 'ping');
}
static AppRoute pkg({Key? key, required ServerPrivateInfo spi}) {
return AppRoute(PkgPage(key: key, spi: spi), 'pkg');
}
static AppRoute process({Key? key, required ServerPrivateInfo spi}) {
return AppRoute(ProcessPage(key: key, spi: spi), 'process');
}
static AppRoute setting({Key? key}) {
return AppRoute(SettingPage(key: key), 'setting');
}
static AppRoute serverOrder({Key? key}) {
return AppRoute(ServerOrderPage(key: key), 'server_order');
}
static AppRoute serverDetailOrder({Key? key}) {
return AppRoute(ServerDetailOrderPage(key: key), 'server_detail_order');
}
}

View File

@@ -6,8 +6,10 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:logging/logging.dart';
import 'package:r_upgrade/r_upgrade.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/utils/misc.dart';
import 'package:toolbox/core/utils/misc.dart' hide pathJoin;
import 'package:toolbox/data/model/app/update.dart';
import 'package:toolbox/data/res/path.dart';
import 'package:toolbox/data/res/ui.dart';
import '../data/provider/app.dart';
import '../data/res/build_data.dart';
@@ -29,7 +31,7 @@ Future<bool> isFileAvailable(String url) async {
}
Future<void> doUpdate(BuildContext context, {bool force = false}) async {
_rmDownloadApks();
await _rmDownloadApks();
final update = await locator<AppService>().getUpdate();
@@ -69,7 +71,7 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
child: Text(s.updateTipTooLow(newest)),
actions: [
TextButton(
onPressed: () => _doUpdate(url, context, s),
onPressed: () => _doUpdate(update, context, s),
child: Text(s.ok),
)
],
@@ -81,17 +83,58 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
context,
'${s.updateTip(newest)} \n${update.changelog.current}',
s.update,
() => _doUpdate(url, context, s),
() => _doUpdate(update, context, s),
);
}
Future<void> _doUpdate(String url, BuildContext context, S s) async {
Future<void> _doUpdate(AppUpdate update, BuildContext context, S s) async {
if (isAndroid) {
await RUpgrade.upgrade(
final url = update.url.current;
if (url == null) return;
final fileName = url.split('/').last;
final id = await RUpgrade.upgrade(
url,
fileName: url.split('/').last,
isAutoRequestInstall: true,
fileName: fileName,
isAutoRequestInstall: false,
);
RUpgrade.stream.listen((event) async {
if (event.status?.value == 3) {
if (id == null) {
showSnackBar(context, const Text('install id is null'));
return;
}
final sha256 = () {
try {
return fileName.split('.').first;
} catch (e) {
_logger.warning('sha256 parse failed: $e');
return null;
}
}();
final dlPath = pathJoin(await _dlDir, fileName);
final computed = await getFileSha256(dlPath);
if (computed != sha256) {
_logger.info('Mismatch sha256: $computed, $sha256');
final resume = await showRoundDialog(
context: context,
title: Text(s.attention),
child: const Text('sha256 is null'),
actions: [
TextButton(
onPressed: () => context.pop(false),
child: Text(s.cancel),
),
TextButton(
onPressed: () => context.pop(true),
child: Text(s.ok, style: textRed),
),
],
);
if (!resume) return;
}
RUpgrade.install(id);
}
});
} else if (isIOS) {
await RUpgrade.upgradeFromAppStore('1586449703');
} else {
@@ -111,8 +154,10 @@ Future<void> _doUpdate(String url, BuildContext context, S s) async {
// rmdir Download
Future<void> _rmDownloadApks() async {
if (!isAndroid) return;
final dlDir = Directory(pathJoin((await docDir).path, 'Download'));
final dlDir = Directory(await _dlDir);
if (await dlDir.exists()) {
await dlDir.delete(recursive: true);
}
}
Future<String> get _dlDir async => pathJoin((await docDir).path, 'Download');

View File

@@ -1,5 +1,5 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@@ -74,6 +74,16 @@ String getTime(int? unixMill) {
.replaceFirst('.000', '');
}
/// Join two path with `/`
String pathJoin(String path1, String path2) {
return path1 + (path1.endsWith('/') ? '' : '/') + path2;
}
Future<String?> getFileSha256(String path) async {
final file = File(path);
if (!(await file.exists())) {
return null;
}
final digest = await sha256.bind(file.openRead()).first;
return digest.toString();
}

View File

@@ -9,7 +9,8 @@ enum PlatformType {
macos,
windows,
web,
unknown,
fuchsia,
unknown;
}
final _p = () {
@@ -31,10 +32,21 @@ final _p = () {
if (Platform.isWindows) {
return PlatformType.windows;
}
if (Platform.isFuchsia) {
return PlatformType.fuchsia;
}
return PlatformType.unknown;
}();
final _pathSep = () {
if (Platform.isWindows) {
return '\\';
}
return '/';
}();
PlatformType get platform => _p;
String get pathSeparator => _pathSep;
bool get isAndroid => _p == PlatformType.android;
bool get isIOS => _p == PlatformType.ios;
@@ -47,3 +59,23 @@ bool get isDesktop =>
_p == PlatformType.linux ||
_p == PlatformType.macos ||
_p == PlatformType.windows;
/// Available only on desktop,
/// return null on mobile
String? getHomeDir() {
final envVars = Platform.environment;
if (isMacOS || isLinux) {
return envVars['HOME'];
} else if (isWindows) {
return envVars['UserProfile'];
}
return null;
}
/// Join two paths with platform specific separator
String pathJoin(String path1, String path2) {
if (isWindows) {
return path1 + (path1.endsWith('\\') ? '' : '\\') + path2;
}
return path1 + (path1.endsWith('/') ? '' : '/') + path2;
}

View File

@@ -32,14 +32,14 @@ enum GenSSHClientStatus {
}
String getPrivateKey(String id) {
final key = locator<PrivateKeyStore>().get(id);
if (key == null) {
final pki = locator<PrivateKeyStore>().get(id);
if (pki == null) {
throw SSHErr(
type: SSHErrType.noPrivateKey,
message: 'key [$id] not found',
);
}
return key.privateKey;
return pki.key;
}
Future<SSHClient> genClient(
@@ -56,11 +56,12 @@ Future<SSHClient> genClient(
timeout: const Duration(seconds: 5),
);
} catch (e) {
if (spi.alterUrl == null) rethrow;
try {
spi.fromStringUrl();
final ipPort = spi.fromStringUrl();
socket = await SSHSocket.connect(
spi.ip,
spi.port,
ipPort.ip,
ipPort.port,
timeout: const Duration(seconds: 5),
);
} catch (e) {

View File

@@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../data/model/server/snippet.dart';
import '../../data/provider/snippet.dart';
import '../../data/res/ui.dart';
import '../../locator.dart';
import '../../view/page/snippet/edit.dart';
import '../../view/widget/picker.dart';
@@ -69,6 +70,14 @@ Future<T?> showRoundDialog<T>({
);
}
void showLoadingDialog(BuildContext context, {bool barrierDismiss = false}) {
showRoundDialog(
context: context,
child: centerSizedLoading,
barrierDismiss: barrierDismiss,
);
}
Widget buildSwitch(
BuildContext context,
StoreProperty<bool> prop, {

View File

@@ -38,6 +38,9 @@ enum DockerErrType {
invalidVersion,
cmdNoPrefix,
segmentsNotMatch,
parsePsItem,
parseImages,
parseStats,
}
class DockerErr extends Err<DockerErrType> {

View File

@@ -0,0 +1,5 @@
typedef GhId = String;
extension GhIdX on GhId {
String get url => 'https://github.com/$this ';
}

View File

@@ -2,12 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum ServerTabMenuType {
terminal,
sftp,
snippet,
pkg,
docker,
process,
edit;
pkg,
snippet,
;
IconData get icon {
switch (this) {
@@ -19,31 +20,12 @@ enum ServerTabMenuType {
return Icons.system_security_update;
case ServerTabMenuType.docker:
return Icons.view_agenda;
case ServerTabMenuType.edit:
return Icons.edit;
case ServerTabMenuType.process:
return Icons.list_alt_outlined;
case ServerTabMenuType.terminal:
return Icons.terminal;
}
}
String text(S s) {
switch (this) {
case ServerTabMenuType.sftp:
return 'SFTP';
case ServerTabMenuType.snippet:
return s.snippet;
case ServerTabMenuType.pkg:
return s.pkg;
case ServerTabMenuType.docker:
return 'Docker';
case ServerTabMenuType.edit:
return s.edit;
case ServerTabMenuType.process:
return s.process;
}
}
PopupMenuItem<ServerTabMenuType> build(S s) => _build(this, icon, text(s));
}
enum DockerMenuType {
@@ -52,11 +34,12 @@ enum DockerMenuType {
restart,
rm,
logs,
terminal;
terminal,
stats;
static List<DockerMenuType> items(bool running) {
if (running) {
return [stop, restart, rm, logs, terminal];
return [stop, restart, rm, logs, terminal, stats];
} else {
return [start, rm, logs];
}
@@ -76,6 +59,8 @@ enum DockerMenuType {
return Icons.logo_dev;
case DockerMenuType.terminal:
return Icons.terminal;
case DockerMenuType.stats:
return Icons.bar_chart;
}
}
@@ -93,6 +78,8 @@ enum DockerMenuType {
return s.log;
case DockerMenuType.terminal:
return s.terminal;
case DockerMenuType.stats:
return s.stats;
}
}

View File

@@ -1,4 +1,4 @@
import 'package:toolbox/core/utils/misc.dart';
import '../../../core/utils/platform.dart';
class PathWithPrefix {
final String _prefixPath;

View File

@@ -1,22 +1,54 @@
import '../../res/server_cmd.dart';
class AppShellFunc {
final String name;
final String cmd;
final String flag;
const _cmdDivider = '\necho $seperator\n';
const AppShellFunc(this.name, this.cmd, this.flag);
enum AppShellFuncType {
status,
docker;
String get flag {
switch (this) {
case AppShellFuncType.status:
return 's';
case AppShellFuncType.docker:
return 'd';
}
}
String get exec => 'sh $shellPath -$flag';
}
typedef AppShellFuncs = List<AppShellFunc>;
String get name {
switch (this) {
case AppShellFuncType.status:
return 'status';
case AppShellFuncType.docker:
// `dockeR` -> avoid conflict with `docker` command
// 以防止循环递归
return 'dockeR';
}
}
extension AppShellFuncsExt on AppShellFuncs {
String get generate {
String get cmd {
switch (this) {
case AppShellFuncType.status:
return statusCmds.join(_cmdDivider);
case AppShellFuncType.docker:
return '''
result=\$(docker version 2>&1)
deniedStr="permission denied"
containStr=\$(echo \$result | grep "\${deniedStr}")
if [[ \$containStr != "" ]]; then
${dockerCmds.join(_cmdDivider)}
else
${dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)}
fi''';
}
}
static String get shellScript {
final sb = StringBuffer();
// Write each func
for (final func in this) {
for (final func in values) {
sb.write('''
${func.name}() {
${func.cmd}
@@ -27,7 +59,7 @@ ${func.cmd}
// Write switch case
sb.write('case \$1 in\n');
for (final func in this) {
for (final func in values) {
sb.write('''
'-${func.flag}')
${func.name}
@@ -38,13 +70,36 @@ ${func.cmd}
*)
echo "Invalid argument \$1"
;;
esac
''');
esac''');
return sb.toString();
}
}
// enum AppShellFuncType {
// status,
// docker;
// }
extension EnumX on Enum {
/// Find out the required segment from [segments]
String find(List<String> segments) {
return segments[index];
}
}
enum StatusCmdType {
time,
net,
sys,
cpu,
uptime,
conn,
disk,
mem,
tempType,
tempVal,
host,
sysRhel;
}
enum DockerCmdType {
version,
ps,
stats,
images;
}

View File

@@ -47,14 +47,14 @@ class Cpus extends TimeSeq<OneTimeCpuStatus> {
}
class OneTimeCpuStatus extends TimeSeqIface<OneTimeCpuStatus> {
late String id;
late int user;
late int sys;
late int nice;
late int idle;
late int iowait;
late int irq;
late int softirq;
final String id;
final int user;
final int sys;
final int nice;
final int idle;
final int iowait;
final int irq;
final int softirq;
OneTimeCpuStatus(
this.id,
@@ -80,7 +80,8 @@ List<OneTimeCpuStatus> parseCPU(String raw) {
if (item == '') break;
final id = item.split(' ').first;
final matches = item.replaceFirst(id, '').trim().split(' ');
cpus.add(OneTimeCpuStatus(
cpus.add(
OneTimeCpuStatus(
id,
int.parse(matches[0]),
int.parse(matches[1]),
@@ -88,7 +89,9 @@ List<OneTimeCpuStatus> parseCPU(String raw) {
int.parse(matches[3]),
int.parse(matches[4]),
int.parse(matches[5]),
int.parse(matches[6])));
int.parse(matches[6]),
),
);
}
return cpus;
}

View File

@@ -36,14 +36,41 @@ List<Disk> parseDisk(String raw) {
vals[0] = pathCache;
pathCache = '';
}
list.add(Disk(
path: vals[0],
loc: vals[5],
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
used: vals[2],
size: vals[1],
avail: vals[3],
));
try {
list.add(Disk(
path: vals[0],
loc: vals[5],
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
used: vals[2],
size: vals[1],
avail: vals[3],
));
} catch (e) {
continue;
}
}
return list;
}
/// Issue 88
///
/// Due to performance issues,
/// if there is no `Disk.loc == '/' || Disk.loc == '/sysroot'`,
/// return the first [Disk] of [disks].
///
/// If we find out the biggest [Disk] of [disks],
/// the fps may lower than 60.
Disk? findRootDisk(List<Disk> disks) {
if (disks.isEmpty) return null;
final roots = disks.where((element) => element.loc == '/');
if (roots.isEmpty) {
final sysRoots = disks.where((element) => element.loc == '/sysroot');
if (sysRoots.isEmpty) {
return disks.first;
} else {
return sysRoots.first;
}
} else {
return roots.first;
}
}

View File

@@ -3,10 +3,11 @@ import 'package:toolbox/core/extension/numx.dart';
import 'time_seq.dart';
class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
String device;
BigInt bytesIn;
BigInt bytesOut;
BigInt time;
final String device;
final BigInt bytesIn;
final BigInt bytesOut;
final int time;
NetSpeedPart(this.device, this.bytesIn, this.bytesOut, this.time);
@override
@@ -18,7 +19,7 @@ class NetSpeed extends TimeSeq<NetSpeedPart> {
List<String> get devices => now.map((e) => e.device).toList();
BigInt get _timeDiff => now[0].time - pre[0].time;
BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time);
double _speedIn(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;
double _speedOut(int i) => (now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
@@ -96,14 +97,12 @@ class NetSpeed extends TimeSeq<NetSpeedPart> {
/// 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) {
List<NetSpeedPart> parseNetSpeed(String raw, int time) {
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(':');

View File

@@ -5,27 +5,23 @@ part 'private_key_info.g.dart';
@HiveType(typeId: 1)
class PrivateKeyInfo {
@HiveField(0)
late String id;
final String id;
@HiveField(1)
late String privateKey;
@HiveField(2)
late String password;
final String key;
PrivateKeyInfo({
required this.id,
required this.key,
});
PrivateKeyInfo.fromJson(Map<String, dynamic> json)
: id = json["id"].toString(),
key = json["private_key"].toString();
PrivateKeyInfo(
this.id,
this.privateKey,
this.password,
);
PrivateKeyInfo.fromJson(Map<String, dynamic> json) {
id = json["id"].toString();
privateKey = json["private_key"].toString();
password = json["password"].toString();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
final data = <String, String>{};
data["id"] = id;
data["private_key"] = privateKey;
data["password"] = password;
data["private_key"] = key;
return data;
}
}

View File

@@ -17,22 +17,19 @@ class PrivateKeyInfoAdapter extends TypeAdapter<PrivateKeyInfo> {
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return PrivateKeyInfo(
fields[0] as String,
fields[1] as String,
fields[2] as String,
id: fields[0] as String,
key: fields[1] as String,
);
}
@override
void write(BinaryWriter writer, PrivateKeyInfo obj) {
writer
..writeByte(3)
..writeByte(2)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.privateKey)
..writeByte(2)
..write(obj.password);
..write(obj.key);
}
@override

View File

@@ -12,11 +12,22 @@ class Server {
}
enum ServerState {
failed,
disconnected,
connecting,
connected,
failed;
bool get shouldConnect =>
this == ServerState.disconnected || this == ServerState.failed;
/// Connected to server
connected,
/// Status parsing
loading,
/// Status parsing finished
finished;
bool get shouldConnect => this < ServerState.connecting;
bool get canViewDetails => this == ServerState.finished;
operator <(ServerState other) => index < other.index;
}

View File

@@ -68,7 +68,7 @@ class ServerPrivateInfo {
alterUrl != old.alterUrl;
}
void fromStringUrl() {
_IpPort fromStringUrl() {
if (alterUrl == null) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
}
@@ -76,20 +76,16 @@ class ServerPrivateInfo {
if (splited.length != 2) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no @');
}
user = splited[0];
final splited2 = splited[1].split(':');
if (splited2.length != 2) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no :');
}
ip = splited2[0];
port = int.tryParse(splited2[1]) ?? 22;
final ip_ = splited2[0];
final port_ = int.tryParse(splited2[1]) ?? 22;
if (port <= 0 || port > 65535) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');
}
// Do not update [id]
// Because [id] is the identity which is used to find the [SSHClient]
// id = '$user@$ip:$port';
return _IpPort(ip_, port_);
}
@override
@@ -97,3 +93,10 @@ class ServerPrivateInfo {
return id;
}
}
class _IpPort {
final String ip;
final int port;
_IpPort(this.ip, this.port);
}

View File

@@ -1,4 +1,4 @@
import '../../res/server_cmd.dart';
import '../app/shell_func.dart';
import 'cpu.dart';
import 'disk.dart';
import 'memory.dart';
@@ -13,50 +13,46 @@ class ServerStatusUpdateReq {
const ServerStatusUpdateReq(this.ss, this.segments);
}
extension _SegmentsExt on List<String> {
String at(CmdType t) {
final index = t.index;
if (index >= length) return '';
return this[index];
}
}
Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
final net = parseNetSpeed(req.segments.at(CmdType.net));
final segments = req.segments;
final time = int.parse(StatusCmdType.time.find(segments));
final net = parseNetSpeed(StatusCmdType.net.find(segments), time);
req.ss.netSpeed.update(net);
final sys = _parseSysVer(
req.segments.at(CmdType.sys),
req.segments.at(CmdType.host),
req.segments.at(CmdType.sysRhel),
StatusCmdType.sys.find(segments),
StatusCmdType.host.find(segments),
StatusCmdType.sysRhel.find(segments),
);
if (sys != null) {
req.ss.sysVer = sys;
}
final cpus = parseCPU(req.segments.at(CmdType.cpu));
final cpus = parseCPU(StatusCmdType.cpu.find(segments));
req.ss.cpu.update(cpus);
req.ss.temps.parse(
req.segments.at(CmdType.tempType),
req.segments.at(CmdType.tempVal),
StatusCmdType.tempType.find(segments),
StatusCmdType.tempVal.find(segments),
);
final tcp = parseConn(req.segments.at(CmdType.conn));
final tcp = parseConn(StatusCmdType.conn.find(segments));
if (tcp != null) {
req.ss.tcp = tcp;
}
req.ss.disk = parseDisk(req.segments.at(CmdType.disk));
req.ss.disk = parseDisk(StatusCmdType.disk.find(segments));
req.ss.mem = parseMem(req.segments.at(CmdType.mem));
req.ss.mem = parseMem(StatusCmdType.mem.find(segments));
final uptime = _parseUpTime(req.segments.at(CmdType.uptime));
final uptime = _parseUpTime(StatusCmdType.uptime.find(segments));
if (uptime != null) {
req.ss.uptime = uptime;
}
req.ss.swap = parseSwap(req.segments.at(CmdType.mem));
req.ss.swap = parseSwap(StatusCmdType.mem.find(segments));
return req.ss;
}
@@ -74,13 +70,16 @@ String? _parseUpTime(String raw) {
}
String? _parseSysVer(String raw, String hostname, String rawRhel) {
if (!rawRhel.contains('No such file')) {
return rawRhel;
try {
final s = raw.split('=');
if (s.length == 2) {
return s[1].replaceAll('"', '').replaceFirst('\n', '');
}
} catch (e) {
if (!rawRhel.contains('cat: /etc/redhat-release:')) {
return rawRhel;
}
if (hostname.isNotEmpty) return hostname;
}
final s = raw.split('=');
if (s.length == 2) {
return s[1].replaceAll('"', '').replaceFirst('\n', '');
}
if (hostname.isNotEmpty) return hostname;
return null;
}

View File

@@ -4,7 +4,7 @@ import '../../store/setting.dart';
class TryLimiter {
final Map<String, int> _triedTimes = {};
bool shouldTry(String id) {
bool canTry(String id) {
final maxCount = locator<SettingStore>().maxRetryCount.fetch()!;
if (maxCount <= 0) {
return true;
@@ -13,10 +13,13 @@ class TryLimiter {
if (times >= maxCount) {
return false;
}
_triedTimes[id] = times + 1;
return true;
}
void inc(String sid) {
_triedTimes[sid] = (_triedTimes[sid] ?? 0) + 1;
}
void reset(String id) {
_triedTimes[id] = 0;
}

View File

@@ -1,6 +1,6 @@
import 'package:toolbox/core/provider_base.dart';
import 'package:flutter/material.dart';
class AppProvider extends BusyProvider {
class AppProvider extends ChangeNotifier {
int? _newestBuild;
int? get newestBuild => _newestBuild;

View File

@@ -2,10 +2,11 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:toolbox/core/extension/ssh_client.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/app/shell_func.dart';
import 'package:toolbox/data/model/docker/image.dart';
import 'package:toolbox/data/model/docker/ps.dart';
import 'package:toolbox/data/model/app/error.dart';
@@ -15,13 +16,14 @@ import 'package:toolbox/locator.dart';
final _dockerNotFound = RegExp(r'command not found|Unknown command');
final _versionReg = RegExp(r'(Version:)\s+([0-9]+\.[0-9]+\.[0-9]+)');
final _editionReg = RegExp(r'(Client:)\s+(.+-.+)');
// eg: `Docker Engine - Community`
final _editionReg = RegExp(r'Docker Engine - [a-zA-Z]+');
final _dockerPrefixReg = RegExp(r'(sudo )?docker ');
final _logger = Logger('DOCKER');
class DockerProvider extends BusyProvider {
final dockerStore = locator<DockerStore>();
class DockerProvider extends ChangeNotifier {
final _dockerStore = locator<DockerStore>();
SSHClient? client;
String? userName;
@@ -35,8 +37,12 @@ class DockerProvider extends BusyProvider {
String? runLog;
bool isRequestingPwd = false;
void init(SSHClient client, String userName, PwdRequestFunc onPwdReq,
String hostId) {
void init(
SSHClient client,
String userName,
PwdRequestFunc onPwdReq,
String hostId,
) {
this.client = client;
this.userName = userName;
this.onPwdReq = onPwdReq;
@@ -50,19 +56,17 @@ class DockerProvider extends BusyProvider {
}
Future<void> refresh() async {
if (isBusy) return;
setBusyState();
var raw = '';
await client!.exec(
shellFuncDocker.exec,
AppShellFuncType.docker.exec,
onStderr: _onPwd,
onStdout: (data, _) => raw = '$raw$data',
);
if (raw.contains(_dockerNotFound)) {
error = DockerErr(type: DockerErrType.notInstalled);
setBusyState(false);
_logger.warning('Docker not installed: $raw');
notifyListeners();
return;
}
@@ -70,65 +74,72 @@ class DockerProvider extends BusyProvider {
final segments = raw.split(seperator);
if (segments.length != dockerCmds.length) {
error = DockerErr(type: DockerErrType.segmentsNotMatch);
setBusyState(false);
_logger.warning('Docker segments not match: ${segments.length}');
notifyListeners();
return;
}
// Parse docker version
final verRaw = segments[0];
try {
version = _versionReg.firstMatch(verRaw)?.group(2);
edition = _editionReg.firstMatch(verRaw)?.group(2);
} catch (e) {
error = DockerErr(type: DockerErrType.unknown, message: e.toString());
rethrow;
}
final verRaw = DockerCmdType.version.find(segments);
version = _versionReg.firstMatch(verRaw)?.group(2);
edition = _editionReg.firstMatch(verRaw)?.group(0);
// Parse docker ps
final psRaw = segments[1];
final psRaw = DockerCmdType.ps.find(segments);
try {
final lines = psRaw.split('\n');
lines.removeWhere((element) => element.isEmpty);
lines.removeAt(0);
if (lines.isNotEmpty) lines.removeAt(0);
items = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
} catch (e) {
error = DockerErr(type: DockerErrType.unknown, message: e.toString());
rethrow;
error = DockerErr(
type: DockerErrType.parsePsItem,
message: '$psRaw\n-\n$e',
);
_logger.warning('Parse docker ps: $psRaw', e);
} finally {
setBusyState(false);
notifyListeners();
}
// Parse docker images
final imageRaw = segments[3];
final imageRaw = DockerCmdType.images.find(segments);
try {
final imageLines = imageRaw.split('\n');
imageLines.removeWhere((element) => element.isEmpty);
imageLines.removeAt(0);
if (imageLines.isNotEmpty) imageLines.removeAt(0);
images = imageLines.map((e) => DockerImage.fromRawStr(e)).toList();
} catch (e) {
error = DockerErr(type: DockerErrType.unknown, message: e.toString());
rethrow;
} catch (e, trace) {
error = DockerErr(
type: DockerErrType.parseImages,
message: '$imageRaw\n-\n$e',
);
_logger.warning('Parse docker images: $imageRaw', e, trace);
} finally {
setBusyState(false);
notifyListeners();
}
// Parse docker stats
final statsRaw = segments[2];
final statsRaw = DockerCmdType.stats.find(segments);
try {
final statsLines = statsRaw.split('\n');
statsLines.removeWhere((element) => element.isEmpty);
statsLines.removeAt(0);
if (statsLines.isNotEmpty) statsLines.removeAt(0);
for (var item in items!) {
final statsLine = statsLines.firstWhere(
(element) => element.contains(item.containerId),
orElse: () => '',
);
if (statsLine.isEmpty) continue;
item.parseStats(statsLine);
}
} catch (e) {
error = DockerErr(type: DockerErrType.unknown, message: e.toString());
} catch (e, trace) {
error = DockerErr(
type: DockerErrType.parseStats,
message: '$statsRaw\n-\n$e',
);
_logger.warning('Parse docker stats: $statsRaw', e, trace);
} finally {
setBusyState(false);
notifyListeners();
}
}
@@ -160,7 +171,6 @@ class DockerProvider extends BusyProvider {
if (!cmd.startsWith(_dockerPrefixReg)) {
return DockerErr(type: DockerErrType.cmdNoPrefix);
}
setBusyState();
runLog = '';
final errs = <String>[];
@@ -176,22 +186,21 @@ class DockerProvider extends BusyProvider {
},
);
runLog = null;
notifyListeners();
if (code != 0) {
setBusyState(false);
return DockerErr(
type: DockerErrType.unknown,
message: errs.join('\n').trim(),
);
}
await refresh();
setBusyState(false);
return null;
}
// judge whether to use DOCKER_HOST
String _wrap(String cmd) {
final dockerHost = dockerStore.getDockerHost(hostId!);
final dockerHost = _dockerStore.fetch(hostId!);
if (dockerHost == null || dockerHost.isEmpty) {
return cmd.withLangExport;
}

View File

@@ -2,16 +2,16 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:toolbox/core/extension/ssh_client.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/pkg/manager.dart';
import 'package:toolbox/data/model/pkg/upgrade_info.dart';
import 'package:toolbox/data/model/server/dist.dart';
class PkgProvider extends BusyProvider {
class PkgProvider extends ChangeNotifier {
final logger = Logger('PKG');
SSHClient? client;

View File

@@ -1,32 +1,36 @@
import 'package:toolbox/core/provider_base.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/locator.dart';
class PrivateKeyProvider extends BusyProvider {
List<PrivateKeyInfo> get infos => _infos;
class PrivateKeyProvider extends ChangeNotifier {
List<PrivateKeyInfo> get pkis => _pkis;
final _store = locator<PrivateKeyStore>();
late List<PrivateKeyInfo> _infos;
late List<PrivateKeyInfo> _pkis;
void loadData() {
_infos = _store.fetch();
_pkis = _store.fetch();
}
void addInfo(PrivateKeyInfo info) {
_infos.add(info);
void add(PrivateKeyInfo info) {
_pkis.add(info);
_store.put(info);
notifyListeners();
}
void delInfo(PrivateKeyInfo info) {
_infos.removeWhere((e) => e.id == info.id);
void delete(PrivateKeyInfo info) {
_pkis.removeWhere((e) => e.id == info.id);
_store.delete(info);
notifyListeners();
}
void updateInfo(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
final idx = _infos.indexWhere((e) => e.id == old.id);
_infos[idx] = newInfo;
void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
final idx = _pkis.indexWhere((e) => e.id == old.id);
if (idx == -1) {
_pkis.add(newInfo);
} else {
_pkis[idx] = newInfo;
}
_store.put(newInfo);
notifyListeners();
}

View File

@@ -2,10 +2,10 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:toolbox/data/model/app/shell_func.dart';
import '../../core/extension/order.dart';
import '../../core/extension/uint8list.dart';
import '../../core/provider_base.dart';
import '../../core/utils/server.dart';
import '../../locator.dart';
import '../model/server/server.dart';
@@ -20,7 +20,7 @@ import '../store/setting.dart';
typedef ServersMap = Map<String, Server>;
class ServerProvider extends BusyProvider {
class ServerProvider extends ChangeNotifier {
final ServersMap _servers = {};
ServersMap get servers => _servers;
final Order<String> _serverOrder = [];
@@ -38,7 +38,6 @@ class ServerProvider extends BusyProvider {
final _settingStore = locator<SettingStore>();
Future<void> loadLocalData() async {
setBusyState(true);
final spis = _serverStore.fetch();
for (final spi in spis) {
_servers[spi.id] = genServer(spi);
@@ -55,7 +54,6 @@ class ServerProvider extends BusyProvider {
}
_settingStore.serverOrder.put(_serverOrder);
_updateTags();
setBusyState(false);
notifyListeners();
}
@@ -203,77 +201,98 @@ class ServerProvider extends BusyProvider {
}
}
void _setServerState(Server s, ServerState ss) {
s.state = ss;
notifyListeners();
}
Future<void> _getData(ServerPrivateInfo spi) async {
final sid = spi.id;
final s = _servers[sid];
if (s == null) return;
var raw = '';
var segments = <String>[];
try {
final state = s.state;
if (state.shouldConnect) {
if (!_limiter.shouldTry(sid)) {
s.state = ServerState.failed;
notifyListeners();
return;
}
s.state = ServerState.connecting;
notifyListeners();
// try to connect
final time1 = DateTime.now();
s.client = await genClient(spi);
final time2 = DateTime.now();
final spentTime = time2.difference(time1).inMilliseconds;
_logger.info('Connected to $sid in $spentTime ms.');
// after connected
s.state = ServerState.connected;
notifyListeners();
// write script to server
final writeResult = await s.client!.run(installShellCmd).string;
// if write failed
if (writeResult.isNotEmpty) {
throw Exception(writeResult);
}
// reset try times if connected successfully
_limiter.reset(sid);
if (!_limiter.canTry(sid)) {
if (s.state != ServerState.failed) {
_setServerState(s, ServerState.failed);
}
return;
}
if (s.client == null) return;
// run script to get server status
raw = await s.client!.run(shellFuncStatus.exec).string;
segments = raw.split(seperator).map((e) => e.trim()).toList();
if (raw.isEmpty || segments.length != CmdType.values.length) {
s.state = ServerState.failed;
if (s.status.failedInfo?.isEmpty ?? true) {
s.status.failedInfo = 'Seperate segments failed, raw:\n$raw';
}
if (s.state.shouldConnect || (s.client?.isClosed ?? true)) {
_setServerState(s, ServerState.connecting);
final time1 = DateTime.now();
try {
s.client = await genClient(spi);
} catch (e) {
_limiter.inc(sid);
s.status.failedInfo = e.toString();
_setServerState(s, ServerState.failed);
_logger.warning('Connect to $sid failed', e);
return;
}
} catch (e) {
s.state = ServerState.failed;
s.status.failedInfo = e.toString();
rethrow;
} finally {
notifyListeners();
final time2 = DateTime.now();
final spentTime = time2.difference(time1).inMilliseconds;
_logger.info('Connected to $sid in $spentTime ms.');
_setServerState(s, ServerState.connected);
try {
final writeResult = await s.client?.run(installShellCmd).string;
if (writeResult == null || writeResult.isNotEmpty) {
_limiter.inc(sid);
s.status.failedInfo = writeResult;
_setServerState(s, ServerState.failed);
return;
}
} catch (e) {
_limiter.inc(sid);
s.status.failedInfo = e.toString();
_setServerState(s, ServerState.failed);
_logger.warning('Write script to $sid failed', e);
return;
}
}
if (s.client == null) return;
if (s.state != ServerState.finished) {
_setServerState(s, ServerState.loading);
}
final raw = await s.client?.run(AppShellFuncType.status.exec).string;
final segments = raw?.split(seperator).map((e) => e.trim()).toList();
if (raw == null ||
raw.isEmpty ||
segments == null ||
segments.length != StatusCmdType.values.length) {
_limiter.inc(sid);
s.status.failedInfo = 'Seperate segments failed, raw:\n$raw';
_setServerState(s, ServerState.failed);
return;
}
try {
final req = ServerStatusUpdateReq(s.status, segments);
s.status = await compute(getStatus, req);
// Comment for debug
// s.status = await getStatus(req);
} catch (e) {
s.state = ServerState.failed;
} catch (e, trace) {
_limiter.inc(sid);
s.status.failedInfo = 'Parse failed: $e\n\n$raw';
rethrow;
} finally {
_setServerState(s, ServerState.failed);
_logger.warning('Parse failed', e, trace);
return;
}
if (s.state != ServerState.finished) {
_setServerState(s, ServerState.finished);
} else {
notifyListeners();
}
// reset try times only after prepared successfully
_limiter.reset(sid);
}
Future<String?> runSnippets(String id, List<Snippet> snippets) async {

View File

@@ -1,10 +1,10 @@
import 'dart:async';
import 'package:toolbox/core/provider_base.dart';
import 'package:flutter/material.dart';
import '../model/sftp/req.dart';
class SftpProvider extends ProviderBase {
class SftpProvider extends ChangeNotifier {
final List<SftpReqStatus> _status = [];
List<SftpReqStatus> get status => _status;

View File

@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:toolbox/core/provider_base.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/store/snippet.dart';
import 'package:toolbox/locator.dart';
@@ -8,7 +8,7 @@ import 'package:toolbox/locator.dart';
import '../../core/extension/order.dart';
import '../store/setting.dart';
class SnippetProvider extends BusyProvider {
class SnippetProvider extends ChangeNotifier {
late Order<Snippet> _snippets;
Order<Snippet> get snippets => _snippets;

View File

@@ -2,8 +2,8 @@
class BuildData {
static const String name = "ServerBox";
static const int build = 406;
static const int build = 491;
static const String engine = "3.10.6";
static const String buildAt = "2023-08-02 23:34:08.619104";
static const int modifications = 2;
static const String buildAt = "2023-08-20 23:32:07.343451";
static const int modifications = 4;
}

View File

@@ -1,5 +1,7 @@
import 'package:flutter/services.dart';
import '../model/app/github_id.dart';
/// RegExp for number
final numReg = RegExp(r'\s{1,}');
@@ -16,3 +18,35 @@ const maxDebugLogLines = 100;
const pkgName = 'tech.lolli.toolbox';
const bgRunChannel = MethodChannel('$pkgName/app_retain');
const homeWidgetChannel = MethodChannel('$pkgName/home_widget');
// Thanks
// If you want to change the url, please open an issue.
const contributors = <GhId>{
'its-tom',
'RainSunMe',
'kalashnikov',
'azkadev',
'calvinweb',
'Liloupar'
};
const participants = <GhId>{
'jaychoubaby',
'fecture',
'Tao173',
'QingAnLe',
'wxdjs',
'Aeorq',
'allonmymind',
'Yuuki-Rin',
'LittleState',
'karuboniru',
'whosphp',
'Climit',
'dianso',
'Jasondeepny',
'kaliwell',
'ymxkiss',
'Ealrang',
'hange33',
'yuchen1204',
};

View File

@@ -1,28 +1,13 @@
import '../model/app/shell_func.dart';
import 'build_data.dart';
const seperator = 'SrvBox';
const seperator = 'SrvBoxSep';
const serverBoxDir = r'$HOME/.config/server_box';
const shellPath = '$serverBoxDir/mobile_app.sh';
const echoPWD = 'echo \$PWD';
enum CmdType {
net,
sys,
cpu,
uptime,
conn,
disk,
mem,
tempType,
tempVal,
host,
sysRhel,
}
const _cmdList = [
'cat /proc/net/dev && date +%s',
const statusCmds = [
'date +%s',
'cat /proc/net/dev',
'cat /etc/os-release | grep PRETTY_NAME',
'cat /proc/stat | grep cpu',
'uptime',
@@ -35,12 +20,6 @@ const _cmdList = [
'cat /etc/redhat-release',
];
final shellFuncStatus = AppShellFunc(
'status',
_cmdList.join('\necho $seperator\n'),
's',
);
const dockerCmds = [
'docker version',
'docker ps -a',
@@ -48,26 +27,13 @@ const dockerCmds = [
'docker image ls',
];
final shellFuncDocker = AppShellFunc(
// `dockeR` -> avoid conflict with `docker` command
// 以防止循环递归
'dockeR',
dockerCmds.join('\necho $seperator\n'),
'd',
);
final _generated = [
shellFuncStatus,
shellFuncDocker,
].generate;
final shellCmd = """
# Script for app `${BuildData.name} v1.0.${BuildData.build}`
# Delete this file while app is running will cause app crash
export LANG=en_US.utf-8
export LANG=en_US.UTF-8
$_generated
${AppShellFuncType.shellScript}
""";
final installShellCmd = "mkdir -p $serverBoxDir && "

View File

@@ -31,7 +31,7 @@ NetSpeedPart get _initNetSpeedPart => NetSpeedPart(
'',
BigInt.zero,
BigInt.zero,
BigInt.zero,
0,
);
NetSpeed get initNetSpeed => NetSpeed(
[_initNetSpeedPart],

View File

@@ -2,15 +2,18 @@ import 'package:flutter/material.dart';
/// Font style
const textSize9Grey = TextStyle(color: Colors.grey, fontSize: 9);
const textSize11 = TextStyle(fontSize: 11);
const textSize12Grey = TextStyle(color: Colors.grey, fontSize: 11);
const textSize11Grey = TextStyle(color: Colors.grey, fontSize: 11);
const textSize13 = TextStyle(fontSize: 13);
const textSize13Bold = TextStyle(fontSize: 13, fontWeight: FontWeight.bold);
const textSize13Grey = TextStyle(color: Colors.grey, fontSize: 13);
const textSize15 = TextStyle(fontSize: 15);
const textSize18 = TextStyle(fontSize: 18);
const textSize27 = TextStyle(fontSize: 27);
const grey = TextStyle(color: Colors.grey);
const textRed = TextStyle(color: Colors.red);
/// Icon
@@ -21,7 +24,10 @@ final appIcon = Image.asset('assets/app_icon.png');
const roundRectCardPadding = EdgeInsets.symmetric(horizontal: 17, vertical: 13);
/// SizedBox
const placeholder = SizedBox();
const height13 = SizedBox(height: 13);
const height77 = SizedBox(height: 77);
const width13 = SizedBox(width: 13);
const width7 = SizedBox(width: 7);

View File

@@ -3,18 +3,3 @@ const baseUrl = '$backendUrl/serverbox';
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
const myGithub = 'https://github.com/lollipopkit';
const appHelpUrl = '$myGithub/flutter_server_box#-help';
// Thanks
// If you want to change the url, please open an issue.
const thanksMap = {
'its-tom': 'https://github.com/its-tom',
'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',
'Aeorq': 'https://github.com/Aeorq',
'jaychoubaby': 'https://github.com/jaychoubaby',
'allonmymind': 'https://github.com/allonmymind',
'azkadev': 'https://github.com/azkadev'
};

View File

@@ -1,15 +1,15 @@
import 'package:toolbox/core/persistant_store.dart';
class DockerStore extends PersistentStore {
String? getDockerHost(String id) {
String? fetch(String id) {
return box.get(id);
}
void setDockerHost(String id, String host) {
void put(String id, String host) {
box.put(id, host);
}
Map<String, String> fetch() {
Map<String, String> fetchAll() {
return box.toMap().cast<String, String>();
}
}

View File

@@ -71,6 +71,9 @@ class SettingStore extends PersistentStore {
StoreProperty<bool> get sshVirtualKeyAutoOff =>
property('sshVirtualKeyAutoOff', defaultValue: true);
StoreProperty<double> get editorFontSize =>
property('editorFontSize', defaultValue: 13);
// Editor theme
StoreProperty<String> get editorTheme =>
property('editorTheme', defaultValue: defaultEditorTheme);
@@ -99,4 +102,13 @@ class SettingStore extends PersistentStore {
// Only valid on iOS
StoreProperty<bool> get autoUpdateHomeWidget =>
property('autoUpdateHomeWidget', defaultValue: isIOS);
StoreProperty<bool> get autoCheckAppUpdate =>
property('autoCheckAppUpdate', defaultValue: true);
/// Display server tab function buttons on the bottom of each server card if [true]
///
/// Otherwise, display them on the top of server detail page
StoreProperty<bool> get moveOutServerTabFuncBtns =>
property('moveOutServerTabFuncBtns', defaultValue: true);
}

View File

@@ -5,12 +5,14 @@
"add": "Neu",
"addAServer": "Server hinzufügen",
"addPrivateKey": "Private key hinzufügen",
"addSystemPrivateKeyTip": "Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?",
"added2List": "Zur Aufgabenliste hinzugefügt",
"all": "Alle",
"alreadyLastDir": "Bereits im letzten Verzeichnis.",
"alterUrl": "Url ändern",
"attention": "Achtung",
"auto": "System folgen",
"autoCheckUpdate": "Aktualisierung automatisch prüfen",
"autoUpdateHomeWidget": "Home-Widget automatisch aktualisieren",
"backup": "Backup",
"backupAndRestore": "Backup und Wiederherstellung",
@@ -26,6 +28,7 @@
"close": "Schließen",
"cmd": "Command",
"conn": "Verbindung",
"connected": "in Verbindung gebracht",
"containerName": "Container Name",
"containerStatus": "Container Status",
"convert": "Konvertieren",
@@ -75,6 +78,7 @@
"fullScreenJitterHelp": "Einbrennen des Bildschirms verhindern",
"getPushTokenFailed": "Push-Token kann nicht abgerufen werden",
"gettingToken": "Getting token...",
"goBackQ": "Zurückkommen?",
"goto": "Pfad öffnen",
"homeWidgetUrlConfig": "Home-Widget-Link konfigurieren",
"host": "Host",
@@ -109,6 +113,8 @@
"maxRetryCountEqual0": "Unbegrenzte Verbindungsversuche zum Server",
"min": "min",
"mission": "Mission",
"moveOutServerFuncBtns": "Position der Server-Funktionsschaltfläche",
"moveOutServerFuncBtnsHelp": "Ein: kann unter jeder Karte auf der Registerkarte \"Server\" angezeigt werden. Aus: kann oben auf der Seite \"Serverdetails\" angezeigt werden.",
"ms": "ms",
"name": "Name",
"needRestart": "App muss neugestartet werden",
@@ -121,6 +127,7 @@
"noSavedPrivateKey": "Keine gespeicherten Private Keys",
"noSavedSnippet": "Keine gespeicherten Snippets.",
"noServerAvailable": "Kein Server verfügbar.",
"noTask": "Nicht fragen",
"noUpdateAvailable": "Kein Update verfügbar",
"notSelected": "Nicht ausgewählt",
"nullToken": "Null token",
@@ -139,7 +146,7 @@
"plzSelectKey": "Wähle einen Key.",
"port": "Port",
"preview": "Vorschau",
"primaryColor": "Farbschema",
"primaryColorSeed": "Farbschema",
"privateKey": "Private Key",
"process": "Prozess",
"pushToken": "Push Token",
@@ -158,6 +165,8 @@
"saved": "Gerettet",
"second": "s",
"server": "Server",
"serverDetailOrder": "Reihenfolge der Widgets auf der Detailseite",
"serverOrder": "Server-Bestellung",
"serverTabConnecting": "Verbinden...",
"serverTabEmpty": "Keine Server vorhanden.",
"serverTabFailed": "Fehlgeschlagen",
@@ -166,7 +175,6 @@
"serverTabUnkown": "Unbekannter Status",
"setting": "Einstellungen",
"sftpDlPrepare": "Verbindung vorbereiten...",
"sftpNoDownloadTask": "Keine aktiven Downloads.",
"sftpSSHConnected": "SFTP Verbunden",
"showDistLogo": "Distributionslogo anzeigen",
"snippet": "Snippet",
@@ -175,11 +183,13 @@
"sshTip": "Diese Funktion befindet sich jetzt in der Experimentierphase.\n\nBitte melde Bugs auf {url} oder mach mit bei der Entwicklung.",
"sshVirtualKeyAutoOff": "Automatische Umschaltung der virtuellen Tasten",
"start": "Start",
"stats": "Statistik",
"stop": "Stop",
"success": "Erfolgreich",
"sureDelete": "Soll [{name}] wirklich gelöscht werden?",
"sureDirEmpty": "Stelle sicher, dass der Ordner leer ist.",
"sureNoPwd": "Bist du sicher, dass du kein Passwort verwenden willst?",
"sureStop": "Sind Sie sicher, dass Sie [{item}] stoppen möchten?",
"sureToDeleteServer": "Bist du sicher, dass du [{server}] löschen willst?",
"system": "Systeme",
"tag": "Tags",
@@ -203,7 +213,7 @@
"urlOrJson": "URL oder JSON",
"user": "Benutzer",
"versionHaveUpdate": "Gefunden: v1.0.{build}, klicke zum Aktualisieren",
"versionUnknownUpdate": "Aktuell: v1.0.{build}",
"versionUnknownUpdate": "Aktuell: v1.0.{build}. Klicken Sie hier, um nach Updates zu suchen",
"versionUpdated": "v1.0.{build} ist bereits die neueste Version",
"viewErr": "Fehler anzeigen",
"virtKeyHelpClipboard": "In die Zwischenablage kopieren, wenn das ausgewählte Terminal nicht leer ist, andernfalls den Inhalt der Zwischenablage in das Terminal einfügen.",

View File

@@ -5,12 +5,14 @@
"add": "Add",
"addAServer": "add a server",
"addPrivateKey": "Add private key",
"addSystemPrivateKeyTip": "Currently don't have any private key, do you add the one that comes with the system (~/.ssh/id_rsa)?",
"added2List": "Added to task list",
"all": "All",
"alreadyLastDir": "Already in last directory.",
"alterUrl": "Alter url",
"attention": "Attention",
"auto": "Auto",
"autoCheckUpdate": "Auto check update",
"autoUpdateHomeWidget": "Auto update home widget",
"backup": "Backup",
"backupAndRestore": "Backup and Restore",
@@ -26,6 +28,7 @@
"close": "Close",
"cmd": "Command",
"conn": "Connection",
"connected": "Connected",
"containerName": "Container name",
"containerStatus": "Container status",
"convert": "Convert",
@@ -75,6 +78,7 @@
"fullScreenJitterHelp": "To avoid screen burn-in",
"getPushTokenFailed": "Can't fetch push token",
"gettingToken": "Getting token...",
"goBackQ": "Go back?",
"goto": "Go to",
"homeWidgetUrlConfig": "Config home widget url",
"host": "Host",
@@ -109,6 +113,8 @@
"maxRetryCountEqual0": "Will retry again and again.",
"min": "min",
"mission": "Mission",
"moveOutServerFuncBtns": "Server function button location",
"moveOutServerFuncBtnsHelp": "On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.",
"ms": "ms",
"name": "Name",
"needRestart": "Need to restart app",
@@ -121,6 +127,7 @@
"noSavedPrivateKey": "No saved private keys.",
"noSavedSnippet": "No saved snippets.",
"noServerAvailable": "No server available.",
"noTask": "No task",
"noUpdateAvailable": "No update available",
"notSelected": "Not selected",
"nullToken": "Null token",
@@ -139,7 +146,7 @@
"plzSelectKey": "Please select a key.",
"port": "Port",
"preview": "Preview",
"primaryColor": "Primary color",
"primaryColorSeed": "Primary color seed",
"privateKey": "Private Key",
"process": "Process",
"pushToken": "Push token",
@@ -158,6 +165,8 @@
"saved": "Saved",
"second": "s",
"server": "Server",
"serverDetailOrder": "Detail page widget order",
"serverOrder": "Server order",
"serverTabConnecting": "Connecting...",
"serverTabEmpty": "There is no server.\nClick the fab to add one.",
"serverTabFailed": "Failed",
@@ -166,7 +175,6 @@
"serverTabUnkown": "Unknown state",
"setting": "Settings",
"sftpDlPrepare": "Preparing to connect...",
"sftpNoDownloadTask": "No download task.",
"sftpSSHConnected": "SFTP Connected",
"showDistLogo": "Show distribution logo",
"snippet": "Snippet",
@@ -175,11 +183,13 @@
"sshTip": "This function is now in the experimental stage.\n\nPlease report bugs on {url} or join our development.",
"sshVirtualKeyAutoOff": "Auto switching of virtual keys",
"start": "Start",
"stats": "Stats",
"stop": "Stop",
"success": "Success",
"sureDelete": "Are you sure to delete [{name}]?",
"sureDirEmpty": "Make sure dir is empty.",
"sureNoPwd": "Are you sure to use no password?",
"sureStop": "Sure to stop [{item}] ?",
"sureToDeleteServer": "Are you sure to delete server [{server}]?",
"system": "System",
"tag": "Tags",
@@ -203,7 +213,7 @@
"urlOrJson": "URL or JSON",
"user": "User",
"versionHaveUpdate": "Found: v1.0.{build}, click to update",
"versionUnknownUpdate": "Current: v1.0.{build}",
"versionUnknownUpdate": "Current: v1.0.{build}, click to check updates",
"versionUpdated": "Current: v1.0.{build}, is up to date",
"viewErr": "See error",
"virtKeyHelpClipboard": "Copy to the clipboard if terminal selected is not empty, otherwise paste the contents of the clipboard to the terminal.",

View File

@@ -5,12 +5,14 @@
"add": "Menambahkan",
"addAServer": "tambahkan server",
"addPrivateKey": "Tambahkan kunci pribadi",
"addSystemPrivateKeyTip": "Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?",
"added2List": "Ditambahkan ke Daftar Tugas",
"all": "Semua",
"alreadyLastDir": "Sudah di direktori terakhir.",
"alterUrl": "Alter url",
"attention": "Perhatian",
"auto": "Auto",
"autoCheckUpdate": "Periksa pembaruan otomatis",
"autoUpdateHomeWidget": "Widget Rumah Pembaruan Otomatis",
"backup": "Cadangan",
"backupAndRestore": "Cadangan dan Pulihkan",
@@ -26,6 +28,7 @@
"close": "Menutup",
"cmd": "Memerintah",
"conn": "Koneksi",
"connected": "Terhubung",
"containerName": "Nama kontainer",
"containerStatus": "Status wadah",
"convert": "Mengubah",
@@ -75,6 +78,7 @@
"fullScreenJitterHelp": "Untuk menghindari pembakaran layar",
"getPushTokenFailed": "Tidak bisa mengambil token dorong",
"gettingToken": "Mendapatkan token ...",
"goBackQ": "Datang kembali?",
"goto": "Pergi ke",
"homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah",
"host": "Host",
@@ -109,6 +113,8 @@
"maxRetryCountEqual0": "Akan mencoba lagi lagi dan lagi.",
"min": "Min",
"mission": "Misi",
"moveOutServerFuncBtns": "Lokasi tombol fungsi server",
"moveOutServerFuncBtnsHelp": "Aktif: dapat ditampilkan di bawah setiap kartu pada halaman Tab Server. Nonaktif: dapat ditampilkan di bagian atas halaman Rincian Server.",
"ms": "MS",
"name": "Nama",
"needRestart": "Perlu memulai ulang aplikasi",
@@ -121,6 +127,7 @@
"noSavedPrivateKey": "Tidak ada kunci pribadi yang disimpan.",
"noSavedSnippet": "Tidak ada cuplikan yang disimpan.",
"noServerAvailable": "Tidak ada server yang tersedia.",
"noTask": "Tidak bertanya",
"noUpdateAvailable": "Tidak ada pembaruan yang tersedia",
"notSelected": "Tidak terpilih",
"nullToken": "Token NULL",
@@ -139,7 +146,7 @@
"plzSelectKey": "Pilih kunci.",
"port": "Port",
"preview": "Pratinjau",
"primaryColor": "Warna utama",
"primaryColorSeed": "Warna utama",
"privateKey": "Kunci Pribadi",
"process": "Proses",
"pushToken": "Dorong token",
@@ -158,6 +165,8 @@
"saved": "Diselamatkan",
"second": "S",
"server": "Server",
"serverDetailOrder": "Detail pesanan widget halaman",
"serverOrder": "Pesanan server",
"serverTabConnecting": "Menghubungkan ...",
"serverTabEmpty": "Tidak ada server.\nKlik fab untuk menambahkan satu.",
"serverTabFailed": "Gagal",
@@ -166,7 +175,6 @@
"serverTabUnkown": "Negara yang tidak diketahui",
"setting": "Pengaturan",
"sftpDlPrepare": "Bersiap untuk terhubung ...",
"sftpNoDownloadTask": "Tidak ada tugas unduhan.",
"sftpSSHConnected": "Sftp terhubung",
"showDistLogo": "Tampilkan logo distribusi",
"snippet": "Snippet",
@@ -175,11 +183,13 @@
"sshTip": "Fungsi ini sekarang dalam tahap eksperimen.\n\nHarap laporkan bug di {url} atau bergabunglah dengan pengembangan kami.",
"sshVirtualKeyAutoOff": "Switching Otomatis Kunci Virtual",
"start": "Awal",
"stats": "Statistik",
"stop": "Berhenti",
"success": "Kesuksesan",
"sureDelete": "Apakah Anda pasti akan menghapus [{name}]?",
"sureDirEmpty": "Pastikan dir kosong.",
"sureNoPwd": "Apakah Anda pasti tidak menggunakan kata sandi?",
"sureStop": "Anda yakin ingin menghentikan [{item}]?",
"sureToDeleteServer": "Apakah Anda pasti akan menghapus server [{server}]?",
"system": "Sistem",
"tag": "Tag",
@@ -203,7 +213,7 @@
"urlOrJson": "URL atau JSON",
"user": "Username",
"versionHaveUpdate": "Ditemukan: v1.0.{build}, klik untuk memperbarui",
"versionUnknownUpdate": "Saat ini: v1.0.{build}",
"versionUnknownUpdate": "Saat ini: v1.0.{build}. Klik untuk memeriksa pembaruan.",
"versionUpdated": "Saat ini: v1.0.{build}, mutakhir",
"viewErr": "Lihat kesalahan",
"virtKeyHelpClipboard": "Salin ke clipboard jika terminal yang dipilih tidak kosong, jika tidak, tempel isi clipboard ke terminal.",

View File

@@ -5,12 +5,14 @@
"add": "新增",
"addAServer": "添加服务器",
"addPrivateKey": "添加一个私钥",
"addSystemPrivateKeyTip": "当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa",
"added2List": "已添加至任务列表",
"all": "所有",
"alreadyLastDir": "已经是最上层目录了",
"alterUrl": "备选链接",
"attention": "注意",
"auto": "自动",
"autoCheckUpdate": "自动检查更新",
"autoUpdateHomeWidget": "自动更新桌面小部件",
"backup": "备份",
"backupAndRestore": "备份和恢复",
@@ -26,6 +28,7 @@
"close": "关闭",
"cmd": "命令",
"conn": "连接",
"connected": "已连接",
"containerName": "容器名",
"containerStatus": "容器状态",
"convert": "转换",
@@ -75,6 +78,7 @@
"fullScreenJitterHelp": "防止烧屏",
"getPushTokenFailed": "未能获取到推送token",
"gettingToken": "正在获取Token...",
"goBackQ": "返回?",
"goto": "前往",
"homeWidgetUrlConfig": "桌面部件链接配置",
"host": "主机",
@@ -109,6 +113,8 @@
"maxRetryCountEqual0": "会无限重试",
"min": "最小",
"mission": "任务",
"moveOutServerFuncBtns": "服务器功能按钮位置",
"moveOutServerFuncBtnsHelp": "开启:可以在服务器 Tab 页的每个卡片下方显示。关闭:在服务器详情页顶部显示。",
"ms": "毫秒",
"name": "名称",
"needRestart": "需要重启 App",
@@ -121,6 +127,7 @@
"noSavedPrivateKey": "没有已保存的私钥。",
"noSavedSnippet": "没有已保存的代码片段。",
"noServerAvailable": "没有可用的服务器。",
"noTask": "没有任务",
"noUpdateAvailable": "没有可用更新",
"notSelected": "未选择",
"nullToken": "无Token",
@@ -139,7 +146,7 @@
"plzSelectKey": "请选择私钥",
"port": "端口",
"preview": "预览",
"primaryColor": "主题色",
"primaryColorSeed": "主题色种子",
"privateKey": "私钥",
"process": "进程",
"pushToken": "消息推送 Token",
@@ -158,6 +165,8 @@
"saved": "已保存",
"second": "秒",
"server": "服务器",
"serverDetailOrder": "详情页部件顺序",
"serverOrder": "服务器顺序",
"serverTabConnecting": "连接中...",
"serverTabEmpty": "现在没有服务器。\n点击右下方按钮来添加。",
"serverTabFailed": "失败",
@@ -166,8 +175,7 @@
"serverTabUnkown": "未知状态",
"setting": "设置",
"sftpDlPrepare": "准备连接至服务器...",
"sftpNoDownloadTask": "没有下载任务",
"sftpSSHConnected": "SFTP 已连接,即将开始下载...",
"sftpSSHConnected": "SFTP 已连接...",
"showDistLogo": "显示发行版 Logo",
"snippet": "代码片段",
"speed": "速度",
@@ -175,11 +183,13 @@
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
"sshVirtualKeyAutoOff": "虚拟按键自动切换",
"start": "开始",
"stats": "统计",
"stop": "停止",
"success": "成功",
"sureDelete": "确定删除 [{name}]",
"sureDirEmpty": "请确保文件夹为空",
"sureNoPwd": "确认使用无密码?",
"sureStop": "确定要停止 [{item}] 吗?",
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
"system": "系统",
"tag": "标签",
@@ -203,7 +213,7 @@
"urlOrJson": "链接或JSON",
"user": "用户",
"versionHaveUpdate": "找到新版本v1.0.{build}, 点击更新",
"versionUnknownUpdate": "当前v1.0.{build}",
"versionUnknownUpdate": "当前v1.0.{build},点击检查更新",
"versionUpdated": "当前v1.0.{build}, 已是最新版本",
"viewErr": "查看错误",
"virtKeyHelpClipboard": "如果终端有选中字符,则复制选中字符至剪切板,否则粘贴剪切板内容至终端。",

View File

@@ -5,12 +5,14 @@
"add": "新增",
"addAServer": "新增服務器",
"addPrivateKey": "新增一個私鑰",
"addSystemPrivateKeyTip": "當前沒有任何私鑰,是否添加系統自帶的(~/.ssh/id_rsa",
"added2List": "已添加至任務列表",
"all": "所有",
"alreadyLastDir": "已經是最上層目錄了",
"alterUrl": "備選鏈接",
"attention": "注意",
"auto": "自動",
"autoCheckUpdate": "自動檢查更新",
"autoUpdateHomeWidget": "自動更新桌面小部件",
"backup": "備份",
"backupAndRestore": "備份和還原",
@@ -26,6 +28,7 @@
"close": "關閉",
"cmd": "命令",
"conn": "連接",
"connected": "已連接",
"containerName": "容器名稱",
"containerStatus": "容器狀態",
"convert": "轉換",
@@ -75,6 +78,7 @@
"fullScreenJitterHelp": "防止燒屏",
"getPushTokenFailed": "未能獲取到推送token",
"gettingToken": "正在獲取Token...",
"goBackQ": "返回?",
"goto": "前往",
"homeWidgetUrlConfig": "桌面部件鏈接配置",
"host": "主機",
@@ -109,6 +113,8 @@
"maxRetryCountEqual0": "會無限重試",
"min": "最小",
"mission": "任務",
"moveOutServerFuncBtns": "服務器功能按鈕位置",
"moveOutServerFuncBtnsHelp": "開啟:可以在服務器 Tab 頁的每個卡片下方顯示。關閉:在服務器詳情頁頂部顯示。",
"ms": "毫秒",
"name": "名稱",
"needRestart": "需要重啓 App",
@@ -121,6 +127,7 @@
"noSavedPrivateKey": "沒有已保存的私鑰。",
"noSavedSnippet": "沒有已保存的程式片段。",
"noServerAvailable": "沒有可用的服務器。",
"noTask": "沒有任務",
"noUpdateAvailable": "沒有可用更新",
"notSelected": "未選擇",
"nullToken": "無Token",
@@ -139,7 +146,7 @@
"plzSelectKey": "請選擇私鑰",
"port": "端口",
"preview": "預覽",
"primaryColor": "主要色調",
"primaryColorSeed": "主要色調種子",
"privateKey": "私鑰",
"process": "進程",
"pushToken": "消息推送 Token",
@@ -158,6 +165,8 @@
"saved": "已保存",
"second": "秒",
"server": "服務器",
"serverDetailOrder": "詳情頁部件順序",
"serverOrder": "服務器順序",
"serverTabConnecting": "連接中...",
"serverTabEmpty": "現在沒有服務器。\n點擊右下方按鈕來新增。",
"serverTabFailed": "失敗",
@@ -166,8 +175,7 @@
"serverTabUnkown": "未知狀態",
"setting": "設置",
"sftpDlPrepare": "準備連接至服務器...",
"sftpNoDownloadTask": "沒有下載任務",
"sftpSSHConnected": "SFTP 已連接,即將開始下載...",
"sftpSSHConnected": "SFTP 已連接...",
"showDistLogo": "顯示發行版 Logo",
"snippet": "程式片段",
"speed": "速度",
@@ -175,11 +183,13 @@
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 反饋問題,或者加入我們開發。",
"sshVirtualKeyAutoOff": "虛擬按鍵自動切換",
"start": "開始",
"stats": "統計",
"stop": "停止",
"success": "成功",
"sureDelete": "確定刪除 [{name}]",
"sureDirEmpty": "請確保文件夾為空",
"sureNoPwd": "確認使用無密碼?",
"sureStop": "確定要停止 [{item}] 嗎?",
"sureToDeleteServer": "你確定要刪除服務器 [{server}] 嗎?",
"system": "系統",
"tag": "标签",
@@ -203,7 +213,7 @@
"urlOrJson": "鏈接或JSON",
"user": "用戶",
"versionHaveUpdate": "找到新版本v1.0.{build}, 點擊更新",
"versionUnknownUpdate": "當前v1.0.{build}",
"versionUnknownUpdate": "當前v1.0.{build},點擊檢查更新",
"versionUpdated": "當前v1.0.{build}, 已是最新版本",
"viewErr": "查看錯誤",
"virtKeyHelpClipboard": "如果終端有選中字符,則復製選中字符至剪切板,否則粘貼剪切板內容至終端。",

View File

@@ -18,11 +18,11 @@ import 'data/store/snippet.dart';
GetIt locator = GetIt.instance;
void setupLocatorForServices() {
void _setupLocatorForServices() {
locator.registerLazySingleton(() => AppService());
}
void setupLocatorForProviders() {
void _setupLocatorForProviders() {
locator.registerSingleton(AppProvider());
locator.registerSingleton(PkgProvider());
locator.registerSingleton(DebugProvider());
@@ -34,7 +34,7 @@ void setupLocatorForProviders() {
locator.registerSingleton(SftpProvider());
}
Future<void> setupLocatorForStores() async {
Future<void> _setupLocatorForStores() async {
final setting = SettingStore();
await setting.init(boxName: 'setting');
locator.registerSingleton(setting);
@@ -57,7 +57,7 @@ Future<void> setupLocatorForStores() async {
}
Future<void> setupLocator() async {
await setupLocatorForStores();
setupLocatorForProviders();
setupLocatorForServices();
await _setupLocatorForStores();
_setupLocatorForProviders();
_setupLocatorForServices();
}

View File

@@ -3,17 +3,21 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:logging/logging.dart';
import 'package:macos_window_utils/window_manipulator.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:toolbox/data/model/app/net_view.dart';
import 'package:toolbox/data/model/ssh/virtual_key.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/view/widget/custom_appbar.dart';
import 'app.dart';
import 'core/analysis.dart';
import 'core/utils/platform.dart';
import 'core/utils/ui.dart';
import 'data/model/app/net_view.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/model/ssh/virtual_key.dart';
import 'data/provider/app.dart';
import 'data/provider/debug.dart';
import 'data/provider/docker.dart';
@@ -27,66 +31,10 @@ 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();
_debug = locator<DebugProvider>();
locator<SnippetProvider>().loadData();
locator<PrivateKeyProvider>().loadData();
final settings = locator<SettingStore>();
await loadFontFile(settings.fontPath.fetch());
SharedPreferences.setPrefix('');
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print('[${record.loggerName}][${record.level.name}]: ${record.message}');
});
}
Future<void> initHive() async {
await Hive.initFlutter();
// 以 typeId 为顺序
Hive.registerAdapter(PrivateKeyInfoAdapter());
Hive.registerAdapter(SnippetAdapter());
Hive.registerAdapter(ServerPrivateInfoAdapter());
Hive.registerAdapter(VirtKeyAdapter());
Hive.registerAdapter(NetViewTypeAdapter());
}
void runInZone(dynamic Function() body) {
final zoneSpec = ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
parent.print(zone, line);
// This is a hack to avoid
// `setState() or markNeedsBuild() called during build`
// error.
Future.delayed(const Duration(milliseconds: 1), () {
_debug.addText(line);
});
},
);
runZonedGuarded(
body,
onError,
zoneSpecification: zoneSpec,
);
}
void onError(Object obj, StackTrace stack) {
Analysis.recordException(obj);
_debug.addMultiline(obj, Colors.red);
_debug.addMultiline(stack, Colors.white);
}
DebugProvider? _debug;
Future<void> main() async {
runInZone(() async {
_runInZone(() async {
await initApp();
runApp(
MultiProvider(
@@ -108,3 +56,91 @@ Future<void> main() async {
);
});
}
void _runInZone(void Function() body) {
final zoneSpec = ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
parent.print(zone, line);
// This is a hack to avoid
// `setState() or markNeedsBuild() called during build`
// error.
Future.delayed(const Duration(milliseconds: 1), () {
_debug?.addText(line);
});
},
);
runZonedGuarded(
body,
(obj, trace) => Analysis.recordException(trace),
zoneSpecification: zoneSpec,
);
}
Future<void> initApp() async {
await _initMacOSWindow();
// Base of all data.
await _initHive();
await setupLocator();
// Setup [DebugProvider] first to catch all logs.
_debug = locator<DebugProvider>();
_setupLogger();
_setupProviders();
// Load font
final settings = locator<SettingStore>();
loadFontFile(settings.fontPath.fetch());
// Android only
if (!isAndroid) return;
// Only start service when [bgRun] is true.
if (locator<SettingStore>().bgRun.fetch() ?? false) {
bgRunChannel.invokeMethod('startService');
}
// SharedPreferences is only used on Android for saving home widgets settings.
SharedPreferences.setPrefix('');
}
void _setupProviders() {
locator<SnippetProvider>().loadData();
locator<PrivateKeyProvider>().loadData();
}
Future<void> _initHive() async {
await Hive.initFlutter();
// 以 typeId 为顺序
Hive.registerAdapter(PrivateKeyInfoAdapter());
Hive.registerAdapter(SnippetAdapter());
Hive.registerAdapter(ServerPrivateInfoAdapter());
Hive.registerAdapter(VirtKeyAdapter());
Hive.registerAdapter(NetViewTypeAdapter());
}
void _setupLogger() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
var str = '[${record.loggerName}][${record.level.name}]: ${record.message}';
if (record.error != null) {
str += '\n${record.error}';
_debug?.addMultiline(record.error.toString(), Colors.red);
}
if (record.stackTrace != null) {
str += '\n${record.stackTrace}';
_debug?.addMultiline(record.stackTrace.toString(), Colors.white);
}
// ignore: avoid_print
print(str);
});
}
Future<void> _initMacOSWindow() async {
if (!isMacOS) return;
WidgetsFlutterBinding.ensureInitialized();
await WindowManipulator.initialize();
WindowManipulator.makeTitlebarTransparent();
WindowManipulator.enableFullSizeContentView();
WindowManipulator.hideTitle();
await CustomAppBar.updateTitlebarHeight();
}

View File

@@ -19,6 +19,7 @@ import '../../data/store/private_key.dart';
import '../../data/store/server.dart';
import '../../data/store/snippet.dart';
import '../../locator.dart';
import '../widget/custom_appbar.dart';
const backupFormatVersion = 1;
@@ -34,7 +35,7 @@ class BackupPage extends StatelessWidget {
Widget build(BuildContext context) {
final s = S.of(context)!;
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
title: Text(s.backupAndRestore, style: textSize18),
),
body: _buildBody(context, s),
@@ -89,7 +90,7 @@ class BackupPage extends StatelessWidget {
spis: _server.fetch(),
snippets: _snippet.fetch(),
keys: _privateKey.fetch(),
dockerHosts: _dockerHosts.fetch(),
dockerHosts: _dockerHosts.fetchAll(),
),
),
);
@@ -170,7 +171,7 @@ class BackupPage extends StatelessWidget {
_privateKey.put(s);
}
for (final k in backup.dockerHosts.keys) {
_dockerHosts.setDockerHost(k, backup.dockerHosts[k]!);
_dockerHosts.put(k, backup.dockerHosts[k]!);
}
context.pop();
showRoundDialog(

View File

@@ -7,6 +7,7 @@ import 'package:toolbox/data/res/ui.dart';
import 'package:toolbox/view/widget/value_notifier.dart';
import '../../core/utils/ui.dart';
import '../widget/custom_appbar.dart';
import '../widget/input_field.dart';
import '../widget/popup_menu.dart';
import '../widget/round_rect_card.dart';
@@ -41,11 +42,18 @@ class _ConvertPageState extends State<ConvertPage>
_s = S.of(context)!;
}
@override
void dispose() {
super.dispose();
_textEditingController.dispose();
_textEditingControllerResult.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
title: Text(_s.convert),
),
body: SingleChildScrollView(

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/data/provider/debug.dart';
import '../widget/custom_appbar.dart';
class DebugPage extends StatefulWidget {
const DebugPage({Key? key}) : super(key: key);
@@ -13,8 +16,12 @@ class _DebugPageState extends State<DebugPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Logs'),
appBar: CustomAppBar(
leading: IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back, color: Colors.white),
),
title: const Text('Logs', style: TextStyle(color: Colors.white)),
backgroundColor: Colors.black,
),
body: _buildTerminal(context),

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:nil/nil.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/docker/image.dart';
import 'package:toolbox/view/page/ssh/term.dart';
import 'package:toolbox/view/page/ssh_term.dart';
import 'package:toolbox/view/widget/input_field.dart';
import '../../core/utils/ui.dart';
@@ -19,6 +18,7 @@ import '../../data/res/ui.dart';
import '../../data/res/url.dart';
import '../../data/store/docker.dart';
import '../../locator.dart';
import '../widget/custom_appbar.dart';
import '../widget/popup_menu.dart';
import '../widget/round_rect_card.dart';
import '../widget/two_line_text.dart';
@@ -26,7 +26,7 @@ import '../widget/url_text.dart';
class DockerManagePage extends StatefulWidget {
final ServerPrivateInfo spi;
const DockerManagePage(this.spi, {Key? key}) : super(key: key);
const DockerManagePage({required this.spi, Key? key}) : super(key: key);
@override
State<DockerManagePage> createState() => _DockerManagePageState();
@@ -34,6 +34,7 @@ class DockerManagePage extends StatefulWidget {
class _DockerManagePageState extends State<DockerManagePage> {
final _docker = locator<DockerProvider>();
final _store = locator<DockerStore>();
final _textController = TextEditingController();
late S _s;
@@ -41,6 +42,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
void dispose() {
super.dispose();
_docker.clear();
_textController.dispose();
}
@override
@@ -54,8 +56,6 @@ class _DockerManagePageState extends State<DockerManagePage> {
super.initState();
final client = locator<ServerProvider>().servers[widget.spi.id]?.client;
if (client == null) {
showSnackBar(context, Text(_s.noClient));
context.pop();
return;
}
_docker.init(client, widget.spi.user, onPwdRequest, widget.spi.id);
@@ -65,12 +65,16 @@ class _DockerManagePageState extends State<DockerManagePage> {
Widget build(BuildContext context) {
return Consumer<DockerProvider>(builder: (_, ___, __) {
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
centerTitle: true,
title: TwoLineText(up: 'Docker', down: widget.spi.name),
actions: [
IconButton(
onPressed: _docker.refresh,
onPressed: () async {
showLoadingDialog(context);
await _docker.refresh();
context.pop();
},
icon: const Icon(Icons.refresh),
)
],
@@ -99,6 +103,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
mainAxisSize: MainAxisSize.min,
children: [
Input(
autoFocus: true,
type: TextInputType.text,
label: _s.image,
hint: 'xxx:1.1',
@@ -153,7 +158,9 @@ class _DockerManagePageState extends State<DockerManagePage> {
TextButton(
onPressed: () async {
context.pop();
showLoadingDialog(context);
final result = await _docker.run(cmd);
context.pop();
if (result != null) {
showSnackBar(context, Text(result.message ?? _s.unknownError));
}
@@ -217,10 +224,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
),
TextButton(
onPressed: onSubmitted,
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
child: Text(_s.ok, style: textRed),
),
],
);
@@ -239,7 +243,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
size: 37,
),
const SizedBox(height: 27),
_buildErr(_docker.error!),
Text(_docker.error?.message ?? _s.unknownError),
const SizedBox(height: 27),
Padding(
padding: const EdgeInsets.all(17),
@@ -250,7 +254,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
);
}
if (_docker.items == null || _docker.images == null) {
Future.delayed(const Duration(milliseconds: 177), () {
Future.delayed(const Duration(milliseconds: 37), () {
if (mounted) {
_docker.refresh();
}
@@ -258,31 +262,21 @@ class _DockerManagePageState extends State<DockerManagePage> {
return centerLoading;
}
final items = <Widget>[];
items.addAll([
final items = <Widget>[
_buildLoading(),
_buildVersion(
_docker.edition ?? _s.unknown,
_docker.version ?? _s.unknown,
),
..._buildPsItems(),
_buildImages(),
_buildVersion(),
_buildPs(),
_buildImage(),
_buildEditHost(),
].map((e) => RoundRectCard(e)));
items.add(const SizedBox(height: 37));
].map((e) => RoundRectCard(e));
return ListView(
padding: const EdgeInsets.all(7),
children: items,
children: items.toList(),
);
}
Widget _buildImages() {
if (_docker.images == null) {
return nil;
}
final items = _docker.images!.map(_buildImageItem).toList();
items.insert(
0,
Widget _buildImage() {
final items = <Widget>[
ListTile(
title: Text(_s.imagesList),
subtitle: Text(
@@ -290,7 +284,8 @@ class _DockerManagePageState extends State<DockerManagePage> {
style: grey,
),
),
);
];
items.addAll(_docker.images!.map(_buildImageItem));
return Column(children: items);
}
@@ -330,17 +325,14 @@ class _DockerManagePageState extends State<DockerManagePage> {
);
}
},
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
child: Text(_s.ok, style: textRed),
),
],
);
}
Widget _buildLoading() {
if (!_docker.isBusy) return nil;
if (_docker.runLog == null) return placeholder;
return Padding(
padding: const EdgeInsets.all(17),
child: Column(
@@ -355,6 +347,165 @@ class _DockerManagePageState extends State<DockerManagePage> {
);
}
Widget _buildSolution(DockerErr err) {
switch (err.type) {
case DockerErrType.notInstalled:
return UrlText(
text: _s.installDockerWithUrl,
replace: _s.install,
);
case DockerErrType.noClient:
return Text(_s.waitConnection);
case DockerErrType.invalidVersion:
return UrlText(
text: _s.invalidVersionHelp(appHelpUrl),
replace: 'Github',
);
case DockerErrType.parseImages:
return const Text('Parse images error');
case DockerErrType.parsePsItem:
return const Text('Parse ps item error');
case DockerErrType.parseStats:
return const Text('Parse stats error');
case DockerErrType.unknown:
return const Text('Unknown error');
case DockerErrType.cmdNoPrefix:
return const Text('Cmd no prefix');
case DockerErrType.segmentsNotMatch:
return const Text('Segments not match');
}
}
Widget _buildVersion() {
return Padding(
padding: const EdgeInsets.all(17),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_docker.edition ?? _s.unknown),
Text(_docker.version ?? _s.unknown),
],
),
);
}
Widget _buildPs() {
final items = <Widget>[
ListTile(
title: Text(_s.containerStatus),
subtitle: Text(_buildPsCardSubtitle(_docker.items!), style: grey),
),
];
items.addAll(_docker.items!.map(_buildPsItem));
return Column(
mainAxisSize: MainAxisSize.min,
children: items,
);
}
Widget _buildPsItem(DockerPsItem item) {
return ListTile(
title: Text(item.image),
subtitle: Text(
'${item.name} - ${item.status}',
style: textSize13Grey,
),
trailing: _buildMoreBtn(item),
);
}
Widget _buildMoreBtn(DockerPsItem dItem) {
return PopupMenu(
items: DockerMenuType.items(dItem.running)
.map(
(e) => e.build(_s),
)
.toList(),
onSelected: (DockerMenuType item) async {
switch (item) {
case DockerMenuType.rm:
showRoundDialog(
context: context,
title: Text(_s.attention),
child: Text(_s.sureDelete(dItem.name)),
actions: [
TextButton(
onPressed: () async {
context.pop();
showLoadingDialog(context);
await _docker.delete(dItem.containerId);
context.pop();
},
child: Text(_s.ok),
)
],
);
break;
case DockerMenuType.start:
showLoadingDialog(context);
await _docker.start(dItem.containerId);
context.pop();
break;
case DockerMenuType.stop:
showLoadingDialog(context);
await _docker.stop(dItem.containerId);
context.pop();
break;
case DockerMenuType.restart:
showLoadingDialog(context);
await _docker.restart(dItem.containerId);
context.pop();
break;
case DockerMenuType.logs:
AppRoute(
SSHPage(
spi: widget.spi,
initCmd: 'docker logs -f --tail 100 ${dItem.containerId}',
),
'Docker logs',
).go(context);
break;
case DockerMenuType.terminal:
AppRoute(
SSHPage(
spi: widget.spi,
initCmd: 'docker exec -it ${dItem.containerId} sh',
),
'Docker terminal',
).go(context);
break;
case DockerMenuType.stats:
showRoundDialog(
context: context,
title: Text(_s.stats),
child: Text(
'CPU: ${dItem.cpu}\n'
'Mem: ${dItem.mem}\n'
'Net: ${dItem.net}\n'
'Block: ${dItem.disk}',
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(_s.ok),
),
],
);
break;
}
},
);
}
String _buildPsCardSubtitle(List<DockerPsItem> running) {
final runningCount = running.where((element) => element.running).length;
final stoped = running.length - runningCount;
if (stoped == 0) {
return _s.dockerStatusRunningFmt(runningCount);
}
return _s.dockerStatusRunningAndStoppedFmt(runningCount, stoped);
}
Widget _buildEditHost() {
final children = <Widget>[];
if (_docker.items!.isEmpty && _docker.images!.isEmpty) {
@@ -378,183 +529,29 @@ class _DockerManagePageState extends State<DockerManagePage> {
}
Future<void> _showEditHostDialog() async {
final id = widget.spi.id;
final host = _store.fetch(id) ?? 'unix:///run/user/1000/docker.sock';
final ctrl = TextEditingController(text: host);
await showRoundDialog(
context: context,
title: Text(_s.dockerEditHost),
child: Input(
maxLines: 1,
controller:
TextEditingController(text: 'unix:///run/user/1000/docker.sock'),
onSubmitted: (value) {
locator<DockerStore>().setDockerHost(widget.spi.id, value.trim());
_docker.refresh();
context.pop();
},
controller: ctrl,
onSubmitted: _onSaveDockerHost,
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(_s.cancel),
onPressed: () => _onSaveDockerHost(ctrl.text),
child: Text(_s.ok),
),
],
);
}
Widget _buildErr(DockerErr err) {
var errStr = '';
switch (err.type) {
case DockerErrType.noClient:
errStr = _s.noClient;
break;
case DockerErrType.notInstalled:
errStr = _s.dockerNotInstalled;
break;
case DockerErrType.invalidVersion:
errStr = _s.invalidVersion;
break;
default:
errStr = err.message ?? _s.unknown;
}
return Text(errStr);
}
Widget _buildSolution(DockerErr err) {
switch (err.type) {
case DockerErrType.notInstalled:
return UrlText(
text: _s.installDockerWithUrl,
replace: _s.install,
);
case DockerErrType.noClient:
return Text(_s.waitConnection);
case DockerErrType.invalidVersion:
return UrlText(
text: _s.invalidVersionHelp(appHelpUrl),
replace: 'Github',
);
default:
return Text(_s.unknownError);
}
}
Widget _buildVersion(String edition, String version) {
return Padding(
padding: const EdgeInsets.all(17),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text(edition), Text(version)],
),
);
}
List<Widget> _buildPsItems() {
final items = _docker.items!.map(_buildPsItem).toList();
items.insert(
0,
ListTile(
title: Text(_s.containerStatus),
subtitle: Text(_buildSubtitle(_docker.items!), style: grey),
),
);
return items;
}
Widget _buildPsItem(DockerPsItem item) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(item.name),
subtitle: Text(
'${item.image}\n${item.status}',
style: textSize13Grey,
),
trailing: _buildMoreBtn(item, _docker.isBusy),
),
_buildPsItemStat(item),
],
);
}
Widget _buildPsItemStat(DockerPsItem item) {
if (!item.running) return const SizedBox();
return Padding(
padding: const EdgeInsets.only(left: 17, bottom: 11, right: 17),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.cpu ?? _s.unknown, style: grey),
Text(item.mem ?? _s.unknown, style: grey),
Text(item.net ?? _s.unknown, style: grey),
Text(item.disk ?? _s.unknown, style: grey),
],
),
);
}
Widget _buildMoreBtn(DockerPsItem dItem, bool busy) {
return PopupMenu(
items:
DockerMenuType.items(dItem.running).map((e) => e.build(_s)).toList(),
onSelected: (DockerMenuType item) async {
if (busy) {
showSnackBar(context, Text(_s.isBusy));
return;
}
switch (item) {
case DockerMenuType.rm:
showRoundDialog(
context: context,
title: Text(_s.attention),
child: Text(_s.sureDelete(dItem.name)),
actions: [
TextButton(
onPressed: () {
context.pop();
_docker.delete(dItem.containerId);
},
child: Text(_s.ok),
)
],
);
break;
case DockerMenuType.start:
_docker.start(dItem.containerId);
break;
case DockerMenuType.stop:
_docker.stop(dItem.containerId);
break;
case DockerMenuType.restart:
_docker.restart(dItem.containerId);
break;
case DockerMenuType.logs:
AppRoute(
SSHPage(
spi: widget.spi,
initCmd: 'docker logs ${dItem.containerId}',
),
'Docker logs',
).go(context);
break;
case DockerMenuType.terminal:
AppRoute(
SSHPage(
spi: widget.spi,
initCmd: 'docker exec -it ${dItem.containerId} /bin/sh',
),
'Docker terminal',
).go(context);
}
},
);
}
String _buildSubtitle(List<DockerPsItem> running) {
final runningCount = running.where((element) => element.running).length;
final stoped = running.length - runningCount;
if (stoped == 0) {
return _s.dockerStatusRunningFmt(runningCount);
}
return _s.dockerStatusRunningAndStoppedFmt(runningCount, stoped);
void _onSaveDockerHost(String val) {
context.pop();
_store.put(widget.spi.id, val.trim());
_docker.refresh();
}
}

View File

@@ -15,6 +15,7 @@ import 'package:toolbox/data/res/highlight.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart';
import '../widget/custom_appbar.dart';
import '../widget/two_line_text.dart';
class EditorPage extends StatefulWidget {
@@ -32,6 +33,7 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
Map<String, TextStyle>? _codeTheme;
late S _s;
late String? _langCode;
late TextStyle _textStyle;
@override
void initState() {
@@ -40,6 +42,7 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
_controller = CodeController(
language: suffix2HighlightMap[_langCode],
);
_textStyle = TextStyle(fontSize: _setting.editorFontSize.fetch());
WidgetsBinding.instance.addPostFrameCallback((Duration duration) async {
if (isDarkMode(context)) {
@@ -68,55 +71,9 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: () {
if (_codeTheme != null) {
return _codeTheme!['root']!.backgroundColor;
}
return null;
}(),
appBar: AppBar(
centerTitle: true,
title: TwoLineText(up: getFileName(widget.path) ?? '', down: _s.editor),
actions: [
PopupMenuButton<String>(
icon: const Icon(Icons.language),
onSelected: (value) {
_controller.language = suffix2HighlightMap[value];
_langCode = value;
},
initialValue: _langCode,
itemBuilder: (BuildContext context) {
return suffix2HighlightMap.keys.map((e) {
return PopupMenuItem(
value: e,
child: Text(e),
);
}).toList();
},
)
],
),
body: Visibility(
visible: (_codeTheme != null),
replacement: const Center(
child: CircularProgressIndicator(),
),
child: SingleChildScrollView(
child: CodeTheme(
data: CodeThemeData(
styles: _codeTheme ??
(isDarkMode(context) ? monokaiTheme : a11yLightTheme)),
child: CodeField(
focusNode: _focusNode,
controller: _controller,
lineNumberStyle: const LineNumberStyle(
width: 47,
margin: 7,
),
),
),
),
),
backgroundColor: _codeTheme?['root']?.backgroundColor,
appBar: _buildAppBar(),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.done),
onPressed: () {
@@ -126,6 +83,55 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
);
}
PreferredSizeWidget _buildAppBar() {
return CustomAppBar(
title: TwoLineText(up: getFileName(widget.path) ?? '', down: _s.editor),
actions: [
PopupMenuButton<String>(
icon: const Icon(Icons.language),
onSelected: (value) {
_controller.language = suffix2HighlightMap[value];
_langCode = value;
},
initialValue: _langCode,
itemBuilder: (BuildContext context) {
return suffix2HighlightMap.keys.map((e) {
return PopupMenuItem(
value: e,
child: Text(e),
);
}).toList();
},
)
],
);
}
Widget _buildBody() {
return Visibility(
visible: _codeTheme != null,
replacement: const Center(
child: CircularProgressIndicator(),
),
child: SingleChildScrollView(
child: CodeTheme(
data: CodeThemeData(
styles: _codeTheme ??
(isDarkMode(context) ? monokaiTheme : a11yLightTheme)),
child: CodeField(
focusNode: _focusNode,
controller: _controller,
textStyle: _textStyle,
lineNumberStyle: const LineNumberStyle(
width: 47,
margin: 7,
),
),
),
),
);
}
@override
FutureOr<void> afterFirstLayout(BuildContext context) async {
if (widget.path != null) {

View File

@@ -6,9 +6,9 @@ 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:nil/nil.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/server/disk.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/ui.dart';
import 'package:toolbox/data/store/setting.dart';
@@ -22,9 +22,8 @@ import '../../data/model/server/server.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/model/server/server_status.dart';
import '../../data/res/color.dart';
import 'server/detail.dart';
import 'server/edit.dart';
import 'setting.dart';
import 'setting/entry.dart';
class FullScreenPage extends StatefulWidget {
const FullScreenPage({Key? key}) : super(key: key);
@@ -62,6 +61,7 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
void dispose() {
super.dispose();
_timer.cancel();
_pageController.dispose();
}
@override
@@ -146,7 +146,7 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
final id = pro.serverOrder[idx];
final s = pro.servers[id];
if (s == null) {
return nil;
return Center(child: Text(_s.noClient));
}
return _buildRealServerCard(s.status, s.state, s.spi);
},
@@ -159,13 +159,10 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
ServerState cs,
ServerPrivateInfo spi,
) {
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/');
final rootDisk = findRootDisk(ss.disk);
return InkWell(
onTap: () => AppRoute(
ServerDetailPage(spi.id),
'server detail page',
).go(context),
onTap: () => AppRoute.serverDetail(spi: spi).go(context),
child: Stack(
children: [
Positioned(
@@ -185,8 +182,8 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
_buildPercentCircle(ss.mem.usedPercent * 100),
_buildNet(ss),
_buildIOData(
'Total:\n${rootDisk.size}',
'Used:\n${rootDisk.usedPercent}%',
'Total:\n${rootDisk?.size}',
'Used:\n${rootDisk?.usedPercent}%',
)
],
),
@@ -253,7 +250,7 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
);
return Text(
topRightStr,
style: textSize12Grey,
style: textSize11Grey,
textScaleFactor: 1.0,
);
}
@@ -373,7 +370,9 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
@override
Future<void> afterFirstLayout(BuildContext context) async {
doUpdate(context);
if (_setting.autoCheckAppUpdate.fetch()!) {
doUpdate(context);
}
await GetIt.I.allReady();
await _serverProvider.loadLocalData();
await _serverProvider.refreshData();

View File

@@ -2,6 +2,7 @@ import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:get_it/get_it.dart';
import 'package:toolbox/data/model/app/github_id.dart';
import 'package:toolbox/data/model/app/tab.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/res/misc.dart';
@@ -19,12 +20,13 @@ import '../../data/res/ui.dart';
import '../../data/res/url.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
import '../widget/custom_appbar.dart';
import '../widget/url_text.dart';
import 'backup.dart';
import 'convert.dart';
import 'debug.dart';
import 'private_key/list.dart';
import 'setting.dart';
import 'setting/entry.dart';
import 'storage/local.dart';
class HomePage extends StatefulWidget {
@@ -74,6 +76,7 @@ class _HomePageState extends State<HomePage>
super.dispose();
WidgetsBinding.instance.removeObserver(this);
_serverProvider.closeServer();
_pageController.dispose();
}
@override
@@ -107,23 +110,13 @@ class _HomePageState extends State<HomePage>
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
drawer: _buildDrawer(),
appBar: AppBar(
title: const Text(BuildData.name),
actions: [
IconButton(
icon: const Icon(Icons.developer_mode, size: 23),
tooltip: _s.debug,
onPressed: () => AppRoute(
const DebugPage(),
'Debug Page',
).go(context),
),
],
),
appBar: _buildAppBar(),
body: PageView.builder(
controller: _pageController,
itemCount: AppTab.values.length,
itemBuilder: (_, index) => AppTab.values[index].page,
onPageChanged: (value) {
if (!_switchingPage) {
@@ -138,9 +131,44 @@ class _HomePageState extends State<HomePage>
);
}
PreferredSizeWidget _buildAppBar() {
final actions = <Widget>[
IconButton(
icon: const Icon(Icons.developer_mode, size: 23),
tooltip: _s.debug,
onPressed: () => AppRoute(
const DebugPage(),
'Debug Page',
).go(context),
),
];
if (isDesktop && _selectIndex.value == AppTab.server.index) {
actions.add(
ValueBuilder(
listenable: _selectIndex,
build: () {
if (_selectIndex.value != AppTab.server.index) {
return const SizedBox();
}
return IconButton(
icon: const Icon(Icons.refresh, size: 23),
tooltip: 'Refresh',
onPressed: () => _serverProvider.refreshData(onlyFailed: true),
);
},
),
);
}
return CustomAppBar(
title: const Text(BuildData.name),
actions: actions,
);
}
Widget _buildBottomBar() {
return NavigationBar(
selectedIndex: _selectIndex.value,
height: kBottomNavigationBarHeight * 1.2,
animationDuration: const Duration(milliseconds: 250),
onDestinationSelected: (int index) {
if (_selectIndex.value == index) return;
@@ -196,92 +224,114 @@ class _HomePageState extends State<HomePage>
),
),
const SizedBox(height: 37),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 17),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.settings),
title: Text(_s.setting),
onTap: () => AppRoute(
const SettingPage(),
'Setting',
).go(context),
),
ListTile(
leading: const Icon(Icons.vpn_key),
title: Text(_s.privateKey),
onTap: () => AppRoute(
const PrivateKeysListPage(),
'private key list',
).go(context),
),
ListTile(
leading: const Icon(Icons.download),
title: Text(_s.download),
onTap: () => AppRoute(
const LocalStoragePage(),
'sftp local page',
).go(context),
),
ListTile(
leading: const Icon(Icons.import_export),
title: Text(_s.backup),
onTap: () => AppRoute(
BackupPage(),
'backup page',
).go(context),
),
ListTile(
leading: const Icon(Icons.code),
title: Text(_s.convert),
onTap: () => AppRoute(
const ConvertPage(),
'convert page',
).go(context),
),
ListTile(
leading: const Icon(Icons.text_snippet),
title: Text('${_s.about} & ${_s.feedback}'),
onTap: () {
showRoundDialog(
context: context,
title: Text(_s.about),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UrlText(
text: _s.madeWithLove(myGithub),
replace: 'lollipopkit'),
UrlText(
text: _s.aboutThanks,
),
// Thanks
...thanksMap.keys.map(
(key) => UrlText(
text: thanksMap[key] ?? '',
replace: key,
),
)
],
),
actions: [
TextButton(
onPressed: () => openUrl(appHelpUrl),
child: Text(_s.feedback),
),
TextButton(
onPressed: () => showLicensePage(context: context),
child: Text(_s.license),
),
],
);
},
)
].map((e) => RoundRectCard(e)).toList(),
_buildTiles(),
],
),
);
}
Widget _buildTiles() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 17),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.settings),
title: Text(_s.setting),
onTap: () => AppRoute(
const SettingPage(),
'Setting',
).go(context),
),
ListTile(
leading: const Icon(Icons.vpn_key),
title: Text(_s.privateKey),
onTap: () => AppRoute(
const PrivateKeysListPage(),
'private key list',
).go(context),
),
ListTile(
leading: const Icon(Icons.download),
title: Text(_s.download),
onTap: () => AppRoute(
const LocalStoragePage(),
'sftp local page',
).go(context),
),
ListTile(
leading: const Icon(Icons.import_export),
title: Text(_s.backup),
onTap: () => AppRoute(
BackupPage(),
'backup page',
).go(context),
),
ListTile(
leading: const Icon(Icons.code),
title: Text(_s.convert),
onTap: () => AppRoute(
const ConvertPage(),
'convert page',
).go(context),
),
ListTile(
leading: const Icon(Icons.text_snippet),
title: Text('${_s.about} & ${_s.feedback}'),
onTap: _showAboutDialog,
)
].map((e) => RoundRectCard(e)).toList(),
),
);
}
void _showAboutDialog() {
showRoundDialog(
context: context,
title: Text(_s.about),
child: _buildAboutContent(),
actions: [
TextButton(
onPressed: () => openUrl(appHelpUrl),
child: Text(_s.feedback),
),
TextButton(
onPressed: () => showLicensePage(context: context),
child: Text(_s.license),
),
],
);
}
Widget _buildAboutContent() {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UrlText(
text: _s.madeWithLove(myGithub),
replace: 'lollipopkit',
),
height13,
// Use [UrlText] for same text style
Text(_s.aboutThanks),
height13,
const Text('Contributors:'),
...contributors.map(
(name) => UrlText(
text: name.url,
replace: name,
),
),
height13,
const Text('Participants:'),
...participants.map(
(name) => UrlText(
text: name.url,
replace: name,
),
)
],
),
);
@@ -307,7 +357,9 @@ class _HomePageState extends State<HomePage>
@override
Future<void> afterFirstLayout(BuildContext context) async {
doUpdate(context);
if (_setting.autoCheckAppUpdate.fetch()!) {
doUpdate(context);
}
updateHomeWidget();
await GetIt.I.allReady();
await _serverProvider.loadLocalData();

View File

@@ -48,6 +48,13 @@ class _PingPageState extends State<PingPage>
_s = S.of(context)!;
}
@override
void dispose() {
super.dispose();
_textEditingController.dispose();
_results.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
@@ -68,6 +75,7 @@ class _PingPageState extends State<PingPage>
context: context,
title: Text(_s.choose),
child: Input(
autoFocus: true,
controller: _textEditingController,
hint: _s.inputDomainHere,
maxLines: 1,

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:nil/nil.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/view/widget/input_field.dart';
@@ -13,11 +12,12 @@ import '../../data/provider/pkg.dart';
import '../../data/provider/server.dart';
import '../../data/res/ui.dart';
import '../../locator.dart';
import '../widget/custom_appbar.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);
class PkgPage extends StatefulWidget {
const PkgPage({Key? key, required this.spi}) : super(key: key);
final ServerPrivateInfo spi;
@@ -25,7 +25,7 @@ class PkgManagePage extends StatefulWidget {
_PkgManagePageState createState() => _PkgManagePageState();
}
class _PkgManagePageState extends State<PkgManagePage>
class _PkgManagePageState extends State<PkgPage>
with SingleTickerProviderStateMixin {
late MediaQueryData _media;
final _scrollController = ScrollController();
@@ -45,18 +45,17 @@ class _PkgManagePageState extends State<PkgManagePage>
void dispose() {
super.dispose();
_pkgProvider.clear();
_textController.dispose();
_scrollController.dispose();
_scrollControllerUpdate.dispose();
}
@override
void initState() {
super.initState();
final si = locator<ServerProvider>().servers[widget.spi.id];
if (si == null || si.client == null) {
showSnackBar(context, Text(_s.waitConnection));
context.pop();
return;
}
if (si == null) return;
_pkgProvider.init(
si.client!,
si.status.sysVer.dist,
@@ -74,7 +73,7 @@ class _PkgManagePageState extends State<PkgManagePage>
Widget build(BuildContext context) {
return Consumer<PkgProvider>(builder: (_, pkg, __) {
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
centerTitle: true,
title: TwoLineText(up: _s.pkg, down: widget.spi.name),
),
@@ -108,6 +107,7 @@ class _PkgManagePageState extends State<PkgManagePage>
context: context,
title: Text(widget.spi.user),
child: Input(
autoFocus: true,
controller: _textController,
type: TextInputType.visiblePassword,
obscureText: true,
@@ -123,19 +123,16 @@ class _PkgManagePageState extends State<PkgManagePage>
child: Text(_s.cancel)),
TextButton(
onPressed: () => onSubmitted(),
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
child: Text(_s.ok, style: textRed),
),
],
);
return _textController.text.trim();
}
Widget _buildFAB(PkgProvider pkg) {
if (pkg.isBusy || (pkg.upgradeable?.isEmpty ?? true)) {
return nil;
Widget? _buildFAB(PkgProvider pkg) {
if (pkg.upgradeable?.isEmpty ?? true) {
return null;
}
return FloatingActionButton(
onPressed: () {

View File

@@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:nil/nil.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/utils/misc.dart';
@@ -18,13 +17,14 @@ import '../../../data/model/server/private_key_info.dart';
import '../../../data/provider/private_key.dart';
import '../../../data/res/ui.dart';
import '../../../locator.dart';
import '../../widget/custom_appbar.dart';
const _format = 'text/plain';
class PrivateKeyEditPage extends StatefulWidget {
const PrivateKeyEditPage({Key? key, this.info}) : super(key: key);
const PrivateKeyEditPage({Key? key, this.pki}) : super(key: key);
final PrivateKeyInfo? info;
final PrivateKeyInfo? pki;
@override
_PrivateKeyEditPageState createState() => _PrivateKeyEditPageState();
@@ -43,7 +43,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
late PrivateKeyProvider _provider;
late S _s;
Widget _loading = nil;
Widget? _loading;
@override
void initState() {
@@ -51,6 +51,17 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
_provider = locator<PrivateKeyProvider>();
}
@override
void dispose() {
super.dispose();
_nameController.dispose();
_keyController.dispose();
_pwdController.dispose();
_nameNode.dispose();
_keyNode.dispose();
_pwdNode.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -68,20 +79,35 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
}
PreferredSizeWidget _buildAppBar() {
final actions = widget.info == null
? null
: [
IconButton(
tooltip: _s.delete,
final actions = [
IconButton(
tooltip: _s.delete,
onPressed: () {
showRoundDialog(
context: context,
title: Text(_s.attention),
child: Text(_s.sureDelete(widget.pki!.id)),
actions: [
TextButton(
onPressed: () {
_provider.delInfo(widget.info!);
_provider.delete(widget.pki!);
context.pop();
context.pop();
},
icon: const Icon(Icons.delete))
];
return AppBar(
child: Text(
_s.ok,
style: textRed,
),
),
],
);
},
icon: const Icon(Icons.delete),
)
];
return CustomAppBar(
title: Text(_s.edit, style: textSize18),
actions: actions,
actions: widget.pki == null ? null : actions,
);
}
@@ -100,22 +126,22 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
setState(() {
_loading = centerSizedLoading;
});
final info = PrivateKeyInfo(name, key, '');
try {
info.privateKey = await compute(decyptPem, [key, pwd]);
final decrypted = await compute(decyptPem, [key, pwd]);
final pki = PrivateKeyInfo(id: name, key: decrypted);
if (widget.pki != null) {
_provider.update(widget.pki!, pki);
} else {
_provider.add(pki);
}
} catch (e) {
showSnackBar(context, Text(e.toString()));
rethrow;
} finally {
setState(() {
_loading = nil;
_loading = null;
});
}
if (widget.info != null) {
_provider.updateInfo(widget.info!, info);
} else {
_provider.addInfo(info);
}
context.pop();
},
child: const Icon(Icons.save),
@@ -127,6 +153,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
padding: const EdgeInsets.all(13),
children: [
Input(
autoFocus: true,
controller: _nameController,
type: TextInputType.text,
node: _nameNode,
@@ -185,17 +212,16 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
icon: Icons.password,
),
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
_loading
_loading ?? placeholder,
],
);
}
@override
Future<void> afterFirstLayout(BuildContext context) async {
if (widget.info != null) {
_nameController.text = widget.info!.id;
_keyController.text = widget.info!.privateKey;
_pwdController.text = widget.info!.password;
if (widget.pki != null) {
_nameController.text = widget.pki!.id;
_keyController.text = widget.pki!.key;
} else {
final clipdata = ((await Clipboard.getData(_format))?.text ?? '').trim();
if (clipdata.startsWith('-----BEGIN') && clipdata.endsWith('-----')) {

View File

@@ -1,10 +1,20 @@
import 'dart:io';
import 'dart:async';
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/extension/navigator.dart';
import 'package:toolbox/core/utils/ui.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/locator.dart';
import '../../../core/route.dart';
import '../../../core/utils/platform.dart';
import '../../../data/model/server/private_key_info.dart';
import '../../../data/provider/private_key.dart';
import '../../../data/res/ui.dart';
import '../../widget/custom_appbar.dart';
import 'edit.dart';
import '../../../view/widget/round_rect_card.dart';
@@ -15,7 +25,8 @@ class PrivateKeysListPage extends StatefulWidget {
_PrivateKeyListState createState() => _PrivateKeyListState();
}
class _PrivateKeyListState extends State<PrivateKeysListPage> {
class _PrivateKeyListState extends State<PrivateKeysListPage>
with AfterLayoutMixin {
late S _s;
@override
@@ -27,7 +38,7 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
title: Text(_s.privateKey, style: textSize18),
),
body: _buildBody(),
@@ -44,21 +55,21 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> {
Widget _buildBody() {
return Consumer<PrivateKeyProvider>(
builder: (_, key, __) {
if (key.infos.isEmpty) {
if (key.pkis.isEmpty) {
return Center(
child: Text(_s.noSavedPrivateKey),
);
}
return ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: key.infos.length,
itemCount: key.pkis.length,
itemBuilder: (context, idx) {
return RoundRectCard(
ListTile(
title: Text(key.infos[idx].id),
title: Text(key.pkis[idx].id),
trailing: TextButton(
onPressed: () => AppRoute(
PrivateKeyEditPage(info: key.infos[idx]),
PrivateKeyEditPage(pki: key.pkis[idx]),
'private key edit page',
).go(context),
child: Text(_s.edit),
@@ -70,4 +81,45 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> {
},
);
}
void autoAddSystemPriavteKey() {
final store = locator<PrivateKeyStore>();
// Only trigger on desktop platform and no private key saved
if (isDesktop && store.box.keys.isEmpty) {
final home = getHomeDir();
if (home == null) return;
final idRsaFile = File(pathJoin(home, '.ssh/id_rsa'));
if (!idRsaFile.existsSync()) return;
final sysPk = PrivateKeyInfo(
id: 'system',
key: idRsaFile.readAsStringSync(),
);
showRoundDialog(
context: context,
title: Text(_s.attention),
child: Text(_s.addSystemPrivateKeyTip),
actions: [
TextButton(
onPressed: () {
context.pop();
AppRoute(
PrivateKeyEditPage(pki: sysPk),
'private key edit page',
).go(context);
},
child: Text(_s.ok),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(_s.cancel),
),
],
);
}
}
@override
FutureOr<void> afterFirstLayout(BuildContext context) {
autoAddSystemPriavteKey();
}
}

View File

@@ -1,7 +1,9 @@
import 'dart:async';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/utils/ui.dart';
@@ -13,6 +15,7 @@ import 'package:toolbox/view/widget/two_line_text.dart';
import '../../data/provider/server.dart';
import '../../locator.dart';
import '../widget/custom_appbar.dart';
class ProcessPage extends StatefulWidget {
final ServerPrivateInfo spi;
@@ -25,6 +28,9 @@ class ProcessPage extends StatefulWidget {
class _ProcessPageState extends State<ProcessPage> {
late S _s;
late Timer _timer;
late MediaQueryData _media;
SSHClient? _client;
PsResult _result = PsResult(procs: []);
int? _lastFocusId;
@@ -35,36 +41,35 @@ class _ProcessPageState extends State<ProcessPage> {
@override
void initState() {
super.initState();
final client = _serverProvider.servers[widget.spi.id]?.client;
if (client == null) {
showSnackBar(context, Text(_s.noClient));
return;
}
_timer = Timer.periodic(const Duration(seconds: 3), (_) async {
if (mounted) {
final result = await client.run('ps -aux'.withLangExport).string;
if (result.isEmpty) {
showSnackBar(context, Text(_s.noResult));
return;
}
_result = PsResult.parse(result, sort: _procSortMode);
setState(() {});
} else {
_timer.cancel();
}
});
}
@override
void dispose() {
super.dispose();
_timer.cancel();
_client = _serverProvider.servers[widget.spi.id]?.client;
_timer = Timer.periodic(const Duration(seconds: 3), (_) => _refresh());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context)!;
_media = MediaQuery.of(context);
}
Future<void> _refresh() async {
if (mounted) {
final result = await _client?.run('ps -aux'.withLangExport).string;
if (result == null || result.isEmpty) {
showSnackBar(context, Text(_s.noResult));
return;
}
_result = PsResult.parse(result, sort: _procSortMode);
setState(() {});
} else {
_timer.cancel();
}
}
@override
void dispose() {
super.dispose();
_timer.cancel();
}
@override
@@ -104,12 +109,23 @@ class _ProcessPageState extends State<ProcessPage> {
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
itemBuilder: (ctx, idx) {
final proc = _result.procs[idx];
return _buildListItem(proc);
return AnimatedSwitcher(
duration: const Duration(milliseconds: 277),
switchInCurve: Curves.easeIn,
switchOutCurve: Curves.easeOut,
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
child: _buildListItem(proc),
);
},
);
}
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
centerTitle: true,
title: TwoLineText(up: widget.spi.name, down: _s.process),
actions: actions,
@@ -119,28 +135,49 @@ class _ProcessPageState extends State<ProcessPage> {
}
Widget _buildListItem(Proc proc) {
return RoundRectCard(ListTile(
leading: SizedBox(
width: 57,
child: TwoLineText(up: proc.pid.toString(), down: proc.user),
return RoundRectCard(
ListTile(
leading: SizedBox(
width: _media.size.width / 6,
child: TwoLineText(up: proc.pid.toString(), down: proc.user),
),
title: Text(proc.binary),
subtitle: Text(
proc.command,
style: grey,
maxLines: 3,
overflow: TextOverflow.fade,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
TwoLineText(up: proc.cpu.toStringAsFixed(1), down: 'cpu'),
width13,
TwoLineText(up: proc.mem.toStringAsFixed(1), down: 'mem'),
],
),
onTap: () => _lastFocusId = proc.pid,
onLongPress: () {
showRoundDialog(
context: context,
title: Text(_s.attention),
child: Text(_s.sureStop(proc.pid)),
actions: [
TextButton(
onPressed: () async {
await _client?.run('kill ${proc.pid}');
await _refresh();
context.pop();
},
child: Text(_s.ok),
),
],
);
},
selected: _lastFocusId == proc.pid,
autofocus: _lastFocusId == proc.pid,
),
title: Text(proc.binary),
subtitle: Text(
proc.command,
style: grey,
maxLines: 3,
overflow: TextOverflow.fade,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
TwoLineText(up: proc.cpu.toStringAsFixed(1), down: 'cpu'),
width13,
TwoLineText(up: proc.mem.toStringAsFixed(1), down: 'mem'),
],
),
onTap: () => _lastFocusId = proc.pid,
autofocus: _lastFocusId == proc.pid,
));
key: ValueKey(proc.pid),
);
}
}

View File

@@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:nil/nil.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/extension/order.dart';
import 'package:toolbox/data/model/server/cpu.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/view/widget/server_func_btns.dart';
import '../../../core/extension/numx.dart';
import '../../../core/route.dart';
import '../../../data/model/server/net_speed.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_status.dart';
@@ -15,12 +18,13 @@ import '../../../data/res/default.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/round_rect_card.dart';
class ServerDetailPage extends StatefulWidget {
const ServerDetailPage(this.id, {Key? key}) : super(key: key);
const ServerDetailPage({Key? key, required this.spi}) : super(key: key);
final String id;
final ServerPrivateInfo spi;
@override
_ServerDetailPageState createState() => _ServerDetailPageState();
@@ -62,7 +66,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
@override
Widget build(BuildContext context) {
return Consumer<ServerProvider>(builder: (_, provider, __) {
final s = provider.servers[widget.id];
final s = provider.servers[widget.spi.id];
if (s == null) {
return Scaffold(
body: Center(
@@ -75,37 +79,42 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildMainPage(Server si) {
final buildFuncs = !_setting.moveOutServerTabFuncBtns.fetch()!;
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
title: Text(si.spi.name, style: textSize18),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () async {
final delete = await AppRoute.serverEdit(spi: si.spi).go(context);
if (delete == true) {
context.pop();
}
},
)
],
),
body: ReorderableListView.builder(
body: ListView.builder(
padding: EdgeInsets.only(
left: 13, right: 13, top: 13, bottom: _media.padding.bottom),
onReorder: (int oldIndex, int newIndex) {
setState(() {
_cardsOrder.move(
oldIndex,
newIndex,
property: _setting.detailCardOrder,
);
});
},
footer: height13,
itemCount: _cardsOrder.length,
buildDefaultDragHandles: false,
itemBuilder: (context, index) => ReorderableDelayedDragStartListener(
key: ValueKey(index),
index: index,
child: SizedBox(
child: _cardBuildMap[_cardsOrder[index]]?.call(si.status),
),
left: 13,
right: 13,
bottom: _media.padding.bottom + 77,
),
itemCount: buildFuncs ? _cardsOrder.length + 1 : _cardsOrder.length,
itemBuilder: (context, index) {
if (index == 0 && buildFuncs) {
return ServerFuncBtns(spi: widget.spi, s: _s, iconSize: 19);
}
if (buildFuncs) index--;
return _cardBuildMap[_cardsOrder[index]]?.call(si.status);
},
),
);
}
Widget _buildCPUView(ServerStatus ss) {
final percent = ss.cpu.usedPercent(coreIdx: 0).toInt();
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
@@ -113,10 +122,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${ss.cpu.usedPercent(coreIdx: 0).toInt()}%',
style: textSize27,
textScaleFactor: 1.0,
_buildAnimatedText(
ValueKey(percent),
'$percent%',
textSize27,
),
Row(
children: [
@@ -206,6 +215,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
final free = ss.mem.free / ss.mem.total * 100;
final avail = ss.mem.availPercent * 100;
final used = ss.mem.usedPercent * 100;
final usedStr = used.toStringAsFixed(0);
return RoundRectCard(
Padding(
@@ -219,7 +229,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
children: [
Row(
children: [
Text('${used.toStringAsFixed(0)}%', style: textSize27),
_buildAnimatedText(
ValueKey(usedStr),
'$usedStr%',
textSize27,
),
width7,
Text('of ${(ss.mem.total * 1024).convertBytes}',
style: textSize13Grey)
@@ -243,7 +257,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildSwapView(ServerStatus ss) {
if (ss.swap.total == 0) return nil;
if (ss.swap.total == 0) return placeholder;
final used = ss.swap.usedPercent * 100;
final cached = ss.swap.cached / ss.swap.total * 100;
return RoundRectCard(
@@ -367,7 +381,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: width / 1.2,
width: width,
child: Text(
device,
style: textSize11,
@@ -402,7 +416,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildTemperature(ServerStatus ss) {
final temps = ss.temps;
if (temps.isEmpty) {
return nil;
return placeholder;
}
final List<Widget> children = [
const Row(
@@ -432,9 +446,27 @@ class _ServerDetailPageState extends State<ServerDetailPage>
),
],
)));
return RoundRectCard(Padding(
padding: roundRectCardPadding,
child: Column(children: children),
));
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Column(children: children),
),
);
}
Widget _buildAnimatedText(Key key, String text, TextStyle style) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 277),
child: Text(
key: key,
text,
style: style,
textScaleFactor: 1.0,
),
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
);
}
}

View File

@@ -15,7 +15,8 @@ import '../../../data/provider/server.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/private_key.dart';
import '../../../locator.dart';
import '../../widget/tag/editor.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/tag.dart';
import '../private_key/edit.dart';
class ServerEditPage extends StatefulWidget {
@@ -55,6 +56,22 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_serverProvider = locator<ServerProvider>();
}
@override
void dispose() {
super.dispose();
_nameController.dispose();
_ipController.dispose();
_alterUrlController.dispose();
_portController.dispose();
_usernameController.dispose();
_passwordController.dispose();
_nameFocus.dispose();
_ipFocus.dispose();
_alterUrlFocus.dispose();
_portFocus.dispose();
_usernameFocus.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -83,24 +100,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
onPressed: () {
_serverProvider.delServer(widget.spi!.id);
context.pop();
context.pop();
context.pop(true);
},
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
child: Text(_s.ok, style: textRed),
),
TextButton(
onPressed: () => context.pop(),
child: Text(_s.cancel),
)
],
);
},
icon: const Icon(Icons.delete),
);
final actions = widget.spi != null ? [delBtn] : null;
return AppBar(
return CustomAppBar(
title: Text(_s.edit, style: textSize18),
actions: actions,
);
@@ -109,6 +119,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
Widget _buildForm() {
final children = [
Input(
autoFocus: true,
controller: _nameController,
type: TextInputType.text,
node: _nameFocus,
@@ -200,17 +211,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
Widget _buildKeyAuth() {
return Consumer<PrivateKeyProvider>(
builder: (_, key, __) {
for (var item in key.infos) {
for (var item in key.pkis) {
if (item.id == widget.spi?.pubKeyId) {
_pubKeyIndex ??= key.infos.indexOf(item);
_pubKeyIndex ??= key.pkis.indexOf(item);
}
}
final tiles = key.infos
final tiles = key.pkis
.map(
(e) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(e.id, textAlign: TextAlign.start),
trailing: _buildRadio(key.infos.indexOf(e), e),
trailing: _buildRadio(key.pkis.indexOf(e), e),
),
)
.toList();

View File

@@ -3,35 +3,26 @@ 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:nil/nil.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/order.dart';
import 'package:toolbox/core/utils/misc.dart';
import 'package:toolbox/data/model/app/net_view.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/view/page/process.dart';
import 'package:toolbox/view/widget/tag/picker.dart';
import 'package:toolbox/view/widget/tag/switcher.dart';
import 'package:toolbox/core/extension/media_queryx.dart';
import '../../../core/route.dart';
import '../../../core/utils/misc.dart' hide pathJoin;
import '../../../core/utils/platform.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/app/net_view.dart';
import '../../../data/model/server/disk.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/res/color.dart';
import '../../../data/model/app/menu.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/popup_menu.dart';
import '../../widget/round_rect_card.dart';
import '../docker.dart';
import '../pkg.dart';
import '../storage/sftp.dart';
import '../ssh/term.dart';
import 'detail.dart';
import '../../widget/server_func_btns.dart';
import '../../widget/tag.dart';
import 'edit.dart';
class ServerPage extends StatefulWidget {
@@ -44,12 +35,12 @@ class ServerPage extends StatefulWidget {
class _ServerPageState extends State<ServerPage>
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late MediaQueryData _media;
late ThemeData _theme;
late ServerProvider _serverProvider;
late SettingStore _settingStore;
late S _s;
String? _tag;
bool _useDoubleColumn = false;
@override
void initState() {
@@ -62,7 +53,7 @@ class _ServerPageState extends State<ServerPage>
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_theme = Theme.of(context);
_useDoubleColumn = _media.useDoubleColumn;
_s = S.of(context)!;
}
@@ -84,72 +75,132 @@ class _ServerPageState extends State<ServerPage>
}
Widget _buildBody() {
final child = Consumer<ServerProvider>(
builder: (_, pro, __) {
if (!pro.tags.contains(_tag)) {
_tag = null;
}
if (pro.serverOrder.isEmpty) {
return Center(
child: Text(
_s.serverTabEmpty,
textAlign: TextAlign.center,
),
);
}
final filtered = _filterServers(pro);
if (_useDoubleColumn) {
return _buildBodyMedium(pro);
}
return _buildBodySmall(provider: pro, filtered: filtered);
},
);
// Desktop doesn't support pull to refresh
if (isDesktop) {
return child;
}
return RefreshIndicator(
onRefresh: () async =>
await _serverProvider.refreshData(onlyFailed: true),
child: Consumer<ServerProvider>(
builder: (_, pro, __) {
if (!pro.tags.contains(_tag)) {
_tag = null;
}
if (pro.serverOrder.isEmpty) {
return Center(
child: Text(
_s.serverTabEmpty,
textAlign: TextAlign.center,
child: child,
);
}
List<String> _filterServers(ServerProvider pro) => pro.serverOrder
.where((e) => pro.servers.containsKey(e))
.where((e) =>
_tag == null || (pro.servers[e]?.spi.tags?.contains(_tag) ?? false))
.toList();
Widget _buildTagsSwitcher(ServerProvider provider) {
return TagSwitcher(
tags: provider.tags,
width: _media.size.width,
onTagChanged: (p0) => setState(() {
_tag = p0;
}),
initTag: _tag,
all: _s.all,
);
}
Widget _buildBodySmall({
required ServerProvider provider,
required List<String> filtered,
EdgeInsets? padding = const EdgeInsets.fromLTRB(7, 0, 7, 7),
bool buildTags = true,
}) {
final count = buildTags ? filtered.length + 2 : filtered.length + 1;
return ListView.builder(
padding: padding,
itemCount: count,
itemBuilder: (_, index) {
if (index == 0 && buildTags) return _buildTagsSwitcher(provider);
// Issue #130
if (index == count - 1) return height77;
if (buildTags) index--;
return _buildEachServerCard(provider.servers[filtered[index]]);
},
);
}
Widget _buildBodyMedium(ServerProvider pro) {
final filtered = _filterServers(pro);
final left = filtered.where((e) => filtered.indexOf(e) % 2 == 0).toList();
final right = filtered.where((e) => filtered.indexOf(e) % 2 == 1).toList();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 7),
child: _buildTagsSwitcher(pro),
),
Expanded(
child: Row(
children: [
Expanded(
child: _buildBodySmall(
provider: pro,
filtered: left,
padding: const EdgeInsets.fromLTRB(7, 0, 0, 7),
buildTags: false,
),
);
}
final filtered = pro.serverOrder
.where((e) => pro.servers.containsKey(e))
.where((e) =>
_tag == null ||
(pro.servers[e]?.spi.tags?.contains(_tag) ?? false))
.toList();
return ReorderableListView.builder(
header: TagSwitcher(
tags: pro.tags,
width: _media.size.width,
onTagChanged: (p0) => setState(() {
_tag = p0;
}),
initTag: _tag,
all: _s.all,
),
padding: const EdgeInsets.fromLTRB(7, 10, 7, 7),
onReorder: (oldIndex, newIndex) => setState(() {
pro.serverOrder.moveByItem(
filtered,
oldIndex,
newIndex,
property: _settingStore.serverOrder,
);
}),
buildDefaultDragHandles: false,
itemBuilder: (_, index) => ReorderableDelayedDragStartListener(
key: ValueKey('$_tag${filtered[index]}'),
index: index,
child: _buildEachServerCard(pro.servers[filtered[index]]),
Expanded(
child: _buildBodySmall(
provider: pro,
filtered: right,
padding: const EdgeInsets.fromLTRB(0, 0, 7, 7),
buildTags: false,
),
),
itemCount: filtered.length,
);
},
),
],
))
],
);
}
Widget _buildEachServerCard(Server? si) {
if (si == null) {
return nil;
return placeholder;
}
return GestureDetector(
return RoundRectCard(
key: Key(si.spi.id + (_tag ?? '')),
onTap: () => AppRoute(
ServerDetailPage(si.spi.id),
'server detail page',
).go(context),
child: RoundRectCard(
Padding(
InkWell(
onTap: () {
if (si.state.canViewDetails) {
AppRoute.serverDetail(spi: si.spi).go(context);
} else if (si.status.failedInfo != null) {
_showFailReason(si.status);
}
},
onLongPress: () => AppRoute.serverEdit(spi: si.spi).go(context),
child: Padding(
padding: const EdgeInsets.all(13),
child: _buildRealServerCard(si.status, si.state, si.spi),
),
@@ -157,40 +208,67 @@ class _ServerPageState extends State<ServerPage>
);
}
Widget _wrapWithSizedbox(Widget child) {
return SizedBox(
width: _useDoubleColumn
? (_media.size.width - 146) / 10
: (_media.size.width - 74) / 5,
child: child,
);
}
Widget _buildRealServerCard(
ServerStatus ss,
ServerState cs,
ServerPrivateInfo spi,
) {
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
final rootDisk = findRootDisk(ss.disk);
late final List<Widget> children;
double? height;
if (cs != ServerState.finished) {
height = 23.0;
children = [
_buildServerCardTitle(ss, cs, spi),
];
} else {
height = 107;
children = [
_buildServerCardTitle(ss, cs, spi),
height13,
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildPercentCircle(ss.cpu.usedPercent()),
_buildPercentCircle(ss.mem.usedPercent * 100),
_buildNet(ss),
_buildIOData(
'Total:\n${rootDisk.size}', 'Used:\n${rootDisk.usedPercent}%')
],
Padding(
padding: const EdgeInsets.symmetric(horizontal: 13),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_wrapWithSizedbox(_buildPercentCircle(ss.cpu.usedPercent())),
_wrapWithSizedbox(_buildPercentCircle(ss.mem.usedPercent * 100)),
_wrapWithSizedbox(_buildNet(ss)),
_wrapWithSizedbox(_buildIOData(
'Total:\n${rootDisk?.size}',
'Used:\n${rootDisk?.usedPercent}%',
)),
],
),
),
height13,
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildExplainText('CPU'),
_buildExplainText('Mem'),
_buildExplainText('Net'),
_buildExplainText('Disk'),
],
),
const SizedBox(height: 3),
],
if (_settingStore.moveOutServerTabFuncBtns.fetch()!)
SizedBox(
height: 27,
child: ServerFuncBtns(spi: spi, s: _s),
),
];
}
return AnimatedContainer(
duration: const Duration(milliseconds: 377),
curve: Curves.fastEaseInToSlowEaseOut,
height: height,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
),
);
}
@@ -208,8 +286,7 @@ class _ServerPageState extends State<ServerPage>
children: [
Text(
spi.name,
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
style: textSize13Bold,
textScaleFactor: 1.0,
),
const Icon(
@@ -219,14 +296,7 @@ class _ServerPageState extends State<ServerPage>
)
],
),
Row(
children: [
_buildTopRightText(ss, cs),
width7,
_buildSSHBtn(spi),
_buildMoreBtn(spi),
],
)
_buildTopRightText(ss, cs),
],
),
);
@@ -239,95 +309,36 @@ class _ServerPageState extends State<ServerPage>
ss.uptime,
ss.failedInfo,
);
final hasError = cs == ServerState.failed && ss.failedInfo != null;
return hasError
? GestureDetector(
onTap: () => showRoundDialog(
context: context,
title: Text(_s.error),
child: Text(ss.failedInfo ?? _s.unknownError),
actions: [
TextButton(
onPressed: () =>
copy2Clipboard(ss.failedInfo ?? _s.unknownError),
child: Text(_s.copy),
)
],
),
child: Text(
_s.viewErr,
style: textSize12Grey,
textScaleFactor: 1.0,
),
)
: Text(
topRightStr,
style: textSize12Grey,
textScaleFactor: 1.0,
);
}
Widget _buildSSHBtn(ServerPrivateInfo spi) {
return GestureDetector(
child: const Icon(
Icons.terminal,
size: 21,
),
onTap: () => AppRoute(SSHPage(spi: spi), 'ssh page').go(context),
if (cs == ServerState.failed && ss.failedInfo != null) {
return GestureDetector(
onTap: () => _showFailReason(ss),
child: Text(
_s.viewErr,
style: textSize11Grey,
textScaleFactor: 1.0,
),
);
}
return Text(
topRightStr,
style: textSize11Grey,
textScaleFactor: 1.0,
);
}
Widget _buildMoreBtn(ServerPrivateInfo spi) {
return PopupMenu(
items: ServerTabMenuType.values.map((e) => e.build(_s)).toList(),
onSelected: (ServerTabMenuType value) async {
switch (value) {
case ServerTabMenuType.pkg:
AppRoute(PkgManagePage(spi), 'pkg manage').go(context);
break;
case ServerTabMenuType.sftp:
AppRoute(SftpPage(spi), 'SFTP').go(context);
break;
case ServerTabMenuType.snippet:
final provider = locator<SnippetProvider>();
final snippets = await showDialog<List<Snippet>>(
context: context,
builder: (_) => TagPicker<Snippet>(
items: provider.snippets,
containsTag: (t, tag) => t.tags?.contains(tag) ?? false,
tags: provider.tags.toSet(),
name: (t) => t.name,
),
);
if (snippets == null) {
return;
}
final result = await _serverProvider.runSnippets(spi.id, snippets);
if (result != null && result.isNotEmpty) {
showRoundDialog(
context: context,
title: Text(_s.result),
child: Text(result),
actions: [
TextButton(
onPressed: () => copy2Clipboard(result),
child: Text(_s.copy),
)
],
);
}
break;
case ServerTabMenuType.edit:
AppRoute(ServerEditPage(spi: spi), 'Edit server info').go(context);
break;
case ServerTabMenuType.docker:
AppRoute(DockerManagePage(spi), 'Docker manage').go(context);
break;
case ServerTabMenuType.process:
AppRoute(ProcessPage(spi: spi), 'process page').go(context);
break;
}
},
void _showFailReason(ServerStatus ss) {
showRoundDialog(
context: context,
title: Text(_s.error),
child: SingleChildScrollView(
child: Text(ss.failedInfo ?? _s.unknownError),
),
actions: [
TextButton(
onPressed: () => copy2Clipboard(ss.failedInfo!),
child: Text(_s.copy),
)
],
);
}
@@ -344,18 +355,6 @@ class _ServerPageState extends State<ServerPage>
);
}
Widget _buildExplainText(String text) {
return SizedBox(
width: _media.size.width * 0.2,
child: Text(
text,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
textScaleFactor: 1.0,
),
);
}
String _getTopRightStr(
ServerState cs,
double? temp,
@@ -365,12 +364,16 @@ class _ServerPageState extends State<ServerPage>
switch (cs) {
case ServerState.disconnected:
return _s.disconnected;
case ServerState.connected:
case ServerState.finished:
final tempStr = temp == null ? '' : '${temp.toStringAsFixed(1)}°C';
final items = [tempStr, upTime];
final str = items.where((element) => element.isNotEmpty).join(' | ');
if (str.isEmpty) return _s.serverTabLoading;
if (str.isEmpty) return _s.noResult;
return str;
case ServerState.loading:
return _s.serverTabLoading;
case ServerState.connected:
return _s.connected;
case ServerState.connecting:
return _s.serverTabConnecting;
case ServerState.failed:
@@ -381,65 +384,56 @@ class _ServerPageState extends State<ServerPage>
return _s.serverTabPlzSave;
}
return failedInfo;
default:
return _s.serverTabUnkown;
}
}
Widget _buildIOData(String up, String down) {
final statusTextStyle = TextStyle(
fontSize: 9, color: _theme.textTheme.bodyLarge!.color!.withAlpha(177));
return SizedBox(
width: _media.size.width * 0.2,
child: Column(
children: [
const SizedBox(height: 5),
Text(
up,
style: statusTextStyle,
textAlign: TextAlign.center,
textScaleFactor: 1.0,
),
const SizedBox(height: 3),
Text(
down,
style: statusTextStyle,
textAlign: TextAlign.center,
textScaleFactor: 1.0,
)
],
),
return Column(
children: [
const SizedBox(height: 5),
Text(
up,
style: textSize9Grey,
textAlign: TextAlign.center,
textScaleFactor: 1.0,
),
const SizedBox(height: 3),
Text(
down,
style: textSize9Grey,
textAlign: TextAlign.center,
textScaleFactor: 1.0,
)
],
);
}
Widget _buildPercentCircle(double percent) {
if (percent <= 0) percent = 0.01;
if (percent >= 100) percent = 99.9;
return SizedBox(
width: _media.size.width * 0.2,
child: Stack(
children: [
Center(
child: CircleChart(
progressColor: primaryColor,
progressNumber: percent,
maxNumber: 100,
width: 53,
height: 53,
return Stack(
children: [
Center(
child: CircleChart(
progressColor: primaryColor,
progressNumber: percent,
maxNumber: 100,
width: 53,
height: 53,
animationDuration: const Duration(milliseconds: 777),
),
),
Positioned.fill(
child: Center(
child: Text(
'${percent.toStringAsFixed(1)}%',
textAlign: TextAlign.center,
style: textSize11,
textScaleFactor: 1.0,
),
),
Positioned.fill(
child: Center(
child: Text(
'${percent.toStringAsFixed(1)}%',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 11),
textScaleFactor: 1.0,
),
),
),
],
),
),
],
);
}

View File

@@ -4,33 +4,35 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_highlight/theme_map.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:toolbox/core/extension/colorx.dart';
import 'package:toolbox/core/extension/locale.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/persistant_store.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/app/net_view.dart';
import 'package:toolbox/data/model/app/tab.dart';
import 'package:toolbox/view/page/ssh/virt_key_setting.dart';
import 'package:toolbox/view/page/setting/virt_key.dart';
import 'package:toolbox/view/widget/input_field.dart';
import 'package:toolbox/view/widget/value_notifier.dart';
import '../../core/utils/misc.dart';
import '../../core/utils/platform.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/path.dart';
import '../../data/res/ui.dart';
import '../../data/store/server.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
import '../widget/future_widget.dart';
import '../widget/round_rect_card.dart';
import '../../../core/utils/misc.dart';
import '../../../core/utils/platform.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/path.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/server.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/future_widget.dart';
import '../../widget/round_rect_card.dart';
class SettingPage extends StatefulWidget {
const SettingPage({Key? key}) : super(key: key);
@@ -41,7 +43,7 @@ class SettingPage extends StatefulWidget {
class _SettingPageState extends State<SettingPage> {
final _themeKey = GlobalKey<PopupMenuButtonState<int>>();
final _startPageKey = GlobalKey<PopupMenuButtonState<int>>();
//final _startPageKey = GlobalKey<PopupMenuButtonState<int>>();
final _updateIntervalKey = GlobalKey<PopupMenuButtonState<int>>();
final _maxRetryKey = GlobalKey<PopupMenuButtonState<int>>();
final _localeKey = GlobalKey<PopupMenuButtonState<String>>();
@@ -53,7 +55,6 @@ class _SettingPageState extends State<SettingPage> {
late final SettingStore _setting;
late final ServerProvider _serverProvider;
late MediaQueryData _media;
late S _s;
late SharedPreferences _sp;
@@ -62,7 +63,8 @@ class _SettingPageState extends State<SettingPage> {
final _nightMode = ValueNotifier(0);
final _maxRetryCount = ValueNotifier(0);
final _updateInterval = ValueNotifier(0);
final _fontSize = ValueNotifier(0.0);
final _termFontSize = ValueNotifier(0.0);
final _editorFontSize = ValueNotifier(0.0);
final _localeCode = ValueNotifier('');
final _editorTheme = ValueNotifier('');
final _editorDarkTheme = ValueNotifier('');
@@ -75,7 +77,6 @@ class _SettingPageState extends State<SettingPage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_s = S.of(context)!;
_localeCode.value = _setting.locale.fetch() ?? _s.localeName;
}
@@ -90,7 +91,8 @@ class _SettingPageState extends State<SettingPage> {
_updateInterval.value = _setting.serverStatusUpdateInterval.fetch()!;
_maxRetryCount.value = _setting.maxRetryCount.fetch()!;
_selectedColorValue.value = _setting.primaryColor.fetch()!;
_fontSize.value = _setting.termFontSize.fetch()!;
_termFontSize.value = _setting.termFontSize.fetch()!;
_editorFontSize.value = _setting.editorFontSize.fetch()!;
_editorTheme.value = _setting.editorTheme.fetch()!;
_editorDarkTheme.value = _setting.editorDarkTheme.fetch()!;
_keyboardType.value = _setting.keyboardType.fetch()!;
@@ -102,7 +104,7 @@ class _SettingPageState extends State<SettingPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
title: Text(_s.setting),
),
body: ListView(
@@ -141,7 +143,7 @@ class _SettingPageState extends State<SettingPage> {
_buildLocale(),
_buildThemeMode(),
_buildAppColor(),
_buildLaunchPage(),
//_buildLaunchPage(),
_buildCheckUpdate(),
];
if (isIOS) {
@@ -170,6 +172,9 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildServer() {
return Column(
children: [
_buildMoveOutServerFuncBtns(),
_buildServerOrder(),
_buildServerDetailOrder(),
_buildNetViewType(),
_buildUpdateInterval(),
_buildMaxRetry(),
@@ -194,6 +199,7 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildEditor() {
return Column(
children: [
_buildEditorFontSize(),
_buildEditorTheme(),
_buildEditorDarkTheme(),
].map((e) => RoundRectCard(e)).toList(),
@@ -214,11 +220,10 @@ class _SettingPageState extends State<SettingPage> {
display = _s.versionUnknownUpdate(BuildData.build);
}
return ListTile(
trailing: const Icon(Icons.keyboard_arrow_right),
title: Text(
display,
),
title: Text(_s.autoCheckUpdate),
subtitle: Text(display, style: grey),
onTap: () => doUpdate(ctx, force: true),
trailing: buildSwitch(context, _setting.autoCheckAppUpdate),
);
},
);
@@ -277,81 +282,76 @@ class _SettingPageState extends State<SettingPage> {
width: 27,
),
),
title: Text(
_s.primaryColor,
),
title: Text(_s.primaryColorSeed),
onTap: () async {
final ctrl = TextEditingController(text: primaryColor.toHex);
await showRoundDialog(
context: context,
title: Text(_s.primaryColor),
child: SizedBox(
height: 211,
child: Center(
child: MaterialColorPicker(
shrinkWrap: true,
allowShades: true,
onColorChange: (color) {
_selectedColorValue.value = color.value;
},
selectedColor: primaryColor,
),
),
title: Text(_s.primaryColorSeed),
child: Input(
autoFocus: true,
onSubmitted: _onSaveColor,
controller: ctrl,
hint: '#8b2252',
icon: Icons.colorize,
),
actions: [
TextButton(
onPressed: () {
_setting.primaryColor.put(_selectedColorValue.value);
Navigator.pop(context);
_showRestartSnackbar();
},
child: Text(_s.ok),
)
],
);
},
);
}
Widget _buildLaunchPage() {
final items = AppTab.values
.map(
(e) => PopupMenuItem(
value: e.index,
child: Text(tabTitleName(context, e)),
),
)
.toList();
return ListTile(
title: Text(
_s.launchPage,
),
onTap: () {
_startPageKey.currentState?.showButtonMenu();
},
trailing: ValueBuilder(
listenable: _launchPageIdx,
build: () => PopupMenuButton(
key: _startPageKey,
itemBuilder: (BuildContext context) => items,
initialValue: _launchPageIdx.value,
onSelected: (int idx) {
_launchPageIdx.value = idx;
_setting.launchPage.put(_launchPageIdx.value);
},
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: _media.size.width * 0.35),
child: Text(
tabTitleName(context, AppTab.values[_launchPageIdx.value]),
textAlign: TextAlign.right,
style: textSize15,
),
),
),
),
);
void _onSaveColor(String s) {
final color = s.hexToColor;
if (color == null) {
showSnackBar(context, Text(_s.failed));
return;
}
_selectedColorValue.value = color.value;
_setting.primaryColor.put(_selectedColorValue.value);
context.pop();
_showRestartSnackbar();
}
// Widget _buildLaunchPage() {
// final items = AppTab.values
// .map(
// (e) => PopupMenuItem(
// value: e.index,
// child: Text(tabTitleName(context, e)),
// ),
// )
// .toList();
// return ListTile(
// title: Text(
// _s.launchPage,
// ),
// onTap: () {
// _startPageKey.currentState?.showButtonMenu();
// },
// trailing: ValueBuilder(
// listenable: _launchPageIdx,
// build: () => PopupMenuButton(
// key: _startPageKey,
// itemBuilder: (BuildContext context) => items,
// initialValue: _launchPageIdx.value,
// onSelected: (int idx) {
// _launchPageIdx.value = idx;
// _setting.launchPage.put(_launchPageIdx.value);
// },
// child: ConstrainedBox(
// constraints: BoxConstraints(maxWidth: _media.size.width * 0.35),
// child: Text(
// tabTitleName(context, AppTab.values[_launchPageIdx.value]),
// textAlign: TextAlign.right,
// style: textSize15,
// ),
// ),
// ),
// ),
// );
// }
Widget _buildMaxRetry() {
final items = List.generate(
10,
@@ -550,45 +550,14 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildTermFontSize() {
return ValueBuilder(
listenable: _fontSize,
listenable: _termFontSize,
build: () => ListTile(
title: Text(_s.fontSize),
trailing: Text(
_fontSize.value.toString(),
_termFontSize.value.toString(),
style: textSize15,
),
onTap: () {
final ctrller =
TextEditingController(text: _fontSize.value.toString());
showRoundDialog(
context: context,
title: Text(_s.fontSize),
child: Input(
controller: ctrller,
type: TextInputType.number,
icon: Icons.font_download,
),
actions: [
TextButton(
onPressed: () {
context.pop();
final fontSize = double.tryParse(ctrller.text);
if (fontSize == null) {
showRoundDialog(
context: context,
title: Text(_s.failed),
child: Text('Parsed failed: ${ctrller.text}'),
);
return;
}
_fontSize.value = fontSize;
_setting.termFontSize.put(_fontSize.value);
},
child: Text(_s.ok),
),
],
);
},
onTap: () => _showFontSizeDialog(_termFontSize, _setting.termFontSize),
),
);
}
@@ -615,6 +584,7 @@ class _SettingPageState extends State<SettingPage> {
context: context,
title: Text(_s.diskIgnorePath),
child: Input(
autoFocus: true,
controller: ctrller,
label: 'JSON',
type: TextInputType.visiblePassword,
@@ -681,7 +651,7 @@ class _SettingPageState extends State<SettingPage> {
},
).toList();
return ListTile(
title: Text(_s.light + _s.theme),
title: Text('${_s.light} ${_s.theme.toLowerCase()}'),
trailing: ValueBuilder(
listenable: _editorTheme,
build: () => PopupMenuButton(
@@ -714,7 +684,7 @@ class _SettingPageState extends State<SettingPage> {
},
).toList();
return ListTile(
title: Text(_s.dark + _s.theme),
title: Text('${_s.dark} ${_s.theme.toLowerCase()}'),
trailing: ValueBuilder(
listenable: _editorDarkTheme,
build: () => PopupMenuButton(
@@ -885,6 +855,7 @@ class _SettingPageState extends State<SettingPage> {
context: context,
title: Text(_s.homeWidgetUrlConfig),
child: Input(
autoFocus: true,
controller: ctrl,
label: 'JSON',
type: TextInputType.visiblePassword,
@@ -977,4 +948,82 @@ class _SettingPageState extends State<SettingPage> {
},
);
}
Widget _buildMoveOutServerFuncBtns() {
return ListTile(
title: Text(_s.moveOutServerFuncBtns),
subtitle: Text(_s.moveOutServerFuncBtnsHelp, style: textSize13Grey),
trailing: buildSwitch(context, _setting.moveOutServerTabFuncBtns),
);
}
Widget _buildServerOrder() {
return ListTile(
title: Text(_s.serverOrder),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () => AppRoute.serverOrder().go(context),
);
}
Widget _buildServerDetailOrder() {
return ListTile(
title: Text(_s.serverDetailOrder),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () => AppRoute.serverDetailOrder().go(context),
);
}
Widget _buildEditorFontSize() {
return ValueBuilder(
listenable: _editorFontSize,
build: () => ListTile(
title: Text(_s.fontSize),
trailing: Text(
_editorFontSize.value.toString(),
style: textSize15,
),
onTap: () =>
_showFontSizeDialog(_editorFontSize, _setting.editorFontSize),
),
);
}
void _showFontSizeDialog(
ValueNotifier<double> notifier,
StoreProperty property,
) {
final ctrller = TextEditingController(text: notifier.value.toString());
void onSave() {
context.pop();
final fontSize = double.tryParse(ctrller.text);
if (fontSize == null) {
showRoundDialog(
context: context,
title: Text(_s.failed),
child: Text('Parsed failed: ${ctrller.text}'),
);
return;
}
notifier.value = fontSize;
property.put(fontSize);
}
showRoundDialog(
context: context,
title: Text(_s.fontSize),
child: Input(
controller: ctrller,
autoFocus: true,
type: TextInputType.number,
icon: Icons.font_download,
onSubmitted: (_) => onSave(),
),
actions: [
TextButton(
onPressed: onSave,
child: Text(_s.ok),
),
],
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../core/extension/order.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/round_rect_card.dart';
class ServerDetailOrderPage extends StatefulWidget {
const ServerDetailOrderPage({super.key});
@override
State<ServerDetailOrderPage> createState() => _ServerDetailOrderPageState();
}
class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
final _store = locator<SettingStore>();
final Order<String> _cardsOrder = [];
late S _s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context)!;
}
@override
void initState() {
super.initState();
_cardsOrder.addAll(_store.detailCardOrder.fetch()!);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: Text(_s.serverOrder),
),
body: _buildBody(),
);
}
Widget _buildBody() {
return ReorderableListView.builder(
footer: const SizedBox(height: 77),
onReorder: (oldIndex, newIndex) => setState(() {
_cardsOrder.move(
oldIndex,
newIndex,
property: _store.serverOrder,
);
}),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
buildDefaultDragHandles: false,
itemBuilder: (_, index) => _buildItem(index, _cardsOrder[index]),
itemCount: _cardsOrder.length,
);
}
Widget _buildItem(int index, String id) {
return ReorderableDelayedDragStartListener(
key: ValueKey('$index'),
index: index,
child: RoundRectCard(ListTile(
title: Text(id),
trailing: const Icon(Icons.drag_handle),
)),
);
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/order.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../../data/provider/server.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/custom_appbar.dart';
class ServerOrderPage extends StatefulWidget {
const ServerOrderPage({Key? key}) : super(key: key);
@override
_ServerOrderPageState createState() => _ServerOrderPageState();
}
class _ServerOrderPageState extends State<ServerOrderPage> {
final _store = locator<SettingStore>();
final _provider = locator<ServerProvider>();
late S _s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context)!;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: Text(_s.serverOrder),
),
body: _buildBody(),
);
}
Widget _buildBody() {
return ReorderableListView.builder(
footer: const SizedBox(height: 77),
onReorder: (oldIndex, newIndex) => setState(() {
_provider.serverOrder.move(
oldIndex,
newIndex,
property: _store.serverOrder,
);
}),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
buildDefaultDragHandles: false,
itemBuilder: (_, index) =>
_buildItem(index, _provider.serverOrder[index]),
itemCount: _provider.serverOrder.length,
);
}
Widget _buildItem(int index, String id) {
final spi = _provider.servers[id]?.spi;
if (spi == null) {
return const SizedBox();
}
return ReorderableDelayedDragStartListener(
key: ValueKey('$index'),
index: index,
child: RoundRectCard(ListTile(
title: Text(spi.name),
subtitle: Text(spi.id),
leading: CircleAvatar(
child: Text(spi.name[0]),
),
trailing: const Icon(Icons.drag_handle),
)),
);
}
}

View File

@@ -9,6 +9,8 @@ import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../widget/custom_appbar.dart';
class SSHVirtKeySettingPage extends StatefulWidget {
const SSHVirtKeySettingPage({Key? key}) : super(key: key);
@@ -29,7 +31,7 @@ class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
title: Text(_s.editVirtKeys),
),
body: _buildBody(),
@@ -50,13 +52,14 @@ class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {
final key = allKeys[idx];
final help = key.help(_s);
return RoundRectCard(
key: ValueKey(idx),
ListTile(
title: _buildTitle(key),
subtitle: help == null ? null : Text(help, style: grey),
leading: _buildCheckBox(keys, key, idx, idx < keys.length),
trailing: isDesktop ? null : const Icon(Icons.drag_handle),
));
key: ValueKey(idx),
ListTile(
title: _buildTitle(key),
subtitle: help == null ? null : Text(help, style: grey),
leading: _buildCheckBox(keys, key, idx, idx < keys.length),
trailing: isDesktop ? null : const Icon(Icons.drag_handle),
),
);
},
itemCount: allKeys.length,
onReorder: (o, n) {

View File

@@ -9,7 +9,8 @@ import '../../../data/model/server/snippet.dart';
import '../../../data/provider/snippet.dart';
import '../../../data/res/ui.dart';
import '../../../locator.dart';
import '../../widget/tag/editor.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/tag.dart';
class SnippetEditPage extends StatefulWidget {
const SnippetEditPage({Key? key, this.snippet}) : super(key: key);
@@ -37,6 +38,14 @@ class _SnippetEditPageState extends State<SnippetEditPage>
_provider = locator<SnippetProvider>();
}
@override
void dispose() {
super.dispose();
_nameController.dispose();
_scriptController.dispose();
_scriptNode.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -46,7 +55,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
title: Text(_s.edit, style: textSize18),
actions: _buildAppBarActions(),
),
@@ -98,6 +107,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
padding: const EdgeInsets.all(13),
children: [
Input(
autoFocus: true,
controller: _nameController,
type: TextInputType.text,
onSubmitted: (_) => FocusScope.of(context).requestFocus(_scriptNode),

View File

@@ -2,17 +2,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/order.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/ui.dart';
import 'package:toolbox/view/widget/tag/switcher.dart';
import '../../../core/utils/misc.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/snippet.dart';
import '../../../data/provider/server.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/tag/picker.dart';
import '../../widget/tag.dart';
import '/core/route.dart';
import '/data/provider/snippet.dart';
import 'edit.dart';
@@ -69,7 +68,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
.toList();
return ReorderableListView.builder(
padding: const EdgeInsets.all(13),
padding: const EdgeInsets.symmetric(horizontal: 13),
itemCount: filtered.length,
onReorder: (oldIdx, newIdx) => setState(() {
provider.snippets.moveByItem(
@@ -88,6 +87,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
all: _s.all,
width: _media.size.width,
),
footer: height77,
buildDefaultDragHandles: false,
itemBuilder: (context, idx) {
final snippet = filtered.elementAt(idx);

View File

@@ -8,22 +8,22 @@ import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/data/res/server_cmd.dart';
import 'package:xterm/xterm.dart';
import '../../../core/route.dart';
import '../../../core/utils/platform.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/model/ssh/virtual_key.dart';
import '../../../data/provider/virtual_keyboard.dart';
import '../../../data/res/color.dart';
import '../../../data/res/terminal.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../storage/sftp.dart';
import '../../core/route.dart';
import '../../core/utils/platform.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/model/ssh/virtual_key.dart';
import '../../data/provider/virtual_keyboard.dart';
import '../../data/res/color.dart';
import '../../data/res/terminal.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
const echoPWD = 'echo \$PWD';
class SSHPage extends StatefulWidget {
final ServerPrivateInfo spi;
@@ -46,13 +46,14 @@ class _SSHPageState extends State<SSHPage> {
late TerminalStyle _terminalStyle;
late TerminalTheme _terminalTheme;
late TextInputType _keyboardType;
late SSHSession _session;
late double _virtKeyWidth;
late double _virtKeysHeight;
double _virtKeyWidth = 0;
double _virtKeysHeight = 0;
bool _isDark = false;
Timer? _virtKeyLongPressTimer;
SSHClient? _client;
SSHSession? _session;
Timer? _discontinuityTimer;
@override
void initState() {
@@ -68,6 +69,19 @@ class _SSHPageState extends State<SSHPage> {
_initVirtKeys();
}
@override
void dispose() {
super.dispose();
_virtKeyLongPressTimer?.cancel();
_terminalController.dispose();
if (_client?.isClosed == false) {
try {
_client?.close();
} catch (_) {}
}
_discontinuityTimer?.cancel();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -75,15 +89,12 @@ class _SSHPageState extends State<SSHPage> {
_media = MediaQuery.of(context);
_s = S.of(context)!;
_terminalTheme = _isDark ? termDarkTheme : termLightTheme;
// Calculate virtkey width / height
_virtKeyWidth = _media.size.width / 7;
_virtKeysHeight = _media.size.height * 0.043 * _virtKeysList.length;
}
@override
void dispose() {
_client?.close();
super.dispose();
// Because the virtual keyboard only displayed on mobile devices
if (isMobile) {
_virtKeyWidth = _media.size.width / 7;
_virtKeysHeight = _media.size.height * 0.043 * _virtKeysList.length;
}
}
@override
@@ -91,7 +102,7 @@ class _SSHPageState extends State<SSHPage> {
Widget child = Scaffold(
backgroundColor: _terminalTheme.background,
body: _buildBody(),
bottomNavigationBar: _buildBottom(),
bottomNavigationBar: isDesktop ? null : _buildBottom(),
);
if (isIOS) {
child = AnnotatedRegion(
@@ -108,15 +119,19 @@ class _SSHPageState extends State<SSHPage> {
_virtKeysHeight -
_media.padding.bottom -
_media.padding.top,
child: TerminalView(
_terminal,
controller: _terminalController,
keyboardType: _keyboardType,
textStyle: _terminalStyle,
theme: _terminalTheme,
deleteDetection: isIOS,
autofocus: true,
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
child: Padding(
padding: EdgeInsets.only(top: _media.padding.top),
child: TerminalView(
_terminal,
controller: _terminalController,
keyboardType: _keyboardType,
textStyle: _terminalStyle,
theme: _terminalTheme,
deleteDetection: isIOS,
autofocus: true,
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
hideScrollBar: isMobile,
),
),
);
}
@@ -259,13 +274,7 @@ class _SSHPageState extends State<SSHPage> {
);
return;
}
AppRoute(
SftpPage(
widget.spi,
initPath: initPath,
),
'SSH SFTP')
.go(context);
AppRoute.sftp(spi: widget.spi, initPath: initPath).go(context);
}
}
@@ -329,31 +338,83 @@ class _SSHPageState extends State<SSHPage> {
),
);
_setupDiscontinuityTimer();
if (_session == null) {
showSnackBar(context, const Text('Null session'));
return;
}
_terminal.buffer.clear();
_terminal.buffer.setCursor(0, 0);
_terminal.onOutput = (data) {
_session.write(utf8.encode(data) as Uint8List);
_session?.write(utf8.encode(data) as Uint8List);
};
_terminal.onResize = (width, height, pixelWidth, pixelHeight) {
_session?.resizeTerminal(width, height);
};
_listen(_session.stdout);
_listen(_session.stderr);
_listen(_session?.stdout);
_listen(_session?.stderr);
if (widget.initCmd != null) {
_terminal.textInput(widget.initCmd!);
_terminal.keyInput(TerminalKey.enter);
}
await _session.done;
await _session?.done;
if (mounted) {
context.pop();
}
}
void _listen(Stream<Uint8List> stream) {
void _listen(Stream<Uint8List>? stream) {
if (stream == null) {
return;
}
stream
.cast<List<int>>()
.transform(const Utf8Decoder())
.listen(_terminal.write);
}
void _setupDiscontinuityTimer() {
_discontinuityTimer = Timer.periodic(
const Duration(seconds: 5),
(_) async {
var throwTimeout = true;
Future.delayed(const Duration(seconds: 3), () {
if (throwTimeout) {
_catchTimeout();
}
});
await _client?.ping();
throwTimeout = false;
},
);
}
void _catchTimeout() {
_discontinuityTimer?.cancel();
if (!mounted) return;
_write('\n\nConnection lost\r\n');
showRoundDialog(
context: context,
title: Text(_s.attention),
child: Text('${_s.disconnected}\n${_s.goBackQ}'),
barrierDismiss: false,
actions: [
TextButton(
onPressed: () {
if (mounted) {
context.pop();
context.pop();
}
},
child: Text(_s.ok),
),
],
);
}
}

View File

@@ -9,7 +9,6 @@ import 'package:toolbox/data/provider/sftp.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/editor.dart';
import 'package:toolbox/view/page/storage/sftp.dart';
import 'package:toolbox/view/widget/input_field.dart';
import 'package:toolbox/view/widget/picker.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -22,6 +21,7 @@ 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/custom_appbar.dart';
import '../../widget/fade_in.dart';
import 'sftp_mission.dart';
@@ -64,24 +64,32 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
leading: IconButton(
icon: const BackButtonIcon(),
onPressed: () {
if (_path != null) {
_path!.update('/');
}
context.pop();
},
),
title: Text(_s.download),
actions: [
IconButton(
icon: const Icon(Icons.downloading),
onPressed: () =>
AppRoute(const SftpMissionPage(), 'sftp downloading')
.go(context),
onPressed: () => AppRoute(
const SftpMissionPage(),
'sftp downloading',
).go(context),
)
],
),
body: FadeIn(
key: UniqueKey(),
child: _buildBody(),
),
bottomNavigationBar: SafeArea(
child: _buildPath(),
child: _wrapPopScope(),
),
bottomNavigationBar: SafeArea(child: _buildPath()),
);
}
@@ -91,13 +99,53 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Divider(),
(_path?.path ?? _s.loadingFiles).omitStartStr(),
_buildBtns(),
],
),
);
}
Widget _buildBtns() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
onPressed: () {
_path?.update('..');
setState(() {});
},
icon: const Icon(Icons.arrow_back),
),
IconButton(
onPressed: () async {
final path = await pickOneFile();
if (path == null) return;
final name = getFileName(path) ?? 'imported';
await File(path).copy(pathJoin(_path!.path, name));
setState(() {});
},
icon: const Icon(Icons.add),
),
],
);
}
Widget _wrapPopScope() {
return WillPopScope(
onWillPop: () async {
if (_path == null) return true;
if (_path!.canBack) {
_path!.update('..');
setState(() {});
return false;
}
return true;
},
child: _buildBody(),
);
}
Widget _buildBody() {
if (_path == null) {
return const Center(
@@ -106,22 +154,10 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
}
final dir = Directory(_path!.path);
final files = dir.listSync();
final canGoBack = _path!.canBack;
return ListView.builder(
itemCount: canGoBack ? files.length + 1 : files.length,
itemCount: files.length,
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
itemBuilder: (context, index) {
if (index == 0 && canGoBack) {
return RoundRectCard(ListTile(
leading: const Icon(Icons.keyboard_arrow_left),
title: const Text('..'),
onTap: () {
_path!.update('..');
setState(() {});
},
));
}
index = canGoBack ? index - 1 : index;
var file = files[index];
var fileName = file.path.split('/').last;
var stat = file.statSync();
@@ -139,9 +175,13 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
.substring(0, stat.modified.toString().length - 4),
style: grey,
),
onLongPress: () {
if (!isDir) return;
_showDirActionDialog(file);
},
onTap: () async {
if (!isDir) {
await showFileActionDialog(file);
await _showFileActionDialog(file);
return;
}
_path!.update(fileName);
@@ -152,7 +192,34 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
);
}
Future<void> showFileActionDialog(FileSystemEntity file) async {
Future<void> _showDirActionDialog(FileSystemEntity file) async {
showRoundDialog(
context: context,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () {
context.pop();
_showRenameDialog(file);
},
title: Text(_s.rename),
leading: const Icon(Icons.abc),
),
ListTile(
onTap: () {
context.pop();
_showDeleteDialog(file);
},
title: Text(_s.delete),
leading: const Icon(Icons.delete),
),
],
),
);
}
Future<void> _showFileActionDialog(FileSystemEntity file) async {
final fileName = file.path.split('/').last;
if (widget.isPickFile) {
await showRoundDialog(
@@ -189,13 +256,13 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
);
return;
}
final f = File(file.absolute.path);
final result = await AppRoute(
EditorPage(
path: file.absolute.path,
),
'sftp dled editor',
).go<String>(context);
final f = File(file.absolute.path);
if (result != null) {
f.writeAsString(result);
showSnackBar(context, Text(_s.saved));
@@ -208,19 +275,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
title: Text(_s.rename),
onTap: () {
context.pop();
showRoundDialog(
context: context,
title: Text(_s.rename),
child: Input(
controller: TextEditingController(text: fileName),
onSubmitted: (p0) {
context.pop();
final newPath = '${file.parent.path}/$p0';
file.renameSync(newPath);
setState(() {});
},
),
);
_showRenameDialog(file);
},
),
ListTile(
@@ -228,25 +283,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
title: Text(_s.delete),
onTap: () {
context.pop();
showRoundDialog(
context: context,
title: Text(_s.delete),
child: Text(_s.sureDelete(fileName)),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () {
file.deleteSync();
setState(() {});
context.pop();
},
child: Text(_s.ok),
),
],
);
_showDeleteDialog(file);
},
),
ListTile(
@@ -274,12 +311,9 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
if (spi == null) {
return;
}
final remotePath = await AppRoute(
SftpPage(
spi,
selectPath: true,
),
'SFTP page (select)',
final remotePath = await AppRoute.sftp(
spi: spi,
isSelect: true,
).go<String>(context);
if (remotePath == null) {
return;
@@ -304,4 +338,56 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
),
);
}
void _showRenameDialog(FileSystemEntity file) {
final fileName = file.path.split('/').last;
showRoundDialog(
context: context,
title: Text(_s.rename),
child: Input(
autoFocus: true,
controller: TextEditingController(text: fileName),
onSubmitted: (p0) {
context.pop();
final newPath = '${file.parent.path}/$p0';
try {
file.renameSync(newPath);
} catch (e) {
showSnackBar(context, Text('${_s.failed}:\n$e'));
return;
}
setState(() {});
},
),
);
}
void _showDeleteDialog(FileSystemEntity file) {
final fileName = file.path.split('/').last;
showRoundDialog(
context: context,
title: Text(_s.delete),
child: Text(_s.sureDelete(fileName)),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () {
context.pop();
try {
file.deleteSync(recursive: true);
} catch (e) {
showSnackBar(context, Text('${_s.failed}:\n$e'));
return;
}
setState(() {});
},
child: Text(_s.ok),
),
],
);
}
}

View File

@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:logging/logging.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/extension/sftpfile.dart';
import 'package:toolbox/data/res/misc.dart';
@@ -16,7 +17,6 @@ import '../../../core/extension/stringx.dart';
import '../../../core/route.dart';
import '../../../core/utils/misc.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';
@@ -26,6 +26,7 @@ import '../../../data/provider/sftp.dart';
import '../../../data/res/path.dart';
import '../../../data/res/ui.dart';
import '../../../locator.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/fade_in.dart';
import '../../widget/input_field.dart';
import '../../widget/two_line_text.dart';
@@ -36,11 +37,11 @@ class SftpPage extends StatefulWidget {
final String? initPath;
final bool selectPath;
const SftpPage(
this.spi, {
const SftpPage({
Key? key,
required this.spi,
required this.selectPath,
this.initPath,
this.selectPath = false,
}) : super(key: key);
@override
@@ -49,15 +50,15 @@ class SftpPage extends StatefulWidget {
class _SftpPageState extends State<SftpPage> {
final SftpBrowserStatus _status = SftpBrowserStatus();
final ScrollController _scrollController = ScrollController();
final _sftp = locator<SftpProvider>();
late S _s;
ServerState? _state;
SSHClient? _client;
final _logger = Logger('SFTP');
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -69,13 +70,21 @@ class _SftpPageState extends State<SftpPage> {
super.initState();
final serverProvider = locator<ServerProvider>();
_client = serverProvider.servers[widget.spi.id]?.client;
_state = serverProvider.servers[widget.spi.id]?.state;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
appBar: CustomAppBar(
leading: IconButton(
icon: const BackButtonIcon(),
onPressed: () {
if (_status.path != null) {
_status.path!.update('/');
}
context.pop();
},
),
centerTitle: true,
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
actions: [
@@ -88,11 +97,24 @@ class _SftpPageState extends State<SftpPage> {
),
],
),
body: _buildFileView(),
body: _buildBody(),
bottomNavigationBar: _buildBottom(),
);
}
Widget _buildBody() {
return WillPopScope(
onWillPop: () async {
if (_status.path == null || _status.path?.path == '/') {
return true;
}
await _backward();
return false;
},
child: _buildFileView(),
);
}
Widget _buildBottom() {
final children = widget.selectPath
? [
@@ -224,6 +246,8 @@ class _SftpPageState extends State<SftpPage> {
mainAxisSize: MainAxisSize.min,
children: [
Input(
autoFocus: true,
icon: Icons.abc,
label: _s.path,
onSubmitted: (value) => context.pop(value),
),
@@ -249,10 +273,6 @@ class _SftpPageState extends State<SftpPage> {
}
Widget _buildFileView() {
if (_client == null || _state != ServerState.connected) {
return centerLoading;
}
if (_status.isBusy) {
return centerLoading;
}
@@ -264,12 +284,17 @@ class _SftpPageState extends State<SftpPage> {
return centerLoading;
}
if (_status.files!.isEmpty) {
return const Center(
child: Text('~'),
);
}
return RefreshIndicator(
child: FadeIn(
key: Key(widget.spi.name + _status.path!.path),
child: ListView.builder(
itemCount: _status.files!.length,
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
itemBuilder: (_, index) => _buildItem(_status.files![index]),
),
@@ -367,7 +392,7 @@ class _SftpPageState extends State<SftpPage> {
SftpReqType.download,
);
_sftp.add(req, completer: completer);
showRoundDialog(context: context, child: centerSizedLoading);
showLoadingDialog(context);
await completer.future;
context.pop();
@@ -430,11 +455,7 @@ class _SftpPageState extends State<SftpPage> {
TextButton(
onPressed: () async {
context.pop();
showRoundDialog(
context: context,
child: centerSizedLoading,
barrierDismiss: false,
);
showLoadingDialog(context);
final remotePath = _getRemotePath(file);
try {
if (file.attr.isDirectory) {
@@ -460,10 +481,7 @@ class _SftpPageState extends State<SftpPage> {
}
_listDir();
},
child: Text(
_s.delete,
style: const TextStyle(color: Colors.red),
),
child: Text(_s.delete, style: textRed),
),
],
);
@@ -476,6 +494,8 @@ class _SftpPageState extends State<SftpPage> {
context: context,
title: Text(_s.createFolder),
child: Input(
autoFocus: true,
icon: Icons.folder,
controller: textController,
label: _s.name,
),
@@ -504,10 +524,7 @@ class _SftpPageState extends State<SftpPage> {
context.pop();
_listDir();
},
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
child: Text(_s.ok, style: textRed),
),
],
);
@@ -520,6 +537,8 @@ class _SftpPageState extends State<SftpPage> {
context: context,
title: Text(_s.createFile),
child: Input(
autoFocus: true,
icon: Icons.insert_drive_file,
controller: textController,
label: _s.name,
),
@@ -550,10 +569,7 @@ class _SftpPageState extends State<SftpPage> {
context.pop();
_listDir();
},
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
child: Text(_s.ok, style: textRed),
),
],
);
@@ -566,6 +582,8 @@ class _SftpPageState extends State<SftpPage> {
context: context,
title: Text(_s.rename),
child: Input(
autoFocus: true,
icon: Icons.abc,
controller: textController,
label: _s.name,
),
@@ -591,10 +609,7 @@ class _SftpPageState extends State<SftpPage> {
context.pop();
_listDir();
},
child: Text(
_s.rename,
style: const TextStyle(color: Colors.red),
),
child: Text(_s.rename, style: textRed),
),
],
);
@@ -619,29 +634,47 @@ class _SftpPageState extends State<SftpPage> {
_status.client = sftpc;
}
try {
final fs =
await _status.client!.listdir(path ?? _status.path?.path ?? '/');
final listPath = path ?? _status.path?.path ?? '/';
final fs = await _status.client!.listdir(listPath);
fs.sort((a, b) => a.filename.compareTo(b.filename));
fs.removeAt(0);
/// Issue #97
/// In order to compatible with the Synology NAS
/// which not has '.' and '..' in listdir
if (fs.isNotEmpty && fs.first.filename == '.') {
fs.removeAt(0);
}
/// Issue #96
/// Due to [WillPopScope] added in this page
/// There is no need to keep '..' folder in listdir
/// So remove it
if (fs.isNotEmpty && fs.first.filename == '..') {
fs.removeAt(0);
}
if (mounted) {
setState(() {
_status.files = fs;
_status.isBusy = false;
});
}
} catch (e) {
await showRoundDialog(
context: context,
title: Text(_s.error),
child: Text(e.toString()),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(_s.ok),
)
],
);
} catch (e, trace) {
_logger.warning('list dir failed', e, trace);
await _backward();
Future.delayed(
const Duration(milliseconds: 177),
() => showRoundDialog(
context: context,
title: Text(_s.error),
child: Text(e.toString()),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(_s.ok),
)
],
),
);
}
}

View File

@@ -13,6 +13,7 @@ import '../../../core/utils/ui.dart';
import '../../../data/model/sftp/req.dart';
import '../../../data/provider/sftp.dart';
import '../../../data/res/ui.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/round_rect_card.dart';
class SftpMissionPage extends StatefulWidget {
@@ -34,11 +35,8 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
_s.mission,
style: textSize18,
),
appBar: CustomAppBar(
title: Text(_s.mission, style: textSize18),
),
body: _buildBody(),
);
@@ -48,7 +46,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
return Consumer<SftpProvider>(builder: (__, pro, _) {
if (pro.status.isEmpty) {
return Center(
child: Text(_s.sftpNoDownloadTask),
child: Text(_s.noTask),
);
}
return ListView.builder(

View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:macos_window_utils/window_manipulator.dart';
double? _titlebarHeight;
class CustomAppBar extends AppBar implements PreferredSizeWidget {
CustomAppBar({
super.key,
super.title,
super.actions,
super.centerTitle,
super.leading,
super.backgroundColor,
}) : super(toolbarHeight: (_titlebarHeight ?? 0) + kToolbarHeight);
static Future<void> updateTitlebarHeight() async {
final newTitlebarHeight = await WindowManipulator.getTitlebarHeight();
if (_titlebarHeight != newTitlebarHeight) {
_titlebarHeight = newTitlebarHeight;
}
}
}

View File

@@ -18,6 +18,7 @@ class Input extends StatelessWidget {
final bool suggestiion;
final String? errorText;
final Widget? prefix;
final bool autoFocus;
const Input({
super.key,
@@ -36,6 +37,7 @@ class Input extends StatelessWidget {
this.suggestiion = false,
this.errorText,
this.prefix,
this.autoFocus = false,
});
@override
Widget build(BuildContext context) {
@@ -49,6 +51,7 @@ class Input extends StatelessWidget {
onChanged: onChanged,
keyboardType: type,
focusNode: node,
autofocus: autoFocus,
autocorrect: autoCorrect,
enableSuggestions: suggestiion,
decoration: InputDecoration(
@@ -57,7 +60,7 @@ class Input extends StatelessWidget {
icon: icon != null ? Icon(icon) : null,
border: InputBorder.none,
errorText: errorText,
prefix: prefix),
prefix: prefix,),
controller: controller,
obscureText: obscureText,
),

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