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'** /// **'Add private key'**
String get addPrivateKey; 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. /// No description provided for @added2List.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -164,6 +170,12 @@ abstract class S {
/// **'Auto'** /// **'Auto'**
String get 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. /// No description provided for @autoUpdateHomeWidget.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -254,6 +266,12 @@ abstract class S {
/// **'Connection'** /// **'Connection'**
String get conn; String get conn;
/// No description provided for @connected.
///
/// In en, this message translates to:
/// **'Connected'**
String get connected;
/// No description provided for @containerName. /// No description provided for @containerName.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -548,6 +566,12 @@ abstract class S {
/// **'Getting token...'** /// **'Getting token...'**
String get gettingToken; String get gettingToken;
/// No description provided for @goBackQ.
///
/// In en, this message translates to:
/// **'Go back?'**
String get goBackQ;
/// No description provided for @goto. /// No description provided for @goto.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -752,6 +776,18 @@ abstract class S {
/// **'Mission'** /// **'Mission'**
String get 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. /// No description provided for @ms.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -824,6 +860,12 @@ abstract class S {
/// **'No server available.'** /// **'No server available.'**
String get noServerAvailable; String get noServerAvailable;
/// No description provided for @noTask.
///
/// In en, this message translates to:
/// **'No task'**
String get noTask;
/// No description provided for @noUpdateAvailable. /// No description provided for @noUpdateAvailable.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -932,11 +974,11 @@ abstract class S {
/// **'Preview'** /// **'Preview'**
String get preview; String get preview;
/// No description provided for @primaryColor. /// No description provided for @primaryColorSeed.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Primary color'** /// **'Primary color seed'**
String get primaryColor; String get primaryColorSeed;
/// No description provided for @privateKey. /// No description provided for @privateKey.
/// ///
@@ -1046,6 +1088,18 @@ abstract class S {
/// **'Server'** /// **'Server'**
String get 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. /// No description provided for @serverTabConnecting.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1094,12 +1148,6 @@ abstract class S {
/// **'Preparing to connect...'** /// **'Preparing to connect...'**
String get sftpDlPrepare; 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. /// No description provided for @sftpSSHConnected.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1148,6 +1196,12 @@ abstract class S {
/// **'Start'** /// **'Start'**
String get start; String get start;
/// No description provided for @stats.
///
/// In en, this message translates to:
/// **'Stats'**
String get stats;
/// No description provided for @stop. /// No description provided for @stop.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1178,6 +1232,12 @@ abstract class S {
/// **'Are you sure to use no password?'** /// **'Are you sure to use no password?'**
String get sureNoPwd; 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. /// No description provided for @sureToDeleteServer.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1319,7 +1379,7 @@ abstract class S {
/// No description provided for @versionUnknownUpdate. /// No description provided for @versionUnknownUpdate.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Current: v1.0.{build}'** /// **'Current: v1.0.{build}, click to check updates'**
String versionUnknownUpdate(Object build); String versionUnknownUpdate(Object build);
/// No description provided for @versionUpdated. /// No description provided for @versionUpdated.

View File

@@ -19,6 +19,9 @@ class SDe extends S {
@override @override
String get addPrivateKey => 'Private key hinzufügen'; 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 @override
String get added2List => 'Zur Aufgabenliste hinzugefügt'; String get added2List => 'Zur Aufgabenliste hinzugefügt';
@@ -37,6 +40,9 @@ class SDe extends S {
@override @override
String get auto => 'System folgen'; String get auto => 'System folgen';
@override
String get autoCheckUpdate => 'Aktualisierung automatisch prüfen';
@override @override
String get autoUpdateHomeWidget => 'Home-Widget automatisch aktualisieren'; String get autoUpdateHomeWidget => 'Home-Widget automatisch aktualisieren';
@@ -82,6 +88,9 @@ class SDe extends S {
@override @override
String get conn => 'Verbindung'; String get conn => 'Verbindung';
@override
String get connected => 'in Verbindung gebracht';
@override @override
String get containerName => 'Container Name'; String get containerName => 'Container Name';
@@ -245,6 +254,9 @@ class SDe extends S {
@override @override
String get gettingToken => 'Getting token...'; String get gettingToken => 'Getting token...';
@override
String get goBackQ => 'Zurückkommen?';
@override @override
String get goto => 'Pfad öffnen'; String get goto => 'Pfad öffnen';
@@ -353,6 +365,12 @@ class SDe extends S {
@override @override
String get mission => 'Mission'; 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 @override
String get ms => 'ms'; String get ms => 'ms';
@@ -389,6 +407,9 @@ class SDe extends S {
@override @override
String get noServerAvailable => 'Kein Server verfügbar.'; String get noServerAvailable => 'Kein Server verfügbar.';
@override
String get noTask => 'Nicht fragen';
@override @override
String get noUpdateAvailable => 'Kein Update verfügbar'; String get noUpdateAvailable => 'Kein Update verfügbar';
@@ -444,7 +465,7 @@ class SDe extends S {
String get preview => 'Vorschau'; String get preview => 'Vorschau';
@override @override
String get primaryColor => 'Farbschema'; String get primaryColorSeed => 'Farbschema';
@override @override
String get privateKey => 'Private Key'; String get privateKey => 'Private Key';
@@ -504,6 +525,12 @@ class SDe extends S {
@override @override
String get server => 'Server'; String get server => 'Server';
@override
String get serverDetailOrder => 'Reihenfolge der Widgets auf der Detailseite';
@override
String get serverOrder => 'Server-Bestellung';
@override @override
String get serverTabConnecting => 'Verbinden...'; String get serverTabConnecting => 'Verbinden...';
@@ -528,9 +555,6 @@ class SDe extends S {
@override @override
String get sftpDlPrepare => 'Verbindung vorbereiten...'; String get sftpDlPrepare => 'Verbindung vorbereiten...';
@override
String get sftpNoDownloadTask => 'Keine aktiven Downloads.';
@override @override
String get sftpSSHConnected => 'SFTP Verbunden'; String get sftpSSHConnected => 'SFTP Verbunden';
@@ -559,6 +583,9 @@ class SDe extends S {
@override @override
String get start => 'Start'; String get start => 'Start';
@override
String get stats => 'Statistik';
@override @override
String get stop => 'Stop'; String get stop => 'Stop';
@@ -576,6 +603,11 @@ class SDe extends S {
@override @override
String get sureNoPwd => 'Bist du sicher, dass du kein Passwort verwenden willst?'; 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 @override
String sureToDeleteServer(Object server) { String sureToDeleteServer(Object server) {
return 'Bist du sicher, dass du [$server] löschen willst?'; return 'Bist du sicher, dass du [$server] löschen willst?';
@@ -655,7 +687,7 @@ class SDe extends S {
@override @override
String versionUnknownUpdate(Object build) { String versionUnknownUpdate(Object build) {
return 'Aktuell: v1.0.$build'; return 'Aktuell: v1.0.$build. Klicken Sie hier, um nach Updates zu suchen';
} }
@override @override

View File

@@ -19,6 +19,9 @@ class SEn extends S {
@override @override
String get addPrivateKey => 'Add private key'; 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 @override
String get added2List => 'Added to task list'; String get added2List => 'Added to task list';
@@ -37,6 +40,9 @@ class SEn extends S {
@override @override
String get auto => 'Auto'; String get auto => 'Auto';
@override
String get autoCheckUpdate => 'Auto check update';
@override @override
String get autoUpdateHomeWidget => 'Auto update home widget'; String get autoUpdateHomeWidget => 'Auto update home widget';
@@ -82,6 +88,9 @@ class SEn extends S {
@override @override
String get conn => 'Connection'; String get conn => 'Connection';
@override
String get connected => 'Connected';
@override @override
String get containerName => 'Container name'; String get containerName => 'Container name';
@@ -245,6 +254,9 @@ class SEn extends S {
@override @override
String get gettingToken => 'Getting token...'; String get gettingToken => 'Getting token...';
@override
String get goBackQ => 'Go back?';
@override @override
String get goto => 'Go to'; String get goto => 'Go to';
@@ -353,6 +365,12 @@ class SEn extends S {
@override @override
String get mission => 'Mission'; 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 @override
String get ms => 'ms'; String get ms => 'ms';
@@ -389,6 +407,9 @@ class SEn extends S {
@override @override
String get noServerAvailable => 'No server available.'; String get noServerAvailable => 'No server available.';
@override
String get noTask => 'No task';
@override @override
String get noUpdateAvailable => 'No update available'; String get noUpdateAvailable => 'No update available';
@@ -444,7 +465,7 @@ class SEn extends S {
String get preview => 'Preview'; String get preview => 'Preview';
@override @override
String get primaryColor => 'Primary color'; String get primaryColorSeed => 'Primary color seed';
@override @override
String get privateKey => 'Private Key'; String get privateKey => 'Private Key';
@@ -504,6 +525,12 @@ class SEn extends S {
@override @override
String get server => 'Server'; String get server => 'Server';
@override
String get serverDetailOrder => 'Detail page widget order';
@override
String get serverOrder => 'Server order';
@override @override
String get serverTabConnecting => 'Connecting...'; String get serverTabConnecting => 'Connecting...';
@@ -528,9 +555,6 @@ class SEn extends S {
@override @override
String get sftpDlPrepare => 'Preparing to connect...'; String get sftpDlPrepare => 'Preparing to connect...';
@override
String get sftpNoDownloadTask => 'No download task.';
@override @override
String get sftpSSHConnected => 'SFTP Connected'; String get sftpSSHConnected => 'SFTP Connected';
@@ -559,6 +583,9 @@ class SEn extends S {
@override @override
String get start => 'Start'; String get start => 'Start';
@override
String get stats => 'Stats';
@override @override
String get stop => 'Stop'; String get stop => 'Stop';
@@ -576,6 +603,11 @@ class SEn extends S {
@override @override
String get sureNoPwd => 'Are you sure to use no password?'; String get sureNoPwd => 'Are you sure to use no password?';
@override
String sureStop(Object item) {
return 'Sure to stop [$item] ?';
}
@override @override
String sureToDeleteServer(Object server) { String sureToDeleteServer(Object server) {
return 'Are you sure to delete server [$server]?'; return 'Are you sure to delete server [$server]?';
@@ -655,7 +687,7 @@ class SEn extends S {
@override @override
String versionUnknownUpdate(Object build) { String versionUnknownUpdate(Object build) {
return 'Current: v1.0.$build'; return 'Current: v1.0.$build, click to check updates';
} }
@override @override

View File

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

View File

@@ -19,6 +19,9 @@ class SZh extends S {
@override @override
String get addPrivateKey => '添加一个私钥'; String get addPrivateKey => '添加一个私钥';
@override
String get addSystemPrivateKeyTip => '当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa';
@override @override
String get added2List => '已添加至任务列表'; String get added2List => '已添加至任务列表';
@@ -37,6 +40,9 @@ class SZh extends S {
@override @override
String get auto => '自动'; String get auto => '自动';
@override
String get autoCheckUpdate => '自动检查更新';
@override @override
String get autoUpdateHomeWidget => '自动更新桌面小部件'; String get autoUpdateHomeWidget => '自动更新桌面小部件';
@@ -82,6 +88,9 @@ class SZh extends S {
@override @override
String get conn => '连接'; String get conn => '连接';
@override
String get connected => '已连接';
@override @override
String get containerName => '容器名'; String get containerName => '容器名';
@@ -245,6 +254,9 @@ class SZh extends S {
@override @override
String get gettingToken => '正在获取Token...'; String get gettingToken => '正在获取Token...';
@override
String get goBackQ => '返回?';
@override @override
String get goto => '前往'; String get goto => '前往';
@@ -353,6 +365,12 @@ class SZh extends S {
@override @override
String get mission => '任务'; String get mission => '任务';
@override
String get moveOutServerFuncBtns => '服务器功能按钮位置';
@override
String get moveOutServerFuncBtnsHelp => '开启:可以在服务器 Tab 页的每个卡片下方显示。关闭:在服务器详情页顶部显示。';
@override @override
String get ms => '毫秒'; String get ms => '毫秒';
@@ -389,6 +407,9 @@ class SZh extends S {
@override @override
String get noServerAvailable => '没有可用的服务器。'; String get noServerAvailable => '没有可用的服务器。';
@override
String get noTask => '没有任务';
@override @override
String get noUpdateAvailable => '没有可用更新'; String get noUpdateAvailable => '没有可用更新';
@@ -444,7 +465,7 @@ class SZh extends S {
String get preview => '预览'; String get preview => '预览';
@override @override
String get primaryColor => '主题色'; String get primaryColorSeed => '主题色种子';
@override @override
String get privateKey => '私钥'; String get privateKey => '私钥';
@@ -504,6 +525,12 @@ class SZh extends S {
@override @override
String get server => '服务器'; String get server => '服务器';
@override
String get serverDetailOrder => '详情页部件顺序';
@override
String get serverOrder => '服务器顺序';
@override @override
String get serverTabConnecting => '连接中...'; String get serverTabConnecting => '连接中...';
@@ -529,10 +556,7 @@ class SZh extends S {
String get sftpDlPrepare => '准备连接至服务器...'; String get sftpDlPrepare => '准备连接至服务器...';
@override @override
String get sftpNoDownloadTask => '没有下载任务'; String get sftpSSHConnected => 'SFTP 已连接...';
@override
String get sftpSSHConnected => 'SFTP 已连接,即将开始下载...';
@override @override
String get showDistLogo => '显示发行版 Logo'; String get showDistLogo => '显示发行版 Logo';
@@ -559,6 +583,9 @@ class SZh extends S {
@override @override
String get start => '开始'; String get start => '开始';
@override
String get stats => '统计';
@override @override
String get stop => '停止'; String get stop => '停止';
@@ -576,6 +603,11 @@ class SZh extends S {
@override @override
String get sureNoPwd => '确认使用无密码?'; String get sureNoPwd => '确认使用无密码?';
@override
String sureStop(Object item) {
return '确定要停止 [$item] 吗?';
}
@override @override
String sureToDeleteServer(Object server) { String sureToDeleteServer(Object server) {
return '你确定要删除服务器 [$server] 吗?'; return '你确定要删除服务器 [$server] 吗?';
@@ -655,7 +687,7 @@ class SZh extends S {
@override @override
String versionUnknownUpdate(Object build) { String versionUnknownUpdate(Object build) {
return '当前v1.0.$build'; return '当前v1.0.$build,点击检查更新';
} }
@override @override
@@ -701,6 +733,9 @@ class SZhTw extends SZh {
@override @override
String get addPrivateKey => '新增一個私鑰'; String get addPrivateKey => '新增一個私鑰';
@override
String get addSystemPrivateKeyTip => '當前沒有任何私鑰,是否添加系統自帶的(~/.ssh/id_rsa';
@override @override
String get added2List => '已添加至任務列表'; String get added2List => '已添加至任務列表';
@@ -719,6 +754,9 @@ class SZhTw extends SZh {
@override @override
String get auto => '自動'; String get auto => '自動';
@override
String get autoCheckUpdate => '自動檢查更新';
@override @override
String get autoUpdateHomeWidget => '自動更新桌面小部件'; String get autoUpdateHomeWidget => '自動更新桌面小部件';
@@ -764,6 +802,9 @@ class SZhTw extends SZh {
@override @override
String get conn => '連接'; String get conn => '連接';
@override
String get connected => '已連接';
@override @override
String get containerName => '容器名稱'; String get containerName => '容器名稱';
@@ -927,6 +968,9 @@ class SZhTw extends SZh {
@override @override
String get gettingToken => '正在獲取Token...'; String get gettingToken => '正在獲取Token...';
@override
String get goBackQ => '返回?';
@override @override
String get goto => '前往'; String get goto => '前往';
@@ -1035,6 +1079,12 @@ class SZhTw extends SZh {
@override @override
String get mission => '任務'; String get mission => '任務';
@override
String get moveOutServerFuncBtns => '服務器功能按鈕位置';
@override
String get moveOutServerFuncBtnsHelp => '開啟:可以在服務器 Tab 頁的每個卡片下方顯示。關閉:在服務器詳情頁頂部顯示。';
@override @override
String get ms => '毫秒'; String get ms => '毫秒';
@@ -1071,6 +1121,9 @@ class SZhTw extends SZh {
@override @override
String get noServerAvailable => '沒有可用的服務器。'; String get noServerAvailable => '沒有可用的服務器。';
@override
String get noTask => '沒有任務';
@override @override
String get noUpdateAvailable => '沒有可用更新'; String get noUpdateAvailable => '沒有可用更新';
@@ -1126,7 +1179,7 @@ class SZhTw extends SZh {
String get preview => '預覽'; String get preview => '預覽';
@override @override
String get primaryColor => '主要色調'; String get primaryColorSeed => '主要色調種子';
@override @override
String get privateKey => '私鑰'; String get privateKey => '私鑰';
@@ -1186,6 +1239,12 @@ class SZhTw extends SZh {
@override @override
String get server => '服務器'; String get server => '服務器';
@override
String get serverDetailOrder => '詳情頁部件順序';
@override
String get serverOrder => '服務器順序';
@override @override
String get serverTabConnecting => '連接中...'; String get serverTabConnecting => '連接中...';
@@ -1211,10 +1270,7 @@ class SZhTw extends SZh {
String get sftpDlPrepare => '準備連接至服務器...'; String get sftpDlPrepare => '準備連接至服務器...';
@override @override
String get sftpNoDownloadTask => '沒有下載任務'; String get sftpSSHConnected => 'SFTP 已連接...';
@override
String get sftpSSHConnected => 'SFTP 已連接,即將開始下載...';
@override @override
String get showDistLogo => '顯示發行版 Logo'; String get showDistLogo => '顯示發行版 Logo';
@@ -1241,6 +1297,9 @@ class SZhTw extends SZh {
@override @override
String get start => '開始'; String get start => '開始';
@override
String get stats => '統計';
@override @override
String get stop => '停止'; String get stop => '停止';
@@ -1258,6 +1317,11 @@ class SZhTw extends SZh {
@override @override
String get sureNoPwd => '確認使用無密碼?'; String get sureNoPwd => '確認使用無密碼?';
@override
String sureStop(Object item) {
return '確定要停止 [$item] 嗎?';
}
@override @override
String sureToDeleteServer(Object server) { String sureToDeleteServer(Object server) {
return '你確定要刪除服務器 [$server] 嗎?'; return '你確定要刪除服務器 [$server] 嗎?';
@@ -1337,7 +1401,7 @@ class SZhTw extends SZh {
@override @override
String versionUnknownUpdate(Object build) { String versionUnknownUpdate(Object build) {
return '當前v1.0.$build'; return '當前v1.0.$build,點擊檢查更新';
} }
@override @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 /android/app/fjy.androidstudio.key
/release /release
test.dart test.dart
.fvm
# Keep generated l10n files # Keep generated l10n files
/.dart_tool/* /.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). If ServerBox app has any bug, please open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
## 📱 ScreenShots ## 🏙️ ScreenShots
<table> <table>
<tr> <tr>
<td> <td>
<img width="200px" src="imgs/server.jpeg"> <img width="200px" src="imgs/server.png">
</td> </td>
<td> <td>
<img width="200px" src="imgs/detail.jpg"> <img width="200px" src="imgs/detail.png">
</td> </td>
<td> <td>
<img width="200px" src="imgs/ssh.jpg"> <img width="200px" src="imgs/sftp.png">
</td> </td>
<td> <td>
<img width="200px" src="imgs/editor.jpg"> <img width="200px" src="imgs/editor.png">
</td> </td>
</tr> </tr>
</table> </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"> <img width="200px" src="imgs/ping.png">
</td> </td>
<td> <td>
<img width="200px" src="imgs/sftp.jpeg"> <img width="200px" src="imgs/ssh.jpg">
</td> </td>
<td> <td>
<img width="200px" src="imgs/docker.jpeg"> <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 Status|Platform
--- | --- --- | ---
Full Support| Android / iOS / macOS Full Support| Android / iOS / macOS
Support, but not tested| Windows / Linux Not tested| Windows / Linux
## 🧱 Contribution ## 🧱 Contribution
**Any positive contribution is welcome**. **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 ### l10n guide
1. Fork this repo and clone forked repo to your local machine. 1. Fork this repo and clone forked repo to your local machine.
2. Create `arb` file in `lib/l10n/` directory 2. Create `arb` file in `lib/l10n/` directory
@@ -100,5 +100,9 @@ Support, but not tested| Windows / Linux
## 📝 License ## 📝 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.) - You can package it for personal use, but you can't distribute it.
2. Except for the above, apply the `GPLv3` license. - 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] 功能
- [x] `SSH` 终端, `SFTP` - [x] `SSH` 终端, `SFTP`
- [x] `Docker & 包` 管理器 - [x] `Docker & 包 & 进程` 管理器
- [x] 状态图表 - [x] 状态图表
- [x] 代码编辑器 - [x] 代码编辑器
- [x] `Ping` 和 更多 - [x] `Ping` 和 更多
@@ -44,20 +44,20 @@
如果 ServerBox app 有任何 bug请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。 如果 ServerBox app 有任何 bug请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
## 📱 截屏 ## 🏙️ 截屏
<table> <table>
<tr> <tr>
<td> <td>
<img width="200px" src="imgs/server.jpeg"> <img width="200px" src="imgs/server.png">
</td> </td>
<td> <td>
<img width="200px" src="imgs/detail.jpg"> <img width="200px" src="imgs/detail.png">
</td> </td>
<td> <td>
<img width="200px" src="imgs/ssh.jpg"> <img width="200px" src="imgs/sftp.png">
</td> </td>
<td> <td>
<img width="200px" src="imgs/editor.jpg"> <img width="200px" src="imgs/editor.png">
</td> </td>
</tr> </tr>
</table> </table>
@@ -67,7 +67,7 @@
<img width="200px" src="imgs/ping.png"> <img width="200px" src="imgs/ping.png">
</td> </td>
<td> <td>
<img width="200px" src="imgs/sftp.jpeg"> <img width="200px" src="imgs/ssh.jpg">
</td> </td>
<td> <td>
<img width="200px" src="imgs/docker.jpeg"> <img width="200px" src="imgs/docker.jpeg">
@@ -83,11 +83,11 @@
状态|平台 状态|平台
--- | --- --- | ---
完整支持 | Android / iOS / macOS 完整支持 | Android / iOS / macOS
可能支持,未测试 | Windows / Linux 未测试 | Windows / Linux
## 🧱 贡献 ## 🧱 贡献
**任何正面的贡献都欢迎**. **任何正面的贡献都欢迎**
第一次参与贡献,会赠送 10 份 iOS App 兑换码。这是我唯一能送的。你可以来送给其他人。:) 第一次参与贡献,会赠送 10 份 iOS App 兑换码。如果没有 iOS 设备,你可以来送给其他人。:)
### l10n ### l10n
1. Fork 本项目,并 Clone 你 Fork 的项目至你的电脑 1. Fork 本项目,并 Clone 你 Fork 的项目至你的电脑
@@ -95,10 +95,14 @@
- 文件名应该类似 `intl_XX.arb`, `XX` 是语言标识码。 例如 `intl_en.arb` 是给英语的, `intl_zh.arb` 是给中文的 - 文件名应该类似 `intl_XX.arb`, `XX` 是语言标识码。 例如 `intl_en.arb` 是给英语的, `intl_zh.arb` 是给中文的
3.`.arb` 本地化文件添加内容。 你可以查看 `intl_en.arb``intl_zh.arb` 的内容,并理解其含义,来创建新的本地化文件 3.`.arb` 本地化文件添加内容。 你可以查看 `intl_en.arb``intl_zh.arb` 的内容,并理解其含义,来创建新的本地化文件
4. 运行 `flutter gen-l10n` 来生成所需文件 4. 运行 `flutter gen-l10n` 来生成所需文件
5. Commit 变更到你 Fork 的 Repo 5. Commit 变更到你 Fork 的 Repo
6. 在我的项目中发起 Pull Request. 6. 在我的项目中发起 Pull Request
## 📝 License ## 📝 开源协议
1. 允许打包自用,但不允许分发举例你可以教别人如何打包避免花钱购买App但不能与他人分享你打包的App - 允许打包自用,但不允许分发
2. 除去上诉情形:遵循 `GPLv3` - 举例你可以教别人如何打包避免花钱购买App但不能与他人分享你打包的App
- 之所以这样做:
1. 安全性:可能会有有心之人植入后门并分发
2. 回血:苹果开发者 **99刀/年**,并且作为刚毕业的独立开发者,我需要收入
- 除去上述情形:遵循 `GPLv3`

View File

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

View File

@@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="tech.lolli.toolbox">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -56,5 +55,7 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/home_widget" /> android:resource="@xml/home_widget" />
</receiver> </receiver>
<service android:name=".KeepAliveService"/>
</application> </application>
</manifest> </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 package tech.lolli.toolbox
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@@ -11,11 +12,18 @@ class MainActivity : FlutterActivity() {
MethodChannel(binaryMessenger, "tech.lolli.toolbox/app_retain").apply { MethodChannel(binaryMessenger, "tech.lolli.toolbox/app_retain").apply {
setMethodCallHandler { method, result -> setMethodCallHandler { method, result ->
if (method.method == "sendToBackground") { when (method.method) {
moveTaskToBack(true) "sendToBackground" -> {
result.success(null) moveTaskToBack(true)
} else { result.success(null)
result.notImplemented() }
"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:textColor="@color/widgetText"
android:textSize="23sp" android:textSize="23sp"
android:textStyle="bold" android:textStyle="bold"
android:maxLines="1"
tools:text="Server Name" /> tools:text="Server Name" />
<RelativeLayout <RelativeLayout

View File

@@ -6,7 +6,7 @@ buildscript {
} }
dependencies { 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" 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 file_picker: 1d63c4949e05e386da864365f8c13e1e64787675
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1 plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114 r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028

View File

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

View File

@@ -28,8 +28,7 @@ class MyApp extends StatelessWidget {
// Issue #57 // Issue #57
// if not [ok] -> [AMOLED] mode, use [ThemeMode.dark] // if not [ok] -> [AMOLED] mode, use [ThemeMode.dark]
final themeMode = isAMOLED ? ThemeMode.values[tMode] : ThemeMode.dark; final themeMode = isAMOLED ? ThemeMode.values[tMode] : ThemeMode.dark;
final localeStr = _setting.locale.fetch(); final locale = _setting.locale.fetch()?.toLocale;
final locale = localeStr?.toLocale;
final darkTheme = ThemeData( final darkTheme = ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.dark, brightness: Brightness.dark,
@@ -47,39 +46,25 @@ class MyApp extends StatelessWidget {
useMaterial3: true, useMaterial3: true,
colorSchemeSeed: primaryColor, colorSchemeSeed: primaryColor,
), ),
darkTheme: isAMOLED darkTheme: isAMOLED ? darkTheme : _getAmoledTheme(darkTheme),
? 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,
),
),
home: fullScreen ? const FullScreenPage() : const HomePage(), 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 { 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 { bool get isBrightColor {
return getBrightnessFromColor == Brightness.light; 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'; import 'package:flutter/material.dart';
extension StringX on String { 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); int get i => int.parse(this);
Uri get uri { 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/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/analysis.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 { class AppRoute {
final Widget page; final Widget page;
@@ -14,4 +47,148 @@ class AppRoute {
MaterialPageRoute(builder: (context) => page), 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:logging/logging.dart';
import 'package:r_upgrade/r_upgrade.dart'; import 'package:r_upgrade/r_upgrade.dart';
import 'package:toolbox/core/extension/navigator.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/path.dart';
import 'package:toolbox/data/res/ui.dart';
import '../data/provider/app.dart'; import '../data/provider/app.dart';
import '../data/res/build_data.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 { Future<void> doUpdate(BuildContext context, {bool force = false}) async {
_rmDownloadApks(); await _rmDownloadApks();
final update = await locator<AppService>().getUpdate(); final update = await locator<AppService>().getUpdate();
@@ -69,7 +71,7 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
child: Text(s.updateTipTooLow(newest)), child: Text(s.updateTipTooLow(newest)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => _doUpdate(url, context, s), onPressed: () => _doUpdate(update, context, s),
child: Text(s.ok), child: Text(s.ok),
) )
], ],
@@ -81,17 +83,58 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
context, context,
'${s.updateTip(newest)} \n${update.changelog.current}', '${s.updateTip(newest)} \n${update.changelog.current}',
s.update, 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) { 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, url,
fileName: url.split('/').last, fileName: fileName,
isAutoRequestInstall: true, 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) { } else if (isIOS) {
await RUpgrade.upgradeFromAppStore('1586449703'); await RUpgrade.upgradeFromAppStore('1586449703');
} else { } else {
@@ -111,8 +154,10 @@ Future<void> _doUpdate(String url, BuildContext context, S s) async {
// rmdir Download // rmdir Download
Future<void> _rmDownloadApks() async { Future<void> _rmDownloadApks() async {
if (!isAndroid) return; if (!isAndroid) return;
final dlDir = Directory(pathJoin((await docDir).path, 'Download')); final dlDir = Directory(await _dlDir);
if (await dlDir.exists()) { if (await dlDir.exists()) {
await dlDir.delete(recursive: true); 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 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@@ -74,6 +74,16 @@ String getTime(int? unixMill) {
.replaceFirst('.000', ''); .replaceFirst('.000', '');
} }
/// Join two path with `/`
String pathJoin(String path1, String path2) { String pathJoin(String path1, String path2) {
return path1 + (path1.endsWith('/') ? '' : '/') + 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, macos,
windows, windows,
web, web,
unknown, fuchsia,
unknown;
} }
final _p = () { final _p = () {
@@ -31,10 +32,21 @@ final _p = () {
if (Platform.isWindows) { if (Platform.isWindows) {
return PlatformType.windows; return PlatformType.windows;
} }
if (Platform.isFuchsia) {
return PlatformType.fuchsia;
}
return PlatformType.unknown; return PlatformType.unknown;
}(); }();
final _pathSep = () {
if (Platform.isWindows) {
return '\\';
}
return '/';
}();
PlatformType get platform => _p; PlatformType get platform => _p;
String get pathSeparator => _pathSep;
bool get isAndroid => _p == PlatformType.android; bool get isAndroid => _p == PlatformType.android;
bool get isIOS => _p == PlatformType.ios; bool get isIOS => _p == PlatformType.ios;
@@ -47,3 +59,23 @@ bool get isDesktop =>
_p == PlatformType.linux || _p == PlatformType.linux ||
_p == PlatformType.macos || _p == PlatformType.macos ||
_p == PlatformType.windows; _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) { String getPrivateKey(String id) {
final key = locator<PrivateKeyStore>().get(id); final pki = locator<PrivateKeyStore>().get(id);
if (key == null) { if (pki == null) {
throw SSHErr( throw SSHErr(
type: SSHErrType.noPrivateKey, type: SSHErrType.noPrivateKey,
message: 'key [$id] not found', message: 'key [$id] not found',
); );
} }
return key.privateKey; return pki.key;
} }
Future<SSHClient> genClient( Future<SSHClient> genClient(
@@ -56,11 +56,12 @@ Future<SSHClient> genClient(
timeout: const Duration(seconds: 5), timeout: const Duration(seconds: 5),
); );
} catch (e) { } catch (e) {
if (spi.alterUrl == null) rethrow;
try { try {
spi.fromStringUrl(); final ipPort = spi.fromStringUrl();
socket = await SSHSocket.connect( socket = await SSHSocket.connect(
spi.ip, ipPort.ip,
spi.port, ipPort.port,
timeout: const Duration(seconds: 5), timeout: const Duration(seconds: 5),
); );
} catch (e) { } catch (e) {

View File

@@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../data/model/server/snippet.dart'; import '../../data/model/server/snippet.dart';
import '../../data/provider/snippet.dart'; import '../../data/provider/snippet.dart';
import '../../data/res/ui.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../../view/page/snippet/edit.dart'; import '../../view/page/snippet/edit.dart';
import '../../view/widget/picker.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( Widget buildSwitch(
BuildContext context, BuildContext context,
StoreProperty<bool> prop, { StoreProperty<bool> prop, {

View File

@@ -38,6 +38,9 @@ enum DockerErrType {
invalidVersion, invalidVersion,
cmdNoPrefix, cmdNoPrefix,
segmentsNotMatch, segmentsNotMatch,
parsePsItem,
parseImages,
parseStats,
} }
class DockerErr extends Err<DockerErrType> { 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'; import 'package:flutter_gen/gen_l10n/l10n.dart';
enum ServerTabMenuType { enum ServerTabMenuType {
terminal,
sftp, sftp,
snippet,
pkg,
docker, docker,
process, process,
edit; pkg,
snippet,
;
IconData get icon { IconData get icon {
switch (this) { switch (this) {
@@ -19,31 +20,12 @@ enum ServerTabMenuType {
return Icons.system_security_update; return Icons.system_security_update;
case ServerTabMenuType.docker: case ServerTabMenuType.docker:
return Icons.view_agenda; return Icons.view_agenda;
case ServerTabMenuType.edit:
return Icons.edit;
case ServerTabMenuType.process: case ServerTabMenuType.process:
return Icons.list_alt_outlined; 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 { enum DockerMenuType {
@@ -52,11 +34,12 @@ enum DockerMenuType {
restart, restart,
rm, rm,
logs, logs,
terminal; terminal,
stats;
static List<DockerMenuType> items(bool running) { static List<DockerMenuType> items(bool running) {
if (running) { if (running) {
return [stop, restart, rm, logs, terminal]; return [stop, restart, rm, logs, terminal, stats];
} else { } else {
return [start, rm, logs]; return [start, rm, logs];
} }
@@ -76,6 +59,8 @@ enum DockerMenuType {
return Icons.logo_dev; return Icons.logo_dev;
case DockerMenuType.terminal: case DockerMenuType.terminal:
return Icons.terminal; return Icons.terminal;
case DockerMenuType.stats:
return Icons.bar_chart;
} }
} }
@@ -93,6 +78,8 @@ enum DockerMenuType {
return s.log; return s.log;
case DockerMenuType.terminal: case DockerMenuType.terminal:
return s.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 { class PathWithPrefix {
final String _prefixPath; final String _prefixPath;

View File

@@ -1,22 +1,54 @@
import '../../res/server_cmd.dart'; import '../../res/server_cmd.dart';
class AppShellFunc { const _cmdDivider = '\necho $seperator\n';
final String name;
final String cmd;
final String flag;
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'; 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 cmd {
String get generate { 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(); final sb = StringBuffer();
// Write each func // Write each func
for (final func in this) { for (final func in values) {
sb.write(''' sb.write('''
${func.name}() { ${func.name}() {
${func.cmd} ${func.cmd}
@@ -27,7 +59,7 @@ ${func.cmd}
// Write switch case // Write switch case
sb.write('case \$1 in\n'); sb.write('case \$1 in\n');
for (final func in this) { for (final func in values) {
sb.write(''' sb.write('''
'-${func.flag}') '-${func.flag}')
${func.name} ${func.name}
@@ -38,13 +70,36 @@ ${func.cmd}
*) *)
echo "Invalid argument \$1" echo "Invalid argument \$1"
;; ;;
esac esac''');
''');
return sb.toString(); return sb.toString();
} }
} }
// enum AppShellFuncType { extension EnumX on Enum {
// status, /// Find out the required segment from [segments]
// docker; 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> { class OneTimeCpuStatus extends TimeSeqIface<OneTimeCpuStatus> {
late String id; final String id;
late int user; final int user;
late int sys; final int sys;
late int nice; final int nice;
late int idle; final int idle;
late int iowait; final int iowait;
late int irq; final int irq;
late int softirq; final int softirq;
OneTimeCpuStatus( OneTimeCpuStatus(
this.id, this.id,
@@ -80,7 +80,8 @@ List<OneTimeCpuStatus> parseCPU(String raw) {
if (item == '') break; if (item == '') break;
final id = item.split(' ').first; final id = item.split(' ').first;
final matches = item.replaceFirst(id, '').trim().split(' '); final matches = item.replaceFirst(id, '').trim().split(' ');
cpus.add(OneTimeCpuStatus( cpus.add(
OneTimeCpuStatus(
id, id,
int.parse(matches[0]), int.parse(matches[0]),
int.parse(matches[1]), int.parse(matches[1]),
@@ -88,7 +89,9 @@ List<OneTimeCpuStatus> parseCPU(String raw) {
int.parse(matches[3]), int.parse(matches[3]),
int.parse(matches[4]), int.parse(matches[4]),
int.parse(matches[5]), int.parse(matches[5]),
int.parse(matches[6]))); int.parse(matches[6]),
),
);
} }
return cpus; return cpus;
} }

View File

@@ -36,14 +36,41 @@ List<Disk> parseDisk(String raw) {
vals[0] = pathCache; vals[0] = pathCache;
pathCache = ''; pathCache = '';
} }
list.add(Disk( try {
path: vals[0], list.add(Disk(
loc: vals[5], path: vals[0],
usedPercent: int.parse(vals[4].replaceFirst('%', '')), loc: vals[5],
used: vals[2], usedPercent: int.parse(vals[4].replaceFirst('%', '')),
size: vals[1], used: vals[2],
avail: vals[3], size: vals[1],
)); avail: vals[3],
));
} catch (e) {
continue;
}
} }
return list; 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'; import 'time_seq.dart';
class NetSpeedPart extends TimeSeqIface<NetSpeedPart> { class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
String device; final String device;
BigInt bytesIn; final BigInt bytesIn;
BigInt bytesOut; final BigInt bytesOut;
BigInt time; final int time;
NetSpeedPart(this.device, this.bytesIn, this.bytesOut, this.time); NetSpeedPart(this.device, this.bytesIn, this.bytesOut, this.time);
@override @override
@@ -18,7 +19,7 @@ class NetSpeed extends TimeSeq<NetSpeedPart> {
List<String> get devices => now.map((e) => e.device).toList(); 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 _speedIn(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;
double _speedOut(int i) => (now[i].bytesOut - pre[i].bytesOut) / _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 /// 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 /// 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 /// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
/// 1635752901 List<NetSpeedPart> parseNetSpeed(String raw, int time) {
List<NetSpeedPart> parseNetSpeed(String raw) {
final split = raw.split('\n'); final split = raw.split('\n');
if (split.length < 4) { if (split.length < 4) {
return []; return [];
} }
final time = BigInt.parse(split[split.length - 1]);
final results = <NetSpeedPart>[]; final results = <NetSpeedPart>[];
for (final item in split.sublist(2, split.length - 1)) { for (final item in split.sublist(2, split.length - 1)) {
final data = item.trim().split(':'); final data = item.trim().split(':');

View File

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

View File

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

View File

@@ -12,11 +12,22 @@ class Server {
} }
enum ServerState { enum ServerState {
failed,
disconnected, disconnected,
connecting, connecting,
connected,
failed;
bool get shouldConnect => /// Connected to server
this == ServerState.disconnected || this == ServerState.failed; 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; alterUrl != old.alterUrl;
} }
void fromStringUrl() { _IpPort fromStringUrl() {
if (alterUrl == null) { if (alterUrl == null) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
} }
@@ -76,20 +76,16 @@ class ServerPrivateInfo {
if (splited.length != 2) { if (splited.length != 2) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no @'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no @');
} }
user = splited[0];
final splited2 = splited[1].split(':'); final splited2 = splited[1].split(':');
if (splited2.length != 2) { if (splited2.length != 2) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no :'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no :');
} }
ip = splited2[0]; final ip_ = splited2[0];
port = int.tryParse(splited2[1]) ?? 22; final port_ = int.tryParse(splited2[1]) ?? 22;
if (port <= 0 || port > 65535) { if (port <= 0 || port > 65535) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');
} }
return _IpPort(ip_, port_);
// Do not update [id]
// Because [id] is the identity which is used to find the [SSHClient]
// id = '$user@$ip:$port';
} }
@override @override
@@ -97,3 +93,10 @@ class ServerPrivateInfo {
return id; 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 'cpu.dart';
import 'disk.dart'; import 'disk.dart';
import 'memory.dart'; import 'memory.dart';
@@ -13,50 +13,46 @@ class ServerStatusUpdateReq {
const ServerStatusUpdateReq(this.ss, this.segments); 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 { 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); req.ss.netSpeed.update(net);
final sys = _parseSysVer( final sys = _parseSysVer(
req.segments.at(CmdType.sys), StatusCmdType.sys.find(segments),
req.segments.at(CmdType.host), StatusCmdType.host.find(segments),
req.segments.at(CmdType.sysRhel), StatusCmdType.sysRhel.find(segments),
); );
if (sys != null) { if (sys != null) {
req.ss.sysVer = sys; 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.cpu.update(cpus);
req.ss.temps.parse( req.ss.temps.parse(
req.segments.at(CmdType.tempType), StatusCmdType.tempType.find(segments),
req.segments.at(CmdType.tempVal), StatusCmdType.tempVal.find(segments),
); );
final tcp = parseConn(req.segments.at(CmdType.conn)); final tcp = parseConn(StatusCmdType.conn.find(segments));
if (tcp != null) { if (tcp != null) {
req.ss.tcp = tcp; 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) { if (uptime != null) {
req.ss.uptime = uptime; req.ss.uptime = uptime;
} }
req.ss.swap = parseSwap(req.segments.at(CmdType.mem)); req.ss.swap = parseSwap(StatusCmdType.mem.find(segments));
return req.ss; return req.ss;
} }
@@ -74,13 +70,16 @@ String? _parseUpTime(String raw) {
} }
String? _parseSysVer(String raw, String hostname, String rawRhel) { String? _parseSysVer(String raw, String hostname, String rawRhel) {
if (!rawRhel.contains('No such file')) { try {
return rawRhel; 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; return null;
} }

View File

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

View File

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

View File

@@ -2,16 +2,16 @@ import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:toolbox/core/extension/ssh_client.dart'; import 'package:toolbox/core/extension/ssh_client.dart';
import 'package:toolbox/core/extension/stringx.dart'; import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/extension/uint8list.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/manager.dart';
import 'package:toolbox/data/model/pkg/upgrade_info.dart'; import 'package:toolbox/data/model/pkg/upgrade_info.dart';
import 'package:toolbox/data/model/server/dist.dart'; import 'package:toolbox/data/model/server/dist.dart';
class PkgProvider extends BusyProvider { class PkgProvider extends ChangeNotifier {
final logger = Logger('PKG'); final logger = Logger('PKG');
SSHClient? client; 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/model/server/private_key_info.dart';
import 'package:toolbox/data/store/private_key.dart'; import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
class PrivateKeyProvider extends BusyProvider { class PrivateKeyProvider extends ChangeNotifier {
List<PrivateKeyInfo> get infos => _infos; List<PrivateKeyInfo> get pkis => _pkis;
final _store = locator<PrivateKeyStore>(); final _store = locator<PrivateKeyStore>();
late List<PrivateKeyInfo> _infos; late List<PrivateKeyInfo> _pkis;
void loadData() { void loadData() {
_infos = _store.fetch(); _pkis = _store.fetch();
} }
void addInfo(PrivateKeyInfo info) { void add(PrivateKeyInfo info) {
_infos.add(info); _pkis.add(info);
_store.put(info); _store.put(info);
notifyListeners(); notifyListeners();
} }
void delInfo(PrivateKeyInfo info) { void delete(PrivateKeyInfo info) {
_infos.removeWhere((e) => e.id == info.id); _pkis.removeWhere((e) => e.id == info.id);
_store.delete(info); _store.delete(info);
notifyListeners(); notifyListeners();
} }
void updateInfo(PrivateKeyInfo old, PrivateKeyInfo newInfo) { void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
final idx = _infos.indexWhere((e) => e.id == old.id); final idx = _pkis.indexWhere((e) => e.id == old.id);
_infos[idx] = newInfo; if (idx == -1) {
_pkis.add(newInfo);
} else {
_pkis[idx] = newInfo;
}
_store.put(newInfo); _store.put(newInfo);
notifyListeners(); notifyListeners();
} }

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,8 @@
class BuildData { class BuildData {
static const String name = "ServerBox"; 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 engine = "3.10.6";
static const String buildAt = "2023-08-02 23:34:08.619104"; static const String buildAt = "2023-08-20 23:32:07.343451";
static const int modifications = 2; static const int modifications = 4;
} }

View File

@@ -1,5 +1,7 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../model/app/github_id.dart';
/// RegExp for number /// RegExp for number
final numReg = RegExp(r'\s{1,}'); final numReg = RegExp(r'\s{1,}');
@@ -16,3 +18,35 @@ const maxDebugLogLines = 100;
const pkgName = 'tech.lolli.toolbox'; const pkgName = 'tech.lolli.toolbox';
const bgRunChannel = MethodChannel('$pkgName/app_retain'); const bgRunChannel = MethodChannel('$pkgName/app_retain');
const homeWidgetChannel = MethodChannel('$pkgName/home_widget'); 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 '../model/app/shell_func.dart';
import 'build_data.dart'; import 'build_data.dart';
const seperator = 'SrvBox'; const seperator = 'SrvBoxSep';
const serverBoxDir = r'$HOME/.config/server_box'; const serverBoxDir = r'$HOME/.config/server_box';
const shellPath = '$serverBoxDir/mobile_app.sh'; const shellPath = '$serverBoxDir/mobile_app.sh';
const echoPWD = 'echo \$PWD'; const statusCmds = [
'date +%s',
enum CmdType { 'cat /proc/net/dev',
net,
sys,
cpu,
uptime,
conn,
disk,
mem,
tempType,
tempVal,
host,
sysRhel,
}
const _cmdList = [
'cat /proc/net/dev && date +%s',
'cat /etc/os-release | grep PRETTY_NAME', 'cat /etc/os-release | grep PRETTY_NAME',
'cat /proc/stat | grep cpu', 'cat /proc/stat | grep cpu',
'uptime', 'uptime',
@@ -35,12 +20,6 @@ const _cmdList = [
'cat /etc/redhat-release', 'cat /etc/redhat-release',
]; ];
final shellFuncStatus = AppShellFunc(
'status',
_cmdList.join('\necho $seperator\n'),
's',
);
const dockerCmds = [ const dockerCmds = [
'docker version', 'docker version',
'docker ps -a', 'docker ps -a',
@@ -48,26 +27,13 @@ const dockerCmds = [
'docker image ls', '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 = """ final shellCmd = """
# Script for app `${BuildData.name} v1.0.${BuildData.build}` # Script for app `${BuildData.name} v1.0.${BuildData.build}`
# Delete this file while app is running will cause app crash # 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 && " final installShellCmd = "mkdir -p $serverBoxDir && "

View File

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

View File

@@ -2,15 +2,18 @@ import 'package:flutter/material.dart';
/// Font style /// Font style
const textSize9Grey = TextStyle(color: Colors.grey, fontSize: 9);
const textSize11 = TextStyle(fontSize: 11); 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 textSize13 = TextStyle(fontSize: 13);
const textSize13Bold = TextStyle(fontSize: 13, fontWeight: FontWeight.bold);
const textSize13Grey = TextStyle(color: Colors.grey, fontSize: 13); const textSize13Grey = TextStyle(color: Colors.grey, fontSize: 13);
const textSize15 = TextStyle(fontSize: 15); const textSize15 = TextStyle(fontSize: 15);
const textSize18 = TextStyle(fontSize: 18); const textSize18 = TextStyle(fontSize: 18);
const textSize27 = TextStyle(fontSize: 27); const textSize27 = TextStyle(fontSize: 27);
const grey = TextStyle(color: Colors.grey); const grey = TextStyle(color: Colors.grey);
const textRed = TextStyle(color: Colors.red);
/// Icon /// Icon
@@ -21,7 +24,10 @@ final appIcon = Image.asset('assets/app_icon.png');
const roundRectCardPadding = EdgeInsets.symmetric(horizontal: 17, vertical: 13); const roundRectCardPadding = EdgeInsets.symmetric(horizontal: 17, vertical: 13);
/// SizedBox /// SizedBox
const placeholder = SizedBox();
const height13 = SizedBox(height: 13); const height13 = SizedBox(height: 13);
const height77 = SizedBox(height: 77);
const width13 = SizedBox(width: 13); const width13 = SizedBox(width: 13);
const width7 = SizedBox(width: 7); 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 joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
const myGithub = 'https://github.com/lollipopkit'; const myGithub = 'https://github.com/lollipopkit';
const appHelpUrl = '$myGithub/flutter_server_box#-help'; 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'; import 'package:toolbox/core/persistant_store.dart';
class DockerStore extends PersistentStore { class DockerStore extends PersistentStore {
String? getDockerHost(String id) { String? fetch(String id) {
return box.get(id); return box.get(id);
} }
void setDockerHost(String id, String host) { void put(String id, String host) {
box.put(id, host); box.put(id, host);
} }
Map<String, String> fetch() { Map<String, String> fetchAll() {
return box.toMap().cast<String, String>(); return box.toMap().cast<String, String>();
} }
} }

View File

@@ -71,6 +71,9 @@ class SettingStore extends PersistentStore {
StoreProperty<bool> get sshVirtualKeyAutoOff => StoreProperty<bool> get sshVirtualKeyAutoOff =>
property('sshVirtualKeyAutoOff', defaultValue: true); property('sshVirtualKeyAutoOff', defaultValue: true);
StoreProperty<double> get editorFontSize =>
property('editorFontSize', defaultValue: 13);
// Editor theme // Editor theme
StoreProperty<String> get editorTheme => StoreProperty<String> get editorTheme =>
property('editorTheme', defaultValue: defaultEditorTheme); property('editorTheme', defaultValue: defaultEditorTheme);
@@ -99,4 +102,13 @@ class SettingStore extends PersistentStore {
// Only valid on iOS // Only valid on iOS
StoreProperty<bool> get autoUpdateHomeWidget => StoreProperty<bool> get autoUpdateHomeWidget =>
property('autoUpdateHomeWidget', defaultValue: isIOS); 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", "add": "Neu",
"addAServer": "Server hinzufügen", "addAServer": "Server hinzufügen",
"addPrivateKey": "Private key 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", "added2List": "Zur Aufgabenliste hinzugefügt",
"all": "Alle", "all": "Alle",
"alreadyLastDir": "Bereits im letzten Verzeichnis.", "alreadyLastDir": "Bereits im letzten Verzeichnis.",
"alterUrl": "Url ändern", "alterUrl": "Url ändern",
"attention": "Achtung", "attention": "Achtung",
"auto": "System folgen", "auto": "System folgen",
"autoCheckUpdate": "Aktualisierung automatisch prüfen",
"autoUpdateHomeWidget": "Home-Widget automatisch aktualisieren", "autoUpdateHomeWidget": "Home-Widget automatisch aktualisieren",
"backup": "Backup", "backup": "Backup",
"backupAndRestore": "Backup und Wiederherstellung", "backupAndRestore": "Backup und Wiederherstellung",
@@ -26,6 +28,7 @@
"close": "Schließen", "close": "Schließen",
"cmd": "Command", "cmd": "Command",
"conn": "Verbindung", "conn": "Verbindung",
"connected": "in Verbindung gebracht",
"containerName": "Container Name", "containerName": "Container Name",
"containerStatus": "Container Status", "containerStatus": "Container Status",
"convert": "Konvertieren", "convert": "Konvertieren",
@@ -75,6 +78,7 @@
"fullScreenJitterHelp": "Einbrennen des Bildschirms verhindern", "fullScreenJitterHelp": "Einbrennen des Bildschirms verhindern",
"getPushTokenFailed": "Push-Token kann nicht abgerufen werden", "getPushTokenFailed": "Push-Token kann nicht abgerufen werden",
"gettingToken": "Getting token...", "gettingToken": "Getting token...",
"goBackQ": "Zurückkommen?",
"goto": "Pfad öffnen", "goto": "Pfad öffnen",
"homeWidgetUrlConfig": "Home-Widget-Link konfigurieren", "homeWidgetUrlConfig": "Home-Widget-Link konfigurieren",
"host": "Host", "host": "Host",
@@ -109,6 +113,8 @@
"maxRetryCountEqual0": "Unbegrenzte Verbindungsversuche zum Server", "maxRetryCountEqual0": "Unbegrenzte Verbindungsversuche zum Server",
"min": "min", "min": "min",
"mission": "Mission", "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", "ms": "ms",
"name": "Name", "name": "Name",
"needRestart": "App muss neugestartet werden", "needRestart": "App muss neugestartet werden",
@@ -121,6 +127,7 @@
"noSavedPrivateKey": "Keine gespeicherten Private Keys", "noSavedPrivateKey": "Keine gespeicherten Private Keys",
"noSavedSnippet": "Keine gespeicherten Snippets.", "noSavedSnippet": "Keine gespeicherten Snippets.",
"noServerAvailable": "Kein Server verfügbar.", "noServerAvailable": "Kein Server verfügbar.",
"noTask": "Nicht fragen",
"noUpdateAvailable": "Kein Update verfügbar", "noUpdateAvailable": "Kein Update verfügbar",
"notSelected": "Nicht ausgewählt", "notSelected": "Nicht ausgewählt",
"nullToken": "Null token", "nullToken": "Null token",
@@ -139,7 +146,7 @@
"plzSelectKey": "Wähle einen Key.", "plzSelectKey": "Wähle einen Key.",
"port": "Port", "port": "Port",
"preview": "Vorschau", "preview": "Vorschau",
"primaryColor": "Farbschema", "primaryColorSeed": "Farbschema",
"privateKey": "Private Key", "privateKey": "Private Key",
"process": "Prozess", "process": "Prozess",
"pushToken": "Push Token", "pushToken": "Push Token",
@@ -158,6 +165,8 @@
"saved": "Gerettet", "saved": "Gerettet",
"second": "s", "second": "s",
"server": "Server", "server": "Server",
"serverDetailOrder": "Reihenfolge der Widgets auf der Detailseite",
"serverOrder": "Server-Bestellung",
"serverTabConnecting": "Verbinden...", "serverTabConnecting": "Verbinden...",
"serverTabEmpty": "Keine Server vorhanden.", "serverTabEmpty": "Keine Server vorhanden.",
"serverTabFailed": "Fehlgeschlagen", "serverTabFailed": "Fehlgeschlagen",
@@ -166,7 +175,6 @@
"serverTabUnkown": "Unbekannter Status", "serverTabUnkown": "Unbekannter Status",
"setting": "Einstellungen", "setting": "Einstellungen",
"sftpDlPrepare": "Verbindung vorbereiten...", "sftpDlPrepare": "Verbindung vorbereiten...",
"sftpNoDownloadTask": "Keine aktiven Downloads.",
"sftpSSHConnected": "SFTP Verbunden", "sftpSSHConnected": "SFTP Verbunden",
"showDistLogo": "Distributionslogo anzeigen", "showDistLogo": "Distributionslogo anzeigen",
"snippet": "Snippet", "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.", "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", "sshVirtualKeyAutoOff": "Automatische Umschaltung der virtuellen Tasten",
"start": "Start", "start": "Start",
"stats": "Statistik",
"stop": "Stop", "stop": "Stop",
"success": "Erfolgreich", "success": "Erfolgreich",
"sureDelete": "Soll [{name}] wirklich gelöscht werden?", "sureDelete": "Soll [{name}] wirklich gelöscht werden?",
"sureDirEmpty": "Stelle sicher, dass der Ordner leer ist.", "sureDirEmpty": "Stelle sicher, dass der Ordner leer ist.",
"sureNoPwd": "Bist du sicher, dass du kein Passwort verwenden willst?", "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?", "sureToDeleteServer": "Bist du sicher, dass du [{server}] löschen willst?",
"system": "Systeme", "system": "Systeme",
"tag": "Tags", "tag": "Tags",
@@ -203,7 +213,7 @@
"urlOrJson": "URL oder JSON", "urlOrJson": "URL oder JSON",
"user": "Benutzer", "user": "Benutzer",
"versionHaveUpdate": "Gefunden: v1.0.{build}, klicke zum Aktualisieren", "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", "versionUpdated": "v1.0.{build} ist bereits die neueste Version",
"viewErr": "Fehler anzeigen", "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.", "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", "add": "Add",
"addAServer": "add a server", "addAServer": "add a server",
"addPrivateKey": "Add private key", "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", "added2List": "Added to task list",
"all": "All", "all": "All",
"alreadyLastDir": "Already in last directory.", "alreadyLastDir": "Already in last directory.",
"alterUrl": "Alter url", "alterUrl": "Alter url",
"attention": "Attention", "attention": "Attention",
"auto": "Auto", "auto": "Auto",
"autoCheckUpdate": "Auto check update",
"autoUpdateHomeWidget": "Auto update home widget", "autoUpdateHomeWidget": "Auto update home widget",
"backup": "Backup", "backup": "Backup",
"backupAndRestore": "Backup and Restore", "backupAndRestore": "Backup and Restore",
@@ -26,6 +28,7 @@
"close": "Close", "close": "Close",
"cmd": "Command", "cmd": "Command",
"conn": "Connection", "conn": "Connection",
"connected": "Connected",
"containerName": "Container name", "containerName": "Container name",
"containerStatus": "Container status", "containerStatus": "Container status",
"convert": "Convert", "convert": "Convert",
@@ -75,6 +78,7 @@
"fullScreenJitterHelp": "To avoid screen burn-in", "fullScreenJitterHelp": "To avoid screen burn-in",
"getPushTokenFailed": "Can't fetch push token", "getPushTokenFailed": "Can't fetch push token",
"gettingToken": "Getting token...", "gettingToken": "Getting token...",
"goBackQ": "Go back?",
"goto": "Go to", "goto": "Go to",
"homeWidgetUrlConfig": "Config home widget url", "homeWidgetUrlConfig": "Config home widget url",
"host": "Host", "host": "Host",
@@ -109,6 +113,8 @@
"maxRetryCountEqual0": "Will retry again and again.", "maxRetryCountEqual0": "Will retry again and again.",
"min": "min", "min": "min",
"mission": "Mission", "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", "ms": "ms",
"name": "Name", "name": "Name",
"needRestart": "Need to restart app", "needRestart": "Need to restart app",
@@ -121,6 +127,7 @@
"noSavedPrivateKey": "No saved private keys.", "noSavedPrivateKey": "No saved private keys.",
"noSavedSnippet": "No saved snippets.", "noSavedSnippet": "No saved snippets.",
"noServerAvailable": "No server available.", "noServerAvailable": "No server available.",
"noTask": "No task",
"noUpdateAvailable": "No update available", "noUpdateAvailable": "No update available",
"notSelected": "Not selected", "notSelected": "Not selected",
"nullToken": "Null token", "nullToken": "Null token",
@@ -139,7 +146,7 @@
"plzSelectKey": "Please select a key.", "plzSelectKey": "Please select a key.",
"port": "Port", "port": "Port",
"preview": "Preview", "preview": "Preview",
"primaryColor": "Primary color", "primaryColorSeed": "Primary color seed",
"privateKey": "Private Key", "privateKey": "Private Key",
"process": "Process", "process": "Process",
"pushToken": "Push token", "pushToken": "Push token",
@@ -158,6 +165,8 @@
"saved": "Saved", "saved": "Saved",
"second": "s", "second": "s",
"server": "Server", "server": "Server",
"serverDetailOrder": "Detail page widget order",
"serverOrder": "Server order",
"serverTabConnecting": "Connecting...", "serverTabConnecting": "Connecting...",
"serverTabEmpty": "There is no server.\nClick the fab to add one.", "serverTabEmpty": "There is no server.\nClick the fab to add one.",
"serverTabFailed": "Failed", "serverTabFailed": "Failed",
@@ -166,7 +175,6 @@
"serverTabUnkown": "Unknown state", "serverTabUnkown": "Unknown state",
"setting": "Settings", "setting": "Settings",
"sftpDlPrepare": "Preparing to connect...", "sftpDlPrepare": "Preparing to connect...",
"sftpNoDownloadTask": "No download task.",
"sftpSSHConnected": "SFTP Connected", "sftpSSHConnected": "SFTP Connected",
"showDistLogo": "Show distribution logo", "showDistLogo": "Show distribution logo",
"snippet": "Snippet", "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.", "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", "sshVirtualKeyAutoOff": "Auto switching of virtual keys",
"start": "Start", "start": "Start",
"stats": "Stats",
"stop": "Stop", "stop": "Stop",
"success": "Success", "success": "Success",
"sureDelete": "Are you sure to delete [{name}]?", "sureDelete": "Are you sure to delete [{name}]?",
"sureDirEmpty": "Make sure dir is empty.", "sureDirEmpty": "Make sure dir is empty.",
"sureNoPwd": "Are you sure to use no password?", "sureNoPwd": "Are you sure to use no password?",
"sureStop": "Sure to stop [{item}] ?",
"sureToDeleteServer": "Are you sure to delete server [{server}]?", "sureToDeleteServer": "Are you sure to delete server [{server}]?",
"system": "System", "system": "System",
"tag": "Tags", "tag": "Tags",
@@ -203,7 +213,7 @@
"urlOrJson": "URL or JSON", "urlOrJson": "URL or JSON",
"user": "User", "user": "User",
"versionHaveUpdate": "Found: v1.0.{build}, click to update", "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", "versionUpdated": "Current: v1.0.{build}, is up to date",
"viewErr": "See error", "viewErr": "See error",
"virtKeyHelpClipboard": "Copy to the clipboard if terminal selected is not empty, otherwise paste the contents of the clipboard to the terminal.", "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", "add": "Menambahkan",
"addAServer": "tambahkan server", "addAServer": "tambahkan server",
"addPrivateKey": "Tambahkan kunci pribadi", "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", "added2List": "Ditambahkan ke Daftar Tugas",
"all": "Semua", "all": "Semua",
"alreadyLastDir": "Sudah di direktori terakhir.", "alreadyLastDir": "Sudah di direktori terakhir.",
"alterUrl": "Alter url", "alterUrl": "Alter url",
"attention": "Perhatian", "attention": "Perhatian",
"auto": "Auto", "auto": "Auto",
"autoCheckUpdate": "Periksa pembaruan otomatis",
"autoUpdateHomeWidget": "Widget Rumah Pembaruan Otomatis", "autoUpdateHomeWidget": "Widget Rumah Pembaruan Otomatis",
"backup": "Cadangan", "backup": "Cadangan",
"backupAndRestore": "Cadangan dan Pulihkan", "backupAndRestore": "Cadangan dan Pulihkan",
@@ -26,6 +28,7 @@
"close": "Menutup", "close": "Menutup",
"cmd": "Memerintah", "cmd": "Memerintah",
"conn": "Koneksi", "conn": "Koneksi",
"connected": "Terhubung",
"containerName": "Nama kontainer", "containerName": "Nama kontainer",
"containerStatus": "Status wadah", "containerStatus": "Status wadah",
"convert": "Mengubah", "convert": "Mengubah",
@@ -75,6 +78,7 @@
"fullScreenJitterHelp": "Untuk menghindari pembakaran layar", "fullScreenJitterHelp": "Untuk menghindari pembakaran layar",
"getPushTokenFailed": "Tidak bisa mengambil token dorong", "getPushTokenFailed": "Tidak bisa mengambil token dorong",
"gettingToken": "Mendapatkan token ...", "gettingToken": "Mendapatkan token ...",
"goBackQ": "Datang kembali?",
"goto": "Pergi ke", "goto": "Pergi ke",
"homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah", "homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah",
"host": "Host", "host": "Host",
@@ -109,6 +113,8 @@
"maxRetryCountEqual0": "Akan mencoba lagi lagi dan lagi.", "maxRetryCountEqual0": "Akan mencoba lagi lagi dan lagi.",
"min": "Min", "min": "Min",
"mission": "Misi", "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", "ms": "MS",
"name": "Nama", "name": "Nama",
"needRestart": "Perlu memulai ulang aplikasi", "needRestart": "Perlu memulai ulang aplikasi",
@@ -121,6 +127,7 @@
"noSavedPrivateKey": "Tidak ada kunci pribadi yang disimpan.", "noSavedPrivateKey": "Tidak ada kunci pribadi yang disimpan.",
"noSavedSnippet": "Tidak ada cuplikan yang disimpan.", "noSavedSnippet": "Tidak ada cuplikan yang disimpan.",
"noServerAvailable": "Tidak ada server yang tersedia.", "noServerAvailable": "Tidak ada server yang tersedia.",
"noTask": "Tidak bertanya",
"noUpdateAvailable": "Tidak ada pembaruan yang tersedia", "noUpdateAvailable": "Tidak ada pembaruan yang tersedia",
"notSelected": "Tidak terpilih", "notSelected": "Tidak terpilih",
"nullToken": "Token NULL", "nullToken": "Token NULL",
@@ -139,7 +146,7 @@
"plzSelectKey": "Pilih kunci.", "plzSelectKey": "Pilih kunci.",
"port": "Port", "port": "Port",
"preview": "Pratinjau", "preview": "Pratinjau",
"primaryColor": "Warna utama", "primaryColorSeed": "Warna utama",
"privateKey": "Kunci Pribadi", "privateKey": "Kunci Pribadi",
"process": "Proses", "process": "Proses",
"pushToken": "Dorong token", "pushToken": "Dorong token",
@@ -158,6 +165,8 @@
"saved": "Diselamatkan", "saved": "Diselamatkan",
"second": "S", "second": "S",
"server": "Server", "server": "Server",
"serverDetailOrder": "Detail pesanan widget halaman",
"serverOrder": "Pesanan server",
"serverTabConnecting": "Menghubungkan ...", "serverTabConnecting": "Menghubungkan ...",
"serverTabEmpty": "Tidak ada server.\nKlik fab untuk menambahkan satu.", "serverTabEmpty": "Tidak ada server.\nKlik fab untuk menambahkan satu.",
"serverTabFailed": "Gagal", "serverTabFailed": "Gagal",
@@ -166,7 +175,6 @@
"serverTabUnkown": "Negara yang tidak diketahui", "serverTabUnkown": "Negara yang tidak diketahui",
"setting": "Pengaturan", "setting": "Pengaturan",
"sftpDlPrepare": "Bersiap untuk terhubung ...", "sftpDlPrepare": "Bersiap untuk terhubung ...",
"sftpNoDownloadTask": "Tidak ada tugas unduhan.",
"sftpSSHConnected": "Sftp terhubung", "sftpSSHConnected": "Sftp terhubung",
"showDistLogo": "Tampilkan logo distribusi", "showDistLogo": "Tampilkan logo distribusi",
"snippet": "Snippet", "snippet": "Snippet",
@@ -175,11 +183,13 @@
"sshTip": "Fungsi ini sekarang dalam tahap eksperimen.\n\nHarap laporkan bug di {url} atau bergabunglah dengan pengembangan kami.", "sshTip": "Fungsi ini sekarang dalam tahap eksperimen.\n\nHarap laporkan bug di {url} atau bergabunglah dengan pengembangan kami.",
"sshVirtualKeyAutoOff": "Switching Otomatis Kunci Virtual", "sshVirtualKeyAutoOff": "Switching Otomatis Kunci Virtual",
"start": "Awal", "start": "Awal",
"stats": "Statistik",
"stop": "Berhenti", "stop": "Berhenti",
"success": "Kesuksesan", "success": "Kesuksesan",
"sureDelete": "Apakah Anda pasti akan menghapus [{name}]?", "sureDelete": "Apakah Anda pasti akan menghapus [{name}]?",
"sureDirEmpty": "Pastikan dir kosong.", "sureDirEmpty": "Pastikan dir kosong.",
"sureNoPwd": "Apakah Anda pasti tidak menggunakan kata sandi?", "sureNoPwd": "Apakah Anda pasti tidak menggunakan kata sandi?",
"sureStop": "Anda yakin ingin menghentikan [{item}]?",
"sureToDeleteServer": "Apakah Anda pasti akan menghapus server [{server}]?", "sureToDeleteServer": "Apakah Anda pasti akan menghapus server [{server}]?",
"system": "Sistem", "system": "Sistem",
"tag": "Tag", "tag": "Tag",
@@ -203,7 +213,7 @@
"urlOrJson": "URL atau JSON", "urlOrJson": "URL atau JSON",
"user": "Username", "user": "Username",
"versionHaveUpdate": "Ditemukan: v1.0.{build}, klik untuk memperbarui", "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", "versionUpdated": "Saat ini: v1.0.{build}, mutakhir",
"viewErr": "Lihat kesalahan", "viewErr": "Lihat kesalahan",
"virtKeyHelpClipboard": "Salin ke clipboard jika terminal yang dipilih tidak kosong, jika tidak, tempel isi clipboard ke terminal.", "virtKeyHelpClipboard": "Salin ke clipboard jika terminal yang dipilih tidak kosong, jika tidak, tempel isi clipboard ke terminal.",

View File

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

View File

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

View File

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

View File

@@ -3,17 +3,21 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:macos_window_utils/window_manipulator.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:toolbox/data/model/app/net_view.dart'; import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/data/model/ssh/virtual_key.dart'; import 'package:toolbox/view/widget/custom_appbar.dart';
import 'app.dart'; import 'app.dart';
import 'core/analysis.dart'; import 'core/analysis.dart';
import 'core/utils/platform.dart';
import 'core/utils/ui.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/private_key_info.dart';
import 'data/model/server/server_private_info.dart'; import 'data/model/server/server_private_info.dart';
import 'data/model/server/snippet.dart'; import 'data/model/server/snippet.dart';
import 'data/model/ssh/virtual_key.dart';
import 'data/provider/app.dart'; import 'data/provider/app.dart';
import 'data/provider/debug.dart'; import 'data/provider/debug.dart';
import 'data/provider/docker.dart'; import 'data/provider/docker.dart';
@@ -27,66 +31,10 @@ import 'data/store/setting.dart';
import 'locator.dart'; import 'locator.dart';
import 'view/widget/rebuild.dart'; import 'view/widget/rebuild.dart';
late final DebugProvider _debug; 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);
}
Future<void> main() async { Future<void> main() async {
runInZone(() async { _runInZone(() async {
await initApp(); await initApp();
runApp( runApp(
MultiProvider( 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/server.dart';
import '../../data/store/snippet.dart'; import '../../data/store/snippet.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../widget/custom_appbar.dart';
const backupFormatVersion = 1; const backupFormatVersion = 1;
@@ -34,7 +35,7 @@ class BackupPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context)!; final s = S.of(context)!;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
title: Text(s.backupAndRestore, style: textSize18), title: Text(s.backupAndRestore, style: textSize18),
), ),
body: _buildBody(context, s), body: _buildBody(context, s),
@@ -89,7 +90,7 @@ class BackupPage extends StatelessWidget {
spis: _server.fetch(), spis: _server.fetch(),
snippets: _snippet.fetch(), snippets: _snippet.fetch(),
keys: _privateKey.fetch(), keys: _privateKey.fetch(),
dockerHosts: _dockerHosts.fetch(), dockerHosts: _dockerHosts.fetchAll(),
), ),
), ),
); );
@@ -170,7 +171,7 @@ class BackupPage extends StatelessWidget {
_privateKey.put(s); _privateKey.put(s);
} }
for (final k in backup.dockerHosts.keys) { for (final k in backup.dockerHosts.keys) {
_dockerHosts.setDockerHost(k, backup.dockerHosts[k]!); _dockerHosts.put(k, backup.dockerHosts[k]!);
} }
context.pop(); context.pop();
showRoundDialog( showRoundDialog(

View File

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

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/data/provider/debug.dart'; import 'package:toolbox/data/provider/debug.dart';
import '../widget/custom_appbar.dart';
class DebugPage extends StatefulWidget { class DebugPage extends StatefulWidget {
const DebugPage({Key? key}) : super(key: key); const DebugPage({Key? key}) : super(key: key);
@@ -13,8 +16,12 @@ class _DebugPageState extends State<DebugPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
title: const Text('Logs'), 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, backgroundColor: Colors.black,
), ),
body: _buildTerminal(context), body: _buildTerminal(context),

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:nil/nil.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/docker/image.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 'package:toolbox/view/widget/input_field.dart';
import '../../core/utils/ui.dart'; import '../../core/utils/ui.dart';
@@ -19,6 +18,7 @@ import '../../data/res/ui.dart';
import '../../data/res/url.dart'; import '../../data/res/url.dart';
import '../../data/store/docker.dart'; import '../../data/store/docker.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../widget/custom_appbar.dart';
import '../widget/popup_menu.dart'; import '../widget/popup_menu.dart';
import '../widget/round_rect_card.dart'; import '../widget/round_rect_card.dart';
import '../widget/two_line_text.dart'; import '../widget/two_line_text.dart';
@@ -26,7 +26,7 @@ import '../widget/url_text.dart';
class DockerManagePage extends StatefulWidget { class DockerManagePage extends StatefulWidget {
final ServerPrivateInfo spi; final ServerPrivateInfo spi;
const DockerManagePage(this.spi, {Key? key}) : super(key: key); const DockerManagePage({required this.spi, Key? key}) : super(key: key);
@override @override
State<DockerManagePage> createState() => _DockerManagePageState(); State<DockerManagePage> createState() => _DockerManagePageState();
@@ -34,6 +34,7 @@ class DockerManagePage extends StatefulWidget {
class _DockerManagePageState extends State<DockerManagePage> { class _DockerManagePageState extends State<DockerManagePage> {
final _docker = locator<DockerProvider>(); final _docker = locator<DockerProvider>();
final _store = locator<DockerStore>();
final _textController = TextEditingController(); final _textController = TextEditingController();
late S _s; late S _s;
@@ -41,6 +42,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
void dispose() { void dispose() {
super.dispose(); super.dispose();
_docker.clear(); _docker.clear();
_textController.dispose();
} }
@override @override
@@ -54,8 +56,6 @@ class _DockerManagePageState extends State<DockerManagePage> {
super.initState(); super.initState();
final client = locator<ServerProvider>().servers[widget.spi.id]?.client; final client = locator<ServerProvider>().servers[widget.spi.id]?.client;
if (client == null) { if (client == null) {
showSnackBar(context, Text(_s.noClient));
context.pop();
return; return;
} }
_docker.init(client, widget.spi.user, onPwdRequest, widget.spi.id); _docker.init(client, widget.spi.user, onPwdRequest, widget.spi.id);
@@ -65,12 +65,16 @@ class _DockerManagePageState extends State<DockerManagePage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<DockerProvider>(builder: (_, ___, __) { return Consumer<DockerProvider>(builder: (_, ___, __) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
centerTitle: true, centerTitle: true,
title: TwoLineText(up: 'Docker', down: widget.spi.name), title: TwoLineText(up: 'Docker', down: widget.spi.name),
actions: [ actions: [
IconButton( IconButton(
onPressed: _docker.refresh, onPressed: () async {
showLoadingDialog(context);
await _docker.refresh();
context.pop();
},
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
) )
], ],
@@ -99,6 +103,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Input( Input(
autoFocus: true,
type: TextInputType.text, type: TextInputType.text,
label: _s.image, label: _s.image,
hint: 'xxx:1.1', hint: 'xxx:1.1',
@@ -153,7 +158,9 @@ class _DockerManagePageState extends State<DockerManagePage> {
TextButton( TextButton(
onPressed: () async { onPressed: () async {
context.pop(); context.pop();
showLoadingDialog(context);
final result = await _docker.run(cmd); final result = await _docker.run(cmd);
context.pop();
if (result != null) { if (result != null) {
showSnackBar(context, Text(result.message ?? _s.unknownError)); showSnackBar(context, Text(result.message ?? _s.unknownError));
} }
@@ -217,10 +224,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
), ),
TextButton( TextButton(
onPressed: onSubmitted, onPressed: onSubmitted,
child: Text( child: Text(_s.ok, style: textRed),
_s.ok,
style: const TextStyle(color: Colors.red),
),
), ),
], ],
); );
@@ -239,7 +243,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
size: 37, size: 37,
), ),
const SizedBox(height: 27), const SizedBox(height: 27),
_buildErr(_docker.error!), Text(_docker.error?.message ?? _s.unknownError),
const SizedBox(height: 27), const SizedBox(height: 27),
Padding( Padding(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
@@ -250,7 +254,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
); );
} }
if (_docker.items == null || _docker.images == null) { if (_docker.items == null || _docker.images == null) {
Future.delayed(const Duration(milliseconds: 177), () { Future.delayed(const Duration(milliseconds: 37), () {
if (mounted) { if (mounted) {
_docker.refresh(); _docker.refresh();
} }
@@ -258,31 +262,21 @@ class _DockerManagePageState extends State<DockerManagePage> {
return centerLoading; return centerLoading;
} }
final items = <Widget>[]; final items = <Widget>[
items.addAll([
_buildLoading(), _buildLoading(),
_buildVersion( _buildVersion(),
_docker.edition ?? _s.unknown, _buildPs(),
_docker.version ?? _s.unknown, _buildImage(),
),
..._buildPsItems(),
_buildImages(),
_buildEditHost(), _buildEditHost(),
].map((e) => RoundRectCard(e))); ].map((e) => RoundRectCard(e));
items.add(const SizedBox(height: 37));
return ListView( return ListView(
padding: const EdgeInsets.all(7), padding: const EdgeInsets.all(7),
children: items, children: items.toList(),
); );
} }
Widget _buildImages() { Widget _buildImage() {
if (_docker.images == null) { final items = <Widget>[
return nil;
}
final items = _docker.images!.map(_buildImageItem).toList();
items.insert(
0,
ListTile( ListTile(
title: Text(_s.imagesList), title: Text(_s.imagesList),
subtitle: Text( subtitle: Text(
@@ -290,7 +284,8 @@ class _DockerManagePageState extends State<DockerManagePage> {
style: grey, style: grey,
), ),
), ),
); ];
items.addAll(_docker.images!.map(_buildImageItem));
return Column(children: items); return Column(children: items);
} }
@@ -330,17 +325,14 @@ class _DockerManagePageState extends State<DockerManagePage> {
); );
} }
}, },
child: Text( child: Text(_s.ok, style: textRed),
_s.ok,
style: const TextStyle(color: Colors.red),
),
), ),
], ],
); );
} }
Widget _buildLoading() { Widget _buildLoading() {
if (!_docker.isBusy) return nil; if (_docker.runLog == null) return placeholder;
return Padding( return Padding(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
child: Column( 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() { Widget _buildEditHost() {
final children = <Widget>[]; final children = <Widget>[];
if (_docker.items!.isEmpty && _docker.images!.isEmpty) { if (_docker.items!.isEmpty && _docker.images!.isEmpty) {
@@ -378,183 +529,29 @@ class _DockerManagePageState extends State<DockerManagePage> {
} }
Future<void> _showEditHostDialog() async { 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( await showRoundDialog(
context: context, context: context,
title: Text(_s.dockerEditHost), title: Text(_s.dockerEditHost),
child: Input( child: Input(
maxLines: 1, maxLines: 1,
controller: controller: ctrl,
TextEditingController(text: 'unix:///run/user/1000/docker.sock'), onSubmitted: _onSaveDockerHost,
onSubmitted: (value) {
locator<DockerStore>().setDockerHost(widget.spi.id, value.trim());
_docker.refresh();
context.pop();
},
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => context.pop(), onPressed: () => _onSaveDockerHost(ctrl.text),
child: Text(_s.cancel), child: Text(_s.ok),
), ),
], ],
); );
} }
Widget _buildErr(DockerErr err) { void _onSaveDockerHost(String val) {
var errStr = ''; context.pop();
switch (err.type) { _store.put(widget.spi.id, val.trim());
case DockerErrType.noClient: _docker.refresh();
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);
} }
} }

View File

@@ -15,6 +15,7 @@ import 'package:toolbox/data/res/highlight.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import '../widget/custom_appbar.dart';
import '../widget/two_line_text.dart'; import '../widget/two_line_text.dart';
class EditorPage extends StatefulWidget { class EditorPage extends StatefulWidget {
@@ -32,6 +33,7 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
Map<String, TextStyle>? _codeTheme; Map<String, TextStyle>? _codeTheme;
late S _s; late S _s;
late String? _langCode; late String? _langCode;
late TextStyle _textStyle;
@override @override
void initState() { void initState() {
@@ -40,6 +42,7 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
_controller = CodeController( _controller = CodeController(
language: suffix2HighlightMap[_langCode], language: suffix2HighlightMap[_langCode],
); );
_textStyle = TextStyle(fontSize: _setting.editorFontSize.fetch());
WidgetsBinding.instance.addPostFrameCallback((Duration duration) async { WidgetsBinding.instance.addPostFrameCallback((Duration duration) async {
if (isDarkMode(context)) { if (isDarkMode(context)) {
@@ -68,55 +71,9 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: () { backgroundColor: _codeTheme?['root']?.backgroundColor,
if (_codeTheme != null) { appBar: _buildAppBar(),
return _codeTheme!['root']!.backgroundColor; body: _buildBody(),
}
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,
),
),
),
),
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.done), child: const Icon(Icons.done),
onPressed: () { 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 @override
FutureOr<void> afterFirstLayout(BuildContext context) async { FutureOr<void> afterFirstLayout(BuildContext context) async {
if (widget.path != null) { 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/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:nil/nil.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/route.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/provider/server.dart';
import 'package:toolbox/data/res/ui.dart'; import 'package:toolbox/data/res/ui.dart';
import 'package:toolbox/data/store/setting.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_private_info.dart';
import '../../data/model/server/server_status.dart'; import '../../data/model/server/server_status.dart';
import '../../data/res/color.dart'; import '../../data/res/color.dart';
import 'server/detail.dart';
import 'server/edit.dart'; import 'server/edit.dart';
import 'setting.dart'; import 'setting/entry.dart';
class FullScreenPage extends StatefulWidget { class FullScreenPage extends StatefulWidget {
const FullScreenPage({Key? key}) : super(key: key); const FullScreenPage({Key? key}) : super(key: key);
@@ -62,6 +61,7 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
void dispose() { void dispose() {
super.dispose(); super.dispose();
_timer.cancel(); _timer.cancel();
_pageController.dispose();
} }
@override @override
@@ -146,7 +146,7 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
final id = pro.serverOrder[idx]; final id = pro.serverOrder[idx];
final s = pro.servers[id]; final s = pro.servers[id];
if (s == null) { if (s == null) {
return nil; return Center(child: Text(_s.noClient));
} }
return _buildRealServerCard(s.status, s.state, s.spi); return _buildRealServerCard(s.status, s.state, s.spi);
}, },
@@ -159,13 +159,10 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
ServerState cs, ServerState cs,
ServerPrivateInfo spi, ServerPrivateInfo spi,
) { ) {
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/'); final rootDisk = findRootDisk(ss.disk);
return InkWell( return InkWell(
onTap: () => AppRoute( onTap: () => AppRoute.serverDetail(spi: spi).go(context),
ServerDetailPage(spi.id),
'server detail page',
).go(context),
child: Stack( child: Stack(
children: [ children: [
Positioned( Positioned(
@@ -185,8 +182,8 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
_buildPercentCircle(ss.mem.usedPercent * 100), _buildPercentCircle(ss.mem.usedPercent * 100),
_buildNet(ss), _buildNet(ss),
_buildIOData( _buildIOData(
'Total:\n${rootDisk.size}', 'Total:\n${rootDisk?.size}',
'Used:\n${rootDisk.usedPercent}%', 'Used:\n${rootDisk?.usedPercent}%',
) )
], ],
), ),
@@ -253,7 +250,7 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
); );
return Text( return Text(
topRightStr, topRightStr,
style: textSize12Grey, style: textSize11Grey,
textScaleFactor: 1.0, textScaleFactor: 1.0,
); );
} }
@@ -373,7 +370,9 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
@override @override
Future<void> afterFirstLayout(BuildContext context) async { Future<void> afterFirstLayout(BuildContext context) async {
doUpdate(context); if (_setting.autoCheckAppUpdate.fetch()!) {
doUpdate(context);
}
await GetIt.I.allReady(); await GetIt.I.allReady();
await _serverProvider.loadLocalData(); await _serverProvider.loadLocalData();
await _serverProvider.refreshData(); await _serverProvider.refreshData();

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.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/navigator.dart';
import 'package:toolbox/core/extension/numx.dart'; import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/utils/misc.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/provider/private_key.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../../locator.dart'; import '../../../locator.dart';
import '../../widget/custom_appbar.dart';
const _format = 'text/plain'; const _format = 'text/plain';
class PrivateKeyEditPage extends StatefulWidget { 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 @override
_PrivateKeyEditPageState createState() => _PrivateKeyEditPageState(); _PrivateKeyEditPageState createState() => _PrivateKeyEditPageState();
@@ -43,7 +43,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
late PrivateKeyProvider _provider; late PrivateKeyProvider _provider;
late S _s; late S _s;
Widget _loading = nil; Widget? _loading;
@override @override
void initState() { void initState() {
@@ -51,6 +51,17 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
_provider = locator<PrivateKeyProvider>(); _provider = locator<PrivateKeyProvider>();
} }
@override
void dispose() {
super.dispose();
_nameController.dispose();
_keyController.dispose();
_pwdController.dispose();
_nameNode.dispose();
_keyNode.dispose();
_pwdNode.dispose();
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -68,20 +79,35 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
} }
PreferredSizeWidget _buildAppBar() { PreferredSizeWidget _buildAppBar() {
final actions = widget.info == null final actions = [
? null IconButton(
: [ tooltip: _s.delete,
IconButton( onPressed: () {
tooltip: _s.delete, showRoundDialog(
context: context,
title: Text(_s.attention),
child: Text(_s.sureDelete(widget.pki!.id)),
actions: [
TextButton(
onPressed: () { onPressed: () {
_provider.delInfo(widget.info!); _provider.delete(widget.pki!);
context.pop();
context.pop(); context.pop();
}, },
icon: const Icon(Icons.delete)) child: Text(
]; _s.ok,
return AppBar( style: textRed,
),
),
],
);
},
icon: const Icon(Icons.delete),
)
];
return CustomAppBar(
title: Text(_s.edit, style: textSize18), title: Text(_s.edit, style: textSize18),
actions: actions, actions: widget.pki == null ? null : actions,
); );
} }
@@ -100,22 +126,22 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
setState(() { setState(() {
_loading = centerSizedLoading; _loading = centerSizedLoading;
}); });
final info = PrivateKeyInfo(name, key, '');
try { 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) { } catch (e) {
showSnackBar(context, Text(e.toString())); showSnackBar(context, Text(e.toString()));
rethrow; rethrow;
} finally { } finally {
setState(() { setState(() {
_loading = nil; _loading = null;
}); });
} }
if (widget.info != null) {
_provider.updateInfo(widget.info!, info);
} else {
_provider.addInfo(info);
}
context.pop(); context.pop();
}, },
child: const Icon(Icons.save), child: const Icon(Icons.save),
@@ -127,6 +153,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
children: [ children: [
Input( Input(
autoFocus: true,
controller: _nameController, controller: _nameController,
type: TextInputType.text, type: TextInputType.text,
node: _nameNode, node: _nameNode,
@@ -185,17 +212,16 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
icon: Icons.password, icon: Icons.password,
), ),
SizedBox(height: MediaQuery.of(context).size.height * 0.1), SizedBox(height: MediaQuery.of(context).size.height * 0.1),
_loading _loading ?? placeholder,
], ],
); );
} }
@override @override
Future<void> afterFirstLayout(BuildContext context) async { Future<void> afterFirstLayout(BuildContext context) async {
if (widget.info != null) { if (widget.pki != null) {
_nameController.text = widget.info!.id; _nameController.text = widget.pki!.id;
_keyController.text = widget.info!.privateKey; _keyController.text = widget.pki!.key;
_pwdController.text = widget.info!.password;
} else { } else {
final clipdata = ((await Clipboard.getData(_format))?.text ?? '').trim(); final clipdata = ((await Clipboard.getData(_format))?.text ?? '').trim();
if (clipdata.startsWith('-----BEGIN') && clipdata.endsWith('-----')) { 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/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.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/route.dart';
import '../../../core/utils/platform.dart';
import '../../../data/model/server/private_key_info.dart';
import '../../../data/provider/private_key.dart'; import '../../../data/provider/private_key.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../widget/custom_appbar.dart';
import 'edit.dart'; import 'edit.dart';
import '../../../view/widget/round_rect_card.dart'; import '../../../view/widget/round_rect_card.dart';
@@ -15,7 +25,8 @@ class PrivateKeysListPage extends StatefulWidget {
_PrivateKeyListState createState() => _PrivateKeyListState(); _PrivateKeyListState createState() => _PrivateKeyListState();
} }
class _PrivateKeyListState extends State<PrivateKeysListPage> { class _PrivateKeyListState extends State<PrivateKeysListPage>
with AfterLayoutMixin {
late S _s; late S _s;
@override @override
@@ -27,7 +38,7 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
title: Text(_s.privateKey, style: textSize18), title: Text(_s.privateKey, style: textSize18),
), ),
body: _buildBody(), body: _buildBody(),
@@ -44,21 +55,21 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> {
Widget _buildBody() { Widget _buildBody() {
return Consumer<PrivateKeyProvider>( return Consumer<PrivateKeyProvider>(
builder: (_, key, __) { builder: (_, key, __) {
if (key.infos.isEmpty) { if (key.pkis.isEmpty) {
return Center( return Center(
child: Text(_s.noSavedPrivateKey), child: Text(_s.noSavedPrivateKey),
); );
} }
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
itemCount: key.infos.length, itemCount: key.pkis.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return RoundRectCard( return RoundRectCard(
ListTile( ListTile(
title: Text(key.infos[idx].id), title: Text(key.pkis[idx].id),
trailing: TextButton( trailing: TextButton(
onPressed: () => AppRoute( onPressed: () => AppRoute(
PrivateKeyEditPage(info: key.infos[idx]), PrivateKeyEditPage(pki: key.pkis[idx]),
'private key edit page', 'private key edit page',
).go(context), ).go(context),
child: Text(_s.edit), 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 'dart:async';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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/stringx.dart';
import 'package:toolbox/core/extension/uint8list.dart'; import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/utils/ui.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 '../../data/provider/server.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../widget/custom_appbar.dart';
class ProcessPage extends StatefulWidget { class ProcessPage extends StatefulWidget {
final ServerPrivateInfo spi; final ServerPrivateInfo spi;
@@ -25,6 +28,9 @@ class ProcessPage extends StatefulWidget {
class _ProcessPageState extends State<ProcessPage> { class _ProcessPageState extends State<ProcessPage> {
late S _s; late S _s;
late Timer _timer; late Timer _timer;
late MediaQueryData _media;
SSHClient? _client;
PsResult _result = PsResult(procs: []); PsResult _result = PsResult(procs: []);
int? _lastFocusId; int? _lastFocusId;
@@ -35,36 +41,35 @@ class _ProcessPageState extends State<ProcessPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final client = _serverProvider.servers[widget.spi.id]?.client; _client = _serverProvider.servers[widget.spi.id]?.client;
if (client == null) { _timer = Timer.periodic(const Duration(seconds: 3), (_) => _refresh());
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();
} }
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_s = S.of(context)!; _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 @override
@@ -104,12 +109,23 @@ class _ProcessPageState extends State<ProcessPage> {
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7), padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
itemBuilder: (ctx, idx) { itemBuilder: (ctx, idx) {
final proc = _result.procs[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( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
centerTitle: true, centerTitle: true,
title: TwoLineText(up: widget.spi.name, down: _s.process), title: TwoLineText(up: widget.spi.name, down: _s.process),
actions: actions, actions: actions,
@@ -119,28 +135,49 @@ class _ProcessPageState extends State<ProcessPage> {
} }
Widget _buildListItem(Proc proc) { Widget _buildListItem(Proc proc) {
return RoundRectCard(ListTile( return RoundRectCard(
leading: SizedBox( ListTile(
width: 57, leading: SizedBox(
child: TwoLineText(up: proc.pid.toString(), down: proc.user), 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), key: ValueKey(proc.pid),
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,
));
} }
} }

View File

@@ -1,11 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:nil/nil.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/core/extension/order.dart';
import 'package:toolbox/data/model/server/cpu.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/extension/numx.dart';
import '../../../core/route.dart';
import '../../../data/model/server/net_speed.dart'; import '../../../data/model/server/net_speed.dart';
import '../../../data/model/server/server.dart'; import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_status.dart'; import '../../../data/model/server/server_status.dart';
@@ -15,12 +18,13 @@ import '../../../data/res/default.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../../data/store/setting.dart'; import '../../../data/store/setting.dart';
import '../../../locator.dart'; import '../../../locator.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/round_rect_card.dart'; import '../../widget/round_rect_card.dart';
class ServerDetailPage extends StatefulWidget { 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 @override
_ServerDetailPageState createState() => _ServerDetailPageState(); _ServerDetailPageState createState() => _ServerDetailPageState();
@@ -62,7 +66,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<ServerProvider>(builder: (_, provider, __) { return Consumer<ServerProvider>(builder: (_, provider, __) {
final s = provider.servers[widget.id]; final s = provider.servers[widget.spi.id];
if (s == null) { if (s == null) {
return Scaffold( return Scaffold(
body: Center( body: Center(
@@ -75,37 +79,42 @@ class _ServerDetailPageState extends State<ServerDetailPage>
} }
Widget _buildMainPage(Server si) { Widget _buildMainPage(Server si) {
final buildFuncs = !_setting.moveOutServerTabFuncBtns.fetch()!;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
title: Text(si.spi.name, style: textSize18), 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( padding: EdgeInsets.only(
left: 13, right: 13, top: 13, bottom: _media.padding.bottom), left: 13,
onReorder: (int oldIndex, int newIndex) { right: 13,
setState(() { bottom: _media.padding.bottom + 77,
_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),
),
), ),
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) { Widget _buildCPUView(ServerStatus ss) {
final percent = ss.cpu.usedPercent(coreIdx: 0).toInt();
return RoundRectCard( return RoundRectCard(
Padding( Padding(
padding: roundRectCardPadding, padding: roundRectCardPadding,
@@ -113,10 +122,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( _buildAnimatedText(
'${ss.cpu.usedPercent(coreIdx: 0).toInt()}%', ValueKey(percent),
style: textSize27, '$percent%',
textScaleFactor: 1.0, textSize27,
), ),
Row( Row(
children: [ children: [
@@ -206,6 +215,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
final free = ss.mem.free / ss.mem.total * 100; final free = ss.mem.free / ss.mem.total * 100;
final avail = ss.mem.availPercent * 100; final avail = ss.mem.availPercent * 100;
final used = ss.mem.usedPercent * 100; final used = ss.mem.usedPercent * 100;
final usedStr = used.toStringAsFixed(0);
return RoundRectCard( return RoundRectCard(
Padding( Padding(
@@ -219,7 +229,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
children: [ children: [
Row( Row(
children: [ children: [
Text('${used.toStringAsFixed(0)}%', style: textSize27), _buildAnimatedText(
ValueKey(usedStr),
'$usedStr%',
textSize27,
),
width7, width7,
Text('of ${(ss.mem.total * 1024).convertBytes}', Text('of ${(ss.mem.total * 1024).convertBytes}',
style: textSize13Grey) style: textSize13Grey)
@@ -243,7 +257,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
} }
Widget _buildSwapView(ServerStatus ss) { 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 used = ss.swap.usedPercent * 100;
final cached = ss.swap.cached / ss.swap.total * 100; final cached = ss.swap.cached / ss.swap.total * 100;
return RoundRectCard( return RoundRectCard(
@@ -367,7 +381,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SizedBox( SizedBox(
width: width / 1.2, width: width,
child: Text( child: Text(
device, device,
style: textSize11, style: textSize11,
@@ -402,7 +416,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildTemperature(ServerStatus ss) { Widget _buildTemperature(ServerStatus ss) {
final temps = ss.temps; final temps = ss.temps;
if (temps.isEmpty) { if (temps.isEmpty) {
return nil; return placeholder;
} }
final List<Widget> children = [ final List<Widget> children = [
const Row( const Row(
@@ -432,9 +446,27 @@ class _ServerDetailPageState extends State<ServerDetailPage>
), ),
], ],
))); )));
return RoundRectCard(Padding( return RoundRectCard(
padding: roundRectCardPadding, Padding(
child: Column(children: children), 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/res/ui.dart';
import '../../../data/store/private_key.dart'; import '../../../data/store/private_key.dart';
import '../../../locator.dart'; import '../../../locator.dart';
import '../../widget/tag/editor.dart'; import '../../widget/custom_appbar.dart';
import '../../widget/tag.dart';
import '../private_key/edit.dart'; import '../private_key/edit.dart';
class ServerEditPage extends StatefulWidget { class ServerEditPage extends StatefulWidget {
@@ -55,6 +56,22 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_serverProvider = locator<ServerProvider>(); _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 @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -83,24 +100,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
onPressed: () { onPressed: () {
_serverProvider.delServer(widget.spi!.id); _serverProvider.delServer(widget.spi!.id);
context.pop(); context.pop();
context.pop(); context.pop(true);
}, },
child: Text( child: Text(_s.ok, style: textRed),
_s.ok,
style: const TextStyle(color: Colors.red),
),
), ),
TextButton(
onPressed: () => context.pop(),
child: Text(_s.cancel),
)
], ],
); );
}, },
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
); );
final actions = widget.spi != null ? [delBtn] : null; final actions = widget.spi != null ? [delBtn] : null;
return AppBar( return CustomAppBar(
title: Text(_s.edit, style: textSize18), title: Text(_s.edit, style: textSize18),
actions: actions, actions: actions,
); );
@@ -109,6 +119,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
Widget _buildForm() { Widget _buildForm() {
final children = [ final children = [
Input( Input(
autoFocus: true,
controller: _nameController, controller: _nameController,
type: TextInputType.text, type: TextInputType.text,
node: _nameFocus, node: _nameFocus,
@@ -200,17 +211,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
Widget _buildKeyAuth() { Widget _buildKeyAuth() {
return Consumer<PrivateKeyProvider>( return Consumer<PrivateKeyProvider>(
builder: (_, key, __) { builder: (_, key, __) {
for (var item in key.infos) { for (var item in key.pkis) {
if (item.id == widget.spi?.pubKeyId) { 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( .map(
(e) => ListTile( (e) => ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(e.id, textAlign: TextAlign.start), title: Text(e.id, textAlign: TextAlign.start),
trailing: _buildRadio(key.infos.indexOf(e), e), trailing: _buildRadio(key.pkis.indexOf(e), e),
), ),
) )
.toList(); .toList();

View File

@@ -3,35 +3,26 @@ import 'package:circle_chart/circle_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:nil/nil.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/core/extension/media_queryx.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 '../../../core/route.dart'; import '../../../core/route.dart';
import '../../../core/utils/misc.dart' hide pathJoin;
import '../../../core/utils/platform.dart';
import '../../../core/utils/ui.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.dart';
import '../../../data/model/server/server_private_info.dart'; import '../../../data/model/server/server_private_info.dart';
import '../../../data/model/server/server_status.dart'; import '../../../data/model/server/server_status.dart';
import '../../../data/provider/server.dart'; import '../../../data/provider/server.dart';
import '../../../data/res/color.dart'; import '../../../data/res/color.dart';
import '../../../data/model/app/menu.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../../data/store/setting.dart'; import '../../../data/store/setting.dart';
import '../../../locator.dart'; import '../../../locator.dart';
import '../../widget/popup_menu.dart';
import '../../widget/round_rect_card.dart'; import '../../widget/round_rect_card.dart';
import '../docker.dart'; import '../../widget/server_func_btns.dart';
import '../pkg.dart'; import '../../widget/tag.dart';
import '../storage/sftp.dart';
import '../ssh/term.dart';
import 'detail.dart';
import 'edit.dart'; import 'edit.dart';
class ServerPage extends StatefulWidget { class ServerPage extends StatefulWidget {
@@ -44,12 +35,12 @@ class ServerPage extends StatefulWidget {
class _ServerPageState extends State<ServerPage> class _ServerPageState extends State<ServerPage>
with AutomaticKeepAliveClientMixin, AfterLayoutMixin { with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late MediaQueryData _media; late MediaQueryData _media;
late ThemeData _theme;
late ServerProvider _serverProvider; late ServerProvider _serverProvider;
late SettingStore _settingStore; late SettingStore _settingStore;
late S _s; late S _s;
String? _tag; String? _tag;
bool _useDoubleColumn = false;
@override @override
void initState() { void initState() {
@@ -62,7 +53,7 @@ class _ServerPageState extends State<ServerPage>
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_media = MediaQuery.of(context); _media = MediaQuery.of(context);
_theme = Theme.of(context); _useDoubleColumn = _media.useDoubleColumn;
_s = S.of(context)!; _s = S.of(context)!;
} }
@@ -84,72 +75,132 @@ class _ServerPageState extends State<ServerPage>
} }
Widget _buildBody() { 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( return RefreshIndicator(
onRefresh: () async => onRefresh: () async =>
await _serverProvider.refreshData(onlyFailed: true), await _serverProvider.refreshData(onlyFailed: true),
child: Consumer<ServerProvider>( child: child,
builder: (_, pro, __) { );
if (!pro.tags.contains(_tag)) { }
_tag = null;
} List<String> _filterServers(ServerProvider pro) => pro.serverOrder
if (pro.serverOrder.isEmpty) { .where((e) => pro.servers.containsKey(e))
return Center( .where((e) =>
child: Text( _tag == null || (pro.servers[e]?.spi.tags?.contains(_tag) ?? false))
_s.serverTabEmpty, .toList();
textAlign: TextAlign.center,
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), Expanded(
onReorder: (oldIndex, newIndex) => setState(() { child: _buildBodySmall(
pro.serverOrder.moveByItem( provider: pro,
filtered, filtered: right,
oldIndex, padding: const EdgeInsets.fromLTRB(0, 0, 7, 7),
newIndex, buildTags: false,
property: _settingStore.serverOrder, ),
);
}),
buildDefaultDragHandles: false,
itemBuilder: (_, index) => ReorderableDelayedDragStartListener(
key: ValueKey('$_tag${filtered[index]}'),
index: index,
child: _buildEachServerCard(pro.servers[filtered[index]]),
), ),
itemCount: filtered.length, ],
); ))
}, ],
),
); );
} }
Widget _buildEachServerCard(Server? si) { Widget _buildEachServerCard(Server? si) {
if (si == null) { if (si == null) {
return nil; return placeholder;
} }
return GestureDetector(
return RoundRectCard(
key: Key(si.spi.id + (_tag ?? '')), key: Key(si.spi.id + (_tag ?? '')),
onTap: () => AppRoute( InkWell(
ServerDetailPage(si.spi.id), onTap: () {
'server detail page', if (si.state.canViewDetails) {
).go(context), AppRoute.serverDetail(spi: si.spi).go(context);
child: RoundRectCard( } else if (si.status.failedInfo != null) {
Padding( _showFailReason(si.status);
}
},
onLongPress: () => AppRoute.serverEdit(spi: si.spi).go(context),
child: Padding(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
child: _buildRealServerCard(si.status, si.state, si.spi), 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( Widget _buildRealServerCard(
ServerStatus ss, ServerStatus ss,
ServerState cs, ServerState cs,
ServerPrivateInfo spi, ServerPrivateInfo spi,
) { ) {
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/'); final rootDisk = findRootDisk(ss.disk);
late final List<Widget> children;
return Column( double? height;
crossAxisAlignment: CrossAxisAlignment.start, if (cs != ServerState.finished) {
children: [ height = 23.0;
children = [
_buildServerCardTitle(ss, cs, spi),
];
} else {
height = 107;
children = [
_buildServerCardTitle(ss, cs, spi), _buildServerCardTitle(ss, cs, spi),
height13, height13,
Row( Padding(
mainAxisAlignment: MainAxisAlignment.spaceAround, padding: const EdgeInsets.symmetric(horizontal: 13),
children: [ child: Row(
_buildPercentCircle(ss.cpu.usedPercent()), mainAxisAlignment: MainAxisAlignment.spaceBetween,
_buildPercentCircle(ss.mem.usedPercent * 100), children: [
_buildNet(ss), _wrapWithSizedbox(_buildPercentCircle(ss.cpu.usedPercent())),
_buildIOData( _wrapWithSizedbox(_buildPercentCircle(ss.mem.usedPercent * 100)),
'Total:\n${rootDisk.size}', 'Used:\n${rootDisk.usedPercent}%') _wrapWithSizedbox(_buildNet(ss)),
], _wrapWithSizedbox(_buildIOData(
'Total:\n${rootDisk?.size}',
'Used:\n${rootDisk?.usedPercent}%',
)),
],
),
), ),
height13, height13,
Row( if (_settingStore.moveOutServerTabFuncBtns.fetch()!)
mainAxisAlignment: MainAxisAlignment.spaceAround, SizedBox(
children: [ height: 27,
_buildExplainText('CPU'), child: ServerFuncBtns(spi: spi, s: _s),
_buildExplainText('Mem'), ),
_buildExplainText('Net'), ];
_buildExplainText('Disk'), }
],
), return AnimatedContainer(
const SizedBox(height: 3), 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: [ children: [
Text( Text(
spi.name, spi.name,
style: style: textSize13Bold,
const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
textScaleFactor: 1.0, textScaleFactor: 1.0,
), ),
const Icon( const Icon(
@@ -219,14 +296,7 @@ class _ServerPageState extends State<ServerPage>
) )
], ],
), ),
Row( _buildTopRightText(ss, cs),
children: [
_buildTopRightText(ss, cs),
width7,
_buildSSHBtn(spi),
_buildMoreBtn(spi),
],
)
], ],
), ),
); );
@@ -239,95 +309,36 @@ class _ServerPageState extends State<ServerPage>
ss.uptime, ss.uptime,
ss.failedInfo, ss.failedInfo,
); );
final hasError = cs == ServerState.failed && ss.failedInfo != null; if (cs == ServerState.failed && ss.failedInfo != null) {
return hasError return GestureDetector(
? GestureDetector( onTap: () => _showFailReason(ss),
onTap: () => showRoundDialog( child: Text(
context: context, _s.viewErr,
title: Text(_s.error), style: textSize11Grey,
child: Text(ss.failedInfo ?? _s.unknownError), textScaleFactor: 1.0,
actions: [ ),
TextButton( );
onPressed: () => }
copy2Clipboard(ss.failedInfo ?? _s.unknownError), return Text(
child: Text(_s.copy), topRightStr,
) style: textSize11Grey,
], textScaleFactor: 1.0,
),
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),
); );
} }
Widget _buildMoreBtn(ServerPrivateInfo spi) { void _showFailReason(ServerStatus ss) {
return PopupMenu( showRoundDialog(
items: ServerTabMenuType.values.map((e) => e.build(_s)).toList(), context: context,
onSelected: (ServerTabMenuType value) async { title: Text(_s.error),
switch (value) { child: SingleChildScrollView(
case ServerTabMenuType.pkg: child: Text(ss.failedInfo ?? _s.unknownError),
AppRoute(PkgManagePage(spi), 'pkg manage').go(context); ),
break; actions: [
case ServerTabMenuType.sftp: TextButton(
AppRoute(SftpPage(spi), 'SFTP').go(context); onPressed: () => copy2Clipboard(ss.failedInfo!),
break; child: Text(_s.copy),
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;
}
},
); );
} }
@@ -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( String _getTopRightStr(
ServerState cs, ServerState cs,
double? temp, double? temp,
@@ -365,12 +364,16 @@ class _ServerPageState extends State<ServerPage>
switch (cs) { switch (cs) {
case ServerState.disconnected: case ServerState.disconnected:
return _s.disconnected; return _s.disconnected;
case ServerState.connected: case ServerState.finished:
final tempStr = temp == null ? '' : '${temp.toStringAsFixed(1)}°C'; final tempStr = temp == null ? '' : '${temp.toStringAsFixed(1)}°C';
final items = [tempStr, upTime]; final items = [tempStr, upTime];
final str = items.where((element) => element.isNotEmpty).join(' | '); final str = items.where((element) => element.isNotEmpty).join(' | ');
if (str.isEmpty) return _s.serverTabLoading; if (str.isEmpty) return _s.noResult;
return str; return str;
case ServerState.loading:
return _s.serverTabLoading;
case ServerState.connected:
return _s.connected;
case ServerState.connecting: case ServerState.connecting:
return _s.serverTabConnecting; return _s.serverTabConnecting;
case ServerState.failed: case ServerState.failed:
@@ -381,65 +384,56 @@ class _ServerPageState extends State<ServerPage>
return _s.serverTabPlzSave; return _s.serverTabPlzSave;
} }
return failedInfo; return failedInfo;
default:
return _s.serverTabUnkown;
} }
} }
Widget _buildIOData(String up, String down) { Widget _buildIOData(String up, String down) {
final statusTextStyle = TextStyle( return Column(
fontSize: 9, color: _theme.textTheme.bodyLarge!.color!.withAlpha(177)); children: [
return SizedBox( const SizedBox(height: 5),
width: _media.size.width * 0.2, Text(
child: Column( up,
children: [ style: textSize9Grey,
const SizedBox(height: 5), textAlign: TextAlign.center,
Text( textScaleFactor: 1.0,
up, ),
style: statusTextStyle, const SizedBox(height: 3),
textAlign: TextAlign.center, Text(
textScaleFactor: 1.0, down,
), style: textSize9Grey,
const SizedBox(height: 3), textAlign: TextAlign.center,
Text( textScaleFactor: 1.0,
down, )
style: statusTextStyle, ],
textAlign: TextAlign.center,
textScaleFactor: 1.0,
)
],
),
); );
} }
Widget _buildPercentCircle(double percent) { Widget _buildPercentCircle(double percent) {
if (percent <= 0) percent = 0.01; if (percent <= 0) percent = 0.01;
if (percent >= 100) percent = 99.9; if (percent >= 100) percent = 99.9;
return SizedBox( return Stack(
width: _media.size.width * 0.2, children: [
child: Stack( Center(
children: [ child: CircleChart(
Center( progressColor: primaryColor,
child: CircleChart( progressNumber: percent,
progressColor: primaryColor, maxNumber: 100,
progressNumber: percent, width: 53,
maxNumber: 100, height: 53,
width: 53, animationDuration: const Duration(milliseconds: 777),
height: 53, ),
),
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/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_highlight/theme_map.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:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.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/locale.dart';
import 'package:toolbox/core/extension/navigator.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/core/route.dart';
import 'package:toolbox/data/model/app/net_view.dart'; import 'package:toolbox/data/model/app/net_view.dart';
import 'package:toolbox/data/model/app/tab.dart'; import 'package:toolbox/view/page/setting/virt_key.dart';
import 'package:toolbox/view/page/ssh/virt_key_setting.dart';
import 'package:toolbox/view/widget/input_field.dart'; import 'package:toolbox/view/widget/input_field.dart';
import 'package:toolbox/view/widget/value_notifier.dart'; import 'package:toolbox/view/widget/value_notifier.dart';
import '../../core/utils/misc.dart'; import '../../../core/utils/misc.dart';
import '../../core/utils/platform.dart'; import '../../../core/utils/platform.dart';
import '../../core/update.dart'; import '../../../core/update.dart';
import '../../core/utils/ui.dart'; import '../../../core/utils/ui.dart';
import '../../data/provider/app.dart'; import '../../../data/provider/app.dart';
import '../../data/provider/server.dart'; import '../../../data/provider/server.dart';
import '../../data/res/build_data.dart'; import '../../../data/res/build_data.dart';
import '../../data/res/color.dart'; import '../../../data/res/color.dart';
import '../../data/res/path.dart'; import '../../../data/res/path.dart';
import '../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../data/store/server.dart'; import '../../../data/store/server.dart';
import '../../data/store/setting.dart'; import '../../../data/store/setting.dart';
import '../../locator.dart'; import '../../../locator.dart';
import '../widget/future_widget.dart'; import '../../widget/custom_appbar.dart';
import '../widget/round_rect_card.dart'; import '../../widget/future_widget.dart';
import '../../widget/round_rect_card.dart';
class SettingPage extends StatefulWidget { class SettingPage extends StatefulWidget {
const SettingPage({Key? key}) : super(key: key); const SettingPage({Key? key}) : super(key: key);
@@ -41,7 +43,7 @@ class SettingPage extends StatefulWidget {
class _SettingPageState extends State<SettingPage> { class _SettingPageState extends State<SettingPage> {
final _themeKey = GlobalKey<PopupMenuButtonState<int>>(); final _themeKey = GlobalKey<PopupMenuButtonState<int>>();
final _startPageKey = GlobalKey<PopupMenuButtonState<int>>(); //final _startPageKey = GlobalKey<PopupMenuButtonState<int>>();
final _updateIntervalKey = GlobalKey<PopupMenuButtonState<int>>(); final _updateIntervalKey = GlobalKey<PopupMenuButtonState<int>>();
final _maxRetryKey = GlobalKey<PopupMenuButtonState<int>>(); final _maxRetryKey = GlobalKey<PopupMenuButtonState<int>>();
final _localeKey = GlobalKey<PopupMenuButtonState<String>>(); final _localeKey = GlobalKey<PopupMenuButtonState<String>>();
@@ -53,7 +55,6 @@ class _SettingPageState extends State<SettingPage> {
late final SettingStore _setting; late final SettingStore _setting;
late final ServerProvider _serverProvider; late final ServerProvider _serverProvider;
late MediaQueryData _media;
late S _s; late S _s;
late SharedPreferences _sp; late SharedPreferences _sp;
@@ -62,7 +63,8 @@ class _SettingPageState extends State<SettingPage> {
final _nightMode = ValueNotifier(0); final _nightMode = ValueNotifier(0);
final _maxRetryCount = ValueNotifier(0); final _maxRetryCount = ValueNotifier(0);
final _updateInterval = 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 _localeCode = ValueNotifier('');
final _editorTheme = ValueNotifier(''); final _editorTheme = ValueNotifier('');
final _editorDarkTheme = ValueNotifier(''); final _editorDarkTheme = ValueNotifier('');
@@ -75,7 +77,6 @@ class _SettingPageState extends State<SettingPage> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_media = MediaQuery.of(context);
_s = S.of(context)!; _s = S.of(context)!;
_localeCode.value = _setting.locale.fetch() ?? _s.localeName; _localeCode.value = _setting.locale.fetch() ?? _s.localeName;
} }
@@ -90,7 +91,8 @@ class _SettingPageState extends State<SettingPage> {
_updateInterval.value = _setting.serverStatusUpdateInterval.fetch()!; _updateInterval.value = _setting.serverStatusUpdateInterval.fetch()!;
_maxRetryCount.value = _setting.maxRetryCount.fetch()!; _maxRetryCount.value = _setting.maxRetryCount.fetch()!;
_selectedColorValue.value = _setting.primaryColor.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()!; _editorTheme.value = _setting.editorTheme.fetch()!;
_editorDarkTheme.value = _setting.editorDarkTheme.fetch()!; _editorDarkTheme.value = _setting.editorDarkTheme.fetch()!;
_keyboardType.value = _setting.keyboardType.fetch()!; _keyboardType.value = _setting.keyboardType.fetch()!;
@@ -102,7 +104,7 @@ class _SettingPageState extends State<SettingPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
title: Text(_s.setting), title: Text(_s.setting),
), ),
body: ListView( body: ListView(
@@ -141,7 +143,7 @@ class _SettingPageState extends State<SettingPage> {
_buildLocale(), _buildLocale(),
_buildThemeMode(), _buildThemeMode(),
_buildAppColor(), _buildAppColor(),
_buildLaunchPage(), //_buildLaunchPage(),
_buildCheckUpdate(), _buildCheckUpdate(),
]; ];
if (isIOS) { if (isIOS) {
@@ -170,6 +172,9 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildServer() { Widget _buildServer() {
return Column( return Column(
children: [ children: [
_buildMoveOutServerFuncBtns(),
_buildServerOrder(),
_buildServerDetailOrder(),
_buildNetViewType(), _buildNetViewType(),
_buildUpdateInterval(), _buildUpdateInterval(),
_buildMaxRetry(), _buildMaxRetry(),
@@ -194,6 +199,7 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildEditor() { Widget _buildEditor() {
return Column( return Column(
children: [ children: [
_buildEditorFontSize(),
_buildEditorTheme(), _buildEditorTheme(),
_buildEditorDarkTheme(), _buildEditorDarkTheme(),
].map((e) => RoundRectCard(e)).toList(), ].map((e) => RoundRectCard(e)).toList(),
@@ -214,11 +220,10 @@ class _SettingPageState extends State<SettingPage> {
display = _s.versionUnknownUpdate(BuildData.build); display = _s.versionUnknownUpdate(BuildData.build);
} }
return ListTile( return ListTile(
trailing: const Icon(Icons.keyboard_arrow_right), title: Text(_s.autoCheckUpdate),
title: Text( subtitle: Text(display, style: grey),
display,
),
onTap: () => doUpdate(ctx, force: true), onTap: () => doUpdate(ctx, force: true),
trailing: buildSwitch(context, _setting.autoCheckAppUpdate),
); );
}, },
); );
@@ -277,81 +282,76 @@ class _SettingPageState extends State<SettingPage> {
width: 27, width: 27,
), ),
), ),
title: Text( title: Text(_s.primaryColorSeed),
_s.primaryColor,
),
onTap: () async { onTap: () async {
final ctrl = TextEditingController(text: primaryColor.toHex);
await showRoundDialog( await showRoundDialog(
context: context, context: context,
title: Text(_s.primaryColor), title: Text(_s.primaryColorSeed),
child: SizedBox( child: Input(
height: 211, autoFocus: true,
child: Center( onSubmitted: _onSaveColor,
child: MaterialColorPicker( controller: ctrl,
shrinkWrap: true, hint: '#8b2252',
allowShades: true, icon: Icons.colorize,
onColorChange: (color) {
_selectedColorValue.value = color.value;
},
selectedColor: primaryColor,
),
),
), ),
actions: [
TextButton(
onPressed: () {
_setting.primaryColor.put(_selectedColorValue.value);
Navigator.pop(context);
_showRestartSnackbar();
},
child: Text(_s.ok),
)
],
); );
}, },
); );
} }
Widget _buildLaunchPage() { void _onSaveColor(String s) {
final items = AppTab.values final color = s.hexToColor;
.map( if (color == null) {
(e) => PopupMenuItem( showSnackBar(context, Text(_s.failed));
value: e.index, return;
child: Text(tabTitleName(context, e)), }
), _selectedColorValue.value = color.value;
) _setting.primaryColor.put(_selectedColorValue.value);
.toList(); context.pop();
_showRestartSnackbar();
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 _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() { Widget _buildMaxRetry() {
final items = List.generate( final items = List.generate(
10, 10,
@@ -550,45 +550,14 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildTermFontSize() { Widget _buildTermFontSize() {
return ValueBuilder( return ValueBuilder(
listenable: _fontSize, listenable: _termFontSize,
build: () => ListTile( build: () => ListTile(
title: Text(_s.fontSize), title: Text(_s.fontSize),
trailing: Text( trailing: Text(
_fontSize.value.toString(), _termFontSize.value.toString(),
style: textSize15, style: textSize15,
), ),
onTap: () { onTap: () => _showFontSizeDialog(_termFontSize, _setting.termFontSize),
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),
),
],
);
},
), ),
); );
} }
@@ -615,6 +584,7 @@ class _SettingPageState extends State<SettingPage> {
context: context, context: context,
title: Text(_s.diskIgnorePath), title: Text(_s.diskIgnorePath),
child: Input( child: Input(
autoFocus: true,
controller: ctrller, controller: ctrller,
label: 'JSON', label: 'JSON',
type: TextInputType.visiblePassword, type: TextInputType.visiblePassword,
@@ -681,7 +651,7 @@ class _SettingPageState extends State<SettingPage> {
}, },
).toList(); ).toList();
return ListTile( return ListTile(
title: Text(_s.light + _s.theme), title: Text('${_s.light} ${_s.theme.toLowerCase()}'),
trailing: ValueBuilder( trailing: ValueBuilder(
listenable: _editorTheme, listenable: _editorTheme,
build: () => PopupMenuButton( build: () => PopupMenuButton(
@@ -714,7 +684,7 @@ class _SettingPageState extends State<SettingPage> {
}, },
).toList(); ).toList();
return ListTile( return ListTile(
title: Text(_s.dark + _s.theme), title: Text('${_s.dark} ${_s.theme.toLowerCase()}'),
trailing: ValueBuilder( trailing: ValueBuilder(
listenable: _editorDarkTheme, listenable: _editorDarkTheme,
build: () => PopupMenuButton( build: () => PopupMenuButton(
@@ -885,6 +855,7 @@ class _SettingPageState extends State<SettingPage> {
context: context, context: context,
title: Text(_s.homeWidgetUrlConfig), title: Text(_s.homeWidgetUrlConfig),
child: Input( child: Input(
autoFocus: true,
controller: ctrl, controller: ctrl,
label: 'JSON', label: 'JSON',
type: TextInputType.visiblePassword, 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/locator.dart';
import 'package:toolbox/view/widget/round_rect_card.dart'; import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../widget/custom_appbar.dart';
class SSHVirtKeySettingPage extends StatefulWidget { class SSHVirtKeySettingPage extends StatefulWidget {
const SSHVirtKeySettingPage({Key? key}) : super(key: key); const SSHVirtKeySettingPage({Key? key}) : super(key: key);
@@ -29,7 +31,7 @@ class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
title: Text(_s.editVirtKeys), title: Text(_s.editVirtKeys),
), ),
body: _buildBody(), body: _buildBody(),
@@ -50,13 +52,14 @@ class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {
final key = allKeys[idx]; final key = allKeys[idx];
final help = key.help(_s); final help = key.help(_s);
return RoundRectCard( return RoundRectCard(
key: ValueKey(idx), key: ValueKey(idx),
ListTile( ListTile(
title: _buildTitle(key), title: _buildTitle(key),
subtitle: help == null ? null : Text(help, style: grey), subtitle: help == null ? null : Text(help, style: grey),
leading: _buildCheckBox(keys, key, idx, idx < keys.length), leading: _buildCheckBox(keys, key, idx, idx < keys.length),
trailing: isDesktop ? null : const Icon(Icons.drag_handle), trailing: isDesktop ? null : const Icon(Icons.drag_handle),
)); ),
);
}, },
itemCount: allKeys.length, itemCount: allKeys.length,
onReorder: (o, n) { onReorder: (o, n) {

View File

@@ -9,7 +9,8 @@ import '../../../data/model/server/snippet.dart';
import '../../../data/provider/snippet.dart'; import '../../../data/provider/snippet.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../../locator.dart'; import '../../../locator.dart';
import '../../widget/tag/editor.dart'; import '../../widget/custom_appbar.dart';
import '../../widget/tag.dart';
class SnippetEditPage extends StatefulWidget { class SnippetEditPage extends StatefulWidget {
const SnippetEditPage({Key? key, this.snippet}) : super(key: key); const SnippetEditPage({Key? key, this.snippet}) : super(key: key);
@@ -37,6 +38,14 @@ class _SnippetEditPageState extends State<SnippetEditPage>
_provider = locator<SnippetProvider>(); _provider = locator<SnippetProvider>();
} }
@override
void dispose() {
super.dispose();
_nameController.dispose();
_scriptController.dispose();
_scriptNode.dispose();
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -46,7 +55,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
title: Text(_s.edit, style: textSize18), title: Text(_s.edit, style: textSize18),
actions: _buildAppBarActions(), actions: _buildAppBarActions(),
), ),
@@ -98,6 +107,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
children: [ children: [
Input( Input(
autoFocus: true,
controller: _nameController, controller: _nameController,
type: TextInputType.text, type: TextInputType.text,
onSubmitted: (_) => FocusScope.of(context).requestFocus(_scriptNode), 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:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/order.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/misc.dart';
import '../../../core/utils/ui.dart'; import '../../../core/utils/ui.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/snippet.dart'; import '../../../data/model/server/snippet.dart';
import '../../../data/provider/server.dart';
import '../../../data/res/ui.dart';
import '../../../data/store/setting.dart'; import '../../../data/store/setting.dart';
import '../../../locator.dart'; import '../../../locator.dart';
import '../../widget/tag/picker.dart'; import '../../widget/tag.dart';
import '/core/route.dart'; import '/core/route.dart';
import '/data/provider/snippet.dart'; import '/data/provider/snippet.dart';
import 'edit.dart'; import 'edit.dart';
@@ -69,7 +68,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
.toList(); .toList();
return ReorderableListView.builder( return ReorderableListView.builder(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.symmetric(horizontal: 13),
itemCount: filtered.length, itemCount: filtered.length,
onReorder: (oldIdx, newIdx) => setState(() { onReorder: (oldIdx, newIdx) => setState(() {
provider.snippets.moveByItem( provider.snippets.moveByItem(
@@ -88,6 +87,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
all: _s.all, all: _s.all,
width: _media.size.width, width: _media.size.width,
), ),
footer: height77,
buildDefaultDragHandles: false, buildDefaultDragHandles: false,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final snippet = filtered.elementAt(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:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/data/res/server_cmd.dart';
import 'package:xterm/xterm.dart'; import 'package:xterm/xterm.dart';
import '../../../core/route.dart'; import '../../core/route.dart';
import '../../../core/utils/platform.dart'; import '../../core/utils/platform.dart';
import '../../../core/utils/misc.dart'; import '../../core/utils/misc.dart';
import '../../../core/utils/ui.dart'; import '../../core/utils/ui.dart';
import '../../../core/utils/server.dart'; import '../../core/utils/server.dart';
import '../../../data/model/server/server_private_info.dart'; import '../../data/model/server/server_private_info.dart';
import '../../../data/model/ssh/virtual_key.dart'; import '../../data/model/ssh/virtual_key.dart';
import '../../../data/provider/virtual_keyboard.dart'; import '../../data/provider/virtual_keyboard.dart';
import '../../../data/res/color.dart'; import '../../data/res/color.dart';
import '../../../data/res/terminal.dart'; import '../../data/res/terminal.dart';
import '../../../data/store/setting.dart'; import '../../data/store/setting.dart';
import '../../../locator.dart'; import '../../locator.dart';
import '../storage/sftp.dart';
const echoPWD = 'echo \$PWD';
class SSHPage extends StatefulWidget { class SSHPage extends StatefulWidget {
final ServerPrivateInfo spi; final ServerPrivateInfo spi;
@@ -46,13 +46,14 @@ class _SSHPageState extends State<SSHPage> {
late TerminalStyle _terminalStyle; late TerminalStyle _terminalStyle;
late TerminalTheme _terminalTheme; late TerminalTheme _terminalTheme;
late TextInputType _keyboardType; late TextInputType _keyboardType;
late SSHSession _session; double _virtKeyWidth = 0;
late double _virtKeyWidth; double _virtKeysHeight = 0;
late double _virtKeysHeight;
bool _isDark = false; bool _isDark = false;
Timer? _virtKeyLongPressTimer; Timer? _virtKeyLongPressTimer;
SSHClient? _client; SSHClient? _client;
SSHSession? _session;
Timer? _discontinuityTimer;
@override @override
void initState() { void initState() {
@@ -68,6 +69,19 @@ class _SSHPageState extends State<SSHPage> {
_initVirtKeys(); _initVirtKeys();
} }
@override
void dispose() {
super.dispose();
_virtKeyLongPressTimer?.cancel();
_terminalController.dispose();
if (_client?.isClosed == false) {
try {
_client?.close();
} catch (_) {}
}
_discontinuityTimer?.cancel();
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -75,15 +89,12 @@ class _SSHPageState extends State<SSHPage> {
_media = MediaQuery.of(context); _media = MediaQuery.of(context);
_s = S.of(context)!; _s = S.of(context)!;
_terminalTheme = _isDark ? termDarkTheme : termLightTheme; _terminalTheme = _isDark ? termDarkTheme : termLightTheme;
// Calculate virtkey width / height
_virtKeyWidth = _media.size.width / 7;
_virtKeysHeight = _media.size.height * 0.043 * _virtKeysList.length;
}
@override // Because the virtual keyboard only displayed on mobile devices
void dispose() { if (isMobile) {
_client?.close(); _virtKeyWidth = _media.size.width / 7;
super.dispose(); _virtKeysHeight = _media.size.height * 0.043 * _virtKeysList.length;
}
} }
@override @override
@@ -91,7 +102,7 @@ class _SSHPageState extends State<SSHPage> {
Widget child = Scaffold( Widget child = Scaffold(
backgroundColor: _terminalTheme.background, backgroundColor: _terminalTheme.background,
body: _buildBody(), body: _buildBody(),
bottomNavigationBar: _buildBottom(), bottomNavigationBar: isDesktop ? null : _buildBottom(),
); );
if (isIOS) { if (isIOS) {
child = AnnotatedRegion( child = AnnotatedRegion(
@@ -108,15 +119,19 @@ class _SSHPageState extends State<SSHPage> {
_virtKeysHeight - _virtKeysHeight -
_media.padding.bottom - _media.padding.bottom -
_media.padding.top, _media.padding.top,
child: TerminalView( child: Padding(
_terminal, padding: EdgeInsets.only(top: _media.padding.top),
controller: _terminalController, child: TerminalView(
keyboardType: _keyboardType, _terminal,
textStyle: _terminalStyle, controller: _terminalController,
theme: _terminalTheme, keyboardType: _keyboardType,
deleteDetection: isIOS, textStyle: _terminalStyle,
autofocus: true, theme: _terminalTheme,
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light, deleteDetection: isIOS,
autofocus: true,
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
hideScrollBar: isMobile,
),
), ),
); );
} }
@@ -259,13 +274,7 @@ class _SSHPageState extends State<SSHPage> {
); );
return; return;
} }
AppRoute( AppRoute.sftp(spi: widget.spi, initPath: initPath).go(context);
SftpPage(
widget.spi,
initPath: initPath,
),
'SSH SFTP')
.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.clear();
_terminal.buffer.setCursor(0, 0); _terminal.buffer.setCursor(0, 0);
_terminal.onOutput = (data) { _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?.stdout);
_listen(_session.stderr); _listen(_session?.stderr);
if (widget.initCmd != null) { if (widget.initCmd != null) {
_terminal.textInput(widget.initCmd!); _terminal.textInput(widget.initCmd!);
_terminal.keyInput(TerminalKey.enter); _terminal.keyInput(TerminalKey.enter);
} }
await _session.done; await _session?.done;
if (mounted) { if (mounted) {
context.pop(); context.pop();
} }
} }
void _listen(Stream<Uint8List> stream) { void _listen(Stream<Uint8List>? stream) {
if (stream == null) {
return;
}
stream stream
.cast<List<int>>() .cast<List<int>>()
.transform(const Utf8Decoder()) .transform(const Utf8Decoder())
.listen(_terminal.write); .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/data/res/misc.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/editor.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/input_field.dart';
import 'package:toolbox/view/widget/picker.dart'; import 'package:toolbox/view/widget/picker.dart';
import 'package:toolbox/view/widget/round_rect_card.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/model/app/path_with_prefix.dart';
import '../../../data/res/path.dart'; import '../../../data/res/path.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/fade_in.dart'; import '../../widget/fade_in.dart';
import 'sftp_mission.dart'; import 'sftp_mission.dart';
@@ -64,24 +64,32 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
leading: IconButton(
icon: const BackButtonIcon(),
onPressed: () {
if (_path != null) {
_path!.update('/');
}
context.pop();
},
),
title: Text(_s.download), title: Text(_s.download),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.downloading), icon: const Icon(Icons.downloading),
onPressed: () => onPressed: () => AppRoute(
AppRoute(const SftpMissionPage(), 'sftp downloading') const SftpMissionPage(),
.go(context), 'sftp downloading',
).go(context),
) )
], ],
), ),
body: FadeIn( body: FadeIn(
key: UniqueKey(), key: UniqueKey(),
child: _buildBody(), child: _wrapPopScope(),
),
bottomNavigationBar: SafeArea(
child: _buildPath(),
), ),
bottomNavigationBar: SafeArea(child: _buildPath()),
); );
} }
@@ -91,13 +99,53 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Divider(),
(_path?.path ?? _s.loadingFiles).omitStartStr(), (_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() { Widget _buildBody() {
if (_path == null) { if (_path == null) {
return const Center( return const Center(
@@ -106,22 +154,10 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
} }
final dir = Directory(_path!.path); final dir = Directory(_path!.path);
final files = dir.listSync(); final files = dir.listSync();
final canGoBack = _path!.canBack;
return ListView.builder( return ListView.builder(
itemCount: canGoBack ? files.length + 1 : files.length, itemCount: files.length,
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7), padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
itemBuilder: (context, index) { 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 file = files[index];
var fileName = file.path.split('/').last; var fileName = file.path.split('/').last;
var stat = file.statSync(); var stat = file.statSync();
@@ -139,9 +175,13 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
.substring(0, stat.modified.toString().length - 4), .substring(0, stat.modified.toString().length - 4),
style: grey, style: grey,
), ),
onLongPress: () {
if (!isDir) return;
_showDirActionDialog(file);
},
onTap: () async { onTap: () async {
if (!isDir) { if (!isDir) {
await showFileActionDialog(file); await _showFileActionDialog(file);
return; return;
} }
_path!.update(fileName); _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; final fileName = file.path.split('/').last;
if (widget.isPickFile) { if (widget.isPickFile) {
await showRoundDialog( await showRoundDialog(
@@ -189,13 +256,13 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
); );
return; return;
} }
final f = File(file.absolute.path);
final result = await AppRoute( final result = await AppRoute(
EditorPage( EditorPage(
path: file.absolute.path, path: file.absolute.path,
), ),
'sftp dled editor', 'sftp dled editor',
).go<String>(context); ).go<String>(context);
final f = File(file.absolute.path);
if (result != null) { if (result != null) {
f.writeAsString(result); f.writeAsString(result);
showSnackBar(context, Text(_s.saved)); showSnackBar(context, Text(_s.saved));
@@ -208,19 +275,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
title: Text(_s.rename), title: Text(_s.rename),
onTap: () { onTap: () {
context.pop(); context.pop();
showRoundDialog( _showRenameDialog(file);
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(() {});
},
),
);
}, },
), ),
ListTile( ListTile(
@@ -228,25 +283,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
title: Text(_s.delete), title: Text(_s.delete),
onTap: () { onTap: () {
context.pop(); context.pop();
showRoundDialog( _showDeleteDialog(file);
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),
),
],
);
}, },
), ),
ListTile( ListTile(
@@ -274,12 +311,9 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
if (spi == null) { if (spi == null) {
return; return;
} }
final remotePath = await AppRoute( final remotePath = await AppRoute.sftp(
SftpPage( spi: spi,
spi, isSelect: true,
selectPath: true,
),
'SFTP page (select)',
).go<String>(context); ).go<String>(context);
if (remotePath == null) { if (remotePath == null) {
return; 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:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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/navigator.dart';
import 'package:toolbox/core/extension/sftpfile.dart'; import 'package:toolbox/core/extension/sftpfile.dart';
import 'package:toolbox/data/res/misc.dart'; import 'package:toolbox/data/res/misc.dart';
@@ -16,7 +17,6 @@ import '../../../core/extension/stringx.dart';
import '../../../core/route.dart'; import '../../../core/route.dart';
import '../../../core/utils/misc.dart'; import '../../../core/utils/misc.dart';
import '../../../core/utils/ui.dart'; import '../../../core/utils/ui.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_private_info.dart'; import '../../../data/model/server/server_private_info.dart';
import '../../../data/model/sftp/absolute_path.dart'; import '../../../data/model/sftp/absolute_path.dart';
import '../../../data/model/sftp/browser_status.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/path.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../../locator.dart'; import '../../../locator.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/fade_in.dart'; import '../../widget/fade_in.dart';
import '../../widget/input_field.dart'; import '../../widget/input_field.dart';
import '../../widget/two_line_text.dart'; import '../../widget/two_line_text.dart';
@@ -36,11 +37,11 @@ class SftpPage extends StatefulWidget {
final String? initPath; final String? initPath;
final bool selectPath; final bool selectPath;
const SftpPage( const SftpPage({
this.spi, {
Key? key, Key? key,
required this.spi,
required this.selectPath,
this.initPath, this.initPath,
this.selectPath = false,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -49,15 +50,15 @@ class SftpPage extends StatefulWidget {
class _SftpPageState extends State<SftpPage> { class _SftpPageState extends State<SftpPage> {
final SftpBrowserStatus _status = SftpBrowserStatus(); final SftpBrowserStatus _status = SftpBrowserStatus();
final ScrollController _scrollController = ScrollController();
final _sftp = locator<SftpProvider>(); final _sftp = locator<SftpProvider>();
late S _s; late S _s;
ServerState? _state;
SSHClient? _client; SSHClient? _client;
final _logger = Logger('SFTP');
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -69,13 +70,21 @@ class _SftpPageState extends State<SftpPage> {
super.initState(); super.initState();
final serverProvider = locator<ServerProvider>(); final serverProvider = locator<ServerProvider>();
_client = serverProvider.servers[widget.spi.id]?.client; _client = serverProvider.servers[widget.spi.id]?.client;
_state = serverProvider.servers[widget.spi.id]?.state;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
leading: IconButton(
icon: const BackButtonIcon(),
onPressed: () {
if (_status.path != null) {
_status.path!.update('/');
}
context.pop();
},
),
centerTitle: true, centerTitle: true,
title: TwoLineText(up: 'SFTP', down: widget.spi.name), title: TwoLineText(up: 'SFTP', down: widget.spi.name),
actions: [ actions: [
@@ -88,11 +97,24 @@ class _SftpPageState extends State<SftpPage> {
), ),
], ],
), ),
body: _buildFileView(), body: _buildBody(),
bottomNavigationBar: _buildBottom(), 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() { Widget _buildBottom() {
final children = widget.selectPath final children = widget.selectPath
? [ ? [
@@ -224,6 +246,8 @@ class _SftpPageState extends State<SftpPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Input( Input(
autoFocus: true,
icon: Icons.abc,
label: _s.path, label: _s.path,
onSubmitted: (value) => context.pop(value), onSubmitted: (value) => context.pop(value),
), ),
@@ -249,10 +273,6 @@ class _SftpPageState extends State<SftpPage> {
} }
Widget _buildFileView() { Widget _buildFileView() {
if (_client == null || _state != ServerState.connected) {
return centerLoading;
}
if (_status.isBusy) { if (_status.isBusy) {
return centerLoading; return centerLoading;
} }
@@ -264,12 +284,17 @@ class _SftpPageState extends State<SftpPage> {
return centerLoading; return centerLoading;
} }
if (_status.files!.isEmpty) {
return const Center(
child: Text('~'),
);
}
return RefreshIndicator( return RefreshIndicator(
child: FadeIn( child: FadeIn(
key: Key(widget.spi.name + _status.path!.path), key: Key(widget.spi.name + _status.path!.path),
child: ListView.builder( child: ListView.builder(
itemCount: _status.files!.length, itemCount: _status.files!.length,
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
itemBuilder: (_, index) => _buildItem(_status.files![index]), itemBuilder: (_, index) => _buildItem(_status.files![index]),
), ),
@@ -367,7 +392,7 @@ class _SftpPageState extends State<SftpPage> {
SftpReqType.download, SftpReqType.download,
); );
_sftp.add(req, completer: completer); _sftp.add(req, completer: completer);
showRoundDialog(context: context, child: centerSizedLoading); showLoadingDialog(context);
await completer.future; await completer.future;
context.pop(); context.pop();
@@ -430,11 +455,7 @@ class _SftpPageState extends State<SftpPage> {
TextButton( TextButton(
onPressed: () async { onPressed: () async {
context.pop(); context.pop();
showRoundDialog( showLoadingDialog(context);
context: context,
child: centerSizedLoading,
barrierDismiss: false,
);
final remotePath = _getRemotePath(file); final remotePath = _getRemotePath(file);
try { try {
if (file.attr.isDirectory) { if (file.attr.isDirectory) {
@@ -460,10 +481,7 @@ class _SftpPageState extends State<SftpPage> {
} }
_listDir(); _listDir();
}, },
child: Text( child: Text(_s.delete, style: textRed),
_s.delete,
style: const TextStyle(color: Colors.red),
),
), ),
], ],
); );
@@ -476,6 +494,8 @@ class _SftpPageState extends State<SftpPage> {
context: context, context: context,
title: Text(_s.createFolder), title: Text(_s.createFolder),
child: Input( child: Input(
autoFocus: true,
icon: Icons.folder,
controller: textController, controller: textController,
label: _s.name, label: _s.name,
), ),
@@ -504,10 +524,7 @@ class _SftpPageState extends State<SftpPage> {
context.pop(); context.pop();
_listDir(); _listDir();
}, },
child: Text( child: Text(_s.ok, style: textRed),
_s.ok,
style: const TextStyle(color: Colors.red),
),
), ),
], ],
); );
@@ -520,6 +537,8 @@ class _SftpPageState extends State<SftpPage> {
context: context, context: context,
title: Text(_s.createFile), title: Text(_s.createFile),
child: Input( child: Input(
autoFocus: true,
icon: Icons.insert_drive_file,
controller: textController, controller: textController,
label: _s.name, label: _s.name,
), ),
@@ -550,10 +569,7 @@ class _SftpPageState extends State<SftpPage> {
context.pop(); context.pop();
_listDir(); _listDir();
}, },
child: Text( child: Text(_s.ok, style: textRed),
_s.ok,
style: const TextStyle(color: Colors.red),
),
), ),
], ],
); );
@@ -566,6 +582,8 @@ class _SftpPageState extends State<SftpPage> {
context: context, context: context,
title: Text(_s.rename), title: Text(_s.rename),
child: Input( child: Input(
autoFocus: true,
icon: Icons.abc,
controller: textController, controller: textController,
label: _s.name, label: _s.name,
), ),
@@ -591,10 +609,7 @@ class _SftpPageState extends State<SftpPage> {
context.pop(); context.pop();
_listDir(); _listDir();
}, },
child: Text( child: Text(_s.rename, style: textRed),
_s.rename,
style: const TextStyle(color: Colors.red),
),
), ),
], ],
); );
@@ -619,29 +634,47 @@ class _SftpPageState extends State<SftpPage> {
_status.client = sftpc; _status.client = sftpc;
} }
try { try {
final fs = final listPath = path ?? _status.path?.path ?? '/';
await _status.client!.listdir(path ?? _status.path?.path ?? '/'); final fs = await _status.client!.listdir(listPath);
fs.sort((a, b) => a.filename.compareTo(b.filename)); 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) { if (mounted) {
setState(() { setState(() {
_status.files = fs; _status.files = fs;
_status.isBusy = false; _status.isBusy = false;
}); });
} }
} catch (e) { } catch (e, trace) {
await showRoundDialog( _logger.warning('list dir failed', e, trace);
context: context,
title: Text(_s.error),
child: Text(e.toString()),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(_s.ok),
)
],
);
await _backward(); 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/model/sftp/req.dart';
import '../../../data/provider/sftp.dart'; import '../../../data/provider/sftp.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../widget/custom_appbar.dart';
import '../../widget/round_rect_card.dart'; import '../../widget/round_rect_card.dart';
class SftpMissionPage extends StatefulWidget { class SftpMissionPage extends StatefulWidget {
@@ -34,11 +35,8 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomAppBar(
title: Text( title: Text(_s.mission, style: textSize18),
_s.mission,
style: textSize18,
),
), ),
body: _buildBody(), body: _buildBody(),
); );
@@ -48,7 +46,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
return Consumer<SftpProvider>(builder: (__, pro, _) { return Consumer<SftpProvider>(builder: (__, pro, _) {
if (pro.status.isEmpty) { if (pro.status.isEmpty) {
return Center( return Center(
child: Text(_s.sftpNoDownloadTask), child: Text(_s.noTask),
); );
} }
return ListView.builder( 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 bool suggestiion;
final String? errorText; final String? errorText;
final Widget? prefix; final Widget? prefix;
final bool autoFocus;
const Input({ const Input({
super.key, super.key,
@@ -36,6 +37,7 @@ class Input extends StatelessWidget {
this.suggestiion = false, this.suggestiion = false,
this.errorText, this.errorText,
this.prefix, this.prefix,
this.autoFocus = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -49,6 +51,7 @@ class Input extends StatelessWidget {
onChanged: onChanged, onChanged: onChanged,
keyboardType: type, keyboardType: type,
focusNode: node, focusNode: node,
autofocus: autoFocus,
autocorrect: autoCorrect, autocorrect: autoCorrect,
enableSuggestions: suggestiion, enableSuggestions: suggestiion,
decoration: InputDecoration( decoration: InputDecoration(
@@ -57,7 +60,7 @@ class Input extends StatelessWidget {
icon: icon != null ? Icon(icon) : null, icon: icon != null ? Icon(icon) : null,
border: InputBorder.none, border: InputBorder.none,
errorText: errorText, errorText: errorText,
prefix: prefix), prefix: prefix,),
controller: controller, controller: controller,
obscureText: obscureText, obscureText: obscureText,
), ),

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