diff --git a/README.md b/README.md
index 752a3ccc..f0edbc03 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ English | [简体中文](README_zh.md)
dartss
Please only download pkgs from the source that **you trust**!
-## 🔖 Feature
+## 🔖 Features
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`, `S.M.A.R.T`...
- Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`...
@@ -61,7 +61,7 @@ Before you open an issue, please read the following:
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
-## 🧱 Contribution
+## 🧱 Contributions
Any positive contribution is welcome.
diff --git a/README_zh.md b/README_zh.md
index 0bfeddfc..fa887ea1 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -10,7 +10,7 @@
-使用 Flutter 开发的 Linux 服务器工具箱,提供服务器状态图表和管理工具。
+使用 Flutter 开发的 Linux, Unix, Windows 服务器工具箱,提供服务器状态图表和管理工具。
特别感谢 dartssh2 & xterm.dart 。
diff --git a/coverage/lcov.info b/coverage/lcov.info
new file mode 100644
index 00000000..f56d4b6a
--- /dev/null
+++ b/coverage/lcov.info
@@ -0,0 +1,6505 @@
+SF:lib/data/model/app/shell_func.dart
+DA:32,3
+DA:38,1
+DA:39,1
+DA:42,1
+DA:43,2
+DA:44,2
+DA:47,0
+DA:48,0
+DA:49,0
+DA:50,0
+DA:51,0
+DA:52,0
+DA:55,1
+DA:56,1
+DA:57,1
+DA:58,1
+DA:59,1
+DA:62,1
+DA:63,1
+DA:64,1
+DA:65,1
+DA:67,2
+DA:69,1
+DA:72,1
+DA:73,1
+DA:74,1
+DA:75,1
+DA:76,1
+DA:77,1
+DA:81,1
+DA:82,1
+DA:83,1
+DA:84,1
+DA:86,1
+DA:92,1
+DA:93,1
+DA:94,1
+DA:96,1
+DA:159,0
+DA:160,0
+DA:161,0
+DA:162,0
+DA:163,0
+DA:164,0
+DA:165,0
+DA:166,0
+DA:239,1
+DA:240,2
+LF:48
+LH:34
+end_of_record
+SF:lib/data/model/server/server_status_update_req.dart
+DA:26,1
+DA:34,1
+DA:35,1
+DA:36,1
+DA:37,1
+DA:38,2
+DA:44,0
+DA:45,0
+DA:47,0
+DA:48,0
+DA:51,0
+DA:52,0
+DA:54,0
+DA:58,0
+DA:60,0
+DA:63,0
+DA:67,0
+DA:69,0
+DA:72,0
+DA:76,0
+DA:77,0
+DA:78,0
+DA:79,0
+DA:80,0
+DA:82,0
+DA:86,0
+DA:87,0
+DA:88,0
+DA:91,0
+DA:95,0
+DA:97,0
+DA:100,0
+DA:104,0
+DA:105,0
+DA:107,0
+DA:111,0
+DA:113,0
+DA:117,0
+DA:119,0
+DA:122,0
+DA:126,0
+DA:128,0
+DA:132,0
+DA:133,0
+DA:135,0
+DA:139,0
+DA:140,0
+DA:142,0
+DA:146,0
+DA:148,0
+DA:152,0
+DA:154,0
+DA:158,0
+DA:161,0
+DA:162,0
+DA:163,0
+DA:164,0
+DA:167,0
+DA:171,0
+DA:172,0
+DA:173,0
+DA:174,0
+DA:177,0
+DA:181,0
+DA:182,0
+DA:183,0
+DA:184,0
+DA:187,0
+DA:190,0
+DA:194,0
+DA:195,0
+DA:198,0
+DA:199,0
+DA:200,0
+DA:202,0
+DA:206,0
+DA:208,0
+DA:212,0
+DA:214,0
+DA:218,0
+DA:220,0
+DA:224,0
+DA:226,0
+DA:229,0
+DA:233,0
+DA:235,0
+DA:237,0
+DA:245,0
+DA:246,0
+DA:247,0
+DA:248,0
+DA:249,0
+DA:251,0
+DA:254,0
+DA:257,0
+DA:258,0
+DA:259,0
+DA:261,0
+DA:262,0
+DA:263,0
+DA:264,0
+DA:271,0
+DA:272,0
+DA:273,0
+DA:278,0
+DA:288,0
+DA:289,0
+DA:290,0
+DA:291,0
+DA:296,1
+DA:297,1
+DA:298,1
+DA:303,1
+DA:304,1
+DA:305,2
+DA:306,3
+DA:309,1
+DA:310,1
+DA:311,1
+DA:312,1
+DA:313,1
+DA:314,1
+DA:315,1
+DA:316,1
+DA:317,1
+DA:318,1
+DA:319,1
+DA:320,1
+DA:321,5
+DA:323,1
+DA:327,1
+DA:329,1
+DA:330,1
+DA:331,1
+DA:332,1
+DA:333,1
+DA:334,0
+DA:335,0
+DA:336,0
+DA:337,0
+DA:341,0
+DA:346,1
+DA:348,1
+DA:349,1
+DA:350,3
+DA:353,0
+DA:358,1
+DA:360,2
+DA:362,3
+DA:365,3
+DA:370,1
+DA:373,1
+DA:374,1
+DA:375,1
+DA:376,1
+DA:377,1
+DA:378,2
+DA:379,1
+DA:380,3
+DA:385,1
+DA:386,2
+DA:387,4
+DA:388,5
+DA:391,3
+DA:396,1
+DA:398,1
+DA:399,1
+DA:400,1
+DA:401,1
+DA:402,1
+DA:403,1
+DA:405,2
+DA:409,3
+DA:414,1
+DA:416,1
+DA:417,2
+DA:418,1
+DA:419,2
+DA:420,3
+DA:423,3
+DA:428,1
+DA:430,2
+DA:432,3
+DA:435,3
+DA:440,1
+DA:442,1
+DA:443,2
+DA:444,1
+DA:445,3
+DA:448,3
+DA:453,1
+DA:455,1
+DA:456,2
+DA:458,3
+DA:461,3
+DA:466,1
+DA:468,1
+DA:469,2
+DA:470,1
+DA:471,3
+DA:472,1
+DA:473,3
+DA:477,3
+DA:482,1
+DA:484,1
+DA:485,2
+DA:486,3
+DA:489,3
+DA:494,1
+DA:496,3
+DA:498,3
+DA:502,4
+DA:504,3
+DA:509,1
+DA:511,1
+DA:512,1
+DA:514,2
+DA:516,2
+DA:518,1
+DA:519,1
+DA:524,1
+DA:525,0
+DA:526,0
+DA:528,1
+DA:529,1
+DA:541,1
+DA:545,0
+DA:547,0
+DA:548,0
+DA:551,0
+DA:552,0
+DA:553,0
+DA:555,0
+DA:556,0
+DA:558,0
+DA:559,0
+DA:560,0
+DA:562,0
+DA:563,0
+DA:564,0
+DA:565,0
+DA:567,0
+DA:568,0
+DA:569,0
+DA:570,0
+DA:576,0
+DA:577,0
+DA:578,0
+DA:580,0
+DA:581,0
+DA:583,0
+DA:584,0
+DA:594,0
+DA:598,0
+DA:601,0
+DA:602,0
+DA:605,1
+DA:607,1
+DA:608,0
+DA:611,0
+DA:612,0
+DA:614,0
+DA:615,0
+DA:617,0
+DA:618,0
+DA:619,0
+DA:621,0
+DA:622,0
+DA:623,0
+DA:624,0
+DA:626,0
+DA:627,0
+DA:628,0
+DA:629,0
+DA:636,0
+DA:637,0
+DA:638,0
+DA:639,0
+DA:640,0
+DA:642,0
+DA:643,0
+DA:656,1
+DA:660,0
+DA:663,0
+DA:664,0
+DA:667,1
+DA:670,1
+DA:671,1
+DA:672,1
+DA:676,1
+DA:677,0
+DA:680,0
+DA:681,0
+DA:683,0
+DA:684,0
+DA:685,0
+DA:686,0
+DA:690,0
+DA:693,0
+DA:697,0
+DA:698,0
+LF:301
+LH:123
+end_of_record
+SF:lib/data/model/server/system.dart
+DA:22,0
+DA:24,0
+DA:25,0
+DA:26,0
+DA:28,0
+DA:29,0
+DA:32,0
+DA:33,0
+DA:38,0
+DA:39,0
+DA:43,0
+DA:44,0
+DA:50,0
+DA:56,3
+DA:58,1
+DA:60,1
+DA:61,0
+DA:62,1
+DA:63,0
+DA:64,1
+DA:65,1
+LF:21
+LH:6
+end_of_record
+SF:lib/data/res/status.dart
+DA:11,1
+DA:12,1
+DA:13,1
+DA:14,5
+DA:15,1
+DA:16,3
+DA:17,1
+DA:18,5
+DA:19,2
+DA:20,1
+DA:22,1
+DA:23,1
+DA:27,1
+DA:28,1
+DA:29,1
+DA:33,1
+DA:36,1
+DA:37,3
+LF:18
+LH:18
+end_of_record
+SF:lib/data/model/server/disk.dart
+DA:30,3
+DA:44,2
+DA:45,2
+DA:46,2
+DA:48,2
+DA:50,2
+DA:51,2
+DA:53,4
+DA:55,2
+DA:59,1
+DA:62,0
+DA:68,2
+DA:69,2
+DA:71,2
+DA:76,3
+DA:77,4
+DA:78,4
+DA:79,4
+DA:82,3
+DA:83,1
+DA:85,1
+DA:92,1
+DA:93,2
+DA:94,2
+DA:95,2
+DA:97,1
+DA:101,1
+DA:105,2
+DA:106,3
+DA:108,2
+DA:109,3
+DA:111,2
+DA:112,3
+DA:115,2
+DA:116,1
+DA:117,1
+DA:119,2
+DA:120,2
+DA:121,2
+DA:123,1
+DA:138,2
+DA:139,4
+DA:140,4
+DA:143,4
+DA:145,2
+DA:148,4
+DA:149,4
+DA:150,2
+DA:152,2
+DA:157,6
+DA:158,4
+DA:159,6
+DA:161,4
+DA:162,6
+DA:164,4
+DA:165,6
+DA:168,4
+DA:169,2
+DA:170,2
+DA:172,4
+DA:173,4
+DA:174,4
+DA:176,2
+DA:189,1
+DA:192,0
+DA:193,0
+DA:201,1
+DA:202,1
+DA:203,1
+DA:204,2
+DA:206,2
+DA:207,1
+DA:210,2
+DA:211,2
+DA:212,0
+DA:215,1
+DA:216,0
+DA:220,1
+DA:221,1
+DA:222,1
+DA:223,1
+DA:224,1
+DA:227,3
+DA:228,4
+DA:229,4
+DA:230,4
+DA:240,0
+DA:241,0
+DA:242,0
+DA:243,0
+DA:244,0
+DA:245,0
+DA:246,0
+DA:247,0
+DA:248,0
+DA:249,0
+DA:250,0
+DA:251,0
+DA:252,0
+DA:257,1
+DA:259,1
+DA:261,2
+DA:264,0
+DA:267,0
+DA:268,0
+DA:272,0
+DA:273,0
+DA:276,0
+DA:277,0
+DA:278,0
+DA:279,0
+DA:280,0
+DA:284,0
+DA:285,0
+DA:287,0
+DA:288,0
+DA:293,1
+DA:294,2
+DA:296,0
+DA:299,0
+DA:300,0
+DA:301,0
+DA:302,0
+DA:303,0
+DA:304,0
+DA:307,0
+DA:308,0
+DA:309,0
+DA:312,0
+DA:313,0
+DA:317,0
+DA:318,0
+DA:319,0
+DA:320,0
+DA:321,0
+DA:322,0
+DA:323,0
+DA:324,0
+DA:325,0
+DA:327,0
+DA:328,0
+DA:329,0
+DA:330,0
+DA:332,0
+DA:333,0
+DA:351,0
+DA:353,0
+DA:354,0
+DA:361,3
+DA:363,1
+DA:365,3
+DA:366,4
+DA:370,3
+DA:372,3
+DA:373,3
+DA:374,6
+DA:375,9
+DA:378,9
+DA:379,3
+DA:380,3
+DA:381,6
+DA:382,6
+DA:384,3
+DA:388,3
+DA:393,3
+DA:395,3
+DA:396,3
+DA:404,6
+LF:168
+LH:106
+end_of_record
+SF:lib/data/model/server/time_seq.dart
+DA:8,2
+DA:10,1
+DA:12,2
+DA:13,4
+DA:14,0
+DA:16,2
+DA:19,1
+DA:20,2
+DA:22,2
+DA:24,0
+DA:26,0
+DA:29,0
+DA:31,0
+DA:34,0
+DA:36,0
+DA:43,3
+DA:45,1
+DA:46,4
+DA:49,1
+DA:50,4
+DA:55,1
+DA:56,1
+DA:58,5
+DA:59,0
+DA:60,0
+DA:63,1
+LF:26
+LH:17
+end_of_record
+SF:lib/data/res/misc.dart
+DA:4,3
+DA:5,3
+DA:8,0
+LF:3
+LH:2
+end_of_record
+SF:lib/data/model/container/image.dart
+DA:13,0
+DA:27,0
+DA:29,0
+DA:30,0
+DA:32,0
+DA:33,0
+DA:35,0
+DA:37,0
+DA:39,0
+DA:40,0
+DA:41,0
+DA:42,0
+DA:43,0
+DA:44,0
+DA:45,0
+DA:48,0
+DA:49,0
+DA:50,0
+DA:51,0
+DA:52,0
+DA:53,0
+DA:54,0
+DA:69,0
+DA:78,0
+DA:79,0
+DA:81,0
+DA:82,0
+DA:84,0
+DA:86,0
+DA:88,0
+DA:89,0
+DA:90,0
+DA:91,0
+DA:93,0
+DA:94,0
+DA:95,0
+DA:96,0
+DA:98,0
+DA:99,0
+DA:100,0
+DA:101,0
+DA:103,0
+DA:105,0
+DA:106,0
+DA:109,0
+DA:113,0
+DA:114,0
+DA:115,0
+DA:116,0
+DA:117,0
+DA:118,0
+DA:119,0
+LF:52
+LH:0
+end_of_record
+SF:lib/data/model/container/type.dart
+DA:8,0
+DA:9,0
+DA:10,0
+DA:13,0
+DA:14,0
+DA:15,0
+LF:6
+LH:0
+end_of_record
+SF:lib/data/model/container/ps.dart
+DA:20,0
+DA:45,0
+DA:47,0
+DA:48,0
+DA:50,0
+DA:51,0
+DA:53,0
+DA:54,0
+DA:56,0
+DA:58,0
+DA:59,0
+DA:60,0
+DA:61,0
+DA:62,0
+DA:63,0
+DA:64,0
+DA:65,0
+DA:66,0
+DA:67,0
+DA:68,0
+DA:69,0
+DA:70,0
+DA:73,0
+DA:75,0
+DA:77,0
+DA:78,0
+DA:79,0
+DA:80,0
+DA:81,0
+DA:82,0
+DA:83,0
+DA:84,0
+DA:87,0
+DA:88,0
+DA:89,0
+DA:90,0
+DA:91,0
+DA:92,0
+DA:93,0
+DA:94,0
+DA:115,1
+DA:117,0
+DA:118,0
+DA:120,0
+DA:123,1
+DA:125,3
+DA:129,0
+DA:131,0
+DA:132,0
+DA:133,0
+DA:134,0
+DA:135,0
+DA:140,1
+DA:141,2
+DA:142,6
+LF:55
+LH:6
+end_of_record
+SF:lib/core/extension/context/locale.dart
+DA:5,0
+DA:8,0
+LF:2
+LH:0
+end_of_record
+SF:lib/core/extension/ssh_client.dart
+DA:18,0
+DA:27,0
+DA:33,0
+DA:34,0
+DA:35,0
+DA:37,0
+DA:38,0
+DA:39,0
+DA:40,0
+DA:42,0
+DA:43,0
+DA:46,0
+DA:47,0
+DA:48,0
+DA:49,0
+DA:51,0
+DA:52,0
+DA:55,0
+DA:57,0
+DA:58,0
+DA:60,0
+DA:63,0
+DA:74,0
+DA:77,0
+DA:84,0
+DA:85,0
+DA:86,0
+DA:88,0
+DA:89,0
+DA:90,0
+DA:91,0
+DA:93,0
+DA:94,0
+DA:97,0
+DA:98,0
+DA:99,0
+DA:100,0
+DA:102,0
+DA:103,0
+DA:106,0
+DA:108,0
+DA:109,0
+DA:111,0
+DA:114,0
+DA:123,0
+DA:124,0
+DA:125,0
+DA:126,0
+DA:128,0
+DA:129,0
+DA:132,0
+DA:134,0
+DA:136,0
+DA:137,0
+DA:138,0
+DA:140,0
+DA:148,0
+DA:151,0
+DA:159,0
+DA:160,0
+DA:161,0
+DA:162,0
+LF:62
+LH:0
+end_of_record
+SF:lib/core/sync.dart
+DA:9,0
+DA:12,6
+DA:14,0
+DA:15,0
+DA:17,0
+DA:19,0
+DA:20,0
+DA:23,0
+DA:25,0
+DA:26,0
+DA:28,0
+DA:29,0
+LF:12
+LH:1
+end_of_record
+SF:lib/data/model/app/bak/backup2.dart
+DA:13,0
+DA:17,0
+DA:41,0
+DA:43,0
+DA:45,0
+DA:48,0
+DA:49,0
+DA:50,0
+DA:51,0
+DA:52,0
+DA:53,0
+DA:56,0
+DA:57,0
+DA:59,0
+DA:64,0
+DA:65,0
+DA:67,0
+DA:68,0
+DA:69,0
+DA:70,0
+DA:71,0
+DA:72,0
+DA:73,0
+DA:77,0
+DA:78,0
+DA:79,0
+DA:81,0
+DA:82,0
+DA:85,0
+DA:86,0
+DA:90,0
+DA:91,0
+DA:92,0
+DA:93,0
+DA:95,0
+DA:98,0
+DA:99,0
+LF:37
+LH:0
+end_of_record
+SF:lib/data/model/app/bak/backup2.g.dart
+DA:9,0
+DA:10,0
+DA:11,0
+DA:12,0
+DA:13,0
+DA:14,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:20,0
+DA:21,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:25,0
+DA:26,0
+DA:27,0
+DA:28,0
+LF:18
+LH:0
+end_of_record
+SF:lib/data/model/app/bak/utils.dart
+DA:6,0
+DA:8,0
+DA:9,0
+DA:11,0
+DA:12,0
+LF:5
+LH:0
+end_of_record
+SF:lib/core/utils/server.dart
+DA:15,0
+DA:16,0
+DA:20,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:29,0
+DA:30,0
+DA:32,0
+DA:34,0
+DA:37,0
+DA:56,0
+DA:60,0
+DA:62,0
+DA:66,0
+DA:67,0
+DA:69,0
+DA:71,0
+DA:76,0
+DA:78,0
+DA:79,0
+DA:81,0
+DA:83,0
+DA:85,0
+DA:89,0
+DA:91,0
+DA:93,0
+DA:94,0
+DA:96,0
+DA:97,0
+DA:103,0
+DA:105,0
+DA:106,0
+DA:108,0
+DA:110,0
+LF:35
+LH:0
+end_of_record
+SF:lib/data/model/app/error.dart
+DA:7,0
+DA:9,0
+DA:10,0
+DA:11,0
+DA:12,0
+DA:13,0
+DA:14,0
+DA:32,0
+DA:34,0
+DA:41,0
+DA:43,0
+DA:50,0
+DA:52,0
+DA:59,0
+DA:61,0
+LF:15
+LH:0
+end_of_record
+SF:lib/data/model/server/server_private_info.dart
+DA:24,0
+DA:53,0
+DA:55,0
+DA:56,0
+DA:58,0
+DA:59,0
+DA:67,0
+DA:70,0
+DA:77,0
+DA:78,0
+DA:79,0
+DA:80,0
+DA:81,0
+DA:82,0
+DA:85,0
+DA:87,0
+DA:88,0
+DA:90,0
+DA:91,0
+DA:92,0
+DA:93,0
+DA:94,0
+DA:95,0
+DA:96,0
+DA:97,0
+DA:98,0
+DA:101,0
+DA:102,0
+DA:103,0
+DA:105,0
+DA:106,0
+DA:107,0
+DA:109,0
+DA:110,0
+DA:111,0
+DA:112,0
+DA:114,0
+DA:115,0
+DA:116,0
+DA:117,0
+DA:125,0
+DA:132,0
+DA:136,0
+DA:139,0
+DA:146,0
+LF:45
+LH:0
+end_of_record
+SF:lib/data/model/server/server_private_info.g.dart
+DA:9,0
+DA:10,0
+DA:11,0
+DA:12,0
+DA:13,0
+DA:14,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:18,0
+DA:19,0
+DA:20,0
+DA:22,0
+DA:23,0
+DA:25,0
+DA:26,0
+DA:27,0
+DA:29,0
+DA:30,0
+DA:32,0
+DA:36,0
+DA:37,0
+DA:38,0
+DA:39,0
+DA:40,0
+DA:41,0
+DA:42,0
+DA:43,0
+DA:44,0
+DA:45,0
+DA:46,0
+DA:47,0
+DA:48,0
+DA:49,0
+DA:50,0
+DA:51,0
+DA:52,0
+LF:37
+LH:0
+end_of_record
+SF:lib/data/res/store.dart
+DA:10,0
+DA:11,0
+DA:12,0
+DA:13,0
+DA:14,0
+DA:15,0
+DA:18,0
+DA:19,0
+DA:20,0
+DA:21,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:27,0
+DA:28,0
+DA:31,0
+DA:33,0
+DA:34,0
+DA:39,0
+DA:40,0
+DA:41,0
+DA:45,0
+LF:22
+LH:0
+end_of_record
+SF:lib/core/utils/ssh_auth.dart
+DA:9,0
+DA:14,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:19,0
+LF:6
+LH:0
+end_of_record
+SF:lib/data/provider/app.dart
+DA:17,0
+DA:22,0
+DA:23,0
+LF:3
+LH:0
+end_of_record
+SF:lib/data/provider/app.g.dart
+DA:9,0
+DA:13,0
+LF:2
+LH:0
+end_of_record
+SF:lib/data/helper/system_detector.dart
+DA:16,0
+DA:21,0
+DA:23,0
+DA:29,0
+DA:30,0
+DA:31,0
+DA:33,0
+DA:38,0
+DA:39,0
+DA:41,0
+DA:43,0
+DA:45,0
+DA:49,0
+DA:54,0
+LF:14
+LH:0
+end_of_record
+SF:lib/data/model/app/bak/backup.dart
+DA:17,0
+DA:32,0
+DA:44,0
+DA:46,0
+DA:48,0
+DA:49,0
+DA:50,0
+DA:52,0
+DA:53,0
+DA:54,0
+DA:55,0
+DA:56,0
+DA:58,0
+DA:59,0
+DA:63,0
+DA:64,0
+DA:65,0
+DA:66,0
+DA:67,0
+DA:71,0
+DA:73,0
+DA:74,0
+DA:75,0
+DA:77,0
+DA:83,0
+DA:84,0
+DA:87,0
+DA:88,0
+DA:89,0
+DA:90,0
+DA:91,0
+DA:92,0
+DA:93,0
+DA:95,0
+DA:96,0
+DA:98,0
+DA:99,0
+DA:105,0
+DA:106,0
+DA:109,0
+DA:110,0
+DA:111,0
+DA:112,0
+DA:113,0
+DA:114,0
+DA:115,0
+DA:117,0
+DA:118,0
+DA:120,0
+DA:121,0
+DA:127,0
+DA:128,0
+DA:131,0
+DA:132,0
+DA:133,0
+DA:134,0
+DA:135,0
+DA:136,0
+DA:137,0
+DA:139,0
+DA:140,0
+DA:142,0
+DA:143,0
+DA:149,0
+DA:151,0
+DA:152,0
+DA:153,0
+DA:154,0
+DA:155,0
+DA:156,0
+DA:157,0
+DA:159,0
+DA:160,0
+DA:162,0
+DA:163,0
+DA:169,0
+DA:171,0
+DA:172,0
+DA:173,0
+DA:174,0
+DA:175,0
+DA:176,0
+DA:177,0
+DA:179,0
+DA:180,0
+DA:182,0
+DA:183,0
+DA:188,0
+DA:191,0
+DA:193,0
+DA:194,0
+DA:195,0
+DA:196,0
+DA:197,0
+DA:198,0
+DA:199,0
+DA:201,0
+DA:202,0
+DA:204,0
+DA:205,0
+DA:210,0
+DA:211,0
+DA:213,0
+DA:216,0
+DA:219,0
+DA:221,0
+DA:223,0
+DA:224,0
+DA:225,0
+DA:226,0
+DA:228,0
+DA:230,0
+LF:112
+LH:0
+end_of_record
+SF:lib/data/model/app/bak/backup.g.dart
+DA:9,0
+DA:10,0
+DA:11,0
+DA:12,0
+DA:13,0
+DA:14,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:18,0
+DA:19,0
+DA:20,0
+DA:21,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:27,0
+DA:28,0
+DA:29,0
+DA:30,0
+DA:31,0
+DA:32,0
+DA:33,0
+DA:34,0
+DA:35,0
+DA:36,0
+LF:26
+LH:0
+end_of_record
+SF:lib/data/model/server/private_key_info.dart
+DA:11,0
+DA:13,0
+DA:15,0
+DA:17,0
+DA:18,0
+DA:19,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:27,0
+LF:10
+LH:0
+end_of_record
+SF:lib/data/model/server/private_key_info.g.dart
+DA:9,0
+DA:10,0
+DA:11,0
+DA:12,0
+DA:15,0
+DA:16,0
+LF:6
+LH:0
+end_of_record
+SF:lib/data/model/server/snippet.dart
+DA:23,0
+DA:35,0
+DA:37,0
+DA:38,0
+DA:39,0
+DA:40,0
+DA:41,0
+DA:47,0
+DA:48,0
+DA:49,0
+DA:52,0
+DA:53,0
+DA:54,0
+DA:59,0
+DA:60,0
+DA:61,0
+DA:66,0
+DA:67,0
+DA:68,0
+DA:69,0
+DA:70,0
+DA:75,0
+DA:76,0
+DA:80,0
+DA:81,0
+DA:82,0
+DA:83,0
+DA:86,0
+DA:88,0
+DA:89,0
+DA:92,0
+DA:94,0
+DA:97,0
+DA:102,0
+DA:103,0
+DA:108,0
+DA:109,0
+DA:112,0
+DA:115,0
+DA:121,0
+DA:122,0
+DA:123,0
+DA:127,0
+DA:130,0
+DA:131,0
+DA:132,0
+DA:134,0
+DA:137,0
+DA:140,0
+DA:141,0
+DA:144,0
+DA:145,0
+DA:146,0
+DA:147,0
+DA:148,0
+DA:149,0
+DA:150,0
+DA:154,0
+DA:162,0
+DA:168,0
+DA:176,0
+DA:177,0
+DA:179,0
+DA:180,0
+DA:183,0
+DA:184,0
+DA:185,0
+DA:186,0
+LF:68
+LH:0
+end_of_record
+SF:lib/data/model/server/snippet.g.dart
+DA:9,0
+DA:10,0
+DA:11,0
+DA:12,0
+DA:13,0
+DA:14,0
+DA:15,0
+DA:16,0
+DA:19,0
+DA:20,0
+DA:21,0
+DA:22,0
+DA:23,0
+DA:24,0
+LF:14
+LH:0
+end_of_record
+SF:lib/data/model/app/menu/server_func.dart
+DA:21,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:25,0
+DA:26,0
+DA:27,0
+DA:32,0
+DA:40,0
+DA:42,0
+DA:43,0
+DA:44,0
+DA:46,0
+DA:47,0
+DA:48,0
+DA:49,0
+DA:50,0
+DA:53,0
+DA:54,0
+DA:55,0
+DA:57,0
+DA:58,0
+DA:59,0
+DA:60,0
+DA:61,0
+LF:25
+LH:0
+end_of_record
+SF:lib/data/model/app/net_view.dart
+DA:10,0
+DA:11,0
+DA:12,0
+DA:13,0
+DA:16,0
+DA:17,0
+DA:18,0
+DA:19,0
+DA:24,0
+DA:25,0
+DA:28,0
+DA:29,0
+DA:30,0
+DA:32,0
+DA:34,0
+DA:35,0
+DA:37,0
+DA:39,0
+DA:42,0
+DA:47,0
+DA:48,0
+DA:49,0
+DA:50,0
+DA:53,0
+DA:54,0
+DA:55,0
+LF:26
+LH:0
+end_of_record
+SF:lib/data/model/server/server.dart
+DA:24,0
+DA:26,0
+DA:28,0
+DA:30,0
+DA:53,1
+DA:84,0
+LF:6
+LH:1
+end_of_record
+SF:lib/data/model/app/script_builders.dart
+DA:6,6
+DA:29,6
+DA:31,1
+DA:34,1
+DA:36,1
+DA:41,1
+DA:43,2
+DA:46,1
+DA:51,2
+DA:52,6
+DA:57,1
+DA:59,1
+DA:60,1
+DA:69,2
+DA:70,1
+DA:72,1
+DA:73,1
+DA:74,7
+DA:77,1
+DA:81,1
+DA:84,2
+DA:85,1
+DA:86,2
+DA:87,1
+DA:89,1
+DA:93,1
+DA:96,1
+DA:97,5
+DA:98,1
+DA:99,1
+DA:100,1
+DA:101,1
+DA:108,6
+DA:110,0
+DA:113,0
+DA:119,0
+DA:122,0
+DA:124,0
+DA:127,0
+DA:132,0
+DA:133,0
+DA:138,0
+DA:140,0
+DA:141,0
+DA:161,0
+DA:162,0
+DA:163,0
+DA:164,0
+DA:165,0
+DA:169,0
+DA:173,0
+DA:174,0
+DA:175,0
+DA:176,0
+DA:177,0
+DA:179,0
+DA:181,0
+DA:186,0
+DA:189,0
+DA:191,0
+DA:194,0
+DA:196,0
+DA:197,0
+DA:198,0
+DA:210,0
+DA:217,0
+DA:224,0
+DA:237,0
+DA:239,1
+LF:69
+LH:34
+end_of_record
+SF:lib/data/model/app/server_detail_card.dart
+DA:28,0
+DA:29,0
+DA:31,0
+DA:33,0
+DA:34,0
+DA:35,0
+DA:36,0
+DA:37,0
+DA:38,0
+DA:39,0
+DA:40,0
+DA:41,0
+DA:42,0
+DA:43,0
+DA:44,0
+DA:45,0
+DA:46,0
+DA:52,0
+DA:53,0
+DA:54,0
+DA:55,0
+DA:56,0
+DA:57,0
+DA:58,0
+DA:62,0
+DA:63,0
+DA:64,0
+DA:65,0
+DA:66,0
+DA:67,0
+LF:30
+LH:0
+end_of_record
+SF:lib/data/provider/server.dart
+DA:24,6
+DA:27,3
+DA:28,0
+DA:29,0
+DA:30,0
+DA:34,0
+DA:36,0
+DA:38,0
+DA:40,0
+DA:43,0
+DA:44,0
+DA:45,0
+DA:47,0
+DA:48,0
+DA:49,0
+DA:50,0
+DA:54,0
+DA:55,0
+DA:56,0
+DA:58,0
+DA:59,0
+DA:62,0
+DA:63,0
+DA:64,0
+DA:65,0
+DA:67,0
+DA:70,0
+DA:71,0
+DA:73,0
+DA:75,0
+DA:81,1
+DA:83,0
+DA:86,2
+DA:91,0
+DA:93,0
+DA:94,0
+DA:96,0
+DA:97,0
+DA:100,0
+DA:103,0
+DA:104,0
+DA:109,0
+DA:111,0
+DA:112,0
+DA:116,0
+DA:117,0
+DA:118,0
+DA:120,0
+DA:121,0
+DA:124,0
+DA:126,0
+DA:131,0
+DA:143,0
+DA:144,0
+DA:149,0
+DA:155,0
+DA:156,0
+DA:159,0
+DA:160,0
+DA:161,0
+DA:162,0
+DA:163,0
+DA:165,0
+DA:167,0
+DA:168,0
+DA:172,0
+DA:174,0
+DA:179,0
+DA:181,0
+DA:182,0
+DA:183,0
+DA:184,0
+DA:189,0
+DA:191,0
+DA:192,0
+DA:196,0
+DA:199,0
+DA:200,0
+DA:202,0
+DA:205,0
+DA:206,0
+DA:207,0
+DA:208,0
+DA:209,0
+DA:210,0
+DA:213,0
+DA:214,0
+DA:215,0
+DA:216,0
+DA:217,0
+DA:218,0
+DA:219,0
+DA:220,0
+DA:221,0
+DA:224,0
+DA:225,0
+DA:226,0
+DA:227,0
+DA:228,0
+DA:229,0
+DA:230,0
+DA:231,0
+DA:234,0
+DA:235,0
+DA:236,0
+DA:237,0
+DA:238,0
+DA:239,0
+DA:240,0
+DA:241,0
+DA:244,0
+DA:245,0
+DA:246,0
+DA:247,0
+DA:249,0
+DA:250,0
+DA:251,0
+DA:252,0
+DA:253,0
+DA:254,0
+DA:258,0
+DA:260,0
+DA:261,0
+DA:264,0
+DA:265,0
+DA:268,0
+DA:269,0
+DA:270,0
+DA:273,0
+DA:274,0
+DA:275,0
+DA:279,0
+DA:280,0
+DA:281,0
+DA:282,0
+DA:287,0
+DA:289,0
+DA:290,0
+DA:292,0
+DA:295,0
+DA:303,0
+DA:309,0
+DA:310,0
+DA:312,0
+DA:313,0
+DA:315,0
+DA:316,0
+DA:317,0
+DA:318,0
+DA:320,0
+DA:323,0
+DA:324,0
+DA:325,0
+DA:328,0
+DA:332,0
+DA:336,0
+DA:337,0
+DA:339,0
+DA:340,0
+DA:341,0
+DA:342,0
+DA:343,0
+DA:344,0
+DA:345,0
+DA:348,0
+DA:349,0
+DA:350,0
+DA:351,0
+DA:352,0
+DA:353,0
+DA:355,0
+DA:356,0
+DA:357,0
+DA:358,0
+DA:359,0
+DA:360,0
+DA:365,0
+DA:366,0
+DA:367,0
+DA:368,0
+DA:372,0
+DA:377,0
+DA:378,0
+DA:385,0
+DA:386,0
+DA:387,0
+DA:388,0
+DA:389,0
+DA:391,0
+DA:395,0
+DA:396,0
+DA:397,0
+DA:401,0
+DA:402,0
+DA:403,0
+DA:404,0
+DA:408,0
+DA:409,0
+DA:410,0
+DA:411,0
+DA:412,0
+DA:413,0
+DA:414,0
+DA:417,0
+DA:418,0
+DA:419,0
+DA:421,0
+DA:423,0
+DA:426,0
+DA:429,0
+DA:430,0
+DA:433,0
+DA:435,0
+DA:437,0
+DA:438,0
+DA:439,0
+DA:440,0
+DA:445,0
+DA:447,0
+LF:219
+LH:4
+end_of_record
+SF:lib/data/model/server/amd.dart
+DA:31,2
+DA:33,2
+DA:34,2
+DA:37,3
+DA:38,2
+DA:39,1
+DA:40,1
+DA:42,2
+DA:46,1
+DA:48,3
+DA:49,4
+DA:52,3
+DA:53,1
+DA:56,2
+DA:57,3
+DA:58,1
+DA:61,4
+DA:64,4
+DA:67,3
+DA:70,4
+DA:72,1
+DA:87,1
+DA:89,1
+DA:90,1
+DA:92,2
+DA:93,1
+DA:98,1
+DA:99,1
+DA:100,1
+DA:102,2
+DA:103,2
+DA:104,1
+DA:107,1
+DA:108,3
+DA:109,3
+DA:110,2
+DA:112,1
+DA:113,1
+DA:114,1
+DA:115,2
+DA:116,1
+DA:117,1
+DA:118,1
+DA:123,1
+DA:126,1
+DA:127,2
+DA:128,4
+DA:129,3
+DA:131,1
+DA:132,1
+DA:146,1
+DA:157,1
+DA:159,6
+DA:169,1
+DA:171,1
+DA:173,6
+DA:182,1
+DA:184,1
+DA:186,4
+LF:59
+LH:59
+end_of_record
+SF:lib/data/model/server/battery.dart
+DA:22,2
+DA:24,1
+DA:25,1
+DA:26,1
+DA:27,2
+DA:28,1
+DA:29,2
+DA:30,3
+DA:33,1
+DA:34,1
+DA:36,1
+DA:37,1
+DA:39,1
+DA:40,1
+DA:41,2
+DA:43,1
+DA:44,1
+DA:48,0
+DA:50,0
+DA:53,0
+DA:62,1
+DA:64,1
+DA:66,1
+DA:68,1
+DA:77,1
+DA:78,1
+DA:79,1
+DA:80,1
+DA:81,2
+DA:82,1
+DA:84,2
+DA:85,0
+DA:86,1
+DA:88,0
+DA:90,1
+DA:92,1
+LF:36
+LH:31
+end_of_record
+SF:lib/data/model/server/conn.dart
+DA:9,7
+DA:11,0
+DA:12,0
+DA:13,0
+DA:14,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:18,0
+DA:19,0
+DA:20,0
+LF:11
+LH:1
+end_of_record
+SF:lib/data/model/server/cpu.dart
+DA:10,1
+DA:14,1
+DA:16,3
+DA:17,8
+DA:18,2
+DA:19,2
+DA:20,2
+DA:21,2
+DA:22,1
+DA:26,1
+DA:27,5
+DA:28,2
+DA:30,7
+DA:31,7
+DA:32,1
+DA:33,3
+DA:35,0
+DA:41,0
+DA:44,2
+DA:47,0
+DA:48,1
+DA:49,5
+DA:50,7
+DA:51,2
+DA:52,2
+DA:56,0
+DA:57,1
+DA:58,5
+DA:59,7
+DA:60,2
+DA:61,2
+DA:65,0
+DA:66,1
+DA:67,5
+DA:68,7
+DA:69,2
+DA:70,2
+DA:74,0
+DA:75,3
+DA:77,1
+DA:86,1
+DA:92,0
+DA:93,1
+DA:94,2
+DA:95,3
+DA:96,3
+DA:98,2
+DA:99,4
+DA:100,1
+DA:145,1
+DA:156,14
+DA:158,0
+DA:159,0
+DA:161,0
+DA:162,0
+DA:164,0
+DA:165,0
+DA:166,0
+DA:168,0
+DA:169,0
+DA:170,0
+DA:172,0
+DA:173,0
+DA:174,0
+DA:175,0
+DA:176,0
+DA:177,0
+DA:178,0
+DA:187,0
+DA:188,0
+DA:190,0
+DA:191,0
+DA:192,0
+DA:193,0
+DA:194,0
+DA:195,0
+DA:202,0
+DA:203,0
+DA:205,0
+DA:215,0
+DA:216,0
+DA:219,0
+DA:221,0
+DA:222,0
+DA:223,0
+DA:225,0
+DA:226,0
+DA:241,0
+DA:243,0
+DA:244,0
+DA:245,0
+DA:246,0
+DA:247,0
+DA:249,0
+DA:250,0
+DA:265,0
+DA:266,0
+DA:267,0
+DA:268,0
+DA:270,0
+DA:272,0
+DA:273,0
+DA:274,0
+DA:277,0
+DA:278,0
+DA:280,0
+DA:281,0
+DA:283,0
+DA:290,0
+DA:291,0
+DA:293,0
+LF:111
+LH:44
+end_of_record
+SF:lib/data/model/server/custom.dart
+DA:27,0
+DA:38,0
+DA:40,0
+DA:42,0
+DA:44,0
+DA:46,0
+DA:47,0
+DA:48,0
+DA:49,0
+DA:50,0
+DA:51,0
+DA:52,0
+DA:55,0
+DA:58,0
+DA:59,0
+DA:60,0
+DA:61,0
+DA:62,0
+DA:63,0
+DA:64,0
+LF:20
+LH:0
+end_of_record
+SF:lib/data/model/server/custom.g.dart
+DA:9,0
+DA:10,0
+DA:11,0
+DA:12,0
+DA:13,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:18,0
+DA:21,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:25,0
+DA:26,0
+DA:27,0
+DA:28,0
+DA:29,0
+LF:18
+LH:0
+end_of_record
+SF:lib/data/model/server/disk_smart.dart
+DA:11,1
+DA:25,0
+DA:27,1
+DA:28,1
+DA:30,5
+DA:32,2
+DA:34,2
+DA:37,3
+DA:39,1
+DA:41,1
+DA:45,1
+DA:46,1
+DA:47,2
+DA:48,3
+DA:51,1
+DA:52,1
+DA:54,2
+DA:56,1
+DA:58,1
+DA:59,1
+DA:72,2
+DA:78,1
+DA:79,1
+DA:82,1
+DA:83,1
+DA:84,1
+DA:85,1
+DA:86,1
+DA:87,1
+DA:88,1
+DA:91,3
+DA:94,1
+DA:96,1
+DA:97,1
+DA:98,0
+DA:99,0
+DA:103,1
+DA:104,0
+DA:106,0
+DA:107,0
+DA:112,1
+DA:114,0
+DA:115,0
+DA:119,2
+DA:123,2
+DA:124,1
+DA:125,2
+DA:126,1
+DA:132,2
+DA:133,1
+DA:134,1
+DA:136,1
+DA:145,0
+DA:156,1
+DA:164,1
+DA:165,1
+DA:167,2
+DA:170,2
+DA:171,1
+DA:172,2
+DA:174,2
+DA:175,1
+DA:177,1
+DA:178,1
+DA:179,1
+DA:180,2
+DA:181,2
+DA:182,3
+DA:183,2
+DA:192,0
+DA:195,1
+DA:197,2
+DA:198,2
+DA:201,0
+DA:204,0
+DA:206,0
+DA:208,0
+DA:213,0
+DA:214,0
+DA:222,3
+DA:224,4
+DA:225,4
+DA:226,4
+DA:227,4
+DA:228,4
+DA:229,4
+DA:231,0
+DA:232,0
+DA:237,1
+DA:251,0
+DA:253,0
+DA:255,0
+DA:261,1
+DA:274,0
+DA:276,1
+DA:277,1
+DA:278,1
+DA:279,2
+DA:280,2
+DA:281,2
+DA:282,2
+DA:283,2
+DA:284,2
+DA:285,2
+DA:289,0
+DA:291,0
+LF:106
+LH:82
+end_of_record
+SF:lib/data/model/server/disk_smart.g.dart
+DA:9,0
+DA:10,0
+DA:11,0
+DA:12,0
+DA:13,0
+DA:14,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:18,0
+DA:19,0
+DA:23,1
+DA:24,1
+DA:25,1
+DA:26,1
+DA:27,1
+DA:28,1
+DA:29,1
+DA:30,1
+DA:31,1
+DA:32,1
+DA:33,1
+DA:36,0
+DA:37,0
+DA:38,0
+DA:39,0
+DA:40,0
+DA:41,0
+DA:42,0
+DA:43,0
+DA:44,0
+DA:45,0
+DA:46,0
+DA:47,0
+DA:51,0
+DA:52,0
+DA:53,0
+DA:54,0
+DA:55,0
+DA:56,0
+DA:57,0
+DA:58,0
+DA:59,0
+DA:60,0
+DA:61,0
+DA:64,0
+DA:65,0
+DA:66,0
+DA:67,0
+DA:68,0
+DA:69,0
+DA:70,0
+DA:71,0
+DA:72,0
+DA:73,0
+DA:76,0
+DA:78,0
+DA:79,0
+DA:80,0
+DA:81,0
+DA:82,0
+DA:83,0
+DA:84,0
+DA:85,0
+DA:86,0
+LF:65
+LH:11
+end_of_record
+SF:lib/data/model/server/memory.dart
+DA:8,7
+DA:10,0
+DA:11,0
+DA:12,0
+DA:14,0
+DA:17,0
+DA:19,0
+DA:20,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:25,0
+DA:26,0
+DA:27,0
+DA:28,0
+DA:29,0
+DA:30,0
+DA:32,0
+DA:36,0
+DA:43,0
+DA:45,0
+DA:47,0
+DA:50,0
+DA:51,0
+DA:52,0
+DA:53,0
+DA:55,0
+DA:56,0
+DA:57,0
+DA:61,0
+DA:63,0
+DA:65,0
+DA:68,0
+DA:69,0
+DA:70,0
+DA:71,0
+DA:72,0
+DA:75,0
+DA:76,0
+DA:77,0
+DA:78,0
+DA:81,0
+DA:85,0
+DA:89,0
+DA:90,0
+DA:91,0
+DA:92,0
+DA:93,0
+DA:94,0
+DA:95,0
+DA:96,0
+DA:97,0
+DA:98,0
+DA:99,0
+DA:101,0
+DA:110,6
+DA:112,0
+DA:114,0
+DA:116,0
+DA:118,0
+DA:121,0
+DA:122,0
+DA:124,0
+DA:125,0
+DA:126,0
+DA:127,0
+DA:128,0
+DA:129,0
+DA:130,0
+DA:131,0
+DA:132,0
+DA:134,0
+LF:72
+LH:2
+end_of_record
+SF:lib/data/model/server/net_speed.dart
+DA:13,1
+DA:15,0
+DA:16,0
+DA:22,1
+DA:24,0
+DA:26,0
+DA:27,0
+DA:29,0
+DA:30,0
+DA:32,0
+DA:33,0
+DA:34,0
+DA:35,0
+DA:37,0
+DA:53,0
+DA:55,0
+DA:56,0
+DA:57,0
+DA:58,0
+DA:60,0
+DA:61,0
+DA:62,0
+DA:65,0
+DA:66,0
+DA:67,0
+DA:68,0
+DA:72,0
+DA:74,0
+DA:75,0
+DA:78,0
+DA:79,0
+DA:80,0
+DA:82,0
+DA:83,0
+DA:84,0
+DA:85,0
+DA:86,0
+DA:90,0
+DA:92,0
+DA:93,0
+DA:96,0
+DA:97,0
+DA:98,0
+DA:101,0
+DA:102,0
+DA:103,0
+DA:104,0
+DA:108,0
+DA:110,0
+DA:111,0
+DA:114,0
+DA:115,0
+DA:116,0
+DA:118,0
+DA:119,0
+DA:120,0
+DA:121,0
+DA:122,0
+DA:126,0
+DA:128,0
+DA:129,0
+DA:132,0
+DA:134,0
+DA:135,0
+DA:136,0
+DA:143,0
+DA:150,0
+DA:151,0
+DA:152,0
+DA:153,0
+DA:156,0
+DA:157,0
+DA:159,0
+DA:160,0
+DA:162,0
+DA:163,0
+DA:164,0
+DA:165,0
+DA:166,0
+DA:196,0
+DA:197,0
+DA:198,0
+DA:199,0
+DA:202,0
+DA:203,0
+DA:204,0
+DA:205,0
+DA:206,0
+DA:209,0
+DA:212,0
+DA:215,0
+DA:216,0
+DA:217,0
+LF:93
+LH:2
+end_of_record
+SF:lib/data/model/server/nvdia.dart
+DA:25,2
+DA:26,2
+DA:27,1
+DA:28,3
+DA:29,1
+DA:30,3
+DA:32,1
+DA:33,1
+DA:34,1
+DA:35,1
+DA:36,1
+DA:37,2
+DA:38,3
+DA:39,3
+DA:40,2
+DA:41,3
+DA:42,3
+DA:43,3
+DA:44,3
+DA:45,1
+DA:46,3
+DA:47,3
+DA:48,3
+DA:50,1
+DA:51,1
+DA:53,3
+DA:58,2
+DA:60,1
+DA:61,1
+DA:62,1
+DA:63,1
+DA:64,1
+DA:65,3
+DA:67,1
+DA:69,3
+DA:70,3
+DA:71,3
+DA:72,1
+DA:73,1
+DA:74,3
+DA:75,3
+DA:77,1
+DA:79,3
+DA:84,2
+DA:85,1
+DA:98,1
+DA:108,0
+DA:110,0
+DA:120,1
+DA:122,0
+DA:124,0
+DA:133,1
+DA:135,0
+DA:137,0
+LF:54
+LH:48
+end_of_record
+SF:lib/data/model/server/sensors.dart
+DA:6,6
+DA:17,1
+DA:18,1
+DA:19,1
+DA:20,1
+DA:21,1
+DA:22,0
+DA:31,1
+DA:33,0
+DA:34,0
+DA:35,0
+DA:36,0
+DA:37,0
+DA:38,0
+DA:40,0
+DA:43,1
+DA:44,3
+DA:47,1
+DA:48,2
+DA:49,1
+DA:51,3
+DA:52,1
+DA:53,1
+DA:54,2
+DA:55,1
+DA:58,2
+DA:61,3
+DA:62,0
+DA:65,1
+DA:66,2
+DA:68,1
+DA:69,1
+DA:70,1
+DA:71,5
+DA:73,1
+DA:74,2
+DA:75,1
+DA:76,1
+DA:77,2
+DA:78,2
+DA:79,2
+DA:80,1
+DA:82,2
+LF:43
+LH:34
+end_of_record
+SF:lib/data/model/server/temp.dart
+DA:4,0
+DA:5,0
+DA:6,0
+DA:7,0
+DA:8,0
+DA:9,0
+DA:10,0
+DA:13,0
+DA:14,0
+DA:18,0
+DA:22,0
+DA:23,0
+DA:26,0
+DA:27,0
+DA:30,1
+DA:31,2
+DA:34,0
+DA:35,0
+DA:38,0
+DA:39,0
+DA:40,0
+DA:43,0
+LF:22
+LH:2
+end_of_record
+SF:lib/data/model/server/wol_cfg.dart
+DA:14,0
+DA:16,0
+DA:17,0
+DA:18,0
+DA:19,0
+DA:20,0
+DA:22,0
+DA:29,0
+DA:30,0
+DA:31,0
+DA:34,0
+DA:35,0
+DA:36,0
+DA:37,0
+DA:38,0
+DA:41,0
+DA:43,0
+LF:17
+LH:0
+end_of_record
+SF:lib/data/model/server/wol_cfg.g.dart
+DA:9,0
+DA:10,0
+DA:11,0
+DA:12,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:18,0
+DA:19,0
+LF:9
+LH:0
+end_of_record
+SF:lib/data/store/server.dart
+DA:9,0
+DA:11,0
+DA:13,0
+DA:14,0
+DA:17,0
+DA:18,0
+DA:19,0
+DA:20,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:25,0
+DA:28,0
+DA:29,0
+DA:32,0
+DA:39,0
+DA:45,0
+DA:46,0
+DA:49,0
+DA:50,0
+DA:51,0
+DA:53,0
+DA:54,0
+DA:57,0
+DA:59,0
+DA:60,0
+DA:61,0
+DA:64,0
+DA:65,0
+DA:69,0
+DA:72,0
+DA:73,0
+DA:74,0
+DA:78,0
+DA:79,0
+DA:80,0
+DA:83,0
+DA:84,0
+DA:85,0
+DA:90,0
+DA:92,0
+DA:94,0
+DA:95,0
+DA:96,0
+DA:97,0
+DA:98,0
+DA:104,0
+DA:105,0
+DA:106,0
+DA:107,0
+DA:108,0
+DA:109,0
+DA:110,0
+DA:111,0
+DA:116,0
+DA:118,0
+DA:119,0
+DA:124,0
+LF:58
+LH:0
+end_of_record
+SF:lib/data/model/server/windows_parser.dart
+DA:16,0
+DA:19,1
+DA:26,2
+DA:27,0
+DA:29,0
+DA:30,0
+DA:31,0
+DA:32,0
+DA:34,0
+DA:36,0
+DA:41,0
+DA:46,1
+DA:49,2
+DA:50,4
+DA:51,1
+DA:53,1
+DA:54,2
+DA:59,1
+DA:60,1
+DA:61,1
+DA:62,1
+DA:63,1
+DA:64,1
+DA:65,1
+DA:66,1
+DA:67,1
+DA:71,2
+DA:72,1
+DA:77,3
+DA:81,1
+DA:82,1
+DA:85,3
+DA:86,0
+DA:90,1
+DA:91,2
+DA:92,2
+DA:94,1
+DA:95,3
+DA:97,0
+DA:100,0
+DA:106,1
+DA:108,1
+DA:109,1
+DA:111,1
+DA:112,0
+DA:113,0
+DA:114,0
+DA:116,0
+DA:119,0
+DA:120,0
+DA:130,0
+DA:131,0
+DA:133,0
+DA:134,0
+DA:135,0
+DA:146,1
+DA:148,1
+DA:150,1
+DA:153,2
+DA:154,2
+DA:157,2
+DA:158,2
+DA:160,1
+DA:161,1
+DA:176,1
+DA:184,1
+DA:186,1
+DA:187,1
+DA:190,1
+DA:191,1
+DA:193,1
+DA:204,1
+DA:206,1
+DA:207,1
+DA:209,2
+DA:211,2
+DA:212,2
+DA:214,3
+DA:216,3
+DA:217,0
+DA:218,2
+DA:221,1
+DA:222,2
+DA:223,2
+DA:224,1
+DA:227,0
+DA:232,2
+DA:233,2
+DA:234,1
+DA:235,2
+DA:236,4
+DA:239,1
+DA:240,1
+DA:254,3
+DA:255,1
+LF:95
+LH:70
+end_of_record
+SF:lib/data/model/server/try_limiter.dart
+DA:6,0
+DA:8,0
+DA:9,0
+DA:10,0
+DA:13,0
+DA:14,0
+DA:20,0
+DA:21,0
+DA:24,0
+DA:25,0
+DA:28,0
+DA:29,0
+LF:12
+LH:0
+end_of_record
+SF:lib/data/model/ssh/virtual_key.dart
+DA:59,0
+DA:60,0
+DA:61,0
+DA:62,0
+DA:63,0
+DA:64,0
+DA:65,0
+DA:66,0
+DA:67,0
+DA:68,0
+DA:69,0
+DA:70,0
+DA:71,0
+DA:72,0
+DA:73,0
+DA:74,0
+DA:75,0
+DA:80,0
+DA:81,0
+DA:84,0
+DA:85,0
+DA:87,0
+DA:88,0
+DA:90,0
+DA:113,0
+DA:114,0
+DA:115,0
+DA:116,0
+DA:117,0
+DA:118,0
+DA:119,0
+DA:120,0
+DA:121,0
+DA:122,0
+DA:123,0
+DA:124,0
+DA:125,0
+DA:126,0
+DA:127,0
+DA:128,0
+DA:129,0
+DA:130,0
+DA:131,0
+DA:132,0
+DA:133,0
+DA:134,0
+DA:135,0
+DA:136,0
+DA:137,0
+DA:138,0
+DA:143,0
+DA:144,0
+DA:145,0
+DA:146,0
+DA:147,0
+DA:148,0
+DA:149,0
+DA:150,0
+DA:151,0
+DA:158,0
+DA:159,0
+DA:160,0
+DA:161,0
+DA:162,0
+DA:166,0
+DA:167,0
+DA:171,0
+DA:172,0
+DA:176,0
+DA:177,0
+DA:178,0
+DA:179,0
+DA:184,0
+DA:186,0
+DA:187,0
+DA:188,0
+DA:189,0
+DA:190,0
+DA:192,0
+LF:79
+LH:0
+end_of_record
+SF:lib/data/store/container.dart
+DA:8,0
+DA:10,0
+DA:12,0
+DA:13,0
+DA:16,0
+DA:17,0
+DA:20,0
+DA:21,0
+DA:23,0
+DA:27,0
+DA:30,0
+DA:31,0
+DA:35,0
+DA:36,0
+DA:38,0
+DA:41,0
+LF:16
+LH:0
+end_of_record
+SF:lib/data/store/history.dart
+DA:10,0
+DA:13,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:18,0
+DA:21,0
+DA:29,0
+DA:32,0
+DA:34,0
+DA:35,0
+DA:36,0
+DA:39,0
+DA:43,0
+DA:45,0
+LF:15
+LH:0
+end_of_record
+SF:lib/data/store/private_key.dart
+DA:6,0
+DA:8,0
+DA:10,0
+DA:11,0
+DA:14,0
+DA:15,0
+DA:16,0
+DA:17,0
+DA:19,0
+DA:20,0
+DA:21,0
+DA:22,0
+DA:25,0
+DA:26,0
+DA:29,0
+DA:36,0
+DA:42,0
+DA:44,0
+DA:47,0
+DA:48,0
+LF:20
+LH:0
+end_of_record
+SF:lib/data/store/setting.dart
+DA:11,0
+DA:13,0
+LF:2
+LH:0
+end_of_record
+SF:lib/data/store/snippet.dart
+DA:6,0
+DA:8,0
+DA:10,0
+DA:11,0
+DA:14,0
+DA:16,0
+DA:17,0
+DA:19,0
+DA:20,0
+DA:21,0
+DA:22,0
+DA:25,0
+DA:26,0
+DA:29,0
+DA:36,0
+DA:39,0
+DA:42,0
+DA:43,0
+DA:46,0
+DA:47,0
+DA:48,0
+DA:50,0
+DA:51,0
+DA:54,0
+LF:24
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n.dart
+DA:75,0
+DA:76,0
+DA:80,0
+DA:81,0
+DA:1550,8
+DA:1552,0
+DA:1554,0
+DA:1557,0
+DA:1558,0
+DA:1571,0
+DA:1573,0
+DA:1577,0
+DA:1579,0
+DA:1580,0
+DA:1582,0
+DA:1583,0
+DA:1584,0
+DA:1591,0
+DA:1592,0
+DA:1593,0
+DA:1594,0
+DA:1595,0
+DA:1596,0
+DA:1597,0
+DA:1598,0
+DA:1599,0
+DA:1600,0
+DA:1601,0
+DA:1602,0
+DA:1603,0
+DA:1604,0
+DA:1605,0
+DA:1606,0
+DA:1607,0
+DA:1608,0
+DA:1609,0
+DA:1610,0
+DA:1611,0
+DA:1612,0
+DA:1613,0
+DA:1614,0
+DA:1615,0
+DA:1618,0
+LF:43
+LH:1
+end_of_record
+SF:lib/generated/l10n/l10n_en.dart
+DA:9,0
+DA:11,0
+DA:15,0
+DA:18,0
+DA:22,0
+DA:25,0
+DA:28,0
+DA:31,0
+DA:35,0
+DA:39,0
+DA:42,0
+DA:45,0
+DA:48,0
+DA:52,0
+DA:55,0
+DA:58,0
+DA:62,0
+DA:65,0
+DA:68,0
+DA:71,0
+DA:74,0
+DA:77,0
+DA:80,0
+DA:83,0
+DA:87,0
+DA:90,0
+DA:93,0
+DA:97,0
+DA:100,0
+DA:103,0
+DA:107,0
+DA:110,0
+DA:113,0
+DA:117,0
+DA:120,0
+DA:123,0
+DA:127,0
+DA:130,0
+DA:133,0
+DA:136,0
+DA:139,0
+DA:143,0
+DA:146,0
+DA:149,0
+DA:152,0
+DA:155,0
+DA:158,0
+DA:161,0
+DA:163,0
+DA:166,0
+DA:170,0
+DA:172,0
+DA:175,0
+DA:178,0
+DA:183,0
+DA:186,0
+DA:188,0
+DA:191,0
+DA:194,0
+DA:198,0
+DA:201,0
+DA:204,0
+DA:208,0
+DA:211,0
+DA:214,0
+DA:217,0
+DA:220,0
+DA:223,0
+DA:226,0
+DA:230,0
+DA:233,0
+DA:237,0
+DA:239,0
+DA:242,0
+DA:245,0
+DA:248,0
+DA:251,0
+DA:254,0
+DA:257,0
+DA:260,0
+DA:263,0
+DA:267,0
+DA:270,0
+DA:273,0
+DA:276,0
+DA:279,0
+DA:282,0
+DA:285,0
+DA:287,0
+DA:290,0
+DA:293,0
+DA:296,0
+DA:299,0
+DA:302,0
+DA:305,0
+DA:308,0
+DA:312,0
+DA:315,0
+DA:318,0
+DA:321,0
+DA:324,0
+DA:328,0
+DA:331,0
+DA:334,0
+DA:338,0
+DA:341,0
+DA:344,0
+DA:347,0
+DA:349,0
+DA:352,0
+DA:355,0
+DA:358,0
+DA:361,0
+DA:364,0
+DA:367,0
+DA:370,0
+DA:373,0
+DA:377,0
+DA:380,0
+DA:384,0
+DA:387,0
+DA:390,0
+DA:393,0
+DA:396,0
+DA:399,0
+DA:402,0
+DA:406,0
+DA:409,0
+DA:412,0
+DA:415,0
+DA:418,0
+DA:421,0
+DA:425,0
+DA:428,0
+DA:432,0
+DA:436,0
+DA:438,0
+DA:441,0
+DA:444,0
+DA:447,0
+DA:450,0
+DA:454,0
+DA:457,0
+DA:460,0
+DA:463,0
+DA:466,0
+DA:469,0
+DA:472,0
+DA:475,0
+DA:478,0
+DA:481,0
+DA:485,0
+DA:489,0
+DA:493,0
+DA:496,0
+DA:499,0
+DA:502,0
+DA:505,0
+DA:508,0
+DA:511,0
+DA:514,0
+DA:517,0
+DA:520,0
+DA:523,0
+DA:526,0
+DA:529,0
+DA:532,0
+DA:535,0
+DA:538,0
+DA:541,0
+DA:544,0
+DA:547,0
+DA:550,0
+DA:553,0
+DA:556,0
+DA:559,0
+DA:562,0
+DA:566,0
+DA:569,0
+DA:572,0
+DA:575,0
+DA:578,0
+DA:581,0
+DA:584,0
+DA:587,0
+DA:590,0
+DA:593,0
+DA:597,0
+DA:600,0
+DA:602,0
+DA:605,0
+DA:609,0
+DA:611,0
+DA:614,0
+DA:617,0
+DA:620,0
+DA:623,0
+DA:626,0
+DA:629,0
+DA:632,0
+DA:635,0
+DA:639,0
+DA:642,0
+DA:646,0
+DA:648,0
+DA:651,0
+DA:654,0
+DA:658,0
+DA:661,0
+DA:664,0
+DA:667,0
+DA:671,0
+DA:674,0
+DA:677,0
+DA:680,0
+DA:684,0
+DA:687,0
+DA:690,0
+DA:693,0
+DA:696,0
+DA:699,0
+DA:702,0
+DA:705,0
+DA:708,0
+DA:711,0
+DA:714,0
+DA:718,0
+DA:721,0
+DA:724,0
+DA:727,0
+DA:730,0
+DA:733,0
+DA:737,0
+DA:740,0
+DA:743,0
+DA:746,0
+DA:749,0
+DA:752,0
+DA:756,0
+DA:759,0
+DA:762,0
+DA:766,0
+DA:769,0
+DA:772,0
+DA:775,0
+DA:778,0
+DA:782,0
+DA:785,0
+DA:789,0
+LF:249
+LH:0
+end_of_record
+SF:lib/data/model/server/pve.dart
+DA:11,2
+DA:12,1
+DA:13,1
+DA:14,1
+DA:15,1
+DA:16,1
+DA:20,0
+DA:21,0
+DA:22,0
+DA:23,0
+DA:24,0
+DA:25,0
+DA:34,1
+DA:35,2
+DA:38,1
+DA:39,1
+DA:40,1
+DA:41,1
+DA:42,1
+DA:43,1
+DA:44,1
+DA:45,1
+DA:46,1
+DA:47,1
+DA:84,1
+DA:104,1
+DA:105,1
+DA:106,1
+DA:108,1
+DA:109,1
+DA:110,1
+DA:111,1
+DA:112,1
+DA:113,1
+DA:114,1
+DA:115,2
+DA:116,1
+DA:117,1
+DA:118,1
+DA:119,1
+DA:120,1
+DA:121,1
+DA:122,1
+DA:126,0
+DA:127,0
+DA:129,0
+DA:131,0
+DA:132,0
+DA:134,0
+DA:162,1
+DA:182,1
+DA:183,1
+DA:184,1
+DA:186,1
+DA:187,1
+DA:188,1
+DA:189,1
+DA:190,1
+DA:191,1
+DA:192,1
+DA:193,2
+DA:194,1
+DA:195,1
+DA:196,1
+DA:197,1
+DA:198,1
+DA:199,1
+DA:200,1
+DA:204,0
+DA:205,0
+DA:207,0
+DA:209,0
+DA:210,0
+DA:212,0
+DA:230,1
+DA:242,1
+DA:243,1
+DA:244,1
+DA:246,1
+DA:247,1
+DA:248,1
+DA:249,1
+DA:250,1
+DA:251,2
+DA:252,1
+DA:256,0
+DA:258,0
+DA:259,0
+DA:260,0
+DA:262,0
+DA:282,1
+DA:295,1
+DA:296,1
+DA:297,1
+DA:299,1
+DA:300,1
+DA:301,1
+DA:302,1
+DA:303,1
+DA:304,1
+DA:305,1
+DA:306,1
+DA:310,0
+DA:311,0
+DA:313,0
+DA:314,0
+DA:316,0
+DA:318,0
+DA:319,0
+DA:321,0
+DA:336,1
+DA:338,1
+DA:339,1
+DA:340,1
+DA:342,1
+DA:343,1
+DA:344,1
+DA:348,0
+DA:349,0
+DA:351,0
+DA:352,0
+DA:354,0
+DA:355,0
+DA:365,0
+DA:373,0
+DA:375,0
+DA:377,0
+DA:378,0
+DA:379,0
+DA:381,0
+DA:382,0
+DA:383,0
+DA:385,0
+DA:386,0
+DA:387,0
+DA:389,0
+DA:390,0
+DA:391,0
+DA:393,0
+DA:394,0
+DA:397,0
+DA:399,0
+DA:400,0
+DA:401,0
+DA:402,0
+DA:403,0
+DA:404,0
+DA:405,0
+DA:407,0
+DA:408,0
+DA:409,0
+DA:411,0
+DA:412,0
+DA:414,0
+DA:415,0
+DA:417,0
+DA:418,0
+DA:420,0
+DA:421,0
+DA:427,0
+DA:428,0
+DA:429,0
+DA:430,0
+DA:431,0
+DA:434,0
+LF:165
+LH:86
+end_of_record
+SF:lib/generated/l10n/l10n_de.dart
+DA:9,0
+DA:11,0
+DA:15,0
+DA:18,0
+DA:22,0
+DA:25,0
+DA:28,0
+DA:31,0
+DA:35,0
+DA:39,0
+DA:42,0
+DA:45,0
+DA:48,0
+DA:52,0
+DA:56,0
+DA:59,0
+DA:63,0
+DA:66,0
+DA:69,0
+DA:72,0
+DA:75,0
+DA:78,0
+DA:81,0
+DA:84,0
+DA:88,0
+DA:91,0
+DA:94,0
+DA:98,0
+DA:101,0
+DA:104,0
+DA:108,0
+DA:111,0
+DA:114,0
+DA:118,0
+DA:121,0
+DA:124,0
+DA:128,0
+DA:131,0
+DA:134,0
+DA:137,0
+DA:140,0
+DA:144,0
+DA:147,0
+DA:150,0
+DA:153,0
+DA:156,0
+DA:159,0
+DA:162,0
+DA:164,0
+DA:167,0
+DA:171,0
+DA:173,0
+DA:176,0
+DA:179,0
+DA:184,0
+DA:187,0
+DA:189,0
+DA:192,0
+DA:195,0
+DA:199,0
+DA:202,0
+DA:205,0
+DA:209,0
+DA:212,0
+DA:215,0
+DA:218,0
+DA:221,0
+DA:224,0
+DA:227,0
+DA:231,0
+DA:234,0
+DA:238,0
+DA:240,0
+DA:243,0
+DA:246,0
+DA:249,0
+DA:252,0
+DA:255,0
+DA:258,0
+DA:261,0
+DA:264,0
+DA:268,0
+DA:271,0
+DA:274,0
+DA:277,0
+DA:280,0
+DA:283,0
+DA:286,0
+DA:288,0
+DA:291,0
+DA:294,0
+DA:297,0
+DA:300,0
+DA:303,0
+DA:306,0
+DA:309,0
+DA:313,0
+DA:316,0
+DA:319,0
+DA:322,0
+DA:325,0
+DA:329,0
+DA:332,0
+DA:335,0
+DA:339,0
+DA:342,0
+DA:345,0
+DA:348,0
+DA:350,0
+DA:353,0
+DA:356,0
+DA:359,0
+DA:362,0
+DA:366,0
+DA:369,0
+DA:372,0
+DA:375,0
+DA:379,0
+DA:382,0
+DA:386,0
+DA:389,0
+DA:392,0
+DA:395,0
+DA:398,0
+DA:401,0
+DA:404,0
+DA:408,0
+DA:411,0
+DA:414,0
+DA:417,0
+DA:420,0
+DA:423,0
+DA:427,0
+DA:430,0
+DA:434,0
+DA:438,0
+DA:440,0
+DA:443,0
+DA:446,0
+DA:449,0
+DA:452,0
+DA:456,0
+DA:459,0
+DA:462,0
+DA:465,0
+DA:468,0
+DA:471,0
+DA:474,0
+DA:477,0
+DA:480,0
+DA:483,0
+DA:487,0
+DA:491,0
+DA:495,0
+DA:498,0
+DA:501,0
+DA:504,0
+DA:507,0
+DA:510,0
+DA:513,0
+DA:516,0
+DA:519,0
+DA:522,0
+DA:525,0
+DA:528,0
+DA:531,0
+DA:535,0
+DA:538,0
+DA:541,0
+DA:544,0
+DA:547,0
+DA:550,0
+DA:553,0
+DA:556,0
+DA:559,0
+DA:562,0
+DA:565,0
+DA:569,0
+DA:573,0
+DA:576,0
+DA:579,0
+DA:582,0
+DA:585,0
+DA:588,0
+DA:591,0
+DA:594,0
+DA:597,0
+DA:601,0
+DA:604,0
+DA:606,0
+DA:609,0
+DA:613,0
+DA:615,0
+DA:618,0
+DA:622,0
+DA:625,0
+DA:628,0
+DA:631,0
+DA:634,0
+DA:637,0
+DA:640,0
+DA:644,0
+DA:647,0
+DA:651,0
+DA:653,0
+DA:656,0
+DA:659,0
+DA:663,0
+DA:666,0
+DA:669,0
+DA:672,0
+DA:676,0
+DA:679,0
+DA:682,0
+DA:685,0
+DA:689,0
+DA:692,0
+DA:695,0
+DA:698,0
+DA:701,0
+DA:704,0
+DA:707,0
+DA:710,0
+DA:713,0
+DA:716,0
+DA:719,0
+DA:723,0
+DA:727,0
+DA:730,0
+DA:733,0
+DA:736,0
+DA:739,0
+DA:743,0
+DA:746,0
+DA:749,0
+DA:752,0
+DA:755,0
+DA:758,0
+DA:762,0
+DA:765,0
+DA:768,0
+DA:772,0
+DA:775,0
+DA:778,0
+DA:781,0
+DA:784,0
+DA:788,0
+DA:791,0
+DA:795,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_es.dart
+DA:9,0
+DA:11,0
+DA:14,0
+DA:17,0
+DA:21,0
+DA:24,0
+DA:27,0
+DA:30,0
+DA:34,0
+DA:38,0
+DA:41,0
+DA:44,0
+DA:48,0
+DA:52,0
+DA:56,0
+DA:59,0
+DA:63,0
+DA:66,0
+DA:69,0
+DA:72,0
+DA:75,0
+DA:78,0
+DA:81,0
+DA:84,0
+DA:88,0
+DA:91,0
+DA:94,0
+DA:98,0
+DA:101,0
+DA:104,0
+DA:108,0
+DA:111,0
+DA:114,0
+DA:118,0
+DA:121,0
+DA:124,0
+DA:128,0
+DA:131,0
+DA:134,0
+DA:137,0
+DA:140,0
+DA:144,0
+DA:147,0
+DA:150,0
+DA:153,0
+DA:156,0
+DA:159,0
+DA:162,0
+DA:164,0
+DA:167,0
+DA:171,0
+DA:173,0
+DA:176,0
+DA:179,0
+DA:184,0
+DA:187,0
+DA:189,0
+DA:192,0
+DA:195,0
+DA:199,0
+DA:202,0
+DA:205,0
+DA:209,0
+DA:212,0
+DA:215,0
+DA:218,0
+DA:221,0
+DA:224,0
+DA:227,0
+DA:231,0
+DA:234,0
+DA:238,0
+DA:240,0
+DA:243,0
+DA:246,0
+DA:249,0
+DA:252,0
+DA:255,0
+DA:258,0
+DA:261,0
+DA:264,0
+DA:268,0
+DA:271,0
+DA:274,0
+DA:277,0
+DA:280,0
+DA:283,0
+DA:286,0
+DA:288,0
+DA:291,0
+DA:294,0
+DA:297,0
+DA:300,0
+DA:303,0
+DA:306,0
+DA:309,0
+DA:313,0
+DA:316,0
+DA:319,0
+DA:322,0
+DA:325,0
+DA:329,0
+DA:332,0
+DA:335,0
+DA:339,0
+DA:342,0
+DA:345,0
+DA:348,0
+DA:350,0
+DA:353,0
+DA:356,0
+DA:359,0
+DA:363,0
+DA:366,0
+DA:369,0
+DA:372,0
+DA:375,0
+DA:379,0
+DA:382,0
+DA:386,0
+DA:389,0
+DA:392,0
+DA:395,0
+DA:398,0
+DA:401,0
+DA:404,0
+DA:408,0
+DA:411,0
+DA:414,0
+DA:417,0
+DA:420,0
+DA:423,0
+DA:427,0
+DA:430,0
+DA:434,0
+DA:438,0
+DA:440,0
+DA:443,0
+DA:446,0
+DA:449,0
+DA:453,0
+DA:457,0
+DA:460,0
+DA:463,0
+DA:466,0
+DA:470,0
+DA:473,0
+DA:476,0
+DA:479,0
+DA:482,0
+DA:485,0
+DA:489,0
+DA:493,0
+DA:497,0
+DA:500,0
+DA:503,0
+DA:506,0
+DA:510,0
+DA:513,0
+DA:516,0
+DA:519,0
+DA:522,0
+DA:525,0
+DA:528,0
+DA:531,0
+DA:534,0
+DA:537,0
+DA:540,0
+DA:543,0
+DA:546,0
+DA:549,0
+DA:552,0
+DA:555,0
+DA:559,0
+DA:562,0
+DA:565,0
+DA:568,0
+DA:572,0
+DA:576,0
+DA:579,0
+DA:582,0
+DA:585,0
+DA:588,0
+DA:591,0
+DA:594,0
+DA:597,0
+DA:600,0
+DA:604,0
+DA:607,0
+DA:609,0
+DA:612,0
+DA:616,0
+DA:618,0
+DA:621,0
+DA:625,0
+DA:628,0
+DA:631,0
+DA:634,0
+DA:637,0
+DA:640,0
+DA:643,0
+DA:646,0
+DA:649,0
+DA:653,0
+DA:655,0
+DA:658,0
+DA:661,0
+DA:665,0
+DA:668,0
+DA:671,0
+DA:674,0
+DA:678,0
+DA:681,0
+DA:684,0
+DA:687,0
+DA:691,0
+DA:694,0
+DA:697,0
+DA:700,0
+DA:703,0
+DA:706,0
+DA:709,0
+DA:712,0
+DA:715,0
+DA:718,0
+DA:721,0
+DA:725,0
+DA:729,0
+DA:732,0
+DA:735,0
+DA:738,0
+DA:741,0
+DA:745,0
+DA:748,0
+DA:751,0
+DA:754,0
+DA:757,0
+DA:760,0
+DA:764,0
+DA:767,0
+DA:770,0
+DA:774,0
+DA:777,0
+DA:780,0
+DA:783,0
+DA:786,0
+DA:790,0
+DA:793,0
+DA:797,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_fr.dart
+DA:9,0
+DA:11,0
+DA:14,0
+DA:17,0
+DA:21,0
+DA:24,0
+DA:27,0
+DA:30,0
+DA:34,0
+DA:38,0
+DA:41,0
+DA:44,0
+DA:48,0
+DA:52,0
+DA:56,0
+DA:59,0
+DA:63,0
+DA:66,0
+DA:69,0
+DA:72,0
+DA:75,0
+DA:78,0
+DA:81,0
+DA:84,0
+DA:88,0
+DA:91,0
+DA:94,0
+DA:98,0
+DA:101,0
+DA:104,0
+DA:108,0
+DA:111,0
+DA:114,0
+DA:118,0
+DA:121,0
+DA:124,0
+DA:128,0
+DA:131,0
+DA:134,0
+DA:137,0
+DA:140,0
+DA:144,0
+DA:147,0
+DA:150,0
+DA:153,0
+DA:156,0
+DA:159,0
+DA:162,0
+DA:164,0
+DA:167,0
+DA:171,0
+DA:173,0
+DA:176,0
+DA:179,0
+DA:184,0
+DA:187,0
+DA:189,0
+DA:192,0
+DA:195,0
+DA:199,0
+DA:202,0
+DA:205,0
+DA:209,0
+DA:212,0
+DA:215,0
+DA:218,0
+DA:221,0
+DA:224,0
+DA:227,0
+DA:231,0
+DA:234,0
+DA:238,0
+DA:240,0
+DA:243,0
+DA:246,0
+DA:249,0
+DA:252,0
+DA:255,0
+DA:258,0
+DA:261,0
+DA:264,0
+DA:268,0
+DA:271,0
+DA:274,0
+DA:277,0
+DA:280,0
+DA:283,0
+DA:286,0
+DA:288,0
+DA:291,0
+DA:294,0
+DA:297,0
+DA:300,0
+DA:303,0
+DA:306,0
+DA:309,0
+DA:313,0
+DA:316,0
+DA:319,0
+DA:322,0
+DA:325,0
+DA:329,0
+DA:332,0
+DA:335,0
+DA:339,0
+DA:342,0
+DA:345,0
+DA:348,0
+DA:350,0
+DA:353,0
+DA:356,0
+DA:359,0
+DA:362,0
+DA:365,0
+DA:368,0
+DA:371,0
+DA:374,0
+DA:378,0
+DA:381,0
+DA:385,0
+DA:388,0
+DA:391,0
+DA:394,0
+DA:397,0
+DA:400,0
+DA:404,0
+DA:408,0
+DA:411,0
+DA:414,0
+DA:417,0
+DA:420,0
+DA:424,0
+DA:428,0
+DA:431,0
+DA:435,0
+DA:439,0
+DA:441,0
+DA:444,0
+DA:447,0
+DA:450,0
+DA:454,0
+DA:458,0
+DA:461,0
+DA:464,0
+DA:467,0
+DA:471,0
+DA:474,0
+DA:477,0
+DA:480,0
+DA:483,0
+DA:486,0
+DA:490,0
+DA:494,0
+DA:498,0
+DA:501,0
+DA:504,0
+DA:507,0
+DA:511,0
+DA:514,0
+DA:517,0
+DA:520,0
+DA:523,0
+DA:526,0
+DA:529,0
+DA:532,0
+DA:535,0
+DA:538,0
+DA:541,0
+DA:544,0
+DA:547,0
+DA:550,0
+DA:553,0
+DA:556,0
+DA:560,0
+DA:563,0
+DA:566,0
+DA:569,0
+DA:573,0
+DA:577,0
+DA:580,0
+DA:583,0
+DA:586,0
+DA:589,0
+DA:592,0
+DA:595,0
+DA:598,0
+DA:601,0
+DA:605,0
+DA:608,0
+DA:610,0
+DA:613,0
+DA:617,0
+DA:619,0
+DA:622,0
+DA:626,0
+DA:629,0
+DA:632,0
+DA:635,0
+DA:638,0
+DA:641,0
+DA:644,0
+DA:648,0
+DA:651,0
+DA:655,0
+DA:657,0
+DA:660,0
+DA:663,0
+DA:667,0
+DA:670,0
+DA:673,0
+DA:676,0
+DA:680,0
+DA:683,0
+DA:686,0
+DA:689,0
+DA:693,0
+DA:696,0
+DA:699,0
+DA:702,0
+DA:705,0
+DA:708,0
+DA:711,0
+DA:714,0
+DA:717,0
+DA:720,0
+DA:723,0
+DA:727,0
+DA:731,0
+DA:734,0
+DA:737,0
+DA:740,0
+DA:743,0
+DA:747,0
+DA:750,0
+DA:753,0
+DA:756,0
+DA:759,0
+DA:762,0
+DA:766,0
+DA:769,0
+DA:772,0
+DA:776,0
+DA:779,0
+DA:782,0
+DA:785,0
+DA:788,0
+DA:792,0
+DA:795,0
+DA:799,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_id.dart
+DA:9,0
+DA:11,0
+DA:15,0
+DA:18,0
+DA:22,0
+DA:25,0
+DA:28,0
+DA:31,0
+DA:35,0
+DA:39,0
+DA:42,0
+DA:45,0
+DA:48,0
+DA:52,0
+DA:55,0
+DA:58,0
+DA:62,0
+DA:65,0
+DA:68,0
+DA:71,0
+DA:74,0
+DA:77,0
+DA:80,0
+DA:83,0
+DA:87,0
+DA:90,0
+DA:93,0
+DA:97,0
+DA:100,0
+DA:103,0
+DA:107,0
+DA:110,0
+DA:113,0
+DA:117,0
+DA:120,0
+DA:123,0
+DA:127,0
+DA:130,0
+DA:133,0
+DA:136,0
+DA:139,0
+DA:143,0
+DA:146,0
+DA:149,0
+DA:152,0
+DA:155,0
+DA:158,0
+DA:161,0
+DA:163,0
+DA:166,0
+DA:170,0
+DA:172,0
+DA:175,0
+DA:178,0
+DA:183,0
+DA:186,0
+DA:188,0
+DA:191,0
+DA:194,0
+DA:198,0
+DA:201,0
+DA:204,0
+DA:208,0
+DA:211,0
+DA:214,0
+DA:217,0
+DA:220,0
+DA:223,0
+DA:226,0
+DA:230,0
+DA:233,0
+DA:237,0
+DA:239,0
+DA:242,0
+DA:245,0
+DA:248,0
+DA:251,0
+DA:254,0
+DA:257,0
+DA:260,0
+DA:263,0
+DA:267,0
+DA:270,0
+DA:273,0
+DA:276,0
+DA:279,0
+DA:282,0
+DA:285,0
+DA:287,0
+DA:290,0
+DA:293,0
+DA:296,0
+DA:299,0
+DA:302,0
+DA:305,0
+DA:308,0
+DA:312,0
+DA:315,0
+DA:318,0
+DA:321,0
+DA:324,0
+DA:328,0
+DA:331,0
+DA:334,0
+DA:338,0
+DA:341,0
+DA:344,0
+DA:347,0
+DA:349,0
+DA:352,0
+DA:355,0
+DA:358,0
+DA:361,0
+DA:364,0
+DA:367,0
+DA:370,0
+DA:373,0
+DA:377,0
+DA:380,0
+DA:384,0
+DA:387,0
+DA:390,0
+DA:393,0
+DA:396,0
+DA:399,0
+DA:402,0
+DA:406,0
+DA:409,0
+DA:412,0
+DA:415,0
+DA:418,0
+DA:422,0
+DA:425,0
+DA:428,0
+DA:432,0
+DA:436,0
+DA:438,0
+DA:441,0
+DA:444,0
+DA:447,0
+DA:450,0
+DA:454,0
+DA:457,0
+DA:460,0
+DA:463,0
+DA:466,0
+DA:469,0
+DA:472,0
+DA:475,0
+DA:478,0
+DA:481,0
+DA:485,0
+DA:489,0
+DA:493,0
+DA:496,0
+DA:499,0
+DA:502,0
+DA:506,0
+DA:509,0
+DA:512,0
+DA:515,0
+DA:518,0
+DA:521,0
+DA:524,0
+DA:527,0
+DA:530,0
+DA:533,0
+DA:536,0
+DA:539,0
+DA:542,0
+DA:545,0
+DA:548,0
+DA:551,0
+DA:554,0
+DA:557,0
+DA:560,0
+DA:563,0
+DA:567,0
+DA:570,0
+DA:573,0
+DA:576,0
+DA:579,0
+DA:582,0
+DA:585,0
+DA:588,0
+DA:591,0
+DA:594,0
+DA:598,0
+DA:601,0
+DA:603,0
+DA:606,0
+DA:610,0
+DA:612,0
+DA:615,0
+DA:618,0
+DA:621,0
+DA:624,0
+DA:627,0
+DA:630,0
+DA:633,0
+DA:636,0
+DA:639,0
+DA:642,0
+DA:646,0
+DA:648,0
+DA:651,0
+DA:654,0
+DA:658,0
+DA:661,0
+DA:664,0
+DA:667,0
+DA:671,0
+DA:674,0
+DA:677,0
+DA:680,0
+DA:684,0
+DA:687,0
+DA:690,0
+DA:693,0
+DA:696,0
+DA:699,0
+DA:702,0
+DA:705,0
+DA:708,0
+DA:711,0
+DA:714,0
+DA:718,0
+DA:721,0
+DA:724,0
+DA:727,0
+DA:730,0
+DA:733,0
+DA:737,0
+DA:740,0
+DA:743,0
+DA:746,0
+DA:749,0
+DA:752,0
+DA:756,0
+DA:759,0
+DA:762,0
+DA:765,0
+DA:768,0
+DA:771,0
+DA:774,0
+DA:777,0
+DA:781,0
+DA:784,0
+DA:788,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_ja.dart
+DA:9,0
+DA:11,0
+DA:14,0
+DA:17,0
+DA:21,0
+DA:24,0
+DA:27,0
+DA:30,0
+DA:33,0
+DA:36,0
+DA:39,0
+DA:42,0
+DA:45,0
+DA:48,0
+DA:51,0
+DA:54,0
+DA:58,0
+DA:61,0
+DA:64,0
+DA:67,0
+DA:70,0
+DA:73,0
+DA:76,0
+DA:79,0
+DA:83,0
+DA:86,0
+DA:89,0
+DA:92,0
+DA:95,0
+DA:98,0
+DA:102,0
+DA:105,0
+DA:108,0
+DA:111,0
+DA:114,0
+DA:117,0
+DA:121,0
+DA:124,0
+DA:127,0
+DA:130,0
+DA:133,0
+DA:136,0
+DA:139,0
+DA:142,0
+DA:145,0
+DA:148,0
+DA:151,0
+DA:154,0
+DA:156,0
+DA:159,0
+DA:163,0
+DA:165,0
+DA:168,0
+DA:171,0
+DA:176,0
+DA:179,0
+DA:181,0
+DA:184,0
+DA:187,0
+DA:191,0
+DA:194,0
+DA:197,0
+DA:201,0
+DA:204,0
+DA:207,0
+DA:210,0
+DA:213,0
+DA:216,0
+DA:219,0
+DA:223,0
+DA:226,0
+DA:230,0
+DA:232,0
+DA:235,0
+DA:238,0
+DA:241,0
+DA:244,0
+DA:247,0
+DA:250,0
+DA:253,0
+DA:256,0
+DA:260,0
+DA:263,0
+DA:266,0
+DA:269,0
+DA:272,0
+DA:275,0
+DA:278,0
+DA:280,0
+DA:283,0
+DA:286,0
+DA:289,0
+DA:292,0
+DA:295,0
+DA:298,0
+DA:301,0
+DA:305,0
+DA:308,0
+DA:311,0
+DA:314,0
+DA:317,0
+DA:320,0
+DA:323,0
+DA:326,0
+DA:329,0
+DA:332,0
+DA:335,0
+DA:338,0
+DA:340,0
+DA:343,0
+DA:346,0
+DA:349,0
+DA:352,0
+DA:355,0
+DA:358,0
+DA:361,0
+DA:364,0
+DA:368,0
+DA:371,0
+DA:375,0
+DA:378,0
+DA:381,0
+DA:384,0
+DA:387,0
+DA:390,0
+DA:393,0
+DA:396,0
+DA:399,0
+DA:402,0
+DA:405,0
+DA:408,0
+DA:411,0
+DA:414,0
+DA:417,0
+DA:420,0
+DA:423,0
+DA:425,0
+DA:428,0
+DA:431,0
+DA:434,0
+DA:437,0
+DA:440,0
+DA:443,0
+DA:446,0
+DA:449,0
+DA:452,0
+DA:455,0
+DA:458,0
+DA:461,0
+DA:464,0
+DA:467,0
+DA:471,0
+DA:475,0
+DA:478,0
+DA:481,0
+DA:484,0
+DA:487,0
+DA:490,0
+DA:493,0
+DA:496,0
+DA:499,0
+DA:502,0
+DA:505,0
+DA:508,0
+DA:511,0
+DA:514,0
+DA:517,0
+DA:520,0
+DA:523,0
+DA:526,0
+DA:529,0
+DA:532,0
+DA:535,0
+DA:538,0
+DA:541,0
+DA:544,0
+DA:547,0
+DA:551,0
+DA:554,0
+DA:557,0
+DA:560,0
+DA:563,0
+DA:566,0
+DA:569,0
+DA:572,0
+DA:575,0
+DA:578,0
+DA:582,0
+DA:585,0
+DA:587,0
+DA:590,0
+DA:594,0
+DA:596,0
+DA:599,0
+DA:602,0
+DA:605,0
+DA:608,0
+DA:611,0
+DA:614,0
+DA:617,0
+DA:620,0
+DA:623,0
+DA:626,0
+DA:629,0
+DA:631,0
+DA:634,0
+DA:637,0
+DA:640,0
+DA:643,0
+DA:646,0
+DA:649,0
+DA:653,0
+DA:656,0
+DA:659,0
+DA:662,0
+DA:666,0
+DA:669,0
+DA:672,0
+DA:675,0
+DA:678,0
+DA:681,0
+DA:684,0
+DA:687,0
+DA:690,0
+DA:693,0
+DA:696,0
+DA:700,0
+DA:703,0
+DA:706,0
+DA:709,0
+DA:712,0
+DA:715,0
+DA:718,0
+DA:721,0
+DA:724,0
+DA:727,0
+DA:730,0
+DA:733,0
+DA:737,0
+DA:740,0
+DA:743,0
+DA:746,0
+DA:749,0
+DA:752,0
+DA:755,0
+DA:758,0
+DA:761,0
+DA:764,0
+DA:768,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_nl.dart
+DA:9,0
+DA:11,0
+DA:15,0
+DA:18,0
+DA:22,0
+DA:25,0
+DA:28,0
+DA:31,0
+DA:35,0
+DA:39,0
+DA:42,0
+DA:45,0
+DA:48,0
+DA:52,0
+DA:55,0
+DA:58,0
+DA:62,0
+DA:65,0
+DA:68,0
+DA:71,0
+DA:74,0
+DA:77,0
+DA:80,0
+DA:83,0
+DA:87,0
+DA:90,0
+DA:93,0
+DA:97,0
+DA:100,0
+DA:103,0
+DA:107,0
+DA:110,0
+DA:113,0
+DA:117,0
+DA:120,0
+DA:123,0
+DA:127,0
+DA:130,0
+DA:133,0
+DA:136,0
+DA:139,0
+DA:143,0
+DA:146,0
+DA:149,0
+DA:152,0
+DA:155,0
+DA:158,0
+DA:161,0
+DA:163,0
+DA:166,0
+DA:170,0
+DA:172,0
+DA:175,0
+DA:178,0
+DA:183,0
+DA:186,0
+DA:188,0
+DA:191,0
+DA:194,0
+DA:198,0
+DA:201,0
+DA:204,0
+DA:208,0
+DA:211,0
+DA:214,0
+DA:217,0
+DA:220,0
+DA:223,0
+DA:226,0
+DA:230,0
+DA:233,0
+DA:237,0
+DA:239,0
+DA:242,0
+DA:245,0
+DA:248,0
+DA:251,0
+DA:254,0
+DA:257,0
+DA:260,0
+DA:263,0
+DA:267,0
+DA:270,0
+DA:273,0
+DA:276,0
+DA:279,0
+DA:282,0
+DA:285,0
+DA:287,0
+DA:290,0
+DA:293,0
+DA:296,0
+DA:299,0
+DA:302,0
+DA:305,0
+DA:308,0
+DA:312,0
+DA:315,0
+DA:318,0
+DA:321,0
+DA:324,0
+DA:328,0
+DA:331,0
+DA:334,0
+DA:338,0
+DA:341,0
+DA:344,0
+DA:347,0
+DA:349,0
+DA:352,0
+DA:355,0
+DA:358,0
+DA:361,0
+DA:364,0
+DA:367,0
+DA:370,0
+DA:373,0
+DA:377,0
+DA:380,0
+DA:384,0
+DA:387,0
+DA:390,0
+DA:393,0
+DA:396,0
+DA:399,0
+DA:402,0
+DA:406,0
+DA:409,0
+DA:412,0
+DA:415,0
+DA:418,0
+DA:421,0
+DA:425,0
+DA:428,0
+DA:432,0
+DA:436,0
+DA:438,0
+DA:441,0
+DA:444,0
+DA:447,0
+DA:450,0
+DA:454,0
+DA:457,0
+DA:460,0
+DA:463,0
+DA:467,0
+DA:470,0
+DA:473,0
+DA:476,0
+DA:479,0
+DA:482,0
+DA:486,0
+DA:490,0
+DA:494,0
+DA:497,0
+DA:500,0
+DA:503,0
+DA:507,0
+DA:510,0
+DA:513,0
+DA:516,0
+DA:519,0
+DA:522,0
+DA:525,0
+DA:528,0
+DA:531,0
+DA:534,0
+DA:537,0
+DA:540,0
+DA:543,0
+DA:546,0
+DA:549,0
+DA:552,0
+DA:555,0
+DA:558,0
+DA:561,0
+DA:564,0
+DA:568,0
+DA:572,0
+DA:575,0
+DA:578,0
+DA:581,0
+DA:584,0
+DA:587,0
+DA:590,0
+DA:593,0
+DA:596,0
+DA:600,0
+DA:603,0
+DA:605,0
+DA:608,0
+DA:612,0
+DA:614,0
+DA:617,0
+DA:621,0
+DA:624,0
+DA:627,0
+DA:630,0
+DA:633,0
+DA:636,0
+DA:639,0
+DA:643,0
+DA:646,0
+DA:650,0
+DA:652,0
+DA:655,0
+DA:658,0
+DA:662,0
+DA:665,0
+DA:668,0
+DA:671,0
+DA:675,0
+DA:678,0
+DA:681,0
+DA:684,0
+DA:688,0
+DA:691,0
+DA:694,0
+DA:697,0
+DA:700,0
+DA:703,0
+DA:706,0
+DA:709,0
+DA:712,0
+DA:715,0
+DA:718,0
+DA:722,0
+DA:726,0
+DA:729,0
+DA:732,0
+DA:735,0
+DA:738,0
+DA:742,0
+DA:745,0
+DA:748,0
+DA:751,0
+DA:754,0
+DA:757,0
+DA:761,0
+DA:764,0
+DA:767,0
+DA:771,0
+DA:774,0
+DA:777,0
+DA:780,0
+DA:783,0
+DA:787,0
+DA:790,0
+DA:794,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_pt.dart
+DA:9,0
+DA:11,0
+DA:14,0
+DA:17,0
+DA:21,0
+DA:24,0
+DA:27,0
+DA:30,0
+DA:34,0
+DA:38,0
+DA:41,0
+DA:44,0
+DA:48,0
+DA:52,0
+DA:56,0
+DA:59,0
+DA:63,0
+DA:66,0
+DA:69,0
+DA:72,0
+DA:75,0
+DA:78,0
+DA:81,0
+DA:84,0
+DA:88,0
+DA:91,0
+DA:94,0
+DA:97,0
+DA:100,0
+DA:103,0
+DA:107,0
+DA:110,0
+DA:113,0
+DA:117,0
+DA:120,0
+DA:123,0
+DA:127,0
+DA:130,0
+DA:133,0
+DA:136,0
+DA:139,0
+DA:143,0
+DA:146,0
+DA:149,0
+DA:152,0
+DA:155,0
+DA:158,0
+DA:161,0
+DA:163,0
+DA:166,0
+DA:170,0
+DA:172,0
+DA:175,0
+DA:178,0
+DA:183,0
+DA:186,0
+DA:188,0
+DA:191,0
+DA:194,0
+DA:198,0
+DA:201,0
+DA:204,0
+DA:208,0
+DA:211,0
+DA:214,0
+DA:217,0
+DA:220,0
+DA:223,0
+DA:226,0
+DA:230,0
+DA:233,0
+DA:237,0
+DA:239,0
+DA:242,0
+DA:245,0
+DA:248,0
+DA:251,0
+DA:254,0
+DA:257,0
+DA:260,0
+DA:263,0
+DA:267,0
+DA:270,0
+DA:273,0
+DA:276,0
+DA:279,0
+DA:283,0
+DA:286,0
+DA:288,0
+DA:291,0
+DA:294,0
+DA:297,0
+DA:300,0
+DA:303,0
+DA:306,0
+DA:309,0
+DA:313,0
+DA:316,0
+DA:319,0
+DA:322,0
+DA:325,0
+DA:328,0
+DA:331,0
+DA:334,0
+DA:338,0
+DA:341,0
+DA:344,0
+DA:347,0
+DA:349,0
+DA:352,0
+DA:355,0
+DA:358,0
+DA:362,0
+DA:365,0
+DA:368,0
+DA:371,0
+DA:374,0
+DA:378,0
+DA:381,0
+DA:385,0
+DA:388,0
+DA:391,0
+DA:394,0
+DA:397,0
+DA:400,0
+DA:403,0
+DA:407,0
+DA:410,0
+DA:413,0
+DA:416,0
+DA:419,0
+DA:422,0
+DA:426,0
+DA:429,0
+DA:433,0
+DA:437,0
+DA:439,0
+DA:442,0
+DA:445,0
+DA:448,0
+DA:451,0
+DA:455,0
+DA:458,0
+DA:461,0
+DA:464,0
+DA:467,0
+DA:470,0
+DA:473,0
+DA:476,0
+DA:479,0
+DA:482,0
+DA:486,0
+DA:490,0
+DA:494,0
+DA:497,0
+DA:500,0
+DA:503,0
+DA:506,0
+DA:509,0
+DA:512,0
+DA:515,0
+DA:518,0
+DA:521,0
+DA:524,0
+DA:527,0
+DA:530,0
+DA:533,0
+DA:536,0
+DA:539,0
+DA:542,0
+DA:545,0
+DA:548,0
+DA:551,0
+DA:555,0
+DA:558,0
+DA:561,0
+DA:564,0
+DA:568,0
+DA:571,0
+DA:574,0
+DA:577,0
+DA:580,0
+DA:583,0
+DA:586,0
+DA:589,0
+DA:592,0
+DA:595,0
+DA:599,0
+DA:602,0
+DA:604,0
+DA:607,0
+DA:611,0
+DA:613,0
+DA:616,0
+DA:620,0
+DA:623,0
+DA:626,0
+DA:629,0
+DA:632,0
+DA:635,0
+DA:638,0
+DA:641,0
+DA:644,0
+DA:648,0
+DA:650,0
+DA:653,0
+DA:656,0
+DA:660,0
+DA:663,0
+DA:666,0
+DA:669,0
+DA:673,0
+DA:676,0
+DA:679,0
+DA:682,0
+DA:686,0
+DA:689,0
+DA:692,0
+DA:695,0
+DA:698,0
+DA:701,0
+DA:704,0
+DA:707,0
+DA:710,0
+DA:713,0
+DA:716,0
+DA:720,0
+DA:724,0
+DA:727,0
+DA:730,0
+DA:733,0
+DA:736,0
+DA:740,0
+DA:743,0
+DA:746,0
+DA:749,0
+DA:752,0
+DA:755,0
+DA:759,0
+DA:762,0
+DA:765,0
+DA:768,0
+DA:771,0
+DA:774,0
+DA:777,0
+DA:780,0
+DA:784,0
+DA:787,0
+DA:791,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_ru.dart
+DA:9,0
+DA:11,0
+DA:14,0
+DA:17,0
+DA:21,0
+DA:24,0
+DA:27,0
+DA:30,0
+DA:34,0
+DA:38,0
+DA:41,0
+DA:44,0
+DA:48,0
+DA:52,0
+DA:56,0
+DA:59,0
+DA:63,0
+DA:66,0
+DA:69,0
+DA:72,0
+DA:75,0
+DA:78,0
+DA:81,0
+DA:84,0
+DA:88,0
+DA:91,0
+DA:94,0
+DA:97,0
+DA:100,0
+DA:103,0
+DA:107,0
+DA:110,0
+DA:113,0
+DA:117,0
+DA:120,0
+DA:123,0
+DA:127,0
+DA:130,0
+DA:133,0
+DA:136,0
+DA:139,0
+DA:143,0
+DA:146,0
+DA:149,0
+DA:152,0
+DA:155,0
+DA:158,0
+DA:161,0
+DA:163,0
+DA:166,0
+DA:170,0
+DA:172,0
+DA:175,0
+DA:178,0
+DA:183,0
+DA:186,0
+DA:188,0
+DA:191,0
+DA:194,0
+DA:198,0
+DA:201,0
+DA:204,0
+DA:208,0
+DA:211,0
+DA:214,0
+DA:217,0
+DA:220,0
+DA:223,0
+DA:226,0
+DA:230,0
+DA:233,0
+DA:237,0
+DA:239,0
+DA:242,0
+DA:245,0
+DA:248,0
+DA:251,0
+DA:254,0
+DA:257,0
+DA:260,0
+DA:263,0
+DA:267,0
+DA:270,0
+DA:273,0
+DA:276,0
+DA:279,0
+DA:282,0
+DA:285,0
+DA:287,0
+DA:290,0
+DA:293,0
+DA:296,0
+DA:299,0
+DA:302,0
+DA:305,0
+DA:308,0
+DA:312,0
+DA:315,0
+DA:318,0
+DA:321,0
+DA:324,0
+DA:328,0
+DA:331,0
+DA:334,0
+DA:338,0
+DA:341,0
+DA:344,0
+DA:347,0
+DA:349,0
+DA:352,0
+DA:355,0
+DA:358,0
+DA:362,0
+DA:366,0
+DA:369,0
+DA:372,0
+DA:375,0
+DA:379,0
+DA:382,0
+DA:386,0
+DA:389,0
+DA:392,0
+DA:395,0
+DA:398,0
+DA:401,0
+DA:404,0
+DA:408,0
+DA:411,0
+DA:414,0
+DA:417,0
+DA:420,0
+DA:423,0
+DA:427,0
+DA:430,0
+DA:434,0
+DA:438,0
+DA:440,0
+DA:443,0
+DA:446,0
+DA:449,0
+DA:452,0
+DA:456,0
+DA:459,0
+DA:462,0
+DA:465,0
+DA:468,0
+DA:471,0
+DA:474,0
+DA:477,0
+DA:480,0
+DA:483,0
+DA:487,0
+DA:491,0
+DA:495,0
+DA:498,0
+DA:501,0
+DA:504,0
+DA:508,0
+DA:511,0
+DA:514,0
+DA:517,0
+DA:520,0
+DA:523,0
+DA:526,0
+DA:529,0
+DA:532,0
+DA:535,0
+DA:538,0
+DA:541,0
+DA:544,0
+DA:547,0
+DA:550,0
+DA:553,0
+DA:557,0
+DA:560,0
+DA:563,0
+DA:566,0
+DA:570,0
+DA:574,0
+DA:577,0
+DA:580,0
+DA:583,0
+DA:586,0
+DA:589,0
+DA:592,0
+DA:595,0
+DA:598,0
+DA:602,0
+DA:605,0
+DA:607,0
+DA:610,0
+DA:614,0
+DA:616,0
+DA:619,0
+DA:623,0
+DA:626,0
+DA:629,0
+DA:632,0
+DA:635,0
+DA:638,0
+DA:641,0
+DA:644,0
+DA:647,0
+DA:651,0
+DA:653,0
+DA:656,0
+DA:659,0
+DA:663,0
+DA:666,0
+DA:669,0
+DA:672,0
+DA:676,0
+DA:679,0
+DA:682,0
+DA:685,0
+DA:689,0
+DA:692,0
+DA:695,0
+DA:698,0
+DA:701,0
+DA:704,0
+DA:707,0
+DA:710,0
+DA:713,0
+DA:716,0
+DA:719,0
+DA:723,0
+DA:727,0
+DA:730,0
+DA:733,0
+DA:736,0
+DA:739,0
+DA:743,0
+DA:746,0
+DA:749,0
+DA:752,0
+DA:755,0
+DA:758,0
+DA:762,0
+DA:765,0
+DA:768,0
+DA:771,0
+DA:774,0
+DA:777,0
+DA:780,0
+DA:783,0
+DA:787,0
+DA:790,0
+DA:794,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_tr.dart
+DA:9,0
+DA:11,0
+DA:14,0
+DA:17,0
+DA:21,0
+DA:24,0
+DA:27,0
+DA:30,0
+DA:34,0
+DA:38,0
+DA:41,0
+DA:44,0
+DA:47,0
+DA:51,0
+DA:54,0
+DA:57,0
+DA:61,0
+DA:64,0
+DA:67,0
+DA:70,0
+DA:73,0
+DA:76,0
+DA:79,0
+DA:82,0
+DA:86,0
+DA:89,0
+DA:92,0
+DA:96,0
+DA:99,0
+DA:102,0
+DA:106,0
+DA:109,0
+DA:112,0
+DA:116,0
+DA:119,0
+DA:122,0
+DA:126,0
+DA:129,0
+DA:132,0
+DA:135,0
+DA:138,0
+DA:142,0
+DA:145,0
+DA:148,0
+DA:151,0
+DA:154,0
+DA:157,0
+DA:160,0
+DA:162,0
+DA:165,0
+DA:169,0
+DA:171,0
+DA:174,0
+DA:177,0
+DA:182,0
+DA:185,0
+DA:187,0
+DA:190,0
+DA:193,0
+DA:197,0
+DA:200,0
+DA:203,0
+DA:207,0
+DA:210,0
+DA:213,0
+DA:216,0
+DA:219,0
+DA:222,0
+DA:225,0
+DA:229,0
+DA:232,0
+DA:236,0
+DA:238,0
+DA:241,0
+DA:244,0
+DA:247,0
+DA:250,0
+DA:253,0
+DA:256,0
+DA:259,0
+DA:262,0
+DA:266,0
+DA:269,0
+DA:272,0
+DA:275,0
+DA:278,0
+DA:281,0
+DA:284,0
+DA:286,0
+DA:289,0
+DA:292,0
+DA:295,0
+DA:298,0
+DA:301,0
+DA:304,0
+DA:307,0
+DA:311,0
+DA:314,0
+DA:317,0
+DA:320,0
+DA:323,0
+DA:327,0
+DA:330,0
+DA:333,0
+DA:337,0
+DA:340,0
+DA:343,0
+DA:346,0
+DA:348,0
+DA:351,0
+DA:354,0
+DA:357,0
+DA:360,0
+DA:363,0
+DA:366,0
+DA:369,0
+DA:372,0
+DA:376,0
+DA:379,0
+DA:383,0
+DA:386,0
+DA:389,0
+DA:392,0
+DA:395,0
+DA:398,0
+DA:401,0
+DA:405,0
+DA:408,0
+DA:411,0
+DA:414,0
+DA:417,0
+DA:420,0
+DA:424,0
+DA:427,0
+DA:431,0
+DA:435,0
+DA:437,0
+DA:440,0
+DA:443,0
+DA:446,0
+DA:449,0
+DA:453,0
+DA:456,0
+DA:459,0
+DA:462,0
+DA:465,0
+DA:468,0
+DA:471,0
+DA:474,0
+DA:477,0
+DA:480,0
+DA:484,0
+DA:488,0
+DA:492,0
+DA:495,0
+DA:498,0
+DA:501,0
+DA:505,0
+DA:508,0
+DA:511,0
+DA:514,0
+DA:517,0
+DA:520,0
+DA:523,0
+DA:526,0
+DA:529,0
+DA:532,0
+DA:535,0
+DA:538,0
+DA:541,0
+DA:544,0
+DA:547,0
+DA:550,0
+DA:553,0
+DA:556,0
+DA:559,0
+DA:562,0
+DA:566,0
+DA:570,0
+DA:573,0
+DA:576,0
+DA:579,0
+DA:582,0
+DA:585,0
+DA:588,0
+DA:591,0
+DA:594,0
+DA:598,0
+DA:601,0
+DA:603,0
+DA:606,0
+DA:610,0
+DA:612,0
+DA:615,0
+DA:618,0
+DA:621,0
+DA:624,0
+DA:627,0
+DA:630,0
+DA:633,0
+DA:636,0
+DA:640,0
+DA:643,0
+DA:647,0
+DA:649,0
+DA:652,0
+DA:655,0
+DA:659,0
+DA:662,0
+DA:665,0
+DA:668,0
+DA:672,0
+DA:675,0
+DA:678,0
+DA:681,0
+DA:685,0
+DA:688,0
+DA:691,0
+DA:694,0
+DA:697,0
+DA:700,0
+DA:703,0
+DA:706,0
+DA:709,0
+DA:712,0
+DA:715,0
+DA:719,0
+DA:722,0
+DA:725,0
+DA:728,0
+DA:731,0
+DA:734,0
+DA:738,0
+DA:741,0
+DA:744,0
+DA:747,0
+DA:750,0
+DA:753,0
+DA:757,0
+DA:760,0
+DA:763,0
+DA:766,0
+DA:769,0
+DA:772,0
+DA:775,0
+DA:778,0
+DA:782,0
+DA:785,0
+DA:789,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_uk.dart
+DA:9,0
+DA:11,0
+DA:14,0
+DA:17,0
+DA:21,0
+DA:24,0
+DA:27,0
+DA:30,0
+DA:34,0
+DA:38,0
+DA:41,0
+DA:44,0
+DA:48,0
+DA:52,0
+DA:56,0
+DA:59,0
+DA:63,0
+DA:66,0
+DA:69,0
+DA:72,0
+DA:75,0
+DA:78,0
+DA:81,0
+DA:84,0
+DA:88,0
+DA:91,0
+DA:94,0
+DA:98,0
+DA:101,0
+DA:104,0
+DA:108,0
+DA:111,0
+DA:114,0
+DA:118,0
+DA:121,0
+DA:124,0
+DA:128,0
+DA:131,0
+DA:134,0
+DA:137,0
+DA:140,0
+DA:144,0
+DA:147,0
+DA:150,0
+DA:153,0
+DA:156,0
+DA:159,0
+DA:162,0
+DA:164,0
+DA:167,0
+DA:171,0
+DA:173,0
+DA:176,0
+DA:179,0
+DA:184,0
+DA:187,0
+DA:189,0
+DA:192,0
+DA:195,0
+DA:199,0
+DA:202,0
+DA:205,0
+DA:209,0
+DA:212,0
+DA:215,0
+DA:218,0
+DA:221,0
+DA:224,0
+DA:227,0
+DA:231,0
+DA:234,0
+DA:238,0
+DA:240,0
+DA:243,0
+DA:246,0
+DA:249,0
+DA:252,0
+DA:255,0
+DA:258,0
+DA:261,0
+DA:264,0
+DA:268,0
+DA:271,0
+DA:274,0
+DA:277,0
+DA:280,0
+DA:284,0
+DA:287,0
+DA:289,0
+DA:292,0
+DA:295,0
+DA:298,0
+DA:301,0
+DA:304,0
+DA:307,0
+DA:310,0
+DA:314,0
+DA:317,0
+DA:320,0
+DA:323,0
+DA:326,0
+DA:330,0
+DA:333,0
+DA:336,0
+DA:340,0
+DA:343,0
+DA:346,0
+DA:349,0
+DA:351,0
+DA:354,0
+DA:357,0
+DA:360,0
+DA:364,0
+DA:368,0
+DA:371,0
+DA:374,0
+DA:377,0
+DA:381,0
+DA:384,0
+DA:388,0
+DA:391,0
+DA:394,0
+DA:397,0
+DA:400,0
+DA:403,0
+DA:406,0
+DA:410,0
+DA:413,0
+DA:416,0
+DA:419,0
+DA:422,0
+DA:425,0
+DA:429,0
+DA:432,0
+DA:436,0
+DA:440,0
+DA:442,0
+DA:445,0
+DA:448,0
+DA:451,0
+DA:454,0
+DA:458,0
+DA:461,0
+DA:464,0
+DA:467,0
+DA:470,0
+DA:473,0
+DA:476,0
+DA:479,0
+DA:482,0
+DA:485,0
+DA:489,0
+DA:493,0
+DA:497,0
+DA:500,0
+DA:503,0
+DA:506,0
+DA:510,0
+DA:513,0
+DA:516,0
+DA:519,0
+DA:522,0
+DA:525,0
+DA:528,0
+DA:531,0
+DA:534,0
+DA:537,0
+DA:540,0
+DA:543,0
+DA:546,0
+DA:549,0
+DA:552,0
+DA:555,0
+DA:558,0
+DA:561,0
+DA:564,0
+DA:567,0
+DA:571,0
+DA:575,0
+DA:578,0
+DA:581,0
+DA:584,0
+DA:587,0
+DA:590,0
+DA:593,0
+DA:596,0
+DA:599,0
+DA:603,0
+DA:606,0
+DA:608,0
+DA:611,0
+DA:615,0
+DA:617,0
+DA:620,0
+DA:624,0
+DA:627,0
+DA:630,0
+DA:633,0
+DA:636,0
+DA:639,0
+DA:642,0
+DA:645,0
+DA:648,0
+DA:652,0
+DA:654,0
+DA:657,0
+DA:660,0
+DA:664,0
+DA:667,0
+DA:670,0
+DA:673,0
+DA:677,0
+DA:680,0
+DA:683,0
+DA:686,0
+DA:690,0
+DA:693,0
+DA:696,0
+DA:699,0
+DA:702,0
+DA:705,0
+DA:708,0
+DA:711,0
+DA:714,0
+DA:717,0
+DA:720,0
+DA:724,0
+DA:727,0
+DA:730,0
+DA:733,0
+DA:736,0
+DA:739,0
+DA:743,0
+DA:746,0
+DA:749,0
+DA:752,0
+DA:755,0
+DA:758,0
+DA:762,0
+DA:765,0
+DA:768,0
+DA:772,0
+DA:775,0
+DA:778,0
+DA:781,0
+DA:784,0
+DA:788,0
+DA:791,0
+DA:795,0
+LF:249
+LH:0
+end_of_record
+SF:lib/generated/l10n/l10n_zh.dart
+DA:9,0
+DA:11,0
+DA:14,0
+DA:17,0
+DA:20,0
+DA:23,0
+DA:26,0
+DA:29,0
+DA:32,0
+DA:35,0
+DA:38,0
+DA:41,0
+DA:44,0
+DA:47,0
+DA:50,0
+DA:53,0
+DA:56,0
+DA:59,0
+DA:62,0
+DA:65,0
+DA:68,0
+DA:71,0
+DA:74,0
+DA:77,0
+DA:81,0
+DA:84,0
+DA:87,0
+DA:90,0
+DA:93,0
+DA:96,0
+DA:100,0
+DA:103,0
+DA:106,0
+DA:109,0
+DA:112,0
+DA:115,0
+DA:119,0
+DA:122,0
+DA:125,0
+DA:128,0
+DA:131,0
+DA:134,0
+DA:137,0
+DA:140,0
+DA:143,0
+DA:146,0
+DA:149,0
+DA:152,0
+DA:154,0
+DA:157,0
+DA:161,0
+DA:163,0
+DA:166,0
+DA:169,0
+DA:174,0
+DA:177,0
+DA:179,0
+DA:182,0
+DA:185,0
+DA:188,0
+DA:191,0
+DA:194,0
+DA:197,0
+DA:200,0
+DA:203,0
+DA:206,0
+DA:209,0
+DA:212,0
+DA:215,0
+DA:218,0
+DA:221,0
+DA:225,0
+DA:227,0
+DA:230,0
+DA:233,0
+DA:236,0
+DA:239,0
+DA:242,0
+DA:245,0
+DA:248,0
+DA:251,0
+DA:254,0
+DA:257,0
+DA:260,0
+DA:263,0
+DA:266,0
+DA:269,0
+DA:272,0
+DA:274,0
+DA:277,0
+DA:280,0
+DA:283,0
+DA:286,0
+DA:289,0
+DA:292,0
+DA:295,0
+DA:299,0
+DA:302,0
+DA:305,0
+DA:308,0
+DA:311,0
+DA:314,0
+DA:317,0
+DA:320,0
+DA:323,0
+DA:326,0
+DA:329,0
+DA:332,0
+DA:334,0
+DA:337,0
+DA:340,0
+DA:343,0
+DA:346,0
+DA:349,0
+DA:352,0
+DA:355,0
+DA:358,0
+DA:362,0
+DA:365,0
+DA:369,0
+DA:372,0
+DA:375,0
+DA:378,0
+DA:381,0
+DA:384,0
+DA:387,0
+DA:390,0
+DA:393,0
+DA:396,0
+DA:399,0
+DA:402,0
+DA:405,0
+DA:408,0
+DA:411,0
+DA:414,0
+DA:417,0
+DA:419,0
+DA:422,0
+DA:425,0
+DA:428,0
+DA:431,0
+DA:434,0
+DA:437,0
+DA:440,0
+DA:443,0
+DA:446,0
+DA:449,0
+DA:452,0
+DA:455,0
+DA:458,0
+DA:461,0
+DA:464,0
+DA:467,0
+DA:470,0
+DA:473,0
+DA:476,0
+DA:479,0
+DA:482,0
+DA:485,0
+DA:488,0
+DA:491,0
+DA:494,0
+DA:497,0
+DA:500,0
+DA:503,0
+DA:506,0
+DA:509,0
+DA:512,0
+DA:515,0
+DA:518,0
+DA:521,0
+DA:524,0
+DA:527,0
+DA:530,0
+DA:533,0
+DA:536,0
+DA:539,0
+DA:543,0
+DA:546,0
+DA:549,0
+DA:552,0
+DA:555,0
+DA:558,0
+DA:561,0
+DA:564,0
+DA:567,0
+DA:570,0
+DA:573,0
+DA:576,0
+DA:578,0
+DA:581,0
+DA:585,0
+DA:587,0
+DA:590,0
+DA:593,0
+DA:596,0
+DA:599,0
+DA:602,0
+DA:605,0
+DA:608,0
+DA:611,0
+DA:614,0
+DA:617,0
+DA:620,0
+DA:622,0
+DA:625,0
+DA:628,0
+DA:631,0
+DA:634,0
+DA:637,0
+DA:640,0
+DA:643,0
+DA:646,0
+DA:649,0
+DA:652,0
+DA:655,0
+DA:658,0
+DA:661,0
+DA:664,0
+DA:667,0
+DA:670,0
+DA:673,0
+DA:676,0
+DA:679,0
+DA:682,0
+DA:685,0
+DA:688,0
+DA:691,0
+DA:694,0
+DA:697,0
+DA:700,0
+DA:703,0
+DA:706,0
+DA:709,0
+DA:712,0
+DA:715,0
+DA:718,0
+DA:721,0
+DA:724,0
+DA:727,0
+DA:730,0
+DA:733,0
+DA:736,0
+DA:739,0
+DA:742,0
+DA:745,0
+DA:748,0
+DA:751,0
+DA:754,0
+DA:761,0
+DA:763,0
+DA:766,0
+DA:769,0
+DA:772,0
+DA:775,0
+DA:778,0
+DA:781,0
+DA:784,0
+DA:787,0
+DA:790,0
+DA:793,0
+DA:796,0
+DA:799,0
+DA:802,0
+DA:805,0
+DA:808,0
+DA:811,0
+DA:814,0
+DA:817,0
+DA:820,0
+DA:823,0
+DA:826,0
+DA:829,0
+DA:833,0
+DA:836,0
+DA:839,0
+DA:842,0
+DA:845,0
+DA:848,0
+DA:852,0
+DA:855,0
+DA:858,0
+DA:861,0
+DA:864,0
+DA:867,0
+DA:871,0
+DA:874,0
+DA:877,0
+DA:880,0
+DA:883,0
+DA:886,0
+DA:889,0
+DA:892,0
+DA:895,0
+DA:898,0
+DA:901,0
+DA:904,0
+DA:906,0
+DA:909,0
+DA:913,0
+DA:915,0
+DA:918,0
+DA:921,0
+DA:926,0
+DA:929,0
+DA:931,0
+DA:934,0
+DA:937,0
+DA:940,0
+DA:943,0
+DA:946,0
+DA:949,0
+DA:952,0
+DA:955,0
+DA:958,0
+DA:961,0
+DA:964,0
+DA:967,0
+DA:970,0
+DA:973,0
+DA:977,0
+DA:979,0
+DA:982,0
+DA:985,0
+DA:988,0
+DA:991,0
+DA:994,0
+DA:997,0
+DA:1000,0
+DA:1003,0
+DA:1006,0
+DA:1009,0
+DA:1012,0
+DA:1015,0
+DA:1018,0
+DA:1021,0
+DA:1024,0
+DA:1026,0
+DA:1029,0
+DA:1032,0
+DA:1035,0
+DA:1038,0
+DA:1041,0
+DA:1044,0
+DA:1047,0
+DA:1051,0
+DA:1054,0
+DA:1057,0
+DA:1060,0
+DA:1063,0
+DA:1066,0
+DA:1069,0
+DA:1072,0
+DA:1075,0
+DA:1078,0
+DA:1081,0
+DA:1084,0
+DA:1086,0
+DA:1089,0
+DA:1092,0
+DA:1095,0
+DA:1098,0
+DA:1101,0
+DA:1104,0
+DA:1107,0
+DA:1110,0
+DA:1114,0
+DA:1117,0
+DA:1121,0
+DA:1124,0
+DA:1127,0
+DA:1130,0
+DA:1133,0
+DA:1136,0
+DA:1139,0
+DA:1142,0
+DA:1145,0
+DA:1148,0
+DA:1151,0
+DA:1154,0
+DA:1157,0
+DA:1160,0
+DA:1163,0
+DA:1166,0
+DA:1169,0
+DA:1171,0
+DA:1174,0
+DA:1177,0
+DA:1180,0
+DA:1183,0
+DA:1186,0
+DA:1189,0
+DA:1192,0
+DA:1195,0
+DA:1198,0
+DA:1201,0
+DA:1204,0
+DA:1207,0
+DA:1210,0
+DA:1213,0
+DA:1216,0
+DA:1219,0
+DA:1222,0
+DA:1225,0
+DA:1228,0
+DA:1231,0
+DA:1234,0
+DA:1237,0
+DA:1240,0
+DA:1243,0
+DA:1246,0
+DA:1249,0
+DA:1252,0
+DA:1255,0
+DA:1258,0
+DA:1261,0
+DA:1264,0
+DA:1267,0
+DA:1270,0
+DA:1273,0
+DA:1276,0
+DA:1279,0
+DA:1282,0
+DA:1285,0
+DA:1288,0
+DA:1291,0
+DA:1295,0
+DA:1298,0
+DA:1301,0
+DA:1304,0
+DA:1307,0
+DA:1310,0
+DA:1313,0
+DA:1316,0
+DA:1319,0
+DA:1322,0
+DA:1325,0
+DA:1328,0
+DA:1330,0
+DA:1333,0
+DA:1337,0
+DA:1339,0
+DA:1342,0
+DA:1345,0
+DA:1348,0
+DA:1351,0
+DA:1354,0
+DA:1357,0
+DA:1360,0
+DA:1363,0
+DA:1366,0
+DA:1369,0
+DA:1372,0
+DA:1374,0
+DA:1377,0
+DA:1380,0
+DA:1383,0
+DA:1386,0
+DA:1389,0
+DA:1392,0
+DA:1395,0
+DA:1398,0
+DA:1401,0
+DA:1404,0
+DA:1407,0
+DA:1410,0
+DA:1413,0
+DA:1416,0
+DA:1419,0
+DA:1422,0
+DA:1425,0
+DA:1428,0
+DA:1431,0
+DA:1434,0
+DA:1437,0
+DA:1440,0
+DA:1443,0
+DA:1446,0
+DA:1449,0
+DA:1452,0
+DA:1455,0
+DA:1458,0
+DA:1461,0
+DA:1464,0
+DA:1467,0
+DA:1470,0
+DA:1473,0
+DA:1476,0
+DA:1479,0
+DA:1482,0
+DA:1485,0
+DA:1488,0
+DA:1491,0
+DA:1494,0
+DA:1497,0
+DA:1500,0
+DA:1503,0
+DA:1506,0
+LF:498
+LH:0
+end_of_record
+SF:lib/data/model/server/proc.dart
+DA:18,1
+DA:47,1
+DA:61,1
+DA:62,2
+DA:63,1
+DA:64,3
+DA:65,3
+DA:66,1
+DA:67,1
+DA:68,3
+DA:69,1
+DA:70,1
+DA:71,3
+DA:72,1
+DA:73,1
+DA:74,3
+DA:78,0
+DA:79,0
+DA:80,0
+DA:81,0
+DA:82,0
+DA:83,0
+DA:84,0
+DA:85,0
+DA:86,0
+DA:87,0
+DA:88,0
+DA:89,0
+DA:90,0
+DA:94,0
+DA:96,0
+DA:99,0
+DA:100,0
+DA:101,0
+DA:110,2
+DA:112,1
+DA:113,5
+DA:114,1
+DA:116,1
+DA:117,2
+DA:118,3
+DA:119,1
+DA:120,1
+DA:121,1
+DA:122,1
+DA:123,1
+DA:124,1
+DA:125,1
+DA:126,1
+DA:127,1
+DA:128,1
+DA:129,1
+DA:130,1
+DA:133,1
+DA:134,1
+DA:135,3
+DA:136,1
+DA:137,1
+DA:139,2
+DA:141,0
+DA:142,0
+DA:147,1
+DA:148,1
+DA:150,0
+DA:151,0
+DA:153,0
+DA:154,0
+DA:156,0
+DA:157,0
+DA:159,0
+DA:160,0
+DA:163,2
+DA:170,1
+DA:171,1
+DA:172,2
+LF:75
+LH:47
+end_of_record
diff --git a/lib/app.dart b/lib/app.dart
index 94b16c6f..bf94a953 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -40,17 +40,13 @@ class MyApp extends StatelessWidget {
light: ThemeData(
useMaterial3: true,
colorSchemeSeed: UIs.colorSeed,
- appBarTheme: AppBarTheme(
- scrolledUnderElevation: 0.0,
- ),
+ appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
),
dark: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: UIs.colorSeed,
- appBarTheme: AppBarTheme(
- scrolledUnderElevation: 0.0,
- ),
+ appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
),
);
}
@@ -58,15 +54,8 @@ class MyApp extends StatelessWidget {
Widget _buildDynamicColor(BuildContext context) {
return DynamicColorBuilder(
builder: (light, dark) {
- final lightTheme = ThemeData(
- useMaterial3: true,
- colorScheme: light,
- );
- final darkTheme = ThemeData(
- useMaterial3: true,
- brightness: Brightness.dark,
- colorScheme: dark,
- );
+ final lightTheme = ThemeData(useMaterial3: true, colorScheme: light);
+ final darkTheme = ThemeData(useMaterial3: true, brightness: Brightness.dark, colorScheme: dark);
if (context.isDark && dark != null) {
UIs.primaryColor = dark.primary;
} else if (!context.isDark && light != null) {
@@ -78,11 +67,7 @@ class MyApp extends StatelessWidget {
);
}
- Widget _buildApp(
- BuildContext ctx, {
- required ThemeData light,
- required ThemeData dark,
- }) {
+ Widget _buildApp(BuildContext ctx, {required ThemeData light, required ThemeData dark}) {
final tMode = Stores.setting.themeMode.fetch();
// Issue #57
final themeMode = switch (tMode) {
@@ -103,10 +88,7 @@ class MyApp extends StatelessWidget {
],
),
locale: locale,
- localizationsDelegates: const [
- LibLocalizations.delegate,
- ...AppLocalizations.localizationsDelegates,
- ],
+ localizationsDelegates: const [LibLocalizations.delegate, ...AppLocalizations.localizationsDelegates],
supportedLocales: AppLocalizations.supportedLocales,
localeListResolutionCallback: LocaleUtil.resolve,
navigatorObservers: [AppRouteObserver.instance],
@@ -128,10 +110,7 @@ class MyApp extends StatelessWidget {
child = const HomePage();
- return VirtualWindowFrame(
- title: BuildData.name,
- child: child,
- );
+ return VirtualWindowFrame(title: BuildData.name, child: child);
},
),
);
diff --git a/lib/core/extension/sftpfile.dart b/lib/core/extension/sftpfile.dart
index cdb73cd4..3b4ace87 100644
--- a/lib/core/extension/sftpfile.dart
+++ b/lib/core/extension/sftpfile.dart
@@ -12,21 +12,9 @@ extension SftpFileX on SftpFileMode {
UnixPerm toUnixPerm() {
return UnixPerm(
- user: UnixPermOp(
- r: userRead,
- w: userWrite,
- x: userExecute,
- ),
- group: UnixPermOp(
- r: groupRead,
- w: groupWrite,
- x: groupExecute,
- ),
- other: UnixPermOp(
- r: otherRead,
- w: otherWrite,
- x: otherExecute,
- ),
+ user: UnixPermOp(r: userRead, w: userWrite, x: userExecute),
+ group: UnixPermOp(r: groupRead, w: groupWrite, x: groupExecute),
+ other: UnixPermOp(r: otherRead, w: otherWrite, x: otherExecute),
);
}
}
diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart
index e3ad656f..4cff3eca 100644
--- a/lib/core/extension/ssh_client.dart
+++ b/lib/core/extension/ssh_client.dart
@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/widgets.dart';
+import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/res/misc.dart';
@@ -13,6 +14,52 @@ typedef OnStdin = void Function(SSHSession session);
typedef PwdRequestFunc = Future Function(String? user);
extension SSHClientX on SSHClient {
+ /// Create a persistent PowerShell session for Windows commands
+ Future<(SSHSession, String)> execPowerShell(
+ OnStdin onStdin, {
+ SSHPtyConfig? pty,
+ OnStdout? onStdout,
+ OnStdout? onStderr,
+ bool stdout = true,
+ bool stderr = true,
+ Map? env,
+ }) async {
+ final session = await execute(
+ 'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',
+ pty: pty,
+ environment: env,
+ );
+
+ final result = BytesBuilder(copy: false);
+ final stdoutDone = Completer();
+ final stderrDone = Completer();
+
+ session.stdout.listen(
+ (e) {
+ onStdout?.call(e.string, session);
+ if (stdout) result.add(e);
+ },
+ onDone: stdoutDone.complete,
+ onError: stderrDone.completeError,
+ );
+
+ session.stderr.listen(
+ (e) {
+ onStderr?.call(e.string, session);
+ if (stderr) result.add(e);
+ },
+ onDone: stderrDone.complete,
+ onError: stderrDone.completeError,
+ );
+
+ onStdin(session);
+
+ await stdoutDone.future;
+ await stderrDone.future;
+
+ return (session, result.takeBytes().string);
+ }
+
Future<(SSHSession, String)> exec(
OnStdin onStdin, {
String? entry,
@@ -22,9 +69,14 @@ extension SSHClientX on SSHClient {
bool stdout = true,
bool stderr = true,
Map? env,
+ SystemType? systemType,
}) async {
final session = await execute(
- entry ?? 'cat | sh',
+ entry ??
+ switch (systemType) {
+ SystemType.windows => 'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',
+ _ => 'cat | sh',
+ },
pty: pty,
environment: env,
);
@@ -81,9 +133,7 @@ extension SSHClientX on SSHClient {
isRequestingPwd = true;
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
if (context == null) return;
- final pwd = context.mounted
- ? await context.showPwdDialog(title: user, id: id)
- : null;
+ final pwd = context.mounted ? await context.showPwdDialog(title: user, id: id) : null;
if (pwd == null || pwd.isEmpty) {
session.stdin.close();
} else {
diff --git a/lib/core/utils/comparator.dart b/lib/core/utils/comparator.dart
index 604cbf9d..5985daac 100644
--- a/lib/core/utils/comparator.dart
+++ b/lib/core/utils/comparator.dart
@@ -6,10 +6,8 @@ class ChainComparator {
ChainComparator.empty() : this._create(null, (a, b) => 0);
ChainComparator.create() : this._create(null, (a, b) => 0);
- static ChainComparator comparing>(
- F Function(T) extractor) {
- return ChainComparator._create(
- null, (a, b) => extractor(a).compareTo(extractor(b)));
+ static ChainComparator comparing>(F Function(T) extractor) {
+ return ChainComparator._create(null, (a, b) => extractor(a).compareTo(extractor(b)));
}
int compare(T a, T b) {
@@ -26,8 +24,9 @@ class ChainComparator {
}
ChainComparator thenCompareBy>(
- F Function(T) extractor,
- {bool reversed = false}) {
+ F Function(T) extractor, {
+ bool reversed = false,
+ }) {
return ChainComparator._create(
this,
reversed
@@ -36,18 +35,12 @@ class ChainComparator {
);
}
- ChainComparator thenWithComparator(Comparator comparator,
- {bool reversed = false}) {
- return ChainComparator._create(
- this,
- !reversed ? comparator : (a, b) => comparator(b, a),
- );
+ ChainComparator thenWithComparator(Comparator comparator, {bool reversed = false}) {
+ return ChainComparator._create(this, !reversed ? comparator : (a, b) => comparator(b, a));
}
- ChainComparator thenCompareByReversed>(
- F Function(T) extractor) {
- return ChainComparator._create(
- this, (a, b) => -extractor(a).compareTo(extractor(b)));
+ ChainComparator thenCompareByReversed>(F Function(T) extractor) {
+ return ChainComparator._create(this, (a, b) => -extractor(a).compareTo(extractor(b)));
}
ChainComparator thenTrueFirst(bool Function(T) f) {
@@ -63,8 +56,7 @@ class ChainComparator {
}
class Comparators {
- static Comparator compareStringCaseInsensitive(
- {bool uppercaseFirst = false}) {
+ static Comparator compareStringCaseInsensitive({bool uppercaseFirst = false}) {
return (String a, String b) {
final r = a.toLowerCase().compareTo(b.toLowerCase());
if (r != 0) return r;
diff --git a/lib/core/utils/server.dart b/lib/core/utils/server.dart
index 16fa124b..aafba3c3 100644
--- a/lib/core/utils/server.dart
+++ b/lib/core/utils/server.dart
@@ -24,19 +24,12 @@ String decyptPem(List args) {
return sshKey.first.toPem();
}
-enum GenSSHClientStatus {
- socket,
- key,
- pwd,
-}
+enum GenSSHClientStatus { socket, key, pwd }
String getPrivateKey(String id) {
final pki = Stores.key.fetchOne(id);
if (pki == null) {
- throw SSHErr(
- type: SSHErrType.noPrivateKey,
- message: 'key [$id] not found',
- );
+ throw SSHErr(type: SSHErrType.noPrivateKey, message: 'key [$id] not found');
}
return pki.key;
}
@@ -73,36 +66,21 @@ Future genClient(
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
}();
if (jumpSpi_ != null) {
- final jumpClient = await genClient(
- jumpSpi_,
- privateKey: jumpPrivateKey,
- timeout: timeout,
- );
+ final jumpClient = await genClient(jumpSpi_, privateKey: jumpPrivateKey, timeout: timeout);
- return await jumpClient.forwardLocal(
- spi.ip,
- spi.port,
- );
+ return await jumpClient.forwardLocal(spi.ip, spi.port);
}
// Direct
try {
- return await SSHSocket.connect(
- spi.ip,
- spi.port,
- timeout: timeout,
- );
+ return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
} catch (e) {
Loggers.app.warning('genClient', e);
if (spi.alterUrl == null) rethrow;
try {
final res = spi.fromStringUrl();
alterUser = res.$2;
- return await SSHSocket.connect(
- res.$1,
- res.$3,
- timeout: timeout,
- );
+ return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
} catch (e) {
Loggers.app.warning('genClient alterUrl', e);
rethrow;
diff --git a/lib/data/helper/system_detector.dart b/lib/data/helper/system_detector.dart
new file mode 100644
index 00000000..4f9d0d9e
--- /dev/null
+++ b/lib/data/helper/system_detector.dart
@@ -0,0 +1,57 @@
+import 'package:dartssh2/dartssh2.dart';
+import 'package:fl_lib/fl_lib.dart';
+import 'package:server_box/data/model/server/server_private_info.dart';
+import 'package:server_box/data/model/server/system.dart';
+
+/// Helper class for detecting remote system types
+class SystemDetector {
+ /// Detects the system type of a remote server
+ ///
+ /// First checks if a custom system type is configured in [spi].
+ /// If not, attempts to detect the system by running commands:
+ /// 1. 'ver' command to detect Windows
+ /// 2. 'uname -a' command to detect Linux/BSD/Darwin
+ ///
+ /// Returns [SystemType.linux] as default if detection fails.
+ static Future detect(
+ SSHClient client,
+ Spi spi,
+ ) async {
+ // First, check if custom system type is defined
+ SystemType? detectedSystemType = spi.customSystemType;
+ if (detectedSystemType != null) {
+ dprint('Using custom system type ${detectedSystemType.name} for ${spi.oldId}');
+ return detectedSystemType;
+ }
+
+ try {
+ // Try to detect Windows systems first (more reliable detection)
+ final powershellResult = await client.run('ver 2>nul').string;
+ if (powershellResult.isNotEmpty &&
+ (powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
+ detectedSystemType = SystemType.windows;
+ dprint('Detected Windows system type for ${spi.oldId}');
+ return detectedSystemType;
+ }
+
+ // Try to detect Unix/Linux/BSD systems
+ final unixResult = await client.run('uname -a').string;
+ if (unixResult.contains('Linux')) {
+ detectedSystemType = SystemType.linux;
+ dprint('Detected Linux system type for ${spi.oldId}');
+ return detectedSystemType;
+ } else if (unixResult.contains('Darwin') || unixResult.contains('BSD')) {
+ detectedSystemType = SystemType.bsd;
+ dprint('Detected BSD system type for ${spi.oldId}');
+ return detectedSystemType;
+ }
+ } catch (e) {
+ Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
+ }
+
+ // Default fallback
+ detectedSystemType = SystemType.linux;
+ dprint('Defaulting to Linux system type for ${spi.oldId}');
+ return detectedSystemType;
+ }
+}
\ No newline at end of file
diff --git a/lib/data/model/app/bak/backup.dart b/lib/data/model/app/bak/backup.dart
index 41c52048..b7e97040 100644
--- a/lib/data/model/app/bak/backup.dart
+++ b/lib/data/model/app/bak/backup.dart
@@ -213,13 +213,10 @@ class Backup implements Mergeable {
_logger.info('Restore success');
}
- factory Backup.fromJsonString(String raw) =>
- Backup.fromJson(json.decode(_diyDecrypt(raw)));
+ factory Backup.fromJsonString(String raw) => Backup.fromJson(json.decode(_diyDecrypt(raw)));
}
-String _diyEncrypt(String raw) => json.encode(
- raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false),
- );
+String _diyEncrypt(String raw) => json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
String _diyDecrypt(String raw) {
try {
@@ -234,4 +231,3 @@ String _diyDecrypt(String raw) {
rethrow;
}
}
-
diff --git a/lib/data/model/app/bak/backup2.dart b/lib/data/model/app/bak/backup2.dart
index efe6629d..c324d00c 100644
--- a/lib/data/model/app/bak/backup2.dart
+++ b/lib/data/model/app/bak/backup2.dart
@@ -81,7 +81,7 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
if (password != null && password.isNotEmpty) {
result = Cryptor.encrypt(result, password);
}
-
+
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
await File(path).writeAsString(result);
return path;
@@ -94,7 +94,7 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
}
jsonString = Cryptor.decrypt(jsonString, password);
}
-
+
final map = json.decode(jsonString) as Map;
return BackupV2.fromJson(map);
}
diff --git a/lib/data/model/app/bak/backup_source.dart b/lib/data/model/app/bak/backup_source.dart
index 44c8a545..30dadffe 100644
--- a/lib/data/model/app/bak/backup_source.dart
+++ b/lib/data/model/app/bak/backup_source.dart
@@ -7,13 +7,13 @@ import 'package:flutter/material.dart';
abstract class BackupSource {
/// Get content from this source for restore
Future getContent();
-
+
/// Save content to this source for backup
Future saveContent(String filePath);
-
+
/// Display name for this source
String get displayName;
-
+
/// Icon for this source
IconData get icon;
}
@@ -59,4 +59,4 @@ class ClipboardBackupSource implements BackupSource {
@override
IconData get icon => Icons.content_paste;
-}
\ No newline at end of file
+}
diff --git a/lib/data/model/app/error.dart b/lib/data/model/app/error.dart
index 15be42a3..ba609aa4 100644
--- a/lib/data/model/app/error.dart
+++ b/lib/data/model/app/error.dart
@@ -1,29 +1,19 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/context/locale.dart';
-enum SSHErrType {
- unknown,
- connect,
- auth,
- noPrivateKey,
- chdir,
- segements,
- writeScript,
- getStatus,
- ;
-}
+enum SSHErrType { unknown, connect, auth, noPrivateKey, chdir, segements, writeScript, getStatus }
class SSHErr extends Err {
SSHErr({required super.type, super.message});
@override
String? get solution => switch (type) {
- SSHErrType.chdir => l10n.needHomeDir,
- SSHErrType.auth => l10n.authFailTip,
- SSHErrType.writeScript => l10n.writeScriptFailTip,
- SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
- _ => null,
- };
+ SSHErrType.chdir => l10n.needHomeDir,
+ SSHErrType.auth => l10n.authFailTip,
+ SSHErrType.writeScript => l10n.writeScriptFailTip,
+ SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
+ _ => null,
+ };
}
enum ContainerErrType {
@@ -45,11 +35,7 @@ class ContainerErr extends Err {
String? get solution => null;
}
-enum ICloudErrType {
- generic,
- notFound,
- multipleFiles,
-}
+enum ICloudErrType { generic, notFound, multipleFiles }
class ICloudErr extends Err {
ICloudErr({required super.type, super.message});
@@ -58,11 +44,7 @@ class ICloudErr extends Err {
String? get solution => null;
}
-enum WebdavErrType {
- generic,
- notFound,
- ;
-}
+enum WebdavErrType { generic, notFound }
class WebdavErr extends Err {
WebdavErr({required super.type, super.message});
@@ -71,12 +53,7 @@ class WebdavErr extends Err {
String? get solution => null;
}
-enum PveErrType {
- unknown,
- net,
- loginFailed,
- ;
-}
+enum PveErrType { unknown, net, loginFailed }
class PveErr extends Err {
PveErr({required super.type, super.message});
diff --git a/lib/data/model/app/menu/container.dart b/lib/data/model/app/menu/container.dart
index 384ebc15..83c5ee28 100644
--- a/lib/data/model/app/menu/container.dart
+++ b/lib/data/model/app/menu/container.dart
@@ -8,7 +8,7 @@ enum ContainerMenu {
restart,
rm,
logs,
- terminal,
+ terminal
//stats,
;
@@ -27,22 +27,22 @@ enum ContainerMenu {
}
IconData get icon => switch (this) {
- ContainerMenu.start => Icons.play_arrow,
- ContainerMenu.stop => Icons.stop,
- ContainerMenu.restart => Icons.restart_alt,
- ContainerMenu.rm => Icons.delete,
- ContainerMenu.logs => Icons.logo_dev,
- ContainerMenu.terminal => Icons.terminal,
- // DockerMenuType.stats => Icons.bar_chart,
- };
+ ContainerMenu.start => Icons.play_arrow,
+ ContainerMenu.stop => Icons.stop,
+ ContainerMenu.restart => Icons.restart_alt,
+ ContainerMenu.rm => Icons.delete,
+ ContainerMenu.logs => Icons.logo_dev,
+ ContainerMenu.terminal => Icons.terminal,
+ // DockerMenuType.stats => Icons.bar_chart,
+ };
String get toStr => switch (this) {
- ContainerMenu.start => l10n.start,
- ContainerMenu.stop => l10n.stop,
- ContainerMenu.restart => l10n.restart,
- ContainerMenu.rm => libL10n.delete,
- ContainerMenu.logs => libL10n.log,
- ContainerMenu.terminal => l10n.terminal,
- // DockerMenuType.stats => s.stats,
- };
+ ContainerMenu.start => l10n.start,
+ ContainerMenu.stop => l10n.stop,
+ ContainerMenu.restart => l10n.restart,
+ ContainerMenu.rm => libL10n.delete,
+ ContainerMenu.logs => libL10n.log,
+ ContainerMenu.terminal => l10n.terminal,
+ // DockerMenuType.stats => s.stats,
+ };
}
diff --git a/lib/data/model/app/menu/server_func.dart b/lib/data/model/app/menu/server_func.dart
index 0730afcb..5be8b323 100644
--- a/lib/data/model/app/menu/server_func.dart
+++ b/lib/data/model/app/menu/server_func.dart
@@ -12,8 +12,7 @@ enum ServerFuncBtn {
snippet(),
iperf(),
// pve(),
- systemd(1058),
- ;
+ systemd(1058);
final int? addedVersion;
@@ -41,24 +40,24 @@ enum ServerFuncBtn {
].map((e) => e.index).toList();
IconData get icon => switch (this) {
- sftp => Icons.insert_drive_file,
- snippet => Icons.code,
- //pkg => Icons.system_security_update,
- container => FontAwesome.docker_brand,
- process => Icons.list_alt_outlined,
- terminal => Icons.terminal,
- iperf => Icons.speed,
- systemd => MingCute.plugin_2_fill,
- };
+ sftp => Icons.insert_drive_file,
+ snippet => Icons.code,
+ //pkg => Icons.system_security_update,
+ container => FontAwesome.docker_brand,
+ process => Icons.list_alt_outlined,
+ terminal => Icons.terminal,
+ iperf => Icons.speed,
+ systemd => MingCute.plugin_2_fill,
+ };
String get toStr => switch (this) {
- sftp => 'SFTP',
- snippet => l10n.snippet,
- //pkg => l10n.pkg,
- container => l10n.container,
- process => l10n.process,
- terminal => l10n.terminal,
- iperf => 'iperf',
- systemd => 'Systemd',
- };
+ sftp => 'SFTP',
+ snippet => l10n.snippet,
+ //pkg => l10n.pkg,
+ container => l10n.container,
+ process => l10n.process,
+ terminal => l10n.terminal,
+ iperf => 'iperf',
+ systemd => 'Systemd',
+ };
}
diff --git a/lib/data/model/app/net_view.dart b/lib/data/model/app/net_view.dart
index 9d0bf8db..a88d2c6f 100644
--- a/lib/data/model/app/net_view.dart
+++ b/lib/data/model/app/net_view.dart
@@ -8,16 +8,16 @@ enum NetViewType {
traffic;
NetViewType get next => switch (this) {
- conn => speed,
- speed => traffic,
- traffic => conn,
- };
+ conn => speed,
+ speed => traffic,
+ traffic => conn,
+ };
String get toStr => switch (this) {
- NetViewType.conn => l10n.conn,
- NetViewType.traffic => l10n.traffic,
- NetViewType.speed => l10n.speed,
- };
+ NetViewType.conn => l10n.conn,
+ NetViewType.traffic => l10n.traffic,
+ NetViewType.speed => l10n.speed,
+ };
/// If no device is specified, return the cached value (only real devices,
/// such as ethX, wlanX...).
@@ -26,32 +26,17 @@ enum NetViewType {
try {
switch (this) {
case NetViewType.conn:
- return (
- '${l10n.conn}:\n${ss.tcp.maxConn}',
- '${libL10n.fail}:\n${ss.tcp.fail}',
- );
+ return ('${l10n.conn}:\n${ss.tcp.maxConn}', '${libL10n.fail}:\n${ss.tcp.fail}');
case NetViewType.speed:
if (notSepcifyDev) {
- return (
- '↓:\n${ss.netSpeed.cachedVals.speedIn}',
- '↑:\n${ss.netSpeed.cachedVals.speedOut}',
- );
+ return ('↓:\n${ss.netSpeed.cachedVals.speedIn}', '↑:\n${ss.netSpeed.cachedVals.speedOut}');
}
- return (
- '↓:\n${ss.netSpeed.speedIn(device: dev)}',
- '↑:\n${ss.netSpeed.speedOut(device: dev)}',
- );
+ return ('↓:\n${ss.netSpeed.speedIn(device: dev)}', '↑:\n${ss.netSpeed.speedOut(device: dev)}');
case NetViewType.traffic:
if (notSepcifyDev) {
- return (
- '↓:\n${ss.netSpeed.cachedVals.sizeIn}',
- '↑:\n${ss.netSpeed.cachedVals.sizeOut}',
- );
+ return ('↓:\n${ss.netSpeed.cachedVals.sizeIn}', '↑:\n${ss.netSpeed.cachedVals.sizeOut}');
}
- return (
- '↓:\n${ss.netSpeed.sizeIn(device: dev)}',
- '↑:\n${ss.netSpeed.sizeOut(device: dev)}',
- );
+ return ('↓:\n${ss.netSpeed.sizeIn(device: dev)}', '↑:\n${ss.netSpeed.sizeOut(device: dev)}');
}
} catch (e, s) {
Loggers.app.warning('NetViewType.build', e, s);
@@ -60,14 +45,14 @@ enum NetViewType {
}
int toJson() => switch (this) {
- NetViewType.conn => 0,
- NetViewType.speed => 1,
- NetViewType.traffic => 2,
- };
+ NetViewType.conn => 0,
+ NetViewType.speed => 1,
+ NetViewType.traffic => 2,
+ };
static NetViewType fromJson(int json) => switch (json) {
- 0 => NetViewType.conn,
- 1 => NetViewType.speed,
- _ => NetViewType.traffic,
- };
+ 0 => NetViewType.conn,
+ 1 => NetViewType.speed,
+ _ => NetViewType.traffic,
+ };
}
diff --git a/lib/data/model/app/script_builders.dart b/lib/data/model/app/script_builders.dart
new file mode 100644
index 00000000..3946eb48
--- /dev/null
+++ b/lib/data/model/app/script_builders.dart
@@ -0,0 +1,242 @@
+import 'package:server_box/data/model/app/shell_func.dart';
+import 'package:server_box/data/res/build_data.dart';
+
+/// Abstract base class for platform-specific script builders
+abstract class ScriptBuilder {
+ const ScriptBuilder();
+
+ /// Generate a complete script for all shell functions
+ String buildScript(Map? customCmds);
+
+ /// Get the script file name for this platform
+ String get scriptFileName;
+
+ /// Get the command to install the script
+ String getInstallCommand(String scriptDir, String scriptPath);
+
+ /// Get the execution command for a specific function
+ String getExecCommand(String scriptPath, ShellFunc func);
+
+ /// Get custom commands string for this platform
+ String getCustomCmdsString(
+ ShellFunc func,
+ Map? customCmds,
+ );
+}
+
+/// Windows PowerShell script builder
+class WindowsScriptBuilder extends ScriptBuilder {
+ const WindowsScriptBuilder();
+
+ @override
+ String get scriptFileName => 'srvboxm_v${BuildData.script}.ps1';
+
+ @override
+ String getInstallCommand(String scriptDir, String scriptPath) {
+ return 'New-Item -ItemType Directory -Force -Path \'$scriptDir\' | Out-Null; '
+ '\$content = [System.Console]::In.ReadToEnd(); '
+ 'Set-Content -Path \'$scriptPath\' -Value \$content -Encoding UTF8';
+ }
+
+ @override
+ String getExecCommand(String scriptPath, ShellFunc func) {
+ return 'powershell -ExecutionPolicy Bypass -File "$scriptPath" -${func.flag}';
+ }
+
+ @override
+ String getCustomCmdsString(
+ ShellFunc func,
+ Map? customCmds,
+ ) {
+ if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
+ return '\n${customCmds.values.map((cmd) => '\t$cmd').join('\n')}';
+ }
+ return '';
+ }
+
+ @override
+ String buildScript(Map? customCmds) {
+ final sb = StringBuffer();
+ sb.write('''
+# PowerShell script for ServerBox app v1.0.${BuildData.build}
+# DO NOT delete this file while app is running
+
+\$ErrorActionPreference = "SilentlyContinue"
+
+''');
+
+ // Write each function
+ for (final func in ShellFunc.values) {
+ final customCmdsStr = getCustomCmdsString(func, customCmds);
+
+ sb.write('''
+function ${func.name} {
+ ${_getWindowsCommand(func).split('\n').map((e) => e.isEmpty ? '' : ' $e').join('\n')}$customCmdsStr
+}
+
+''');
+ }
+
+ // Write switch case
+ sb.write('''
+switch (\$args[0]) {
+''');
+ for (final func in ShellFunc.values) {
+ sb.write('''
+ "-${func.flag}" { ${func.name} }
+''');
+ }
+ sb.write('''
+ default { Write-Host "Invalid argument \$(\$args[0])" }
+}
+''');
+ return sb.toString();
+ }
+
+ String _getWindowsCommand(ShellFunc func) => switch (func) {
+ ShellFunc.status => WindowsStatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider),
+ ShellFunc.process => 'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json',
+ ShellFunc.shutdown => 'Stop-Computer -Force',
+ ShellFunc.reboot => 'Restart-Computer -Force',
+ ShellFunc.suspend =>
+ 'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState(\'Suspend\', \$false, \$false)',
+ };
+}
+
+/// Unix shell script builder
+class UnixScriptBuilder extends ScriptBuilder {
+ const UnixScriptBuilder();
+
+ @override
+ String get scriptFileName => 'srvboxm_v${BuildData.script}.sh';
+
+ @override
+ String getInstallCommand(String scriptDir, String scriptPath) {
+ return '''
+mkdir -p $scriptDir
+cat > $scriptPath
+chmod 755 $scriptPath
+''';
+ }
+
+ @override
+ String getExecCommand(String scriptPath, ShellFunc func) {
+ return 'sh $scriptPath -${func.flag}';
+ }
+
+ @override
+ String getCustomCmdsString(
+ ShellFunc func,
+ Map? customCmds,
+ ) {
+ if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
+ return '${ShellFunc.cmdDivider}\n\t${customCmds.values.join(ShellFunc.cmdDivider)}';
+ }
+ return '';
+ }
+
+ @override
+ String buildScript(Map? customCmds) {
+ final sb = StringBuffer();
+ sb.write('''
+#!/bin/sh
+# Script for ServerBox app v1.0.${BuildData.build}
+# DO NOT delete this file while app is running
+
+export LANG=en_US.UTF-8
+
+# If macSign & bsdSign are both empty, then it's linux
+macSign=\$(uname -a 2>&1 | grep "Darwin")
+bsdSign=\$(uname -a 2>&1 | grep "BSD")
+
+# Link /bin/sh to busybox?
+isBusybox=\$(ls -l /bin/sh | grep "busybox")
+
+userId=\$(id -u)
+
+exec 2>/dev/null
+
+''');
+ // Write each function
+ for (final func in ShellFunc.values) {
+ final customCmdsStr = getCustomCmdsString(func, customCmds);
+ sb.write('''
+${func.name}() {
+${_getUnixCommand(func).split('\n').map((e) => '\t$e').join('\n')}
+$customCmdsStr
+}
+
+''');
+ }
+
+ // Write switch case
+ sb.write('case \$1 in\n');
+ for (final func in ShellFunc.values) {
+ sb.write('''
+ '-${func.flag}')
+ ${func.name}
+ ;;
+''');
+ }
+ sb.write('''
+ *)
+ echo "Invalid argument \$1"
+ ;;
+esac''');
+ return sb.toString();
+ }
+
+ String _getUnixCommand(ShellFunc func) {
+ switch (func) {
+ case ShellFunc.status:
+ return '''
+if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
+\t${StatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider)}
+else
+\t${BSDStatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider)}
+fi''';
+ case ShellFunc.process:
+ return '''
+if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
+\tif [ "\$isBusybox" != "" ]; then
+\t\tps w
+\telse
+\t\tps -aux
+\tfi
+else
+\tps -ax
+fi
+''';
+ case ShellFunc.shutdown:
+ return '''
+if [ "\$userId" = "0" ]; then
+\tshutdown -h now
+else
+\tsudo -S shutdown -h now
+fi''';
+ case ShellFunc.reboot:
+ return '''
+if [ "\$userId" = "0" ]; then
+\treboot
+else
+\tsudo -S reboot
+fi''';
+ case ShellFunc.suspend:
+ return '''
+if [ "\$userId" = "0" ]; then
+\tsystemctl suspend
+else
+\tsudo -S systemctl suspend
+fi''';
+ }
+ }
+}
+
+/// Factory class to get appropriate script builder for platform
+class ScriptBuilderFactory {
+ const ScriptBuilderFactory._();
+
+ static ScriptBuilder getBuilder(bool isWindows) {
+ return isWindows ? const WindowsScriptBuilder() : const UnixScriptBuilder();
+ }
+}
\ No newline at end of file
diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart
index 92814c38..cb042f47 100644
--- a/lib/data/model/app/shell_func.dart
+++ b/lib/data/model/app/shell_func.dart
@@ -1,31 +1,33 @@
import 'package:server_box/core/extension/context/locale.dart';
+import 'package:server_box/data/model/app/script_builders.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/build_data.dart';
enum ShellFunc {
- status,
+ status('SbStatus'),
//docker,
- process,
- shutdown,
- reboot,
- suspend;
+ process('SbProcess'),
+ shutdown('SbShutdown'),
+ reboot('SbReboot'),
+ suspend('SbSuspend');
+
+ final String name;
+
+ const ShellFunc(this.name);
static const seperator = 'SrvBoxSep';
/// The suffix `\t` is for formatting
static const cmdDivider = '\necho $seperator\n\t';
- /// Cached Linux status commands string
- static final _linuxStatusCmds = StatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
-
- /// Cached BSD status commands string
- static final _bsdStatusCmds = BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
-
/// srvboxm -> ServerBox Mobile
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
+ static const scriptFileWindows = 'srvboxm_v${BuildData.script}.ps1';
static const scriptDirHome = '~/.config/server_box';
static const scriptDirTmp = '/tmp/server_box';
+ static const scriptDirHomeWindows = '%USERPROFILE%/.config/server_box';
+ static const scriptDirTmpWindows = '%TEMP%/server_box';
static final _scriptDirMap = {};
@@ -33,31 +35,38 @@ enum ShellFunc {
///
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
/// it will be changed to [scriptDirHome]/[scriptFile].
- static String getScriptDir(String id) {
+ static String getScriptDir(String id, {SystemType? systemType}) {
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) return customScriptDir;
- _scriptDirMap[id] ??= scriptDirTmp;
+
+ final defaultTmpDir = systemType == SystemType.windows ? scriptDirTmpWindows : scriptDirTmp;
+ _scriptDirMap[id] ??= defaultTmpDir;
return _scriptDirMap[id]!;
}
- static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
+ static void switchScriptDir(String id, {SystemType? systemType}) => switch (_scriptDirMap[id]) {
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
+ scriptDirTmpWindows => _scriptDirMap[id] = scriptDirHomeWindows,
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
- _ => _scriptDirMap[id] = scriptDirHome,
+ scriptDirHomeWindows => _scriptDirMap[id] = scriptDirTmpWindows,
+ _ => _scriptDirMap[id] = systemType == SystemType.windows ? scriptDirHomeWindows : scriptDirHome,
};
- static String getScriptPath(String id) {
- return '${getScriptDir(id)}/$scriptFile';
+ static String getScriptPath(String id, {SystemType? systemType}) {
+ final dir = getScriptDir(id, systemType: systemType);
+ final fileName = systemType == SystemType.windows ? scriptFileWindows : scriptFile;
+ final separator = systemType == SystemType.windows ? '\\' : '/';
+ return '$dir$separator$fileName';
}
- static String getInstallShellCmd(String id) {
- final scriptDir = getScriptDir(id);
- final scriptPath = '$scriptDir/$scriptFile';
- return '''
-mkdir -p $scriptDir
-cat > $scriptPath
-chmod 755 $scriptPath
-''';
+ static String getInstallShellCmd(String id, {SystemType? systemType}) {
+ final scriptDir = getScriptDir(id, systemType: systemType);
+ final isWindows = systemType == SystemType.windows;
+ final builder = ScriptBuilderFactory.getBuilder(isWindows);
+ final separator = isWindows ? '\\' : '/';
+ final scriptPath = '$scriptDir$separator${builder.scriptFileName}';
+
+ return builder.getInstallCommand(scriptDir, scriptPath);
}
String get flag => switch (this) {
@@ -69,120 +78,24 @@ chmod 755 $scriptPath
// ShellFunc.docker=> 'd',
};
- String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
-
- String get name => switch (this) {
- ShellFunc.status => 'status',
- ShellFunc.process => 'process',
- ShellFunc.shutdown => 'ShutDown',
- ShellFunc.reboot => 'Reboot',
- ShellFunc.suspend => 'Suspend',
- };
-
- String get _cmd => switch (this) {
- ShellFunc.status =>
- '''
-if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
-\t$_linuxStatusCmds
-else
-\t$_bsdStatusCmds
-fi''',
- ShellFunc.process =>
- '''
-if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
-\tif [ "\$isBusybox" != "" ]; then
-\t\tps w
-\telse
-\t\tps -aux
-\tfi
-else
-\tps -ax
-fi
-''',
- ShellFunc.shutdown =>
- '''
-if [ "\$userId" = "0" ]; then
-\tshutdown -h now
-else
-\tsudo -S shutdown -h now
-fi''',
- ShellFunc.reboot =>
- '''
-if [ "\$userId" = "0" ]; then
-\treboot
-else
-\tsudo -S reboot
-fi''',
- ShellFunc.suspend =>
- '''
-if [ "\$userId" = "0" ]; then
-\tsystemctl suspend
-else
-\tsudo -S systemctl suspend
-fi''',
- };
-
- static String allScript(Map? customCmds) {
- final sb = StringBuffer();
- sb.write('''
-#!/bin/sh
-# Script for ServerBox app v1.0.${BuildData.build}
-# DO NOT delete this file while app is running
-
-export LANG=en_US.UTF-8
-
-# If macSign & bsdSign are both empty, then it's linux
-macSign=\$(uname -a 2>&1 | grep "Darwin")
-bsdSign=\$(uname -a 2>&1 | grep "BSD")
-
-# Link /bin/sh to busybox?
-isBusybox=\$(ls -l /bin/sh | grep "busybox")
-
-userId=\$(id -u)
-
-exec 2>/dev/null
-
-''');
- // Write each func
- for (final func in values) {
- final customCmdsStr = () {
- if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
- return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
- }
- return '';
- }();
- sb.write('''
-${func.name}() {
-${func._cmd.split('\n').map((e) => '\t$e').join('\n')}
-$customCmdsStr
-}
-
-''');
- }
-
- // Write switch case
- sb.write('case \$1 in\n');
- for (final func in values) {
- sb.write('''
- '-${func.flag}')
- ${func.name}
- ;;
-''');
- }
- sb.write('''
- *)
- echo "Invalid argument \$1"
- ;;
-esac''');
- return sb.toString();
+ String exec(String id, {SystemType? systemType}) {
+ final scriptPath = getScriptPath(id, systemType: systemType);
+ final isWindows = systemType == SystemType.windows;
+ final builder = ScriptBuilderFactory.getBuilder(isWindows);
+
+ return builder.getExecCommand(scriptPath, this);
}
-}
-extension EnumX on Enum {
- /// Find out the required segment from [segments]
- String find(List segments) {
- return segments[index];
+
+
+ /// Generate script based on system type
+ static String allScript(Map? customCmds, {SystemType? systemType}) {
+ final isWindows = systemType == SystemType.windows;
+ final builder = ScriptBuilderFactory.getBuilder(isWindows);
+
+ return builder.buildScript(customCmds);
}
+
}
enum StatusCmdType {
@@ -193,7 +106,10 @@ enum StatusCmdType {
cpu._('cat /proc/stat | grep cpu'),
uptime._('uptime'),
conn._('cat /proc/net/snmp'),
- disk._('lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID'),
+ disk._(
+ 'lsblk --bytes --json --output '
+ 'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
+ ),
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
@@ -201,7 +117,16 @@ enum StatusCmdType {
diskio._('cat /proc/diskstats'),
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
nvidia._('nvidia-smi -q -x'),
- amd._('if command -v amd-smi >/dev/null 2>&1; then amd-smi list --json && amd-smi metric --json; elif command -v rocm-smi >/dev/null 2>&1; then rocm-smi --json || rocm-smi --showunique --showuse --showtemp --showfan --showclocks --showmemuse --showpower; elif command -v radeontop >/dev/null 2>&1; then timeout 2s radeontop -d - -l 1 | tail -n +2; else echo "No AMD GPU monitoring tools found"; fi'),
+ amd._(
+ 'if command -v amd-smi >/dev/null 2>&1; then '
+ 'amd-smi list --json && amd-smi metric --json; '
+ 'elif command -v rocm-smi >/dev/null 2>&1; then '
+ 'rocm-smi --json || rocm-smi --showunique --showuse --showtemp '
+ '--showfan --showclocks --showmemuse --showpower; '
+ 'elif command -v radeontop >/dev/null 2>&1; then '
+ 'timeout 2s radeontop -d - -l 1 | tail -n +2; '
+ 'else echo "No AMD GPU monitoring tools found"; fi',
+ ),
sensors._('sensors'),
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
@@ -241,3 +166,77 @@ extension StatusCmdTypeX on StatusCmdType {
final val => val.name,
};
}
+
+enum WindowsStatusCmdType {
+ echo._('echo ${SystemType.windowsSign}'),
+ time._('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),
+ net._(
+ r'Get-Counter -Counter '
+ r'"\\NetworkInterface(*)\\Bytes Received/sec", '
+ r'"\\NetworkInterface(*)\\Bytes Sent/sec" '
+ r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
+ ),
+ sys._('(Get-ComputerInfo).OsName'),
+ cpu._(
+ 'Get-WmiObject -Class Win32_Processor | '
+ 'Select-Object Name, LoadPercentage | ConvertTo-Json',
+ ),
+ uptime._('(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime'),
+ conn._('(netstat -an | findstr ESTABLISHED | Measure-Object -Line).Count'),
+ disk._(
+ 'Get-WmiObject -Class Win32_LogicalDisk | '
+ 'Select-Object DeviceID, Size, FreeSpace, FileSystem | ConvertTo-Json',
+ ),
+ mem._(
+ 'Get-WmiObject -Class Win32_OperatingSystem | '
+ 'Select-Object TotalVisibleMemorySize, FreePhysicalMemory | ConvertTo-Json',
+ ),
+ temp._(
+ 'Get-CimInstance -ClassName MSAcpi_ThermalZoneTemperature '
+ '-Namespace root/wmi -ErrorAction SilentlyContinue | '
+ 'Select-Object InstanceName, @{Name=\'Temperature\';'
+ 'Expression={[math]::Round((\$_.CurrentTemperature - 2732) / 10, 1)}} | '
+ 'ConvertTo-Json',
+ ),
+ host._(r'Write-Output $env:COMPUTERNAME'),
+ diskio._(
+ r'Get-Counter -Counter '
+ r'"\\PhysicalDisk(*)\\Disk Read Bytes/sec", '
+ r'"\\PhysicalDisk(*)\\Disk Write Bytes/sec" '
+ r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
+ ),
+ battery._(
+ 'Get-WmiObject -Class Win32_Battery | '
+ 'Select-Object EstimatedChargeRemaining, BatteryStatus | ConvertTo-Json',
+ ),
+ nvidia._(
+ 'if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { '
+ 'nvidia-smi -q -x } else { echo "NVIDIA driver not found" }',
+ ),
+ amd._(
+ 'if (Get-Command amd-smi -ErrorAction SilentlyContinue) { '
+ 'amd-smi list --json } else { echo "AMD driver not found" }',
+ ),
+ sensors._(
+ 'Get-CimInstance -ClassName Win32_TemperatureProbe '
+ '-ErrorAction SilentlyContinue | '
+ 'Select-Object Name, CurrentReading | ConvertTo-Json',
+ ),
+ diskSmart._(
+ 'Get-PhysicalDisk | Get-StorageReliabilityCounter | '
+ 'Select-Object DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours | '
+ 'ConvertTo-Json',
+ ),
+ cpuBrand._('(Get-WmiObject -Class Win32_Processor).Name');
+
+ final String cmd;
+
+ const WindowsStatusCmdType._(this.cmd);
+}
+
+extension EnumX on Enum {
+ /// Find out the required segment from [segments]
+ String find(List segments) {
+ return segments[index];
+ }
+}
diff --git a/lib/data/model/app/tab.dart b/lib/data/model/app/tab.dart
index 0c36122a..94ab9d0d 100644
--- a/lib/data/model/app/tab.dart
+++ b/lib/data/model/app/tab.dart
@@ -12,7 +12,7 @@ enum AppTab {
server,
ssh,
file,
- snippet,
+ snippet
//settings,
;
@@ -29,60 +29,60 @@ enum AppTab {
NavigationDestination get navDestination {
return switch (this) {
server => NavigationDestination(
- icon: const Icon(BoxIcons.bx_server),
- label: l10n.server,
- selectedIcon: const Icon(BoxIcons.bxs_server),
- ),
+ icon: const Icon(BoxIcons.bx_server),
+ label: l10n.server,
+ selectedIcon: const Icon(BoxIcons.bxs_server),
+ ),
// settings => NavigationDestination(
// icon: const Icon(Icons.settings),
// label: libL10n.setting,
// selectedIcon: const Icon(Icons.settings),
// ),
ssh => const NavigationDestination(
- icon: Icon(Icons.terminal_outlined),
- label: 'SSH',
- selectedIcon: Icon(Icons.terminal),
- ),
+ icon: Icon(Icons.terminal_outlined),
+ label: 'SSH',
+ selectedIcon: Icon(Icons.terminal),
+ ),
snippet => NavigationDestination(
- icon: const Icon(Icons.code),
- label: l10n.snippet,
- selectedIcon: const Icon(Icons.code),
- ),
+ icon: const Icon(Icons.code),
+ label: l10n.snippet,
+ selectedIcon: const Icon(Icons.code),
+ ),
file => NavigationDestination(
- icon: const Icon(Icons.folder_open),
- label: libL10n.file,
- selectedIcon: const Icon(Icons.folder),
- ),
+ icon: const Icon(Icons.folder_open),
+ label: libL10n.file,
+ selectedIcon: const Icon(Icons.folder),
+ ),
};
}
NavigationRailDestination get navRailDestination {
return switch (this) {
server => NavigationRailDestination(
- icon: const Icon(BoxIcons.bx_server),
- label: Text(l10n.server),
- selectedIcon: const Icon(BoxIcons.bxs_server),
- ),
+ icon: const Icon(BoxIcons.bx_server),
+ label: Text(l10n.server),
+ selectedIcon: const Icon(BoxIcons.bxs_server),
+ ),
// settings => NavigationRailDestination(
// icon: const Icon(Icons.settings),
// label: libL10n.setting,
// selectedIcon: const Icon(Icons.settings),
// ),
ssh => const NavigationRailDestination(
- icon: Icon(Icons.terminal_outlined),
- label: Text('SSH'),
- selectedIcon: Icon(Icons.terminal),
- ),
+ icon: Icon(Icons.terminal_outlined),
+ label: Text('SSH'),
+ selectedIcon: Icon(Icons.terminal),
+ ),
snippet => NavigationRailDestination(
- icon: const Icon(Icons.code),
- label: Text(l10n.snippet),
- selectedIcon: const Icon(Icons.code),
- ),
+ icon: const Icon(Icons.code),
+ label: Text(l10n.snippet),
+ selectedIcon: const Icon(Icons.code),
+ ),
file => NavigationRailDestination(
- icon: const Icon(Icons.folder_open),
- label: Text(libL10n.file),
- selectedIcon: const Icon(Icons.folder),
- ),
+ icon: const Icon(Icons.folder_open),
+ label: Text(libL10n.file),
+ selectedIcon: const Icon(Icons.folder),
+ ),
};
}
diff --git a/lib/data/model/container/image.dart b/lib/data/model/container/image.dart
index 1df001c9..d2b8e5e5 100644
--- a/lib/data/model/container/image.dart
+++ b/lib/data/model/container/image.dart
@@ -24,14 +24,7 @@ final class PodmanImg implements ContainerImg {
final int? size;
final int? containers;
- PodmanImg({
- this.repository,
- this.tag,
- this.id,
- this.created,
- this.size,
- this.containers,
- });
+ PodmanImg({this.repository, this.tag, this.id, this.created, this.size, this.containers});
@override
String? get sizeMB => size?.bytes2Str;
@@ -39,28 +32,27 @@ final class PodmanImg implements ContainerImg {
@override
int? get containersCount => containers;
- factory PodmanImg.fromRawJson(String str) =>
- PodmanImg.fromJson(json.decode(str));
+ factory PodmanImg.fromRawJson(String str) => PodmanImg.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PodmanImg.fromJson(Map json) => PodmanImg(
- repository: json['repository'],
- tag: json['tag'],
- id: json['Id'],
- created: json['Created'],
- size: json['Size'],
- containers: json['Containers'],
- );
+ repository: json['repository'],
+ tag: json['tag'],
+ id: json['Id'],
+ created: json['Created'],
+ size: json['Size'],
+ containers: json['Containers'],
+ );
Map toJson() => {
- 'repository': repository,
- 'tag': tag,
- 'Id': id,
- 'Created': created,
- 'Size': size,
- 'Containers': containers,
- };
+ 'repository': repository,
+ 'tag': tag,
+ 'Id': id,
+ 'Created': created,
+ 'Size': size,
+ 'Containers': containers,
+ };
}
final class DockerImg implements ContainerImg {
@@ -87,11 +79,9 @@ final class DockerImg implements ContainerImg {
String? get sizeMB => size;
@override
- int? get containersCount =>
- containers == 'N/A' ? 0 : int.tryParse(containers);
+ int? get containersCount => containers == 'N/A' ? 0 : int.tryParse(containers);
- factory DockerImg.fromRawJson(String str) =>
- DockerImg.fromJson(json.decode(str));
+ factory DockerImg.fromRawJson(String str) => DockerImg.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
@@ -121,11 +111,11 @@ final class DockerImg implements ContainerImg {
}
Map toJson() => {
- 'Containers': containers,
- 'CreatedAt': createdAt,
- 'ID': id,
- 'Repository': repository,
- 'Size': size,
- 'Tag': tag,
- };
+ 'Containers': containers,
+ 'CreatedAt': createdAt,
+ 'ID': id,
+ 'Repository': repository,
+ 'Size': size,
+ 'Tag': tag,
+ };
}
diff --git a/lib/data/model/container/ps.dart b/lib/data/model/container/ps.dart
index 9d5865a9..3833c467 100644
--- a/lib/data/model/container/ps.dart
+++ b/lib/data/model/container/ps.dart
@@ -42,15 +42,7 @@ final class PodmanPs implements ContainerPs {
@override
String? disk;
- PodmanPs({
- this.command,
- this.created,
- this.exited,
- this.id,
- this.image,
- this.names,
- this.startedAt,
- });
+ PodmanPs({this.command, this.created, this.exited, this.id, this.image, this.names, this.startedAt});
@override
String? get name => names?.firstOrNull;
@@ -78,36 +70,29 @@ final class PodmanPs implements ContainerPs {
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
}
- factory PodmanPs.fromRawJson(String str) =>
- PodmanPs.fromJson(json.decode(str));
+ factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PodmanPs.fromJson(Map json) => PodmanPs(
- command: json['Command'] == null
- ? []
- : List.from(json['Command']!.map((x) => x)),
- created:
- json['Created'] == null ? null : DateTime.parse(json['Created']),
- exited: json['Exited'],
- id: json['Id'],
- image: json['Image'],
- names: json['Names'] == null
- ? []
- : List.from(json['Names']!.map((x) => x)),
- startedAt: json['StartedAt'],
- );
+ command: json['Command'] == null ? [] : List.from(json['Command']!.map((x) => x)),
+ created: json['Created'] == null ? null : DateTime.parse(json['Created']),
+ exited: json['Exited'],
+ id: json['Id'],
+ image: json['Image'],
+ names: json['Names'] == null ? [] : List.from(json['Names']!.map((x) => x)),
+ startedAt: json['StartedAt'],
+ );
Map toJson() => {
- 'Command':
- command == null ? [] : List.from(command!.map((x) => x)),
- 'Created': created?.toIso8601String(),
- 'Exited': exited,
- 'Id': id,
- 'Image': image,
- 'Names': names == null ? [] : List.from(names!.map((x) => x)),
- 'StartedAt': startedAt,
- };
+ 'Command': command == null ? [] : List.from(command!.map((x) => x)),
+ 'Created': created?.toIso8601String(),
+ 'Exited': exited,
+ 'Id': id,
+ 'Image': image,
+ 'Names': names == null ? [] : List.from(names!.map((x) => x)),
+ 'StartedAt': startedAt,
+ };
}
final class DockerPs implements ContainerPs {
@@ -127,12 +112,7 @@ final class DockerPs implements ContainerPs {
@override
String? disk;
- DockerPs({
- this.id,
- this.image,
- this.names,
- this.state,
- });
+ DockerPs({this.id, this.image, this.names, this.state});
@override
String? get name => names;
@@ -159,11 +139,6 @@ final class DockerPs implements ContainerPs {
/// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
factory DockerPs.parse(String raw) {
final parts = raw.split(Miscs.multiBlankreg);
- return DockerPs(
- id: parts[0],
- state: parts[1],
- names: parts[2],
- image: parts[3].trim(),
- );
+ return DockerPs(id: parts[0], state: parts[1], names: parts[2], image: parts[3].trim());
}
}
diff --git a/lib/data/model/container/type.dart b/lib/data/model/container/type.dart
index 02b97ef1..967952f4 100644
--- a/lib/data/model/container/type.dart
+++ b/lib/data/model/container/type.dart
@@ -3,16 +3,15 @@ import 'package:server_box/data/model/container/ps.dart';
enum ContainerType {
docker,
- podman,
- ;
+ podman;
ContainerPs Function(String str) get ps => switch (this) {
- ContainerType.docker => DockerPs.parse,
- ContainerType.podman => PodmanPs.fromRawJson,
- };
+ ContainerType.docker => DockerPs.parse,
+ ContainerType.podman => PodmanPs.fromRawJson,
+ };
ContainerImg Function(String str) get img => switch (this) {
- ContainerType.docker => DockerImg.fromRawJson,
- ContainerType.podman => PodmanImg.fromRawJson,
- };
+ ContainerType.docker => DockerImg.fromRawJson,
+ ContainerType.podman => PodmanImg.fromRawJson,
+ };
}
diff --git a/lib/data/model/pkg/manager.dart b/lib/data/model/pkg/manager.dart
index 32abb3b4..a8b58f7e 100644
--- a/lib/data/model/pkg/manager.dart
+++ b/lib/data/model/pkg/manager.dart
@@ -62,8 +62,7 @@ enum PkgManager {
case PkgManager.yum:
list = list.sublist(2);
list.removeWhere((element) => element.isEmpty);
- final endLine = list.lastIndexWhere(
- (element) => element.contains('Obsoleting Packages'));
+ final endLine = list.lastIndexWhere((element) => element.contains('Obsoleting Packages'));
if (endLine != -1 && list.isNotEmpty) {
list = list.sublist(0, endLine);
}
@@ -71,8 +70,7 @@ enum PkgManager {
case PkgManager.apt:
// avoid other outputs
// such as: [Could not chdir to home directory /home/test: No such file or directory, , WARNING: apt does not have a stable CLI interface. Use with caution in scripts., , Listing...]
- final idx =
- list.indexWhere((element) => element.contains('[upgradable from:'));
+ final idx = list.indexWhere((element) => element.contains('[upgradable from:'));
if (idx == -1) {
return [];
}
diff --git a/lib/data/model/server/amd.dart b/lib/data/model/server/amd.dart
index f822b938..94d4ed81 100644
--- a/lib/data/model/server/amd.dart
+++ b/lib/data/model/server/amd.dart
@@ -32,7 +32,7 @@ class AmdSmi {
try {
final jsonData = json.decode(raw);
if (jsonData is! List) return [];
-
+
return jsonData
.map((gpu) => _parseGpuItem(gpu))
.where((item) => item != null)
@@ -47,28 +47,28 @@ class AmdSmi {
try {
final name = gpu['name'] ?? gpu['card_model'] ?? gpu['device_name'] ?? 'Unknown AMD GPU';
final deviceId = gpu['device_id']?.toString() ?? gpu['gpu_id']?.toString() ?? '0';
-
+
// Temperature parsing
final tempRaw = gpu['temperature'] ?? gpu['temp'] ?? gpu['gpu_temp'];
final temp = _parseIntValue(tempRaw);
-
+
// Power parsing
final powerDraw = gpu['power_draw'] ?? gpu['current_power'];
final powerCap = gpu['power_cap'] ?? gpu['power_limit'] ?? gpu['max_power'];
final power = _formatPower(powerDraw, powerCap);
-
+
// Memory parsing
final memory = _parseMemory(gpu['memory'] ?? gpu['vram'] ?? {});
-
+
// Utilization parsing
final utilization = _parseIntValue(gpu['utilization'] ?? gpu['gpu_util'] ?? gpu['activity']);
-
+
// Fan speed parsing
final fanSpeed = _parseIntValue(gpu['fan_speed'] ?? gpu['fan_rpm']);
-
+
// Clock speed parsing
final clockSpeed = _parseIntValue(gpu['clock_speed'] ?? gpu['gpu_clock'] ?? gpu['sclk']);
-
+
return AmdSmiItem(
deviceId: deviceId,
name: name,
@@ -98,7 +98,7 @@ class AmdSmi {
static String _formatPower(dynamic draw, dynamic cap) {
final drawValue = _parseIntValue(draw);
final capValue = _parseIntValue(cap);
-
+
if (drawValue == 0 && capValue == 0) return 'N/A';
if (capValue == 0) return '${drawValue}W';
return '${drawValue}W / ${capValue}W';
@@ -108,7 +108,7 @@ class AmdSmi {
final total = _parseIntValue(memData['total'] ?? memData['total_memory']);
final used = _parseIntValue(memData['used'] ?? memData['used_memory']);
final unit = memData['unit']?.toString() ?? 'MB';
-
+
final processes = [];
final processesData = memData['processes'];
if (processesData is List) {
@@ -119,7 +119,7 @@ class AmdSmi {
}
}
}
-
+
return AmdSmiMem(total, used, unit, processes);
}
@@ -127,7 +127,7 @@ class AmdSmi {
final pid = _parseIntValue(procData['pid']);
final name = procData['name']?.toString() ?? procData['process_name']?.toString() ?? 'Unknown';
final memory = _parseIntValue(procData['memory'] ?? procData['used_memory']);
-
+
if (pid == 0) return null;
return AmdSmiMemProcess(pid, name, memory);
}
@@ -185,4 +185,4 @@ class AmdSmiMemProcess {
String toString() {
return 'AmdSmiMemProcess{pid: $pid, name: $name, memory: $memory}';
}
-}
\ No newline at end of file
+}
diff --git a/lib/data/model/server/battery.dart b/lib/data/model/server/battery.dart
index 6dbcf01d..487ab5f7 100644
--- a/lib/data/model/server/battery.dart
+++ b/lib/data/model/server/battery.dart
@@ -19,13 +19,7 @@ class Battery {
final int? cycle;
final String? tech;
- const Battery({
- required this.status,
- this.percent,
- this.name,
- this.cycle,
- this.tech,
- });
+ const Battery({required this.status, this.percent, this.name, this.cycle, this.tech});
factory Battery.fromRaw(String raw) {
final lines = raw.split('\n');
@@ -63,8 +57,7 @@ enum BatteryStatus {
charging,
discharging,
full,
- unknown,
- ;
+ unknown;
static BatteryStatus parse(String? status) {
switch (status) {
diff --git a/lib/data/model/server/conn.dart b/lib/data/model/server/conn.dart
index 320e4fc6..5750066e 100644
--- a/lib/data/model/server/conn.dart
+++ b/lib/data/model/server/conn.dart
@@ -6,17 +6,11 @@ class Conn {
final int passive;
final int fail;
- const Conn({
- required this.maxConn,
- required this.active,
- required this.passive,
- required this.fail,
- });
+ const Conn({required this.maxConn, required this.active, required this.passive, required this.fail});
static Conn? parse(String raw) {
final lines = raw.split('\n');
- final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
- orElse: () => '');
+ final idx = lines.lastWhere((element) => element.startsWith('Tcp:'), orElse: () => '');
if (idx != '') {
final vals = idx.split(Miscs.blankReg);
return Conn(
diff --git a/lib/data/model/server/cpu.dart b/lib/data/model/server/cpu.dart
index 97066cd7..684e0854 100644
--- a/lib/data/model/server/cpu.dart
+++ b/lib/data/model/server/cpu.dart
@@ -200,22 +200,98 @@ final class CpuBrand {
}
final _bsdCpuPercentReg = RegExp(r'(\d+\.\d+)%');
+final _macCpuPercentReg = RegExp(
+ r'CPU usage: ([\d.]+)% user, ([\d.]+)% sys, ([\d.]+)% idle');
+final _freebsdCpuPercentReg = RegExp(
+ r'CPU: ([\d.]+)% user, ([\d.]+)% nice, ([\d.]+)% system, '
+ r'([\d.]+)% interrupt, ([\d.]+)% idle');
-/// TODO: Change this implementation to parse cpu status on BSD system
+/// Parse CPU status on BSD system with support for different BSD variants
///
-/// [raw]:
-/// CPU usage: 14.70% user, 12.76% sys, 72.52% idle
+/// Supports multiple formats:
+/// - macOS: "CPU usage: 14.70% user, 12.76% sys, 72.52% idle"
+/// - FreeBSD: "CPU: 5.2% user, 0.0% nice, 3.1% system, 0.1% interrupt, 91.6% idle"
+/// - Generic BSD: fallback to percentage extraction
Cpus parseBsdCpu(String raw) {
+ final init = InitStatus.cpus;
+
+ // Try macOS format first
+ final macMatch = _macCpuPercentReg.firstMatch(raw);
+ if (macMatch != null) {
+ final userPercent = double.parse(macMatch.group(1)!).toInt();
+ final sysPercent = double.parse(macMatch.group(2)!).toInt();
+ final idlePercent = double.parse(macMatch.group(3)!).toInt();
+
+ init.add([
+ SingleCpuCore(
+ 'cpu0',
+ userPercent,
+ sysPercent,
+ 0, // nice
+ idlePercent,
+ 0, // iowait
+ 0, // irq
+ 0, // softirq
+ ),
+ ]);
+ return init;
+ }
+
+ // Try FreeBSD format
+ final freebsdMatch = _freebsdCpuPercentReg.firstMatch(raw);
+ if (freebsdMatch != null) {
+ final userPercent = double.parse(freebsdMatch.group(1)!).toInt();
+ final nicePercent = double.parse(freebsdMatch.group(2)!).toInt();
+ final sysPercent = double.parse(freebsdMatch.group(3)!).toInt();
+ final irqPercent = double.parse(freebsdMatch.group(4)!).toInt();
+ final idlePercent = double.parse(freebsdMatch.group(5)!).toInt();
+
+ init.add([
+ SingleCpuCore(
+ 'cpu0',
+ userPercent,
+ sysPercent,
+ nicePercent,
+ idlePercent,
+ 0, // iowait
+ irqPercent,
+ 0, // softirq
+ ),
+ ]);
+ return init;
+ }
+
+ // Fallback to generic percentage extraction
final percents = _bsdCpuPercentReg
.allMatches(raw)
- .map((e) => double.parse(e.group(1) ?? '0') * 100)
+ .map((e) => double.parse(e.group(1) ?? '0'))
.toList();
- if (percents.length != 3) return InitStatus.cpus;
-
- final init = InitStatus.cpus;
- init.add([
- SingleCpuCore('cpu', percents[0].toInt(), 0, 0,
- percents[2].toInt() + percents[1].toInt(), 0, 0, 0),
- ]);
+
+ if (percents.length >= 3) {
+ // Validate that percentages are reasonable (0-100 range)
+ final validPercents = percents.where((p) => p >= 0 && p <= 100).toList();
+ if (validPercents.length != percents.length) {
+ Loggers.app.warning('BSD CPU fallback parsing found invalid percentages in: $raw');
+ }
+
+ init.add([
+ SingleCpuCore(
+ 'cpu0',
+ percents[0].toInt(), // user
+ percents.length > 1 ? percents[1].toInt() : 0, // sys
+ 0, // nice
+ percents.length > 2 ? percents[2].toInt() : 0, // idle
+ 0, // iowait
+ 0, // irq
+ 0, // softirq
+ ),
+ ]);
+ return init;
+ } else if (percents.isNotEmpty) {
+ Loggers.app.warning('BSD CPU fallback parsing found ${percents.length} percentages (expected at least 3) in: $raw');
+ } else {
+ Loggers.app.warning('BSD CPU fallback parsing found no percentages in: $raw');
+ }
+
return init;
}
diff --git a/lib/data/model/server/disk.dart b/lib/data/model/server/disk.dart
index 6f7001e5..2b7f0c7d 100644
--- a/lib/data/model/server/disk.dart
+++ b/lib/data/model/server/disk.dart
@@ -70,14 +70,14 @@ class Disk with EquatableMixin {
if (disk != null) {
list.add(disk);
}
-
+
// For devices with children (like physical disks with partitions),
// also process each child individually to ensure BTRFS RAID disks are properly handled
final List childDevices = device['children'] ?? [];
for (final childDevice in childDevices) {
final String childPath = childDevice['path']?.toString() ?? '';
final String childFsType = childDevice['fstype']?.toString() ?? '';
-
+
// If this is a BTRFS partition, add it directly to ensure it's properly represented
if (childFsType == 'btrfs' && childPath.isNotEmpty) {
final childDisk = _processSingleDevice(childDevice);
@@ -93,11 +93,11 @@ class Disk with EquatableMixin {
final fstype = device['fstype']?.toString();
final String mountpoint = device['mountpoint']?.toString() ?? '';
final String path = device['path']?.toString() ?? '';
-
+
if (path.isEmpty || (fstype == null && mountpoint.isEmpty)) {
return null;
}
-
+
if (!_shouldCalc(fstype ?? '', mountpoint)) {
return null;
}
@@ -154,8 +154,7 @@ class Disk with EquatableMixin {
}
// Handle common filesystem cases or parent devices with children
- if ((fstype != null && _shouldCalc(fstype, mount)) ||
- (childDisks.isNotEmpty && path.isNotEmpty)) {
+ if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {
final sizeStr = device['fssize']?.toString() ?? '0';
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
@@ -221,14 +220,16 @@ class Disk with EquatableMixin {
final fs = vals[0];
final mount = vals[5];
if (!_shouldCalc(fs, mount)) continue;
- list.add(Disk(
- path: fs,
- mount: mount,
- usedPercent: int.parse(vals[4].replaceFirst('%', '')),
- used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),
- size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),
- avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),
- ));
+ list.add(
+ Disk(
+ path: fs,
+ mount: mount,
+ usedPercent: int.parse(vals[4].replaceFirst('%', '')),
+ used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),
+ size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),
+ avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),
+ ),
+ );
} catch (e) {
continue;
}
@@ -237,8 +238,19 @@ class Disk with EquatableMixin {
}
@override
- List get props =>
- [path, name, kname, fsTyp, mount, usedPercent, used, size, avail, uuid, children];
+ List get props => [
+ path,
+ name,
+ kname,
+ fsTyp,
+ mount,
+ usedPercent,
+ used,
+ size,
+ avail,
+ uuid,
+ children,
+ ];
}
class DiskIO extends TimeSeq> {
@@ -314,12 +326,14 @@ class DiskIO extends TimeSeq> {
try {
final dev = vals[2];
if (dev.startsWith('loop')) continue;
- items.add(DiskIOPiece(
- dev: dev,
- sectorsRead: int.parse(vals[5]),
- sectorsWrite: int.parse(vals[9]),
- time: time,
- ));
+ items.add(
+ DiskIOPiece(
+ dev: dev,
+ sectorsRead: int.parse(vals[5]),
+ sectorsWrite: int.parse(vals[9]),
+ time: time,
+ ),
+ );
} catch (e) {
continue;
}
@@ -334,12 +348,7 @@ class DiskIOPiece extends TimeSeqIface {
final int sectorsWrite;
final int time;
- DiskIOPiece({
- required this.dev,
- required this.sectorsRead,
- required this.sectorsWrite,
- required this.time,
- });
+ DiskIOPiece({required this.dev, required this.sectorsRead, required this.sectorsWrite, required this.time});
@override
bool same(DiskIOPiece other) => dev == other.dev;
@@ -349,10 +358,7 @@ class DiskUsage {
final BigInt used;
final BigInt size;
- DiskUsage({
- required this.used,
- required this.size,
- });
+ DiskUsage({required this.used, required this.size});
double get usedPercent {
// Avoid division by zero
diff --git a/lib/data/model/server/dist.dart b/lib/data/model/server/dist.dart
index 515228b1..f929c5dc 100644
--- a/lib/data/model/server/dist.dart
+++ b/lib/data/model/server/dist.dart
@@ -12,7 +12,6 @@ enum Dist {
rocky,
deepin,
coreelec,
- ;
}
extension StringX on String {
@@ -34,6 +33,4 @@ extension StringX on String {
// Special rules
-const _wrts = [
- 'istoreos',
-];
+const _wrts = ['istoreos'];
diff --git a/lib/data/model/server/memory.dart b/lib/data/model/server/memory.dart
index 624e7fc5..ccbfb3cb 100644
--- a/lib/data/model/server/memory.dart
+++ b/lib/data/model/server/memory.dart
@@ -5,11 +5,7 @@ class Memory {
final int free;
final int avail;
- const Memory({
- required this.total,
- required this.free,
- required this.avail,
- });
+ const Memory({required this.total, required this.free, required this.avail});
double get availPercent {
if (avail == 0) {
@@ -23,46 +19,99 @@ class Memory {
static Memory parse(String raw) {
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
- final total = int.tryParse(items
- .firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')
- ?.group(2) ??
- '1') ??
- 1;
- final free = int.tryParse(items
- .firstWhereOrNull((e) => e?.group(1) == 'MemFree:')
- ?.group(2) ??
- '0') ??
- 0;
- final available = int.tryParse(items
- .firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')
- ?.group(2) ??
- '0') ??
- 0;
+ final total = int.tryParse(
+ items.firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')
+ ?.group(2) ?? '1') ?? 1;
+ final free = int.tryParse(
+ items.firstWhereOrNull((e) => e?.group(1) == 'MemFree:')
+ ?.group(2) ?? '0') ?? 0;
+ final available = int.tryParse(
+ items.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')
+ ?.group(2) ?? '0') ?? 0;
- return Memory(
- total: total,
- free: free,
- avail: available,
- );
+ return Memory(total: total, free: free, avail: available);
}
}
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
+/// Parse BSD/macOS memory from top output
+///
+/// Supports formats like:
+/// - macOS: "PhysMem: 32G used (1536M wired), 64G unused."
+/// - FreeBSD: "Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free"
+Memory parseBsdMemory(String raw) {
+ // Try macOS format first: "PhysMem: 32G used (1536M wired), 64G unused."
+ final macMemReg = RegExp(
+ r'PhysMem:\s*([\d.]+)([KMGT])\s*used.*?,\s*([\d.]+)([KMGT])\s*unused');
+ final macMatch = macMemReg.firstMatch(raw);
+
+ if (macMatch != null) {
+ final usedAmount = double.parse(macMatch.group(1)!);
+ final usedUnit = macMatch.group(2)!;
+ final freeAmount = double.parse(macMatch.group(3)!);
+ final freeUnit = macMatch.group(4)!;
+
+ final usedKB = _convertToKB(usedAmount, usedUnit);
+ final freeKB = _convertToKB(freeAmount, freeUnit);
+ return Memory(total: usedKB + freeKB, free: freeKB, avail: freeKB);
+ }
+
+ // Try FreeBSD format: "Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free"
+ final freeBsdReg = RegExp(
+ r'(\d+)([KMGT])\s+(Active|Inact|Wired|Cache|Buf|Free)', caseSensitive: false);
+ final matches = freeBsdReg.allMatches(raw);
+
+ if (matches.isNotEmpty) {
+ double usedKB = 0;
+ double freeKB = 0;
+ for (final match in matches) {
+ final amount = double.parse(match.group(1)!);
+ final unit = match.group(2)!;
+ final keyword = match.group(3)!.toLowerCase();
+ final kb = _convertToKB(amount, unit);
+
+ // Only sum known keywords
+ if (keyword == 'active' || keyword == 'inact' || keyword == 'wired' || keyword == 'cache' || keyword == 'buf') {
+ usedKB += kb;
+ } else if (keyword == 'free') {
+ freeKB += kb;
+ }
+ }
+ return Memory(total: (usedKB + freeKB).round(), free: freeKB.round(), avail: freeKB.round());
+ }
+
+ // If neither format matches, throw an error to avoid misinterpretation
+ throw FormatException('Unrecognized BSD/macOS memory format: $raw');
+}
+
+/// Convert memory size to KB based on unit
+int _convertToKB(double amount, String unit) {
+ switch (unit.toUpperCase()) {
+ case 'T':
+ return (amount * 1024 * 1024 * 1024).round();
+ case 'G':
+ return (amount * 1024 * 1024).round();
+ case 'M':
+ return (amount * 1024).round();
+ case 'K':
+ case '':
+ return amount.round();
+ default:
+ return amount.round();
+ }
+}
+
class Swap {
final int total;
final int free;
final int cached;
- const Swap({
- required this.total,
- required this.free,
- required this.cached,
- });
+ const Swap({required this.total, required this.free, required this.cached});
- double get usedPercent => 1 - free / total;
+ double get usedPercent => total == 0 ? 0.0 : 1 - free / total;
- double get freePercent => free / total;
+ double get freePercent => total == 0 ? 0.0 : free / total;
@override
String toString() {
@@ -72,26 +121,16 @@ class Swap {
static Swap parse(String raw) {
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
- final total = int.tryParse(items
- .firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')
- ?.group(2) ??
- '1') ??
- 0;
- final free = int.tryParse(items
- .firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')
- ?.group(2) ??
- '1') ??
- 0;
- final cached = int.tryParse(items
- .firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')
- ?.group(2) ??
- '0') ??
- 0;
+ final total = int.tryParse(
+ items.firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')
+ ?.group(2) ?? '1') ?? 0;
+ final free = int.tryParse(
+ items.firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')
+ ?.group(2) ?? '1') ?? 0;
+ final cached = int.tryParse(
+ items.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')
+ ?.group(2) ?? '0') ?? 0;
- return Swap(
- total: total,
- free: free,
- cached: cached,
- );
+ return Swap(total: total, free: free, cached: cached);
}
}
diff --git a/lib/data/model/server/net_speed.dart b/lib/data/model/server/net_speed.dart
index 8f237479..09407e60 100644
--- a/lib/data/model/server/net_speed.dart
+++ b/lib/data/model/server/net_speed.dart
@@ -16,12 +16,7 @@ class NetSpeedPart extends TimeSeqIface {
bool same(NetSpeedPart other) => device == other.device;
}
-typedef CachedNetVals = ({
- String sizeIn,
- String sizeOut,
- String speedIn,
- String speedOut,
-});
+typedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut});
class NetSpeed extends TimeSeq> {
NetSpeed(super.init1, super.init2);
@@ -32,20 +27,14 @@ class NetSpeed extends TimeSeq> {
devices.addAll(now.map((e) => e.device).toList());
realIfaces.clear();
- realIfaces.addAll(devices
- .where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
+ realIfaces.addAll(devices.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
final sizeIn = this.sizeIn();
final sizeOut = this.sizeOut();
final speedIn = this.speedIn();
final speedOut = this.speedOut();
- cachedVals = (
- sizeIn: sizeIn,
- sizeOut: sizeOut,
- speedIn: speedIn,
- speedOut: speedOut,
- );
+ cachedVals = (sizeIn: sizeIn, sizeOut: sizeOut, speedIn: speedIn, speedOut: speedOut);
}
/// Cached network device list
@@ -58,15 +47,13 @@ class NetSpeed extends TimeSeq> {
/// Cached non-virtual network device prefix
final realIfaces = [];
- CachedNetVals cachedVals =
- (sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
+ CachedNetVals cachedVals = (sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
/// Time diff between [pre] and [now]
BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time);
double speedInBytes(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;
- double speedOutBytes(int i) =>
- (now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
+ double speedOutBytes(int i) => (now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
BigInt sizeInBytes(int i) => now[i].bytesIn;
BigInt sizeOutBytes(int i) => now[i].bytesOut;
diff --git a/lib/data/model/server/nvdia.dart b/lib/data/model/server/nvdia.dart
index 5e0aabe8..d6ad99f6 100644
--- a/lib/data/model/server/nvdia.dart
+++ b/lib/data/model/server/nvdia.dart
@@ -35,25 +35,17 @@ class NvidiaSmi {
.firstOrNull
?.innerText;
final power = gpu.findElements('gpu_power_readings').firstOrNull;
- final powerDraw =
- power?.findElements('power_draw').firstOrNull?.innerText;
- final powerLimit =
- power?.findElements('current_power_limit').firstOrNull?.innerText;
+ final powerDraw = power?.findElements('power_draw').firstOrNull?.innerText;
+ final powerLimit = power?.findElements('current_power_limit').firstOrNull?.innerText;
final memory = gpu.findElements('fb_memory_usage').firstOrNull;
final memoryUsed = memory?.findElements('used').firstOrNull?.innerText;
final memoryTotal = memory?.findElements('total').firstOrNull?.innerText;
- final processes = gpu
- .findElements('processes')
- .firstOrNull
- ?.findElements('process_info');
- final memoryProcesses =
- List.generate(processes?.length ?? 0, (index) {
+ final processes = gpu.findElements('processes').firstOrNull?.findElements('process_info');
+ final memoryProcesses = List.generate(processes?.length ?? 0, (index) {
final process = processes?.elementAt(index);
final pid = process?.findElements('pid').firstOrNull?.innerText;
- final name =
- process?.findElements('process_name').firstOrNull?.innerText;
- final memory =
- process?.findElements('used_memory').firstOrNull?.innerText;
+ final name = process?.findElements('process_name').firstOrNull?.innerText;
+ final memory = process?.findElements('used_memory').firstOrNull?.innerText;
if (pid != null && name != null && memory != null) {
return NvidiaSmiMemProcess(
int.tryParse(pid) ?? 0,
diff --git a/lib/data/model/server/ping_result.dart b/lib/data/model/server/ping_result.dart
index de59f73c..89e98090 100644
--- a/lib/data/model/server/ping_result.dart
+++ b/lib/data/model/server/ping_result.dart
@@ -1,7 +1,6 @@
final parseFailed = Exception('Parse failed');
final seqReg = RegExp(r'seq=(.+) ttl=(.+) time=(.+) ms');
-final packetReg =
- RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
+final packetReg = RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
final timeReg = RegExp(r'min/avg/max/mdev = (.+)/(.+)/(.+)/(.+) ms');
final timeAlpineReg = RegExp(r'round-trip min/avg/max = (.+)/(.+)/(.+) ms');
final ipReg = RegExp(r' \((\S+)\)');
@@ -15,17 +14,13 @@ class PingResult {
PingResult.parse(this.serverName, String raw) {
final lines = raw.split('\n');
lines.removeWhere((element) => element.isEmpty);
- final statisticIndex =
- lines.indexWhere((element) => element.startsWith('---'));
+ final statisticIndex = lines.indexWhere((element) => element.startsWith('---'));
if (statisticIndex == -1) {
throw parseFailed;
}
final statisticRaw = lines.sublist(statisticIndex + 1);
statistic = PingStatistics.parse(statisticRaw);
- results = lines
- .sublist(1, statisticIndex)
- .map((e) => PingSeqResult.parse(e))
- .toList();
+ results = lines.sublist(1, statisticIndex).map((e) => PingSeqResult.parse(e)).toList();
ip = ipReg.firstMatch(lines[0])?.group(1);
}
}
diff --git a/lib/data/model/server/private_key_info.dart b/lib/data/model/server/private_key_info.dart
index 6d04bab0..657894b4 100644
--- a/lib/data/model/server/private_key_info.dart
+++ b/lib/data/model/server/private_key_info.dart
@@ -8,10 +8,7 @@ class PrivateKeyInfo {
@JsonKey(name: 'private_key')
final String key;
- const PrivateKeyInfo({
- required this.id,
- required this.key,
- });
+ const PrivateKeyInfo({required this.id, required this.key});
factory PrivateKeyInfo.fromJson(Map json) => _$PrivateKeyInfoFromJson(json);
diff --git a/lib/data/model/server/proc.dart b/lib/data/model/server/proc.dart
index 6ce77738..39e3f5d9 100644
--- a/lib/data/model/server/proc.dart
+++ b/lib/data/model/server/proc.dart
@@ -107,10 +107,7 @@ class PsResult {
final List procs;
final String? error;
- const PsResult({
- required this.procs,
- this.error,
- });
+ const PsResult({required this.procs, this.error});
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
final lines = raw.split('\n').map((e) => e.trim()).toList();
@@ -167,14 +164,7 @@ class PsResult {
}
}
-enum ProcSortMode {
- cpu,
- mem,
- pid,
- user,
- name,
- ;
-}
+enum ProcSortMode { cpu, mem, pid, user, name }
extension _StrIndex on List {
int? indexOfOrNull(String val) {
diff --git a/lib/data/model/server/pve.dart b/lib/data/model/server/pve.dart
index cc397a6a..063c0098 100644
--- a/lib/data/model/server/pve.dart
+++ b/lib/data/model/server/pve.dart
@@ -6,25 +6,24 @@ enum PveResType {
qemu,
node,
storage,
- sdn,
- ;
+ sdn;
static PveResType? fromString(String type) => switch (type.toLowerCase()) {
- 'lxc' => PveResType.lxc,
- 'qemu' => PveResType.qemu,
- 'node' => PveResType.node,
- 'storage' => PveResType.storage,
- 'sdn' => PveResType.sdn,
- _ => null,
- };
+ 'lxc' => PveResType.lxc,
+ 'qemu' => PveResType.qemu,
+ 'node' => PveResType.node,
+ 'storage' => PveResType.storage,
+ 'sdn' => PveResType.sdn,
+ _ => null,
+ };
String get toStr => switch (this) {
- PveResType.node => l10n.node,
- PveResType.qemu => 'QEMU',
- PveResType.lxc => 'LXC',
- PveResType.storage => l10n.storage,
- PveResType.sdn => 'SDN',
- };
+ PveResType.node => l10n.node,
+ PveResType.qemu => 'QEMU',
+ PveResType.lxc => 'LXC',
+ PveResType.storage => l10n.storage,
+ PveResType.sdn => 'SDN',
+ };
}
sealed class PveResIface {
@@ -334,13 +333,7 @@ final class PveSdn extends PveResIface implements PveCtrlIface {
@override
final String status;
- PveSdn({
- required this.id,
- required this.type,
- required this.sdn,
- required this.node,
- required this.status,
- });
+ PveSdn({required this.id, required this.type, required this.sdn, required this.node, required this.status});
static PveSdn fromJson(Map json) {
return PveSdn(
@@ -379,8 +372,7 @@ final class PveRes {
bool get onlyOneNode => nodes.length == 1;
- int get length =>
- qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
+ int get length => qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
PveResIface operator [](int index) {
if (index < nodes.length) {
@@ -432,29 +424,13 @@ final class PveRes {
}
if (old != null) {
- qemus.reorder(
- order: old.qemus.map((e) => e.id).toList(),
- finder: (e, s) => e.id == s);
- lxcs.reorder(
- order: old.lxcs.map((e) => e.id).toList(),
- finder: (e, s) => e.id == s);
- nodes.reorder(
- order: old.nodes.map((e) => e.id).toList(),
- finder: (e, s) => e.id == s);
- storages.reorder(
- order: old.storages.map((e) => e.id).toList(),
- finder: (e, s) => e.id == s);
- sdns.reorder(
- order: old.sdns.map((e) => e.id).toList(),
- finder: (e, s) => e.id == s);
+ qemus.reorder(order: old.qemus.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
+ lxcs.reorder(order: old.lxcs.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
+ nodes.reorder(order: old.nodes.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
+ storages.reorder(order: old.storages.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
+ sdns.reorder(order: old.sdns.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
}
- return PveRes(
- qemus: qemus,
- lxcs: lxcs,
- nodes: nodes,
- storages: storages,
- sdns: sdns,
- );
+ return PveRes(qemus: qemus, lxcs: lxcs, nodes: nodes, storages: storages, sdns: sdns);
}
}
diff --git a/lib/data/model/server/sensors.dart b/lib/data/model/server/sensors.dart
index c250afc6..7acfb390 100644
--- a/lib/data/model/server/sensors.dart
+++ b/lib/data/model/server/sensors.dart
@@ -15,12 +15,12 @@ final class SensorAdaptor {
static const isa = SensorAdaptor(isaRaw);
static SensorAdaptor parse(String raw) => switch (raw) {
- acpiRaw => acpi,
- pciRaw => pci,
- virtualRaw => virtual,
- isaRaw => isa,
- _ => SensorAdaptor(raw),
- };
+ acpiRaw => acpi,
+ pciRaw => pci,
+ virtualRaw => virtual,
+ isaRaw => isa,
+ _ => SensorAdaptor(raw),
+ };
}
final class SensorItem {
@@ -28,11 +28,7 @@ final class SensorItem {
final SensorAdaptor adapter;
final Map details;
- const SensorItem({
- required this.device,
- required this.adapter,
- required this.details,
- });
+ const SensorItem({required this.device, required this.adapter, required this.details});
String get toMarkdown {
final sb = StringBuffer();
@@ -72,8 +68,7 @@ final class SensorItem {
final len = sensorLines.length;
if (len < 3) continue;
final device = sensorLines.first;
- final adapter =
- SensorAdaptor.parse(sensorLines[1].split(':').last.trim());
+ final adapter = SensorAdaptor.parse(sensorLines[1].split(':').last.trim());
final details = {};
for (var idx = 2; idx < len; idx++) {
@@ -84,11 +79,7 @@ final class SensorItem {
final value = detailParts[1].trim();
details[key] = value;
}
- sensors.add(SensorItem(
- device: device,
- adapter: adapter,
- details: details,
- ));
+ sensors.add(SensorItem(device: device, adapter: adapter, details: details));
}
return sensors;
diff --git a/lib/data/model/server/server_private_info.dart b/lib/data/model/server/server_private_info.dart
index 174a1ec3..105d81d3 100644
--- a/lib/data/model/server/server_private_info.dart
+++ b/lib/data/model/server/server_private_info.dart
@@ -5,6 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server.dart';
+import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/store/server.dart';
@@ -44,6 +45,9 @@ abstract class Spi with _$Spi {
/// It only applies to SSH terminal.
Map? envs,
@Default('') @JsonKey(fromJson: Spi.parseId) String id,
+
+ /// Custom system type (unix or windows). If set, skip auto-detection.
+ @JsonKey(includeIfNull: false) SystemType? customSystemType,
}) = _Spi;
factory Spi.fromJson(Map json) => _$SpiFromJson(json);
@@ -119,26 +123,25 @@ extension Spix on Spi {
///
/// **NOT** the default value.
static final example = Spi(
- name: 'name',
- ip: 'ip',
- port: 22,
- user: 'root',
- pwd: 'pwd',
- keyId: 'private_key_id',
- tags: ['tag1', 'tag2'],
- alterUrl: 'user@ip:port',
- autoConnect: true,
- jumpId: 'jump_server_id',
- custom: ServerCustom(
- pveAddr: 'http://localhost:8006',
- pveIgnoreCert: false,
- cmds: {
- 'echo': 'echo hello',
- },
- preferTempDev: 'nvme-pci-0400',
- logoUrl: 'https://example.com/logo.png',
- ),
- id: 'id');
+ name: 'name',
+ ip: 'ip',
+ port: 22,
+ user: 'root',
+ pwd: 'pwd',
+ keyId: 'private_key_id',
+ tags: ['tag1', 'tag2'],
+ alterUrl: 'user@ip:port',
+ autoConnect: true,
+ jumpId: 'jump_server_id',
+ custom: ServerCustom(
+ pveAddr: 'http://localhost:8006',
+ pveIgnoreCert: false,
+ cmds: {'echo': 'echo hello'},
+ preferTempDev: 'nvme-pci-0400',
+ logoUrl: 'https://example.com/logo.png',
+ ),
+ id: 'id',
+ );
bool get isRoot => user == 'root';
}
diff --git a/lib/data/model/server/server_private_info.freezed.dart b/lib/data/model/server/server_private_info.freezed.dart
index ed20a746..7e782f5c 100644
--- a/lib/data/model/server/server_private_info.freezed.dart
+++ b/lib/data/model/server/server_private_info.freezed.dart
@@ -19,7 +19,8 @@ mixin _$Spi {
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
@JsonKey(name: 'pubKeyId') String? get keyId; List? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
- Map? get envs;@JsonKey(fromJson: Spi.parseId) String get id;
+ Map? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
+@JsonKey(includeIfNull: false) SystemType? get customSystemType;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -32,12 +33,12 @@ $SpiCopyWith get copyWith => _$SpiCopyWithImpl(this as Spi, _$identity
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
-int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id);
+int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType);
@@ -48,7 +49,7 @@ abstract mixin class $SpiCopyWith<$Res> {
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
@useResult
$Res call({
- String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map? envs,@JsonKey(fromJson: Spi.parseId) String id
+ String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType
});
@@ -65,7 +66,7 @@ class _$SpiCopyWithImpl<$Res>
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
-@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,}) {
+@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
@@ -81,7 +82,8 @@ as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nul
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
as Map?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
-as String,
+as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
+as SystemType?,
));
}
@@ -92,7 +94,7 @@ as String,
@JsonSerializable(includeIfNull: false)
class _Spi extends Spi {
- const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map? envs, @JsonKey(fromJson: Spi.parseId) this.id = ''}): _tags = tags,_envs = envs,super._();
+ const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType}): _tags = tags,_envs = envs,super._();
factory _Spi.fromJson(Map json) => _$SpiFromJson(json);
@override final String name;
@@ -129,6 +131,8 @@ class _Spi extends Spi {
}
@override@JsonKey(fromJson: Spi.parseId) final String id;
+/// Custom system type (unix or windows). If set, skip auto-detection.
+@override@JsonKey(includeIfNull: false) final SystemType? customSystemType;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@@ -143,12 +147,12 @@ Map toJson() {
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
-int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id);
+int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType);
@@ -159,7 +163,7 @@ abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
@override @useResult
$Res call({
- String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map? envs,@JsonKey(fromJson: Spi.parseId) String id
+ String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType
});
@@ -176,7 +180,7 @@ class __$SpiCopyWithImpl<$Res>
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
-@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,}) {
+@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,}) {
return _then(_Spi(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
@@ -192,7 +196,8 @@ as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nul
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
as Map?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
-as String,
+as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
+as SystemType?,
));
}
diff --git a/lib/data/model/server/server_private_info.g.dart b/lib/data/model/server/server_private_info.g.dart
index 6c332426..9c9e0dae 100644
--- a/lib/data/model/server/server_private_info.g.dart
+++ b/lib/data/model/server/server_private_info.g.dart
@@ -27,6 +27,10 @@ _Spi _$SpiFromJson(Map json) => _Spi(
(k, e) => MapEntry(k, e as String),
),
id: json['id'] == null ? '' : Spi.parseId(json['id']),
+ customSystemType: $enumDecodeNullable(
+ _$SystemTypeEnumMap,
+ json['customSystemType'],
+ ),
);
Map _$SpiToJson(_Spi instance) => {
@@ -44,4 +48,12 @@ Map _$SpiToJson(_Spi instance) => {
if (instance.wolCfg case final value?) 'wolCfg': value,
if (instance.envs case final value?) 'envs': value,
'id': instance.id,
+ if (_$SystemTypeEnumMap[instance.customSystemType] case final value?)
+ 'customSystemType': value,
+};
+
+const _$SystemTypeEnumMap = {
+ SystemType.linux: 'linux',
+ SystemType.bsd: 'bsd',
+ SystemType.windows: 'windows',
};
diff --git a/lib/data/model/server/server_status_update_req.dart b/lib/data/model/server/server_status_update_req.dart
index ae9e2466..7d160b02 100644
--- a/lib/data/model/server/server_status_update_req.dart
+++ b/lib/data/model/server/server_status_update_req.dart
@@ -1,3 +1,5 @@
+import 'dart:convert';
+
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/amd.dart';
@@ -12,6 +14,8 @@ import 'package:server_box/data/model/server/nvdia.dart';
import 'package:server_box/data/model/server/sensors.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/system.dart';
+import 'package:server_box/data/model/server/temp.dart';
+import 'package:server_box/data/model/server/windows_parser.dart';
class ServerStatusUpdateReq {
final ServerStatus ss;
@@ -31,6 +35,7 @@ Future getStatus(ServerStatusUpdateReq req) async {
return switch (req.system) {
SystemType.linux => _getLinuxStatus(req),
SystemType.bsd => _getBsdStatus(req),
+ SystemType.windows => _getWindowsStatus(req),
};
}
@@ -39,8 +44,7 @@ Future getStatus(ServerStatusUpdateReq req) async {
Future _getLinuxStatus(ServerStatusUpdateReq req) async {
final segments = req.segments;
- final time =
- int.tryParse(StatusCmdType.time.find(segments)) ??
+ final time = int.tryParse(StatusCmdType.time.find(segments)) ??
DateTime.now().millisecondsSinceEpoch ~/ 1000;
try {
@@ -210,11 +214,11 @@ Future _getBsdStatus(ServerStatusUpdateReq req) async {
Loggers.app.warning(e, s);
}
- // try {
- // req.ss.mem = parseBsdMem(BSDStatusCmdType.mem.find(segments));
- // } catch (e, s) {
- // Loggers.app.warning(e, s);
- // }
+ try {
+ req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.find(segments));
+ } catch (e, s) {
+ Loggers.app.warning(e, s);
+ }
try {
final uptime = _parseUpTime(BSDStatusCmdType.uptime.find(segments));
@@ -235,13 +239,48 @@ Future _getBsdStatus(ServerStatusUpdateReq req) async {
// raw:
// 19:39:15 up 61 days, 18:16, 1 user, load average: 0.00, 0.00, 0.00
+// 19:39:15 up 1 day, 2:34, 1 user, load average: 0.00, 0.00, 0.00
+// 19:39:15 up 2:34, 1 user, load average: 0.00, 0.00, 0.00
+// 19:39:15 up 34 min, 1 user, load average: 0.00, 0.00, 0.00
String? _parseUpTime(String raw) {
final splitedUp = raw.split('up ');
if (splitedUp.length == 2) {
- final splitedComma = splitedUp[1].split(', ');
- if (splitedComma.length >= 2) {
- return splitedComma[0];
+ final uptimePart = splitedUp[1];
+ final splitedComma = uptimePart.split(', ');
+
+ if (splitedComma.isEmpty) return null;
+
+ // Handle different uptime formats
+ final firstPart = splitedComma[0].trim();
+
+ // Case 1: "61 days" or "1 day" - need to get the time part from next segment
+ if (firstPart.contains('day')) {
+ if (splitedComma.length >= 2) {
+ final timePart = splitedComma[1].trim();
+ // Check if it's in HH:MM format
+ if (timePart.contains(':') &&
+ !timePart.contains('user') &&
+ !timePart.contains('load')) {
+ return '$firstPart, $timePart';
+ }
+ }
+ return firstPart;
}
+
+ // Case 2: "2:34" (hours:minutes) - already in good format
+ if (firstPart.contains(':') &&
+ !firstPart.contains('user') &&
+ !firstPart.contains('load')) {
+ return firstPart;
+ }
+
+ // Case 3: "34 min" - already in good format
+ if (firstPart.contains('min')) {
+ return firstPart;
+ }
+
+ // Fallback: return first part
+ return firstPart;
}
return null;
}
@@ -259,3 +298,406 @@ String? _parseHostName(String raw) {
if (raw.contains(ShellFunc.scriptFile)) return null;
return raw;
}
+
+// Windows status parsing implementation
+Future _getWindowsStatus(ServerStatusUpdateReq req) async {
+ final segments = req.segments;
+ final time = int.tryParse(WindowsStatusCmdType.time.find(segments)) ??
+ DateTime.now().millisecondsSinceEpoch ~/ 1000;
+
+ // Parse all different resource types using helper methods
+ _parseWindowsNetworkData(req, segments, time);
+ _parseWindowsSystemData(req, segments);
+ _parseWindowsHostData(req, segments);
+ _parseWindowsCpuData(req, segments);
+ _parseWindowsMemoryData(req, segments);
+ _parseWindowsDiskData(req, segments);
+ _parseWindowsUptimeData(req, segments);
+ _parseWindowsDiskIOData(req, segments, time);
+ _parseWindowsConnectionData(req, segments);
+ _parseWindowsBatteryData(req, segments);
+ _parseWindowsTemperatureData(req, segments);
+ _parseWindowsGpuData(req, segments);
+ WindowsParser.parseCustomCommands(req.ss, segments, req.customCmds, req.system.segmentsLen);
+
+ return req.ss;
+}
+
+/// Parse Windows network data
+void _parseWindowsNetworkData(ServerStatusUpdateReq req, List segments, int time) {
+ try {
+ final netRaw = WindowsStatusCmdType.net.find(segments);
+ if (netRaw.isNotEmpty &&
+ netRaw != 'null' &&
+ !netRaw.contains('network_error') &&
+ !netRaw.contains('error') &&
+ !netRaw.contains('Exception')) {
+ final netParts = _parseWindowsNetwork(netRaw, time);
+ if (netParts.isNotEmpty) {
+ req.ss.netSpeed.update(netParts);
+ }
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows network parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows system information
+void _parseWindowsSystemData(ServerStatusUpdateReq req, List segments) {
+ try {
+ final sys = WindowsStatusCmdType.sys.find(segments);
+ if (sys.isNotEmpty) {
+ req.ss.more[StatusCmdType.sys] = sys;
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows system parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows host information
+void _parseWindowsHostData(ServerStatusUpdateReq req, List segments) {
+ try {
+ final host = _parseHostName(WindowsStatusCmdType.host.find(segments));
+ if (host != null) {
+ req.ss.more[StatusCmdType.host] = host;
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows host parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows CPU data and brand information
+void _parseWindowsCpuData(ServerStatusUpdateReq req, List segments) {
+ try {
+ // Windows CPU parsing - JSON format from PowerShell
+ final cpuRaw = WindowsStatusCmdType.cpu.find(segments);
+ if (cpuRaw.isNotEmpty &&
+ cpuRaw != 'null' &&
+ !cpuRaw.contains('error') &&
+ !cpuRaw.contains('Exception')) {
+ final cpus = WindowsParser.parseCpu(cpuRaw, req.ss);
+ if (cpus.isNotEmpty) {
+ req.ss.cpu.update(cpus);
+ }
+ }
+
+ // Windows CPU brand parsing
+ final brandRaw = WindowsStatusCmdType.cpuBrand.find(segments);
+ if (brandRaw.isNotEmpty && brandRaw != 'null') {
+ req.ss.cpu.brand.clear();
+ req.ss.cpu.brand[brandRaw.trim()] = 1;
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows CPU parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows memory data
+void _parseWindowsMemoryData(ServerStatusUpdateReq req, List segments) {
+ try {
+ final memRaw = WindowsStatusCmdType.mem.find(segments);
+ if (memRaw.isNotEmpty &&
+ memRaw != 'null' &&
+ !memRaw.contains('error') &&
+ !memRaw.contains('Exception')) {
+ final memory = WindowsParser.parseMemory(memRaw);
+ if (memory != null) {
+ req.ss.mem = memory;
+ }
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows memory parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows disk data
+void _parseWindowsDiskData(ServerStatusUpdateReq req, List segments) {
+ try {
+ final diskRaw = WindowsStatusCmdType.disk.find(segments);
+ if (diskRaw.isNotEmpty && diskRaw != 'null') {
+ final disks = WindowsParser.parseDisks(diskRaw);
+ req.ss.disk = disks;
+ req.ss.diskUsage = DiskUsage.parse(disks);
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows disk parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows uptime data
+void _parseWindowsUptimeData(ServerStatusUpdateReq req, List segments) {
+ try {
+ final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.find(segments));
+ if (uptime != null) {
+ req.ss.more[StatusCmdType.uptime] = uptime;
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows uptime parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows disk I/O data
+void _parseWindowsDiskIOData(ServerStatusUpdateReq req, List segments, int time) {
+ try {
+ final diskIOraw = WindowsStatusCmdType.diskio.find(segments);
+ if (diskIOraw.isNotEmpty && diskIOraw != 'null') {
+ final diskio = _parseWindowsDiskIO(diskIOraw, time);
+ req.ss.diskIO.update(diskio);
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows disk I/O parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows connection data
+void _parseWindowsConnectionData(ServerStatusUpdateReq req, List segments) {
+ try {
+ final connStr = WindowsStatusCmdType.conn.find(segments);
+ final connCount = int.tryParse(connStr.trim());
+ if (connCount != null) {
+ req.ss.tcp = Conn(maxConn: 0, active: connCount, passive: 0, fail: 0);
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows connection parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows battery data
+void _parseWindowsBatteryData(ServerStatusUpdateReq req, List segments) {
+ try {
+ final batteryRaw = WindowsStatusCmdType.battery.find(segments);
+ if (batteryRaw.isNotEmpty && batteryRaw != 'null') {
+ final batteries = _parseWindowsBatteries(batteryRaw);
+ req.ss.batteries.clear();
+ if (batteries.isNotEmpty) {
+ req.ss.batteries.addAll(batteries);
+ }
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows battery parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows temperature data
+void _parseWindowsTemperatureData(ServerStatusUpdateReq req, List segments) {
+ try {
+ final tempRaw = WindowsStatusCmdType.temp.find(segments);
+ if (tempRaw.isNotEmpty && tempRaw != 'null') {
+ _parseWindowsTemperatures(req.ss.temps, tempRaw);
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows temperature parsing failed: $e', s);
+ }
+}
+
+/// Parse Windows GPU data (NVIDIA/AMD)
+void _parseWindowsGpuData(ServerStatusUpdateReq req, List segments) {
+ try {
+ req.ss.nvidia = NvidiaSmi.fromXml(WindowsStatusCmdType.nvidia.find(segments));
+ } catch (e, s) {
+ Loggers.app.warning('Windows NVIDIA GPU parsing failed: $e', s);
+ }
+
+ try {
+ req.ss.amd = AmdSmi.fromJson(WindowsStatusCmdType.amd.find(segments));
+ } catch (e, s) {
+ Loggers.app.warning('Windows AMD GPU parsing failed: $e', s);
+ }
+}
+
+
+List _parseWindowsBatteries(String raw) {
+ try {
+ final dynamic jsonData = json.decode(raw);
+ final List batteries = [];
+
+ final batteryList = jsonData is List ? jsonData : [jsonData];
+
+ for (final batteryData in batteryList) {
+ final chargeRemaining =
+ batteryData['EstimatedChargeRemaining'] as int? ?? 0;
+ final batteryStatus = batteryData['BatteryStatus'] as int? ?? 0;
+
+ // Windows battery status: 1=Other, 2=Unknown, 3=Full, 4=Low,
+ // 5=Critical, 6=Charging, 7=ChargingAndLow, 8=ChargingAndCritical,
+ // 9=Undefined, 10=PartiallyCharged
+ final isCharging = batteryStatus == 6 ||
+ batteryStatus == 7 ||
+ batteryStatus == 8;
+
+ batteries.add(
+ Battery(
+ name: 'Battery',
+ percent: chargeRemaining,
+ status: isCharging
+ ? BatteryStatus.charging
+ : BatteryStatus.discharging,
+ ),
+ );
+ }
+
+ return batteries;
+ } catch (e) {
+ return [];
+ }
+}
+
+List _parseWindowsNetwork(String raw, int currentTime) {
+ try {
+ final dynamic jsonData = json.decode(raw);
+ final List netParts = [];
+
+ // PowerShell Get-Counter returns a structure with CounterSamples
+ if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
+ final samples = jsonData['CounterSamples'] as List?;
+ if (samples != null && samples.length >= 2) {
+ // We need 2 samples to calculate speed (interval between them)
+ final Map interfaceRx = {};
+ final Map interfaceTx = {};
+
+ for (final sample in samples) {
+ final path = sample['Path']?.toString() ?? '';
+ final cookedValue = sample['CookedValue'] as num? ?? 0;
+
+ if (path.contains('Bytes Received/sec')) {
+ final interfaceName = _extractInterfaceName(path);
+ if (interfaceName.isNotEmpty) {
+ interfaceRx[interfaceName] = cookedValue.toDouble();
+ }
+ } else if (path.contains('Bytes Sent/sec')) {
+ final interfaceName = _extractInterfaceName(path);
+ if (interfaceName.isNotEmpty) {
+ interfaceTx[interfaceName] = cookedValue.toDouble();
+ }
+ }
+ }
+
+ // Create NetSpeedPart for each interface
+ for (final interfaceName in interfaceRx.keys) {
+ final rx = interfaceRx[interfaceName] ?? 0;
+ final tx = interfaceTx[interfaceName] ?? 0;
+
+ netParts.add(
+ NetSpeedPart(
+ interfaceName,
+ BigInt.from(rx.toInt()),
+ BigInt.from(tx.toInt()),
+ currentTime,
+ ),
+ );
+ }
+ }
+ }
+
+ return netParts;
+ } catch (e) {
+ return [];
+ }
+}
+
+String _extractInterfaceName(String path) {
+ // Extract interface name from path like
+ // "\\Computer\\NetworkInterface(Interface Name)\\..."
+ final match = RegExp(r'\\NetworkInterface\(([^)]+)\)\\').firstMatch(path);
+ return match?.group(1) ?? '';
+}
+
+List _parseWindowsDiskIO(String raw, int currentTime) {
+ try {
+ final dynamic jsonData = json.decode(raw);
+ final List diskParts = [];
+
+ // PowerShell Get-Counter returns a structure with CounterSamples
+ if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
+ final samples = jsonData['CounterSamples'] as List?;
+ if (samples != null) {
+ final Map diskReads = {};
+ final Map diskWrites = {};
+
+ for (final sample in samples) {
+ final path = sample['Path']?.toString() ?? '';
+ final cookedValue = sample['CookedValue'] as num? ?? 0;
+
+ if (path.contains('Disk Read Bytes/sec')) {
+ final diskName = _extractDiskName(path);
+ if (diskName.isNotEmpty) {
+ diskReads[diskName] = cookedValue.toDouble();
+ }
+ } else if (path.contains('Disk Write Bytes/sec')) {
+ final diskName = _extractDiskName(path);
+ if (diskName.isNotEmpty) {
+ diskWrites[diskName] = cookedValue.toDouble();
+ }
+ }
+ }
+
+ // Create DiskIOPiece for each disk - convert bytes to sectors
+ // (assuming 512 bytes per sector)
+ for (final diskName in diskReads.keys) {
+ final readBytes = diskReads[diskName] ?? 0;
+ final writeBytes = diskWrites[diskName] ?? 0;
+ final sectorsRead = (readBytes / 512).round();
+ final sectorsWrite = (writeBytes / 512).round();
+
+ diskParts.add(
+ DiskIOPiece(
+ dev: diskName,
+ sectorsRead: sectorsRead,
+ sectorsWrite: sectorsWrite,
+ time: currentTime,
+ ),
+ );
+ }
+ }
+ }
+
+ return diskParts;
+ } catch (e) {
+ return [];
+ }
+}
+
+String _extractDiskName(String path) {
+ // Extract disk name from path like
+ // "\\Computer\\PhysicalDisk(Disk Name)\\..."
+ final match = RegExp(r'\\PhysicalDisk\(([^)]+)\)\\').firstMatch(path);
+ return match?.group(1) ?? '';
+}
+
+void _parseWindowsTemperatures(Temperatures temps, String raw) {
+ try {
+ // Handle error output
+ if (raw.contains('Error') ||
+ raw.contains('Exception') ||
+ raw.contains('The term')) {
+ return;
+ }
+
+ final dynamic jsonData = json.decode(raw);
+ final tempList = jsonData is List ? jsonData : [jsonData];
+
+ // Create fake type and value strings that the existing parse method can handle
+ final typeLines = [];
+ final valueLines = [];
+
+ for (int i = 0; i < tempList.length; i++) {
+ final item = tempList[i];
+ final typeName = item['InstanceName']?.toString() ?? 'Unknown';
+ final temperature = item['Temperature'] as num?;
+
+ if (temperature != null) {
+ // Convert to the format expected by the existing parse method
+ typeLines.add('/sys/class/thermal/thermal_zone$i/$typeName');
+ // Convert to millicelsius (multiply by 1000)
+ // as expected by Linux parsing
+ valueLines.add((temperature * 1000).round().toString());
+ }
+ }
+
+ if (typeLines.isNotEmpty && valueLines.isNotEmpty) {
+ temps.parse(typeLines.join('\n'), valueLines.join('\n'));
+ }
+ } catch (e) {
+ // If JSON parsing fails, ignore temperature data
+ }
+}
diff --git a/lib/data/model/server/snippet.dart b/lib/data/model/server/snippet.dart
index 43de686a..b7651cd5 100644
--- a/lib/data/model/server/snippet.dart
+++ b/lib/data/model/server/snippet.dart
@@ -35,23 +35,16 @@ extension SnippetX on Snippet {
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
String fmtWithSpi(Spi spi) {
- return script.replaceAllMapped(
- fmtFinder,
- (match) {
- final key = match.group(0);
- final func = fmtArgs[key];
- if (func != null) return func(spi);
- // If not found, return the original content for further processing
- return key ?? '';
- },
- );
+ return script.replaceAllMapped(fmtFinder, (match) {
+ final key = match.group(0);
+ final func = fmtArgs[key];
+ if (func != null) return func(spi);
+ // If not found, return the original content for further processing
+ return key ?? '';
+ });
}
- Future runInTerm(
- Terminal terminal,
- Spi spi, {
- bool autoEnter = false,
- }) async {
+ Future runInTerm(Terminal terminal, Spi spi, {bool autoEnter = false}) async {
final argsFmted = fmtWithSpi(spi);
final matches = fmtFinder.allMatches(argsFmted);
@@ -119,11 +112,7 @@ extension SnippetX on Snippet {
if (autoEnter) terminal.keyInput(TerminalKey.enter);
}
- Future _doTermKeys(
- Terminal terminal,
- MapEntry termKey,
- String key,
- ) async {
+ Future _doTermKeys(Terminal terminal, MapEntry termKey, String key) async {
// if (termKey.value == TerminalKey.enter) {
// terminal.keyInput(TerminalKey.enter);
// return;
@@ -140,11 +129,7 @@ extension SnippetX on Snippet {
// `${ctrl+ad}` -> `ctrla + d`
final chars = key.substring(termKey.key.length + 1, key.length - 1);
if (chars.isEmpty) return;
- final ok = terminal.charInput(
- chars.codeUnitAt(0),
- ctrl: ctrlAlt.ctrl,
- alt: ctrlAlt.alt,
- );
+ final ok = terminal.charInput(chars.codeUnitAt(0), ctrl: ctrlAlt.ctrl, alt: ctrlAlt.alt);
if (!ok) {
Loggers.app.warning('Failed to input: $key');
}
@@ -166,10 +151,7 @@ extension SnippetX on Snippet {
};
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
- static final fmtTermKeys = {
- r'${ctrl': TerminalKey.control,
- r'${alt': TerminalKey.alt,
- };
+ static final fmtTermKeys = {r'${ctrl': TerminalKey.control, r'${alt': TerminalKey.alt};
}
class SnippetResult {
@@ -177,11 +159,7 @@ class SnippetResult {
final String result;
final Duration time;
- SnippetResult({
- required this.dest,
- required this.result,
- required this.time,
- });
+ SnippetResult({required this.dest, required this.result, required this.time});
}
typedef SnippetFuncCtx = ({Terminal term, String raw});
@@ -193,10 +171,7 @@ abstract final class SnippetFuncs {
r'${enter': SnippetFuncs.enter,
};
- static const help = {
- 'sleep': 'Sleep for a few seconds',
- 'enter': 'Enter a few times',
- };
+ static const help = {'sleep': 'Sleep for a few seconds', 'enter': 'Enter a few times'};
static FutureOr sleep(SnippetFuncCtx ctx) async {
final seconds = int.tryParse(ctx.raw);
diff --git a/lib/data/model/server/system.dart b/lib/data/model/server/system.dart
index 30a4eaf6..73cea8bf 100644
--- a/lib/data/model/server/system.dart
+++ b/lib/data/model/server/system.dart
@@ -1,21 +1,55 @@
+import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/shell_func.dart';
enum SystemType {
- linux._(linuxSign),
- bsd._(bsdSign),
- ;
+ linux(linuxSign),
+ bsd(bsdSign),
+ windows(windowsSign);
- final String value;
+ final String? value;
- const SystemType._(this.value);
+ const SystemType([this.value]);
static const linuxSign = '__linux';
static const bsdSign = '__bsd';
+ static const windowsSign = '__windows';
+ /// Used for parsing system types from shell output.
+ ///
+ /// This method looks for specific system signatures in the shell output
+ /// and returns the corresponding SystemType. If no signature is found,
+ /// it defaults to Linux but logs the detection failure for debugging.
static SystemType parse(String value) {
+ // Log the raw value for debugging purposes (truncated to avoid spam)
+ final truncatedValue = value.length > 100
+ ? '${value.substring(0, 100)}...'
+ : value;
+
+ if (value.contains(windowsSign)) {
+ Loggers.app.info('System detected as Windows from signature in: $truncatedValue');
+ return SystemType.windows;
+ }
if (value.contains(bsdSign)) {
+ Loggers.app.info('System detected as BSD from signature in: $truncatedValue');
return SystemType.bsd;
}
+
+ // Log when falling back to Linux detection
+ if (value.trim().isEmpty) {
+ Loggers.app.warning(
+ 'System detection received empty input, defaulting to Linux. '
+ 'This may indicate a script execution issue.'
+ );
+ } else if (!value.contains(linuxSign)) {
+ Loggers.app.warning(
+ 'System detection could not find any known signatures (Windows: $windowsSign, '
+ 'BSD: $bsdSign, Linux: $linuxSign) in output: "$truncatedValue". '
+ 'Defaulting to Linux, but this may cause incorrect parsing.'
+ );
+ } else {
+ Loggers.app.info('System detected as Linux from signature in: $truncatedValue');
+ }
+
return SystemType.linux;
}
@@ -27,6 +61,8 @@ enum SystemType {
return StatusCmdType.values.length;
case SystemType.bsd:
return BSDStatusCmdType.values.length;
+ case SystemType.windows:
+ return WindowsStatusCmdType.values.length;
}
}
}
diff --git a/lib/data/model/server/systemd.dart b/lib/data/model/server/systemd.dart
index f378e5c5..c6a3a15c 100644
--- a/lib/data/model/server/systemd.dart
+++ b/lib/data/model/server/systemd.dart
@@ -8,26 +8,24 @@ enum SystemdUnitFunc {
reload,
enable,
disable,
- status,
- ;
+ status;
IconData get icon => switch (this) {
- start => Icons.play_arrow,
- stop => Icons.stop,
- restart => Icons.refresh,
- reload => Icons.refresh,
- enable => Icons.check,
- disable => Icons.close,
- status => Icons.info,
- };
+ start => Icons.play_arrow,
+ stop => Icons.stop,
+ restart => Icons.refresh,
+ reload => Icons.refresh,
+ enable => Icons.check,
+ disable => Icons.close,
+ status => Icons.info,
+ };
}
enum SystemdUnitType {
service,
socket,
mount,
- timer,
- ;
+ timer;
static SystemdUnitType? fromString(String? value) {
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
@@ -36,13 +34,12 @@ enum SystemdUnitType {
enum SystemdUnitScope {
system,
- user,
- ;
+ user;
Color? get color => switch (this) {
- system => Colors.red,
- _ => null,
- };
+ system => Colors.red,
+ _ => null,
+ };
String getCmdPrefix(bool isRoot) {
if (this == system) {
@@ -57,17 +54,16 @@ enum SystemdUnitState {
inactive,
failed,
activating,
- deactivating,
- ;
+ deactivating;
static SystemdUnitState? fromString(String? value) {
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
}
Color? get color => switch (this) {
- failed => Colors.red,
- _ => null,
- };
+ failed => Colors.red,
+ _ => null,
+ };
}
final class SystemdUnit {
@@ -85,10 +81,7 @@ final class SystemdUnit {
required this.state,
});
- String getCmd({
- required SystemdUnitFunc func,
- required bool isRoot,
- }) {
+ String getCmd({required SystemdUnitFunc func, required bool isRoot}) {
final prefix = scope.getCmdPrefix(isRoot);
return '$prefix ${func.name} $name';
}
diff --git a/lib/data/model/server/time_seq.dart b/lib/data/model/server/time_seq.dart
index fc930b86..b36c782d 100644
--- a/lib/data/model/server/time_seq.dart
+++ b/lib/data/model/server/time_seq.dart
@@ -40,11 +40,7 @@ class Fifo extends ListBase {
abstract class TimeSeq> extends Fifo {
/// Due to the design, at least two elements are required, otherwise [pre] /
/// [now] will throw.
- TimeSeq(
- T init1,
- T init2, {
- super.capacity,
- }) : super(list: [init1, init2]);
+ TimeSeq(T init1, T init2, {super.capacity}) : super(list: [init1, init2]);
T get pre {
return _list[length - 2];
diff --git a/lib/data/model/server/windows_parser.dart b/lib/data/model/server/windows_parser.dart
new file mode 100644
index 00000000..2a99e8cf
--- /dev/null
+++ b/lib/data/model/server/windows_parser.dart
@@ -0,0 +1,258 @@
+import 'dart:convert';
+
+import 'package:fl_lib/fl_lib.dart';
+import 'package:intl/intl.dart';
+import 'package:server_box/data/model/server/cpu.dart';
+import 'package:server_box/data/model/server/disk.dart';
+import 'package:server_box/data/model/server/memory.dart';
+import 'package:server_box/data/model/server/server.dart';
+
+/// Windows-specific status parsing utilities
+///
+/// This module handles parsing of Windows PowerShell command outputs
+/// for server monitoring. It extracts the Windows parsing logic
+/// to improve maintainability and readability.
+class WindowsParser {
+ const WindowsParser._();
+
+ /// Parse Windows custom commands from segments
+ static void parseCustomCommands(
+ ServerStatus serverStatus,
+ List segments,
+ Map customCmds,
+ int systemSegmentsLength,
+ ) {
+ try {
+ for (int idx = 0; idx < customCmds.length; idx++) {
+ final key = customCmds.keys.elementAt(idx);
+ // Ensure we don't go out of bounds when accessing segments
+ final segmentIndex = idx + systemSegmentsLength;
+ if (segmentIndex < segments.length) {
+ final value = segments[segmentIndex];
+ serverStatus.customCmds[key] = value;
+ } else {
+ Loggers.app.warning(
+ 'Windows custom commands: segment index $segmentIndex out of bounds '
+ '(segments length: ${segments.length}, systemSegmentsLength: $systemSegmentsLength)'
+ );
+ }
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows custom commands parsing failed: $e', s);
+ }
+ }
+
+ /// Parse Windows uptime from PowerShell output
+ static String? parseUpTime(String raw) {
+ try {
+ // Clean the input - trim whitespace and get the first non-empty line
+ final cleanedInput = raw.trim().split('\n')
+ .where((line) => line.trim().isNotEmpty)
+ .firstOrNull;
+
+ if (cleanedInput == null || cleanedInput.isEmpty) {
+ Loggers.app.warning('Windows uptime parsing: empty or null input');
+ return null;
+ }
+
+ // Try multiple date formats to handle different Windows locale/version outputs
+ final formatters = [
+ DateFormat('EEEE, MMMM d, yyyy h:mm:ss a', 'en_US'), // Original format
+ DateFormat('EEEE, MMMM dd, yyyy h:mm:ss a', 'en_US'), // Double-digit day
+ DateFormat('EEE, MMM d, yyyy h:mm:ss a', 'en_US'), // Shortened format
+ DateFormat('EEE, MMM dd, yyyy h:mm:ss a', 'en_US'), // Shortened with double-digit day
+ DateFormat('M/d/yyyy h:mm:ss a', 'en_US'), // Short US format
+ DateFormat('MM/dd/yyyy h:mm:ss a', 'en_US'), // Short US format with zero padding
+ DateFormat('d/M/yyyy h:mm:ss a', 'en_US'), // Short European format
+ DateFormat('dd/MM/yyyy h:mm:ss a', 'en_US'), // Short European format with zero padding
+ ];
+
+ DateTime? dateTime;
+ for (final formatter in formatters) {
+ dateTime = formatter.tryParseLoose(cleanedInput);
+ if (dateTime != null) break;
+ }
+
+ if (dateTime == null) {
+ Loggers.app.warning('Windows uptime parsing: could not parse date format for: $cleanedInput');
+ return null;
+ }
+
+ final now = DateTime.now();
+ final uptime = now.difference(dateTime);
+
+ // Validate that the uptime is reasonable (not negative, not too far in the future)
+ if (uptime.isNegative || uptime.inDays > 3650) { // More than 10 years seems unreasonable
+ Loggers.app.warning('Windows uptime parsing: unreasonable uptime calculated: ${uptime.inDays} days for date: $cleanedInput');
+ return null;
+ }
+
+ final days = uptime.inDays;
+ final hours = uptime.inHours % 24;
+ final minutes = uptime.inMinutes % 60;
+
+ if (days > 0) {
+ return '$days days, $hours:${minutes.toString().padLeft(2, '0')}';
+ } else {
+ return '$hours:${minutes.toString().padLeft(2, '0')}';
+ }
+ } catch (e, s) {
+ Loggers.app.warning('Windows uptime parsing failed: $e for input: $raw', s);
+ return null;
+ }
+ }
+
+ /// Parse Windows CPU information from PowerShell output
+ static List parseCpu(String raw, ServerStatus serverStatus) {
+ try {
+ final dynamic jsonData = json.decode(raw);
+ final List cpus = [];
+
+ if (jsonData is List) {
+ for (int i = 0; i < jsonData.length; i++) {
+ final cpu = jsonData[i];
+ final loadPercentage = cpu['LoadPercentage'] ?? 0;
+ final usage = loadPercentage as int;
+ final idle = 100 - usage;
+
+ // Get previous CPU data to calculate cumulative values
+ final prevCpus = serverStatus.cpu.now;
+ final prevCpu = i < prevCpus.length ? prevCpus[i] : null;
+
+ // LIMITATION: Windows CPU counters approach
+ // PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
+ // We simulate cumulative counters by adding current percentages to previous totals.
+ // This approach has limitations:
+ // 1. Not as accurate as true cumulative time counters (Linux /proc/stat)
+ // 2. May drift over time with variable polling intervals
+ // 3. Results depend on consistent polling frequency
+ // However, this allows compatibility with existing delta-based CPU calculation logic.
+ final newUser = (prevCpu?.user ?? 0) + usage;
+ final newIdle = (prevCpu?.idle ?? 0) + idle;
+
+ cpus.add(
+ SingleCpuCore(
+ 'cpu$i',
+ newUser, // cumulative user time
+ 0, // sys (not available)
+ 0, // nice (not available)
+ newIdle, // cumulative idle time
+ 0, // iowait (not available)
+ 0, // irq (not available)
+ 0, // softirq (not available)
+ ),
+ );
+ }
+ } else if (jsonData is Map) {
+ // Single CPU core
+ final loadPercentage = jsonData['LoadPercentage'] ?? 0;
+ final usage = loadPercentage as int;
+ final idle = 100 - usage;
+
+ // Get previous CPU data to calculate cumulative values
+ final prevCpus = serverStatus.cpu.now;
+ final prevCpu = prevCpus.isNotEmpty ? prevCpus[0] : null;
+
+ // LIMITATION: See comment above for Windows CPU counter limitations
+ final newUser = (prevCpu?.user ?? 0) + usage;
+ final newIdle = (prevCpu?.idle ?? 0) + idle;
+
+ cpus.add(
+ SingleCpuCore(
+ 'cpu0',
+ newUser, // cumulative user time
+ 0, // sys
+ 0, // nice
+ newIdle, // cumulative idle time
+ 0, // iowait
+ 0, // irq
+ 0, // softirq
+ ),
+ );
+ }
+
+ return cpus;
+ } catch (e) {
+ return [];
+ }
+ }
+
+ /// Parse Windows memory information from PowerShell output
+ ///
+ /// NOTE: Windows Win32_OperatingSystem properties TotalVisibleMemorySize
+ /// and FreePhysicalMemory are returned in KB units.
+ static Memory? parseMemory(String raw) {
+ try {
+ final dynamic jsonData = json.decode(raw);
+ final data = jsonData is List ? jsonData.first : jsonData;
+
+ // Win32_OperatingSystem properties are in KB
+ final totalKB = data['TotalVisibleMemorySize'] as int? ?? 0;
+ final freeKB = data['FreePhysicalMemory'] as int? ?? 0;
+
+ return Memory(
+ total: totalKB,
+ free: freeKB,
+ avail: freeKB, // Windows doesn't distinguish between free and available
+ );
+ } catch (e) {
+ return null;
+ }
+ }
+
+ /// Parse Windows disk information from PowerShell output
+ static List parseDisks(String raw) {
+ try {
+ final dynamic jsonData = json.decode(raw);
+ final List disks = [];
+
+ final diskList = jsonData is List ? jsonData : [jsonData];
+
+ for (final diskData in diskList) {
+ final deviceId = diskData['DeviceID']?.toString() ?? '';
+ final size =
+ BigInt.tryParse(diskData['Size']?.toString() ?? '0') ?? BigInt.zero;
+ final freeSpace =
+ BigInt.tryParse(diskData['FreeSpace']?.toString() ?? '0') ??
+ BigInt.zero;
+ final fileSystem = diskData['FileSystem']?.toString() ?? '';
+
+ // Validate all required fields
+ final hasRequiredFields = deviceId.isNotEmpty &&
+ size != BigInt.zero &&
+ freeSpace != BigInt.zero &&
+ fileSystem.isNotEmpty;
+
+ if (!hasRequiredFields) {
+ Loggers.app.warning('Windows disk parsing: skipping disk with missing required fields. '
+ 'DeviceID: $deviceId, Size: $size, FreeSpace: $freeSpace, FileSystem: $fileSystem');
+ continue;
+ }
+
+ final sizeKB = size ~/ BigInt.from(1024);
+ final freeKB = freeSpace ~/ BigInt.from(1024);
+ final usedKB = sizeKB - freeKB;
+ final usedPercent = sizeKB > BigInt.zero
+ ? ((usedKB * BigInt.from(100)) ~/ sizeKB).toInt()
+ : 0;
+
+ disks.add(
+ Disk(
+ path: deviceId,
+ fsTyp: fileSystem,
+ size: sizeKB,
+ avail: freeKB,
+ used: usedKB,
+ usedPercent: usedPercent,
+ mount: deviceId, // Windows uses drive letters as mount points
+ ),
+ );
+ }
+
+ return disks;
+ } catch (e) {
+ Loggers.app.warning('Windows disk parsing failed: $e');
+ return [];
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/data/model/server/wol_cfg.dart b/lib/data/model/server/wol_cfg.dart
index 0974b870..861f0e93 100644
--- a/lib/data/model/server/wol_cfg.dart
+++ b/lib/data/model/server/wol_cfg.dart
@@ -11,11 +11,7 @@ final class WakeOnLanCfg {
final String ip;
final String? pwd;
- const WakeOnLanCfg({
- required this.mac,
- required this.ip,
- this.pwd,
- });
+ const WakeOnLanCfg({required this.mac, required this.ip, this.pwd});
(Object?, bool) validate() {
final macValidation = MACAddress.validate(mac);
@@ -39,10 +35,7 @@ final class WakeOnLanCfg {
final mac_ = MACAddress(mac);
final pwd_ = pwd != null ? SecureONPassword(pwd!) : null;
final obj = WakeOnLAN(ip_, mac_, password: pwd_);
- return obj.wake(
- repeat: 3,
- repeatDelay: const Duration(milliseconds: 500),
- );
+ return obj.wake(repeat: 3, repeatDelay: const Duration(milliseconds: 500));
}
factory WakeOnLanCfg.fromJson(Map json) => _$WakeOnLanCfgFromJson(json);
diff --git a/lib/data/model/sftp/req.dart b/lib/data/model/sftp/req.dart
index edca44eb..68147079 100644
--- a/lib/data/model/sftp/req.dart
+++ b/lib/data/model/sftp/req.dart
@@ -9,12 +9,7 @@ class SftpReq {
Spi? jumpSpi;
String? jumpPrivateKey;
- SftpReq(
- this.spi,
- this.remotePath,
- this.localPath,
- this.type,
- ) {
+ SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
final keyId = spi.keyId;
if (keyId != null) {
privateKey = getPrivateKey(keyId);
@@ -44,15 +39,9 @@ class SftpReqStatus {
Exception? error;
Duration? spentTime;
- SftpReqStatus({
- required this.req,
- required this.notifyListeners,
- this.completer,
- }) : id = DateTime.now().microsecondsSinceEpoch {
- worker = SftpWorker(
- onNotify: onNotify,
- req: req,
- )..init();
+ SftpReqStatus({required this.req, required this.notifyListeners, this.completer})
+ : id = DateTime.now().microsecondsSinceEpoch {
+ worker = SftpWorker(onNotify: onNotify, req: req)..init();
}
@override
diff --git a/lib/data/model/sftp/worker.dart b/lib/data/model/sftp/worker.dart
index ff14f0d8..42ca6f60 100644
--- a/lib/data/model/sftp/worker.dart
+++ b/lib/data/model/sftp/worker.dart
@@ -18,10 +18,7 @@ class SftpWorker {
final worker = Worker();
- SftpWorker({
- required this.onNotify,
- required this.req,
- });
+ SftpWorker({required this.onNotify, required this.req});
void _dispose() {
worker.dispose();
@@ -31,11 +28,7 @@ class SftpWorker {
/// the threads
Future init() async {
if (worker.isInitialized) worker.dispose();
- await worker.init(
- mainMessageHandler,
- isolateMessageHandler,
- errorHandler: print,
- );
+ await worker.init(mainMessageHandler, isolateMessageHandler, errorHandler: print);
worker.sendMessage(req);
}
@@ -46,11 +39,7 @@ class SftpWorker {
}
/// Handle the messages coming from the main
-Future isolateMessageHandler(
- dynamic data,
- SendPort mainSendPort,
- SendErrorFunction sendError,
-) async {
+Future isolateMessageHandler(dynamic data, SendPort mainSendPort, SendErrorFunction sendError) async {
switch (data) {
case final SftpReq val:
switch (val.type) {
@@ -67,11 +56,7 @@ Future isolateMessageHandler(
}
}
-Future _download(
- SftpReq req,
- SendPort mainSendPort,
- SendErrorFunction sendError,
-) async {
+Future _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sendError) async {
try {
mainSendPort.send(SftpWorkerStatus.preparing);
final watch = Stopwatch()..start();
@@ -103,12 +88,12 @@ Future _download(
// Due to single core performance, limit the chunk size
const defaultChunkSize = 1024 * 1024 * 5;
var totalRead = 0;
-
+
while (totalRead < size) {
final remaining = size - totalRead;
final chunkSize = remaining > defaultChunkSize ? defaultChunkSize : remaining;
dprint('Size: $size, Total Read: $totalRead, Chunk Size: $chunkSize');
-
+
final fileData = file.read(offset: totalRead, length: chunkSize);
await for (var chunk in fileData) {
localFile.add(chunk);
@@ -127,11 +112,7 @@ Future _download(
}
}
-Future _upload(
- SftpReq req,
- SendPort mainSendPort,
- SendErrorFunction sendError,
-) async {
+Future _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendError) async {
try {
mainSendPort.send(SftpWorkerStatus.preparing);
final watch = Stopwatch()..start();
@@ -156,9 +137,7 @@ Future _upload(
// If remote exists, overwrite it
final file = await sftp.open(
req.remotePath,
- mode: SftpFileOpenMode.truncate |
- SftpFileOpenMode.create |
- SftpFileOpenMode.write,
+ mode: SftpFileOpenMode.truncate | SftpFileOpenMode.create | SftpFileOpenMode.write,
);
final writer = file.write(
localFile,
diff --git a/lib/data/model/ssh/virtual_key.dart b/lib/data/model/ssh/virtual_key.dart
index f46cc058..ac61fa4b 100644
--- a/lib/data/model/ssh/virtual_key.dart
+++ b/lib/data/model/ssh/virtual_key.dart
@@ -51,30 +51,30 @@ enum VirtKey {
f9,
f10,
f11,
- f12;
+ f12,
}
extension VirtKeyX on VirtKey {
/// Used for input to terminal
String? get inputRaw => switch (this) {
- VirtKey.slash => '/',
- VirtKey.backSlash => '\\',
- VirtKey.underscore => '_',
- VirtKey.plus => '+',
- VirtKey.equal => '=',
- VirtKey.minus => '-',
- VirtKey.parenLeft => '(',
- VirtKey.parenRight => ')',
- VirtKey.bracketLeft => '[',
- VirtKey.bracketRight => ']',
- VirtKey.braceLeft => '{',
- VirtKey.braceRight => '}',
- VirtKey.chevronLeft => '<',
- VirtKey.chevronRight => '>',
- VirtKey.colon => ':',
- VirtKey.semicolon => ';',
- _ => null,
- };
+ VirtKey.slash => '/',
+ VirtKey.backSlash => '\\',
+ VirtKey.underscore => '_',
+ VirtKey.plus => '+',
+ VirtKey.equal => '=',
+ VirtKey.minus => '-',
+ VirtKey.parenLeft => '(',
+ VirtKey.parenRight => ')',
+ VirtKey.bracketLeft => '[',
+ VirtKey.bracketRight => ']',
+ VirtKey.braceLeft => '{',
+ VirtKey.braceRight => '}',
+ VirtKey.chevronLeft => '<',
+ VirtKey.chevronRight => '>',
+ VirtKey.colon => ':',
+ VirtKey.semicolon => ';',
+ _ => null,
+ };
/// Used for displaying on UI
String get text {
@@ -111,74 +111,74 @@ extension VirtKeyX on VirtKey {
/// Corresponding [TerminalKey]
TerminalKey? get key => switch (this) {
- VirtKey.esc => TerminalKey.escape,
- VirtKey.alt => TerminalKey.alt,
- VirtKey.home => TerminalKey.home,
- VirtKey.up => TerminalKey.arrowUp,
- VirtKey.end => TerminalKey.end,
- VirtKey.tab => TerminalKey.tab,
- VirtKey.ctrl => TerminalKey.control,
- VirtKey.left => TerminalKey.arrowLeft,
- VirtKey.down => TerminalKey.arrowDown,
- VirtKey.right => TerminalKey.arrowRight,
- VirtKey.shift => TerminalKey.shift,
- VirtKey.pgup => TerminalKey.pageUp,
- VirtKey.pgdn => TerminalKey.pageDown,
- VirtKey.f1 => TerminalKey.f1,
- VirtKey.f2 => TerminalKey.f2,
- VirtKey.f3 => TerminalKey.f3,
- VirtKey.f4 => TerminalKey.f4,
- VirtKey.f5 => TerminalKey.f5,
- VirtKey.f6 => TerminalKey.f6,
- VirtKey.f7 => TerminalKey.f7,
- VirtKey.f8 => TerminalKey.f8,
- VirtKey.f9 => TerminalKey.f9,
- VirtKey.f10 => TerminalKey.f10,
- VirtKey.f11 => TerminalKey.f11,
- VirtKey.f12 => TerminalKey.f12,
- _ => null,
- };
+ VirtKey.esc => TerminalKey.escape,
+ VirtKey.alt => TerminalKey.alt,
+ VirtKey.home => TerminalKey.home,
+ VirtKey.up => TerminalKey.arrowUp,
+ VirtKey.end => TerminalKey.end,
+ VirtKey.tab => TerminalKey.tab,
+ VirtKey.ctrl => TerminalKey.control,
+ VirtKey.left => TerminalKey.arrowLeft,
+ VirtKey.down => TerminalKey.arrowDown,
+ VirtKey.right => TerminalKey.arrowRight,
+ VirtKey.shift => TerminalKey.shift,
+ VirtKey.pgup => TerminalKey.pageUp,
+ VirtKey.pgdn => TerminalKey.pageDown,
+ VirtKey.f1 => TerminalKey.f1,
+ VirtKey.f2 => TerminalKey.f2,
+ VirtKey.f3 => TerminalKey.f3,
+ VirtKey.f4 => TerminalKey.f4,
+ VirtKey.f5 => TerminalKey.f5,
+ VirtKey.f6 => TerminalKey.f6,
+ VirtKey.f7 => TerminalKey.f7,
+ VirtKey.f8 => TerminalKey.f8,
+ VirtKey.f9 => TerminalKey.f9,
+ VirtKey.f10 => TerminalKey.f10,
+ VirtKey.f11 => TerminalKey.f11,
+ VirtKey.f12 => TerminalKey.f12,
+ _ => null,
+ };
/// Icons for virtual keys
IconData? get icon => switch (this) {
- VirtKey.up => Icons.arrow_upward,
- VirtKey.left => Icons.arrow_back,
- VirtKey.down => Icons.arrow_downward,
- VirtKey.right => Icons.arrow_forward,
- VirtKey.sftp => Icons.file_open,
- VirtKey.snippet => Icons.code,
- VirtKey.clipboard => Icons.paste,
- VirtKey.ime => Icons.keyboard,
- _ => null,
- };
+ VirtKey.up => Icons.arrow_upward,
+ VirtKey.left => Icons.arrow_back,
+ VirtKey.down => Icons.arrow_downward,
+ VirtKey.right => Icons.arrow_forward,
+ VirtKey.sftp => Icons.file_open,
+ VirtKey.snippet => Icons.code,
+ VirtKey.clipboard => Icons.paste,
+ VirtKey.ime => Icons.keyboard,
+ _ => null,
+ };
// Use [VirtualKeyFunc] instead of [VirtKey]
// This can help linter to enum all [VirtualKeyFunc]
// and make sure all [VirtualKeyFunc] are handled
VirtualKeyFunc? get func => switch (this) {
- VirtKey.sftp => VirtualKeyFunc.file,
- VirtKey.snippet => VirtualKeyFunc.snippet,
- VirtKey.clipboard => VirtualKeyFunc.clipboard,
- VirtKey.ime => VirtualKeyFunc.toggleIME,
- _ => null,
- };
+ VirtKey.sftp => VirtualKeyFunc.file,
+ VirtKey.snippet => VirtualKeyFunc.snippet,
+ VirtKey.clipboard => VirtualKeyFunc.clipboard,
+ VirtKey.ime => VirtualKeyFunc.toggleIME,
+ _ => null,
+ };
bool get toggleable => switch (this) {
- VirtKey.alt || VirtKey.ctrl || VirtKey.shift => true,
- _ => false,
- };
+ VirtKey.alt || VirtKey.ctrl || VirtKey.shift => true,
+ _ => false,
+ };
bool get canLongPress => switch (this) {
- VirtKey.up || VirtKey.left || VirtKey.down || VirtKey.right => true,
- _ => false,
- };
+ VirtKey.up || VirtKey.left || VirtKey.down || VirtKey.right => true,
+ _ => false,
+ };
String? get help => switch (this) {
- VirtKey.sftp => l10n.virtKeyHelpSFTP,
- VirtKey.clipboard => l10n.virtKeyHelpClipboard,
- VirtKey.ime => l10n.virtKeyHelpIME,
- _ => null,
- };
+ VirtKey.sftp => l10n.virtKeyHelpSFTP,
+ VirtKey.clipboard => l10n.virtKeyHelpClipboard,
+ VirtKey.ime => l10n.virtKeyHelpIME,
+ _ => null,
+ };
/// - [saveDefaultIfErr] if the stored raw values is invalid, save default order to store
static List loadFromStore({bool saveDefaultIfErr = true}) {
diff --git a/lib/data/provider/app.dart b/lib/data/provider/app.dart
index 40de951b..09028857 100644
--- a/lib/data/provider/app.dart
+++ b/lib/data/provider/app.dart
@@ -7,9 +7,7 @@ part 'app.freezed.dart';
@freezed
abstract class AppState with _$AppState {
- const factory AppState({
- @Default(false) bool desktopMode,
- }) = _AppState;
+ const factory AppState({@Default(false) bool desktopMode}) = _AppState;
}
@Riverpod(keepAlive: true)
diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart
index f5e705ae..d642dfbb 100644
--- a/lib/data/provider/container.dart
+++ b/lib/data/provider/container.dart
@@ -12,8 +12,7 @@ import 'package:server_box/data/model/container/ps.dart';
import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/store.dart';
-final _dockerNotFound =
- RegExp(r"command not found|Unknown command|Command '\w+' not found");
+final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found");
class ContainerProvider extends ChangeNotifier {
final SSHClient? client;
@@ -90,11 +89,7 @@ class ContainerProvider extends ChangeNotifier {
final includeStats = Stores.setting.containerParseStat.fetch();
var raw = '';
- final cmd = _wrap(ContainerCmdType.execAll(
- type,
- sudo: sudo,
- includeStats: includeStats,
- ));
+ final cmd = _wrap(ContainerCmdType.execAll(type, sudo: sudo, includeStats: includeStats));
final code = await client?.execWithPwd(
cmd,
context: context,
@@ -130,10 +125,7 @@ class ContainerProvider extends ChangeNotifier {
try {
version = json.decode(verRaw)['Client']['Version'];
} catch (e, trace) {
- error = ContainerErr(
- type: ContainerErrType.invalidVersion,
- message: '$e',
- );
+ error = ContainerErr(type: ContainerErrType.invalidVersion, message: '$e');
Loggers.app.warning('Container version failed', e, trace);
} finally {
notifyListeners();
@@ -150,10 +142,7 @@ class ContainerProvider extends ChangeNotifier {
lines.removeWhere((element) => element.isEmpty);
items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
} catch (e, trace) {
- error = ContainerErr(
- type: ContainerErrType.parsePs,
- message: '$e',
- );
+ error = ContainerErr(type: ContainerErrType.parsePs, message: '$e');
Loggers.app.warning('Container ps failed', e, trace);
} finally {
notifyListeners();
@@ -173,10 +162,7 @@ class ContainerProvider extends ChangeNotifier {
images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
}
} catch (e, trace) {
- error = ContainerErr(
- type: ContainerErrType.parseImages,
- message: '$e',
- );
+ error = ContainerErr(type: ContainerErrType.parseImages, message: '$e');
Loggers.app.warning('Container images failed', e, trace);
} finally {
notifyListeners();
@@ -199,10 +185,7 @@ class ContainerProvider extends ChangeNotifier {
item.parseStats(statsLine);
}
} catch (e, trace) {
- error = ContainerErr(
- type: ContainerErrType.parseStats,
- message: '$e',
- );
+ error = ContainerErr(type: ContainerErrType.parseStats, message: '$e');
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
} finally {
notifyListeners();
@@ -261,10 +244,7 @@ class ContainerProvider extends ChangeNotifier {
notifyListeners();
if (code != 0) {
- return ContainerErr(
- type: ContainerErrType.unknown,
- message: errs.join('\n').trim(),
- );
+ return ContainerErr(type: ContainerErrType.unknown, message: errs.join('\n').trim());
}
if (autoRefresh) await refresh();
return null;
@@ -288,40 +268,32 @@ enum ContainerCmdType {
version,
ps,
stats,
- images,
+ images
// No specific commands needed for prune actions as they are simple
// and don't require splitting output with ShellFunc.seperator
;
- String exec(
- ContainerType type, {
- bool sudo = false,
- bool includeStats = false,
- }) {
+ String exec(ContainerType type, {bool sudo = false, bool includeStats = false}) {
final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
return switch (this) {
ContainerCmdType.version => '$prefix version $_jsonFmt',
ContainerCmdType.ps => switch (type) {
- /// TODO: Rollback to json format when permformance recovers.
- /// Use [_jsonFmt] in Docker will cause the operation to slow down.
- ContainerType.docker => '$prefix ps -a --format "table {{printf \\"'
+ /// TODO: Rollback to json format when permformance recovers.
+ /// Use [_jsonFmt] in Docker will cause the operation to slow down.
+ ContainerType.docker =>
+ '$prefix ps -a --format "table {{printf \\"'
'%-15.15s '
'%-30.30s '
'${"%-50.50s " * 2}\\"'
' .ID .Status .Names .Image}}"',
- ContainerType.podman => '$prefix ps -a $_jsonFmt',
- },
- ContainerCmdType.stats =>
- includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
+ ContainerType.podman => '$prefix ps -a $_jsonFmt',
+ },
+ ContainerCmdType.stats => includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
};
}
- static String execAll(
- ContainerType type, {
- bool sudo = false,
- bool includeStats = false,
- }) {
+ static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false}) {
return ContainerCmdType.values
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
.join('\necho ${ShellFunc.seperator}\n');
diff --git a/lib/data/provider/pve.dart b/lib/data/provider/pve.dart
index 518b50ed..8ef23730 100644
--- a/lib/data/provider/pve.dart
+++ b/lib/data/provider/pve.dart
@@ -86,15 +86,18 @@ final class PveProvider extends ChangeNotifier {
forward.stream.cast>().pipe(socket);
socket.cast>().pipe(forward.sink);
});
- final newUrl = Uri.parse(addr)
- .replace(host: 'localhost', port: _localPort)
- .toString();
+ final newUrl = Uri.parse(
+ addr,
+ ).replace(host: 'localhost', port: _localPort).toString();
debugPrint('Forwarding $newUrl to $addr');
}
}
Future> cf(
- Uri url, String? proxyHost, int? proxyPort) async {
+ Uri url,
+ String? proxyHost,
+ int? proxyPort,
+ ) async {
/* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
final _localPort = serverSocket.port;
serverSocket.listen((socket) async {
@@ -105,8 +108,11 @@ final class PveProvider extends ChangeNotifier {
});*/
if (url.isScheme('https')) {
- return SecureSocket.startConnect('localhost', _localPort,
- onBadCertificate: (_) => true);
+ return SecureSocket.startConnect(
+ 'localhost',
+ _localPort,
+ onBadCertificate: (_) => true,
+ );
} else {
return Socket.startConnect('localhost', _localPort);
}
@@ -119,7 +125,7 @@ final class PveProvider extends ChangeNotifier {
'username': spi.user,
'password': spi.pwd,
'realm': 'pam',
- 'new-format': '1'
+ 'new-format': '1',
},
options: Options(
headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType},
@@ -151,8 +157,10 @@ final class PveProvider extends ChangeNotifier {
try {
final resp = await session.get('$addr/api2/json/cluster/resources');
final res = resp.data['data'] as List;
- final result =
- await Computer.shared.start(PveRes.parse, (res, data.value));
+ final result = await Computer.shared.start(PveRes.parse, (
+ res,
+ data.value,
+ ));
data.value = result;
} catch (e) {
Loggers.app.warning('PVE list failed', e);
@@ -164,29 +172,33 @@ final class PveProvider extends ChangeNotifier {
Future reboot(String node, String id) async {
await connected.future;
- final resp =
- await session.post('$addr/api2/json/nodes/$node/$id/status/reboot');
+ final resp = await session.post(
+ '$addr/api2/json/nodes/$node/$id/status/reboot',
+ );
return _isCtrlSuc(resp);
}
Future start(String node, String id) async {
await connected.future;
- final resp =
- await session.post('$addr/api2/json/nodes/$node/$id/status/start');
+ final resp = await session.post(
+ '$addr/api2/json/nodes/$node/$id/status/start',
+ );
return _isCtrlSuc(resp);
}
Future stop(String node, String id) async {
await connected.future;
- final resp =
- await session.post('$addr/api2/json/nodes/$node/$id/status/stop');
+ final resp = await session.post(
+ '$addr/api2/json/nodes/$node/$id/status/stop',
+ );
return _isCtrlSuc(resp);
}
Future shutdown(String node, String id) async {
await connected.future;
- final resp =
- await session.post('$addr/api2/json/nodes/$node/$id/status/shutdown');
+ final resp = await session.post(
+ '$addr/api2/json/nodes/$node/$id/status/shutdown',
+ );
return _isCtrlSuc(resp);
}
diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart
index 58049616..ca404fb1 100644
--- a/lib/data/provider/server.dart
+++ b/lib/data/provider/server.dart
@@ -9,6 +9,7 @@ import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/core/sync.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/core/utils/ssh_auth.dart';
+import 'package:server_box/data/helper/system_detector.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/server.dart';
@@ -32,6 +33,8 @@ class ServerProvider extends Provider {
static final _manualDisconnectedIds = {};
+ static final _serverIdsUpdating = ?>{};
+
@override
Future load() async {
super.load();
@@ -124,11 +127,35 @@ class ServerProvider extends Provider {
return;
}
- return await _getData(s.spi);
+ // Check if already updating, and if so, wait for it to complete
+ final existingUpdate = _serverIdsUpdating[s.spi.id];
+ if (existingUpdate != null) {
+ // Already updating, wait for the existing update to complete
+ try {
+ await existingUpdate;
+ } catch (e) {
+ // Ignore errors from the existing update, we'll try our own
+ }
+ return;
+ }
+
+ // Start a new update operation
+ final updateFuture = _updateServer(s.spi);
+ _serverIdsUpdating[s.spi.id] = updateFuture;
+
+ try {
+ await updateFuture;
+ } finally {
+ _serverIdsUpdating.remove(s.spi.id);
+ }
}),
);
}
+ static Future _updateServer(Spi spi) async {
+ await _getData(spi);
+ }
+
static Future startAutoRefresh() async {
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
stopAutoRefresh();
@@ -305,13 +332,17 @@ class ServerProvider extends Provider {
_setServerState(s, ServerConn.connected);
try {
+ // Detect system type using helper
+ final detectedSystemType = await SystemDetector.detect(sv.client!, spi);
+ sv.status.system = detectedSystemType;
+
final (_, writeScriptResult) = await sv.client!.exec((session) async {
- final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
+ final scriptRaw = ShellFunc.allScript(spi.custom?.cmds, systemType: detectedSystemType).uint8List;
session.stdin.add(scriptRaw);
session.stdin.close();
- }, entry: ShellFunc.getInstallShellCmd(spi.id));
- if (writeScriptResult.isNotEmpty) {
- ShellFunc.switchScriptDir(spi.id);
+ }, entry: ShellFunc.getInstallShellCmd(spi.id, systemType: detectedSystemType));
+ if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
+ ShellFunc.switchScriptDir(spi.id, systemType: detectedSystemType);
throw writeScriptResult;
}
} on SSHAuthAbortError catch (e) {
@@ -351,7 +382,8 @@ class ServerProvider extends Provider {
String? raw;
try {
- raw = await sv.client?.run(ShellFunc.status.exec(spi.id)).string;
+ raw = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)).string;
+ dprint('Get status from ${spi.name}:\n$raw');
segments = raw?.split(ShellFunc.seperator).map((e) => e.trim()).toList();
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
if (Stores.setting.keepStatusWhenErr.fetch()) {
diff --git a/lib/data/provider/systemd.dart b/lib/data/provider/systemd.dart
index e55d7fa7..131c5245 100644
--- a/lib/data/provider/systemd.dart
+++ b/lib/data/provider/systemd.dart
@@ -44,10 +44,8 @@ final class SystemdProvider {
}
}
- final parsedUserUnits =
- await _parseUnitObj(userUnits, SystemdUnitScope.user);
- final parsedSystemUnits =
- await _parseUnitObj(systemUnits, SystemdUnitScope.system);
+ final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user);
+ final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system);
this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
} catch (e, s) {
Loggers.app.warning('Parse systemd', e, s);
@@ -56,14 +54,10 @@ final class SystemdProvider {
isBusy.value = false;
}
- Future> _parseUnitObj(
- List unitNames,
- SystemdUnitScope scope,
- ) async {
- final unitNames_ = unitNames
- .map((e) => e.trim().split('/').last.split('.').first)
- .toList();
- final script = '''
+ Future> _parseUnitObj(List unitNames, SystemdUnitScope scope) async {
+ final unitNames_ = unitNames.map((e) => e.trim().split('/').last.split('.').first).toList();
+ final script =
+ '''
for unit in ${unitNames_.join(' ')}; do
state=\$(systemctl show --no-pager \$unit)
echo -n "${ShellFunc.seperator}\n\$state"
@@ -108,13 +102,9 @@ done
continue;
}
- parsedUnits.add(SystemdUnit(
- name: name,
- type: unitType,
- scope: scope,
- state: unitState,
- description: description,
- ));
+ parsedUnits.add(
+ SystemdUnit(name: name, type: unitType, scope: scope, state: unitState, description: description),
+ );
}
parsedUnits.sort((a, b) {
@@ -131,7 +121,8 @@ done
return parsedUnits;
}
- late final _getUnitsCmd = '''
+ late final _getUnitsCmd =
+ '''
get_files() {
unit_type=\$1
base_dir=\$2
diff --git a/lib/data/store/container.dart b/lib/data/store/container.dart
index aa19fed3..aecdb7df 100644
--- a/lib/data/store/container.dart
+++ b/lib/data/store/container.dart
@@ -20,8 +20,7 @@ class ContainerStore extends HiveStore {
ContainerType getType([String id = '']) {
final cfg = box.get(_keyConfig + id);
if (cfg != null) {
- final type =
- ContainerType.values.firstWhereOrNull((e) => e.toString() == cfg);
+ final type = ContainerType.values.firstWhereOrNull((e) => e.toString() == cfg);
if (type != null) return type;
}
diff --git a/lib/data/store/history.dart b/lib/data/store/history.dart
index ad81394d..6d2d708c 100644
--- a/lib/data/store/history.dart
+++ b/lib/data/store/history.dart
@@ -7,12 +7,10 @@ class _ListHistory {
final String _name;
final Box _box;
- _ListHistory({
- required Box box,
- required String name,
- }) : _box = box,
- _name = name,
- _history = box.get(name, defaultValue: [])!;
+ _ListHistory({required Box box, required String name})
+ : _box = box,
+ _name = name,
+ _history = box.get(name, defaultValue: [])!;
void add(String path) {
_history.remove(path);
@@ -28,12 +26,10 @@ class _MapHistory {
final String _name;
final Box _box;
- _MapHistory({
- required Box box,
- required String name,
- }) : _box = box,
- _name = name,
- _history = box.get(name, defaultValue: {})!;
+ _MapHistory({required Box box, required String name})
+ : _box = box,
+ _name = name,
+ _history = box.get(name, defaultValue: {})!;
void put(String id, String val) {
_history[id] = val;
@@ -56,6 +52,5 @@ class HistoryStore extends HiveStore {
late final sshCmds = _ListHistory(box: box, name: 'sshCmds');
/// Notify users that this app will write script to server to works properly
- late final writeScriptTipShown =
- propertyDefault('writeScriptTipShown', false);
+ late final writeScriptTipShown = propertyDefault('writeScriptTipShown', false);
}
diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart
index 2dd065b5..44355b16 100644
--- a/lib/generated/l10n/l10n_zh.dart
+++ b/lib/generated/l10n/l10n_zh.dart
@@ -15,7 +15,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get acceptBeta => '接受测试版更新推送';
@override
- String get addSystemPrivateKeyTip => '当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?';
+ String get addSystemPrivateKeyTip => '检测到暂无私钥,是否添加系统默认的私钥(~/.ssh/id_rsa)?';
@override
String get added2List => '已添加至任务列表';
@@ -24,13 +24,13 @@ class AppLocalizationsZh extends AppLocalizations {
String get addr => '地址';
@override
- String get alreadyLastDir => '已经是最上层目录了';
+ String get alreadyLastDir => '已是顶级目录';
@override
- String get authFailTip => '认证失败,请检查密码/密钥/主机/用户等是否错误';
+ String get authFailTip => '认证失败,请检查连接信息是否正确';
@override
- String get autoBackupConflict => '只能同时开启一个自动备份';
+ String get autoBackupConflict => '仅可启用一个自动备份任务';
@override
String get autoConnect => '自动连接';
@@ -42,10 +42,10 @@ class AppLocalizationsZh extends AppLocalizations {
String get autoUpdateHomeWidget => '自动更新桌面小部件';
@override
- String get backupTip => '导出的数据可以使用密码加密,请妥善保管。';
+ String get backupTip => '导出数据可通过密码加密,请妥善保管。';
@override
- String get backupVersionNotMatch => '备份版本不匹配,无法恢复';
+ String get backupVersionNotMatch => '备份版本不兼容,无法恢复';
@override
String get backupPassword => '备份密码';
@@ -76,7 +76,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get bgRunTip =>
- '此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请修改省电策略为“无限制”。';
+ '此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请将省电策略改为“无限制”。';
@override
String get closeAfterSave => '保存后关闭';
@@ -132,10 +132,10 @@ class AppLocalizationsZh extends AppLocalizations {
String get desktopTerminalTip => '启动 SSH 连接所用的终端模拟器命令';
@override
- String get dirEmpty => '请确保文件夹为空';
+ String get dirEmpty => '请确保目录为空';
@override
- String get disconnected => '连接断开';
+ String get disconnected => '已断开连接';
@override
String get disk => '磁盘';
@@ -160,11 +160,11 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String dockerImagesFmt(Object count) {
- return '共 $count 个镜像';
+ return '$count 个镜像';
}
@override
- String get dockerNotInstalled => 'Docker 未安装';
+ String get dockerNotInstalled => '未安装 Docker';
@override
String dockerStatusRunningAndStoppedFmt(
@@ -183,7 +183,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get doubleColumnMode => '双列模式';
@override
- String get doubleColumnTip => '此选项仅开启功能,实际是否能开启还取决于设备的宽度';
+ String get doubleColumnTip => '此选项仅用于启用该功能,是否生效取决于设备宽度';
@override
String get editVirtKeys => '编辑虚拟按键';
@@ -192,7 +192,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get editor => '编辑器';
@override
- String get editorHighlightTip => '目前的代码高亮性能较为糟糕,可以选择关闭以改善。';
+ String get editorHighlightTip => '代码高亮功能可能影响性能,可选择关闭。';
@override
String get emulator => '模拟器';
@@ -246,7 +246,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get fullScreenJitter => '全屏模式抖动';
@override
- String get fullScreenJitterHelp => '防止烧屏';
+ String get fullScreenJitterHelp => '用于防止屏幕烧屏';
@override
String get fullScreenTip => '当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器 Tab 页。';
@@ -271,7 +271,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String httpFailedWithCode(Object code) {
- return '请求失败, 状态码: $code';
+ return '请求失败,状态码: $code';
}
@override
@@ -294,7 +294,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get installDockerWithUrl =>
- '请先 https://docs.docker.com/engine/install docker';
+ '请先前往 https://docs.docker.com/engine/install 安装 Docker';
@override
String get invalid => '无效';
@@ -303,7 +303,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get jumpServer => '跳板服务器';
@override
- String get keepForeground => '请保持应用处于前台!';
+ String get keepForeground => '请将应用保持在前台运行';
@override
String get keepStatusWhenErr => '保留上次的服务器状态';
@@ -344,7 +344,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get maxRetryCount => '服务器尝试重连次数';
@override
- String get maxRetryCountEqual0 => '会无限重试';
+ String get maxRetryCountEqual0 => '将无限次重试';
@override
String get min => '最小';
@@ -409,7 +409,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get openLastPath => '打开上次的路径';
@override
- String get openLastPathTip => '不同的服务器会有不同的记录,且记录的是退出时的路径';
+ String get openLastPathTip => '将为每台服务器记录其最后访问路径';
@override
String get parseContainerStatsTip => 'Docker 解析占用状态较为缓慢';
@@ -462,7 +462,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get pveIgnoreCertTip => '不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项';
@override
- String get pveLoginFailed => '登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。';
+ String get pveLoginFailed => '登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。';
@override
String get pveVersionLow => '当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用';
@@ -544,7 +544,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 来删除文件夹';
@override
- String get sftpSSHConnected => 'SFTP 已连接...';
+ String get sftpSSHConnected => 'SFTP 已连接';
@override
String get sftpShowFoldersFirst => '文件夹显示在前';
@@ -575,7 +575,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String spentTime(Object time) {
- return '耗时: $time';
+ return '耗时:$time';
}
@override
@@ -683,7 +683,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get update => '更新';
@override
- String get updateIntervalEqual0 => '你设置为 0,服务器状态不会自动刷新。\n且不能计算 CPU 使用情况。';
+ String get updateIntervalEqual0 => '设置为 0 将不自动刷新服务器状态。\n且无法计算 CPU 使用率。';
@override
String get updateServerStatusInterval => '服务器状态刷新间隔';
@@ -743,7 +743,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get whenOpenApp => '当打开 App 时';
@override
- String get wolTip => '在配置 WOL 后,每次连接服务器都会先发送一次 WOL 请求';
+ String get wolTip => '配置 WOL 后,每次连接服务器时将自动发送唤醒请求';
@override
String get write => '写';
@@ -767,7 +767,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get acceptBeta => '接受測試版更新推送';
@override
- String get addSystemPrivateKeyTip => '目前沒有任何私鑰,是否新增系統原有的 (~/.ssh/id_rsa)?';
+ String get addSystemPrivateKeyTip => '偵測到尚無私鑰,是否要加入系統預設的私鑰(~/.ssh/id_rsa)?';
@override
String get added2List => '已新增至任務清單';
@@ -776,13 +776,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get addr => '位址';
@override
- String get alreadyLastDir => '已經是最上層目錄了';
+ String get alreadyLastDir => '已是頂層目錄';
@override
- String get authFailTip => '認證失敗,請檢查密碼/金鑰/主機/使用者等是否錯誤。';
+ String get authFailTip => '認證失敗,請檢查連線資訊是否正確';
@override
- String get autoBackupConflict => '只能同時開啓一個自動備份';
+ String get autoBackupConflict => '僅能啟用一項自動備份任務';
@override
String get autoConnect => '自動連線';
@@ -794,10 +794,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get autoUpdateHomeWidget => '自動更新桌面小工具';
@override
- String get backupTip => '匯出的資料可以使用密碼加密。 \n請妥善保管。';
+ String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。';
@override
- String get backupVersionNotMatch => '備份版本不相符,無法還原';
+ String get backupVersionNotMatch => '備份版本不相容,無法還原';
@override
String get backupPassword => '備份密碼';
@@ -828,7 +828,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get bgRunTip =>
- '此開關只代表程式會嘗試在背景執行,具體能否在後臺執行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池最佳化”,MIUI / HyperOS 請修改省電策略為“無限制”。';
+ '此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。';
@override
String get closeAfterSave => '儲存後關閉';
@@ -884,10 +884,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get desktopTerminalTip => '啟動 SSH 連線時用於打開終端機模擬器的指令。';
@override
- String get dirEmpty => '請確保資料夾為空';
+ String get dirEmpty => '請確保目錄為空';
@override
- String get disconnected => '連線中斷';
+ String get disconnected => '已中斷連線';
@override
String get disk => '磁碟';
@@ -912,11 +912,11 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String dockerImagesFmt(Object count) {
- return '共 $count 個映像檔';
+ return '$count 個映像檔';
}
@override
- String get dockerNotInstalled => 'Docker 未安裝';
+ String get dockerNotInstalled => '未安裝 Docker';
@override
String dockerStatusRunningAndStoppedFmt(
@@ -935,7 +935,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get doubleColumnMode => '雙列模式';
@override
- String get doubleColumnTip => '此選項僅開啟功能,實際是否能開啟還取決於設備的頻寬';
+ String get doubleColumnTip => '此選項僅用於啟用此功能,是否生效取決於裝置寬度';
@override
String get editVirtKeys => '編輯虛擬按鍵';
@@ -944,7 +944,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get editor => '編輯器';
@override
- String get editorHighlightTip => '目前的程式碼標記效能較為糟糕,可以選擇關閉以改善。';
+ String get editorHighlightTip => '程式碼高亮功能可能影響效能,可選擇性關閉。';
@override
String get emulator => '模擬器';
@@ -998,7 +998,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get fullScreenJitter => '全螢幕模式抖動';
@override
- String get fullScreenJitterHelp => '防止烙印';
+ String get fullScreenJitterHelp => '防止螢幕烙印';
@override
String get fullScreenTip => '當設備旋轉為橫向時,是否開啟全螢幕模式?此選項僅適用於伺服器分頁。';
@@ -1023,7 +1023,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String httpFailedWithCode(Object code) {
- return '請求失敗, 狀態碼: $code';
+ return '請求失敗,狀態碼:$code';
}
@override
@@ -1046,7 +1046,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get installDockerWithUrl =>
- '請先 https://docs.docker.com/engine/install docker';
+ '請先前往 https://docs.docker.com/engine/install 安裝 Docker';
@override
String get invalid => '無效';
@@ -1055,7 +1055,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get jumpServer => '跳板伺服器';
@override
- String get keepForeground => '請保持App處於前端!';
+ String get keepForeground => '請讓 App 保持在前景執行';
@override
String get keepStatusWhenErr => '保留上次的伺服器狀態';
@@ -1096,7 +1096,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get maxRetryCount => '伺服器嘗試重連次數';
@override
- String get maxRetryCountEqual0 => '會無限重試';
+ String get maxRetryCountEqual0 => '將無限次重試';
@override
String get min => '最小';
@@ -1161,7 +1161,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get openLastPath => '打開上次的路徑';
@override
- String get openLastPathTip => '不同的伺服器會有不同的記錄,且記錄的是退出時的路徑';
+ String get openLastPathTip => '將為每台伺服器紀錄其最後存取路徑';
@override
String get parseContainerStatsTip => 'Docker 解析消耗狀態較為緩慢';
@@ -1214,7 +1214,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get pveIgnoreCertTip => '不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。';
@override
- String get pveLoginFailed => '登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。';
+ String get pveLoginFailed => '登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。';
@override
String get pveVersionLow => '此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。';
@@ -1296,7 +1296,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除檔案夾';
@override
- String get sftpSSHConnected => 'SFTP 已連線...';
+ String get sftpSSHConnected => 'SFTP 已連線';
@override
String get sftpShowFoldersFirst => '資料夾顯示在前';
@@ -1327,7 +1327,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String spentTime(Object time) {
- return '耗時: $time';
+ return '耗時:$time';
}
@override
@@ -1435,7 +1435,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get update => '更新';
@override
- String get updateIntervalEqual0 => '你設定為 0,伺服器狀態不會自動更新。\n且不能計算CPU使用情況。';
+ String get updateIntervalEqual0 => '設定為 0 將不自動刷新伺服器狀態,\n也無法計算 CPU 使用率。';
@override
String get updateServerStatusInterval => '伺服器狀態更新間隔';
@@ -1495,7 +1495,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get whenOpenApp => '當打開 App 時';
@override
- String get wolTip => '在配置 WOL(網絡喚醒)後,每次連線伺服器都會先發送一次 WOL 請求。';
+ String get wolTip => '設定 WOL 後,每次連線伺服器時將自動發送喚醒請求';
@override
String get write => '寫入';
diff --git a/lib/hive/hive_adapters.dart b/lib/hive/hive_adapters.dart
index 92701fe7..63ecce7e 100644
--- a/lib/hive/hive_adapters.dart
+++ b/lib/hive/hive_adapters.dart
@@ -5,6 +5,7 @@ import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/snippet.dart';
+import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/model/ssh/virtual_key.dart';
@@ -17,5 +18,6 @@ import 'package:server_box/data/model/ssh/virtual_key.dart';
AdapterSpec(),
AdapterSpec(),
AdapterSpec(),
+ AdapterSpec(),
])
part 'hive_adapters.g.dart';
diff --git a/lib/hive/hive_adapters.g.dart b/lib/hive/hive_adapters.g.dart
index 323edd21..23676ff9 100644
--- a/lib/hive/hive_adapters.g.dart
+++ b/lib/hive/hive_adapters.g.dart
@@ -111,13 +111,14 @@ class SpiAdapter extends TypeAdapter {
wolCfg: fields[11] as WakeOnLanCfg?,
envs: (fields[12] as Map?)?.cast(),
id: fields[13] == null ? '' : fields[13] as String,
+ customSystemType: fields[14] as SystemType?,
);
}
@override
void write(BinaryWriter writer, Spi obj) {
writer
- ..writeByte(14)
+ ..writeByte(15)
..writeByte(0)
..write(obj.name)
..writeByte(1)
@@ -145,7 +146,9 @@ class SpiAdapter extends TypeAdapter {
..writeByte(12)
..write(obj.envs)
..writeByte(13)
- ..write(obj.id);
+ ..write(obj.id)
+ ..writeByte(14)
+ ..write(obj.customSystemType);
}
@override
@@ -557,3 +560,44 @@ class WakeOnLanCfgAdapter extends TypeAdapter {
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
+
+class SystemTypeAdapter extends TypeAdapter {
+ @override
+ final typeId = 9;
+
+ @override
+ SystemType read(BinaryReader reader) {
+ switch (reader.readByte()) {
+ case 0:
+ return SystemType.linux;
+ case 1:
+ return SystemType.bsd;
+ case 2:
+ return SystemType.windows;
+ default:
+ return SystemType.linux;
+ }
+ }
+
+ @override
+ void write(BinaryWriter writer, SystemType obj) {
+ switch (obj) {
+ case SystemType.linux:
+ writer.writeByte(0);
+ case SystemType.bsd:
+ writer.writeByte(1);
+ case SystemType.windows:
+ writer.writeByte(2);
+ }
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is SystemTypeAdapter &&
+ runtimeType == other.runtimeType &&
+ typeId == other.typeId;
+}
diff --git a/lib/hive/hive_adapters.g.yaml b/lib/hive/hive_adapters.g.yaml
index 1a678928..e20260dd 100644
--- a/lib/hive/hive_adapters.g.yaml
+++ b/lib/hive/hive_adapters.g.yaml
@@ -1,7 +1,7 @@
# Generated by Hive CE
# Manual modifications may be necessary for certain migrations
# Check in to version control
-nextTypeId: 9
+nextTypeId: 10
types:
PrivateKeyInfo:
typeId: 1
@@ -27,7 +27,7 @@ types:
index: 4
Spi:
typeId: 3
- nextIndex: 14
+ nextIndex: 15
fields:
name:
index: 0
@@ -57,6 +57,8 @@ types:
index: 12
id:
index: 13
+ customSystemType:
+ index: 14
VirtKey:
typeId: 4
nextIndex: 45
@@ -207,3 +209,13 @@ types:
index: 1
pwd:
index: 2
+ SystemType:
+ typeId: 9
+ nextIndex: 3
+ fields:
+ linux:
+ index: 0
+ bsd:
+ index: 1
+ windows:
+ index: 2
diff --git a/lib/hive/hive_registrar.g.dart b/lib/hive/hive_registrar.g.dart
index 6b4ba3ef..5b30f95e 100644
--- a/lib/hive/hive_registrar.g.dart
+++ b/lib/hive/hive_registrar.g.dart
@@ -13,6 +13,7 @@ extension HiveRegistrar on HiveInterface {
registerAdapter(ServerFuncBtnAdapter());
registerAdapter(SnippetAdapter());
registerAdapter(SpiAdapter());
+ registerAdapter(SystemTypeAdapter());
registerAdapter(VirtKeyAdapter());
registerAdapter(WakeOnLanCfgAdapter());
}
@@ -26,6 +27,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
registerAdapter(ServerFuncBtnAdapter());
registerAdapter(SnippetAdapter());
registerAdapter(SpiAdapter());
+ registerAdapter(SystemTypeAdapter());
registerAdapter(VirtKeyAdapter());
registerAdapter(WakeOnLanCfgAdapter());
}
diff --git a/lib/intro.dart b/lib/intro.dart
index 89adf5d1..5e8b5c85 100644
--- a/lib/intro.dart
+++ b/lib/intro.dart
@@ -5,9 +5,7 @@ final class _IntroPage extends StatelessWidget {
const _IntroPage(this.pages);
- static const _builders = {
- 1: _buildAppSettings,
- };
+ static const _builders = {1: _buildAppSettings};
@override
Widget build(BuildContext context) {
@@ -20,9 +18,7 @@ final class _IntroPage extends StatelessWidget {
pages: pages_,
onDone: (ctx) {
Stores.setting.introVer.put(BuildData.build);
- Navigator.of(ctx).pushReplacement(
- MaterialPageRoute(builder: (_) => const HomePage()),
- );
+ Navigator.of(ctx).pushReplacement(MaterialPageRoute(builder: (_) => const HomePage()));
},
),
);
@@ -52,17 +48,12 @@ final class _IntroPage extends StatelessWidget {
RNodes.app.notify();
}
},
- trailing: Text(
- ctx.localeNativeName,
- style: const TextStyle(fontSize: 15, color: Colors.grey),
- ),
+ trailing: Text(ctx.localeNativeName, style: const TextStyle(fontSize: 15, color: Colors.grey)),
).cardx,
ListTile(
leading: const Icon(Icons.update),
title: Text(libL10n.checkUpdate),
- subtitle: isAndroid
- ? Text(l10n.fdroidReleaseTip, style: UIs.textGrey)
- : null,
+ subtitle: isAndroid ? Text(l10n.fdroidReleaseTip, style: UIs.textGrey) : null,
trailing: StoreSwitch(prop: _setting.autoCheckAppUpdate),
).cardx,
ListTile(
@@ -87,10 +78,7 @@ final class _IntroPage extends StatelessWidget {
static List get builders {
final storedVer = _setting.introVer.fetch();
- return _builders.entries
- .where((e) => e.key > storedVer)
- .map((e) => e.value)
- .toList();
+ return _builders.entries.where((e) => e.key > storedVer).map((e) => e.value).toList();
}
static final _setting = Stores.setting;
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index e699ab9b..d5a1be34 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -2,17 +2,17 @@
"@@locale": "zh",
"aboutThanks": "感谢以下参与的各位。",
"acceptBeta": "接受测试版更新推送",
- "addSystemPrivateKeyTip": "当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?",
+ "addSystemPrivateKeyTip": "检测到暂无私钥,是否添加系统默认的私钥(~/.ssh/id_rsa)?",
"added2List": "已添加至任务列表",
"addr": "地址",
- "alreadyLastDir": "已经是最上层目录了",
- "authFailTip": "认证失败,请检查密码/密钥/主机/用户等是否错误",
- "autoBackupConflict": "只能同时开启一个自动备份",
+ "alreadyLastDir": "已是顶级目录",
+ "authFailTip": "认证失败,请检查连接信息是否正确",
+ "autoBackupConflict": "仅可启用一个自动备份任务",
"autoConnect": "自动连接",
"autoRun": "自动运行",
"autoUpdateHomeWidget": "自动更新桌面小部件",
- "backupTip": "导出的数据可以使用密码加密,请妥善保管。",
- "backupVersionNotMatch": "备份版本不匹配,无法恢复",
+ "backupTip": "导出数据可通过密码加密,请妥善保管。",
+ "backupVersionNotMatch": "备份版本不兼容,无法恢复",
"backupPassword": "备份密码",
"backupPasswordTip": "设置密码以加密备份文件。留空则禁用加密。",
"backupPasswordWrong": "备份密码错误",
@@ -22,7 +22,7 @@
"backupPasswordRemoved": "备份密码已移除",
"battery": "电池",
"bgRun": "后台运行",
- "bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请修改省电策略为“无限制”。",
+ "bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请将省电策略改为“无限制”。",
"closeAfterSave": "保存后关闭",
"cmd": "命令",
"collapseUITip": "是否默认折叠 UI 中的长列表",
@@ -40,23 +40,23 @@
"decompress": "解压缩",
"deleteServers": "批量删除服务器",
"desktopTerminalTip": "启动 SSH 连接所用的终端模拟器命令",
- "dirEmpty": "请确保文件夹为空",
- "disconnected": "连接断开",
+ "dirEmpty": "请确保目录为空",
+ "disconnected": "已断开连接",
"disk": "磁盘",
"diskHealth": "磁盘健康",
"diskIgnorePath": "忽略的磁盘路径",
"displayCpuIndex": "显示 CPU 索引",
"dl2Local": "下载 {fileName} 到本地?",
"dockerEmptyRunningItems": "没有正在运行的容器。\n这可能是因为:\n- Docker 安装用户与 App 内配置的用户名不同\n- 环境变量 DOCKER_HOST 没有被正确读取。可以通过在终端内运行 `echo $DOCKER_HOST` 来获取。",
- "dockerImagesFmt": "共 {count} 个镜像",
- "dockerNotInstalled": "Docker 未安装",
+ "dockerImagesFmt": "{count} 个镜像",
+ "dockerNotInstalled": "未安装 Docker",
"dockerStatusRunningAndStoppedFmt": "{runningCount} 个正在运行, {stoppedCount} 个已停止",
"dockerStatusRunningFmt": "{count} 个容器正在运行",
"doubleColumnMode": "双列模式",
- "doubleColumnTip": "此选项仅开启功能,实际是否能开启还取决于设备的宽度",
+ "doubleColumnTip": "此选项仅用于启用该功能,是否生效取决于设备宽度",
"editVirtKeys": "编辑虚拟按键",
"editor": "编辑器",
- "editorHighlightTip": "目前的代码高亮性能较为糟糕,可以选择关闭以改善。",
+ "editorHighlightTip": "代码高亮功能可能影响性能,可选择关闭。",
"emulator": "模拟器",
"encode": "编码",
"envVars": "环境变量",
@@ -73,7 +73,7 @@
"force": "强制",
"fullScreen": "全屏模式",
"fullScreenJitter": "全屏模式抖动",
- "fullScreenJitterHelp": "防止烧屏",
+ "fullScreenJitterHelp": "用于防止屏幕烧屏",
"fullScreenTip": "当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器 Tab 页。",
"goBackQ": "返回?",
"goto": "前往",
@@ -81,17 +81,17 @@
"highlight": "代码高亮",
"homeWidgetUrlConfig": "桌面部件链接配置",
"host": "主机",
- "httpFailedWithCode": "请求失败, 状态码: {code}",
+ "httpFailedWithCode": "请求失败,状态码: {code}",
"ignoreCert": "忽略证书",
"image": "镜像",
"imagesList": "镜像列表",
"init": "初始化",
"inner": "内置",
"install": "安装",
- "installDockerWithUrl": "请先 https://docs.docker.com/engine/install docker",
+ "installDockerWithUrl": "请先前往 https://docs.docker.com/engine/install 安装 Docker",
"invalid": "无效",
"jumpServer": "跳板服务器",
- "keepForeground": "请保持应用处于前台!",
+ "keepForeground": "请将应用保持在前台运行",
"keepStatusWhenErr": "保留上次的服务器状态",
"keepStatusWhenErrTip": "仅限于执行脚本出错",
"keyAuth": "密钥认证",
@@ -104,7 +104,7 @@
"manual": "手动",
"max": "最大",
"maxRetryCount": "服务器尝试重连次数",
- "maxRetryCountEqual0": "会无限重试",
+ "maxRetryCountEqual0": "将无限次重试",
"min": "最小",
"mission": "任务",
"more": "更多",
@@ -125,7 +125,7 @@
"onlyOneLine": "仅显示为一行(可滚动)",
"onlyWhenCoreBiggerThan8": "仅当核心数大于 8 时生效",
"openLastPath": "打开上次的路径",
- "openLastPathTip": "不同的服务器会有不同的记录,且记录的是退出时的路径",
+ "openLastPathTip": "将为每台服务器记录其最后访问路径",
"parseContainerStatsTip": "Docker 解析占用状态较为缓慢",
"percentOfSize": "{size} 的 {percent}%",
"permission": "权限",
@@ -142,7 +142,7 @@
"prune": "修剪",
"pushToken": "消息推送 Token",
"pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项",
- "pveLoginFailed": "登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。",
+ "pveLoginFailed": "登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。",
"pveVersionLow": "当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用",
"read": "读",
"reboot": "重启",
@@ -169,7 +169,7 @@
"sftpDlPrepare": "准备连接至服务器...",
"sftpEditorTip": "如果为空, 使用App内置的文件编辑器. 如果有值, 这是用远程服务器的编辑器, 例如 `vim` (建议根据 `EDITOR` 自动获取).",
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 来删除文件夹",
- "sftpSSHConnected": "SFTP 已连接...",
+ "sftpSSHConnected": "SFTP 已连接",
"sftpShowFoldersFirst": "文件夹显示在前",
"showDistLogo": "显示发行版 Logo",
"shutdown": "关机",
@@ -179,7 +179,7 @@
"specifyDev": "指定设备",
"specifyDevTip": "例如网络流量统计默认是所有设备,你可以在这里指定特定的设备",
"speed": "速度",
- "spentTime": "耗时: {time}",
+ "spentTime": "耗时:{time}",
"sshTermHelp": "在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容,在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。",
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
"sshVirtualKeyAutoOff": "虚拟按键自动切换",
@@ -213,7 +213,7 @@
"unknown": "未知",
"unkownConvertMode": "未知转换模式",
"update": "更新",
- "updateIntervalEqual0": "你设置为 0,服务器状态不会自动刷新。\n且不能计算 CPU 使用情况。",
+ "updateIntervalEqual0": "设置为 0 将不自动刷新服务器状态。\n且无法计算 CPU 使用率。",
"updateServerStatusInterval": "服务器状态刷新间隔",
"upload": "上传",
"upsideDown": "上下交换",
@@ -233,7 +233,7 @@
"watchNotPaired": "没有已配对的 Apple Watch",
"webdavSettingEmpty": "WebDav 设置项为空",
"whenOpenApp": "当打开 App 时",
- "wolTip": "在配置 WOL 后,每次连接服务器都会先发送一次 WOL 请求",
+ "wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
"write": "写",
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb
index fa1e2290..86ec6d9d 100644
--- a/lib/l10n/app_zh_tw.arb
+++ b/lib/l10n/app_zh_tw.arb
@@ -2,17 +2,17 @@
"@@locale": "zh_TW",
"aboutThanks": "感謝以下參與的各位。",
"acceptBeta": "接受測試版更新推送",
- "addSystemPrivateKeyTip": "目前沒有任何私鑰,是否新增系統原有的 (~/.ssh/id_rsa)?",
+ "addSystemPrivateKeyTip": "偵測到尚無私鑰,是否要加入系統預設的私鑰(~/.ssh/id_rsa)?",
"added2List": "已新增至任務清單",
"addr": "位址",
- "alreadyLastDir": "已經是最上層目錄了",
- "authFailTip": "認證失敗,請檢查密碼/金鑰/主機/使用者等是否錯誤。",
- "autoBackupConflict": "只能同時開啓一個自動備份",
+ "alreadyLastDir": "已是頂層目錄",
+ "authFailTip": "認證失敗,請檢查連線資訊是否正確",
+ "autoBackupConflict": "僅能啟用一項自動備份任務",
"autoConnect": "自動連線",
"autoRun": "自動執行",
"autoUpdateHomeWidget": "自動更新桌面小工具",
- "backupTip": "匯出的資料可以使用密碼加密。 \n請妥善保管。",
- "backupVersionNotMatch": "備份版本不相符,無法還原",
+ "backupTip": "匯出的資料可透過密碼加密,請妥善保管。",
+ "backupVersionNotMatch": "備份版本不相容,無法還原",
"backupPassword": "備份密碼",
"backupPasswordTip": "設定密碼來加密備份檔案。留空則停用加密。",
"backupPasswordWrong": "備份密碼錯誤",
@@ -22,7 +22,7 @@
"backupPasswordRemoved": "備份密碼已移除",
"battery": "電池",
"bgRun": "背景執行",
- "bgRunTip": "此開關只代表程式會嘗試在背景執行,具體能否在後臺執行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池最佳化”,MIUI / HyperOS 請修改省電策略為“無限制”。",
+ "bgRunTip": "此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。",
"closeAfterSave": "儲存後關閉",
"cmd": "指令",
"collapseUITip": "是否預設折疊 UI 中存在的長列表",
@@ -40,23 +40,23 @@
"decompress": "解壓縮",
"deleteServers": "大量刪除伺服器",
"desktopTerminalTip": "啟動 SSH 連線時用於打開終端機模擬器的指令。",
- "dirEmpty": "請確保資料夾為空",
- "disconnected": "連線中斷",
+ "dirEmpty": "請確保目錄為空",
+ "disconnected": "已中斷連線",
"disk": "磁碟",
"diskHealth": "磁碟健康",
"diskIgnorePath": "忽略的磁碟路徑",
"displayCpuIndex": "顯示 CPU 索引",
"dl2Local": "下載 {fileName} 到本地?",
"dockerEmptyRunningItems": "沒有正在執行的容器。\n這可能是因為:\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變數 DOCKER_HOST 沒有被正確讀取。你可以通過在終端機內執行 `echo $DOCKER_HOST` 來獲取。",
- "dockerImagesFmt": "共 {count} 個映像檔",
- "dockerNotInstalled": "Docker 未安裝",
+ "dockerImagesFmt": "{count} 個映像檔",
+ "dockerNotInstalled": "未安裝 Docker",
"dockerStatusRunningAndStoppedFmt": "{runningCount} 個正在執行, {stoppedCount} 個已停止",
"dockerStatusRunningFmt": "{count} 個容器正在執行",
"doubleColumnMode": "雙列模式",
- "doubleColumnTip": "此選項僅開啟功能,實際是否能開啟還取決於設備的頻寬",
+ "doubleColumnTip": "此選項僅用於啟用此功能,是否生效取決於裝置寬度",
"editVirtKeys": "編輯虛擬按鍵",
"editor": "編輯器",
- "editorHighlightTip": "目前的程式碼標記效能較為糟糕,可以選擇關閉以改善。",
+ "editorHighlightTip": "程式碼高亮功能可能影響效能,可選擇性關閉。",
"emulator": "模擬器",
"encode": "編碼",
"envVars": "環境變數",
@@ -73,7 +73,7 @@
"force": "強制",
"fullScreen": "全螢幕模式",
"fullScreenJitter": "全螢幕模式抖動",
- "fullScreenJitterHelp": "防止烙印",
+ "fullScreenJitterHelp": "防止螢幕烙印",
"fullScreenTip": "當設備旋轉為橫向時,是否開啟全螢幕模式?此選項僅適用於伺服器分頁。",
"goBackQ": "返回?",
"goto": "前往",
@@ -81,17 +81,17 @@
"highlight": "程式碼標記",
"homeWidgetUrlConfig": "桌面小工具連結配置",
"host": "主機",
- "httpFailedWithCode": "請求失敗, 狀態碼: {code}",
+ "httpFailedWithCode": "請求失敗,狀態碼:{code}",
"ignoreCert": "忽略憑證",
"image": "映像檔",
"imagesList": "映像檔列表",
"init": "初始化",
"inner": "內建",
"install": "安裝",
- "installDockerWithUrl": "請先 https://docs.docker.com/engine/install docker",
+ "installDockerWithUrl": "請先前往 https://docs.docker.com/engine/install 安裝 Docker",
"invalid": "無效",
"jumpServer": "跳板伺服器",
- "keepForeground": "請保持App處於前端!",
+ "keepForeground": "請讓 App 保持在前景執行",
"keepStatusWhenErr": "保留上次的伺服器狀態",
"keepStatusWhenErrTip": "僅在執行腳本出錯時",
"keyAuth": "金鑰認證",
@@ -104,7 +104,7 @@
"manual": "手動",
"max": "最大",
"maxRetryCount": "伺服器嘗試重連次數",
- "maxRetryCountEqual0": "會無限重試",
+ "maxRetryCountEqual0": "將無限次重試",
"min": "最小",
"mission": "任務",
"more": "更多",
@@ -125,7 +125,7 @@
"onlyOneLine": "僅顯示為一行(可捲動)",
"onlyWhenCoreBiggerThan8": "僅當核心數大於 8 時生效",
"openLastPath": "打開上次的路徑",
- "openLastPathTip": "不同的伺服器會有不同的記錄,且記錄的是退出時的路徑",
+ "openLastPathTip": "將為每台伺服器紀錄其最後存取路徑",
"parseContainerStatsTip": "Docker 解析消耗狀態較為緩慢",
"percentOfSize": "{size} 的 {percent}%",
"permission": "權限",
@@ -142,7 +142,7 @@
"prune": "修剪",
"pushToken": "消息推送 Token",
"pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。",
- "pveLoginFailed": "登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。",
+ "pveLoginFailed": "登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。",
"pveVersionLow": "此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。",
"read": "讀取",
"reboot": "重開",
@@ -169,7 +169,7 @@
"sftpDlPrepare": "準備連線至伺服器...",
"sftpEditorTip": "如果為空, 使用App內建的檔案編輯器。如果有值, 則使用遠端伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。",
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除檔案夾",
- "sftpSSHConnected": "SFTP 已連線...",
+ "sftpSSHConnected": "SFTP 已連線",
"sftpShowFoldersFirst": "資料夾顯示在前",
"showDistLogo": "顯示發行版 Logo",
"shutdown": "關機",
@@ -179,7 +179,7 @@
"specifyDev": "指定裝置",
"specifyDevTip": "例如網路流量統計預設是所有裝置,你可以在這裡指定特定的裝置。",
"speed": "速度",
- "spentTime": "耗時: {time}",
+ "spentTime": "耗時:{time}",
"sshTermHelp": "在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容,在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。",
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 回饋問題,或者加入我們開發。",
"sshVirtualKeyAutoOff": "虛擬按鍵自動切換",
@@ -213,7 +213,7 @@
"unknown": "未知",
"unkownConvertMode": "未知轉換模式",
"update": "更新",
- "updateIntervalEqual0": "你設定為 0,伺服器狀態不會自動更新。\n且不能計算CPU使用情況。",
+ "updateIntervalEqual0": "設定為 0 將不自動刷新伺服器狀態,\n也無法計算 CPU 使用率。",
"updateServerStatusInterval": "伺服器狀態更新間隔",
"upload": "上傳",
"upsideDown": "上下交換",
@@ -233,7 +233,7 @@
"watchNotPaired": "沒有已配對的 Apple Watch",
"webdavSettingEmpty": "WebDav 設定項爲空",
"whenOpenApp": "當打開 App 時",
- "wolTip": "在配置 WOL(網絡喚醒)後,每次連線伺服器都會先發送一次 WOL 請求。",
+ "wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
"write": "寫入",
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
diff --git a/lib/view/page/backup.dart b/lib/view/page/backup.dart
index 9b2bc164..16de25b4 100644
--- a/lib/view/page/backup.dart
+++ b/lib/view/page/backup.dart
@@ -76,9 +76,9 @@ final class _BackupPageState extends State with AutomaticKeepAliveCl
initiallyExpanded: false,
children: [
ListTile(
- title: Text(libL10n.backup),
- trailing: const Icon(Icons.save),
- onTap: () => BackupService.backup(context, FileBackupSource())
+ title: Text(libL10n.backup),
+ trailing: const Icon(Icons.save),
+ onTap: () => BackupService.backup(context, FileBackupSource()),
),
ListTile(
trailing: const Icon(Icons.restore),
@@ -264,7 +264,6 @@ final class _BackupPageState extends State with AutomaticKeepAliveCl
).cardx;
}
-
Future _onTapWebdavDl(BuildContext context) async {
webdavLoading.value = true;
try {
@@ -357,7 +356,6 @@ final class _BackupPageState extends State with AutomaticKeepAliveCl
}
}
-
void _onBulkImportServers(BuildContext context) async {
final data = await context.showImportDialog(title: l10n.server, modelDef: Spix.example.toJson());
if (data == null) return;
@@ -394,11 +392,6 @@ final class _BackupPageState extends State with AutomaticKeepAliveCl
}
}
-
-
-
-
-
@override
bool get wantKeepAlive => true;
}
diff --git a/lib/view/page/container/types.dart b/lib/view/page/container/types.dart
index ca8ebd64..03bb2154 100644
--- a/lib/view/page/container/types.dart
+++ b/lib/view/page/container/types.dart
@@ -15,4 +15,4 @@ enum _PruneTypes {
_ => null,
};
}
-}
\ No newline at end of file
+}
diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart
index 168a04fc..0844058d 100644
--- a/lib/view/page/home.dart
+++ b/lib/view/page/home.dart
@@ -17,10 +17,7 @@ class HomePage extends StatefulWidget {
@override
State createState() => _HomePageState();
- static const route = AppRouteNoArg(
- page: HomePage.new,
- path: '/',
- );
+ static const route = AppRouteNoArg(page: HomePage.new, path: '/');
}
class _HomePageState extends State
@@ -181,11 +178,7 @@ class _HomePageState extends State
//_reqNotiPerm();
if (Stores.setting.autoCheckAppUpdate.fetch()) {
- AppUpdateIface.doUpdate(
- build: BuildData.build,
- url: Urls.updateCfg,
- context: context,
- );
+ AppUpdateIface.doUpdate(build: BuildData.build, url: Urls.updateCfg, context: context);
}
MethodChans.updateHomeWidget();
await ServerProvider.refresh();
@@ -216,10 +209,7 @@ class _HomePageState extends State
void _goAuth() {
if (Stores.setting.useBioAuth.fetch()) {
if (LocalAuthPage.route.alreadyIn) return;
- LocalAuthPage.route.go(
- context,
- args: LocalAuthPageArgs(onAuthSuccess: () => _shouldAuth = false),
- );
+ LocalAuthPage.route.go(context, args: LocalAuthPageArgs(onAuthSuccess: () => _shouldAuth = false));
}
}
@@ -245,9 +235,7 @@ final class _AppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
- return SizedBox(
- height: preferredSize.height,
- );
+ return SizedBox(height: preferredSize.height);
}
@override
diff --git a/lib/view/page/iperf.dart b/lib/view/page/iperf.dart
index 76116153..f7e13f48 100644
--- a/lib/view/page/iperf.dart
+++ b/lib/view/page/iperf.dart
@@ -4,7 +4,6 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/view/page/ssh/page/page.dart';
-
class IPerfPage extends StatefulWidget {
final SpiRequiredArgs args;
@@ -13,10 +12,7 @@ class IPerfPage extends StatefulWidget {
@override
State createState() => _IPerfPageState();
- static const route = AppRouteArg(
- page: IPerfPage.new,
- path: '/iperf',
- );
+ static const route = AppRouteArg(page: IPerfPage.new, path: '/iperf');
}
class _IPerfPageState extends State {
@@ -33,9 +29,7 @@ class _IPerfPageState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: CustomAppBar(
- title: const Text('iperf'),
- ),
+ appBar: CustomAppBar(title: const Text('iperf')),
body: _buildBody(),
floatingActionButton: _buildFAB(),
);
@@ -63,12 +57,7 @@ class _IPerfPageState extends State {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 17),
children: [
- Input(
- controller: _hostCtrl,
- label: l10n.host,
- icon: Icons.computer,
- suggestion: false,
- ),
+ Input(controller: _hostCtrl, label: l10n.host, icon: Icons.computer, suggestion: false),
Input(
controller: _portCtrl,
label: l10n.port,
diff --git a/lib/view/page/private_key/edit.dart b/lib/view/page/private_key/edit.dart
index 7703dff8..4ce260ad 100644
--- a/lib/view/page/private_key/edit.dart
+++ b/lib/view/page/private_key/edit.dart
@@ -24,10 +24,7 @@ class PrivateKeyEditPage extends StatefulWidget {
@override
State createState() => _PrivateKeyEditPageState();
- static const route = AppRoute(
- page: PrivateKeyEditPage.new,
- path: '/private_key/edit',
- );
+ static const route = AppRoute(page: PrivateKeyEditPage.new, path: '/private_key/edit');
}
class _PrivateKeyEditPageState extends State {
@@ -82,11 +79,7 @@ class _PrivateKeyEditPageState extends State {
@override
Widget build(BuildContext context) {
- return Scaffold(
- appBar: _buildAppBar(),
- body: _buildBody(),
- floatingActionButton: _buildFAB(),
- );
+ return Scaffold(appBar: _buildAppBar(), body: _buildBody(), floatingActionButton: _buildFAB());
}
CustomAppBar _buildAppBar() {
@@ -98,9 +91,7 @@ class _PrivateKeyEditPageState extends State {
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
- child: Text(libL10n.askContinue(
- '${libL10n.delete} ${l10n.privateKey}(${pki.id})',
- )),
+ child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.privateKey}(${pki.id})')),
actions: Btn.ok(
onTap: () {
PrivateKeyProvider.delete(pki);
@@ -112,13 +103,10 @@ class _PrivateKeyEditPageState extends State {
);
},
icon: const Icon(Icons.delete),
- )
+ ),
]
: null;
- return CustomAppBar(
- title: Text(libL10n.edit),
- actions: actions,
- );
+ return CustomAppBar(title: Text(libL10n.edit), actions: actions);
}
String _standardizeLineSeparators(String value) {
@@ -126,11 +114,7 @@ class _PrivateKeyEditPageState extends State {
}
Widget _buildFAB() {
- return FloatingActionButton(
- tooltip: l10n.save,
- onPressed: _onTapSave,
- child: const Icon(Icons.save),
- );
+ return FloatingActionButton(tooltip: l10n.save, onPressed: _onTapSave, child: const Icon(Icons.save));
}
Widget _buildBody() {
@@ -170,11 +154,7 @@ class _PrivateKeyEditPageState extends State {
final size = (await file.stat()).size;
if (size > Miscs.privateKeyMaxSize) {
context.showSnackBar(
- l10n.fileTooLarge(
- path,
- size.bytes2Str,
- Miscs.privateKeyMaxSize.bytes2Str,
- ),
+ l10n.fileTooLarge(path, size.bytes2Str, Miscs.privateKeyMaxSize.bytes2Str),
);
return;
}
@@ -196,10 +176,7 @@ class _PrivateKeyEditPageState extends State {
onSubmitted: (_) => _onTapSave(),
),
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
- ValBuilder(
- listenable: _loading,
- builder: (val) => val ?? UIs.placeholder,
- ),
+ ValBuilder(listenable: _loading, builder: (val) => val ?? UIs.placeholder),
],
);
}
diff --git a/lib/view/page/private_key/list.dart b/lib/view/page/private_key/list.dart
index 499dea5c..3351c0d7 100644
--- a/lib/view/page/private_key/list.dart
+++ b/lib/view/page/private_key/list.dart
@@ -15,10 +15,7 @@ class PrivateKeysListPage extends StatefulWidget {
@override
State createState() => _PrivateKeyListState();
- static const route = AppRouteNoArg(
- page: PrivateKeysListPage.new,
- path: '/private_key',
- );
+ static const route = AppRouteNoArg(page: PrivateKeysListPage.new, path: '/private_key');
}
class _PrivateKeyListState extends State with AfterLayoutMixin {
@@ -34,26 +31,21 @@ class _PrivateKeyListState extends State with AfterLayoutMi
}
Widget _buildBody() {
- return PrivateKeyProvider.pkis.listenVal(
- (pkis) {
- if (pkis.isEmpty) {
- return Center(child: Text(libL10n.empty));
- }
+ return PrivateKeyProvider.pkis.listenVal((pkis) {
+ if (pkis.isEmpty) {
+ return Center(child: Text(libL10n.empty));
+ }
- final children = pkis.map(_buildKeyItem).toList();
- return AutoMultiList(children: children);
- },
- );
+ final children = pkis.map(_buildKeyItem).toList();
+ return AutoMultiList(children: children);
+ });
}
Widget _buildKeyItem(PrivateKeyInfo item) {
return ListTile(
title: Text(item.id),
subtitle: Text(item.type ?? l10n.unknown, style: UIs.textGrey),
- onTap: () => PrivateKeyEditPage.route.go(
- context,
- args: PrivateKeyEditPageArgs(pki: item),
- ),
+ onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: item)),
trailing: const Icon(Icons.edit),
).cardx;
}
@@ -72,20 +64,16 @@ extension on _PrivateKeyListState {
if (home == null) return;
final idRsaFile = File(home.joinPath('.ssh/id_rsa'));
if (!idRsaFile.existsSync()) return;
- final sysPk = PrivateKeyInfo(
- id: 'system',
- key: await idRsaFile.readAsString(),
- );
+ final sysPk = PrivateKeyInfo(id: 'system', key: await idRsaFile.readAsString());
context.showRoundDialog(
title: libL10n.attention,
child: Text(l10n.addSystemPrivateKeyTip),
- actions: Btn.ok(onTap: () {
- context.pop();
- PrivateKeyEditPage.route.go(
- context,
- args: PrivateKeyEditPageArgs(pki: sysPk),
- );
- }).toList,
+ actions: Btn.ok(
+ onTap: () {
+ context.pop();
+ PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: sysPk));
+ },
+ ).toList,
);
}
}
diff --git a/lib/view/page/process.dart b/lib/view/page/process.dart
index ea4f299d..d761807b 100644
--- a/lib/view/page/process.dart
+++ b/lib/view/page/process.dart
@@ -12,16 +12,13 @@ import 'package:server_box/data/res/store.dart';
class ProcessPage extends StatefulWidget {
final SpiRequiredArgs args;
-
+
const ProcessPage({super.key, required this.args});
@override
State createState() => _ProcessPageState();
- static const route = AppRouteArg(
- page: ProcessPage.new,
- path: '/process',
- );
+ static const route = AppRouteArg(page: ProcessPage.new, path: '/process');
}
class _ProcessPageState extends State {
@@ -49,8 +46,7 @@ class _ProcessPageState extends State {
void initState() {
super.initState();
_client = widget.args.spi.server?.value.client;
- final duration =
- Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
+ final duration = Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
_timer = Timer.periodic(duration, (_) => _refresh());
}
@@ -62,8 +58,10 @@ class _ProcessPageState extends State {
Future _refresh() async {
if (mounted) {
- final result =
- await _client?.run(ShellFunc.process.exec(widget.args.spi.id)).string;
+ final systemType = widget.args.spi.server?.value.status.system;
+ final result = await _client
+ ?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType))
+ .string;
if (result == null || result.isEmpty) {
context.showSnackBar(libL10n.empty);
return;
@@ -72,8 +70,7 @@ class _ProcessPageState extends State {
// If there are any [Proc]'s data is not complete,
// the option to sort by cpu/mem will not be available.
- final isAnyProcDataNotComplete =
- _result.procs.any((e) => e.cpu == null || e.mem == null);
+ final isAnyProcDataNotComplete = _result.procs.any((e) => e.cpu == null || e.mem == null);
if (isAnyProcDataNotComplete) {
_sortModes.removeWhere((e) => e == ProcSortMode.cpu);
_sortModes.removeWhere((e) => e == ProcSortMode.mem);
@@ -97,25 +94,20 @@ class _ProcessPageState extends State {
},
icon: const Icon(Icons.sort),
initialValue: _procSortMode,
- itemBuilder: (_) => _sortModes
- .map((e) => PopupMenuItem(value: e, child: Text(e.name)))
- .toList(),
+ itemBuilder: (_) => _sortModes.map((e) => PopupMenuItem(value: e, child: Text(e.name))).toList(),
),
];
if (_result.error != null) {
- actions.add(IconButton(
- icon: const Icon(Icons.error),
- onPressed: () => context.showRoundDialog(
- title: libL10n.error,
- child: SingleChildScrollView(child: Text(_result.error!)),
- actions: [
- TextButton(
- onPressed: () => Pfs.copy(_result.error!),
- child: Text(libL10n.copy),
- ),
- ],
+ actions.add(
+ IconButton(
+ icon: const Icon(Icons.error),
+ onPressed: () => context.showRoundDialog(
+ title: libL10n.error,
+ child: SingleChildScrollView(child: Text(_result.error!)),
+ actions: [TextButton(onPressed: () => Pfs.copy(_result.error!), child: Text(libL10n.copy))],
+ ),
),
- ));
+ );
}
Widget child;
if (_result.procs.isEmpty) {
@@ -144,32 +136,26 @@ class _ProcessPageState extends State {
return CardX(
key: ValueKey(proc.pid),
child: ListTile(
- leading: SizedBox(
- width: _media.size.width / 6,
- child: leading,
- ),
+ leading: SizedBox(width: _media.size.width / 6, child: leading),
title: Text(proc.binary),
- subtitle: Text(
- proc.command,
- style: UIs.textGrey,
- maxLines: 3,
- overflow: TextOverflow.fade,
- ),
+ subtitle: Text(proc.command, style: UIs.textGrey, maxLines: 3, overflow: TextOverflow.fade),
trailing: _buildItemTrail(proc),
onTap: () => _lastFocusId = proc.pid,
onLongPress: () {
context.showRoundDialog(
title: libL10n.attention,
- child: Text(libL10n.askContinue(
- '${l10n.stop} ${l10n.process}(${proc.pid})',
- )),
- actions: Btn.ok(onTap: () async {
- context.pop();
- await context.showLoadingDialog(fn: () async {
- await _client?.run('kill ${proc.pid}');
- await _refresh();
- });
- }).toList,
+ child: Text(libL10n.askContinue('${l10n.stop} ${l10n.process}(${proc.pid})')),
+ actions: Btn.ok(
+ onTap: () async {
+ context.pop();
+ await context.showLoadingDialog(
+ fn: () async {
+ await _client?.run('kill ${proc.pid}');
+ await _refresh();
+ },
+ );
+ },
+ ).toList,
);
},
selected: _lastFocusId == proc.pid,
@@ -185,17 +171,9 @@ class _ProcessPageState extends State {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
- if (proc.cpu != null)
- TwoLineText(
- up: proc.cpu!.toStringAsFixed(1),
- down: 'cpu',
- ),
+ if (proc.cpu != null) TwoLineText(up: proc.cpu!.toStringAsFixed(1), down: 'cpu'),
UIs.width13,
- if (proc.mem != null)
- TwoLineText(
- up: proc.mem!.toStringAsFixed(1),
- down: 'mem',
- ),
+ if (proc.mem != null) TwoLineText(up: proc.mem!.toStringAsFixed(1), down: 'mem'),
],
);
}
diff --git a/lib/view/page/pve.dart b/lib/view/page/pve.dart
index cb301965..0cfcaa41 100644
--- a/lib/view/page/pve.dart
+++ b/lib/view/page/pve.dart
@@ -18,18 +18,12 @@ final class PvePageArgs {
final class PvePage extends StatefulWidget {
final PvePageArgs args;
- const PvePage({
- super.key,
- required this.args,
- });
+ const PvePage({super.key, required this.args});
@override
State createState() => _PvePageState();
- static const route = AppRouteArg(
- page: PvePage.new,
- path: '/pve',
- );
+ static const route = AppRouteArg(page: PvePage.new, path: '/pve');
}
const _kHorziPadding = 11.0;
@@ -87,9 +81,7 @@ final class _PvePageState extends State {
_timer?.cancel();
return Padding(
padding: const EdgeInsets.all(13),
- child: Center(
- child: Text(val),
- ),
+ child: Center(child: Text(val)),
);
}
return ValBuilder(
@@ -110,10 +102,7 @@ final class _PvePageState extends State {
PveResType? lastType;
return ListView.builder(
- padding: const EdgeInsets.symmetric(
- horizontal: _kHorziPadding,
- vertical: 7,
- ),
+ padding: const EdgeInsets.symmetric(horizontal: _kHorziPadding, vertical: 7),
itemCount: data.length * 2,
itemBuilder: (context, index) {
final item = data[index ~/ 2];
@@ -135,10 +124,7 @@ final class _PvePageState extends State {
alignment: Alignment.center,
child: Text(
type.toStr,
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- color: Colors.grey,
- ),
+ style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
textAlign: TextAlign.start,
),
),
@@ -183,18 +169,11 @@ final class _PvePageState extends State {
UIs.width7,
const Text('CPU', style: UIs.text12Grey),
const Spacer(),
- Text(
- '${(item.cpu * 100).toStringAsFixed(1)} %',
- style: UIs.text12Grey,
- ),
+ Text('${(item.cpu * 100).toStringAsFixed(1)} %', style: UIs.text12Grey),
],
),
const SizedBox(height: 3),
- LinearProgressIndicator(
- value: item.cpu / item.maxcpu,
- minHeight: 7,
- valueColor: valueAnim,
- ),
+ LinearProgressIndicator(value: item.cpu / item.maxcpu, minHeight: 7, valueColor: valueAnim),
UIs.height7,
Row(
children: [
@@ -202,18 +181,11 @@ final class _PvePageState extends State {
UIs.width7,
const Text('RAM', style: UIs.text12Grey),
const Spacer(),
- Text(
- '${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}',
- style: UIs.text12Grey,
- ),
+ Text('${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}', style: UIs.text12Grey),
],
),
const SizedBox(height: 3),
- LinearProgressIndicator(
- value: item.mem / item.maxmem,
- minHeight: 7,
- valueColor: valueAnim,
- ),
+ LinearProgressIndicator(value: item.mem / item.maxmem, minHeight: 7, valueColor: valueAnim),
],
),
).cardx;
@@ -232,14 +204,8 @@ final class _PvePageState extends State {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 15),
- Text(
- _wrapNodeName(item),
- style: UIs.text13Bold,
- ),
- Text(
- ' / ${item.summary}',
- style: UIs.text12Grey,
- ),
+ Text(_wrapNodeName(item), style: UIs.text13Bold),
+ Text(' / ${item.summary}', style: UIs.text12Grey),
const Spacer(),
_buildCtrlBtns(item),
UIs.width13,
@@ -266,34 +232,23 @@ final class _PvePageState extends State {
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
style: UIs.text11Grey,
textAlign: TextAlign.center,
- )
+ ),
],
),
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Text(
- '↓:\n${item.netin.bytes2Str}',
- style: UIs.text11Grey,
- textAlign: TextAlign.center,
- ),
+ Text('↓:\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
const SizedBox(height: 3),
- Text(
- '↑:\n${item.netout.bytes2Str}',
- style: UIs.text11Grey,
- textAlign: TextAlign.center,
- )
+ Text('↑:\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
],
),
],
),
- const SizedBox(height: 21)
+ const SizedBox(height: 21),
];
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: children,
- ).cardx;
+ return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;
}
Widget _buildLxc(PveLxc item) {
@@ -309,14 +264,8 @@ final class _PvePageState extends State {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 15),
- Text(
- _wrapNodeName(item),
- style: UIs.text13Bold,
- ),
- Text(
- ' / ${item.summary}',
- style: UIs.text12Grey,
- ),
+ Text(_wrapNodeName(item), style: UIs.text13Bold),
+ Text(' / ${item.summary}', style: UIs.text12Grey),
const Spacer(),
_buildCtrlBtns(item),
UIs.width13,
@@ -343,34 +292,23 @@ final class _PvePageState extends State {
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
style: UIs.text11Grey,
textAlign: TextAlign.center,
- )
+ ),
],
),
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Text(
- '↓:\n${item.netin.bytes2Str}',
- style: UIs.text11Grey,
- textAlign: TextAlign.center,
- ),
+ Text('↓:\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
const SizedBox(height: 3),
- Text(
- '↑:\n${item.netout.bytes2Str}',
- style: UIs.text11Grey,
- textAlign: TextAlign.center,
- )
+ Text('↑:\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
],
),
],
),
- const SizedBox(height: 21)
+ const SizedBox(height: 21),
];
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: children,
- ).cardx;
+ return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;
}
Widget _buildStorage(PveStorage item) {
@@ -396,33 +334,34 @@ final class _PvePageState extends State {
}
Widget _buildSdn(PveSdn item) {
- return ListTile(
- title: Text(_wrapNodeName(item)),
- trailing: Text(item.summary),
- ).cardx;
+ return ListTile(title: Text(_wrapNodeName(item)), trailing: Text(item.summary)).cardx;
}
Widget _buildCtrlBtns(PveCtrlIface item) {
const pad = EdgeInsets.symmetric(horizontal: 7, vertical: 5);
if (!item.available) {
return Btn.icon(
- icon: const Icon(Icons.play_arrow, color: Colors.grey),
- onTap: () => _onCtrl(_pve.start, l10n.start, item));
+ icon: const Icon(Icons.play_arrow, color: Colors.grey),
+ onTap: () => _onCtrl(_pve.start, l10n.start, item),
+ );
}
return Row(
children: [
Btn.icon(
- icon: const Icon(Icons.stop, color: Colors.grey, size: 20),
- padding: pad,
- onTap: () => _onCtrl(_pve.stop, l10n.stop, item)),
+ icon: const Icon(Icons.stop, color: Colors.grey, size: 20),
+ padding: pad,
+ onTap: () => _onCtrl(_pve.stop, l10n.stop, item),
+ ),
Btn.icon(
- icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
- padding: pad,
- onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item)),
+ icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
+ padding: pad,
+ onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item),
+ ),
Btn.icon(
- icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),
- padding: pad,
- onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item)),
+ icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),
+ padding: pad,
+ onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item),
+ ),
],
);
}
@@ -437,9 +376,7 @@ extension on _PvePageState {
);
if (sure != true) return;
- final (suc, err) = await context.showLoadingDialog(
- fn: () => func(item.node, item.id),
- );
+ final (suc, err) = await context.showLoadingDialog(fn: () => func(item.node, item.id));
if (suc == true) {
context.showSnackBar(libL10n.success);
} else {
diff --git a/lib/view/page/server/detail/misc.dart b/lib/view/page/server/detail/misc.dart
index 7a4fd5fa..95d765a8 100644
--- a/lib/view/page/server/detail/misc.dart
+++ b/lib/view/page/server/detail/misc.dart
@@ -61,7 +61,7 @@ extension on _ServerDetailPageState {
titleMaxLines: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
UIs.height13,
Text('Memory: ${process.memory} ${process.memory > 1024 ? 'MB' : 'KB'}'),
diff --git a/lib/view/page/server/edit.dart b/lib/view/page/server/edit.dart
index cad73425..117674a0 100644
--- a/lib/view/page/server/edit.dart
+++ b/lib/view/page/server/edit.dart
@@ -9,6 +9,7 @@ import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
+import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/data/provider/server.dart';
@@ -59,6 +60,7 @@ class _ServerEditPageState extends State with AfterLayoutMixin {
final _env = {}.vn;
final _customCmds = {}.vn;
final _tags = {}.vn;
+ final _systemType = ValueNotifier(null);
@override
void dispose() {
@@ -91,6 +93,7 @@ class _ServerEditPageState extends State with AfterLayoutMixin {
_env.dispose();
_customCmds.dispose();
_tags.dispose();
+ _systemType.dispose();
}
@override
@@ -174,6 +177,7 @@ class _ServerEditPageState extends State