Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3f2b211a9 | ||
|
|
6a2191ff92 | ||
|
|
8111a83703 | ||
|
|
e643378249 | ||
|
|
d663106f9f | ||
|
|
d5f8cf6cf0 | ||
|
|
a0287a9f36 | ||
|
|
9c8ed3dfa8 | ||
|
|
f02cca1981 | ||
|
|
46cc363413 | ||
|
|
a59286473f | ||
|
|
f88f5c3bda | ||
|
|
b5d8b8771e | ||
|
|
35e9ecedd0 | ||
|
|
b5c705a1fe | ||
|
|
fe51669369 | ||
|
|
46cffb836c | ||
|
|
b78949cf0c | ||
|
|
1be87d0ec0 | ||
|
|
329922a836 | ||
|
|
c62c8e2c43 | ||
|
|
cfca40b7be | ||
|
|
8057c24947 | ||
|
|
1af7271a06 | ||
|
|
7ce03c18b2 | ||
|
|
ab8fdf3106 | ||
|
|
1d1b186d1e | ||
|
|
fb1f868c42 | ||
|
|
e08fa188ec | ||
|
|
e30bf47f0d | ||
|
|
253ab40e5c | ||
|
|
58a08757f5 | ||
|
|
9ca096094f | ||
|
|
4788f1dddc | ||
|
|
cf1c9643b9 | ||
|
|
c512a6a274 | ||
|
|
58fbd62779 | ||
|
|
173b7f6362 | ||
|
|
9fb738eda1 | ||
|
|
d35d106ad4 | ||
|
|
159942de95 | ||
|
|
693eef8f7e | ||
|
|
2887d23381 | ||
|
|
096d41088f | ||
|
|
bd84eeca0b | ||
|
|
b804f43d5a | ||
|
|
36b24bedb4 | ||
|
|
c1b3ff7bfd | ||
|
|
20c859b0a1 | ||
|
|
c4925ee2c7 | ||
|
|
d37a1fbea7 | ||
|
|
2142ae3e1c | ||
|
|
e686df45c9 | ||
|
|
ed9ed905ed | ||
|
|
98e77b9d0f | ||
|
|
879a347f23 | ||
|
|
cab58c30a7 | ||
|
|
75b9a3eeb0 | ||
|
|
00bf34965a | ||
|
|
81ab841fa5 | ||
|
|
df313adf39 | ||
|
|
c991c20cc1 | ||
|
|
0e54be8f66 | ||
|
|
140a3de5ed | ||
|
|
ae97012456 | ||
|
|
20d81e4353 | ||
|
|
8abdcf15d4 | ||
|
|
7f35ddfe30 | ||
|
|
7431de094f | ||
|
|
4b7397de46 | ||
|
|
a716254557 | ||
|
|
c406d92b82 | ||
|
|
432e3b1824 | ||
|
|
73611dacf1 | ||
|
|
8f4f141a64 | ||
|
|
51af3c63f1 | ||
|
|
b3c35b385b | ||
|
|
5b2ed02428 | ||
|
|
3405172d76 | ||
|
|
d88a078cd6 | ||
|
|
ee3e30d9b5 | ||
|
|
0250589be2 | ||
|
|
a66204f672 | ||
|
|
91967e6ce3 | ||
|
|
60507ea4bc | ||
|
|
486b920d6b |
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"dart.flutterSdkPath": ".fvm",
|
|
||||||
"files.watcherExclude": {
|
|
||||||
"**/.fvm": true
|
|
||||||
},
|
|
||||||
"git.ignoredRepositories": [
|
|
||||||
".fvm"
|
|
||||||
],
|
|
||||||
"search.exclude": {
|
|
||||||
"**/.fvm": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
README.md
@@ -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.
|
||||||
|
|||||||
34
README_zh.md
@@ -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`
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ android {
|
|||||||
applicationIdSuffix '.debug'
|
applicationIdSuffix '.debug'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
namespace 'tech.lolli.toolbox'
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
imgs/detail.jpg
|
Before Width: | Height: | Size: 297 KiB |
BIN
imgs/detail.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
imgs/editor.jpg
|
Before Width: | Height: | Size: 596 KiB |
BIN
imgs/editor.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
imgs/server.jpeg
|
Before Width: | Height: | Size: 273 KiB |
BIN
imgs/server.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
imgs/sftp.jpeg
|
Before Width: | Height: | Size: 323 KiB |
BIN
imgs/sftp.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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)";
|
||||||
|
|||||||
49
lib/app.dart
@@ -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),
|
||||||
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
5
lib/core/extension/media_queryx.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
extension MideaQueryX on MediaQueryData {
|
||||||
|
bool get useDoubleColumn => size.width > 639;
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
5
lib/data/model/app/github_id.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
typedef GhId = String;
|
||||||
|
|
||||||
|
extension GhIdX on GhId {
|
||||||
|
String get url => 'https://github.com/$this ';
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(':');
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 && "
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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": "如果终端有选中字符,则复制选中字符至剪切板,否则粘贴剪切板内容至终端。",
|
||||||
|
|||||||
@@ -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": "如果終端有選中字符,則復製選中字符至剪切板,否則粘貼剪切板內容至終端。",
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
156
lib/main.dart
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: () {
|
||||||
|
|||||||
@@ -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('-----')) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
73
lib/view/page/setting/srv_detail_seq.dart
Normal 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),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
lib/view/page/setting/srv_seq.dart
Normal 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),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
22
lib/view/widget/custom_appbar.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||