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'**
|
||||
String get addPrivateKey;
|
||||
|
||||
/// No description provided for @addSystemPrivateKeyTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Currently don\'t have any private key, do you add the one that comes with the system (~/.ssh/id_rsa)?'**
|
||||
String get addSystemPrivateKeyTip;
|
||||
|
||||
/// No description provided for @added2List.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -164,6 +170,12 @@ abstract class S {
|
||||
/// **'Auto'**
|
||||
String get auto;
|
||||
|
||||
/// No description provided for @autoCheckUpdate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto check update'**
|
||||
String get autoCheckUpdate;
|
||||
|
||||
/// No description provided for @autoUpdateHomeWidget.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -254,6 +266,12 @@ abstract class S {
|
||||
/// **'Connection'**
|
||||
String get conn;
|
||||
|
||||
/// No description provided for @connected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connected'**
|
||||
String get connected;
|
||||
|
||||
/// No description provided for @containerName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -548,6 +566,12 @@ abstract class S {
|
||||
/// **'Getting token...'**
|
||||
String get gettingToken;
|
||||
|
||||
/// No description provided for @goBackQ.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Go back?'**
|
||||
String get goBackQ;
|
||||
|
||||
/// No description provided for @goto.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -752,6 +776,18 @@ abstract class S {
|
||||
/// **'Mission'**
|
||||
String get mission;
|
||||
|
||||
/// No description provided for @moveOutServerFuncBtns.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Server function button location'**
|
||||
String get moveOutServerFuncBtns;
|
||||
|
||||
/// No description provided for @moveOutServerFuncBtnsHelp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.'**
|
||||
String get moveOutServerFuncBtnsHelp;
|
||||
|
||||
/// No description provided for @ms.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -824,6 +860,12 @@ abstract class S {
|
||||
/// **'No server available.'**
|
||||
String get noServerAvailable;
|
||||
|
||||
/// No description provided for @noTask.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No task'**
|
||||
String get noTask;
|
||||
|
||||
/// No description provided for @noUpdateAvailable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -932,11 +974,11 @@ abstract class S {
|
||||
/// **'Preview'**
|
||||
String get preview;
|
||||
|
||||
/// No description provided for @primaryColor.
|
||||
/// No description provided for @primaryColorSeed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Primary color'**
|
||||
String get primaryColor;
|
||||
/// **'Primary color seed'**
|
||||
String get primaryColorSeed;
|
||||
|
||||
/// No description provided for @privateKey.
|
||||
///
|
||||
@@ -1046,6 +1088,18 @@ abstract class S {
|
||||
/// **'Server'**
|
||||
String get server;
|
||||
|
||||
/// No description provided for @serverDetailOrder.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Detail page widget order'**
|
||||
String get serverDetailOrder;
|
||||
|
||||
/// No description provided for @serverOrder.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Server order'**
|
||||
String get serverOrder;
|
||||
|
||||
/// No description provided for @serverTabConnecting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1094,12 +1148,6 @@ abstract class S {
|
||||
/// **'Preparing to connect...'**
|
||||
String get sftpDlPrepare;
|
||||
|
||||
/// No description provided for @sftpNoDownloadTask.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No download task.'**
|
||||
String get sftpNoDownloadTask;
|
||||
|
||||
/// No description provided for @sftpSSHConnected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1148,6 +1196,12 @@ abstract class S {
|
||||
/// **'Start'**
|
||||
String get start;
|
||||
|
||||
/// No description provided for @stats.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stats'**
|
||||
String get stats;
|
||||
|
||||
/// No description provided for @stop.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1178,6 +1232,12 @@ abstract class S {
|
||||
/// **'Are you sure to use no password?'**
|
||||
String get sureNoPwd;
|
||||
|
||||
/// No description provided for @sureStop.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sure to stop [{item}] ?'**
|
||||
String sureStop(Object item);
|
||||
|
||||
/// No description provided for @sureToDeleteServer.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1319,7 +1379,7 @@ abstract class S {
|
||||
/// No description provided for @versionUnknownUpdate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Current: v1.0.{build}'**
|
||||
/// **'Current: v1.0.{build}, click to check updates'**
|
||||
String versionUnknownUpdate(Object build);
|
||||
|
||||
/// No description provided for @versionUpdated.
|
||||
|
||||
@@ -19,6 +19,9 @@ class SDe extends S {
|
||||
@override
|
||||
String get addPrivateKey => 'Private key hinzufügen';
|
||||
|
||||
@override
|
||||
String get addSystemPrivateKeyTip => 'Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?';
|
||||
|
||||
@override
|
||||
String get added2List => 'Zur Aufgabenliste hinzugefügt';
|
||||
|
||||
@@ -37,6 +40,9 @@ class SDe extends S {
|
||||
@override
|
||||
String get auto => 'System folgen';
|
||||
|
||||
@override
|
||||
String get autoCheckUpdate => 'Aktualisierung automatisch prüfen';
|
||||
|
||||
@override
|
||||
String get autoUpdateHomeWidget => 'Home-Widget automatisch aktualisieren';
|
||||
|
||||
@@ -82,6 +88,9 @@ class SDe extends S {
|
||||
@override
|
||||
String get conn => 'Verbindung';
|
||||
|
||||
@override
|
||||
String get connected => 'in Verbindung gebracht';
|
||||
|
||||
@override
|
||||
String get containerName => 'Container Name';
|
||||
|
||||
@@ -245,6 +254,9 @@ class SDe extends S {
|
||||
@override
|
||||
String get gettingToken => 'Getting token...';
|
||||
|
||||
@override
|
||||
String get goBackQ => 'Zurückkommen?';
|
||||
|
||||
@override
|
||||
String get goto => 'Pfad öffnen';
|
||||
|
||||
@@ -353,6 +365,12 @@ class SDe extends S {
|
||||
@override
|
||||
String get mission => 'Mission';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtns => 'Position der Server-Funktionsschaltfläche';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtnsHelp => 'Ein: kann unter jeder Karte auf der Registerkarte \"Server\" angezeigt werden. Aus: kann oben auf der Seite \"Serverdetails\" angezeigt werden.';
|
||||
|
||||
@override
|
||||
String get ms => 'ms';
|
||||
|
||||
@@ -389,6 +407,9 @@ class SDe extends S {
|
||||
@override
|
||||
String get noServerAvailable => 'Kein Server verfügbar.';
|
||||
|
||||
@override
|
||||
String get noTask => 'Nicht fragen';
|
||||
|
||||
@override
|
||||
String get noUpdateAvailable => 'Kein Update verfügbar';
|
||||
|
||||
@@ -444,7 +465,7 @@ class SDe extends S {
|
||||
String get preview => 'Vorschau';
|
||||
|
||||
@override
|
||||
String get primaryColor => 'Farbschema';
|
||||
String get primaryColorSeed => 'Farbschema';
|
||||
|
||||
@override
|
||||
String get privateKey => 'Private Key';
|
||||
@@ -504,6 +525,12 @@ class SDe extends S {
|
||||
@override
|
||||
String get server => 'Server';
|
||||
|
||||
@override
|
||||
String get serverDetailOrder => 'Reihenfolge der Widgets auf der Detailseite';
|
||||
|
||||
@override
|
||||
String get serverOrder => 'Server-Bestellung';
|
||||
|
||||
@override
|
||||
String get serverTabConnecting => 'Verbinden...';
|
||||
|
||||
@@ -528,9 +555,6 @@ class SDe extends S {
|
||||
@override
|
||||
String get sftpDlPrepare => 'Verbindung vorbereiten...';
|
||||
|
||||
@override
|
||||
String get sftpNoDownloadTask => 'Keine aktiven Downloads.';
|
||||
|
||||
@override
|
||||
String get sftpSSHConnected => 'SFTP Verbunden';
|
||||
|
||||
@@ -559,6 +583,9 @@ class SDe extends S {
|
||||
@override
|
||||
String get start => 'Start';
|
||||
|
||||
@override
|
||||
String get stats => 'Statistik';
|
||||
|
||||
@override
|
||||
String get stop => 'Stop';
|
||||
|
||||
@@ -576,6 +603,11 @@ class SDe extends S {
|
||||
@override
|
||||
String get sureNoPwd => 'Bist du sicher, dass du kein Passwort verwenden willst?';
|
||||
|
||||
@override
|
||||
String sureStop(Object item) {
|
||||
return 'Sind Sie sicher, dass Sie [$item] stoppen möchten?';
|
||||
}
|
||||
|
||||
@override
|
||||
String sureToDeleteServer(Object server) {
|
||||
return 'Bist du sicher, dass du [$server] löschen willst?';
|
||||
@@ -655,7 +687,7 @@ class SDe extends S {
|
||||
|
||||
@override
|
||||
String versionUnknownUpdate(Object build) {
|
||||
return 'Aktuell: v1.0.$build';
|
||||
return 'Aktuell: v1.0.$build. Klicken Sie hier, um nach Updates zu suchen';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -19,6 +19,9 @@ class SEn extends S {
|
||||
@override
|
||||
String get addPrivateKey => 'Add private key';
|
||||
|
||||
@override
|
||||
String get addSystemPrivateKeyTip => 'Currently don\'t have any private key, do you add the one that comes with the system (~/.ssh/id_rsa)?';
|
||||
|
||||
@override
|
||||
String get added2List => 'Added to task list';
|
||||
|
||||
@@ -37,6 +40,9 @@ class SEn extends S {
|
||||
@override
|
||||
String get auto => 'Auto';
|
||||
|
||||
@override
|
||||
String get autoCheckUpdate => 'Auto check update';
|
||||
|
||||
@override
|
||||
String get autoUpdateHomeWidget => 'Auto update home widget';
|
||||
|
||||
@@ -82,6 +88,9 @@ class SEn extends S {
|
||||
@override
|
||||
String get conn => 'Connection';
|
||||
|
||||
@override
|
||||
String get connected => 'Connected';
|
||||
|
||||
@override
|
||||
String get containerName => 'Container name';
|
||||
|
||||
@@ -245,6 +254,9 @@ class SEn extends S {
|
||||
@override
|
||||
String get gettingToken => 'Getting token...';
|
||||
|
||||
@override
|
||||
String get goBackQ => 'Go back?';
|
||||
|
||||
@override
|
||||
String get goto => 'Go to';
|
||||
|
||||
@@ -353,6 +365,12 @@ class SEn extends S {
|
||||
@override
|
||||
String get mission => 'Mission';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtns => 'Server function button location';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtnsHelp => 'On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.';
|
||||
|
||||
@override
|
||||
String get ms => 'ms';
|
||||
|
||||
@@ -389,6 +407,9 @@ class SEn extends S {
|
||||
@override
|
||||
String get noServerAvailable => 'No server available.';
|
||||
|
||||
@override
|
||||
String get noTask => 'No task';
|
||||
|
||||
@override
|
||||
String get noUpdateAvailable => 'No update available';
|
||||
|
||||
@@ -444,7 +465,7 @@ class SEn extends S {
|
||||
String get preview => 'Preview';
|
||||
|
||||
@override
|
||||
String get primaryColor => 'Primary color';
|
||||
String get primaryColorSeed => 'Primary color seed';
|
||||
|
||||
@override
|
||||
String get privateKey => 'Private Key';
|
||||
@@ -504,6 +525,12 @@ class SEn extends S {
|
||||
@override
|
||||
String get server => 'Server';
|
||||
|
||||
@override
|
||||
String get serverDetailOrder => 'Detail page widget order';
|
||||
|
||||
@override
|
||||
String get serverOrder => 'Server order';
|
||||
|
||||
@override
|
||||
String get serverTabConnecting => 'Connecting...';
|
||||
|
||||
@@ -528,9 +555,6 @@ class SEn extends S {
|
||||
@override
|
||||
String get sftpDlPrepare => 'Preparing to connect...';
|
||||
|
||||
@override
|
||||
String get sftpNoDownloadTask => 'No download task.';
|
||||
|
||||
@override
|
||||
String get sftpSSHConnected => 'SFTP Connected';
|
||||
|
||||
@@ -559,6 +583,9 @@ class SEn extends S {
|
||||
@override
|
||||
String get start => 'Start';
|
||||
|
||||
@override
|
||||
String get stats => 'Stats';
|
||||
|
||||
@override
|
||||
String get stop => 'Stop';
|
||||
|
||||
@@ -576,6 +603,11 @@ class SEn extends S {
|
||||
@override
|
||||
String get sureNoPwd => 'Are you sure to use no password?';
|
||||
|
||||
@override
|
||||
String sureStop(Object item) {
|
||||
return 'Sure to stop [$item] ?';
|
||||
}
|
||||
|
||||
@override
|
||||
String sureToDeleteServer(Object server) {
|
||||
return 'Are you sure to delete server [$server]?';
|
||||
@@ -655,7 +687,7 @@ class SEn extends S {
|
||||
|
||||
@override
|
||||
String versionUnknownUpdate(Object build) {
|
||||
return 'Current: v1.0.$build';
|
||||
return 'Current: v1.0.$build, click to check updates';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -19,6 +19,9 @@ class SId extends S {
|
||||
@override
|
||||
String get addPrivateKey => 'Tambahkan kunci pribadi';
|
||||
|
||||
@override
|
||||
String get addSystemPrivateKeyTip => 'Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?';
|
||||
|
||||
@override
|
||||
String get added2List => 'Ditambahkan ke Daftar Tugas';
|
||||
|
||||
@@ -37,6 +40,9 @@ class SId extends S {
|
||||
@override
|
||||
String get auto => 'Auto';
|
||||
|
||||
@override
|
||||
String get autoCheckUpdate => 'Periksa pembaruan otomatis';
|
||||
|
||||
@override
|
||||
String get autoUpdateHomeWidget => 'Widget Rumah Pembaruan Otomatis';
|
||||
|
||||
@@ -82,6 +88,9 @@ class SId extends S {
|
||||
@override
|
||||
String get conn => 'Koneksi';
|
||||
|
||||
@override
|
||||
String get connected => 'Terhubung';
|
||||
|
||||
@override
|
||||
String get containerName => 'Nama kontainer';
|
||||
|
||||
@@ -245,6 +254,9 @@ class SId extends S {
|
||||
@override
|
||||
String get gettingToken => 'Mendapatkan token ...';
|
||||
|
||||
@override
|
||||
String get goBackQ => 'Datang kembali?';
|
||||
|
||||
@override
|
||||
String get goto => 'Pergi ke';
|
||||
|
||||
@@ -353,6 +365,12 @@ class SId extends S {
|
||||
@override
|
||||
String get mission => 'Misi';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtns => 'Lokasi tombol fungsi server';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtnsHelp => 'Aktif: dapat ditampilkan di bawah setiap kartu pada halaman Tab Server. Nonaktif: dapat ditampilkan di bagian atas halaman Rincian Server.';
|
||||
|
||||
@override
|
||||
String get ms => 'MS';
|
||||
|
||||
@@ -389,6 +407,9 @@ class SId extends S {
|
||||
@override
|
||||
String get noServerAvailable => 'Tidak ada server yang tersedia.';
|
||||
|
||||
@override
|
||||
String get noTask => 'Tidak bertanya';
|
||||
|
||||
@override
|
||||
String get noUpdateAvailable => 'Tidak ada pembaruan yang tersedia';
|
||||
|
||||
@@ -444,7 +465,7 @@ class SId extends S {
|
||||
String get preview => 'Pratinjau';
|
||||
|
||||
@override
|
||||
String get primaryColor => 'Warna utama';
|
||||
String get primaryColorSeed => 'Warna utama';
|
||||
|
||||
@override
|
||||
String get privateKey => 'Kunci Pribadi';
|
||||
@@ -504,6 +525,12 @@ class SId extends S {
|
||||
@override
|
||||
String get server => 'Server';
|
||||
|
||||
@override
|
||||
String get serverDetailOrder => 'Detail pesanan widget halaman';
|
||||
|
||||
@override
|
||||
String get serverOrder => 'Pesanan server';
|
||||
|
||||
@override
|
||||
String get serverTabConnecting => 'Menghubungkan ...';
|
||||
|
||||
@@ -528,9 +555,6 @@ class SId extends S {
|
||||
@override
|
||||
String get sftpDlPrepare => 'Bersiap untuk terhubung ...';
|
||||
|
||||
@override
|
||||
String get sftpNoDownloadTask => 'Tidak ada tugas unduhan.';
|
||||
|
||||
@override
|
||||
String get sftpSSHConnected => 'Sftp terhubung';
|
||||
|
||||
@@ -559,6 +583,9 @@ class SId extends S {
|
||||
@override
|
||||
String get start => 'Awal';
|
||||
|
||||
@override
|
||||
String get stats => 'Statistik';
|
||||
|
||||
@override
|
||||
String get stop => 'Berhenti';
|
||||
|
||||
@@ -576,6 +603,11 @@ class SId extends S {
|
||||
@override
|
||||
String get sureNoPwd => 'Apakah Anda pasti tidak menggunakan kata sandi?';
|
||||
|
||||
@override
|
||||
String sureStop(Object item) {
|
||||
return 'Anda yakin ingin menghentikan [$item]?';
|
||||
}
|
||||
|
||||
@override
|
||||
String sureToDeleteServer(Object server) {
|
||||
return 'Apakah Anda pasti akan menghapus server [$server]?';
|
||||
@@ -655,7 +687,7 @@ class SId extends S {
|
||||
|
||||
@override
|
||||
String versionUnknownUpdate(Object build) {
|
||||
return 'Saat ini: v1.0.$build';
|
||||
return 'Saat ini: v1.0.$build. Klik untuk memeriksa pembaruan.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -19,6 +19,9 @@ class SZh extends S {
|
||||
@override
|
||||
String get addPrivateKey => '添加一个私钥';
|
||||
|
||||
@override
|
||||
String get addSystemPrivateKeyTip => '当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?';
|
||||
|
||||
@override
|
||||
String get added2List => '已添加至任务列表';
|
||||
|
||||
@@ -37,6 +40,9 @@ class SZh extends S {
|
||||
@override
|
||||
String get auto => '自动';
|
||||
|
||||
@override
|
||||
String get autoCheckUpdate => '自动检查更新';
|
||||
|
||||
@override
|
||||
String get autoUpdateHomeWidget => '自动更新桌面小部件';
|
||||
|
||||
@@ -82,6 +88,9 @@ class SZh extends S {
|
||||
@override
|
||||
String get conn => '连接';
|
||||
|
||||
@override
|
||||
String get connected => '已连接';
|
||||
|
||||
@override
|
||||
String get containerName => '容器名';
|
||||
|
||||
@@ -245,6 +254,9 @@ class SZh extends S {
|
||||
@override
|
||||
String get gettingToken => '正在获取Token...';
|
||||
|
||||
@override
|
||||
String get goBackQ => '返回?';
|
||||
|
||||
@override
|
||||
String get goto => '前往';
|
||||
|
||||
@@ -353,6 +365,12 @@ class SZh extends S {
|
||||
@override
|
||||
String get mission => '任务';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtns => '服务器功能按钮位置';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtnsHelp => '开启:可以在服务器 Tab 页的每个卡片下方显示。关闭:在服务器详情页顶部显示。';
|
||||
|
||||
@override
|
||||
String get ms => '毫秒';
|
||||
|
||||
@@ -389,6 +407,9 @@ class SZh extends S {
|
||||
@override
|
||||
String get noServerAvailable => '没有可用的服务器。';
|
||||
|
||||
@override
|
||||
String get noTask => '没有任务';
|
||||
|
||||
@override
|
||||
String get noUpdateAvailable => '没有可用更新';
|
||||
|
||||
@@ -444,7 +465,7 @@ class SZh extends S {
|
||||
String get preview => '预览';
|
||||
|
||||
@override
|
||||
String get primaryColor => '主题色';
|
||||
String get primaryColorSeed => '主题色种子';
|
||||
|
||||
@override
|
||||
String get privateKey => '私钥';
|
||||
@@ -504,6 +525,12 @@ class SZh extends S {
|
||||
@override
|
||||
String get server => '服务器';
|
||||
|
||||
@override
|
||||
String get serverDetailOrder => '详情页部件顺序';
|
||||
|
||||
@override
|
||||
String get serverOrder => '服务器顺序';
|
||||
|
||||
@override
|
||||
String get serverTabConnecting => '连接中...';
|
||||
|
||||
@@ -529,10 +556,7 @@ class SZh extends S {
|
||||
String get sftpDlPrepare => '准备连接至服务器...';
|
||||
|
||||
@override
|
||||
String get sftpNoDownloadTask => '没有下载任务';
|
||||
|
||||
@override
|
||||
String get sftpSSHConnected => 'SFTP 已连接,即将开始下载...';
|
||||
String get sftpSSHConnected => 'SFTP 已连接...';
|
||||
|
||||
@override
|
||||
String get showDistLogo => '显示发行版 Logo';
|
||||
@@ -559,6 +583,9 @@ class SZh extends S {
|
||||
@override
|
||||
String get start => '开始';
|
||||
|
||||
@override
|
||||
String get stats => '统计';
|
||||
|
||||
@override
|
||||
String get stop => '停止';
|
||||
|
||||
@@ -576,6 +603,11 @@ class SZh extends S {
|
||||
@override
|
||||
String get sureNoPwd => '确认使用无密码?';
|
||||
|
||||
@override
|
||||
String sureStop(Object item) {
|
||||
return '确定要停止 [$item] 吗?';
|
||||
}
|
||||
|
||||
@override
|
||||
String sureToDeleteServer(Object server) {
|
||||
return '你确定要删除服务器 [$server] 吗?';
|
||||
@@ -655,7 +687,7 @@ class SZh extends S {
|
||||
|
||||
@override
|
||||
String versionUnknownUpdate(Object build) {
|
||||
return '当前:v1.0.$build';
|
||||
return '当前:v1.0.$build,点击检查更新';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -701,6 +733,9 @@ class SZhTw extends SZh {
|
||||
@override
|
||||
String get addPrivateKey => '新增一個私鑰';
|
||||
|
||||
@override
|
||||
String get addSystemPrivateKeyTip => '當前沒有任何私鑰,是否添加系統自帶的(~/.ssh/id_rsa)?';
|
||||
|
||||
@override
|
||||
String get added2List => '已添加至任務列表';
|
||||
|
||||
@@ -719,6 +754,9 @@ class SZhTw extends SZh {
|
||||
@override
|
||||
String get auto => '自動';
|
||||
|
||||
@override
|
||||
String get autoCheckUpdate => '自動檢查更新';
|
||||
|
||||
@override
|
||||
String get autoUpdateHomeWidget => '自動更新桌面小部件';
|
||||
|
||||
@@ -764,6 +802,9 @@ class SZhTw extends SZh {
|
||||
@override
|
||||
String get conn => '連接';
|
||||
|
||||
@override
|
||||
String get connected => '已連接';
|
||||
|
||||
@override
|
||||
String get containerName => '容器名稱';
|
||||
|
||||
@@ -927,6 +968,9 @@ class SZhTw extends SZh {
|
||||
@override
|
||||
String get gettingToken => '正在獲取Token...';
|
||||
|
||||
@override
|
||||
String get goBackQ => '返回?';
|
||||
|
||||
@override
|
||||
String get goto => '前往';
|
||||
|
||||
@@ -1035,6 +1079,12 @@ class SZhTw extends SZh {
|
||||
@override
|
||||
String get mission => '任務';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtns => '服務器功能按鈕位置';
|
||||
|
||||
@override
|
||||
String get moveOutServerFuncBtnsHelp => '開啟:可以在服務器 Tab 頁的每個卡片下方顯示。關閉:在服務器詳情頁頂部顯示。';
|
||||
|
||||
@override
|
||||
String get ms => '毫秒';
|
||||
|
||||
@@ -1071,6 +1121,9 @@ class SZhTw extends SZh {
|
||||
@override
|
||||
String get noServerAvailable => '沒有可用的服務器。';
|
||||
|
||||
@override
|
||||
String get noTask => '沒有任務';
|
||||
|
||||
@override
|
||||
String get noUpdateAvailable => '沒有可用更新';
|
||||
|
||||
@@ -1126,7 +1179,7 @@ class SZhTw extends SZh {
|
||||
String get preview => '預覽';
|
||||
|
||||
@override
|
||||
String get primaryColor => '主要色調';
|
||||
String get primaryColorSeed => '主要色調種子';
|
||||
|
||||
@override
|
||||
String get privateKey => '私鑰';
|
||||
@@ -1186,6 +1239,12 @@ class SZhTw extends SZh {
|
||||
@override
|
||||
String get server => '服務器';
|
||||
|
||||
@override
|
||||
String get serverDetailOrder => '詳情頁部件順序';
|
||||
|
||||
@override
|
||||
String get serverOrder => '服務器順序';
|
||||
|
||||
@override
|
||||
String get serverTabConnecting => '連接中...';
|
||||
|
||||
@@ -1211,10 +1270,7 @@ class SZhTw extends SZh {
|
||||
String get sftpDlPrepare => '準備連接至服務器...';
|
||||
|
||||
@override
|
||||
String get sftpNoDownloadTask => '沒有下載任務';
|
||||
|
||||
@override
|
||||
String get sftpSSHConnected => 'SFTP 已連接,即將開始下載...';
|
||||
String get sftpSSHConnected => 'SFTP 已連接...';
|
||||
|
||||
@override
|
||||
String get showDistLogo => '顯示發行版 Logo';
|
||||
@@ -1241,6 +1297,9 @@ class SZhTw extends SZh {
|
||||
@override
|
||||
String get start => '開始';
|
||||
|
||||
@override
|
||||
String get stats => '統計';
|
||||
|
||||
@override
|
||||
String get stop => '停止';
|
||||
|
||||
@@ -1258,6 +1317,11 @@ class SZhTw extends SZh {
|
||||
@override
|
||||
String get sureNoPwd => '確認使用無密碼?';
|
||||
|
||||
@override
|
||||
String sureStop(Object item) {
|
||||
return '確定要停止 [$item] 嗎?';
|
||||
}
|
||||
|
||||
@override
|
||||
String sureToDeleteServer(Object server) {
|
||||
return '你確定要刪除服務器 [$server] 嗎?';
|
||||
@@ -1337,7 +1401,7 @@ class SZhTw extends SZh {
|
||||
|
||||
@override
|
||||
String versionUnknownUpdate(Object build) {
|
||||
return '當前:v1.0.$build';
|
||||
return '當前:v1.0.$build,點擊檢查更新';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
39
.github/workflows/analysis.yml
vendored
Normal file
@@ -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
|
||||
/release
|
||||
test.dart
|
||||
.fvm
|
||||
|
||||
# Keep generated l10n files
|
||||
/.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).
|
||||
|
||||
|
||||
## 📱 ScreenShots
|
||||
## 🏙️ ScreenShots
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<img width="200px" src="imgs/server.jpeg">
|
||||
<img width="200px" src="imgs/server.png">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/detail.jpg">
|
||||
<img width="200px" src="imgs/detail.png">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/ssh.jpg">
|
||||
<img width="200px" src="imgs/sftp.png">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/editor.jpg">
|
||||
<img width="200px" src="imgs/editor.png">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -67,7 +67,7 @@ If ServerBox app has any bug, please open an [issue](https://github.com/lollipop
|
||||
<img width="200px" src="imgs/ping.png">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/sftp.jpeg">
|
||||
<img width="200px" src="imgs/ssh.jpg">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/docker.jpeg">
|
||||
@@ -83,12 +83,12 @@ If ServerBox app has any bug, please open an [issue](https://github.com/lollipop
|
||||
Status|Platform
|
||||
--- | ---
|
||||
Full Support| Android / iOS / macOS
|
||||
Support, but not tested| Windows / Linux
|
||||
Not tested| Windows / Linux
|
||||
|
||||
|
||||
## 🧱 Contribution
|
||||
**Any positive contribution is welcome**.
|
||||
10 iOS app redemption codes will be given away for the first time you participate in the contribution. This is the only thing I can do to thank you. :)
|
||||
10 iOS app redemption codes will be given away for the first time you participate in the contribution. :)
|
||||
### l10n guide
|
||||
1. Fork this repo and clone forked repo to your local machine.
|
||||
2. Create `arb` file in `lib/l10n/` directory
|
||||
@@ -100,5 +100,9 @@ Support, but not tested| Windows / Linux
|
||||
|
||||
|
||||
## 📝 License
|
||||
1. You can package it for personal use, but you can't distribute it. (For example: You can teach others how to package it to avoid spending money to buy App, but you can't directly distribute the App you packaged.)
|
||||
2. Except for the above, apply the `GPLv3` license.
|
||||
- You can package it for personal use, but you can't distribute it.
|
||||
- For example: You can teach others how to package it to avoid spending money to buy App, but you can't directly distribute the App you packaged.
|
||||
- Why do I have to do this?
|
||||
- Security: If anyone inject malicious code into the source code and distribute it, it will cause a lot of trouble.
|
||||
- Income: Apple developer account = $99 per year. As a freshly graduated independent developer, I need income.
|
||||
- Except for the above, apply the `GPLv3` license.
|
||||
|
||||
34
README_zh.md
@@ -27,7 +27,7 @@
|
||||
## 🔖 特点
|
||||
- [x] 功能
|
||||
- [x] `SSH` 终端, `SFTP`
|
||||
- [x] `Docker & 包` 管理器
|
||||
- [x] `Docker & 包 & 进程` 管理器
|
||||
- [x] 状态图表
|
||||
- [x] 代码编辑器
|
||||
- [x] `Ping` 和 更多
|
||||
@@ -44,20 +44,20 @@
|
||||
如果 ServerBox app 有任何 bug,请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
|
||||
|
||||
|
||||
## 📱 截屏
|
||||
## 🏙️ 截屏
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<img width="200px" src="imgs/server.jpeg">
|
||||
<img width="200px" src="imgs/server.png">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/detail.jpg">
|
||||
<img width="200px" src="imgs/detail.png">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/ssh.jpg">
|
||||
<img width="200px" src="imgs/sftp.png">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/editor.jpg">
|
||||
<img width="200px" src="imgs/editor.png">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -67,7 +67,7 @@
|
||||
<img width="200px" src="imgs/ping.png">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/sftp.jpeg">
|
||||
<img width="200px" src="imgs/ssh.jpg">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="imgs/docker.jpeg">
|
||||
@@ -83,11 +83,11 @@
|
||||
状态|平台
|
||||
--- | ---
|
||||
完整支持 | Android / iOS / macOS
|
||||
可能支持,未测试 | Windows / Linux
|
||||
未测试 | Windows / Linux
|
||||
|
||||
## 🧱 贡献
|
||||
**任何正面的贡献都欢迎**.
|
||||
第一次参与贡献,会赠送 10 份 iOS App 兑换码。这是我唯一能送的。你可以同来送给其他人。:)
|
||||
**任何正面的贡献都欢迎**。
|
||||
第一次参与贡献,会赠送 10 份 iOS App 兑换码。如果没有 iOS 设备,你可以用来送给其他人。:)
|
||||
|
||||
### l10n
|
||||
1. Fork 本项目,并 Clone 你 Fork 的项目至你的电脑
|
||||
@@ -95,10 +95,14 @@
|
||||
- 文件名应该类似 `intl_XX.arb`, `XX` 是语言标识码。 例如 `intl_en.arb` 是给英语的, `intl_zh.arb` 是给中文的
|
||||
3. 向 `.arb` 本地化文件添加内容。 你可以查看 `intl_en.arb` 和 `intl_zh.arb` 的内容,并理解其含义,来创建新的本地化文件
|
||||
4. 运行 `flutter gen-l10n` 来生成所需文件
|
||||
5. Commit 变更到你的 Fork 的 Repo
|
||||
6. 在我的项目中发起 Pull Request.
|
||||
5. Commit 变更到你 Fork 的 Repo
|
||||
6. 在我的项目中发起 Pull Request
|
||||
|
||||
|
||||
## 📝 License
|
||||
1. 允许打包自用,但不允许分发(举例:你可以教别人如何打包,避免花钱购买App,但不能与他人分享你打包的App)
|
||||
2. 除去上诉情形:遵循 `GPLv3`
|
||||
## 📝 开源协议
|
||||
- 允许打包自用,但不允许分发
|
||||
- 举例:你可以教别人如何打包,避免花钱购买App,但不能与他人分享你打包的App)
|
||||
- 之所以这样做:
|
||||
1. 安全性:可能会有有心之人植入后门并分发
|
||||
2. 回血:苹果开发者 **99刀/年**,并且作为刚毕业的独立开发者,我需要收入
|
||||
- 除去上述情形:遵循 `GPLv3`
|
||||
|
||||
@@ -79,6 +79,7 @@ android {
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
}
|
||||
namespace 'tech.lolli.toolbox'
|
||||
}
|
||||
|
||||
flutter {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="tech.lolli.toolbox">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
@@ -56,5 +55,7 @@
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/home_widget" />
|
||||
</receiver>
|
||||
|
||||
<service android:name=".KeepAliveService"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.content.Intent
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@@ -11,11 +12,18 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
MethodChannel(binaryMessenger, "tech.lolli.toolbox/app_retain").apply {
|
||||
setMethodCallHandler { method, result ->
|
||||
if (method.method == "sendToBackground") {
|
||||
moveTaskToBack(true)
|
||||
result.success(null)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
when (method.method) {
|
||||
"sendToBackground" -> {
|
||||
moveTaskToBack(true)
|
||||
result.success(null)
|
||||
}
|
||||
"startService" -> {
|
||||
val intent = Intent(this@MainActivity, KeepAliveService::class.java)
|
||||
startService(intent)
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
android:textColor="@color/widgetText"
|
||||
android:textSize="23sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
tools:text="Server Name" />
|
||||
|
||||
<RelativeLayout
|
||||
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
|
||||
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
|
||||
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
|
||||
|
||||
@@ -470,7 +470,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 406;
|
||||
CURRENT_PROJECT_VERSION = 491;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -478,7 +478,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.406;
|
||||
MARKETING_VERSION = 1.0.491;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -602,7 +602,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 406;
|
||||
CURRENT_PROJECT_VERSION = 491;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -610,7 +610,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.406;
|
||||
MARKETING_VERSION = 1.0.491;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -628,7 +628,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 406;
|
||||
CURRENT_PROJECT_VERSION = 491;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -636,7 +636,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.406;
|
||||
MARKETING_VERSION = 1.0.491;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -657,7 +657,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 406;
|
||||
CURRENT_PROJECT_VERSION = 491;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -670,7 +670,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.406;
|
||||
MARKETING_VERSION = 1.0.491;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
@@ -696,7 +696,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 406;
|
||||
CURRENT_PROJECT_VERSION = 491;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -709,7 +709,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.406;
|
||||
MARKETING_VERSION = 1.0.491;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -732,7 +732,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 406;
|
||||
CURRENT_PROJECT_VERSION = 491;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -745,7 +745,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.406;
|
||||
MARKETING_VERSION = 1.0.491;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
49
lib/app.dart
@@ -28,8 +28,7 @@ class MyApp extends StatelessWidget {
|
||||
// Issue #57
|
||||
// if not [ok] -> [AMOLED] mode, use [ThemeMode.dark]
|
||||
final themeMode = isAMOLED ? ThemeMode.values[tMode] : ThemeMode.dark;
|
||||
final localeStr = _setting.locale.fetch();
|
||||
final locale = localeStr?.toLocale;
|
||||
final locale = _setting.locale.fetch()?.toLocale;
|
||||
final darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
@@ -47,39 +46,25 @@ class MyApp extends StatelessWidget {
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: primaryColor,
|
||||
),
|
||||
darkTheme: isAMOLED
|
||||
? darkTheme
|
||||
: darkTheme.copyWith(
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
dialogBackgroundColor: Colors.black,
|
||||
drawerTheme: const DrawerThemeData(
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
dialogTheme: const DialogTheme(
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
listTileTheme: const ListTileThemeData(
|
||||
tileColor: Colors.black12,
|
||||
),
|
||||
cardTheme: const CardTheme(
|
||||
color: Colors.black12,
|
||||
),
|
||||
navigationBarTheme: const NavigationBarThemeData(
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
popupMenuTheme: const PopupMenuThemeData(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
darkTheme: isAMOLED ? darkTheme : _getAmoledTheme(darkTheme),
|
||||
home: fullScreen ? const FullScreenPage() : const HomePage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ThemeData _getAmoledTheme(ThemeData darkTheme) => darkTheme.copyWith(
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
dialogBackgroundColor: Colors.black,
|
||||
drawerTheme: const DrawerThemeData(backgroundColor: Colors.black),
|
||||
appBarTheme: const AppBarTheme(backgroundColor: Colors.black),
|
||||
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
|
||||
bottomSheetTheme:
|
||||
const BottomSheetThemeData(backgroundColor: Colors.black),
|
||||
listTileTheme: const ListTileThemeData(tileColor: Colors.black12),
|
||||
cardTheme: const CardTheme(color: Colors.black12),
|
||||
navigationBarTheme:
|
||||
const NavigationBarThemeData(backgroundColor: Colors.black),
|
||||
popupMenuTheme: const PopupMenuThemeData(color: Colors.black),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,13 @@ const _interactiveStates = <MaterialState>{
|
||||
};
|
||||
|
||||
extension ColorX on Color {
|
||||
String get toHex {
|
||||
final redStr = red.toRadixString(16).padLeft(2, '0');
|
||||
final greenStr = green.toRadixString(16).padLeft(2, '0');
|
||||
final blueStr = blue.toRadixString(16).padLeft(2, '0');
|
||||
return '#$redStr$greenStr$blueStr';
|
||||
}
|
||||
|
||||
bool get isBrightColor {
|
||||
return getBrightnessFromColor == Brightness.light;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
extension StringX on String {
|
||||
/// Format: `#8b2252` or `8b2252`
|
||||
Color? get hexToColor {
|
||||
final hexCode = replaceAll('#', '');
|
||||
final val = int.tryParse('FF$hexCode', radix: 16);
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
return Color(val);
|
||||
}
|
||||
|
||||
int get i => int.parse(this);
|
||||
|
||||
Uri get uri {
|
||||
|
||||
@@ -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_gen/gen_l10n/l10n.dart';
|
||||
import 'package:toolbox/core/analysis.dart';
|
||||
import 'package:toolbox/data/model/server/private_key_info.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/backup.dart';
|
||||
import 'package:toolbox/view/page/docker.dart';
|
||||
import 'package:toolbox/view/page/home.dart';
|
||||
import 'package:toolbox/view/page/ping.dart';
|
||||
import 'package:toolbox/view/page/private_key/edit.dart';
|
||||
import 'package:toolbox/view/page/private_key/list.dart';
|
||||
import 'package:toolbox/view/page/server/detail.dart';
|
||||
import 'package:toolbox/view/page/ssh_term.dart';
|
||||
import 'package:toolbox/view/page/setting/virt_key.dart';
|
||||
import 'package:toolbox/view/page/storage/local.dart';
|
||||
|
||||
import '../data/model/server/snippet.dart';
|
||||
import '../view/page/convert.dart';
|
||||
import '../view/page/debug.dart';
|
||||
import '../view/page/editor.dart';
|
||||
import '../view/page/full_screen.dart';
|
||||
import '../view/page/pkg.dart';
|
||||
import '../view/page/process.dart';
|
||||
import '../view/page/server/edit.dart';
|
||||
import '../view/page/server/tab.dart';
|
||||
import '../view/page/setting/entry.dart';
|
||||
import '../view/page/setting/srv_detail_seq.dart';
|
||||
import '../view/page/setting/srv_seq.dart';
|
||||
import '../view/page/snippet/edit.dart';
|
||||
import '../view/page/snippet/list.dart';
|
||||
import '../view/page/storage/sftp.dart';
|
||||
import '../view/page/storage/sftp_mission.dart';
|
||||
import 'utils/ui.dart';
|
||||
|
||||
class AppRoute {
|
||||
final Widget page;
|
||||
@@ -14,4 +47,148 @@ class AppRoute {
|
||||
MaterialPageRoute(builder: (context) => page),
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> checkClientAndGo<T>({
|
||||
required BuildContext context,
|
||||
required S s,
|
||||
required String id,
|
||||
}) {
|
||||
final server = locator<ServerProvider>().servers[id];
|
||||
if (server == null || server.client == null) {
|
||||
showSnackBar(context, Text(s.waitConnection));
|
||||
return Future.value(null);
|
||||
}
|
||||
return go(context);
|
||||
}
|
||||
|
||||
static AppRoute serverDetail({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(ServerDetailPage(key: key, spi: spi), 'server_detail');
|
||||
}
|
||||
|
||||
static AppRoute serverTab({Key? key}) {
|
||||
return AppRoute(ServerPage(key: key), 'server_tab');
|
||||
}
|
||||
|
||||
static AppRoute serverEdit({Key? key, ServerPrivateInfo? spi}) {
|
||||
return AppRoute(
|
||||
ServerEditPage(spi: spi),
|
||||
'server_${spi == null ? 'add' : 'edit'}',
|
||||
);
|
||||
}
|
||||
|
||||
static AppRoute keyEdit({Key? key, PrivateKeyInfo? pki}) {
|
||||
return AppRoute(
|
||||
PrivateKeyEditPage(pki: pki),
|
||||
'key_${pki == null ? 'add' : 'edit'}',
|
||||
);
|
||||
}
|
||||
|
||||
static AppRoute keyList({Key? key}) {
|
||||
return AppRoute(PrivateKeysListPage(key: key), 'key_detail');
|
||||
}
|
||||
|
||||
static AppRoute snippetEdit({Key? key, Snippet? snippet}) {
|
||||
return AppRoute(
|
||||
SnippetEditPage(snippet: snippet),
|
||||
'snippet_${snippet == null ? 'add' : 'edit'}',
|
||||
);
|
||||
}
|
||||
|
||||
static AppRoute snippetList({Key? key}) {
|
||||
return AppRoute(SnippetListPage(key: key), 'snippet_detail');
|
||||
}
|
||||
|
||||
static AppRoute ssh({
|
||||
Key? key,
|
||||
required ServerPrivateInfo spi,
|
||||
String? initCmd,
|
||||
}) {
|
||||
return AppRoute(
|
||||
SSHPage(
|
||||
key: key,
|
||||
spi: spi,
|
||||
initCmd: initCmd,
|
||||
),
|
||||
'ssh_term',
|
||||
);
|
||||
}
|
||||
|
||||
static AppRoute sshVirtKeySetting({Key? key}) {
|
||||
return AppRoute(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
|
||||
}
|
||||
|
||||
static AppRoute localStorage({Key? key}) {
|
||||
return AppRoute(LocalStoragePage(key: key), 'local_storage');
|
||||
}
|
||||
|
||||
static AppRoute sftpMission({Key? key}) {
|
||||
return AppRoute(SftpMissionPage(key: key), 'sftp_mission');
|
||||
}
|
||||
|
||||
static AppRoute sftp(
|
||||
{Key? key,
|
||||
required ServerPrivateInfo spi,
|
||||
String? initPath,
|
||||
bool isSelect = false}) {
|
||||
return AppRoute(
|
||||
SftpPage(
|
||||
key: key,
|
||||
spi: spi,
|
||||
initPath: initPath,
|
||||
selectPath: isSelect,
|
||||
),
|
||||
'sftp');
|
||||
}
|
||||
|
||||
static AppRoute backup({Key? key}) {
|
||||
return AppRoute(BackupPage(key: key), 'backup');
|
||||
}
|
||||
|
||||
static AppRoute convert({Key? key}) {
|
||||
return AppRoute(ConvertPage(key: key), 'convert');
|
||||
}
|
||||
|
||||
static AppRoute debug({Key? key}) {
|
||||
return AppRoute(DebugPage(key: key), 'debug');
|
||||
}
|
||||
|
||||
static AppRoute docker({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(DockerManagePage(key: key, spi: spi), 'docker');
|
||||
}
|
||||
|
||||
static AppRoute editor({Key? key, required String path}) {
|
||||
return AppRoute(EditorPage(key: key, path: path), 'editor');
|
||||
}
|
||||
|
||||
static AppRoute fullscreen({Key? key}) {
|
||||
return AppRoute(FullScreenPage(key: key), 'fullscreen');
|
||||
}
|
||||
|
||||
static AppRoute home({Key? key}) {
|
||||
return AppRoute(HomePage(key: key), 'home');
|
||||
}
|
||||
|
||||
static AppRoute ping({Key? key}) {
|
||||
return AppRoute(PingPage(key: key), 'ping');
|
||||
}
|
||||
|
||||
static AppRoute pkg({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(PkgPage(key: key, spi: spi), 'pkg');
|
||||
}
|
||||
|
||||
static AppRoute process({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(ProcessPage(key: key, spi: spi), 'process');
|
||||
}
|
||||
|
||||
static AppRoute setting({Key? key}) {
|
||||
return AppRoute(SettingPage(key: key), 'setting');
|
||||
}
|
||||
|
||||
static AppRoute serverOrder({Key? key}) {
|
||||
return AppRoute(ServerOrderPage(key: key), 'server_order');
|
||||
}
|
||||
|
||||
static AppRoute serverDetailOrder({Key? key}) {
|
||||
return AppRoute(ServerDetailOrderPage(key: key), 'server_detail_order');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:r_upgrade/r_upgrade.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart' hide pathJoin;
|
||||
import 'package:toolbox/data/model/app/update.dart';
|
||||
import 'package:toolbox/data/res/path.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
|
||||
import '../data/provider/app.dart';
|
||||
import '../data/res/build_data.dart';
|
||||
@@ -29,7 +31,7 @@ Future<bool> isFileAvailable(String url) async {
|
||||
}
|
||||
|
||||
Future<void> doUpdate(BuildContext context, {bool force = false}) async {
|
||||
_rmDownloadApks();
|
||||
await _rmDownloadApks();
|
||||
|
||||
final update = await locator<AppService>().getUpdate();
|
||||
|
||||
@@ -69,7 +71,7 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
|
||||
child: Text(s.updateTipTooLow(newest)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => _doUpdate(url, context, s),
|
||||
onPressed: () => _doUpdate(update, context, s),
|
||||
child: Text(s.ok),
|
||||
)
|
||||
],
|
||||
@@ -81,17 +83,58 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
|
||||
context,
|
||||
'${s.updateTip(newest)} \n${update.changelog.current}',
|
||||
s.update,
|
||||
() => _doUpdate(url, context, s),
|
||||
() => _doUpdate(update, context, s),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _doUpdate(String url, BuildContext context, S s) async {
|
||||
Future<void> _doUpdate(AppUpdate update, BuildContext context, S s) async {
|
||||
if (isAndroid) {
|
||||
await RUpgrade.upgrade(
|
||||
final url = update.url.current;
|
||||
if (url == null) return;
|
||||
final fileName = url.split('/').last;
|
||||
final id = await RUpgrade.upgrade(
|
||||
url,
|
||||
fileName: url.split('/').last,
|
||||
isAutoRequestInstall: true,
|
||||
fileName: fileName,
|
||||
isAutoRequestInstall: false,
|
||||
);
|
||||
RUpgrade.stream.listen((event) async {
|
||||
if (event.status?.value == 3) {
|
||||
if (id == null) {
|
||||
showSnackBar(context, const Text('install id is null'));
|
||||
return;
|
||||
}
|
||||
final sha256 = () {
|
||||
try {
|
||||
return fileName.split('.').first;
|
||||
} catch (e) {
|
||||
_logger.warning('sha256 parse failed: $e');
|
||||
return null;
|
||||
}
|
||||
}();
|
||||
final dlPath = pathJoin(await _dlDir, fileName);
|
||||
final computed = await getFileSha256(dlPath);
|
||||
if (computed != sha256) {
|
||||
_logger.info('Mismatch sha256: $computed, $sha256');
|
||||
final resume = await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(s.attention),
|
||||
child: const Text('sha256 is null'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(false),
|
||||
child: Text(s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.pop(true),
|
||||
child: Text(s.ok, style: textRed),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!resume) return;
|
||||
}
|
||||
RUpgrade.install(id);
|
||||
}
|
||||
});
|
||||
} else if (isIOS) {
|
||||
await RUpgrade.upgradeFromAppStore('1586449703');
|
||||
} else {
|
||||
@@ -111,8 +154,10 @@ Future<void> _doUpdate(String url, BuildContext context, S s) async {
|
||||
// rmdir Download
|
||||
Future<void> _rmDownloadApks() async {
|
||||
if (!isAndroid) return;
|
||||
final dlDir = Directory(pathJoin((await docDir).path, 'Download'));
|
||||
final dlDir = Directory(await _dlDir);
|
||||
if (await dlDir.exists()) {
|
||||
await dlDir.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> get _dlDir async => pathJoin((await docDir).path, 'Download');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@@ -74,6 +74,16 @@ String getTime(int? unixMill) {
|
||||
.replaceFirst('.000', '');
|
||||
}
|
||||
|
||||
/// Join two path with `/`
|
||||
String pathJoin(String path1, String path2) {
|
||||
return path1 + (path1.endsWith('/') ? '' : '/') + path2;
|
||||
}
|
||||
|
||||
Future<String?> getFileSha256(String path) async {
|
||||
final file = File(path);
|
||||
if (!(await file.exists())) {
|
||||
return null;
|
||||
}
|
||||
final digest = await sha256.bind(file.openRead()).first;
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ enum PlatformType {
|
||||
macos,
|
||||
windows,
|
||||
web,
|
||||
unknown,
|
||||
fuchsia,
|
||||
unknown;
|
||||
}
|
||||
|
||||
final _p = () {
|
||||
@@ -31,10 +32,21 @@ final _p = () {
|
||||
if (Platform.isWindows) {
|
||||
return PlatformType.windows;
|
||||
}
|
||||
if (Platform.isFuchsia) {
|
||||
return PlatformType.fuchsia;
|
||||
}
|
||||
return PlatformType.unknown;
|
||||
}();
|
||||
|
||||
final _pathSep = () {
|
||||
if (Platform.isWindows) {
|
||||
return '\\';
|
||||
}
|
||||
return '/';
|
||||
}();
|
||||
|
||||
PlatformType get platform => _p;
|
||||
String get pathSeparator => _pathSep;
|
||||
|
||||
bool get isAndroid => _p == PlatformType.android;
|
||||
bool get isIOS => _p == PlatformType.ios;
|
||||
@@ -47,3 +59,23 @@ bool get isDesktop =>
|
||||
_p == PlatformType.linux ||
|
||||
_p == PlatformType.macos ||
|
||||
_p == PlatformType.windows;
|
||||
|
||||
/// Available only on desktop,
|
||||
/// return null on mobile
|
||||
String? getHomeDir() {
|
||||
final envVars = Platform.environment;
|
||||
if (isMacOS || isLinux) {
|
||||
return envVars['HOME'];
|
||||
} else if (isWindows) {
|
||||
return envVars['UserProfile'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Join two paths with platform specific separator
|
||||
String pathJoin(String path1, String path2) {
|
||||
if (isWindows) {
|
||||
return path1 + (path1.endsWith('\\') ? '' : '\\') + path2;
|
||||
}
|
||||
return path1 + (path1.endsWith('/') ? '' : '/') + path2;
|
||||
}
|
||||
|
||||
@@ -32,14 +32,14 @@ enum GenSSHClientStatus {
|
||||
}
|
||||
|
||||
String getPrivateKey(String id) {
|
||||
final key = locator<PrivateKeyStore>().get(id);
|
||||
if (key == null) {
|
||||
final pki = locator<PrivateKeyStore>().get(id);
|
||||
if (pki == null) {
|
||||
throw SSHErr(
|
||||
type: SSHErrType.noPrivateKey,
|
||||
message: 'key [$id] not found',
|
||||
);
|
||||
}
|
||||
return key.privateKey;
|
||||
return pki.key;
|
||||
}
|
||||
|
||||
Future<SSHClient> genClient(
|
||||
@@ -56,11 +56,12 @@ Future<SSHClient> genClient(
|
||||
timeout: const Duration(seconds: 5),
|
||||
);
|
||||
} catch (e) {
|
||||
if (spi.alterUrl == null) rethrow;
|
||||
try {
|
||||
spi.fromStringUrl();
|
||||
final ipPort = spi.fromStringUrl();
|
||||
socket = await SSHSocket.connect(
|
||||
spi.ip,
|
||||
spi.port,
|
||||
ipPort.ip,
|
||||
ipPort.port,
|
||||
timeout: const Duration(seconds: 5),
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../data/model/server/snippet.dart';
|
||||
import '../../data/provider/snippet.dart';
|
||||
import '../../data/res/ui.dart';
|
||||
import '../../locator.dart';
|
||||
import '../../view/page/snippet/edit.dart';
|
||||
import '../../view/widget/picker.dart';
|
||||
@@ -69,6 +70,14 @@ Future<T?> showRoundDialog<T>({
|
||||
);
|
||||
}
|
||||
|
||||
void showLoadingDialog(BuildContext context, {bool barrierDismiss = false}) {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: centerSizedLoading,
|
||||
barrierDismiss: barrierDismiss,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSwitch(
|
||||
BuildContext context,
|
||||
StoreProperty<bool> prop, {
|
||||
|
||||
@@ -38,6 +38,9 @@ enum DockerErrType {
|
||||
invalidVersion,
|
||||
cmdNoPrefix,
|
||||
segmentsNotMatch,
|
||||
parsePsItem,
|
||||
parseImages,
|
||||
parseStats,
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
enum ServerTabMenuType {
|
||||
terminal,
|
||||
sftp,
|
||||
snippet,
|
||||
pkg,
|
||||
docker,
|
||||
process,
|
||||
edit;
|
||||
pkg,
|
||||
snippet,
|
||||
;
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
@@ -19,31 +20,12 @@ enum ServerTabMenuType {
|
||||
return Icons.system_security_update;
|
||||
case ServerTabMenuType.docker:
|
||||
return Icons.view_agenda;
|
||||
case ServerTabMenuType.edit:
|
||||
return Icons.edit;
|
||||
case ServerTabMenuType.process:
|
||||
return Icons.list_alt_outlined;
|
||||
case ServerTabMenuType.terminal:
|
||||
return Icons.terminal;
|
||||
}
|
||||
}
|
||||
|
||||
String text(S s) {
|
||||
switch (this) {
|
||||
case ServerTabMenuType.sftp:
|
||||
return 'SFTP';
|
||||
case ServerTabMenuType.snippet:
|
||||
return s.snippet;
|
||||
case ServerTabMenuType.pkg:
|
||||
return s.pkg;
|
||||
case ServerTabMenuType.docker:
|
||||
return 'Docker';
|
||||
case ServerTabMenuType.edit:
|
||||
return s.edit;
|
||||
case ServerTabMenuType.process:
|
||||
return s.process;
|
||||
}
|
||||
}
|
||||
|
||||
PopupMenuItem<ServerTabMenuType> build(S s) => _build(this, icon, text(s));
|
||||
}
|
||||
|
||||
enum DockerMenuType {
|
||||
@@ -52,11 +34,12 @@ enum DockerMenuType {
|
||||
restart,
|
||||
rm,
|
||||
logs,
|
||||
terminal;
|
||||
terminal,
|
||||
stats;
|
||||
|
||||
static List<DockerMenuType> items(bool running) {
|
||||
if (running) {
|
||||
return [stop, restart, rm, logs, terminal];
|
||||
return [stop, restart, rm, logs, terminal, stats];
|
||||
} else {
|
||||
return [start, rm, logs];
|
||||
}
|
||||
@@ -76,6 +59,8 @@ enum DockerMenuType {
|
||||
return Icons.logo_dev;
|
||||
case DockerMenuType.terminal:
|
||||
return Icons.terminal;
|
||||
case DockerMenuType.stats:
|
||||
return Icons.bar_chart;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +78,8 @@ enum DockerMenuType {
|
||||
return s.log;
|
||||
case DockerMenuType.terminal:
|
||||
return s.terminal;
|
||||
case DockerMenuType.stats:
|
||||
return s.stats;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
import '../../../core/utils/platform.dart';
|
||||
|
||||
class PathWithPrefix {
|
||||
final String _prefixPath;
|
||||
|
||||
@@ -1,22 +1,54 @@
|
||||
import '../../res/server_cmd.dart';
|
||||
|
||||
class AppShellFunc {
|
||||
final String name;
|
||||
final String cmd;
|
||||
final String flag;
|
||||
const _cmdDivider = '\necho $seperator\n';
|
||||
|
||||
const AppShellFunc(this.name, this.cmd, this.flag);
|
||||
enum AppShellFuncType {
|
||||
status,
|
||||
docker;
|
||||
|
||||
String get flag {
|
||||
switch (this) {
|
||||
case AppShellFuncType.status:
|
||||
return 's';
|
||||
case AppShellFuncType.docker:
|
||||
return 'd';
|
||||
}
|
||||
}
|
||||
|
||||
String get exec => 'sh $shellPath -$flag';
|
||||
}
|
||||
|
||||
typedef AppShellFuncs = List<AppShellFunc>;
|
||||
String get name {
|
||||
switch (this) {
|
||||
case AppShellFuncType.status:
|
||||
return 'status';
|
||||
case AppShellFuncType.docker:
|
||||
// `dockeR` -> avoid conflict with `docker` command
|
||||
// 以防止循环递归
|
||||
return 'dockeR';
|
||||
}
|
||||
}
|
||||
|
||||
extension AppShellFuncsExt on AppShellFuncs {
|
||||
String get generate {
|
||||
String get cmd {
|
||||
switch (this) {
|
||||
case AppShellFuncType.status:
|
||||
return statusCmds.join(_cmdDivider);
|
||||
case AppShellFuncType.docker:
|
||||
return '''
|
||||
result=\$(docker version 2>&1)
|
||||
deniedStr="permission denied"
|
||||
containStr=\$(echo \$result | grep "\${deniedStr}")
|
||||
if [[ \$containStr != "" ]]; then
|
||||
${dockerCmds.join(_cmdDivider)}
|
||||
else
|
||||
${dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)}
|
||||
fi''';
|
||||
}
|
||||
}
|
||||
|
||||
static String get shellScript {
|
||||
final sb = StringBuffer();
|
||||
// Write each func
|
||||
for (final func in this) {
|
||||
for (final func in values) {
|
||||
sb.write('''
|
||||
${func.name}() {
|
||||
${func.cmd}
|
||||
@@ -27,7 +59,7 @@ ${func.cmd}
|
||||
|
||||
// Write switch case
|
||||
sb.write('case \$1 in\n');
|
||||
for (final func in this) {
|
||||
for (final func in values) {
|
||||
sb.write('''
|
||||
'-${func.flag}')
|
||||
${func.name}
|
||||
@@ -38,13 +70,36 @@ ${func.cmd}
|
||||
*)
|
||||
echo "Invalid argument \$1"
|
||||
;;
|
||||
esac
|
||||
''');
|
||||
esac''');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// enum AppShellFuncType {
|
||||
// status,
|
||||
// docker;
|
||||
// }
|
||||
extension EnumX on Enum {
|
||||
/// Find out the required segment from [segments]
|
||||
String find(List<String> segments) {
|
||||
return segments[index];
|
||||
}
|
||||
}
|
||||
|
||||
enum StatusCmdType {
|
||||
time,
|
||||
net,
|
||||
sys,
|
||||
cpu,
|
||||
uptime,
|
||||
conn,
|
||||
disk,
|
||||
mem,
|
||||
tempType,
|
||||
tempVal,
|
||||
host,
|
||||
sysRhel;
|
||||
}
|
||||
|
||||
enum DockerCmdType {
|
||||
version,
|
||||
ps,
|
||||
stats,
|
||||
images;
|
||||
}
|
||||
|
||||
@@ -47,14 +47,14 @@ class Cpus extends TimeSeq<OneTimeCpuStatus> {
|
||||
}
|
||||
|
||||
class OneTimeCpuStatus extends TimeSeqIface<OneTimeCpuStatus> {
|
||||
late String id;
|
||||
late int user;
|
||||
late int sys;
|
||||
late int nice;
|
||||
late int idle;
|
||||
late int iowait;
|
||||
late int irq;
|
||||
late int softirq;
|
||||
final String id;
|
||||
final int user;
|
||||
final int sys;
|
||||
final int nice;
|
||||
final int idle;
|
||||
final int iowait;
|
||||
final int irq;
|
||||
final int softirq;
|
||||
|
||||
OneTimeCpuStatus(
|
||||
this.id,
|
||||
@@ -80,7 +80,8 @@ List<OneTimeCpuStatus> parseCPU(String raw) {
|
||||
if (item == '') break;
|
||||
final id = item.split(' ').first;
|
||||
final matches = item.replaceFirst(id, '').trim().split(' ');
|
||||
cpus.add(OneTimeCpuStatus(
|
||||
cpus.add(
|
||||
OneTimeCpuStatus(
|
||||
id,
|
||||
int.parse(matches[0]),
|
||||
int.parse(matches[1]),
|
||||
@@ -88,7 +89,9 @@ List<OneTimeCpuStatus> parseCPU(String raw) {
|
||||
int.parse(matches[3]),
|
||||
int.parse(matches[4]),
|
||||
int.parse(matches[5]),
|
||||
int.parse(matches[6])));
|
||||
int.parse(matches[6]),
|
||||
),
|
||||
);
|
||||
}
|
||||
return cpus;
|
||||
}
|
||||
|
||||
@@ -36,14 +36,41 @@ List<Disk> parseDisk(String raw) {
|
||||
vals[0] = pathCache;
|
||||
pathCache = '';
|
||||
}
|
||||
list.add(Disk(
|
||||
path: vals[0],
|
||||
loc: vals[5],
|
||||
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
|
||||
used: vals[2],
|
||||
size: vals[1],
|
||||
avail: vals[3],
|
||||
));
|
||||
try {
|
||||
list.add(Disk(
|
||||
path: vals[0],
|
||||
loc: vals[5],
|
||||
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
|
||||
used: vals[2],
|
||||
size: vals[1],
|
||||
avail: vals[3],
|
||||
));
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Issue 88
|
||||
///
|
||||
/// Due to performance issues,
|
||||
/// if there is no `Disk.loc == '/' || Disk.loc == '/sysroot'`,
|
||||
/// return the first [Disk] of [disks].
|
||||
///
|
||||
/// If we find out the biggest [Disk] of [disks],
|
||||
/// the fps may lower than 60.
|
||||
Disk? findRootDisk(List<Disk> disks) {
|
||||
if (disks.isEmpty) return null;
|
||||
final roots = disks.where((element) => element.loc == '/');
|
||||
if (roots.isEmpty) {
|
||||
final sysRoots = disks.where((element) => element.loc == '/sysroot');
|
||||
if (sysRoots.isEmpty) {
|
||||
return disks.first;
|
||||
} else {
|
||||
return sysRoots.first;
|
||||
}
|
||||
} else {
|
||||
return roots.first;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'time_seq.dart';
|
||||
|
||||
class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
|
||||
String device;
|
||||
BigInt bytesIn;
|
||||
BigInt bytesOut;
|
||||
BigInt time;
|
||||
final String device;
|
||||
final BigInt bytesIn;
|
||||
final BigInt bytesOut;
|
||||
final int time;
|
||||
|
||||
NetSpeedPart(this.device, this.bytesIn, this.bytesOut, this.time);
|
||||
|
||||
@override
|
||||
@@ -18,7 +19,7 @@ class NetSpeed extends TimeSeq<NetSpeedPart> {
|
||||
|
||||
List<String> get devices => now.map((e) => e.device).toList();
|
||||
|
||||
BigInt get _timeDiff => now[0].time - pre[0].time;
|
||||
BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time);
|
||||
|
||||
double _speedIn(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;
|
||||
double _speedOut(int i) => (now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
|
||||
@@ -96,14 +97,12 @@ class NetSpeed extends TimeSeq<NetSpeedPart> {
|
||||
/// face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
|
||||
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
|
||||
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
|
||||
/// 1635752901
|
||||
List<NetSpeedPart> parseNetSpeed(String raw) {
|
||||
List<NetSpeedPart> parseNetSpeed(String raw, int time) {
|
||||
final split = raw.split('\n');
|
||||
if (split.length < 4) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final time = BigInt.parse(split[split.length - 1]);
|
||||
final results = <NetSpeedPart>[];
|
||||
for (final item in split.sublist(2, split.length - 1)) {
|
||||
final data = item.trim().split(':');
|
||||
|
||||
@@ -5,27 +5,23 @@ part 'private_key_info.g.dart';
|
||||
@HiveType(typeId: 1)
|
||||
class PrivateKeyInfo {
|
||||
@HiveField(0)
|
||||
late String id;
|
||||
final String id;
|
||||
@HiveField(1)
|
||||
late String privateKey;
|
||||
@HiveField(2)
|
||||
late String password;
|
||||
final String key;
|
||||
|
||||
PrivateKeyInfo({
|
||||
required this.id,
|
||||
required this.key,
|
||||
});
|
||||
|
||||
PrivateKeyInfo.fromJson(Map<String, dynamic> json)
|
||||
: id = json["id"].toString(),
|
||||
key = json["private_key"].toString();
|
||||
|
||||
PrivateKeyInfo(
|
||||
this.id,
|
||||
this.privateKey,
|
||||
this.password,
|
||||
);
|
||||
PrivateKeyInfo.fromJson(Map<String, dynamic> json) {
|
||||
id = json["id"].toString();
|
||||
privateKey = json["private_key"].toString();
|
||||
password = json["password"].toString();
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
final data = <String, String>{};
|
||||
data["id"] = id;
|
||||
data["private_key"] = privateKey;
|
||||
data["password"] = password;
|
||||
data["private_key"] = key;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,22 +17,19 @@ class PrivateKeyInfoAdapter extends TypeAdapter<PrivateKeyInfo> {
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return PrivateKeyInfo(
|
||||
fields[0] as String,
|
||||
fields[1] as String,
|
||||
fields[2] as String,
|
||||
id: fields[0] as String,
|
||||
key: fields[1] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, PrivateKeyInfo obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(2)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.privateKey)
|
||||
..writeByte(2)
|
||||
..write(obj.password);
|
||||
..write(obj.key);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -12,11 +12,22 @@ class Server {
|
||||
}
|
||||
|
||||
enum ServerState {
|
||||
failed,
|
||||
disconnected,
|
||||
connecting,
|
||||
connected,
|
||||
failed;
|
||||
|
||||
bool get shouldConnect =>
|
||||
this == ServerState.disconnected || this == ServerState.failed;
|
||||
/// Connected to server
|
||||
connected,
|
||||
|
||||
/// Status parsing
|
||||
loading,
|
||||
|
||||
/// Status parsing finished
|
||||
finished;
|
||||
|
||||
bool get shouldConnect => this < ServerState.connecting;
|
||||
|
||||
bool get canViewDetails => this == ServerState.finished;
|
||||
|
||||
operator <(ServerState other) => index < other.index;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class ServerPrivateInfo {
|
||||
alterUrl != old.alterUrl;
|
||||
}
|
||||
|
||||
void fromStringUrl() {
|
||||
_IpPort fromStringUrl() {
|
||||
if (alterUrl == null) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
|
||||
}
|
||||
@@ -76,20 +76,16 @@ class ServerPrivateInfo {
|
||||
if (splited.length != 2) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no @');
|
||||
}
|
||||
user = splited[0];
|
||||
final splited2 = splited[1].split(':');
|
||||
if (splited2.length != 2) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no :');
|
||||
}
|
||||
ip = splited2[0];
|
||||
port = int.tryParse(splited2[1]) ?? 22;
|
||||
final ip_ = splited2[0];
|
||||
final port_ = int.tryParse(splited2[1]) ?? 22;
|
||||
if (port <= 0 || port > 65535) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');
|
||||
}
|
||||
|
||||
// Do not update [id]
|
||||
// Because [id] is the identity which is used to find the [SSHClient]
|
||||
// id = '$user@$ip:$port';
|
||||
return _IpPort(ip_, port_);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -97,3 +93,10 @@ class ServerPrivateInfo {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
class _IpPort {
|
||||
final String ip;
|
||||
final int port;
|
||||
|
||||
_IpPort(this.ip, this.port);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import '../../res/server_cmd.dart';
|
||||
import '../app/shell_func.dart';
|
||||
import 'cpu.dart';
|
||||
import 'disk.dart';
|
||||
import 'memory.dart';
|
||||
@@ -13,50 +13,46 @@ class ServerStatusUpdateReq {
|
||||
const ServerStatusUpdateReq(this.ss, this.segments);
|
||||
}
|
||||
|
||||
extension _SegmentsExt on List<String> {
|
||||
String at(CmdType t) {
|
||||
final index = t.index;
|
||||
if (index >= length) return '';
|
||||
return this[index];
|
||||
}
|
||||
}
|
||||
|
||||
Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
|
||||
final net = parseNetSpeed(req.segments.at(CmdType.net));
|
||||
final segments = req.segments;
|
||||
|
||||
final time = int.parse(StatusCmdType.time.find(segments));
|
||||
|
||||
final net = parseNetSpeed(StatusCmdType.net.find(segments), time);
|
||||
req.ss.netSpeed.update(net);
|
||||
|
||||
final sys = _parseSysVer(
|
||||
req.segments.at(CmdType.sys),
|
||||
req.segments.at(CmdType.host),
|
||||
req.segments.at(CmdType.sysRhel),
|
||||
StatusCmdType.sys.find(segments),
|
||||
StatusCmdType.host.find(segments),
|
||||
StatusCmdType.sysRhel.find(segments),
|
||||
);
|
||||
if (sys != null) {
|
||||
req.ss.sysVer = sys;
|
||||
}
|
||||
|
||||
final cpus = parseCPU(req.segments.at(CmdType.cpu));
|
||||
final cpus = parseCPU(StatusCmdType.cpu.find(segments));
|
||||
req.ss.cpu.update(cpus);
|
||||
|
||||
req.ss.temps.parse(
|
||||
req.segments.at(CmdType.tempType),
|
||||
req.segments.at(CmdType.tempVal),
|
||||
StatusCmdType.tempType.find(segments),
|
||||
StatusCmdType.tempVal.find(segments),
|
||||
);
|
||||
|
||||
final tcp = parseConn(req.segments.at(CmdType.conn));
|
||||
final tcp = parseConn(StatusCmdType.conn.find(segments));
|
||||
if (tcp != null) {
|
||||
req.ss.tcp = tcp;
|
||||
}
|
||||
|
||||
req.ss.disk = parseDisk(req.segments.at(CmdType.disk));
|
||||
req.ss.disk = parseDisk(StatusCmdType.disk.find(segments));
|
||||
|
||||
req.ss.mem = parseMem(req.segments.at(CmdType.mem));
|
||||
req.ss.mem = parseMem(StatusCmdType.mem.find(segments));
|
||||
|
||||
final uptime = _parseUpTime(req.segments.at(CmdType.uptime));
|
||||
final uptime = _parseUpTime(StatusCmdType.uptime.find(segments));
|
||||
if (uptime != null) {
|
||||
req.ss.uptime = uptime;
|
||||
}
|
||||
|
||||
req.ss.swap = parseSwap(req.segments.at(CmdType.mem));
|
||||
req.ss.swap = parseSwap(StatusCmdType.mem.find(segments));
|
||||
return req.ss;
|
||||
}
|
||||
|
||||
@@ -74,13 +70,16 @@ String? _parseUpTime(String raw) {
|
||||
}
|
||||
|
||||
String? _parseSysVer(String raw, String hostname, String rawRhel) {
|
||||
if (!rawRhel.contains('No such file')) {
|
||||
return rawRhel;
|
||||
try {
|
||||
final s = raw.split('=');
|
||||
if (s.length == 2) {
|
||||
return s[1].replaceAll('"', '').replaceFirst('\n', '');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!rawRhel.contains('cat: /etc/redhat-release:')) {
|
||||
return rawRhel;
|
||||
}
|
||||
if (hostname.isNotEmpty) return hostname;
|
||||
}
|
||||
final s = raw.split('=');
|
||||
if (s.length == 2) {
|
||||
return s[1].replaceAll('"', '').replaceFirst('\n', '');
|
||||
}
|
||||
if (hostname.isNotEmpty) return hostname;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import '../../store/setting.dart';
|
||||
class TryLimiter {
|
||||
final Map<String, int> _triedTimes = {};
|
||||
|
||||
bool shouldTry(String id) {
|
||||
bool canTry(String id) {
|
||||
final maxCount = locator<SettingStore>().maxRetryCount.fetch()!;
|
||||
if (maxCount <= 0) {
|
||||
return true;
|
||||
@@ -13,10 +13,13 @@ class TryLimiter {
|
||||
if (times >= maxCount) {
|
||||
return false;
|
||||
}
|
||||
_triedTimes[id] = times + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void inc(String sid) {
|
||||
_triedTimes[sid] = (_triedTimes[sid] ?? 0) + 1;
|
||||
}
|
||||
|
||||
void reset(String id) {
|
||||
_triedTimes[id] = 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppProvider extends BusyProvider {
|
||||
class AppProvider extends ChangeNotifier {
|
||||
int? _newestBuild;
|
||||
int? get newestBuild => _newestBuild;
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/core/extension/ssh_client.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/docker/image.dart';
|
||||
import 'package:toolbox/data/model/docker/ps.dart';
|
||||
import 'package:toolbox/data/model/app/error.dart';
|
||||
@@ -15,13 +16,14 @@ import 'package:toolbox/locator.dart';
|
||||
|
||||
final _dockerNotFound = RegExp(r'command not found|Unknown command');
|
||||
final _versionReg = RegExp(r'(Version:)\s+([0-9]+\.[0-9]+\.[0-9]+)');
|
||||
final _editionReg = RegExp(r'(Client:)\s+(.+-.+)');
|
||||
// eg: `Docker Engine - Community`
|
||||
final _editionReg = RegExp(r'Docker Engine - [a-zA-Z]+');
|
||||
final _dockerPrefixReg = RegExp(r'(sudo )?docker ');
|
||||
|
||||
final _logger = Logger('DOCKER');
|
||||
|
||||
class DockerProvider extends BusyProvider {
|
||||
final dockerStore = locator<DockerStore>();
|
||||
class DockerProvider extends ChangeNotifier {
|
||||
final _dockerStore = locator<DockerStore>();
|
||||
|
||||
SSHClient? client;
|
||||
String? userName;
|
||||
@@ -35,8 +37,12 @@ class DockerProvider extends BusyProvider {
|
||||
String? runLog;
|
||||
bool isRequestingPwd = false;
|
||||
|
||||
void init(SSHClient client, String userName, PwdRequestFunc onPwdReq,
|
||||
String hostId) {
|
||||
void init(
|
||||
SSHClient client,
|
||||
String userName,
|
||||
PwdRequestFunc onPwdReq,
|
||||
String hostId,
|
||||
) {
|
||||
this.client = client;
|
||||
this.userName = userName;
|
||||
this.onPwdReq = onPwdReq;
|
||||
@@ -50,19 +56,17 @@ class DockerProvider extends BusyProvider {
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
if (isBusy) return;
|
||||
setBusyState();
|
||||
|
||||
var raw = '';
|
||||
await client!.exec(
|
||||
shellFuncDocker.exec,
|
||||
AppShellFuncType.docker.exec,
|
||||
onStderr: _onPwd,
|
||||
onStdout: (data, _) => raw = '$raw$data',
|
||||
);
|
||||
|
||||
if (raw.contains(_dockerNotFound)) {
|
||||
error = DockerErr(type: DockerErrType.notInstalled);
|
||||
setBusyState(false);
|
||||
_logger.warning('Docker not installed: $raw');
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,65 +74,72 @@ class DockerProvider extends BusyProvider {
|
||||
final segments = raw.split(seperator);
|
||||
if (segments.length != dockerCmds.length) {
|
||||
error = DockerErr(type: DockerErrType.segmentsNotMatch);
|
||||
setBusyState(false);
|
||||
_logger.warning('Docker segments not match: ${segments.length}');
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse docker version
|
||||
final verRaw = segments[0];
|
||||
try {
|
||||
version = _versionReg.firstMatch(verRaw)?.group(2);
|
||||
edition = _editionReg.firstMatch(verRaw)?.group(2);
|
||||
} catch (e) {
|
||||
error = DockerErr(type: DockerErrType.unknown, message: e.toString());
|
||||
rethrow;
|
||||
}
|
||||
final verRaw = DockerCmdType.version.find(segments);
|
||||
version = _versionReg.firstMatch(verRaw)?.group(2);
|
||||
edition = _editionReg.firstMatch(verRaw)?.group(0);
|
||||
|
||||
// Parse docker ps
|
||||
final psRaw = segments[1];
|
||||
final psRaw = DockerCmdType.ps.find(segments);
|
||||
try {
|
||||
final lines = psRaw.split('\n');
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
lines.removeAt(0);
|
||||
if (lines.isNotEmpty) lines.removeAt(0);
|
||||
items = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
|
||||
} catch (e) {
|
||||
error = DockerErr(type: DockerErrType.unknown, message: e.toString());
|
||||
rethrow;
|
||||
error = DockerErr(
|
||||
type: DockerErrType.parsePsItem,
|
||||
message: '$psRaw\n-\n$e',
|
||||
);
|
||||
_logger.warning('Parse docker ps: $psRaw', e);
|
||||
} finally {
|
||||
setBusyState(false);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse docker images
|
||||
final imageRaw = segments[3];
|
||||
final imageRaw = DockerCmdType.images.find(segments);
|
||||
try {
|
||||
final imageLines = imageRaw.split('\n');
|
||||
imageLines.removeWhere((element) => element.isEmpty);
|
||||
imageLines.removeAt(0);
|
||||
if (imageLines.isNotEmpty) imageLines.removeAt(0);
|
||||
images = imageLines.map((e) => DockerImage.fromRawStr(e)).toList();
|
||||
} catch (e) {
|
||||
error = DockerErr(type: DockerErrType.unknown, message: e.toString());
|
||||
rethrow;
|
||||
} catch (e, trace) {
|
||||
error = DockerErr(
|
||||
type: DockerErrType.parseImages,
|
||||
message: '$imageRaw\n-\n$e',
|
||||
);
|
||||
_logger.warning('Parse docker images: $imageRaw', e, trace);
|
||||
} finally {
|
||||
setBusyState(false);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse docker stats
|
||||
final statsRaw = segments[2];
|
||||
final statsRaw = DockerCmdType.stats.find(segments);
|
||||
try {
|
||||
final statsLines = statsRaw.split('\n');
|
||||
statsLines.removeWhere((element) => element.isEmpty);
|
||||
statsLines.removeAt(0);
|
||||
if (statsLines.isNotEmpty) statsLines.removeAt(0);
|
||||
for (var item in items!) {
|
||||
final statsLine = statsLines.firstWhere(
|
||||
(element) => element.contains(item.containerId),
|
||||
orElse: () => '',
|
||||
);
|
||||
if (statsLine.isEmpty) continue;
|
||||
item.parseStats(statsLine);
|
||||
}
|
||||
} catch (e) {
|
||||
error = DockerErr(type: DockerErrType.unknown, message: e.toString());
|
||||
} catch (e, trace) {
|
||||
error = DockerErr(
|
||||
type: DockerErrType.parseStats,
|
||||
message: '$statsRaw\n-\n$e',
|
||||
);
|
||||
_logger.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
} finally {
|
||||
setBusyState(false);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +171,6 @@ class DockerProvider extends BusyProvider {
|
||||
if (!cmd.startsWith(_dockerPrefixReg)) {
|
||||
return DockerErr(type: DockerErrType.cmdNoPrefix);
|
||||
}
|
||||
setBusyState();
|
||||
|
||||
runLog = '';
|
||||
final errs = <String>[];
|
||||
@@ -176,22 +186,21 @@ class DockerProvider extends BusyProvider {
|
||||
},
|
||||
);
|
||||
runLog = null;
|
||||
notifyListeners();
|
||||
|
||||
if (code != 0) {
|
||||
setBusyState(false);
|
||||
return DockerErr(
|
||||
type: DockerErrType.unknown,
|
||||
message: errs.join('\n').trim(),
|
||||
);
|
||||
}
|
||||
await refresh();
|
||||
setBusyState(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
// judge whether to use DOCKER_HOST
|
||||
String _wrap(String cmd) {
|
||||
final dockerHost = dockerStore.getDockerHost(hostId!);
|
||||
final dockerHost = _dockerStore.fetch(hostId!);
|
||||
if (dockerHost == null || dockerHost.isEmpty) {
|
||||
return cmd.withLangExport;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/core/extension/ssh_client.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:toolbox/data/model/pkg/manager.dart';
|
||||
import 'package:toolbox/data/model/pkg/upgrade_info.dart';
|
||||
import 'package:toolbox/data/model/server/dist.dart';
|
||||
|
||||
class PkgProvider extends BusyProvider {
|
||||
class PkgProvider extends ChangeNotifier {
|
||||
final logger = Logger('PKG');
|
||||
|
||||
SSHClient? client;
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/model/server/private_key_info.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
class PrivateKeyProvider extends BusyProvider {
|
||||
List<PrivateKeyInfo> get infos => _infos;
|
||||
class PrivateKeyProvider extends ChangeNotifier {
|
||||
List<PrivateKeyInfo> get pkis => _pkis;
|
||||
final _store = locator<PrivateKeyStore>();
|
||||
late List<PrivateKeyInfo> _infos;
|
||||
late List<PrivateKeyInfo> _pkis;
|
||||
|
||||
void loadData() {
|
||||
_infos = _store.fetch();
|
||||
_pkis = _store.fetch();
|
||||
}
|
||||
|
||||
void addInfo(PrivateKeyInfo info) {
|
||||
_infos.add(info);
|
||||
void add(PrivateKeyInfo info) {
|
||||
_pkis.add(info);
|
||||
_store.put(info);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void delInfo(PrivateKeyInfo info) {
|
||||
_infos.removeWhere((e) => e.id == info.id);
|
||||
void delete(PrivateKeyInfo info) {
|
||||
_pkis.removeWhere((e) => e.id == info.id);
|
||||
_store.delete(info);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateInfo(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
|
||||
final idx = _infos.indexWhere((e) => e.id == old.id);
|
||||
_infos[idx] = newInfo;
|
||||
void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
|
||||
final idx = _pkis.indexWhere((e) => e.id == old.id);
|
||||
if (idx == -1) {
|
||||
_pkis.add(newInfo);
|
||||
} else {
|
||||
_pkis[idx] = newInfo;
|
||||
}
|
||||
_store.put(newInfo);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
|
||||
import '../../core/extension/order.dart';
|
||||
import '../../core/extension/uint8list.dart';
|
||||
import '../../core/provider_base.dart';
|
||||
import '../../core/utils/server.dart';
|
||||
import '../../locator.dart';
|
||||
import '../model/server/server.dart';
|
||||
@@ -20,7 +20,7 @@ import '../store/setting.dart';
|
||||
|
||||
typedef ServersMap = Map<String, Server>;
|
||||
|
||||
class ServerProvider extends BusyProvider {
|
||||
class ServerProvider extends ChangeNotifier {
|
||||
final ServersMap _servers = {};
|
||||
ServersMap get servers => _servers;
|
||||
final Order<String> _serverOrder = [];
|
||||
@@ -38,7 +38,6 @@ class ServerProvider extends BusyProvider {
|
||||
final _settingStore = locator<SettingStore>();
|
||||
|
||||
Future<void> loadLocalData() async {
|
||||
setBusyState(true);
|
||||
final spis = _serverStore.fetch();
|
||||
for (final spi in spis) {
|
||||
_servers[spi.id] = genServer(spi);
|
||||
@@ -55,7 +54,6 @@ class ServerProvider extends BusyProvider {
|
||||
}
|
||||
_settingStore.serverOrder.put(_serverOrder);
|
||||
_updateTags();
|
||||
setBusyState(false);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -203,77 +201,98 @@ class ServerProvider extends BusyProvider {
|
||||
}
|
||||
}
|
||||
|
||||
void _setServerState(Server s, ServerState ss) {
|
||||
s.state = ss;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _getData(ServerPrivateInfo spi) async {
|
||||
final sid = spi.id;
|
||||
final s = _servers[sid];
|
||||
|
||||
if (s == null) return;
|
||||
|
||||
var raw = '';
|
||||
var segments = <String>[];
|
||||
|
||||
try {
|
||||
final state = s.state;
|
||||
if (state.shouldConnect) {
|
||||
if (!_limiter.shouldTry(sid)) {
|
||||
s.state = ServerState.failed;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
s.state = ServerState.connecting;
|
||||
notifyListeners();
|
||||
|
||||
// try to connect
|
||||
final time1 = DateTime.now();
|
||||
s.client = await genClient(spi);
|
||||
final time2 = DateTime.now();
|
||||
final spentTime = time2.difference(time1).inMilliseconds;
|
||||
_logger.info('Connected to $sid in $spentTime ms.');
|
||||
|
||||
// after connected
|
||||
s.state = ServerState.connected;
|
||||
notifyListeners();
|
||||
// write script to server
|
||||
final writeResult = await s.client!.run(installShellCmd).string;
|
||||
|
||||
// if write failed
|
||||
if (writeResult.isNotEmpty) {
|
||||
throw Exception(writeResult);
|
||||
}
|
||||
// reset try times if connected successfully
|
||||
_limiter.reset(sid);
|
||||
if (!_limiter.canTry(sid)) {
|
||||
if (s.state != ServerState.failed) {
|
||||
_setServerState(s, ServerState.failed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (s.client == null) return;
|
||||
// run script to get server status
|
||||
raw = await s.client!.run(shellFuncStatus.exec).string;
|
||||
segments = raw.split(seperator).map((e) => e.trim()).toList();
|
||||
if (raw.isEmpty || segments.length != CmdType.values.length) {
|
||||
s.state = ServerState.failed;
|
||||
if (s.status.failedInfo?.isEmpty ?? true) {
|
||||
s.status.failedInfo = 'Seperate segments failed, raw:\n$raw';
|
||||
}
|
||||
if (s.state.shouldConnect || (s.client?.isClosed ?? true)) {
|
||||
_setServerState(s, ServerState.connecting);
|
||||
|
||||
final time1 = DateTime.now();
|
||||
|
||||
try {
|
||||
s.client = await genClient(spi);
|
||||
} catch (e) {
|
||||
_limiter.inc(sid);
|
||||
s.status.failedInfo = e.toString();
|
||||
_setServerState(s, ServerState.failed);
|
||||
_logger.warning('Connect to $sid failed', e);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
s.state = ServerState.failed;
|
||||
s.status.failedInfo = e.toString();
|
||||
rethrow;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
|
||||
final time2 = DateTime.now();
|
||||
final spentTime = time2.difference(time1).inMilliseconds;
|
||||
_logger.info('Connected to $sid in $spentTime ms.');
|
||||
|
||||
_setServerState(s, ServerState.connected);
|
||||
|
||||
try {
|
||||
final writeResult = await s.client?.run(installShellCmd).string;
|
||||
if (writeResult == null || writeResult.isNotEmpty) {
|
||||
_limiter.inc(sid);
|
||||
s.status.failedInfo = writeResult;
|
||||
_setServerState(s, ServerState.failed);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
_limiter.inc(sid);
|
||||
s.status.failedInfo = e.toString();
|
||||
_setServerState(s, ServerState.failed);
|
||||
_logger.warning('Write script to $sid failed', e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (s.client == null) return;
|
||||
|
||||
if (s.state != ServerState.finished) {
|
||||
_setServerState(s, ServerState.loading);
|
||||
}
|
||||
|
||||
final raw = await s.client?.run(AppShellFuncType.status.exec).string;
|
||||
final segments = raw?.split(seperator).map((e) => e.trim()).toList();
|
||||
if (raw == null ||
|
||||
raw.isEmpty ||
|
||||
segments == null ||
|
||||
segments.length != StatusCmdType.values.length) {
|
||||
_limiter.inc(sid);
|
||||
s.status.failedInfo = 'Seperate segments failed, raw:\n$raw';
|
||||
_setServerState(s, ServerState.failed);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final req = ServerStatusUpdateReq(s.status, segments);
|
||||
s.status = await compute(getStatus, req);
|
||||
// Comment for debug
|
||||
// s.status = await getStatus(req);
|
||||
} catch (e) {
|
||||
s.state = ServerState.failed;
|
||||
} catch (e, trace) {
|
||||
_limiter.inc(sid);
|
||||
s.status.failedInfo = 'Parse failed: $e\n\n$raw';
|
||||
rethrow;
|
||||
} finally {
|
||||
_setServerState(s, ServerState.failed);
|
||||
_logger.warning('Parse failed', e, trace);
|
||||
return;
|
||||
}
|
||||
|
||||
if (s.state != ServerState.finished) {
|
||||
_setServerState(s, ServerState.finished);
|
||||
} else {
|
||||
notifyListeners();
|
||||
}
|
||||
// reset try times only after prepared successfully
|
||||
_limiter.reset(sid);
|
||||
}
|
||||
|
||||
Future<String?> runSnippets(String id, List<Snippet> snippets) async {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../model/sftp/req.dart';
|
||||
|
||||
class SftpProvider extends ProviderBase {
|
||||
class SftpProvider extends ChangeNotifier {
|
||||
final List<SftpReqStatus> _status = [];
|
||||
List<SftpReqStatus> get status => _status;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/store/snippet.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
@@ -8,7 +8,7 @@ import 'package:toolbox/locator.dart';
|
||||
import '../../core/extension/order.dart';
|
||||
import '../store/setting.dart';
|
||||
|
||||
class SnippetProvider extends BusyProvider {
|
||||
class SnippetProvider extends ChangeNotifier {
|
||||
late Order<Snippet> _snippets;
|
||||
Order<Snippet> get snippets => _snippets;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 406;
|
||||
static const int build = 491;
|
||||
static const String engine = "3.10.6";
|
||||
static const String buildAt = "2023-08-02 23:34:08.619104";
|
||||
static const int modifications = 2;
|
||||
static const String buildAt = "2023-08-20 23:32:07.343451";
|
||||
static const int modifications = 4;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../model/app/github_id.dart';
|
||||
|
||||
/// RegExp for number
|
||||
final numReg = RegExp(r'\s{1,}');
|
||||
|
||||
@@ -16,3 +18,35 @@ const maxDebugLogLines = 100;
|
||||
const pkgName = 'tech.lolli.toolbox';
|
||||
const bgRunChannel = MethodChannel('$pkgName/app_retain');
|
||||
const homeWidgetChannel = MethodChannel('$pkgName/home_widget');
|
||||
|
||||
// Thanks
|
||||
// If you want to change the url, please open an issue.
|
||||
const contributors = <GhId>{
|
||||
'its-tom',
|
||||
'RainSunMe',
|
||||
'kalashnikov',
|
||||
'azkadev',
|
||||
'calvinweb',
|
||||
'Liloupar'
|
||||
};
|
||||
const participants = <GhId>{
|
||||
'jaychoubaby',
|
||||
'fecture',
|
||||
'Tao173',
|
||||
'QingAnLe',
|
||||
'wxdjs',
|
||||
'Aeorq',
|
||||
'allonmymind',
|
||||
'Yuuki-Rin',
|
||||
'LittleState',
|
||||
'karuboniru',
|
||||
'whosphp',
|
||||
'Climit',
|
||||
'dianso',
|
||||
'Jasondeepny',
|
||||
'kaliwell',
|
||||
'ymxkiss',
|
||||
'Ealrang',
|
||||
'hange33',
|
||||
'yuchen1204',
|
||||
};
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import '../model/app/shell_func.dart';
|
||||
import 'build_data.dart';
|
||||
|
||||
const seperator = 'SrvBox';
|
||||
const seperator = 'SrvBoxSep';
|
||||
const serverBoxDir = r'$HOME/.config/server_box';
|
||||
const shellPath = '$serverBoxDir/mobile_app.sh';
|
||||
|
||||
const echoPWD = 'echo \$PWD';
|
||||
|
||||
enum CmdType {
|
||||
net,
|
||||
sys,
|
||||
cpu,
|
||||
uptime,
|
||||
conn,
|
||||
disk,
|
||||
mem,
|
||||
tempType,
|
||||
tempVal,
|
||||
host,
|
||||
sysRhel,
|
||||
}
|
||||
|
||||
const _cmdList = [
|
||||
'cat /proc/net/dev && date +%s',
|
||||
const statusCmds = [
|
||||
'date +%s',
|
||||
'cat /proc/net/dev',
|
||||
'cat /etc/os-release | grep PRETTY_NAME',
|
||||
'cat /proc/stat | grep cpu',
|
||||
'uptime',
|
||||
@@ -35,12 +20,6 @@ const _cmdList = [
|
||||
'cat /etc/redhat-release',
|
||||
];
|
||||
|
||||
final shellFuncStatus = AppShellFunc(
|
||||
'status',
|
||||
_cmdList.join('\necho $seperator\n'),
|
||||
's',
|
||||
);
|
||||
|
||||
const dockerCmds = [
|
||||
'docker version',
|
||||
'docker ps -a',
|
||||
@@ -48,26 +27,13 @@ const dockerCmds = [
|
||||
'docker image ls',
|
||||
];
|
||||
|
||||
final shellFuncDocker = AppShellFunc(
|
||||
// `dockeR` -> avoid conflict with `docker` command
|
||||
// 以防止循环递归
|
||||
'dockeR',
|
||||
dockerCmds.join('\necho $seperator\n'),
|
||||
'd',
|
||||
);
|
||||
|
||||
final _generated = [
|
||||
shellFuncStatus,
|
||||
shellFuncDocker,
|
||||
].generate;
|
||||
|
||||
final shellCmd = """
|
||||
# Script for app `${BuildData.name} v1.0.${BuildData.build}`
|
||||
# Delete this file while app is running will cause app crash
|
||||
|
||||
export LANG=en_US.utf-8
|
||||
export LANG=en_US.UTF-8
|
||||
|
||||
$_generated
|
||||
${AppShellFuncType.shellScript}
|
||||
""";
|
||||
|
||||
final installShellCmd = "mkdir -p $serverBoxDir && "
|
||||
|
||||
@@ -31,7 +31,7 @@ NetSpeedPart get _initNetSpeedPart => NetSpeedPart(
|
||||
'',
|
||||
BigInt.zero,
|
||||
BigInt.zero,
|
||||
BigInt.zero,
|
||||
0,
|
||||
);
|
||||
NetSpeed get initNetSpeed => NetSpeed(
|
||||
[_initNetSpeedPart],
|
||||
|
||||
@@ -2,15 +2,18 @@ import 'package:flutter/material.dart';
|
||||
|
||||
/// Font style
|
||||
|
||||
const textSize9Grey = TextStyle(color: Colors.grey, fontSize: 9);
|
||||
const textSize11 = TextStyle(fontSize: 11);
|
||||
const textSize12Grey = TextStyle(color: Colors.grey, fontSize: 11);
|
||||
const textSize11Grey = TextStyle(color: Colors.grey, fontSize: 11);
|
||||
const textSize13 = TextStyle(fontSize: 13);
|
||||
const textSize13Bold = TextStyle(fontSize: 13, fontWeight: FontWeight.bold);
|
||||
const textSize13Grey = TextStyle(color: Colors.grey, fontSize: 13);
|
||||
const textSize15 = TextStyle(fontSize: 15);
|
||||
const textSize18 = TextStyle(fontSize: 18);
|
||||
const textSize27 = TextStyle(fontSize: 27);
|
||||
|
||||
const grey = TextStyle(color: Colors.grey);
|
||||
const textRed = TextStyle(color: Colors.red);
|
||||
|
||||
/// Icon
|
||||
|
||||
@@ -21,7 +24,10 @@ final appIcon = Image.asset('assets/app_icon.png');
|
||||
const roundRectCardPadding = EdgeInsets.symmetric(horizontal: 17, vertical: 13);
|
||||
|
||||
/// SizedBox
|
||||
|
||||
const placeholder = SizedBox();
|
||||
const height13 = SizedBox(height: 13);
|
||||
const height77 = SizedBox(height: 77);
|
||||
const width13 = SizedBox(width: 13);
|
||||
const width7 = SizedBox(width: 7);
|
||||
|
||||
|
||||
@@ -3,18 +3,3 @@ const baseUrl = '$backendUrl/serverbox';
|
||||
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
|
||||
const myGithub = 'https://github.com/lollipopkit';
|
||||
const appHelpUrl = '$myGithub/flutter_server_box#-help';
|
||||
|
||||
// Thanks
|
||||
// If you want to change the url, please open an issue.
|
||||
const thanksMap = {
|
||||
'its-tom': 'https://github.com/its-tom',
|
||||
'RainSunMe': 'https://github.com/RainSunMe',
|
||||
'fecture': 'https://github.com/fecture',
|
||||
'Tao173': 'https://github.com/Tao173',
|
||||
'QingAnLe': 'https://github.com/QingAnLe',
|
||||
'wxdjs': 'https://github.com/wxdjs',
|
||||
'Aeorq': 'https://github.com/Aeorq',
|
||||
'jaychoubaby': 'https://github.com/jaychoubaby',
|
||||
'allonmymind': 'https://github.com/allonmymind',
|
||||
'azkadev': 'https://github.com/azkadev'
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'package:toolbox/core/persistant_store.dart';
|
||||
|
||||
class DockerStore extends PersistentStore {
|
||||
String? getDockerHost(String id) {
|
||||
String? fetch(String id) {
|
||||
return box.get(id);
|
||||
}
|
||||
|
||||
void setDockerHost(String id, String host) {
|
||||
void put(String id, String host) {
|
||||
box.put(id, host);
|
||||
}
|
||||
|
||||
Map<String, String> fetch() {
|
||||
Map<String, String> fetchAll() {
|
||||
return box.toMap().cast<String, String>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ class SettingStore extends PersistentStore {
|
||||
StoreProperty<bool> get sshVirtualKeyAutoOff =>
|
||||
property('sshVirtualKeyAutoOff', defaultValue: true);
|
||||
|
||||
StoreProperty<double> get editorFontSize =>
|
||||
property('editorFontSize', defaultValue: 13);
|
||||
|
||||
// Editor theme
|
||||
StoreProperty<String> get editorTheme =>
|
||||
property('editorTheme', defaultValue: defaultEditorTheme);
|
||||
@@ -99,4 +102,13 @@ class SettingStore extends PersistentStore {
|
||||
// Only valid on iOS
|
||||
StoreProperty<bool> get autoUpdateHomeWidget =>
|
||||
property('autoUpdateHomeWidget', defaultValue: isIOS);
|
||||
|
||||
StoreProperty<bool> get autoCheckAppUpdate =>
|
||||
property('autoCheckAppUpdate', defaultValue: true);
|
||||
|
||||
/// Display server tab function buttons on the bottom of each server card if [true]
|
||||
///
|
||||
/// Otherwise, display them on the top of server detail page
|
||||
StoreProperty<bool> get moveOutServerTabFuncBtns =>
|
||||
property('moveOutServerTabFuncBtns', defaultValue: true);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"add": "Neu",
|
||||
"addAServer": "Server hinzufügen",
|
||||
"addPrivateKey": "Private key hinzufügen",
|
||||
"addSystemPrivateKeyTip": "Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?",
|
||||
"added2List": "Zur Aufgabenliste hinzugefügt",
|
||||
"all": "Alle",
|
||||
"alreadyLastDir": "Bereits im letzten Verzeichnis.",
|
||||
"alterUrl": "Url ändern",
|
||||
"attention": "Achtung",
|
||||
"auto": "System folgen",
|
||||
"autoCheckUpdate": "Aktualisierung automatisch prüfen",
|
||||
"autoUpdateHomeWidget": "Home-Widget automatisch aktualisieren",
|
||||
"backup": "Backup",
|
||||
"backupAndRestore": "Backup und Wiederherstellung",
|
||||
@@ -26,6 +28,7 @@
|
||||
"close": "Schließen",
|
||||
"cmd": "Command",
|
||||
"conn": "Verbindung",
|
||||
"connected": "in Verbindung gebracht",
|
||||
"containerName": "Container Name",
|
||||
"containerStatus": "Container Status",
|
||||
"convert": "Konvertieren",
|
||||
@@ -75,6 +78,7 @@
|
||||
"fullScreenJitterHelp": "Einbrennen des Bildschirms verhindern",
|
||||
"getPushTokenFailed": "Push-Token kann nicht abgerufen werden",
|
||||
"gettingToken": "Getting token...",
|
||||
"goBackQ": "Zurückkommen?",
|
||||
"goto": "Pfad öffnen",
|
||||
"homeWidgetUrlConfig": "Home-Widget-Link konfigurieren",
|
||||
"host": "Host",
|
||||
@@ -109,6 +113,8 @@
|
||||
"maxRetryCountEqual0": "Unbegrenzte Verbindungsversuche zum Server",
|
||||
"min": "min",
|
||||
"mission": "Mission",
|
||||
"moveOutServerFuncBtns": "Position der Server-Funktionsschaltfläche",
|
||||
"moveOutServerFuncBtnsHelp": "Ein: kann unter jeder Karte auf der Registerkarte \"Server\" angezeigt werden. Aus: kann oben auf der Seite \"Serverdetails\" angezeigt werden.",
|
||||
"ms": "ms",
|
||||
"name": "Name",
|
||||
"needRestart": "App muss neugestartet werden",
|
||||
@@ -121,6 +127,7 @@
|
||||
"noSavedPrivateKey": "Keine gespeicherten Private Keys",
|
||||
"noSavedSnippet": "Keine gespeicherten Snippets.",
|
||||
"noServerAvailable": "Kein Server verfügbar.",
|
||||
"noTask": "Nicht fragen",
|
||||
"noUpdateAvailable": "Kein Update verfügbar",
|
||||
"notSelected": "Nicht ausgewählt",
|
||||
"nullToken": "Null token",
|
||||
@@ -139,7 +146,7 @@
|
||||
"plzSelectKey": "Wähle einen Key.",
|
||||
"port": "Port",
|
||||
"preview": "Vorschau",
|
||||
"primaryColor": "Farbschema",
|
||||
"primaryColorSeed": "Farbschema",
|
||||
"privateKey": "Private Key",
|
||||
"process": "Prozess",
|
||||
"pushToken": "Push Token",
|
||||
@@ -158,6 +165,8 @@
|
||||
"saved": "Gerettet",
|
||||
"second": "s",
|
||||
"server": "Server",
|
||||
"serverDetailOrder": "Reihenfolge der Widgets auf der Detailseite",
|
||||
"serverOrder": "Server-Bestellung",
|
||||
"serverTabConnecting": "Verbinden...",
|
||||
"serverTabEmpty": "Keine Server vorhanden.",
|
||||
"serverTabFailed": "Fehlgeschlagen",
|
||||
@@ -166,7 +175,6 @@
|
||||
"serverTabUnkown": "Unbekannter Status",
|
||||
"setting": "Einstellungen",
|
||||
"sftpDlPrepare": "Verbindung vorbereiten...",
|
||||
"sftpNoDownloadTask": "Keine aktiven Downloads.",
|
||||
"sftpSSHConnected": "SFTP Verbunden",
|
||||
"showDistLogo": "Distributionslogo anzeigen",
|
||||
"snippet": "Snippet",
|
||||
@@ -175,11 +183,13 @@
|
||||
"sshTip": "Diese Funktion befindet sich jetzt in der Experimentierphase.\n\nBitte melde Bugs auf {url} oder mach mit bei der Entwicklung.",
|
||||
"sshVirtualKeyAutoOff": "Automatische Umschaltung der virtuellen Tasten",
|
||||
"start": "Start",
|
||||
"stats": "Statistik",
|
||||
"stop": "Stop",
|
||||
"success": "Erfolgreich",
|
||||
"sureDelete": "Soll [{name}] wirklich gelöscht werden?",
|
||||
"sureDirEmpty": "Stelle sicher, dass der Ordner leer ist.",
|
||||
"sureNoPwd": "Bist du sicher, dass du kein Passwort verwenden willst?",
|
||||
"sureStop": "Sind Sie sicher, dass Sie [{item}] stoppen möchten?",
|
||||
"sureToDeleteServer": "Bist du sicher, dass du [{server}] löschen willst?",
|
||||
"system": "Systeme",
|
||||
"tag": "Tags",
|
||||
@@ -203,7 +213,7 @@
|
||||
"urlOrJson": "URL oder JSON",
|
||||
"user": "Benutzer",
|
||||
"versionHaveUpdate": "Gefunden: v1.0.{build}, klicke zum Aktualisieren",
|
||||
"versionUnknownUpdate": "Aktuell: v1.0.{build}",
|
||||
"versionUnknownUpdate": "Aktuell: v1.0.{build}. Klicken Sie hier, um nach Updates zu suchen",
|
||||
"versionUpdated": "v1.0.{build} ist bereits die neueste Version",
|
||||
"viewErr": "Fehler anzeigen",
|
||||
"virtKeyHelpClipboard": "In die Zwischenablage kopieren, wenn das ausgewählte Terminal nicht leer ist, andernfalls den Inhalt der Zwischenablage in das Terminal einfügen.",
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"add": "Add",
|
||||
"addAServer": "add a server",
|
||||
"addPrivateKey": "Add private key",
|
||||
"addSystemPrivateKeyTip": "Currently don't have any private key, do you add the one that comes with the system (~/.ssh/id_rsa)?",
|
||||
"added2List": "Added to task list",
|
||||
"all": "All",
|
||||
"alreadyLastDir": "Already in last directory.",
|
||||
"alterUrl": "Alter url",
|
||||
"attention": "Attention",
|
||||
"auto": "Auto",
|
||||
"autoCheckUpdate": "Auto check update",
|
||||
"autoUpdateHomeWidget": "Auto update home widget",
|
||||
"backup": "Backup",
|
||||
"backupAndRestore": "Backup and Restore",
|
||||
@@ -26,6 +28,7 @@
|
||||
"close": "Close",
|
||||
"cmd": "Command",
|
||||
"conn": "Connection",
|
||||
"connected": "Connected",
|
||||
"containerName": "Container name",
|
||||
"containerStatus": "Container status",
|
||||
"convert": "Convert",
|
||||
@@ -75,6 +78,7 @@
|
||||
"fullScreenJitterHelp": "To avoid screen burn-in",
|
||||
"getPushTokenFailed": "Can't fetch push token",
|
||||
"gettingToken": "Getting token...",
|
||||
"goBackQ": "Go back?",
|
||||
"goto": "Go to",
|
||||
"homeWidgetUrlConfig": "Config home widget url",
|
||||
"host": "Host",
|
||||
@@ -109,6 +113,8 @@
|
||||
"maxRetryCountEqual0": "Will retry again and again.",
|
||||
"min": "min",
|
||||
"mission": "Mission",
|
||||
"moveOutServerFuncBtns": "Server function button location",
|
||||
"moveOutServerFuncBtnsHelp": "On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.",
|
||||
"ms": "ms",
|
||||
"name": "Name",
|
||||
"needRestart": "Need to restart app",
|
||||
@@ -121,6 +127,7 @@
|
||||
"noSavedPrivateKey": "No saved private keys.",
|
||||
"noSavedSnippet": "No saved snippets.",
|
||||
"noServerAvailable": "No server available.",
|
||||
"noTask": "No task",
|
||||
"noUpdateAvailable": "No update available",
|
||||
"notSelected": "Not selected",
|
||||
"nullToken": "Null token",
|
||||
@@ -139,7 +146,7 @@
|
||||
"plzSelectKey": "Please select a key.",
|
||||
"port": "Port",
|
||||
"preview": "Preview",
|
||||
"primaryColor": "Primary color",
|
||||
"primaryColorSeed": "Primary color seed",
|
||||
"privateKey": "Private Key",
|
||||
"process": "Process",
|
||||
"pushToken": "Push token",
|
||||
@@ -158,6 +165,8 @@
|
||||
"saved": "Saved",
|
||||
"second": "s",
|
||||
"server": "Server",
|
||||
"serverDetailOrder": "Detail page widget order",
|
||||
"serverOrder": "Server order",
|
||||
"serverTabConnecting": "Connecting...",
|
||||
"serverTabEmpty": "There is no server.\nClick the fab to add one.",
|
||||
"serverTabFailed": "Failed",
|
||||
@@ -166,7 +175,6 @@
|
||||
"serverTabUnkown": "Unknown state",
|
||||
"setting": "Settings",
|
||||
"sftpDlPrepare": "Preparing to connect...",
|
||||
"sftpNoDownloadTask": "No download task.",
|
||||
"sftpSSHConnected": "SFTP Connected",
|
||||
"showDistLogo": "Show distribution logo",
|
||||
"snippet": "Snippet",
|
||||
@@ -175,11 +183,13 @@
|
||||
"sshTip": "This function is now in the experimental stage.\n\nPlease report bugs on {url} or join our development.",
|
||||
"sshVirtualKeyAutoOff": "Auto switching of virtual keys",
|
||||
"start": "Start",
|
||||
"stats": "Stats",
|
||||
"stop": "Stop",
|
||||
"success": "Success",
|
||||
"sureDelete": "Are you sure to delete [{name}]?",
|
||||
"sureDirEmpty": "Make sure dir is empty.",
|
||||
"sureNoPwd": "Are you sure to use no password?",
|
||||
"sureStop": "Sure to stop [{item}] ?",
|
||||
"sureToDeleteServer": "Are you sure to delete server [{server}]?",
|
||||
"system": "System",
|
||||
"tag": "Tags",
|
||||
@@ -203,7 +213,7 @@
|
||||
"urlOrJson": "URL or JSON",
|
||||
"user": "User",
|
||||
"versionHaveUpdate": "Found: v1.0.{build}, click to update",
|
||||
"versionUnknownUpdate": "Current: v1.0.{build}",
|
||||
"versionUnknownUpdate": "Current: v1.0.{build}, click to check updates",
|
||||
"versionUpdated": "Current: v1.0.{build}, is up to date",
|
||||
"viewErr": "See error",
|
||||
"virtKeyHelpClipboard": "Copy to the clipboard if terminal selected is not empty, otherwise paste the contents of the clipboard to the terminal.",
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"add": "Menambahkan",
|
||||
"addAServer": "tambahkan server",
|
||||
"addPrivateKey": "Tambahkan kunci pribadi",
|
||||
"addSystemPrivateKeyTip": "Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?",
|
||||
"added2List": "Ditambahkan ke Daftar Tugas",
|
||||
"all": "Semua",
|
||||
"alreadyLastDir": "Sudah di direktori terakhir.",
|
||||
"alterUrl": "Alter url",
|
||||
"attention": "Perhatian",
|
||||
"auto": "Auto",
|
||||
"autoCheckUpdate": "Periksa pembaruan otomatis",
|
||||
"autoUpdateHomeWidget": "Widget Rumah Pembaruan Otomatis",
|
||||
"backup": "Cadangan",
|
||||
"backupAndRestore": "Cadangan dan Pulihkan",
|
||||
@@ -26,6 +28,7 @@
|
||||
"close": "Menutup",
|
||||
"cmd": "Memerintah",
|
||||
"conn": "Koneksi",
|
||||
"connected": "Terhubung",
|
||||
"containerName": "Nama kontainer",
|
||||
"containerStatus": "Status wadah",
|
||||
"convert": "Mengubah",
|
||||
@@ -75,6 +78,7 @@
|
||||
"fullScreenJitterHelp": "Untuk menghindari pembakaran layar",
|
||||
"getPushTokenFailed": "Tidak bisa mengambil token dorong",
|
||||
"gettingToken": "Mendapatkan token ...",
|
||||
"goBackQ": "Datang kembali?",
|
||||
"goto": "Pergi ke",
|
||||
"homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah",
|
||||
"host": "Host",
|
||||
@@ -109,6 +113,8 @@
|
||||
"maxRetryCountEqual0": "Akan mencoba lagi lagi dan lagi.",
|
||||
"min": "Min",
|
||||
"mission": "Misi",
|
||||
"moveOutServerFuncBtns": "Lokasi tombol fungsi server",
|
||||
"moveOutServerFuncBtnsHelp": "Aktif: dapat ditampilkan di bawah setiap kartu pada halaman Tab Server. Nonaktif: dapat ditampilkan di bagian atas halaman Rincian Server.",
|
||||
"ms": "MS",
|
||||
"name": "Nama",
|
||||
"needRestart": "Perlu memulai ulang aplikasi",
|
||||
@@ -121,6 +127,7 @@
|
||||
"noSavedPrivateKey": "Tidak ada kunci pribadi yang disimpan.",
|
||||
"noSavedSnippet": "Tidak ada cuplikan yang disimpan.",
|
||||
"noServerAvailable": "Tidak ada server yang tersedia.",
|
||||
"noTask": "Tidak bertanya",
|
||||
"noUpdateAvailable": "Tidak ada pembaruan yang tersedia",
|
||||
"notSelected": "Tidak terpilih",
|
||||
"nullToken": "Token NULL",
|
||||
@@ -139,7 +146,7 @@
|
||||
"plzSelectKey": "Pilih kunci.",
|
||||
"port": "Port",
|
||||
"preview": "Pratinjau",
|
||||
"primaryColor": "Warna utama",
|
||||
"primaryColorSeed": "Warna utama",
|
||||
"privateKey": "Kunci Pribadi",
|
||||
"process": "Proses",
|
||||
"pushToken": "Dorong token",
|
||||
@@ -158,6 +165,8 @@
|
||||
"saved": "Diselamatkan",
|
||||
"second": "S",
|
||||
"server": "Server",
|
||||
"serverDetailOrder": "Detail pesanan widget halaman",
|
||||
"serverOrder": "Pesanan server",
|
||||
"serverTabConnecting": "Menghubungkan ...",
|
||||
"serverTabEmpty": "Tidak ada server.\nKlik fab untuk menambahkan satu.",
|
||||
"serverTabFailed": "Gagal",
|
||||
@@ -166,7 +175,6 @@
|
||||
"serverTabUnkown": "Negara yang tidak diketahui",
|
||||
"setting": "Pengaturan",
|
||||
"sftpDlPrepare": "Bersiap untuk terhubung ...",
|
||||
"sftpNoDownloadTask": "Tidak ada tugas unduhan.",
|
||||
"sftpSSHConnected": "Sftp terhubung",
|
||||
"showDistLogo": "Tampilkan logo distribusi",
|
||||
"snippet": "Snippet",
|
||||
@@ -175,11 +183,13 @@
|
||||
"sshTip": "Fungsi ini sekarang dalam tahap eksperimen.\n\nHarap laporkan bug di {url} atau bergabunglah dengan pengembangan kami.",
|
||||
"sshVirtualKeyAutoOff": "Switching Otomatis Kunci Virtual",
|
||||
"start": "Awal",
|
||||
"stats": "Statistik",
|
||||
"stop": "Berhenti",
|
||||
"success": "Kesuksesan",
|
||||
"sureDelete": "Apakah Anda pasti akan menghapus [{name}]?",
|
||||
"sureDirEmpty": "Pastikan dir kosong.",
|
||||
"sureNoPwd": "Apakah Anda pasti tidak menggunakan kata sandi?",
|
||||
"sureStop": "Anda yakin ingin menghentikan [{item}]?",
|
||||
"sureToDeleteServer": "Apakah Anda pasti akan menghapus server [{server}]?",
|
||||
"system": "Sistem",
|
||||
"tag": "Tag",
|
||||
@@ -203,7 +213,7 @@
|
||||
"urlOrJson": "URL atau JSON",
|
||||
"user": "Username",
|
||||
"versionHaveUpdate": "Ditemukan: v1.0.{build}, klik untuk memperbarui",
|
||||
"versionUnknownUpdate": "Saat ini: v1.0.{build}",
|
||||
"versionUnknownUpdate": "Saat ini: v1.0.{build}. Klik untuk memeriksa pembaruan.",
|
||||
"versionUpdated": "Saat ini: v1.0.{build}, mutakhir",
|
||||
"viewErr": "Lihat kesalahan",
|
||||
"virtKeyHelpClipboard": "Salin ke clipboard jika terminal yang dipilih tidak kosong, jika tidak, tempel isi clipboard ke terminal.",
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"add": "新增",
|
||||
"addAServer": "添加服务器",
|
||||
"addPrivateKey": "添加一个私钥",
|
||||
"addSystemPrivateKeyTip": "当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?",
|
||||
"added2List": "已添加至任务列表",
|
||||
"all": "所有",
|
||||
"alreadyLastDir": "已经是最上层目录了",
|
||||
"alterUrl": "备选链接",
|
||||
"attention": "注意",
|
||||
"auto": "自动",
|
||||
"autoCheckUpdate": "自动检查更新",
|
||||
"autoUpdateHomeWidget": "自动更新桌面小部件",
|
||||
"backup": "备份",
|
||||
"backupAndRestore": "备份和恢复",
|
||||
@@ -26,6 +28,7 @@
|
||||
"close": "关闭",
|
||||
"cmd": "命令",
|
||||
"conn": "连接",
|
||||
"connected": "已连接",
|
||||
"containerName": "容器名",
|
||||
"containerStatus": "容器状态",
|
||||
"convert": "转换",
|
||||
@@ -75,6 +78,7 @@
|
||||
"fullScreenJitterHelp": "防止烧屏",
|
||||
"getPushTokenFailed": "未能获取到推送token",
|
||||
"gettingToken": "正在获取Token...",
|
||||
"goBackQ": "返回?",
|
||||
"goto": "前往",
|
||||
"homeWidgetUrlConfig": "桌面部件链接配置",
|
||||
"host": "主机",
|
||||
@@ -109,6 +113,8 @@
|
||||
"maxRetryCountEqual0": "会无限重试",
|
||||
"min": "最小",
|
||||
"mission": "任务",
|
||||
"moveOutServerFuncBtns": "服务器功能按钮位置",
|
||||
"moveOutServerFuncBtnsHelp": "开启:可以在服务器 Tab 页的每个卡片下方显示。关闭:在服务器详情页顶部显示。",
|
||||
"ms": "毫秒",
|
||||
"name": "名称",
|
||||
"needRestart": "需要重启 App",
|
||||
@@ -121,6 +127,7 @@
|
||||
"noSavedPrivateKey": "没有已保存的私钥。",
|
||||
"noSavedSnippet": "没有已保存的代码片段。",
|
||||
"noServerAvailable": "没有可用的服务器。",
|
||||
"noTask": "没有任务",
|
||||
"noUpdateAvailable": "没有可用更新",
|
||||
"notSelected": "未选择",
|
||||
"nullToken": "无Token",
|
||||
@@ -139,7 +146,7 @@
|
||||
"plzSelectKey": "请选择私钥",
|
||||
"port": "端口",
|
||||
"preview": "预览",
|
||||
"primaryColor": "主题色",
|
||||
"primaryColorSeed": "主题色种子",
|
||||
"privateKey": "私钥",
|
||||
"process": "进程",
|
||||
"pushToken": "消息推送 Token",
|
||||
@@ -158,6 +165,8 @@
|
||||
"saved": "已保存",
|
||||
"second": "秒",
|
||||
"server": "服务器",
|
||||
"serverDetailOrder": "详情页部件顺序",
|
||||
"serverOrder": "服务器顺序",
|
||||
"serverTabConnecting": "连接中...",
|
||||
"serverTabEmpty": "现在没有服务器。\n点击右下方按钮来添加。",
|
||||
"serverTabFailed": "失败",
|
||||
@@ -166,8 +175,7 @@
|
||||
"serverTabUnkown": "未知状态",
|
||||
"setting": "设置",
|
||||
"sftpDlPrepare": "准备连接至服务器...",
|
||||
"sftpNoDownloadTask": "没有下载任务",
|
||||
"sftpSSHConnected": "SFTP 已连接,即将开始下载...",
|
||||
"sftpSSHConnected": "SFTP 已连接...",
|
||||
"showDistLogo": "显示发行版 Logo",
|
||||
"snippet": "代码片段",
|
||||
"speed": "速度",
|
||||
@@ -175,11 +183,13 @@
|
||||
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
|
||||
"sshVirtualKeyAutoOff": "虚拟按键自动切换",
|
||||
"start": "开始",
|
||||
"stats": "统计",
|
||||
"stop": "停止",
|
||||
"success": "成功",
|
||||
"sureDelete": "确定删除 [{name}]?",
|
||||
"sureDirEmpty": "请确保文件夹为空",
|
||||
"sureNoPwd": "确认使用无密码?",
|
||||
"sureStop": "确定要停止 [{item}] 吗?",
|
||||
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
|
||||
"system": "系统",
|
||||
"tag": "标签",
|
||||
@@ -203,7 +213,7 @@
|
||||
"urlOrJson": "链接或JSON",
|
||||
"user": "用户",
|
||||
"versionHaveUpdate": "找到新版本:v1.0.{build}, 点击更新",
|
||||
"versionUnknownUpdate": "当前:v1.0.{build}",
|
||||
"versionUnknownUpdate": "当前:v1.0.{build},点击检查更新",
|
||||
"versionUpdated": "当前:v1.0.{build}, 已是最新版本",
|
||||
"viewErr": "查看错误",
|
||||
"virtKeyHelpClipboard": "如果终端有选中字符,则复制选中字符至剪切板,否则粘贴剪切板内容至终端。",
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"add": "新增",
|
||||
"addAServer": "新增服務器",
|
||||
"addPrivateKey": "新增一個私鑰",
|
||||
"addSystemPrivateKeyTip": "當前沒有任何私鑰,是否添加系統自帶的(~/.ssh/id_rsa)?",
|
||||
"added2List": "已添加至任務列表",
|
||||
"all": "所有",
|
||||
"alreadyLastDir": "已經是最上層目錄了",
|
||||
"alterUrl": "備選鏈接",
|
||||
"attention": "注意",
|
||||
"auto": "自動",
|
||||
"autoCheckUpdate": "自動檢查更新",
|
||||
"autoUpdateHomeWidget": "自動更新桌面小部件",
|
||||
"backup": "備份",
|
||||
"backupAndRestore": "備份和還原",
|
||||
@@ -26,6 +28,7 @@
|
||||
"close": "關閉",
|
||||
"cmd": "命令",
|
||||
"conn": "連接",
|
||||
"connected": "已連接",
|
||||
"containerName": "容器名稱",
|
||||
"containerStatus": "容器狀態",
|
||||
"convert": "轉換",
|
||||
@@ -75,6 +78,7 @@
|
||||
"fullScreenJitterHelp": "防止燒屏",
|
||||
"getPushTokenFailed": "未能獲取到推送token",
|
||||
"gettingToken": "正在獲取Token...",
|
||||
"goBackQ": "返回?",
|
||||
"goto": "前往",
|
||||
"homeWidgetUrlConfig": "桌面部件鏈接配置",
|
||||
"host": "主機",
|
||||
@@ -109,6 +113,8 @@
|
||||
"maxRetryCountEqual0": "會無限重試",
|
||||
"min": "最小",
|
||||
"mission": "任務",
|
||||
"moveOutServerFuncBtns": "服務器功能按鈕位置",
|
||||
"moveOutServerFuncBtnsHelp": "開啟:可以在服務器 Tab 頁的每個卡片下方顯示。關閉:在服務器詳情頁頂部顯示。",
|
||||
"ms": "毫秒",
|
||||
"name": "名稱",
|
||||
"needRestart": "需要重啓 App",
|
||||
@@ -121,6 +127,7 @@
|
||||
"noSavedPrivateKey": "沒有已保存的私鑰。",
|
||||
"noSavedSnippet": "沒有已保存的程式片段。",
|
||||
"noServerAvailable": "沒有可用的服務器。",
|
||||
"noTask": "沒有任務",
|
||||
"noUpdateAvailable": "沒有可用更新",
|
||||
"notSelected": "未選擇",
|
||||
"nullToken": "無Token",
|
||||
@@ -139,7 +146,7 @@
|
||||
"plzSelectKey": "請選擇私鑰",
|
||||
"port": "端口",
|
||||
"preview": "預覽",
|
||||
"primaryColor": "主要色調",
|
||||
"primaryColorSeed": "主要色調種子",
|
||||
"privateKey": "私鑰",
|
||||
"process": "進程",
|
||||
"pushToken": "消息推送 Token",
|
||||
@@ -158,6 +165,8 @@
|
||||
"saved": "已保存",
|
||||
"second": "秒",
|
||||
"server": "服務器",
|
||||
"serverDetailOrder": "詳情頁部件順序",
|
||||
"serverOrder": "服務器順序",
|
||||
"serverTabConnecting": "連接中...",
|
||||
"serverTabEmpty": "現在沒有服務器。\n點擊右下方按鈕來新增。",
|
||||
"serverTabFailed": "失敗",
|
||||
@@ -166,8 +175,7 @@
|
||||
"serverTabUnkown": "未知狀態",
|
||||
"setting": "設置",
|
||||
"sftpDlPrepare": "準備連接至服務器...",
|
||||
"sftpNoDownloadTask": "沒有下載任務",
|
||||
"sftpSSHConnected": "SFTP 已連接,即將開始下載...",
|
||||
"sftpSSHConnected": "SFTP 已連接...",
|
||||
"showDistLogo": "顯示發行版 Logo",
|
||||
"snippet": "程式片段",
|
||||
"speed": "速度",
|
||||
@@ -175,11 +183,13 @@
|
||||
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 反饋問題,或者加入我們開發。",
|
||||
"sshVirtualKeyAutoOff": "虛擬按鍵自動切換",
|
||||
"start": "開始",
|
||||
"stats": "統計",
|
||||
"stop": "停止",
|
||||
"success": "成功",
|
||||
"sureDelete": "確定刪除 [{name}]?",
|
||||
"sureDirEmpty": "請確保文件夾為空",
|
||||
"sureNoPwd": "確認使用無密碼?",
|
||||
"sureStop": "確定要停止 [{item}] 嗎?",
|
||||
"sureToDeleteServer": "你確定要刪除服務器 [{server}] 嗎?",
|
||||
"system": "系統",
|
||||
"tag": "标签",
|
||||
@@ -203,7 +213,7 @@
|
||||
"urlOrJson": "鏈接或JSON",
|
||||
"user": "用戶",
|
||||
"versionHaveUpdate": "找到新版本:v1.0.{build}, 點擊更新",
|
||||
"versionUnknownUpdate": "當前:v1.0.{build}",
|
||||
"versionUnknownUpdate": "當前:v1.0.{build},點擊檢查更新",
|
||||
"versionUpdated": "當前:v1.0.{build}, 已是最新版本",
|
||||
"viewErr": "查看錯誤",
|
||||
"virtKeyHelpClipboard": "如果終端有選中字符,則復製選中字符至剪切板,否則粘貼剪切板內容至終端。",
|
||||
|
||||
@@ -18,11 +18,11 @@ import 'data/store/snippet.dart';
|
||||
|
||||
GetIt locator = GetIt.instance;
|
||||
|
||||
void setupLocatorForServices() {
|
||||
void _setupLocatorForServices() {
|
||||
locator.registerLazySingleton(() => AppService());
|
||||
}
|
||||
|
||||
void setupLocatorForProviders() {
|
||||
void _setupLocatorForProviders() {
|
||||
locator.registerSingleton(AppProvider());
|
||||
locator.registerSingleton(PkgProvider());
|
||||
locator.registerSingleton(DebugProvider());
|
||||
@@ -34,7 +34,7 @@ void setupLocatorForProviders() {
|
||||
locator.registerSingleton(SftpProvider());
|
||||
}
|
||||
|
||||
Future<void> setupLocatorForStores() async {
|
||||
Future<void> _setupLocatorForStores() async {
|
||||
final setting = SettingStore();
|
||||
await setting.init(boxName: 'setting');
|
||||
locator.registerSingleton(setting);
|
||||
@@ -57,7 +57,7 @@ Future<void> setupLocatorForStores() async {
|
||||
}
|
||||
|
||||
Future<void> setupLocator() async {
|
||||
await setupLocatorForStores();
|
||||
setupLocatorForProviders();
|
||||
setupLocatorForServices();
|
||||
await _setupLocatorForStores();
|
||||
_setupLocatorForProviders();
|
||||
_setupLocatorForServices();
|
||||
}
|
||||
|
||||
156
lib/main.dart
@@ -3,17 +3,21 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:macos_window_utils/window_manipulator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:toolbox/data/model/app/net_view.dart';
|
||||
import 'package:toolbox/data/model/ssh/virtual_key.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
import 'package:toolbox/view/widget/custom_appbar.dart';
|
||||
|
||||
import 'app.dart';
|
||||
import 'core/analysis.dart';
|
||||
import 'core/utils/platform.dart';
|
||||
import 'core/utils/ui.dart';
|
||||
import 'data/model/app/net_view.dart';
|
||||
import 'data/model/server/private_key_info.dart';
|
||||
import 'data/model/server/server_private_info.dart';
|
||||
import 'data/model/server/snippet.dart';
|
||||
import 'data/model/ssh/virtual_key.dart';
|
||||
import 'data/provider/app.dart';
|
||||
import 'data/provider/debug.dart';
|
||||
import 'data/provider/docker.dart';
|
||||
@@ -27,66 +31,10 @@ import 'data/store/setting.dart';
|
||||
import 'locator.dart';
|
||||
import 'view/widget/rebuild.dart';
|
||||
|
||||
late final DebugProvider _debug;
|
||||
|
||||
Future<void> initApp() async {
|
||||
await initHive();
|
||||
await setupLocator();
|
||||
|
||||
_debug = locator<DebugProvider>();
|
||||
locator<SnippetProvider>().loadData();
|
||||
locator<PrivateKeyProvider>().loadData();
|
||||
|
||||
final settings = locator<SettingStore>();
|
||||
await loadFontFile(settings.fontPath.fetch());
|
||||
|
||||
SharedPreferences.setPrefix('');
|
||||
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
// ignore: avoid_print
|
||||
print('[${record.loggerName}][${record.level.name}]: ${record.message}');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initHive() async {
|
||||
await Hive.initFlutter();
|
||||
// 以 typeId 为顺序
|
||||
Hive.registerAdapter(PrivateKeyInfoAdapter());
|
||||
Hive.registerAdapter(SnippetAdapter());
|
||||
Hive.registerAdapter(ServerPrivateInfoAdapter());
|
||||
Hive.registerAdapter(VirtKeyAdapter());
|
||||
Hive.registerAdapter(NetViewTypeAdapter());
|
||||
}
|
||||
|
||||
void runInZone(dynamic Function() body) {
|
||||
final zoneSpec = ZoneSpecification(
|
||||
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
|
||||
parent.print(zone, line);
|
||||
// This is a hack to avoid
|
||||
// `setState() or markNeedsBuild() called during build`
|
||||
// error.
|
||||
Future.delayed(const Duration(milliseconds: 1), () {
|
||||
_debug.addText(line);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
runZonedGuarded(
|
||||
body,
|
||||
onError,
|
||||
zoneSpecification: zoneSpec,
|
||||
);
|
||||
}
|
||||
|
||||
void onError(Object obj, StackTrace stack) {
|
||||
Analysis.recordException(obj);
|
||||
_debug.addMultiline(obj, Colors.red);
|
||||
_debug.addMultiline(stack, Colors.white);
|
||||
}
|
||||
DebugProvider? _debug;
|
||||
|
||||
Future<void> main() async {
|
||||
runInZone(() async {
|
||||
_runInZone(() async {
|
||||
await initApp();
|
||||
runApp(
|
||||
MultiProvider(
|
||||
@@ -108,3 +56,91 @@ Future<void> main() async {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _runInZone(void Function() body) {
|
||||
final zoneSpec = ZoneSpecification(
|
||||
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
|
||||
parent.print(zone, line);
|
||||
// This is a hack to avoid
|
||||
// `setState() or markNeedsBuild() called during build`
|
||||
// error.
|
||||
Future.delayed(const Duration(milliseconds: 1), () {
|
||||
_debug?.addText(line);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
runZonedGuarded(
|
||||
body,
|
||||
(obj, trace) => Analysis.recordException(trace),
|
||||
zoneSpecification: zoneSpec,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> initApp() async {
|
||||
await _initMacOSWindow();
|
||||
|
||||
// Base of all data.
|
||||
await _initHive();
|
||||
await setupLocator();
|
||||
|
||||
// Setup [DebugProvider] first to catch all logs.
|
||||
_debug = locator<DebugProvider>();
|
||||
_setupLogger();
|
||||
_setupProviders();
|
||||
|
||||
// Load font
|
||||
final settings = locator<SettingStore>();
|
||||
loadFontFile(settings.fontPath.fetch());
|
||||
|
||||
// Android only
|
||||
if (!isAndroid) return;
|
||||
// Only start service when [bgRun] is true.
|
||||
if (locator<SettingStore>().bgRun.fetch() ?? false) {
|
||||
bgRunChannel.invokeMethod('startService');
|
||||
}
|
||||
// SharedPreferences is only used on Android for saving home widgets settings.
|
||||
SharedPreferences.setPrefix('');
|
||||
}
|
||||
|
||||
void _setupProviders() {
|
||||
locator<SnippetProvider>().loadData();
|
||||
locator<PrivateKeyProvider>().loadData();
|
||||
}
|
||||
|
||||
Future<void> _initHive() async {
|
||||
await Hive.initFlutter();
|
||||
// 以 typeId 为顺序
|
||||
Hive.registerAdapter(PrivateKeyInfoAdapter());
|
||||
Hive.registerAdapter(SnippetAdapter());
|
||||
Hive.registerAdapter(ServerPrivateInfoAdapter());
|
||||
Hive.registerAdapter(VirtKeyAdapter());
|
||||
Hive.registerAdapter(NetViewTypeAdapter());
|
||||
}
|
||||
|
||||
void _setupLogger() {
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
var str = '[${record.loggerName}][${record.level.name}]: ${record.message}';
|
||||
if (record.error != null) {
|
||||
str += '\n${record.error}';
|
||||
_debug?.addMultiline(record.error.toString(), Colors.red);
|
||||
}
|
||||
if (record.stackTrace != null) {
|
||||
str += '\n${record.stackTrace}';
|
||||
_debug?.addMultiline(record.stackTrace.toString(), Colors.white);
|
||||
}
|
||||
// ignore: avoid_print
|
||||
print(str);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initMacOSWindow() async {
|
||||
if (!isMacOS) return;
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await WindowManipulator.initialize();
|
||||
WindowManipulator.makeTitlebarTransparent();
|
||||
WindowManipulator.enableFullSizeContentView();
|
||||
WindowManipulator.hideTitle();
|
||||
await CustomAppBar.updateTitlebarHeight();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../../data/store/private_key.dart';
|
||||
import '../../data/store/server.dart';
|
||||
import '../../data/store/snippet.dart';
|
||||
import '../../locator.dart';
|
||||
import '../widget/custom_appbar.dart';
|
||||
|
||||
const backupFormatVersion = 1;
|
||||
|
||||
@@ -34,7 +35,7 @@ class BackupPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context)!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(s.backupAndRestore, style: textSize18),
|
||||
),
|
||||
body: _buildBody(context, s),
|
||||
@@ -89,7 +90,7 @@ class BackupPage extends StatelessWidget {
|
||||
spis: _server.fetch(),
|
||||
snippets: _snippet.fetch(),
|
||||
keys: _privateKey.fetch(),
|
||||
dockerHosts: _dockerHosts.fetch(),
|
||||
dockerHosts: _dockerHosts.fetchAll(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -170,7 +171,7 @@ class BackupPage extends StatelessWidget {
|
||||
_privateKey.put(s);
|
||||
}
|
||||
for (final k in backup.dockerHosts.keys) {
|
||||
_dockerHosts.setDockerHost(k, backup.dockerHosts[k]!);
|
||||
_dockerHosts.put(k, backup.dockerHosts[k]!);
|
||||
}
|
||||
context.pop();
|
||||
showRoundDialog(
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/value_notifier.dart';
|
||||
|
||||
import '../../core/utils/ui.dart';
|
||||
import '../widget/custom_appbar.dart';
|
||||
import '../widget/input_field.dart';
|
||||
import '../widget/popup_menu.dart';
|
||||
import '../widget/round_rect_card.dart';
|
||||
@@ -41,11 +42,18 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
_s = S.of(context)!;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_textEditingController.dispose();
|
||||
_textEditingControllerResult.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(_s.convert),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/data/provider/debug.dart';
|
||||
|
||||
import '../widget/custom_appbar.dart';
|
||||
|
||||
class DebugPage extends StatefulWidget {
|
||||
const DebugPage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -13,8 +16,12 @@ class _DebugPageState extends State<DebugPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Logs'),
|
||||
appBar: CustomAppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.pop(),
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
),
|
||||
title: const Text('Logs', style: TextStyle(color: Colors.white)),
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
body: _buildTerminal(context),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:nil/nil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/model/docker/image.dart';
|
||||
import 'package:toolbox/view/page/ssh/term.dart';
|
||||
import 'package:toolbox/view/page/ssh_term.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
|
||||
import '../../core/utils/ui.dart';
|
||||
@@ -19,6 +18,7 @@ import '../../data/res/ui.dart';
|
||||
import '../../data/res/url.dart';
|
||||
import '../../data/store/docker.dart';
|
||||
import '../../locator.dart';
|
||||
import '../widget/custom_appbar.dart';
|
||||
import '../widget/popup_menu.dart';
|
||||
import '../widget/round_rect_card.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
@@ -26,7 +26,7 @@ import '../widget/url_text.dart';
|
||||
|
||||
class DockerManagePage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
const DockerManagePage(this.spi, {Key? key}) : super(key: key);
|
||||
const DockerManagePage({required this.spi, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DockerManagePage> createState() => _DockerManagePageState();
|
||||
@@ -34,6 +34,7 @@ class DockerManagePage extends StatefulWidget {
|
||||
|
||||
class _DockerManagePageState extends State<DockerManagePage> {
|
||||
final _docker = locator<DockerProvider>();
|
||||
final _store = locator<DockerStore>();
|
||||
final _textController = TextEditingController();
|
||||
late S _s;
|
||||
|
||||
@@ -41,6 +42,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_docker.clear();
|
||||
_textController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -54,8 +56,6 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
super.initState();
|
||||
final client = locator<ServerProvider>().servers[widget.spi.id]?.client;
|
||||
if (client == null) {
|
||||
showSnackBar(context, Text(_s.noClient));
|
||||
context.pop();
|
||||
return;
|
||||
}
|
||||
_docker.init(client, widget.spi.user, onPwdRequest, widget.spi.id);
|
||||
@@ -65,12 +65,16 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DockerProvider>(builder: (_, ___, __) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: 'Docker', down: widget.spi.name),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _docker.refresh,
|
||||
onPressed: () async {
|
||||
showLoadingDialog(context);
|
||||
await _docker.refresh();
|
||||
context.pop();
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
)
|
||||
],
|
||||
@@ -99,6 +103,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Input(
|
||||
autoFocus: true,
|
||||
type: TextInputType.text,
|
||||
label: _s.image,
|
||||
hint: 'xxx:1.1',
|
||||
@@ -153,7 +158,9 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
context.pop();
|
||||
showLoadingDialog(context);
|
||||
final result = await _docker.run(cmd);
|
||||
context.pop();
|
||||
if (result != null) {
|
||||
showSnackBar(context, Text(result.message ?? _s.unknownError));
|
||||
}
|
||||
@@ -217,10 +224,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: onSubmitted,
|
||||
child: Text(
|
||||
_s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
child: Text(_s.ok, style: textRed),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -239,7 +243,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
size: 37,
|
||||
),
|
||||
const SizedBox(height: 27),
|
||||
_buildErr(_docker.error!),
|
||||
Text(_docker.error?.message ?? _s.unknownError),
|
||||
const SizedBox(height: 27),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
@@ -250,7 +254,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
);
|
||||
}
|
||||
if (_docker.items == null || _docker.images == null) {
|
||||
Future.delayed(const Duration(milliseconds: 177), () {
|
||||
Future.delayed(const Duration(milliseconds: 37), () {
|
||||
if (mounted) {
|
||||
_docker.refresh();
|
||||
}
|
||||
@@ -258,31 +262,21 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
return centerLoading;
|
||||
}
|
||||
|
||||
final items = <Widget>[];
|
||||
items.addAll([
|
||||
final items = <Widget>[
|
||||
_buildLoading(),
|
||||
_buildVersion(
|
||||
_docker.edition ?? _s.unknown,
|
||||
_docker.version ?? _s.unknown,
|
||||
),
|
||||
..._buildPsItems(),
|
||||
_buildImages(),
|
||||
_buildVersion(),
|
||||
_buildPs(),
|
||||
_buildImage(),
|
||||
_buildEditHost(),
|
||||
].map((e) => RoundRectCard(e)));
|
||||
items.add(const SizedBox(height: 37));
|
||||
].map((e) => RoundRectCard(e));
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(7),
|
||||
children: items,
|
||||
children: items.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImages() {
|
||||
if (_docker.images == null) {
|
||||
return nil;
|
||||
}
|
||||
final items = _docker.images!.map(_buildImageItem).toList();
|
||||
items.insert(
|
||||
0,
|
||||
Widget _buildImage() {
|
||||
final items = <Widget>[
|
||||
ListTile(
|
||||
title: Text(_s.imagesList),
|
||||
subtitle: Text(
|
||||
@@ -290,7 +284,8 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
style: grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
];
|
||||
items.addAll(_docker.images!.map(_buildImageItem));
|
||||
return Column(children: items);
|
||||
}
|
||||
|
||||
@@ -330,17 +325,14 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
_s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
child: Text(_s.ok, style: textRed),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoading() {
|
||||
if (!_docker.isBusy) return nil;
|
||||
if (_docker.runLog == null) return placeholder;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
child: Column(
|
||||
@@ -355,6 +347,165 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSolution(DockerErr err) {
|
||||
switch (err.type) {
|
||||
case DockerErrType.notInstalled:
|
||||
return UrlText(
|
||||
text: _s.installDockerWithUrl,
|
||||
replace: _s.install,
|
||||
);
|
||||
case DockerErrType.noClient:
|
||||
return Text(_s.waitConnection);
|
||||
case DockerErrType.invalidVersion:
|
||||
return UrlText(
|
||||
text: _s.invalidVersionHelp(appHelpUrl),
|
||||
replace: 'Github',
|
||||
);
|
||||
case DockerErrType.parseImages:
|
||||
return const Text('Parse images error');
|
||||
case DockerErrType.parsePsItem:
|
||||
return const Text('Parse ps item error');
|
||||
case DockerErrType.parseStats:
|
||||
return const Text('Parse stats error');
|
||||
case DockerErrType.unknown:
|
||||
return const Text('Unknown error');
|
||||
case DockerErrType.cmdNoPrefix:
|
||||
return const Text('Cmd no prefix');
|
||||
case DockerErrType.segmentsNotMatch:
|
||||
return const Text('Segments not match');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildVersion() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(_docker.edition ?? _s.unknown),
|
||||
Text(_docker.version ?? _s.unknown),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPs() {
|
||||
final items = <Widget>[
|
||||
ListTile(
|
||||
title: Text(_s.containerStatus),
|
||||
subtitle: Text(_buildPsCardSubtitle(_docker.items!), style: grey),
|
||||
),
|
||||
];
|
||||
items.addAll(_docker.items!.map(_buildPsItem));
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: items,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPsItem(DockerPsItem item) {
|
||||
return ListTile(
|
||||
title: Text(item.image),
|
||||
subtitle: Text(
|
||||
'${item.name} - ${item.status}',
|
||||
style: textSize13Grey,
|
||||
),
|
||||
trailing: _buildMoreBtn(item),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMoreBtn(DockerPsItem dItem) {
|
||||
return PopupMenu(
|
||||
items: DockerMenuType.items(dItem.running)
|
||||
.map(
|
||||
(e) => e.build(_s),
|
||||
)
|
||||
.toList(),
|
||||
onSelected: (DockerMenuType item) async {
|
||||
switch (item) {
|
||||
case DockerMenuType.rm:
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text(_s.sureDelete(dItem.name)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
context.pop();
|
||||
showLoadingDialog(context);
|
||||
await _docker.delete(dItem.containerId);
|
||||
context.pop();
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
)
|
||||
],
|
||||
);
|
||||
break;
|
||||
case DockerMenuType.start:
|
||||
showLoadingDialog(context);
|
||||
await _docker.start(dItem.containerId);
|
||||
context.pop();
|
||||
break;
|
||||
case DockerMenuType.stop:
|
||||
showLoadingDialog(context);
|
||||
await _docker.stop(dItem.containerId);
|
||||
context.pop();
|
||||
break;
|
||||
case DockerMenuType.restart:
|
||||
showLoadingDialog(context);
|
||||
await _docker.restart(dItem.containerId);
|
||||
context.pop();
|
||||
break;
|
||||
case DockerMenuType.logs:
|
||||
AppRoute(
|
||||
SSHPage(
|
||||
spi: widget.spi,
|
||||
initCmd: 'docker logs -f --tail 100 ${dItem.containerId}',
|
||||
),
|
||||
'Docker logs',
|
||||
).go(context);
|
||||
break;
|
||||
case DockerMenuType.terminal:
|
||||
AppRoute(
|
||||
SSHPage(
|
||||
spi: widget.spi,
|
||||
initCmd: 'docker exec -it ${dItem.containerId} sh',
|
||||
),
|
||||
'Docker terminal',
|
||||
).go(context);
|
||||
break;
|
||||
case DockerMenuType.stats:
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.stats),
|
||||
child: Text(
|
||||
'CPU: ${dItem.cpu}\n'
|
||||
'Mem: ${dItem.mem}\n'
|
||||
'Net: ${dItem.net}\n'
|
||||
'Block: ${dItem.disk}',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _buildPsCardSubtitle(List<DockerPsItem> running) {
|
||||
final runningCount = running.where((element) => element.running).length;
|
||||
final stoped = running.length - runningCount;
|
||||
if (stoped == 0) {
|
||||
return _s.dockerStatusRunningFmt(runningCount);
|
||||
}
|
||||
return _s.dockerStatusRunningAndStoppedFmt(runningCount, stoped);
|
||||
}
|
||||
|
||||
Widget _buildEditHost() {
|
||||
final children = <Widget>[];
|
||||
if (_docker.items!.isEmpty && _docker.images!.isEmpty) {
|
||||
@@ -378,183 +529,29 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
}
|
||||
|
||||
Future<void> _showEditHostDialog() async {
|
||||
final id = widget.spi.id;
|
||||
final host = _store.fetch(id) ?? 'unix:///run/user/1000/docker.sock';
|
||||
final ctrl = TextEditingController(text: host);
|
||||
await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.dockerEditHost),
|
||||
child: Input(
|
||||
maxLines: 1,
|
||||
controller:
|
||||
TextEditingController(text: 'unix:///run/user/1000/docker.sock'),
|
||||
onSubmitted: (value) {
|
||||
locator<DockerStore>().setDockerHost(widget.spi.id, value.trim());
|
||||
_docker.refresh();
|
||||
context.pop();
|
||||
},
|
||||
controller: ctrl,
|
||||
onSubmitted: _onSaveDockerHost,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.cancel),
|
||||
onPressed: () => _onSaveDockerHost(ctrl.text),
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErr(DockerErr err) {
|
||||
var errStr = '';
|
||||
switch (err.type) {
|
||||
case DockerErrType.noClient:
|
||||
errStr = _s.noClient;
|
||||
break;
|
||||
case DockerErrType.notInstalled:
|
||||
errStr = _s.dockerNotInstalled;
|
||||
break;
|
||||
case DockerErrType.invalidVersion:
|
||||
errStr = _s.invalidVersion;
|
||||
break;
|
||||
default:
|
||||
errStr = err.message ?? _s.unknown;
|
||||
}
|
||||
return Text(errStr);
|
||||
}
|
||||
|
||||
Widget _buildSolution(DockerErr err) {
|
||||
switch (err.type) {
|
||||
case DockerErrType.notInstalled:
|
||||
return UrlText(
|
||||
text: _s.installDockerWithUrl,
|
||||
replace: _s.install,
|
||||
);
|
||||
case DockerErrType.noClient:
|
||||
return Text(_s.waitConnection);
|
||||
case DockerErrType.invalidVersion:
|
||||
return UrlText(
|
||||
text: _s.invalidVersionHelp(appHelpUrl),
|
||||
replace: 'Github',
|
||||
);
|
||||
default:
|
||||
return Text(_s.unknownError);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildVersion(String edition, String version) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [Text(edition), Text(version)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildPsItems() {
|
||||
final items = _docker.items!.map(_buildPsItem).toList();
|
||||
items.insert(
|
||||
0,
|
||||
ListTile(
|
||||
title: Text(_s.containerStatus),
|
||||
subtitle: Text(_buildSubtitle(_docker.items!), style: grey),
|
||||
),
|
||||
);
|
||||
return items;
|
||||
}
|
||||
|
||||
Widget _buildPsItem(DockerPsItem item) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(item.name),
|
||||
subtitle: Text(
|
||||
'${item.image}\n${item.status}',
|
||||
style: textSize13Grey,
|
||||
),
|
||||
trailing: _buildMoreBtn(item, _docker.isBusy),
|
||||
),
|
||||
_buildPsItemStat(item),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPsItemStat(DockerPsItem item) {
|
||||
if (!item.running) return const SizedBox();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 17, bottom: 11, right: 17),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(item.cpu ?? _s.unknown, style: grey),
|
||||
Text(item.mem ?? _s.unknown, style: grey),
|
||||
Text(item.net ?? _s.unknown, style: grey),
|
||||
Text(item.disk ?? _s.unknown, style: grey),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMoreBtn(DockerPsItem dItem, bool busy) {
|
||||
return PopupMenu(
|
||||
items:
|
||||
DockerMenuType.items(dItem.running).map((e) => e.build(_s)).toList(),
|
||||
onSelected: (DockerMenuType item) async {
|
||||
if (busy) {
|
||||
showSnackBar(context, Text(_s.isBusy));
|
||||
return;
|
||||
}
|
||||
switch (item) {
|
||||
case DockerMenuType.rm:
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text(_s.sureDelete(dItem.name)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
_docker.delete(dItem.containerId);
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
)
|
||||
],
|
||||
);
|
||||
break;
|
||||
case DockerMenuType.start:
|
||||
_docker.start(dItem.containerId);
|
||||
break;
|
||||
case DockerMenuType.stop:
|
||||
_docker.stop(dItem.containerId);
|
||||
break;
|
||||
case DockerMenuType.restart:
|
||||
_docker.restart(dItem.containerId);
|
||||
break;
|
||||
case DockerMenuType.logs:
|
||||
AppRoute(
|
||||
SSHPage(
|
||||
spi: widget.spi,
|
||||
initCmd: 'docker logs ${dItem.containerId}',
|
||||
),
|
||||
'Docker logs',
|
||||
).go(context);
|
||||
break;
|
||||
case DockerMenuType.terminal:
|
||||
AppRoute(
|
||||
SSHPage(
|
||||
spi: widget.spi,
|
||||
initCmd: 'docker exec -it ${dItem.containerId} /bin/sh',
|
||||
),
|
||||
'Docker terminal',
|
||||
).go(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _buildSubtitle(List<DockerPsItem> running) {
|
||||
final runningCount = running.where((element) => element.running).length;
|
||||
final stoped = running.length - runningCount;
|
||||
if (stoped == 0) {
|
||||
return _s.dockerStatusRunningFmt(runningCount);
|
||||
}
|
||||
return _s.dockerStatusRunningAndStoppedFmt(runningCount, stoped);
|
||||
void _onSaveDockerHost(String val) {
|
||||
context.pop();
|
||||
_store.put(widget.spi.id, val.trim());
|
||||
_docker.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:toolbox/data/res/highlight.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
import '../widget/custom_appbar.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
|
||||
class EditorPage extends StatefulWidget {
|
||||
@@ -32,6 +33,7 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
|
||||
Map<String, TextStyle>? _codeTheme;
|
||||
late S _s;
|
||||
late String? _langCode;
|
||||
late TextStyle _textStyle;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -40,6 +42,7 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
|
||||
_controller = CodeController(
|
||||
language: suffix2HighlightMap[_langCode],
|
||||
);
|
||||
_textStyle = TextStyle(fontSize: _setting.editorFontSize.fetch());
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((Duration duration) async {
|
||||
if (isDarkMode(context)) {
|
||||
@@ -68,55 +71,9 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: () {
|
||||
if (_codeTheme != null) {
|
||||
return _codeTheme!['root']!.backgroundColor;
|
||||
}
|
||||
return null;
|
||||
}(),
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: getFileName(widget.path) ?? '', down: _s.editor),
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.language),
|
||||
onSelected: (value) {
|
||||
_controller.language = suffix2HighlightMap[value];
|
||||
_langCode = value;
|
||||
},
|
||||
initialValue: _langCode,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return suffix2HighlightMap.keys.map((e) {
|
||||
return PopupMenuItem(
|
||||
value: e,
|
||||
child: Text(e),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Visibility(
|
||||
visible: (_codeTheme != null),
|
||||
replacement: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: CodeTheme(
|
||||
data: CodeThemeData(
|
||||
styles: _codeTheme ??
|
||||
(isDarkMode(context) ? monokaiTheme : a11yLightTheme)),
|
||||
child: CodeField(
|
||||
focusNode: _focusNode,
|
||||
controller: _controller,
|
||||
lineNumberStyle: const LineNumberStyle(
|
||||
width: 47,
|
||||
margin: 7,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: _codeTheme?['root']?.backgroundColor,
|
||||
appBar: _buildAppBar(),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.done),
|
||||
onPressed: () {
|
||||
@@ -126,6 +83,55 @@ class _EditorPageState extends State<EditorPage> with AfterLayoutMixin {
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return CustomAppBar(
|
||||
title: TwoLineText(up: getFileName(widget.path) ?? '', down: _s.editor),
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.language),
|
||||
onSelected: (value) {
|
||||
_controller.language = suffix2HighlightMap[value];
|
||||
_langCode = value;
|
||||
},
|
||||
initialValue: _langCode,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return suffix2HighlightMap.keys.map((e) {
|
||||
return PopupMenuItem(
|
||||
value: e,
|
||||
child: Text(e),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Visibility(
|
||||
visible: _codeTheme != null,
|
||||
replacement: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: CodeTheme(
|
||||
data: CodeThemeData(
|
||||
styles: _codeTheme ??
|
||||
(isDarkMode(context) ? monokaiTheme : a11yLightTheme)),
|
||||
child: CodeField(
|
||||
focusNode: _focusNode,
|
||||
controller: _controller,
|
||||
textStyle: _textStyle,
|
||||
lineNumberStyle: const LineNumberStyle(
|
||||
width: 47,
|
||||
margin: 7,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> afterFirstLayout(BuildContext context) async {
|
||||
if (widget.path != null) {
|
||||
|
||||
@@ -6,9 +6,9 @@ import 'package:circle_chart/circle_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:nil/nil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/model/server/disk.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
@@ -22,9 +22,8 @@ import '../../data/model/server/server.dart';
|
||||
import '../../data/model/server/server_private_info.dart';
|
||||
import '../../data/model/server/server_status.dart';
|
||||
import '../../data/res/color.dart';
|
||||
import 'server/detail.dart';
|
||||
import 'server/edit.dart';
|
||||
import 'setting.dart';
|
||||
import 'setting/entry.dart';
|
||||
|
||||
class FullScreenPage extends StatefulWidget {
|
||||
const FullScreenPage({Key? key}) : super(key: key);
|
||||
@@ -62,6 +61,7 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_timer.cancel();
|
||||
_pageController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -146,7 +146,7 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
|
||||
final id = pro.serverOrder[idx];
|
||||
final s = pro.servers[id];
|
||||
if (s == null) {
|
||||
return nil;
|
||||
return Center(child: Text(_s.noClient));
|
||||
}
|
||||
return _buildRealServerCard(s.status, s.state, s.spi);
|
||||
},
|
||||
@@ -159,13 +159,10 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
|
||||
ServerState cs,
|
||||
ServerPrivateInfo spi,
|
||||
) {
|
||||
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/');
|
||||
final rootDisk = findRootDisk(ss.disk);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => AppRoute(
|
||||
ServerDetailPage(spi.id),
|
||||
'server detail page',
|
||||
).go(context),
|
||||
onTap: () => AppRoute.serverDetail(spi: spi).go(context),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
@@ -185,8 +182,8 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
|
||||
_buildPercentCircle(ss.mem.usedPercent * 100),
|
||||
_buildNet(ss),
|
||||
_buildIOData(
|
||||
'Total:\n${rootDisk.size}',
|
||||
'Used:\n${rootDisk.usedPercent}%',
|
||||
'Total:\n${rootDisk?.size}',
|
||||
'Used:\n${rootDisk?.usedPercent}%',
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -253,7 +250,7 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
|
||||
);
|
||||
return Text(
|
||||
topRightStr,
|
||||
style: textSize12Grey,
|
||||
style: textSize11Grey,
|
||||
textScaleFactor: 1.0,
|
||||
);
|
||||
}
|
||||
@@ -373,7 +370,9 @@ class _FullScreenPageState extends State<FullScreenPage> with AfterLayoutMixin {
|
||||
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
doUpdate(context);
|
||||
if (_setting.autoCheckAppUpdate.fetch()!) {
|
||||
doUpdate(context);
|
||||
}
|
||||
await GetIt.I.allReady();
|
||||
await _serverProvider.loadLocalData();
|
||||
await _serverProvider.refreshData();
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:after_layout/after_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:toolbox/data/model/app/github_id.dart';
|
||||
import 'package:toolbox/data/model/app/tab.dart';
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
@@ -19,12 +20,13 @@ import '../../data/res/ui.dart';
|
||||
import '../../data/res/url.dart';
|
||||
import '../../data/store/setting.dart';
|
||||
import '../../locator.dart';
|
||||
import '../widget/custom_appbar.dart';
|
||||
import '../widget/url_text.dart';
|
||||
import 'backup.dart';
|
||||
import 'convert.dart';
|
||||
import 'debug.dart';
|
||||
import 'private_key/list.dart';
|
||||
import 'setting.dart';
|
||||
import 'setting/entry.dart';
|
||||
import 'storage/local.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
@@ -74,6 +76,7 @@ class _HomePageState extends State<HomePage>
|
||||
super.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_serverProvider.closeServer();
|
||||
_pageController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -107,23 +110,13 @@ class _HomePageState extends State<HomePage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
return Scaffold(
|
||||
drawer: _buildDrawer(),
|
||||
appBar: AppBar(
|
||||
title: const Text(BuildData.name),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.developer_mode, size: 23),
|
||||
tooltip: _s.debug,
|
||||
onPressed: () => AppRoute(
|
||||
const DebugPage(),
|
||||
'Debug Page',
|
||||
).go(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: _buildAppBar(),
|
||||
body: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: AppTab.values.length,
|
||||
itemBuilder: (_, index) => AppTab.values[index].page,
|
||||
onPageChanged: (value) {
|
||||
if (!_switchingPage) {
|
||||
@@ -138,9 +131,44 @@ class _HomePageState extends State<HomePage>
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
final actions = <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.developer_mode, size: 23),
|
||||
tooltip: _s.debug,
|
||||
onPressed: () => AppRoute(
|
||||
const DebugPage(),
|
||||
'Debug Page',
|
||||
).go(context),
|
||||
),
|
||||
];
|
||||
if (isDesktop && _selectIndex.value == AppTab.server.index) {
|
||||
actions.add(
|
||||
ValueBuilder(
|
||||
listenable: _selectIndex,
|
||||
build: () {
|
||||
if (_selectIndex.value != AppTab.server.index) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 23),
|
||||
tooltip: 'Refresh',
|
||||
onPressed: () => _serverProvider.refreshData(onlyFailed: true),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return CustomAppBar(
|
||||
title: const Text(BuildData.name),
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return NavigationBar(
|
||||
selectedIndex: _selectIndex.value,
|
||||
height: kBottomNavigationBarHeight * 1.2,
|
||||
animationDuration: const Duration(milliseconds: 250),
|
||||
onDestinationSelected: (int index) {
|
||||
if (_selectIndex.value == index) return;
|
||||
@@ -196,92 +224,114 @@ class _HomePageState extends State<HomePage>
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 37),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(_s.setting),
|
||||
onTap: () => AppRoute(
|
||||
const SettingPage(),
|
||||
'Setting',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: Text(_s.privateKey),
|
||||
onTap: () => AppRoute(
|
||||
const PrivateKeysListPage(),
|
||||
'private key list',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
title: Text(_s.download),
|
||||
onTap: () => AppRoute(
|
||||
const LocalStoragePage(),
|
||||
'sftp local page',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.import_export),
|
||||
title: Text(_s.backup),
|
||||
onTap: () => AppRoute(
|
||||
BackupPage(),
|
||||
'backup page',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.code),
|
||||
title: Text(_s.convert),
|
||||
onTap: () => AppRoute(
|
||||
const ConvertPage(),
|
||||
'convert page',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.text_snippet),
|
||||
title: Text('${_s.about} & ${_s.feedback}'),
|
||||
onTap: () {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.about),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UrlText(
|
||||
text: _s.madeWithLove(myGithub),
|
||||
replace: 'lollipopkit'),
|
||||
UrlText(
|
||||
text: _s.aboutThanks,
|
||||
),
|
||||
// Thanks
|
||||
...thanksMap.keys.map(
|
||||
(key) => UrlText(
|
||||
text: thanksMap[key] ?? '',
|
||||
replace: key,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => openUrl(appHelpUrl),
|
||||
child: Text(_s.feedback),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => showLicensePage(context: context),
|
||||
child: Text(_s.license),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
].map((e) => RoundRectCard(e)).toList(),
|
||||
_buildTiles(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTiles() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(_s.setting),
|
||||
onTap: () => AppRoute(
|
||||
const SettingPage(),
|
||||
'Setting',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: Text(_s.privateKey),
|
||||
onTap: () => AppRoute(
|
||||
const PrivateKeysListPage(),
|
||||
'private key list',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
title: Text(_s.download),
|
||||
onTap: () => AppRoute(
|
||||
const LocalStoragePage(),
|
||||
'sftp local page',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.import_export),
|
||||
title: Text(_s.backup),
|
||||
onTap: () => AppRoute(
|
||||
BackupPage(),
|
||||
'backup page',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.code),
|
||||
title: Text(_s.convert),
|
||||
onTap: () => AppRoute(
|
||||
const ConvertPage(),
|
||||
'convert page',
|
||||
).go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.text_snippet),
|
||||
title: Text('${_s.about} & ${_s.feedback}'),
|
||||
onTap: _showAboutDialog,
|
||||
)
|
||||
].map((e) => RoundRectCard(e)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAboutDialog() {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.about),
|
||||
child: _buildAboutContent(),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => openUrl(appHelpUrl),
|
||||
child: Text(_s.feedback),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => showLicensePage(context: context),
|
||||
child: Text(_s.license),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAboutContent() {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UrlText(
|
||||
text: _s.madeWithLove(myGithub),
|
||||
replace: 'lollipopkit',
|
||||
),
|
||||
height13,
|
||||
// Use [UrlText] for same text style
|
||||
Text(_s.aboutThanks),
|
||||
height13,
|
||||
const Text('Contributors:'),
|
||||
...contributors.map(
|
||||
(name) => UrlText(
|
||||
text: name.url,
|
||||
replace: name,
|
||||
),
|
||||
),
|
||||
height13,
|
||||
const Text('Participants:'),
|
||||
...participants.map(
|
||||
(name) => UrlText(
|
||||
text: name.url,
|
||||
replace: name,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -307,7 +357,9 @@ class _HomePageState extends State<HomePage>
|
||||
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
doUpdate(context);
|
||||
if (_setting.autoCheckAppUpdate.fetch()!) {
|
||||
doUpdate(context);
|
||||
}
|
||||
updateHomeWidget();
|
||||
await GetIt.I.allReady();
|
||||
await _serverProvider.loadLocalData();
|
||||
|
||||
@@ -48,6 +48,13 @@ class _PingPageState extends State<PingPage>
|
||||
_s = S.of(context)!;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_textEditingController.dispose();
|
||||
_results.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@@ -68,6 +75,7 @@ class _PingPageState extends State<PingPage>
|
||||
context: context,
|
||||
title: Text(_s.choose),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
controller: _textEditingController,
|
||||
hint: _s.inputDomainHere,
|
||||
maxLines: 1,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:nil/nil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
@@ -13,11 +12,12 @@ import '../../data/provider/pkg.dart';
|
||||
import '../../data/provider/server.dart';
|
||||
import '../../data/res/ui.dart';
|
||||
import '../../locator.dart';
|
||||
import '../widget/custom_appbar.dart';
|
||||
import '../widget/round_rect_card.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
|
||||
class PkgManagePage extends StatefulWidget {
|
||||
const PkgManagePage(this.spi, {Key? key}) : super(key: key);
|
||||
class PkgPage extends StatefulWidget {
|
||||
const PkgPage({Key? key, required this.spi}) : super(key: key);
|
||||
|
||||
final ServerPrivateInfo spi;
|
||||
|
||||
@@ -25,7 +25,7 @@ class PkgManagePage extends StatefulWidget {
|
||||
_PkgManagePageState createState() => _PkgManagePageState();
|
||||
}
|
||||
|
||||
class _PkgManagePageState extends State<PkgManagePage>
|
||||
class _PkgManagePageState extends State<PkgPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late MediaQueryData _media;
|
||||
final _scrollController = ScrollController();
|
||||
@@ -45,18 +45,17 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_pkgProvider.clear();
|
||||
_textController.dispose();
|
||||
_scrollController.dispose();
|
||||
_scrollControllerUpdate.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final si = locator<ServerProvider>().servers[widget.spi.id];
|
||||
if (si == null || si.client == null) {
|
||||
showSnackBar(context, Text(_s.waitConnection));
|
||||
context.pop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (si == null) return;
|
||||
_pkgProvider.init(
|
||||
si.client!,
|
||||
si.status.sysVer.dist,
|
||||
@@ -74,7 +73,7 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<PkgProvider>(builder: (_, pkg, __) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: _s.pkg, down: widget.spi.name),
|
||||
),
|
||||
@@ -108,6 +107,7 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
context: context,
|
||||
title: Text(widget.spi.user),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
controller: _textController,
|
||||
type: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
@@ -123,19 +123,16 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
child: Text(_s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () => onSubmitted(),
|
||||
child: Text(
|
||||
_s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
child: Text(_s.ok, style: textRed),
|
||||
),
|
||||
],
|
||||
);
|
||||
return _textController.text.trim();
|
||||
}
|
||||
|
||||
Widget _buildFAB(PkgProvider pkg) {
|
||||
if (pkg.isBusy || (pkg.upgradeable?.isEmpty ?? true)) {
|
||||
return nil;
|
||||
Widget? _buildFAB(PkgProvider pkg) {
|
||||
if (pkg.upgradeable?.isEmpty ?? true) {
|
||||
return null;
|
||||
}
|
||||
return FloatingActionButton(
|
||||
onPressed: () {
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:nil/nil.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
@@ -18,13 +17,14 @@ import '../../../data/model/server/private_key_info.dart';
|
||||
import '../../../data/provider/private_key.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/custom_appbar.dart';
|
||||
|
||||
const _format = 'text/plain';
|
||||
|
||||
class PrivateKeyEditPage extends StatefulWidget {
|
||||
const PrivateKeyEditPage({Key? key, this.info}) : super(key: key);
|
||||
const PrivateKeyEditPage({Key? key, this.pki}) : super(key: key);
|
||||
|
||||
final PrivateKeyInfo? info;
|
||||
final PrivateKeyInfo? pki;
|
||||
|
||||
@override
|
||||
_PrivateKeyEditPageState createState() => _PrivateKeyEditPageState();
|
||||
@@ -43,7 +43,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
late PrivateKeyProvider _provider;
|
||||
late S _s;
|
||||
|
||||
Widget _loading = nil;
|
||||
Widget? _loading;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -51,6 +51,17 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
_provider = locator<PrivateKeyProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_nameController.dispose();
|
||||
_keyController.dispose();
|
||||
_pwdController.dispose();
|
||||
_nameNode.dispose();
|
||||
_keyNode.dispose();
|
||||
_pwdNode.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -68,20 +79,35 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
final actions = widget.info == null
|
||||
? null
|
||||
: [
|
||||
IconButton(
|
||||
tooltip: _s.delete,
|
||||
final actions = [
|
||||
IconButton(
|
||||
tooltip: _s.delete,
|
||||
onPressed: () {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text(_s.sureDelete(widget.pki!.id)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_provider.delInfo(widget.info!);
|
||||
_provider.delete(widget.pki!);
|
||||
context.pop();
|
||||
context.pop();
|
||||
},
|
||||
icon: const Icon(Icons.delete))
|
||||
];
|
||||
return AppBar(
|
||||
child: Text(
|
||||
_s.ok,
|
||||
style: textRed,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
)
|
||||
];
|
||||
return CustomAppBar(
|
||||
title: Text(_s.edit, style: textSize18),
|
||||
actions: actions,
|
||||
actions: widget.pki == null ? null : actions,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,22 +126,22 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
setState(() {
|
||||
_loading = centerSizedLoading;
|
||||
});
|
||||
final info = PrivateKeyInfo(name, key, '');
|
||||
try {
|
||||
info.privateKey = await compute(decyptPem, [key, pwd]);
|
||||
final decrypted = await compute(decyptPem, [key, pwd]);
|
||||
final pki = PrivateKeyInfo(id: name, key: decrypted);
|
||||
if (widget.pki != null) {
|
||||
_provider.update(widget.pki!, pki);
|
||||
} else {
|
||||
_provider.add(pki);
|
||||
}
|
||||
} catch (e) {
|
||||
showSnackBar(context, Text(e.toString()));
|
||||
rethrow;
|
||||
} finally {
|
||||
setState(() {
|
||||
_loading = nil;
|
||||
_loading = null;
|
||||
});
|
||||
}
|
||||
if (widget.info != null) {
|
||||
_provider.updateInfo(widget.info!, info);
|
||||
} else {
|
||||
_provider.addInfo(info);
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
child: const Icon(Icons.save),
|
||||
@@ -127,6 +153,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
Input(
|
||||
autoFocus: true,
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
node: _nameNode,
|
||||
@@ -185,17 +212,16 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
icon: Icons.password,
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
|
||||
_loading
|
||||
_loading ?? placeholder,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
if (widget.info != null) {
|
||||
_nameController.text = widget.info!.id;
|
||||
_keyController.text = widget.info!.privateKey;
|
||||
_pwdController.text = widget.info!.password;
|
||||
if (widget.pki != null) {
|
||||
_nameController.text = widget.pki!.id;
|
||||
_keyController.text = widget.pki!.key;
|
||||
} else {
|
||||
final clipdata = ((await Clipboard.getData(_format))?.text ?? '').trim();
|
||||
if (clipdata.startsWith('-----BEGIN') && clipdata.endsWith('-----')) {
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/utils/ui.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../core/utils/platform.dart';
|
||||
import '../../../data/model/server/private_key_info.dart';
|
||||
import '../../../data/provider/private_key.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/custom_appbar.dart';
|
||||
import 'edit.dart';
|
||||
import '../../../view/widget/round_rect_card.dart';
|
||||
|
||||
@@ -15,7 +25,8 @@ class PrivateKeysListPage extends StatefulWidget {
|
||||
_PrivateKeyListState createState() => _PrivateKeyListState();
|
||||
}
|
||||
|
||||
class _PrivateKeyListState extends State<PrivateKeysListPage> {
|
||||
class _PrivateKeyListState extends State<PrivateKeysListPage>
|
||||
with AfterLayoutMixin {
|
||||
late S _s;
|
||||
|
||||
@override
|
||||
@@ -27,7 +38,7 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(_s.privateKey, style: textSize18),
|
||||
),
|
||||
body: _buildBody(),
|
||||
@@ -44,21 +55,21 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> {
|
||||
Widget _buildBody() {
|
||||
return Consumer<PrivateKeyProvider>(
|
||||
builder: (_, key, __) {
|
||||
if (key.infos.isEmpty) {
|
||||
if (key.pkis.isEmpty) {
|
||||
return Center(
|
||||
child: Text(_s.noSavedPrivateKey),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(13),
|
||||
itemCount: key.infos.length,
|
||||
itemCount: key.pkis.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return RoundRectCard(
|
||||
ListTile(
|
||||
title: Text(key.infos[idx].id),
|
||||
title: Text(key.pkis[idx].id),
|
||||
trailing: TextButton(
|
||||
onPressed: () => AppRoute(
|
||||
PrivateKeyEditPage(info: key.infos[idx]),
|
||||
PrivateKeyEditPage(pki: key.pkis[idx]),
|
||||
'private key edit page',
|
||||
).go(context),
|
||||
child: Text(_s.edit),
|
||||
@@ -70,4 +81,45 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void autoAddSystemPriavteKey() {
|
||||
final store = locator<PrivateKeyStore>();
|
||||
// Only trigger on desktop platform and no private key saved
|
||||
if (isDesktop && store.box.keys.isEmpty) {
|
||||
final home = getHomeDir();
|
||||
if (home == null) return;
|
||||
final idRsaFile = File(pathJoin(home, '.ssh/id_rsa'));
|
||||
if (!idRsaFile.existsSync()) return;
|
||||
final sysPk = PrivateKeyInfo(
|
||||
id: 'system',
|
||||
key: idRsaFile.readAsStringSync(),
|
||||
);
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text(_s.addSystemPrivateKeyTip),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
AppRoute(
|
||||
PrivateKeyEditPage(pki: sysPk),
|
||||
'private key edit page',
|
||||
).go(context);
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(_s.cancel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> afterFirstLayout(BuildContext context) {
|
||||
autoAddSystemPriavteKey();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
import 'package:toolbox/core/utils/ui.dart';
|
||||
@@ -13,6 +15,7 @@ import 'package:toolbox/view/widget/two_line_text.dart';
|
||||
|
||||
import '../../data/provider/server.dart';
|
||||
import '../../locator.dart';
|
||||
import '../widget/custom_appbar.dart';
|
||||
|
||||
class ProcessPage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
@@ -25,6 +28,9 @@ class ProcessPage extends StatefulWidget {
|
||||
class _ProcessPageState extends State<ProcessPage> {
|
||||
late S _s;
|
||||
late Timer _timer;
|
||||
late MediaQueryData _media;
|
||||
|
||||
SSHClient? _client;
|
||||
|
||||
PsResult _result = PsResult(procs: []);
|
||||
int? _lastFocusId;
|
||||
@@ -35,36 +41,35 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final client = _serverProvider.servers[widget.spi.id]?.client;
|
||||
if (client == null) {
|
||||
showSnackBar(context, Text(_s.noClient));
|
||||
return;
|
||||
}
|
||||
_timer = Timer.periodic(const Duration(seconds: 3), (_) async {
|
||||
if (mounted) {
|
||||
final result = await client.run('ps -aux'.withLangExport).string;
|
||||
if (result.isEmpty) {
|
||||
showSnackBar(context, Text(_s.noResult));
|
||||
return;
|
||||
}
|
||||
_result = PsResult.parse(result, sort: _procSortMode);
|
||||
setState(() {});
|
||||
} else {
|
||||
_timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_timer.cancel();
|
||||
_client = _serverProvider.servers[widget.spi.id]?.client;
|
||||
_timer = Timer.periodic(const Duration(seconds: 3), (_) => _refresh());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_s = S.of(context)!;
|
||||
_media = MediaQuery.of(context);
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
if (mounted) {
|
||||
final result = await _client?.run('ps -aux'.withLangExport).string;
|
||||
if (result == null || result.isEmpty) {
|
||||
showSnackBar(context, Text(_s.noResult));
|
||||
return;
|
||||
}
|
||||
_result = PsResult.parse(result, sort: _procSortMode);
|
||||
setState(() {});
|
||||
} else {
|
||||
_timer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_timer.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -104,12 +109,23 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
|
||||
itemBuilder: (ctx, idx) {
|
||||
final proc = _result.procs[idx];
|
||||
return _buildListItem(proc);
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 277),
|
||||
switchInCurve: Curves.easeIn,
|
||||
switchOutCurve: Curves.easeOut,
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _buildListItem(proc),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: widget.spi.name, down: _s.process),
|
||||
actions: actions,
|
||||
@@ -119,28 +135,49 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
}
|
||||
|
||||
Widget _buildListItem(Proc proc) {
|
||||
return RoundRectCard(ListTile(
|
||||
leading: SizedBox(
|
||||
width: 57,
|
||||
child: TwoLineText(up: proc.pid.toString(), down: proc.user),
|
||||
return RoundRectCard(
|
||||
ListTile(
|
||||
leading: SizedBox(
|
||||
width: _media.size.width / 6,
|
||||
child: TwoLineText(up: proc.pid.toString(), down: proc.user),
|
||||
),
|
||||
title: Text(proc.binary),
|
||||
subtitle: Text(
|
||||
proc.command,
|
||||
style: grey,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TwoLineText(up: proc.cpu.toStringAsFixed(1), down: 'cpu'),
|
||||
width13,
|
||||
TwoLineText(up: proc.mem.toStringAsFixed(1), down: 'mem'),
|
||||
],
|
||||
),
|
||||
onTap: () => _lastFocusId = proc.pid,
|
||||
onLongPress: () {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text(_s.sureStop(proc.pid)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await _client?.run('kill ${proc.pid}');
|
||||
await _refresh();
|
||||
context.pop();
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
selected: _lastFocusId == proc.pid,
|
||||
autofocus: _lastFocusId == proc.pid,
|
||||
),
|
||||
title: Text(proc.binary),
|
||||
subtitle: Text(
|
||||
proc.command,
|
||||
style: grey,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TwoLineText(up: proc.cpu.toStringAsFixed(1), down: 'cpu'),
|
||||
width13,
|
||||
TwoLineText(up: proc.mem.toStringAsFixed(1), down: 'mem'),
|
||||
],
|
||||
),
|
||||
onTap: () => _lastFocusId = proc.pid,
|
||||
autofocus: _lastFocusId == proc.pid,
|
||||
));
|
||||
key: ValueKey(proc.pid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:nil/nil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/extension/order.dart';
|
||||
import 'package:toolbox/data/model/server/cpu.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/view/widget/server_func_btns.dart';
|
||||
|
||||
import '../../../core/extension/numx.dart';
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/server/net_speed.dart';
|
||||
import '../../../data/model/server/server.dart';
|
||||
import '../../../data/model/server/server_status.dart';
|
||||
@@ -15,12 +18,13 @@ import '../../../data/res/default.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../data/store/setting.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/custom_appbar.dart';
|
||||
import '../../widget/round_rect_card.dart';
|
||||
|
||||
class ServerDetailPage extends StatefulWidget {
|
||||
const ServerDetailPage(this.id, {Key? key}) : super(key: key);
|
||||
const ServerDetailPage({Key? key, required this.spi}) : super(key: key);
|
||||
|
||||
final String id;
|
||||
final ServerPrivateInfo spi;
|
||||
|
||||
@override
|
||||
_ServerDetailPageState createState() => _ServerDetailPageState();
|
||||
@@ -62,7 +66,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ServerProvider>(builder: (_, provider, __) {
|
||||
final s = provider.servers[widget.id];
|
||||
final s = provider.servers[widget.spi.id];
|
||||
if (s == null) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
@@ -75,37 +79,42 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
}
|
||||
|
||||
Widget _buildMainPage(Server si) {
|
||||
final buildFuncs = !_setting.moveOutServerTabFuncBtns.fetch()!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(si.spi.name, style: textSize18),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () async {
|
||||
final delete = await AppRoute.serverEdit(spi: si.spi).go(context);
|
||||
if (delete == true) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: ReorderableListView.builder(
|
||||
body: ListView.builder(
|
||||
padding: EdgeInsets.only(
|
||||
left: 13, right: 13, top: 13, bottom: _media.padding.bottom),
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
_cardsOrder.move(
|
||||
oldIndex,
|
||||
newIndex,
|
||||
property: _setting.detailCardOrder,
|
||||
);
|
||||
});
|
||||
},
|
||||
footer: height13,
|
||||
itemCount: _cardsOrder.length,
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) => ReorderableDelayedDragStartListener(
|
||||
key: ValueKey(index),
|
||||
index: index,
|
||||
child: SizedBox(
|
||||
child: _cardBuildMap[_cardsOrder[index]]?.call(si.status),
|
||||
),
|
||||
left: 13,
|
||||
right: 13,
|
||||
bottom: _media.padding.bottom + 77,
|
||||
),
|
||||
itemCount: buildFuncs ? _cardsOrder.length + 1 : _cardsOrder.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0 && buildFuncs) {
|
||||
return ServerFuncBtns(spi: widget.spi, s: _s, iconSize: 19);
|
||||
}
|
||||
if (buildFuncs) index--;
|
||||
return _cardBuildMap[_cardsOrder[index]]?.call(si.status);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCPUView(ServerStatus ss) {
|
||||
final percent = ss.cpu.usedPercent(coreIdx: 0).toInt();
|
||||
return RoundRectCard(
|
||||
Padding(
|
||||
padding: roundRectCardPadding,
|
||||
@@ -113,10 +122,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${ss.cpu.usedPercent(coreIdx: 0).toInt()}%',
|
||||
style: textSize27,
|
||||
textScaleFactor: 1.0,
|
||||
_buildAnimatedText(
|
||||
ValueKey(percent),
|
||||
'$percent%',
|
||||
textSize27,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
@@ -206,6 +215,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
final free = ss.mem.free / ss.mem.total * 100;
|
||||
final avail = ss.mem.availPercent * 100;
|
||||
final used = ss.mem.usedPercent * 100;
|
||||
final usedStr = used.toStringAsFixed(0);
|
||||
|
||||
return RoundRectCard(
|
||||
Padding(
|
||||
@@ -219,7 +229,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('${used.toStringAsFixed(0)}%', style: textSize27),
|
||||
_buildAnimatedText(
|
||||
ValueKey(usedStr),
|
||||
'$usedStr%',
|
||||
textSize27,
|
||||
),
|
||||
width7,
|
||||
Text('of ${(ss.mem.total * 1024).convertBytes}',
|
||||
style: textSize13Grey)
|
||||
@@ -243,7 +257,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
}
|
||||
|
||||
Widget _buildSwapView(ServerStatus ss) {
|
||||
if (ss.swap.total == 0) return nil;
|
||||
if (ss.swap.total == 0) return placeholder;
|
||||
final used = ss.swap.usedPercent * 100;
|
||||
final cached = ss.swap.cached / ss.swap.total * 100;
|
||||
return RoundRectCard(
|
||||
@@ -367,7 +381,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: width / 1.2,
|
||||
width: width,
|
||||
child: Text(
|
||||
device,
|
||||
style: textSize11,
|
||||
@@ -402,7 +416,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
Widget _buildTemperature(ServerStatus ss) {
|
||||
final temps = ss.temps;
|
||||
if (temps.isEmpty) {
|
||||
return nil;
|
||||
return placeholder;
|
||||
}
|
||||
final List<Widget> children = [
|
||||
const Row(
|
||||
@@ -432,9 +446,27 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
),
|
||||
],
|
||||
)));
|
||||
return RoundRectCard(Padding(
|
||||
padding: roundRectCardPadding,
|
||||
child: Column(children: children),
|
||||
));
|
||||
return RoundRectCard(
|
||||
Padding(
|
||||
padding: roundRectCardPadding,
|
||||
child: Column(children: children),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimatedText(Key key, String text, TextStyle style) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 277),
|
||||
child: Text(
|
||||
key: key,
|
||||
text,
|
||||
style: style,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import '../../../data/provider/server.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../data/store/private_key.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/tag/editor.dart';
|
||||
import '../../widget/custom_appbar.dart';
|
||||
import '../../widget/tag.dart';
|
||||
import '../private_key/edit.dart';
|
||||
|
||||
class ServerEditPage extends StatefulWidget {
|
||||
@@ -55,6 +56,22 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
_serverProvider = locator<ServerProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_nameController.dispose();
|
||||
_ipController.dispose();
|
||||
_alterUrlController.dispose();
|
||||
_portController.dispose();
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
_nameFocus.dispose();
|
||||
_ipFocus.dispose();
|
||||
_alterUrlFocus.dispose();
|
||||
_portFocus.dispose();
|
||||
_usernameFocus.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -83,24 +100,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
onPressed: () {
|
||||
_serverProvider.delServer(widget.spi!.id);
|
||||
context.pop();
|
||||
context.pop();
|
||||
context.pop(true);
|
||||
},
|
||||
child: Text(
|
||||
_s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
child: Text(_s.ok, style: textRed),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.cancel),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
);
|
||||
final actions = widget.spi != null ? [delBtn] : null;
|
||||
return AppBar(
|
||||
return CustomAppBar(
|
||||
title: Text(_s.edit, style: textSize18),
|
||||
actions: actions,
|
||||
);
|
||||
@@ -109,6 +119,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
Widget _buildForm() {
|
||||
final children = [
|
||||
Input(
|
||||
autoFocus: true,
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
node: _nameFocus,
|
||||
@@ -200,17 +211,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
Widget _buildKeyAuth() {
|
||||
return Consumer<PrivateKeyProvider>(
|
||||
builder: (_, key, __) {
|
||||
for (var item in key.infos) {
|
||||
for (var item in key.pkis) {
|
||||
if (item.id == widget.spi?.pubKeyId) {
|
||||
_pubKeyIndex ??= key.infos.indexOf(item);
|
||||
_pubKeyIndex ??= key.pkis.indexOf(item);
|
||||
}
|
||||
}
|
||||
final tiles = key.infos
|
||||
final tiles = key.pkis
|
||||
.map(
|
||||
(e) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(e.id, textAlign: TextAlign.start),
|
||||
trailing: _buildRadio(key.infos.indexOf(e), e),
|
||||
trailing: _buildRadio(key.pkis.indexOf(e), e),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
@@ -3,35 +3,26 @@ import 'package:circle_chart/circle_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:nil/nil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/order.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
import 'package:toolbox/data/model/app/net_view.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/provider/snippet.dart';
|
||||
import 'package:toolbox/view/page/process.dart';
|
||||
import 'package:toolbox/view/widget/tag/picker.dart';
|
||||
import 'package:toolbox/view/widget/tag/switcher.dart';
|
||||
import 'package:toolbox/core/extension/media_queryx.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../core/utils/misc.dart' hide pathJoin;
|
||||
import '../../../core/utils/platform.dart';
|
||||
import '../../../core/utils/ui.dart';
|
||||
import '../../../data/model/app/net_view.dart';
|
||||
import '../../../data/model/server/disk.dart';
|
||||
import '../../../data/model/server/server.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/model/server/server_status.dart';
|
||||
import '../../../data/provider/server.dart';
|
||||
import '../../../data/res/color.dart';
|
||||
import '../../../data/model/app/menu.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../data/store/setting.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/popup_menu.dart';
|
||||
import '../../widget/round_rect_card.dart';
|
||||
import '../docker.dart';
|
||||
import '../pkg.dart';
|
||||
import '../storage/sftp.dart';
|
||||
import '../ssh/term.dart';
|
||||
import 'detail.dart';
|
||||
import '../../widget/server_func_btns.dart';
|
||||
import '../../widget/tag.dart';
|
||||
import 'edit.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget {
|
||||
@@ -44,12 +35,12 @@ class ServerPage extends StatefulWidget {
|
||||
class _ServerPageState extends State<ServerPage>
|
||||
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
||||
late MediaQueryData _media;
|
||||
late ThemeData _theme;
|
||||
late ServerProvider _serverProvider;
|
||||
late SettingStore _settingStore;
|
||||
late S _s;
|
||||
|
||||
String? _tag;
|
||||
bool _useDoubleColumn = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -62,7 +53,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
_theme = Theme.of(context);
|
||||
_useDoubleColumn = _media.useDoubleColumn;
|
||||
_s = S.of(context)!;
|
||||
}
|
||||
|
||||
@@ -84,72 +75,132 @@ class _ServerPageState extends State<ServerPage>
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
final child = Consumer<ServerProvider>(
|
||||
builder: (_, pro, __) {
|
||||
if (!pro.tags.contains(_tag)) {
|
||||
_tag = null;
|
||||
}
|
||||
if (pro.serverOrder.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_s.serverTabEmpty,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final filtered = _filterServers(pro);
|
||||
if (_useDoubleColumn) {
|
||||
return _buildBodyMedium(pro);
|
||||
}
|
||||
return _buildBodySmall(provider: pro, filtered: filtered);
|
||||
},
|
||||
);
|
||||
|
||||
// Desktop doesn't support pull to refresh
|
||||
if (isDesktop) {
|
||||
return child;
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async =>
|
||||
await _serverProvider.refreshData(onlyFailed: true),
|
||||
child: Consumer<ServerProvider>(
|
||||
builder: (_, pro, __) {
|
||||
if (!pro.tags.contains(_tag)) {
|
||||
_tag = null;
|
||||
}
|
||||
if (pro.serverOrder.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_s.serverTabEmpty,
|
||||
textAlign: TextAlign.center,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _filterServers(ServerProvider pro) => pro.serverOrder
|
||||
.where((e) => pro.servers.containsKey(e))
|
||||
.where((e) =>
|
||||
_tag == null || (pro.servers[e]?.spi.tags?.contains(_tag) ?? false))
|
||||
.toList();
|
||||
|
||||
Widget _buildTagsSwitcher(ServerProvider provider) {
|
||||
return TagSwitcher(
|
||||
tags: provider.tags,
|
||||
width: _media.size.width,
|
||||
onTagChanged: (p0) => setState(() {
|
||||
_tag = p0;
|
||||
}),
|
||||
initTag: _tag,
|
||||
all: _s.all,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBodySmall({
|
||||
required ServerProvider provider,
|
||||
required List<String> filtered,
|
||||
EdgeInsets? padding = const EdgeInsets.fromLTRB(7, 0, 7, 7),
|
||||
bool buildTags = true,
|
||||
}) {
|
||||
final count = buildTags ? filtered.length + 2 : filtered.length + 1;
|
||||
return ListView.builder(
|
||||
padding: padding,
|
||||
itemCount: count,
|
||||
itemBuilder: (_, index) {
|
||||
if (index == 0 && buildTags) return _buildTagsSwitcher(provider);
|
||||
|
||||
// Issue #130
|
||||
if (index == count - 1) return height77;
|
||||
|
||||
if (buildTags) index--;
|
||||
return _buildEachServerCard(provider.servers[filtered[index]]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBodyMedium(ServerProvider pro) {
|
||||
final filtered = _filterServers(pro);
|
||||
final left = filtered.where((e) => filtered.indexOf(e) % 2 == 0).toList();
|
||||
final right = filtered.where((e) => filtered.indexOf(e) % 2 == 1).toList();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: _buildTagsSwitcher(pro),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildBodySmall(
|
||||
provider: pro,
|
||||
filtered: left,
|
||||
padding: const EdgeInsets.fromLTRB(7, 0, 0, 7),
|
||||
buildTags: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
final filtered = pro.serverOrder
|
||||
.where((e) => pro.servers.containsKey(e))
|
||||
.where((e) =>
|
||||
_tag == null ||
|
||||
(pro.servers[e]?.spi.tags?.contains(_tag) ?? false))
|
||||
.toList();
|
||||
return ReorderableListView.builder(
|
||||
header: TagSwitcher(
|
||||
tags: pro.tags,
|
||||
width: _media.size.width,
|
||||
onTagChanged: (p0) => setState(() {
|
||||
_tag = p0;
|
||||
}),
|
||||
initTag: _tag,
|
||||
all: _s.all,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(7, 10, 7, 7),
|
||||
onReorder: (oldIndex, newIndex) => setState(() {
|
||||
pro.serverOrder.moveByItem(
|
||||
filtered,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
property: _settingStore.serverOrder,
|
||||
);
|
||||
}),
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (_, index) => ReorderableDelayedDragStartListener(
|
||||
key: ValueKey('$_tag${filtered[index]}'),
|
||||
index: index,
|
||||
child: _buildEachServerCard(pro.servers[filtered[index]]),
|
||||
Expanded(
|
||||
child: _buildBodySmall(
|
||||
provider: pro,
|
||||
filtered: right,
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 7, 7),
|
||||
buildTags: false,
|
||||
),
|
||||
),
|
||||
itemCount: filtered.length,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEachServerCard(Server? si) {
|
||||
if (si == null) {
|
||||
return nil;
|
||||
return placeholder;
|
||||
}
|
||||
return GestureDetector(
|
||||
|
||||
return RoundRectCard(
|
||||
key: Key(si.spi.id + (_tag ?? '')),
|
||||
onTap: () => AppRoute(
|
||||
ServerDetailPage(si.spi.id),
|
||||
'server detail page',
|
||||
).go(context),
|
||||
child: RoundRectCard(
|
||||
Padding(
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (si.state.canViewDetails) {
|
||||
AppRoute.serverDetail(spi: si.spi).go(context);
|
||||
} else if (si.status.failedInfo != null) {
|
||||
_showFailReason(si.status);
|
||||
}
|
||||
},
|
||||
onLongPress: () => AppRoute.serverEdit(spi: si.spi).go(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(13),
|
||||
child: _buildRealServerCard(si.status, si.state, si.spi),
|
||||
),
|
||||
@@ -157,40 +208,67 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapWithSizedbox(Widget child) {
|
||||
return SizedBox(
|
||||
width: _useDoubleColumn
|
||||
? (_media.size.width - 146) / 10
|
||||
: (_media.size.width - 74) / 5,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRealServerCard(
|
||||
ServerStatus ss,
|
||||
ServerState cs,
|
||||
ServerPrivateInfo spi,
|
||||
) {
|
||||
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/');
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
final rootDisk = findRootDisk(ss.disk);
|
||||
late final List<Widget> children;
|
||||
double? height;
|
||||
if (cs != ServerState.finished) {
|
||||
height = 23.0;
|
||||
children = [
|
||||
_buildServerCardTitle(ss, cs, spi),
|
||||
];
|
||||
} else {
|
||||
height = 107;
|
||||
children = [
|
||||
_buildServerCardTitle(ss, cs, spi),
|
||||
height13,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildPercentCircle(ss.cpu.usedPercent()),
|
||||
_buildPercentCircle(ss.mem.usedPercent * 100),
|
||||
_buildNet(ss),
|
||||
_buildIOData(
|
||||
'Total:\n${rootDisk.size}', 'Used:\n${rootDisk.usedPercent}%')
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 13),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_wrapWithSizedbox(_buildPercentCircle(ss.cpu.usedPercent())),
|
||||
_wrapWithSizedbox(_buildPercentCircle(ss.mem.usedPercent * 100)),
|
||||
_wrapWithSizedbox(_buildNet(ss)),
|
||||
_wrapWithSizedbox(_buildIOData(
|
||||
'Total:\n${rootDisk?.size}',
|
||||
'Used:\n${rootDisk?.usedPercent}%',
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
height13,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildExplainText('CPU'),
|
||||
_buildExplainText('Mem'),
|
||||
_buildExplainText('Net'),
|
||||
_buildExplainText('Disk'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
],
|
||||
if (_settingStore.moveOutServerTabFuncBtns.fetch()!)
|
||||
SizedBox(
|
||||
height: 27,
|
||||
child: ServerFuncBtns(spi: spi, s: _s),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
height: height,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -208,8 +286,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
children: [
|
||||
Text(
|
||||
spi.name,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
style: textSize13Bold,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
const Icon(
|
||||
@@ -219,14 +296,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildTopRightText(ss, cs),
|
||||
width7,
|
||||
_buildSSHBtn(spi),
|
||||
_buildMoreBtn(spi),
|
||||
],
|
||||
)
|
||||
_buildTopRightText(ss, cs),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -239,95 +309,36 @@ class _ServerPageState extends State<ServerPage>
|
||||
ss.uptime,
|
||||
ss.failedInfo,
|
||||
);
|
||||
final hasError = cs == ServerState.failed && ss.failedInfo != null;
|
||||
return hasError
|
||||
? GestureDetector(
|
||||
onTap: () => showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.error),
|
||||
child: Text(ss.failedInfo ?? _s.unknownError),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
copy2Clipboard(ss.failedInfo ?? _s.unknownError),
|
||||
child: Text(_s.copy),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
_s.viewErr,
|
||||
style: textSize12Grey,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
topRightStr,
|
||||
style: textSize12Grey,
|
||||
textScaleFactor: 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSSHBtn(ServerPrivateInfo spi) {
|
||||
return GestureDetector(
|
||||
child: const Icon(
|
||||
Icons.terminal,
|
||||
size: 21,
|
||||
),
|
||||
onTap: () => AppRoute(SSHPage(spi: spi), 'ssh page').go(context),
|
||||
if (cs == ServerState.failed && ss.failedInfo != null) {
|
||||
return GestureDetector(
|
||||
onTap: () => _showFailReason(ss),
|
||||
child: Text(
|
||||
_s.viewErr,
|
||||
style: textSize11Grey,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
topRightStr,
|
||||
style: textSize11Grey,
|
||||
textScaleFactor: 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMoreBtn(ServerPrivateInfo spi) {
|
||||
return PopupMenu(
|
||||
items: ServerTabMenuType.values.map((e) => e.build(_s)).toList(),
|
||||
onSelected: (ServerTabMenuType value) async {
|
||||
switch (value) {
|
||||
case ServerTabMenuType.pkg:
|
||||
AppRoute(PkgManagePage(spi), 'pkg manage').go(context);
|
||||
break;
|
||||
case ServerTabMenuType.sftp:
|
||||
AppRoute(SftpPage(spi), 'SFTP').go(context);
|
||||
break;
|
||||
case ServerTabMenuType.snippet:
|
||||
final provider = locator<SnippetProvider>();
|
||||
final snippets = await showDialog<List<Snippet>>(
|
||||
context: context,
|
||||
builder: (_) => TagPicker<Snippet>(
|
||||
items: provider.snippets,
|
||||
containsTag: (t, tag) => t.tags?.contains(tag) ?? false,
|
||||
tags: provider.tags.toSet(),
|
||||
name: (t) => t.name,
|
||||
),
|
||||
);
|
||||
if (snippets == null) {
|
||||
return;
|
||||
}
|
||||
final result = await _serverProvider.runSnippets(spi.id, snippets);
|
||||
if (result != null && result.isNotEmpty) {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.result),
|
||||
child: Text(result),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => copy2Clipboard(result),
|
||||
child: Text(_s.copy),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ServerTabMenuType.edit:
|
||||
AppRoute(ServerEditPage(spi: spi), 'Edit server info').go(context);
|
||||
break;
|
||||
case ServerTabMenuType.docker:
|
||||
AppRoute(DockerManagePage(spi), 'Docker manage').go(context);
|
||||
break;
|
||||
case ServerTabMenuType.process:
|
||||
AppRoute(ProcessPage(spi: spi), 'process page').go(context);
|
||||
break;
|
||||
}
|
||||
},
|
||||
void _showFailReason(ServerStatus ss) {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.error),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(ss.failedInfo ?? _s.unknownError),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => copy2Clipboard(ss.failedInfo!),
|
||||
child: Text(_s.copy),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -344,18 +355,6 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExplainText(String text) {
|
||||
return SizedBox(
|
||||
width: _media.size.width * 0.2,
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getTopRightStr(
|
||||
ServerState cs,
|
||||
double? temp,
|
||||
@@ -365,12 +364,16 @@ class _ServerPageState extends State<ServerPage>
|
||||
switch (cs) {
|
||||
case ServerState.disconnected:
|
||||
return _s.disconnected;
|
||||
case ServerState.connected:
|
||||
case ServerState.finished:
|
||||
final tempStr = temp == null ? '' : '${temp.toStringAsFixed(1)}°C';
|
||||
final items = [tempStr, upTime];
|
||||
final str = items.where((element) => element.isNotEmpty).join(' | ');
|
||||
if (str.isEmpty) return _s.serverTabLoading;
|
||||
if (str.isEmpty) return _s.noResult;
|
||||
return str;
|
||||
case ServerState.loading:
|
||||
return _s.serverTabLoading;
|
||||
case ServerState.connected:
|
||||
return _s.connected;
|
||||
case ServerState.connecting:
|
||||
return _s.serverTabConnecting;
|
||||
case ServerState.failed:
|
||||
@@ -381,65 +384,56 @@ class _ServerPageState extends State<ServerPage>
|
||||
return _s.serverTabPlzSave;
|
||||
}
|
||||
return failedInfo;
|
||||
default:
|
||||
return _s.serverTabUnkown;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildIOData(String up, String down) {
|
||||
final statusTextStyle = TextStyle(
|
||||
fontSize: 9, color: _theme.textTheme.bodyLarge!.color!.withAlpha(177));
|
||||
return SizedBox(
|
||||
width: _media.size.width * 0.2,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
up,
|
||||
style: statusTextStyle,
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
down,
|
||||
style: statusTextStyle,
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
up,
|
||||
style: textSize9Grey,
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
down,
|
||||
style: textSize9Grey,
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPercentCircle(double percent) {
|
||||
if (percent <= 0) percent = 0.01;
|
||||
if (percent >= 100) percent = 99.9;
|
||||
return SizedBox(
|
||||
width: _media.size.width * 0.2,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: CircleChart(
|
||||
progressColor: primaryColor,
|
||||
progressNumber: percent,
|
||||
maxNumber: 100,
|
||||
width: 53,
|
||||
height: 53,
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: CircleChart(
|
||||
progressColor: primaryColor,
|
||||
progressNumber: percent,
|
||||
maxNumber: 100,
|
||||
width: 53,
|
||||
height: 53,
|
||||
animationDuration: const Duration(milliseconds: 777),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: textSize11,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,33 +4,35 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_highlight/theme_map.dart';
|
||||
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:toolbox/core/extension/colorx.dart';
|
||||
import 'package:toolbox/core/extension/locale.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/persistant_store.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/model/app/net_view.dart';
|
||||
import 'package:toolbox/data/model/app/tab.dart';
|
||||
import 'package:toolbox/view/page/ssh/virt_key_setting.dart';
|
||||
import 'package:toolbox/view/page/setting/virt_key.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/value_notifier.dart';
|
||||
|
||||
import '../../core/utils/misc.dart';
|
||||
import '../../core/utils/platform.dart';
|
||||
import '../../core/update.dart';
|
||||
import '../../core/utils/ui.dart';
|
||||
import '../../data/provider/app.dart';
|
||||
import '../../data/provider/server.dart';
|
||||
import '../../data/res/build_data.dart';
|
||||
import '../../data/res/color.dart';
|
||||
import '../../data/res/path.dart';
|
||||
import '../../data/res/ui.dart';
|
||||
import '../../data/store/server.dart';
|
||||
import '../../data/store/setting.dart';
|
||||
import '../../locator.dart';
|
||||
import '../widget/future_widget.dart';
|
||||
import '../widget/round_rect_card.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../core/utils/platform.dart';
|
||||
import '../../../core/update.dart';
|
||||
import '../../../core/utils/ui.dart';
|
||||
import '../../../data/provider/app.dart';
|
||||
import '../../../data/provider/server.dart';
|
||||
import '../../../data/res/build_data.dart';
|
||||
import '../../../data/res/color.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../data/store/server.dart';
|
||||
import '../../../data/store/setting.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/custom_appbar.dart';
|
||||
import '../../widget/future_widget.dart';
|
||||
import '../../widget/round_rect_card.dart';
|
||||
|
||||
class SettingPage extends StatefulWidget {
|
||||
const SettingPage({Key? key}) : super(key: key);
|
||||
@@ -41,7 +43,7 @@ class SettingPage extends StatefulWidget {
|
||||
|
||||
class _SettingPageState extends State<SettingPage> {
|
||||
final _themeKey = GlobalKey<PopupMenuButtonState<int>>();
|
||||
final _startPageKey = GlobalKey<PopupMenuButtonState<int>>();
|
||||
//final _startPageKey = GlobalKey<PopupMenuButtonState<int>>();
|
||||
final _updateIntervalKey = GlobalKey<PopupMenuButtonState<int>>();
|
||||
final _maxRetryKey = GlobalKey<PopupMenuButtonState<int>>();
|
||||
final _localeKey = GlobalKey<PopupMenuButtonState<String>>();
|
||||
@@ -53,7 +55,6 @@ class _SettingPageState extends State<SettingPage> {
|
||||
|
||||
late final SettingStore _setting;
|
||||
late final ServerProvider _serverProvider;
|
||||
late MediaQueryData _media;
|
||||
late S _s;
|
||||
late SharedPreferences _sp;
|
||||
|
||||
@@ -62,7 +63,8 @@ class _SettingPageState extends State<SettingPage> {
|
||||
final _nightMode = ValueNotifier(0);
|
||||
final _maxRetryCount = ValueNotifier(0);
|
||||
final _updateInterval = ValueNotifier(0);
|
||||
final _fontSize = ValueNotifier(0.0);
|
||||
final _termFontSize = ValueNotifier(0.0);
|
||||
final _editorFontSize = ValueNotifier(0.0);
|
||||
final _localeCode = ValueNotifier('');
|
||||
final _editorTheme = ValueNotifier('');
|
||||
final _editorDarkTheme = ValueNotifier('');
|
||||
@@ -75,7 +77,6 @@ class _SettingPageState extends State<SettingPage> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
_s = S.of(context)!;
|
||||
_localeCode.value = _setting.locale.fetch() ?? _s.localeName;
|
||||
}
|
||||
@@ -90,7 +91,8 @@ class _SettingPageState extends State<SettingPage> {
|
||||
_updateInterval.value = _setting.serverStatusUpdateInterval.fetch()!;
|
||||
_maxRetryCount.value = _setting.maxRetryCount.fetch()!;
|
||||
_selectedColorValue.value = _setting.primaryColor.fetch()!;
|
||||
_fontSize.value = _setting.termFontSize.fetch()!;
|
||||
_termFontSize.value = _setting.termFontSize.fetch()!;
|
||||
_editorFontSize.value = _setting.editorFontSize.fetch()!;
|
||||
_editorTheme.value = _setting.editorTheme.fetch()!;
|
||||
_editorDarkTheme.value = _setting.editorDarkTheme.fetch()!;
|
||||
_keyboardType.value = _setting.keyboardType.fetch()!;
|
||||
@@ -102,7 +104,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(_s.setting),
|
||||
),
|
||||
body: ListView(
|
||||
@@ -141,7 +143,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
_buildLocale(),
|
||||
_buildThemeMode(),
|
||||
_buildAppColor(),
|
||||
_buildLaunchPage(),
|
||||
//_buildLaunchPage(),
|
||||
_buildCheckUpdate(),
|
||||
];
|
||||
if (isIOS) {
|
||||
@@ -170,6 +172,9 @@ class _SettingPageState extends State<SettingPage> {
|
||||
Widget _buildServer() {
|
||||
return Column(
|
||||
children: [
|
||||
_buildMoveOutServerFuncBtns(),
|
||||
_buildServerOrder(),
|
||||
_buildServerDetailOrder(),
|
||||
_buildNetViewType(),
|
||||
_buildUpdateInterval(),
|
||||
_buildMaxRetry(),
|
||||
@@ -194,6 +199,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
Widget _buildEditor() {
|
||||
return Column(
|
||||
children: [
|
||||
_buildEditorFontSize(),
|
||||
_buildEditorTheme(),
|
||||
_buildEditorDarkTheme(),
|
||||
].map((e) => RoundRectCard(e)).toList(),
|
||||
@@ -214,11 +220,10 @@ class _SettingPageState extends State<SettingPage> {
|
||||
display = _s.versionUnknownUpdate(BuildData.build);
|
||||
}
|
||||
return ListTile(
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
title: Text(
|
||||
display,
|
||||
),
|
||||
title: Text(_s.autoCheckUpdate),
|
||||
subtitle: Text(display, style: grey),
|
||||
onTap: () => doUpdate(ctx, force: true),
|
||||
trailing: buildSwitch(context, _setting.autoCheckAppUpdate),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -277,81 +282,76 @@ class _SettingPageState extends State<SettingPage> {
|
||||
width: 27,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
_s.primaryColor,
|
||||
),
|
||||
title: Text(_s.primaryColorSeed),
|
||||
onTap: () async {
|
||||
final ctrl = TextEditingController(text: primaryColor.toHex);
|
||||
await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.primaryColor),
|
||||
child: SizedBox(
|
||||
height: 211,
|
||||
child: Center(
|
||||
child: MaterialColorPicker(
|
||||
shrinkWrap: true,
|
||||
allowShades: true,
|
||||
onColorChange: (color) {
|
||||
_selectedColorValue.value = color.value;
|
||||
},
|
||||
selectedColor: primaryColor,
|
||||
),
|
||||
),
|
||||
title: Text(_s.primaryColorSeed),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
onSubmitted: _onSaveColor,
|
||||
controller: ctrl,
|
||||
hint: '#8b2252',
|
||||
icon: Icons.colorize,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_setting.primaryColor.put(_selectedColorValue.value);
|
||||
Navigator.pop(context);
|
||||
_showRestartSnackbar();
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLaunchPage() {
|
||||
final items = AppTab.values
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e.index,
|
||||
child: Text(tabTitleName(context, e)),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
_s.launchPage,
|
||||
),
|
||||
onTap: () {
|
||||
_startPageKey.currentState?.showButtonMenu();
|
||||
},
|
||||
trailing: ValueBuilder(
|
||||
listenable: _launchPageIdx,
|
||||
build: () => PopupMenuButton(
|
||||
key: _startPageKey,
|
||||
itemBuilder: (BuildContext context) => items,
|
||||
initialValue: _launchPageIdx.value,
|
||||
onSelected: (int idx) {
|
||||
_launchPageIdx.value = idx;
|
||||
_setting.launchPage.put(_launchPageIdx.value);
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: _media.size.width * 0.35),
|
||||
child: Text(
|
||||
tabTitleName(context, AppTab.values[_launchPageIdx.value]),
|
||||
textAlign: TextAlign.right,
|
||||
style: textSize15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
void _onSaveColor(String s) {
|
||||
final color = s.hexToColor;
|
||||
if (color == null) {
|
||||
showSnackBar(context, Text(_s.failed));
|
||||
return;
|
||||
}
|
||||
_selectedColorValue.value = color.value;
|
||||
_setting.primaryColor.put(_selectedColorValue.value);
|
||||
context.pop();
|
||||
_showRestartSnackbar();
|
||||
}
|
||||
|
||||
// Widget _buildLaunchPage() {
|
||||
// final items = AppTab.values
|
||||
// .map(
|
||||
// (e) => PopupMenuItem(
|
||||
// value: e.index,
|
||||
// child: Text(tabTitleName(context, e)),
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
|
||||
// return ListTile(
|
||||
// title: Text(
|
||||
// _s.launchPage,
|
||||
// ),
|
||||
// onTap: () {
|
||||
// _startPageKey.currentState?.showButtonMenu();
|
||||
// },
|
||||
// trailing: ValueBuilder(
|
||||
// listenable: _launchPageIdx,
|
||||
// build: () => PopupMenuButton(
|
||||
// key: _startPageKey,
|
||||
// itemBuilder: (BuildContext context) => items,
|
||||
// initialValue: _launchPageIdx.value,
|
||||
// onSelected: (int idx) {
|
||||
// _launchPageIdx.value = idx;
|
||||
// _setting.launchPage.put(_launchPageIdx.value);
|
||||
// },
|
||||
// child: ConstrainedBox(
|
||||
// constraints: BoxConstraints(maxWidth: _media.size.width * 0.35),
|
||||
// child: Text(
|
||||
// tabTitleName(context, AppTab.values[_launchPageIdx.value]),
|
||||
// textAlign: TextAlign.right,
|
||||
// style: textSize15,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget _buildMaxRetry() {
|
||||
final items = List.generate(
|
||||
10,
|
||||
@@ -550,45 +550,14 @@ class _SettingPageState extends State<SettingPage> {
|
||||
|
||||
Widget _buildTermFontSize() {
|
||||
return ValueBuilder(
|
||||
listenable: _fontSize,
|
||||
listenable: _termFontSize,
|
||||
build: () => ListTile(
|
||||
title: Text(_s.fontSize),
|
||||
trailing: Text(
|
||||
_fontSize.value.toString(),
|
||||
_termFontSize.value.toString(),
|
||||
style: textSize15,
|
||||
),
|
||||
onTap: () {
|
||||
final ctrller =
|
||||
TextEditingController(text: _fontSize.value.toString());
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.fontSize),
|
||||
child: Input(
|
||||
controller: ctrller,
|
||||
type: TextInputType.number,
|
||||
icon: Icons.font_download,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
final fontSize = double.tryParse(ctrller.text);
|
||||
if (fontSize == null) {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.failed),
|
||||
child: Text('Parsed failed: ${ctrller.text}'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_fontSize.value = fontSize;
|
||||
_setting.termFontSize.put(_fontSize.value);
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
onTap: () => _showFontSizeDialog(_termFontSize, _setting.termFontSize),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -615,6 +584,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
context: context,
|
||||
title: Text(_s.diskIgnorePath),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
controller: ctrller,
|
||||
label: 'JSON',
|
||||
type: TextInputType.visiblePassword,
|
||||
@@ -681,7 +651,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
},
|
||||
).toList();
|
||||
return ListTile(
|
||||
title: Text(_s.light + _s.theme),
|
||||
title: Text('${_s.light} ${_s.theme.toLowerCase()}'),
|
||||
trailing: ValueBuilder(
|
||||
listenable: _editorTheme,
|
||||
build: () => PopupMenuButton(
|
||||
@@ -714,7 +684,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
},
|
||||
).toList();
|
||||
return ListTile(
|
||||
title: Text(_s.dark + _s.theme),
|
||||
title: Text('${_s.dark} ${_s.theme.toLowerCase()}'),
|
||||
trailing: ValueBuilder(
|
||||
listenable: _editorDarkTheme,
|
||||
build: () => PopupMenuButton(
|
||||
@@ -885,6 +855,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
context: context,
|
||||
title: Text(_s.homeWidgetUrlConfig),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
controller: ctrl,
|
||||
label: 'JSON',
|
||||
type: TextInputType.visiblePassword,
|
||||
@@ -977,4 +948,82 @@ class _SettingPageState extends State<SettingPage> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMoveOutServerFuncBtns() {
|
||||
return ListTile(
|
||||
title: Text(_s.moveOutServerFuncBtns),
|
||||
subtitle: Text(_s.moveOutServerFuncBtnsHelp, style: textSize13Grey),
|
||||
trailing: buildSwitch(context, _setting.moveOutServerTabFuncBtns),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildServerOrder() {
|
||||
return ListTile(
|
||||
title: Text(_s.serverOrder),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () => AppRoute.serverOrder().go(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildServerDetailOrder() {
|
||||
return ListTile(
|
||||
title: Text(_s.serverDetailOrder),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () => AppRoute.serverDetailOrder().go(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEditorFontSize() {
|
||||
return ValueBuilder(
|
||||
listenable: _editorFontSize,
|
||||
build: () => ListTile(
|
||||
title: Text(_s.fontSize),
|
||||
trailing: Text(
|
||||
_editorFontSize.value.toString(),
|
||||
style: textSize15,
|
||||
),
|
||||
onTap: () =>
|
||||
_showFontSizeDialog(_editorFontSize, _setting.editorFontSize),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFontSizeDialog(
|
||||
ValueNotifier<double> notifier,
|
||||
StoreProperty property,
|
||||
) {
|
||||
final ctrller = TextEditingController(text: notifier.value.toString());
|
||||
void onSave() {
|
||||
context.pop();
|
||||
final fontSize = double.tryParse(ctrller.text);
|
||||
if (fontSize == null) {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.failed),
|
||||
child: Text('Parsed failed: ${ctrller.text}'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifier.value = fontSize;
|
||||
property.put(fontSize);
|
||||
}
|
||||
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.fontSize),
|
||||
child: Input(
|
||||
controller: ctrller,
|
||||
autoFocus: true,
|
||||
type: TextInputType.number,
|
||||
icon: Icons.font_download,
|
||||
onSubmitted: (_) => onSave(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: onSave,
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
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/view/widget/round_rect_card.dart';
|
||||
|
||||
import '../../widget/custom_appbar.dart';
|
||||
|
||||
class SSHVirtKeySettingPage extends StatefulWidget {
|
||||
const SSHVirtKeySettingPage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -29,7 +31,7 @@ class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(_s.editVirtKeys),
|
||||
),
|
||||
body: _buildBody(),
|
||||
@@ -50,13 +52,14 @@ class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {
|
||||
final key = allKeys[idx];
|
||||
final help = key.help(_s);
|
||||
return RoundRectCard(
|
||||
key: ValueKey(idx),
|
||||
ListTile(
|
||||
title: _buildTitle(key),
|
||||
subtitle: help == null ? null : Text(help, style: grey),
|
||||
leading: _buildCheckBox(keys, key, idx, idx < keys.length),
|
||||
trailing: isDesktop ? null : const Icon(Icons.drag_handle),
|
||||
));
|
||||
key: ValueKey(idx),
|
||||
ListTile(
|
||||
title: _buildTitle(key),
|
||||
subtitle: help == null ? null : Text(help, style: grey),
|
||||
leading: _buildCheckBox(keys, key, idx, idx < keys.length),
|
||||
trailing: isDesktop ? null : const Icon(Icons.drag_handle),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: allKeys.length,
|
||||
onReorder: (o, n) {
|
||||
@@ -9,7 +9,8 @@ import '../../../data/model/server/snippet.dart';
|
||||
import '../../../data/provider/snippet.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/tag/editor.dart';
|
||||
import '../../widget/custom_appbar.dart';
|
||||
import '../../widget/tag.dart';
|
||||
|
||||
class SnippetEditPage extends StatefulWidget {
|
||||
const SnippetEditPage({Key? key, this.snippet}) : super(key: key);
|
||||
@@ -37,6 +38,14 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
_provider = locator<SnippetProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_nameController.dispose();
|
||||
_scriptController.dispose();
|
||||
_scriptNode.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -46,7 +55,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(_s.edit, style: textSize18),
|
||||
actions: _buildAppBarActions(),
|
||||
),
|
||||
@@ -98,6 +107,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
Input(
|
||||
autoFocus: true,
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
onSubmitted: (_) => FocusScope.of(context).requestFocus(_scriptNode),
|
||||
|
||||
@@ -2,17 +2,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/order.dart';
|
||||
import 'package:toolbox/data/model/server/server.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/tag/switcher.dart';
|
||||
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../core/utils/ui.dart';
|
||||
import '../../../data/model/server/server.dart';
|
||||
import '../../../data/model/server/snippet.dart';
|
||||
import '../../../data/provider/server.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../data/store/setting.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/tag/picker.dart';
|
||||
import '../../widget/tag.dart';
|
||||
import '/core/route.dart';
|
||||
import '/data/provider/snippet.dart';
|
||||
import 'edit.dart';
|
||||
@@ -69,7 +68,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
.toList();
|
||||
|
||||
return ReorderableListView.builder(
|
||||
padding: const EdgeInsets.all(13),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 13),
|
||||
itemCount: filtered.length,
|
||||
onReorder: (oldIdx, newIdx) => setState(() {
|
||||
provider.snippets.moveByItem(
|
||||
@@ -88,6 +87,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
all: _s.all,
|
||||
width: _media.size.width,
|
||||
),
|
||||
footer: height77,
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, idx) {
|
||||
final snippet = filtered.elementAt(idx);
|
||||
|
||||
@@ -8,22 +8,22 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/data/res/server_cmd.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../core/utils/platform.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../core/utils/ui.dart';
|
||||
import '../../../core/utils/server.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/model/ssh/virtual_key.dart';
|
||||
import '../../../data/provider/virtual_keyboard.dart';
|
||||
import '../../../data/res/color.dart';
|
||||
import '../../../data/res/terminal.dart';
|
||||
import '../../../data/store/setting.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../storage/sftp.dart';
|
||||
import '../../core/route.dart';
|
||||
import '../../core/utils/platform.dart';
|
||||
import '../../core/utils/misc.dart';
|
||||
import '../../core/utils/ui.dart';
|
||||
import '../../core/utils/server.dart';
|
||||
import '../../data/model/server/server_private_info.dart';
|
||||
import '../../data/model/ssh/virtual_key.dart';
|
||||
import '../../data/provider/virtual_keyboard.dart';
|
||||
import '../../data/res/color.dart';
|
||||
import '../../data/res/terminal.dart';
|
||||
import '../../data/store/setting.dart';
|
||||
import '../../locator.dart';
|
||||
|
||||
const echoPWD = 'echo \$PWD';
|
||||
|
||||
class SSHPage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
@@ -46,13 +46,14 @@ class _SSHPageState extends State<SSHPage> {
|
||||
late TerminalStyle _terminalStyle;
|
||||
late TerminalTheme _terminalTheme;
|
||||
late TextInputType _keyboardType;
|
||||
late SSHSession _session;
|
||||
late double _virtKeyWidth;
|
||||
late double _virtKeysHeight;
|
||||
double _virtKeyWidth = 0;
|
||||
double _virtKeysHeight = 0;
|
||||
|
||||
bool _isDark = false;
|
||||
Timer? _virtKeyLongPressTimer;
|
||||
SSHClient? _client;
|
||||
SSHSession? _session;
|
||||
Timer? _discontinuityTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -68,6 +69,19 @@ class _SSHPageState extends State<SSHPage> {
|
||||
_initVirtKeys();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_virtKeyLongPressTimer?.cancel();
|
||||
_terminalController.dispose();
|
||||
if (_client?.isClosed == false) {
|
||||
try {
|
||||
_client?.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
_discontinuityTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -75,15 +89,12 @@ class _SSHPageState extends State<SSHPage> {
|
||||
_media = MediaQuery.of(context);
|
||||
_s = S.of(context)!;
|
||||
_terminalTheme = _isDark ? termDarkTheme : termLightTheme;
|
||||
// Calculate virtkey width / height
|
||||
_virtKeyWidth = _media.size.width / 7;
|
||||
_virtKeysHeight = _media.size.height * 0.043 * _virtKeysList.length;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_client?.close();
|
||||
super.dispose();
|
||||
// Because the virtual keyboard only displayed on mobile devices
|
||||
if (isMobile) {
|
||||
_virtKeyWidth = _media.size.width / 7;
|
||||
_virtKeysHeight = _media.size.height * 0.043 * _virtKeysList.length;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -91,7 +102,7 @@ class _SSHPageState extends State<SSHPage> {
|
||||
Widget child = Scaffold(
|
||||
backgroundColor: _terminalTheme.background,
|
||||
body: _buildBody(),
|
||||
bottomNavigationBar: _buildBottom(),
|
||||
bottomNavigationBar: isDesktop ? null : _buildBottom(),
|
||||
);
|
||||
if (isIOS) {
|
||||
child = AnnotatedRegion(
|
||||
@@ -108,15 +119,19 @@ class _SSHPageState extends State<SSHPage> {
|
||||
_virtKeysHeight -
|
||||
_media.padding.bottom -
|
||||
_media.padding.top,
|
||||
child: TerminalView(
|
||||
_terminal,
|
||||
controller: _terminalController,
|
||||
keyboardType: _keyboardType,
|
||||
textStyle: _terminalStyle,
|
||||
theme: _terminalTheme,
|
||||
deleteDetection: isIOS,
|
||||
autofocus: true,
|
||||
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: _media.padding.top),
|
||||
child: TerminalView(
|
||||
_terminal,
|
||||
controller: _terminalController,
|
||||
keyboardType: _keyboardType,
|
||||
textStyle: _terminalStyle,
|
||||
theme: _terminalTheme,
|
||||
deleteDetection: isIOS,
|
||||
autofocus: true,
|
||||
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
|
||||
hideScrollBar: isMobile,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -259,13 +274,7 @@ class _SSHPageState extends State<SSHPage> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
AppRoute(
|
||||
SftpPage(
|
||||
widget.spi,
|
||||
initPath: initPath,
|
||||
),
|
||||
'SSH SFTP')
|
||||
.go(context);
|
||||
AppRoute.sftp(spi: widget.spi, initPath: initPath).go(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,31 +338,83 @@ class _SSHPageState extends State<SSHPage> {
|
||||
),
|
||||
);
|
||||
|
||||
_setupDiscontinuityTimer();
|
||||
|
||||
if (_session == null) {
|
||||
showSnackBar(context, const Text('Null session'));
|
||||
return;
|
||||
}
|
||||
|
||||
_terminal.buffer.clear();
|
||||
_terminal.buffer.setCursor(0, 0);
|
||||
|
||||
_terminal.onOutput = (data) {
|
||||
_session.write(utf8.encode(data) as Uint8List);
|
||||
_session?.write(utf8.encode(data) as Uint8List);
|
||||
};
|
||||
_terminal.onResize = (width, height, pixelWidth, pixelHeight) {
|
||||
_session?.resizeTerminal(width, height);
|
||||
};
|
||||
|
||||
_listen(_session.stdout);
|
||||
_listen(_session.stderr);
|
||||
_listen(_session?.stdout);
|
||||
_listen(_session?.stderr);
|
||||
|
||||
if (widget.initCmd != null) {
|
||||
_terminal.textInput(widget.initCmd!);
|
||||
_terminal.keyInput(TerminalKey.enter);
|
||||
}
|
||||
|
||||
await _session.done;
|
||||
await _session?.done;
|
||||
if (mounted) {
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _listen(Stream<Uint8List> stream) {
|
||||
void _listen(Stream<Uint8List>? stream) {
|
||||
if (stream == null) {
|
||||
return;
|
||||
}
|
||||
stream
|
||||
.cast<List<int>>()
|
||||
.transform(const Utf8Decoder())
|
||||
.listen(_terminal.write);
|
||||
}
|
||||
|
||||
void _setupDiscontinuityTimer() {
|
||||
_discontinuityTimer = Timer.periodic(
|
||||
const Duration(seconds: 5),
|
||||
(_) async {
|
||||
var throwTimeout = true;
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (throwTimeout) {
|
||||
_catchTimeout();
|
||||
}
|
||||
});
|
||||
await _client?.ping();
|
||||
throwTimeout = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _catchTimeout() {
|
||||
_discontinuityTimer?.cancel();
|
||||
if (!mounted) return;
|
||||
_write('\n\nConnection lost\r\n');
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text('${_s.disconnected}\n${_s.goBackQ}'),
|
||||
barrierDismiss: false,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (mounted) {
|
||||
context.pop();
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import 'package:toolbox/data/provider/sftp.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/editor.dart';
|
||||
import 'package:toolbox/view/page/storage/sftp.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/picker.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
@@ -22,6 +21,7 @@ import '../../../core/utils/ui.dart';
|
||||
import '../../../data/model/app/path_with_prefix.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/custom_appbar.dart';
|
||||
import '../../widget/fade_in.dart';
|
||||
import 'sftp_mission.dart';
|
||||
|
||||
@@ -64,24 +64,32 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
leading: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
onPressed: () {
|
||||
if (_path != null) {
|
||||
_path!.update('/');
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
title: Text(_s.download),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.downloading),
|
||||
onPressed: () =>
|
||||
AppRoute(const SftpMissionPage(), 'sftp downloading')
|
||||
.go(context),
|
||||
onPressed: () => AppRoute(
|
||||
const SftpMissionPage(),
|
||||
'sftp downloading',
|
||||
).go(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: FadeIn(
|
||||
key: UniqueKey(),
|
||||
child: _buildBody(),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: _buildPath(),
|
||||
child: _wrapPopScope(),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(child: _buildPath()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,13 +99,53 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Divider(),
|
||||
(_path?.path ?? _s.loadingFiles).omitStartStr(),
|
||||
_buildBtns(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBtns() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_path?.update('..');
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final path = await pickOneFile();
|
||||
if (path == null) return;
|
||||
final name = getFileName(path) ?? 'imported';
|
||||
await File(path).copy(pathJoin(_path!.path, name));
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapPopScope() {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (_path == null) return true;
|
||||
if (_path!.canBack) {
|
||||
_path!.update('..');
|
||||
setState(() {});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_path == null) {
|
||||
return const Center(
|
||||
@@ -106,22 +154,10 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
}
|
||||
final dir = Directory(_path!.path);
|
||||
final files = dir.listSync();
|
||||
final canGoBack = _path!.canBack;
|
||||
return ListView.builder(
|
||||
itemCount: canGoBack ? files.length + 1 : files.length,
|
||||
itemCount: files.length,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0 && canGoBack) {
|
||||
return RoundRectCard(ListTile(
|
||||
leading: const Icon(Icons.keyboard_arrow_left),
|
||||
title: const Text('..'),
|
||||
onTap: () {
|
||||
_path!.update('..');
|
||||
setState(() {});
|
||||
},
|
||||
));
|
||||
}
|
||||
index = canGoBack ? index - 1 : index;
|
||||
var file = files[index];
|
||||
var fileName = file.path.split('/').last;
|
||||
var stat = file.statSync();
|
||||
@@ -139,9 +175,13 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
.substring(0, stat.modified.toString().length - 4),
|
||||
style: grey,
|
||||
),
|
||||
onLongPress: () {
|
||||
if (!isDir) return;
|
||||
_showDirActionDialog(file);
|
||||
},
|
||||
onTap: () async {
|
||||
if (!isDir) {
|
||||
await showFileActionDialog(file);
|
||||
await _showFileActionDialog(file);
|
||||
return;
|
||||
}
|
||||
_path!.update(fileName);
|
||||
@@ -152,7 +192,34 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showFileActionDialog(FileSystemEntity file) async {
|
||||
Future<void> _showDirActionDialog(FileSystemEntity file) async {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
_showRenameDialog(file);
|
||||
},
|
||||
title: Text(_s.rename),
|
||||
leading: const Icon(Icons.abc),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
_showDeleteDialog(file);
|
||||
},
|
||||
title: Text(_s.delete),
|
||||
leading: const Icon(Icons.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showFileActionDialog(FileSystemEntity file) async {
|
||||
final fileName = file.path.split('/').last;
|
||||
if (widget.isPickFile) {
|
||||
await showRoundDialog(
|
||||
@@ -189,13 +256,13 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
final f = File(file.absolute.path);
|
||||
final result = await AppRoute(
|
||||
EditorPage(
|
||||
path: file.absolute.path,
|
||||
),
|
||||
'sftp dled editor',
|
||||
).go<String>(context);
|
||||
final f = File(file.absolute.path);
|
||||
if (result != null) {
|
||||
f.writeAsString(result);
|
||||
showSnackBar(context, Text(_s.saved));
|
||||
@@ -208,19 +275,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
title: Text(_s.rename),
|
||||
onTap: () {
|
||||
context.pop();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.rename),
|
||||
child: Input(
|
||||
controller: TextEditingController(text: fileName),
|
||||
onSubmitted: (p0) {
|
||||
context.pop();
|
||||
final newPath = '${file.parent.path}/$p0';
|
||||
file.renameSync(newPath);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
_showRenameDialog(file);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
@@ -228,25 +283,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
title: Text(_s.delete),
|
||||
onTap: () {
|
||||
context.pop();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.delete),
|
||||
child: Text(_s.sureDelete(fileName)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
file.deleteSync();
|
||||
setState(() {});
|
||||
context.pop();
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
_showDeleteDialog(file);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
@@ -274,12 +311,9 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
if (spi == null) {
|
||||
return;
|
||||
}
|
||||
final remotePath = await AppRoute(
|
||||
SftpPage(
|
||||
spi,
|
||||
selectPath: true,
|
||||
),
|
||||
'SFTP page (select)',
|
||||
final remotePath = await AppRoute.sftp(
|
||||
spi: spi,
|
||||
isSelect: true,
|
||||
).go<String>(context);
|
||||
if (remotePath == null) {
|
||||
return;
|
||||
@@ -304,4 +338,56 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRenameDialog(FileSystemEntity file) {
|
||||
final fileName = file.path.split('/').last;
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.rename),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
controller: TextEditingController(text: fileName),
|
||||
onSubmitted: (p0) {
|
||||
context.pop();
|
||||
final newPath = '${file.parent.path}/$p0';
|
||||
try {
|
||||
file.renameSync(newPath);
|
||||
} catch (e) {
|
||||
showSnackBar(context, Text('${_s.failed}:\n$e'));
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteDialog(FileSystemEntity file) {
|
||||
final fileName = file.path.split('/').last;
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.delete),
|
||||
child: Text(_s.sureDelete(fileName)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
try {
|
||||
file.deleteSync(recursive: true);
|
||||
} catch (e) {
|
||||
showSnackBar(context, Text('${_s.failed}:\n$e'));
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/extension/sftpfile.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
@@ -16,7 +17,6 @@ import '../../../core/extension/stringx.dart';
|
||||
import '../../../core/route.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../core/utils/ui.dart';
|
||||
import '../../../data/model/server/server.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/model/sftp/absolute_path.dart';
|
||||
import '../../../data/model/sftp/browser_status.dart';
|
||||
@@ -26,6 +26,7 @@ import '../../../data/provider/sftp.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/custom_appbar.dart';
|
||||
import '../../widget/fade_in.dart';
|
||||
import '../../widget/input_field.dart';
|
||||
import '../../widget/two_line_text.dart';
|
||||
@@ -36,11 +37,11 @@ class SftpPage extends StatefulWidget {
|
||||
final String? initPath;
|
||||
final bool selectPath;
|
||||
|
||||
const SftpPage(
|
||||
this.spi, {
|
||||
const SftpPage({
|
||||
Key? key,
|
||||
required this.spi,
|
||||
required this.selectPath,
|
||||
this.initPath,
|
||||
this.selectPath = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -49,15 +50,15 @@ class SftpPage extends StatefulWidget {
|
||||
|
||||
class _SftpPageState extends State<SftpPage> {
|
||||
final SftpBrowserStatus _status = SftpBrowserStatus();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
final _sftp = locator<SftpProvider>();
|
||||
|
||||
late S _s;
|
||||
|
||||
ServerState? _state;
|
||||
SSHClient? _client;
|
||||
|
||||
final _logger = Logger('SFTP');
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -69,13 +70,21 @@ class _SftpPageState extends State<SftpPage> {
|
||||
super.initState();
|
||||
final serverProvider = locator<ServerProvider>();
|
||||
_client = serverProvider.servers[widget.spi.id]?.client;
|
||||
_state = serverProvider.servers[widget.spi.id]?.state;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: CustomAppBar(
|
||||
leading: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
onPressed: () {
|
||||
if (_status.path != null) {
|
||||
_status.path!.update('/');
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
|
||||
actions: [
|
||||
@@ -88,11 +97,24 @@ class _SftpPageState extends State<SftpPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildFileView(),
|
||||
body: _buildBody(),
|
||||
bottomNavigationBar: _buildBottom(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (_status.path == null || _status.path?.path == '/') {
|
||||
return true;
|
||||
}
|
||||
await _backward();
|
||||
return false;
|
||||
},
|
||||
child: _buildFileView(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottom() {
|
||||
final children = widget.selectPath
|
||||
? [
|
||||
@@ -224,6 +246,8 @@ class _SftpPageState extends State<SftpPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.abc,
|
||||
label: _s.path,
|
||||
onSubmitted: (value) => context.pop(value),
|
||||
),
|
||||
@@ -249,10 +273,6 @@ class _SftpPageState extends State<SftpPage> {
|
||||
}
|
||||
|
||||
Widget _buildFileView() {
|
||||
if (_client == null || _state != ServerState.connected) {
|
||||
return centerLoading;
|
||||
}
|
||||
|
||||
if (_status.isBusy) {
|
||||
return centerLoading;
|
||||
}
|
||||
@@ -264,12 +284,17 @@ class _SftpPageState extends State<SftpPage> {
|
||||
return centerLoading;
|
||||
}
|
||||
|
||||
if (_status.files!.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('~'),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
child: FadeIn(
|
||||
key: Key(widget.spi.name + _status.path!.path),
|
||||
child: ListView.builder(
|
||||
itemCount: _status.files!.length,
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||
itemBuilder: (_, index) => _buildItem(_status.files![index]),
|
||||
),
|
||||
@@ -367,7 +392,7 @@ class _SftpPageState extends State<SftpPage> {
|
||||
SftpReqType.download,
|
||||
);
|
||||
_sftp.add(req, completer: completer);
|
||||
showRoundDialog(context: context, child: centerSizedLoading);
|
||||
showLoadingDialog(context);
|
||||
await completer.future;
|
||||
context.pop();
|
||||
|
||||
@@ -430,11 +455,7 @@ class _SftpPageState extends State<SftpPage> {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
context.pop();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: centerSizedLoading,
|
||||
barrierDismiss: false,
|
||||
);
|
||||
showLoadingDialog(context);
|
||||
final remotePath = _getRemotePath(file);
|
||||
try {
|
||||
if (file.attr.isDirectory) {
|
||||
@@ -460,10 +481,7 @@ class _SftpPageState extends State<SftpPage> {
|
||||
}
|
||||
_listDir();
|
||||
},
|
||||
child: Text(
|
||||
_s.delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
child: Text(_s.delete, style: textRed),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -476,6 +494,8 @@ class _SftpPageState extends State<SftpPage> {
|
||||
context: context,
|
||||
title: Text(_s.createFolder),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.folder,
|
||||
controller: textController,
|
||||
label: _s.name,
|
||||
),
|
||||
@@ -504,10 +524,7 @@ class _SftpPageState extends State<SftpPage> {
|
||||
context.pop();
|
||||
_listDir();
|
||||
},
|
||||
child: Text(
|
||||
_s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
child: Text(_s.ok, style: textRed),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -520,6 +537,8 @@ class _SftpPageState extends State<SftpPage> {
|
||||
context: context,
|
||||
title: Text(_s.createFile),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.insert_drive_file,
|
||||
controller: textController,
|
||||
label: _s.name,
|
||||
),
|
||||
@@ -550,10 +569,7 @@ class _SftpPageState extends State<SftpPage> {
|
||||
context.pop();
|
||||
_listDir();
|
||||
},
|
||||
child: Text(
|
||||
_s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
child: Text(_s.ok, style: textRed),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -566,6 +582,8 @@ class _SftpPageState extends State<SftpPage> {
|
||||
context: context,
|
||||
title: Text(_s.rename),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.abc,
|
||||
controller: textController,
|
||||
label: _s.name,
|
||||
),
|
||||
@@ -591,10 +609,7 @@ class _SftpPageState extends State<SftpPage> {
|
||||
context.pop();
|
||||
_listDir();
|
||||
},
|
||||
child: Text(
|
||||
_s.rename,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
child: Text(_s.rename, style: textRed),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -619,29 +634,47 @@ class _SftpPageState extends State<SftpPage> {
|
||||
_status.client = sftpc;
|
||||
}
|
||||
try {
|
||||
final fs =
|
||||
await _status.client!.listdir(path ?? _status.path?.path ?? '/');
|
||||
final listPath = path ?? _status.path?.path ?? '/';
|
||||
final fs = await _status.client!.listdir(listPath);
|
||||
fs.sort((a, b) => a.filename.compareTo(b.filename));
|
||||
fs.removeAt(0);
|
||||
|
||||
/// Issue #97
|
||||
/// In order to compatible with the Synology NAS
|
||||
/// which not has '.' and '..' in listdir
|
||||
if (fs.isNotEmpty && fs.first.filename == '.') {
|
||||
fs.removeAt(0);
|
||||
}
|
||||
|
||||
/// Issue #96
|
||||
/// Due to [WillPopScope] added in this page
|
||||
/// There is no need to keep '..' folder in listdir
|
||||
/// So remove it
|
||||
if (fs.isNotEmpty && fs.first.filename == '..') {
|
||||
fs.removeAt(0);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_status.files = fs;
|
||||
_status.isBusy = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.error),
|
||||
child: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.ok),
|
||||
)
|
||||
],
|
||||
);
|
||||
} catch (e, trace) {
|
||||
_logger.warning('list dir failed', e, trace);
|
||||
await _backward();
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 177),
|
||||
() => showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.error),
|
||||
child: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.ok),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../../core/utils/ui.dart';
|
||||
import '../../../data/model/sftp/req.dart';
|
||||
import '../../../data/provider/sftp.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/custom_appbar.dart';
|
||||
import '../../widget/round_rect_card.dart';
|
||||
|
||||
class SftpMissionPage extends StatefulWidget {
|
||||
@@ -34,11 +35,8 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_s.mission,
|
||||
style: textSize18,
|
||||
),
|
||||
appBar: CustomAppBar(
|
||||
title: Text(_s.mission, style: textSize18),
|
||||
),
|
||||
body: _buildBody(),
|
||||
);
|
||||
@@ -48,7 +46,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
return Consumer<SftpProvider>(builder: (__, pro, _) {
|
||||
if (pro.status.isEmpty) {
|
||||
return Center(
|
||||
child: Text(_s.sftpNoDownloadTask),
|
||||
child: Text(_s.noTask),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
|
||||
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 String? errorText;
|
||||
final Widget? prefix;
|
||||
final bool autoFocus;
|
||||
|
||||
const Input({
|
||||
super.key,
|
||||
@@ -36,6 +37,7 @@ class Input extends StatelessWidget {
|
||||
this.suggestiion = false,
|
||||
this.errorText,
|
||||
this.prefix,
|
||||
this.autoFocus = false,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -49,6 +51,7 @@ class Input extends StatelessWidget {
|
||||
onChanged: onChanged,
|
||||
keyboardType: type,
|
||||
focusNode: node,
|
||||
autofocus: autoFocus,
|
||||
autocorrect: autoCorrect,
|
||||
enableSuggestions: suggestiion,
|
||||
decoration: InputDecoration(
|
||||
@@ -57,7 +60,7 @@ class Input extends StatelessWidget {
|
||||
icon: icon != null ? Icon(icon) : null,
|
||||
border: InputBorder.none,
|
||||
errorText: errorText,
|
||||
prefix: prefix),
|
||||
prefix: prefix,),
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
),
|
||||
|
||||