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)

-A Flutter project which provide charts to display Linux server status and tools to manage server. +A Flutter project which provides charts to display Linux, Unix and Windows server status and tools to manage servers.
Especially thanks to dartssh2 & xterm.dart.

@@ -26,7 +26,7 @@ Especially thanks to dartss -## 📥 Install +## 📥 Installation |Platform| From| |--|--| @@ -36,7 +36,7 @@ Especially thanks to 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 with AfterLayoutMixin { ), ), _buildAuth(), + _buildSystemType(), _buildJumpServer(), _buildMore(), ]; @@ -331,6 +335,26 @@ class _ServerEditPageState extends State with AfterLayoutMixin { ); } + Widget _buildSystemType() { + return _systemType.listenVal((val) { + return ListTile( + leading: Icon(MingCute.laptop_2_line), + title: Text(l10n.system), + trailing: PopupMenu( + initialValue: val, + items: [ + PopupMenuItem(value: null, child: Text(libL10n.auto)), + PopupMenuItem(value: SystemType.linux, child: Text('Linux')), + PopupMenuItem(value: SystemType.bsd, child: Text('BSD')), + PopupMenuItem(value: SystemType.windows, child: Text('Windows')), + ], + onSelected: (value) => _systemType.value = value, + child: Text(val?.name ?? libL10n.auto, style: TextStyle(color: val == null ? Colors.grey : null)), + ), + ).cardx; + }); + } + Widget _buildAltUrl() { return Input( controller: _altUrlController, @@ -614,6 +638,7 @@ extension on _ServerEditPageState { wolCfg: wol, envs: _env.value.isEmpty ? null : _env.value, id: widget.args?.spi.id ?? ShortId.generate(), + customSystemType: _systemType.value, ); if (this.spi == null) { @@ -668,5 +693,7 @@ extension on _ServerEditPageState { _netDevCtrl.text = spi.custom?.netDev ?? ''; _scriptDirCtrl.text = spi.custom?.scriptDir ?? ''; + + _systemType.value = spi.customSystemType; } } diff --git a/lib/view/page/server/tab/card_stat.dart b/lib/view/page/server/tab/card_stat.dart index 809645ea..38a2221e 100644 --- a/lib/view/page/server/tab/card_stat.dart +++ b/lib/view/page/server/tab/card_stat.dart @@ -7,21 +7,9 @@ class _CardStatus { final bool? diskIO; final NetViewType? net; - const _CardStatus({ - this.flip = false, - this.diskIO, - this.net, - }); + const _CardStatus({this.flip = false, this.diskIO, this.net}); - _CardStatus copyWith({ - bool? flip, - bool? diskIO, - NetViewType? net, - }) { - return _CardStatus( - flip: flip ?? this.flip, - diskIO: diskIO ?? this.diskIO, - net: net ?? this.net, - ); + _CardStatus copyWith({bool? flip, bool? diskIO, NetViewType? net}) { + return _CardStatus(flip: flip ?? this.flip, diskIO: diskIO ?? this.diskIO, net: net ?? this.net); } } diff --git a/lib/view/page/server/tab/tab.dart b/lib/view/page/server/tab/tab.dart index db03df13..a527ae04 100644 --- a/lib/view/page/server/tab/tab.dart +++ b/lib/view/page/server/tab/tab.dart @@ -319,8 +319,7 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi ], ), UIs.height13, - if (Stores.setting.moveServerFuncs.fetch()) - SizedBox(height: 27, child: ServerFuncBtns(spi: spi)), + if (Stores.setting.moveServerFuncs.fetch()) SizedBox(height: 27, child: ServerFuncBtns(spi: spi)), ], ); }, diff --git a/lib/view/page/server/tab/top_bar.dart b/lib/view/page/server/tab/top_bar.dart index 61180383..6bed035d 100644 --- a/lib/view/page/server/tab/top_bar.dart +++ b/lib/view/page/server/tab/top_bar.dart @@ -5,11 +5,7 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget { final void Function(String) onTagChanged; final String initTag; - const _TopBar({ - required this.initTag, - required this.onTagChanged, - required this.tags, - }); + const _TopBar({required this.initTag, required this.onTagChanged, required this.tags}); @override Widget build(BuildContext context) { @@ -31,15 +27,9 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget { padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3), child: Row( children: [ - Text( - BuildData.name, - style: TextStyle(fontSize: 19), - ), + Text(BuildData.name, style: TextStyle(fontSize: 19)), SizedBox(width: 5), - Icon( - Icons.settings, - size: 17, - ), + Icon(Icons.settings, size: 17), ], ), ), diff --git a/lib/view/page/server/tab/utils.dart b/lib/view/page/server/tab/utils.dart index c59bef4d..a5f0dc6e 100644 --- a/lib/view/page/server/tab/utils.dart +++ b/lib/view/page/server/tab/utils.dart @@ -49,7 +49,11 @@ extension _Operation on _ServerPageState { await context.showRoundDialog(title: libL10n.attention, child: Text(l10n.suspendTip)); Stores.setting.showSuspendTip.put(false); } - srv.client?.execWithPwd(ShellFunc.suspend.exec(srv.spi.id), context: context, id: srv.id); + srv.client?.execWithPwd( + ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system), + context: context, + id: srv.id, + ); }, typ: l10n.suspend, name: srv.spi.name, @@ -58,7 +62,11 @@ extension _Operation on _ServerPageState { void _onTapShutdown(Server srv) { _askFor( - func: () => srv.client?.execWithPwd(ShellFunc.shutdown.exec(srv.spi.id), context: context, id: srv.id), + func: () => srv.client?.execWithPwd( + ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system), + context: context, + id: srv.id, + ), typ: l10n.shutdown, name: srv.spi.name, ); @@ -66,7 +74,11 @@ extension _Operation on _ServerPageState { void _onTapReboot(Server srv) { _askFor( - func: () => srv.client?.execWithPwd(ShellFunc.reboot.exec(srv.spi.id), context: context, id: srv.id), + func: () => srv.client?.execWithPwd( + ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system), + context: context, + id: srv.id, + ), typ: l10n.reboot, name: srv.spi.name, ); diff --git a/lib/view/page/setting/about.dart b/lib/view/page/setting/about.dart index 6c196895..2e9d2177 100644 --- a/lib/view/page/setting/about.dart +++ b/lib/view/page/setting/about.dart @@ -7,8 +7,7 @@ final class _AppAboutPage extends StatefulWidget { State<_AppAboutPage> createState() => _AppAboutPageState(); } -final class _AppAboutPageState extends State<_AppAboutPage> - with AutomaticKeepAliveClientMixin { +final class _AppAboutPageState extends State<_AppAboutPage> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); @@ -16,15 +15,8 @@ final class _AppAboutPageState extends State<_AppAboutPage> padding: const EdgeInsets.all(13), children: [ UIs.height13, - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47), - child: UIs.appIcon, - ), - const Text( - '${BuildData.name}\nv${BuildData.build}', - textAlign: TextAlign.center, - style: UIs.text15, - ), + ConstrainedBox(constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47), child: UIs.appIcon), + const Text('${BuildData.name}\nv${BuildData.build}', textAlign: TextAlign.center, style: UIs.text15), UIs.height13, SizedBox( height: 77, @@ -52,7 +44,8 @@ final class _AppAboutPageState extends State<_AppAboutPage> ), UIs.height13, SimpleMarkdown( - data: ''' + data: + ''' #### Contributors ${GithubIds.contributors.map((e) => '[$e](${e.url})').join(' ')} diff --git a/lib/view/page/setting/seq/srv_detail_seq.dart b/lib/view/page/setting/seq/srv_detail_seq.dart index f02a3587..a77071b2 100644 --- a/lib/view/page/setting/seq/srv_detail_seq.dart +++ b/lib/view/page/setting/seq/srv_detail_seq.dart @@ -10,10 +10,7 @@ class ServerDetailOrderPage extends StatefulWidget { @override State createState() => _ServerDetailOrderPageState(); - static const route = AppRouteNoArg( - page: ServerDetailOrderPage.new, - path: '/settings/order/server_detail', - ); + static const route = AppRouteNoArg(page: ServerDetailOrderPage.new, path: '/settings/order/server_detail'); } class _ServerDetailOrderPageState extends State { @@ -31,8 +28,7 @@ class _ServerDetailOrderPageState extends State { return ValBuilder( listenable: prop.listenable(), builder: (keys) { - final disabled = - ServerDetailCards.names.where((e) => !keys.contains(e)).toList(); + final disabled = ServerDetailCards.names.where((e) => !keys.contains(e)).toList(); final allKeys = [...keys, ...disabled]; return ReorderableListView.builder( padding: const EdgeInsets.all(7), diff --git a/lib/view/page/setting/seq/srv_func_seq.dart b/lib/view/page/setting/seq/srv_func_seq.dart index a50c52e5..b453abed 100644 --- a/lib/view/page/setting/seq/srv_func_seq.dart +++ b/lib/view/page/setting/seq/srv_func_seq.dart @@ -10,10 +10,7 @@ class ServerFuncBtnsOrderPage extends StatefulWidget { @override State createState() => _ServerDetailOrderPageState(); - static const route = AppRouteNoArg( - page: ServerFuncBtnsOrderPage.new, - path: '/setting/seq/srv_func', - ); + static const route = AppRouteNoArg(page: ServerFuncBtnsOrderPage.new, path: '/setting/seq/srv_func'); } class _ServerDetailOrderPageState extends State { @@ -67,12 +64,7 @@ class _ServerDetailOrderPageState extends State { ); } - Widget _buildCheckBox( - List keys, - int key, - int idx, - bool value, - ) { + Widget _buildCheckBox(List keys, int key, int idx, bool value) { return Checkbox( value: value, onChanged: (val) { diff --git a/lib/view/page/setting/seq/srv_seq.dart b/lib/view/page/setting/seq/srv_seq.dart index d983aa64..742d6b04 100644 --- a/lib/view/page/setting/seq/srv_seq.dart +++ b/lib/view/page/setting/seq/srv_seq.dart @@ -12,10 +12,7 @@ class ServerOrderPage extends StatefulWidget { @override State createState() => _ServerOrderPageState(); - static const route = AppRouteNoArg( - page: ServerOrderPage.new, - path: '/settings/order/server', - ); + static const route = AppRouteNoArg(page: ServerOrderPage.new, path: '/settings/order/server'); } class _ServerOrderPageState extends State { @@ -36,10 +33,7 @@ class _ServerOrderPageState extends State { final double scale = lerpDouble(1, 1.02, animValue)!; return Transform.scale( scale: scale, - child: Card( - elevation: elevation, - child: child, - ), + child: Card(elevation: elevation, child: child), ); }, child: _buildCardTile(index), @@ -56,11 +50,7 @@ class _ServerOrderPageState extends State { footer: const SizedBox(height: 77), onReorder: (oldIndex, newIndex) { setState(() { - orders.value.move( - oldIndex, - newIndex, - property: Stores.setting.serverOrder, - ); + orders.value.move(oldIndex, newIndex, property: Stores.setting.serverOrder); }); }, padding: const EdgeInsets.all(8), @@ -78,9 +68,7 @@ class _ServerOrderPageState extends State { index: index, child: Padding( padding: const EdgeInsets.only(bottom: 8), - child: CardX( - child: _buildCardTile(index), - ), + child: CardX(child: _buildCardTile(index)), ), ); } @@ -93,20 +81,14 @@ class _ServerOrderPageState extends State { } return ListTile( - title: Text( - spi.name, - style: const TextStyle(fontWeight: FontWeight.w500), - ), + title: Text(spi.name, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(spi.oldId, style: UIs.textGrey), leading: CircleAvatar( backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Theme.of(context).colorScheme.onPrimary, child: Text(spi.name[0]), ), - trailing: ReorderableDragStartListener( - index: index, - child: const Icon(Icons.drag_handle), - ), + trailing: ReorderableDragStartListener(index: index, child: const Icon(Icons.drag_handle)), ); } } diff --git a/lib/view/page/snippet/edit.dart b/lib/view/page/snippet/edit.dart index c1ffe84c..bf5d2274 100644 --- a/lib/view/page/snippet/edit.dart +++ b/lib/view/page/snippet/edit.dart @@ -19,10 +19,7 @@ class SnippetEditPage extends StatefulWidget { @override State createState() => _SnippetEditPageState(); - static const route = AppRoute( - page: SnippetEditPage.new, - path: '/snippets/edit', - ); + static const route = AppRoute(page: SnippetEditPage.new, path: '/snippets/edit'); } class _SnippetEditPageState extends State with AfterLayoutMixin { @@ -47,10 +44,7 @@ class _SnippetEditPageState extends State with AfterLayoutMixin @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - title: Text(libL10n.edit), - actions: _buildAppBarActions(), - ), + appBar: CustomAppBar(title: Text(libL10n.edit), actions: _buildAppBarActions()), body: _buildBody(), floatingActionButton: _buildFAB(), ); @@ -64,9 +58,7 @@ class _SnippetEditPageState extends State with AfterLayoutMixin onPressed: () { context.showRoundDialog( title: libL10n.attention, - child: Text(libL10n.askContinue( - '${libL10n.delete} ${l10n.snippet}(${snippet.name})', - )), + child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.snippet}(${snippet.name})')), actions: Btn.ok( onTap: () { SnippetProvider.del(snippet); @@ -79,7 +71,7 @@ class _SnippetEditPageState extends State with AfterLayoutMixin }, tooltip: libL10n.delete, icon: const Icon(Icons.delete), - ) + ), ]; } @@ -168,12 +160,7 @@ class _SnippetEditPageState extends State with AfterLayoutMixin trailing: const Icon(Icons.keyboard_arrow_right), subtitle: subtitle == null ? null - : Text( - subtitle, - maxLines: 1, - style: UIs.textGrey, - overflow: TextOverflow.ellipsis, - ), + : Text(subtitle, maxLines: 1, style: UIs.textGrey, overflow: TextOverflow.ellipsis), onTap: () async { vals.removeWhere((e) => !ServerProvider.serverOrder.value.contains(e)); final serverIds = await context.showPickDialog( @@ -198,7 +185,8 @@ class _SnippetEditPageState extends State with AfterLayoutMixin child: Padding( padding: const EdgeInsets.all(13), child: SimpleMarkdown( - data: ''' + data: + ''' 📌 ${l10n.supportFmtArgs}\n ${SnippetX.fmtArgs.keys.map((e) => '`$e`').join(', ')}\n @@ -207,11 +195,7 @@ ${libL10n.example}: - `\${ctrl+c}` (Control + C) - `\${ctrl+b}d` (Tmux Detach) ''', - styleSheet: MarkdownStyleSheet( - codeblockDecoration: const BoxDecoration( - color: Colors.transparent, - ), - ), + styleSheet: MarkdownStyleSheet(codeblockDecoration: const BoxDecoration(color: Colors.transparent)), ), ), ); diff --git a/lib/view/page/snippet/list.dart b/lib/view/page/snippet/list.dart index b5bd2b7f..e9b8fbd1 100644 --- a/lib/view/page/snippet/list.dart +++ b/lib/view/page/snippet/list.dart @@ -11,10 +11,7 @@ class SnippetListPage extends StatefulWidget { @override State createState() => _SnippetListPageState(); - static const route = AppRouteNoArg( - page: SnippetListPage.new, - path: '/snippets', - ); + static const route = AppRouteNoArg(page: SnippetListPage.new, path: '/snippets'); } class _SnippetListPageState extends State with AutomaticKeepAliveClientMixin { @@ -38,24 +35,22 @@ class _SnippetListPageState extends State with AutomaticKeepAli Widget _buildBody() { // final isMobile = ResponsiveBreakpoints.of(context).isMobile; - return SnippetProvider.snippets.listenVal( - (snippets) { - return _tag.listenVal((tag) { - final child = _buildScaffold(snippets, tag); - // if (isMobile) { - return child; - // } + return SnippetProvider.snippets.listenVal((snippets) { + return _tag.listenVal((tag) { + final child = _buildScaffold(snippets, tag); + // if (isMobile) { + return child; + // } - // return SplitView( - // controller: _splitViewCtrl, - // leftWeight: 1, - // rightWeight: 1.3, - // initialRight: Center(child: Text(libL10n.empty)), - // leftBuilder: (_, __) => child, - // ); - }); - }, - ); + // return SplitView( + // controller: _splitViewCtrl, + // leftWeight: 1, + // rightWeight: 1.3, + // initialRight: Center(child: Text(libL10n.empty)), + // leftBuilder: (_, __) => child, + // ); + }); + }); } Widget _buildScaffold(List snippets, String tag) { @@ -104,11 +99,7 @@ class _SnippetListPageState extends State with AutomaticKeepAli Widget _buildSnippetItem(Snippet snippet) { return ListTile( contentPadding: const EdgeInsets.only(left: 23, right: 17), - title: Text( - snippet.name, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + title: Text(snippet.name, overflow: TextOverflow.ellipsis, maxLines: 1), subtitle: Text( snippet.note ?? snippet.script, overflow: TextOverflow.ellipsis, @@ -119,10 +110,7 @@ class _SnippetListPageState extends State with AutomaticKeepAli onTap: () { // final isMobile = ResponsiveBreakpoints.of(context).isMobile; // if (isMobile) { - SnippetEditPage.route.go( - context, - args: SnippetEditPageArgs(snippet: snippet), - ); + SnippetEditPage.route.go(context, args: SnippetEditPageArgs(snippet: snippet)); // } else { // _splitViewCtrl.replace(SnippetEditPage( // args: SnippetEditPageArgs(snippet: snippet), diff --git a/lib/view/page/snippet/result.dart b/lib/view/page/snippet/result.dart index 89110d65..a0252f6b 100644 --- a/lib/view/page/snippet/result.dart +++ b/lib/view/page/snippet/result.dart @@ -8,10 +8,7 @@ class SnippetResultPage extends StatelessWidget { const SnippetResultPage({super.key, required this.args}); - static const route = AppRouteArg( - page: SnippetResultPage.new, - path: '/snippets/result', - ); + static const route = AppRouteArg(page: SnippetResultPage.new, path: '/snippets/result'); @override Widget build(BuildContext context) { @@ -37,10 +34,7 @@ class SnippetResultPage extends StatelessWidget { SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 17), scrollDirection: Axis.horizontal, - child: Text( - item.result, - textAlign: TextAlign.start, - ), + child: Text(item.result, textAlign: TextAlign.start), ), ], ), diff --git a/lib/view/page/ssh/page/keyboard.dart b/lib/view/page/ssh/page/keyboard.dart index 8d2d25fb..ce51783b 100644 --- a/lib/view/page/ssh/page/keyboard.dart +++ b/lib/view/page/ssh/page/keyboard.dart @@ -13,7 +13,7 @@ extension _Keyboard on SSHPageState { _handleEscKeyOrBackButton(); return true; // Mark as handled so it doesn't propagate } - if (event.logicalKey == LogicalKeyboardKey.shiftLeft || + if (event.logicalKey == LogicalKeyboardKey.shiftLeft || event.logicalKey == LogicalKeyboardKey.shiftRight) { // Handle shift key press _terminal.keyInput(TerminalKey.shift); diff --git a/lib/view/page/ssh/page/virt_key.dart b/lib/view/page/ssh/page/virt_key.dart index 140e2556..759c93c7 100644 --- a/lib/view/page/ssh/page/virt_key.dart +++ b/lib/view/page/ssh/page/virt_key.dart @@ -88,10 +88,7 @@ extension _VirtKey on SSHPageState { while (initPath == null) { // Check if we've exceeded timeout if (DateTime.now().difference(startTime) > timeout) { - contextSafe?.showRoundDialog( - title: libL10n.error, - child: Text(libL10n.empty), - ); + contextSafe?.showRoundDialog(title: libL10n.error, child: Text(libL10n.empty)); return; } @@ -119,10 +116,7 @@ extension _VirtKey on SSHPageState { } if (!initPath.startsWith('/')) { - context.showRoundDialog( - title: libL10n.error, - child: Text('${l10n.remotePath}: $initPath'), - ); + context.showRoundDialog(title: libL10n.error, child: Text('${l10n.remotePath}: $initPath')); return; } @@ -138,10 +132,7 @@ extension _VirtKey on SSHPageState { if (text != null) { _terminal.textInput(text); } else { - context.showRoundDialog( - title: libL10n.error, - child: Text(libL10n.empty), - ); + context.showRoundDialog(title: libL10n.error, child: Text(libL10n.empty)); } }); } diff --git a/lib/view/page/ssh/tab.dart b/lib/view/page/ssh/tab.dart index 0ff097b6..72dc32cb 100644 --- a/lib/view/page/ssh/tab.dart +++ b/lib/view/page/ssh/tab.dart @@ -16,19 +16,14 @@ class SSHTabPage extends StatefulWidget { @override State createState() => _SSHTabPageState(); - static const route = AppRouteNoArg( - page: SSHTabPage.new, - path: '/ssh', - ); + static const route = AppRouteNoArg(page: SSHTabPage.new, path: '/ssh'); } typedef _TabMap = Map; class _SSHTabPageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { - late final _TabMap _tabMap = { - libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null), - }; + late final _TabMap _tabMap = {libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null)}; final _pageCtrl = PageController(); final _fabVN = 0.vn; final _tabRN = RNode(); @@ -48,12 +43,7 @@ class _SSHTabPageState extends State appBar: PreferredSizeListenBuilder( listenable: _tabRN, builder: () { - return _TabBar( - idxVN: _fabVN, - map: _tabMap, - onTap: _onTapTab, - onClose: _onTapClose, - ); + return _TabBar(idxVN: _fabVN, map: _tabMap, onTap: _onTapTab, onClose: _onTapClose); }, ), body: _buildBody(), @@ -159,12 +149,7 @@ extension on _SSHTabPageState { } final class _TabBar extends StatelessWidget implements PreferredSizeWidget { - const _TabBar({ - required this.idxVN, - required this.map, - required this.onTap, - required this.onClose, - }); + const _TabBar({required this.idxVN, required this.map, required this.onTap, required this.onClose}); final ValueListenable idxVN; final _TabMap map; @@ -188,10 +173,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget { itemBuilder: (_, idx) => _buildItem(idx), separatorBuilder: (_, _) => Padding( padding: const EdgeInsets.symmetric(vertical: 17), - child: Container( - color: const Color.fromARGB(61, 158, 158, 158), - width: 3, - ), + child: Container(color: const Color.fromARGB(61, 158, 158, 158), width: 3), ), ); }, @@ -242,10 +224,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget { width: selected ? kWideWidth : kNarrowWidth, duration: Durations.medium3, curve: Curves.fastEaseInToSlowEaseOut, - child: OverflowBox( - maxWidth: selected ? kWideWidth : null, - child: btn, - ), + child: OverflowBox(maxWidth: selected ? kWideWidth : null, child: btn), ); } @@ -280,9 +259,7 @@ class _AddPage extends StatelessWidget { return ServerProvider.serverOrder.listenVal((order) { if (order.isEmpty) { - return Center( - child: Text(libL10n.empty, textAlign: TextAlign.center), - ); + return Center(child: Text(libL10n.empty, textAlign: TextAlign.center)); } // Custom grid @@ -316,7 +293,7 @@ class _AddPage extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - const Icon(Icons.chevron_right) + const Icon(Icons.chevron_right), ], ), ), diff --git a/lib/view/page/storage/local.dart b/lib/view/page/storage/local.dart index d9b8fd9f..b9d2a6fd 100644 --- a/lib/view/page/storage/local.dart +++ b/lib/view/page/storage/local.dart @@ -16,10 +16,7 @@ import 'package:server_box/view/page/storage/sftp_mission.dart'; final class LocalFilePageArgs { final bool? isPickFile; final String? initDir; - const LocalFilePageArgs({ - this.isPickFile, - this.initDir, - }); + const LocalFilePageArgs({this.isPickFile, this.initDir}); } class LocalFilePage extends StatefulWidget { @@ -27,10 +24,7 @@ class LocalFilePage extends StatefulWidget { const LocalFilePage({super.key, this.args}); - static const route = AppRoute( - page: LocalFilePage.new, - path: '/files/local', - ); + static const route = AppRoute(page: LocalFilePage.new, path: '/files/local'); @override State createState() => _LocalFilePageState(); @@ -98,9 +92,7 @@ class _LocalFilePageState extends State with AutomaticKeepAliveCl Future> getEntities() async { final files = await Directory(_path.path).list().toList(); final sorted = _sortType.value.sort(files); - final stats = await Future.wait( - sorted.map((e) async => (e, await e.stat())), - ); + final stats = await Future.wait(sorted.map((e) async => (e, await e.stat()))); return stats; } @@ -133,12 +125,7 @@ class _LocalFilePageState extends State with AutomaticKeepAliveCl final stat = item.$2; final isDir = stat.type == FileSystemEntityType.directory; - return _buildItem( - file: file, - fileName: fileName, - stat: stat, - isDir: isDir, - ); + return _buildItem(file: file, fileName: fileName, stat: stat, isDir: isDir); }, ); }, @@ -156,10 +143,7 @@ class _LocalFilePageState extends State with AutomaticKeepAliveCl leading: isDir ? const Icon(Icons.folder_open) : const Icon(Icons.insert_drive_file), title: Text(fileName), subtitle: isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey), - trailing: Text( - stat.modified.ymdhms(), - style: UIs.textGrey, - ), + trailing: Text(stat.modified.ymdhms(), style: UIs.textGrey), onLongPress: () { if (isDir) { _showDirActionDialog(file); @@ -187,17 +171,15 @@ class _LocalFilePageState extends State with AutomaticKeepAliveCl } Widget _buildSortBtn() { - return _sortType.listenVal( - (value) { - return PopupMenuButton<_SortType>( - icon: const Icon(Icons.sort), - itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(), - onSelected: (value) { - _sortType.value = value; - }, - ); - }, - ); + return _sortType.listenVal((value) { + return PopupMenuButton<_SortType>( + icon: const Icon(Icons.sort), + itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(), + onSelected: (value) { + _sortType.value = value; + }, + ); + }); } @override @@ -238,10 +220,12 @@ extension _Actions on _LocalFilePageState { title: libL10n.file, child: Text(fileName), actions: [ - Btn.ok(onTap: () { - context.pop(); - context.pop(file.path); - }), + Btn.ok( + onTap: () { + context.pop(); + context.pop(file.path); + }, + ), ], ); return; @@ -382,21 +366,13 @@ extension _OnTapFile on _LocalFilePageState { ); if (spi == null) return; - final args = SftpPageArgs( - spi: spi, - isSelect: true, - ); + final args = SftpPageArgs(spi: spi, isSelect: true); final remotePath = await SftpPage.route.go(context, args); if (remotePath == null) { return; } - SftpProvider.add(SftpReq( - spi, - '$remotePath/$fileName', - file.absolute.path, - SftpReqType.upload, - )); + SftpProvider.add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload)); context.showSnackBar(l10n.added2List); } } @@ -404,8 +380,7 @@ extension _OnTapFile on _LocalFilePageState { enum _SortType { name, size, - time, - ; + time; List sort(List files) { switch (this) { @@ -423,27 +398,21 @@ enum _SortType { } String get i18n => switch (this) { - name => libL10n.name, - size => l10n.size, - time => l10n.time, - }; + name => libL10n.name, + size => l10n.size, + time => l10n.time, + }; IconData get icon => switch (this) { - name => Icons.sort_by_alpha, - size => Icons.sort, - time => Icons.access_time, - }; + name => Icons.sort_by_alpha, + size => Icons.sort, + time => Icons.access_time, + }; PopupMenuItem<_SortType> get menuItem { return PopupMenuItem( value: this, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Icon(icon), - Text(i18n), - ], - ), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [Icon(icon), Text(i18n)]), ); } } diff --git a/lib/view/page/storage/sftp_mission.dart b/lib/view/page/storage/sftp_mission.dart index fa696794..6e9fb10a 100644 --- a/lib/view/page/storage/sftp_mission.dart +++ b/lib/view/page/storage/sftp_mission.dart @@ -11,19 +11,14 @@ class SftpMissionPage extends StatefulWidget { @override State createState() => _SftpMissionPageState(); - static const route = AppRouteNoArg( - page: SftpMissionPage.new, - path: '/sftp/mission', - ); + static const route = AppRouteNoArg(page: SftpMissionPage.new, path: '/sftp/mission'); } class _SftpMissionPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - title: Text(l10n.mission, style: UIs.text18), - ), + appBar: CustomAppBar(title: Text(l10n.mission, style: UIs.text18)), body: _buildBody(), ); } @@ -50,10 +45,7 @@ class _SftpMissionPageState extends State { status: status, subtitle: libL10n.error, trailing: IconButton( - onPressed: () => context.showRoundDialog( - title: libL10n.error, - child: Text(err.toString()), - ), + onPressed: () => context.showRoundDialog(title: libL10n.error, child: Text(err.toString())), icon: const Icon(Icons.error), ), ); @@ -109,9 +101,7 @@ class _SftpMissionPageState extends State { Widget _buildFinished(SftpReqStatus status) { final time = status.spentTime.toString(); - final str = l10n.spentTime( - time == 'null' ? l10n.unknown : (time.substring(0, time.length - 7)), - ); + final str = l10n.spentTime(time == 'null' ? l10n.unknown : (time.substring(0, time.length - 7))); final btns = Row( mainAxisSize: MainAxisSize.min, @@ -120,41 +110,26 @@ class _SftpMissionPageState extends State { onPressed: () { final idx = status.req.localPath.lastIndexOf(Pfs.seperator); final dir = status.req.localPath.substring(0, idx); - LocalFilePage.route.go( - context, - args: LocalFilePageArgs(initDir: dir), - ); + LocalFilePage.route.go(context, args: LocalFilePageArgs(initDir: dir)); }, icon: const Icon(Icons.file_open), ), IconButton( onPressed: () => Pfs.sharePaths(paths: [status.req.localPath]), icon: const Icon(Icons.open_in_new), - ) + ), ], ); - return _wrapInCard( - status: status, - subtitle: str, - trailing: btns, - ); + return _wrapInCard(status: status, subtitle: str, trailing: btns); } - Widget _wrapInCard({ - required SftpReqStatus status, - String? subtitle, - Widget? trailing, - }) { + Widget _wrapInCard({required SftpReqStatus status, String? subtitle, Widget? trailing}) { final time = DateTime.fromMicrosecondsSinceEpoch(status.id); return CardX( child: ListTile( leading: Text(time.hourMinute), - title: Text( - status.fileName, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + title: Text(status.fileName, overflow: TextOverflow.ellipsis, maxLines: 1), subtitle: subtitle == null ? null : Text(subtitle, style: UIs.textGrey), trailing: trailing, ), @@ -165,9 +140,7 @@ class _SftpMissionPageState extends State { return IconButton( onPressed: () => context.showRoundDialog( title: libL10n.attention, - child: Text(libL10n.askContinue( - '${libL10n.delete} ${l10n.mission}($name)', - )), + child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.mission}($name)')), actions: Btn.ok( onTap: () { SftpProvider.cancel(id); diff --git a/lib/view/page/systemd.dart b/lib/view/page/systemd.dart index ec6a04f3..2b988a1d 100644 --- a/lib/view/page/systemd.dart +++ b/lib/view/page/systemd.dart @@ -9,15 +9,9 @@ import 'package:server_box/view/page/ssh/page/page.dart'; final class SystemdPage extends StatefulWidget { final SpiRequiredArgs args; - const SystemdPage({ - super.key, - required this.args, - }); + const SystemdPage({super.key, required this.args}); - static const route = AppRouteArg( - page: SystemdPage.new, - path: '/systemd', - ); + static const route = AppRouteArg(page: SystemdPage.new, path: '/systemd'); @override State createState() => _SystemdPageState(); @@ -37,9 +31,7 @@ final class _SystemdPageState extends State { return Scaffold( appBar: CustomAppBar( title: const Text('Systemd'), - actions: isDesktop - ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)] - : null, + actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)] : null, ), body: RefreshIndicator(onRefresh: _pro.getUnits, child: _buildBody()), ); @@ -54,9 +46,7 @@ final class _SystemdPageState extends State { duration: Durations.medium1, curve: Curves.fastEaseInToSlowEaseOut, height: isBusy ? SizedLoading.medium.size : 0, - child: isBusy - ? SizedLoading.medium - : const SizedBox.shrink(), + child: isBusy ? SizedLoading.medium : const SizedBox.shrink(), ), ), ), @@ -66,35 +56,24 @@ final class _SystemdPageState extends State { } Widget _buildUnitList(VNode> units) { - return units.listenVal( - (units) { - if (units.isEmpty) { - return SliverToBoxAdapter( - child: - CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13), - ); - } - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final unit = units[index]; - return ListTile( - leading: _buildScopeTag(unit.scope), - title: unit.description != null - ? TipText(unit.name, unit.description!) - : Text(unit.name), - subtitle: Wrap(children: [ - _buildStateTag(unit.state), - _buildTypeTag(unit.type), - ]).paddingOnly(top: 7), - trailing: _buildUnitFuncs(unit), - ).cardx.paddingSymmetric(horizontal: 13); - }, - childCount: units.length, - ), - ); - }, - ); + return units.listenVal((units) { + if (units.isEmpty) { + return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13)); + } + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final unit = units[index]; + return ListTile( + leading: _buildScopeTag(unit.scope), + title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name), + subtitle: Wrap( + children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)], + ).paddingOnly(top: 7), + trailing: _buildUnitFuncs(unit), + ).cardx.paddingSymmetric(horizontal: 13); + }, childCount: units.length), + ); + }); } Widget _buildUnitFuncs(SystemdUnit unit) { @@ -128,11 +107,7 @@ final class _SystemdPageState extends State { child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Icon(func.icon, size: 19), - const SizedBox(width: 10), - Text(func.name.capitalize), - ], + children: [Icon(func.icon, size: 19), const SizedBox(width: 10), Text(func.name.capitalize)], ), ); } @@ -155,8 +130,7 @@ final class _SystemdPageState extends State { color: color?.withValues(alpha: 0.7) ?? UIs.halfAlpha, borderRadius: BorderRadius.circular(5), ), - child: Text(tag, style: UIs.text11) - .paddingSymmetric(horizontal: 5, vertical: 1), + child: Text(tag, style: UIs.text11).paddingSymmetric(horizontal: 5, vertical: 1), ).paddingOnly(right: noPad ? 0 : 5); } } diff --git a/lib/view/widget/omit_start_text.dart b/lib/view/widget/omit_start_text.dart index 84c53748..f0bbfc70 100644 --- a/lib/view/widget/omit_start_text.dart +++ b/lib/view/widget/omit_start_text.dart @@ -6,47 +6,39 @@ class OmitStartText extends StatelessWidget { final TextStyle? style; final TextOverflow? overflow; - const OmitStartText( - this.text, { - super.key, - this.maxLines, - this.style, - this.overflow, - }); + const OmitStartText(this.text, {super.key, this.maxLines, this.style, this.overflow}); @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, size) { - bool exceeded = false; - int len = 0; - for (; !exceeded && len < text.length; len++) { - // Build the textspan - final span = TextSpan( - text: 'A' * 7 + text.substring(text.length - len), - style: style ?? Theme.of(context).textTheme.bodyMedium, - ); + return LayoutBuilder( + builder: (context, size) { + bool exceeded = false; + int len = 0; + for (; !exceeded && len < text.length; len++) { + // Build the textspan + final span = TextSpan( + text: 'A' * 7 + text.substring(text.length - len), + style: style ?? Theme.of(context).textTheme.bodyMedium, + ); - // Use a textpainter to determine if it will exceed max lines - final tp = TextPainter( + // Use a textpainter to determine if it will exceed max lines + final tp = TextPainter(maxLines: maxLines ?? 1, textDirection: TextDirection.ltr, text: span); + + // trigger it to layout + tp.layout(maxWidth: size.maxWidth); + + // whether the text overflowed or not + exceeded = tp.didExceedMaxLines; + } + + return Text( + (exceeded ? '...' : '') + text.substring(text.length - len), + overflow: overflow ?? TextOverflow.fade, + softWrap: false, maxLines: maxLines ?? 1, - textDirection: TextDirection.ltr, - text: span, + style: style, ); - - // trigger it to layout - tp.layout(maxWidth: size.maxWidth); - - // whether the text overflowed or not - exceeded = tp.didExceedMaxLines; - } - - return Text( - (exceeded ? '...' : '') + text.substring(text.length - len), - overflow: overflow ?? TextOverflow.fade, - softWrap: false, - maxLines: maxLines ?? 1, - style: style, - ); - }); + }, + ); } } diff --git a/lib/view/widget/percent_circle.dart b/lib/view/widget/percent_circle.dart index 89da8f89..aead0144 100644 --- a/lib/view/widget/percent_circle.dart +++ b/lib/view/widget/percent_circle.dart @@ -5,18 +5,13 @@ import 'package:flutter/material.dart'; final class PercentCircle extends StatelessWidget { final double percent; - const PercentCircle({ - super.key, - required this.percent, - }); + const PercentCircle({super.key, required this.percent}); @override Widget build(BuildContext context) { final percent = switch (this.percent) { - 0 => 0.01, - 100 => 99.9, - // NaN - final val when val.isNaN => 0.01, + <= 0.01 => 0.01, + >= 99.9 => 99.9, _ => this.percent, }; return Stack( diff --git a/lib/view/widget/unix_perm.dart b/lib/view/widget/unix_perm.dart index 5db69ea3..77647cf7 100644 --- a/lib/view/widget/unix_perm.dart +++ b/lib/view/widget/unix_perm.dart @@ -6,11 +6,7 @@ final class UnixPermOp { final bool w; final bool x; - const UnixPermOp({ - required this.r, - required this.w, - required this.x, - }); + const UnixPermOp({required this.r, required this.w, required this.x}); UnixPermOp copyWith({bool? r, bool? w, bool? x}) { return UnixPermOp(r: r ?? this.r, w: w ?? this.w, x: x ?? this.x); @@ -24,8 +20,7 @@ final class UnixPermOp { enum UnixPermScope { user, group, - other, - ; + other; String get title { return switch (this) { @@ -72,10 +67,10 @@ final class UnixPerm { } static UnixPerm get empty => const UnixPerm( - user: UnixPermOp(r: false, w: false, x: false), - group: UnixPermOp(r: false, w: false, x: false), - other: UnixPermOp(r: false, w: false, x: false), - ); + user: UnixPermOp(r: false, w: false, x: false), + group: UnixPermOp(r: false, w: false, x: false), + other: UnixPermOp(r: false, w: false, x: false), + ); } final class UnixPermEditor extends StatefulWidget { @@ -150,9 +145,6 @@ final class _UnixPermEditorState extends State { } Widget _buildSwitch(bool value, void Function(bool) onChanged) { - return Switch( - value: value, - onChanged: onChanged, - ); + return Switch(value: value, onChanged: onChanged); } } diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 5ec7fc68..b69504b1 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -96,9 +96,12 @@ include(flutter/generated_plugins.cmake) # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() +# if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) +# set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +# endif() +# Always set the install prefix to the build bundle directory, even if +# CMAKE_INSTALL_PREFIX was set to something else before. +set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) # Start with a clean build bundle directory every time. install(CODE " diff --git a/test/amd_smi_test.dart b/test/amd_smi_test.dart index d7b6d7fc..191181c6 100644 --- a/test/amd_smi_test.dart +++ b/test/amd_smi_test.dart @@ -313,7 +313,7 @@ void main() { } ] '''; - + final gpu = AmdSmi.fromJson(jsonWithInvalidProcess)[0]; expect(gpu.memory.processes.length, 1); expect(gpu.memory.processes[0].pid, 1234); @@ -409,4 +409,4 @@ void main() { expect(gpus[0].clockSpeed, 0); }); }); -} \ No newline at end of file +} diff --git a/test/btrfs_test.dart b/test/btrfs_test.dart index d61961ce..9354e80b 100644 --- a/test/btrfs_test.dart +++ b/test/btrfs_test.dart @@ -9,25 +9,25 @@ void main() { final disks = Disk.parse(_btrfsRaidJsonOutput); expect(disks, isNotEmpty); expect(disks.length, 4); // Should have 2 parent disks + 2 BTRFS partitions - + // We should get two distinct disks with the same UUID but different paths final nvme1Disk = disks.firstWhere((disk) => disk.path == '/dev/nvme1n1p1'); final nvme2Disk = disks.firstWhere((disk) => disk.path == '/dev/nvme2n1p1'); - + // Both should exist expect(nvme1Disk, isNotNull); expect(nvme2Disk, isNotNull); - + // They should have the same UUID (since they're part of the same BTRFS volume) expect(nvme1Disk.uuid, nvme2Disk.uuid); - + // But they should be treated as distinct disks expect(identical(nvme1Disk, nvme2Disk), isFalse); - + // Verify DiskUsage counts physical disks correctly final usage = DiskUsage.parse(disks); // With our unique path+kname identifier, both disks should be counted - expect(usage.size, nvme1Disk.size + nvme2Disk.size); + expect(usage.size, nvme1Disk.size + nvme2Disk.size); expect(usage.used, nvme1Disk.used + nvme2Disk.used); }); }); diff --git a/test/container_test.dart b/test/container_test.dart index ea810c78..0c5d0a52 100644 --- a/test/container_test.dart +++ b/test/container_test.dart @@ -15,7 +15,7 @@ fa1215b4be74 Up 12 hours firefly const images = [ 'rustdesk/rustdesk-server:latest', 'rustdesk/rustdesk-server:latest', - 'uusec/firefly:latest' + 'uusec/firefly:latest', ]; const states = ['Up 2 hours', 'Up 41 minutes', 'Up 12 hours']; for (var idx = 1; idx < lines.length; idx++) { diff --git a/test/disk_test.dart b/test/disk_test.dart index bad3432b..bb0c04e3 100644 --- a/test/disk_test.dart +++ b/test/disk_test.dart @@ -11,12 +11,12 @@ void main() { expect(disks, isNotEmpty); } }); - + test('parse lsblk JSON output', () { final disks = Disk.parse(_jsonLsblkOutput); expect(disks, isNotEmpty); - expect(disks.length, 6); // Should find ext4 root, vfat efi, and ext2 boot - + expect(disks.length, 6); // Should find ext4 root, vfat efi, and ext2 boot + // Verify root filesystem final rootFs = disks.firstWhere((disk) => disk.mount == '/'); expect(rootFs.fsTyp, 'ext4'); @@ -24,44 +24,44 @@ void main() { expect(rootFs.used, BigInt.parse('552718364672') ~/ BigInt.from(1024)); expect(rootFs.avail, BigInt.parse('379457622016') ~/ BigInt.from(1024)); expect(rootFs.usedPercent, 56); - + // Verify boot/efi filesystem final efiFs = disks.firstWhere((disk) => disk.mount == '/boot/efi'); expect(efiFs.fsTyp, 'vfat'); expect(efiFs.size, BigInt.parse('535805952') ~/ BigInt.from(1024)); expect(efiFs.usedPercent, 1); - + // Verify boot filesystem final bootFs = disks.firstWhere((disk) => disk.mount == '/boot'); expect(bootFs.fsTyp, 'ext2'); expect(bootFs.usedPercent, 34); }); - + test('parse nested lsblk JSON output with parent/child relationships', () { final disks = Disk.parse(_nestedJsonLsblkOutput); expect(disks, isNotEmpty); - + // Check parent device with children final parentDisk = disks.firstWhere((disk) => disk.path == '/dev/nvme0n1'); expect(parentDisk.children, isNotEmpty); expect(parentDisk.children.length, 3); - + // Check one of the children final rootPartition = parentDisk.children.firstWhere((disk) => disk.mount == '/'); expect(rootPartition.fsTyp, 'ext4'); expect(rootPartition.path, '/dev/nvme0n1p2'); expect(rootPartition.usedPercent, 45); - + // Verify we have a child partition with UUID final bootPartition = parentDisk.children.firstWhere((disk) => disk.mount == '/boot'); expect(bootPartition.uuid, '12345678-abcd-1234-abcd-1234567890ab'); }); - + test('DiskUsage handles zero size correctly', () { final usage = DiskUsage(used: BigInt.from(1000), size: BigInt.zero); expect(usage.usedPercent, 0); // Should return 0 instead of throwing }); - + test('DiskUsage handles null kname', () { final disks = [ Disk( @@ -74,7 +74,7 @@ void main() { kname: null, // Explicitly null kname ), ]; - + final usage = DiskUsage.parse(disks); expect(usage.used, BigInt.from(5000)); expect(usage.size, BigInt.from(10000)); @@ -198,16 +198,16 @@ const _nestedJsonLsblkOutput = ''' '''; const _raws = [ -// ''' -// Filesystem 1K-blocks Used Available Use% Mounted on -// udev 864088 0 864088 0% /dev -// tmpfs 176724 688 176036 1% /run -// /dev/vda3 40910528 18067948 20951380 47% / -// tmpfs 883612 0 883612 0% /dev/shm -// tmpfs 5120 0 5120 0% /run/lock -// /dev/vda2 192559 11807 180752 7% /boot/efi -// tmpfs 176720 104 176616 1% /run/user/1000 -// ''', + // ''' + // Filesystem 1K-blocks Used Available Use% Mounted on + // udev 864088 0 864088 0% /dev + // tmpfs 176724 688 176036 1% /run + // /dev/vda3 40910528 18067948 20951380 47% / + // tmpfs 883612 0 883612 0% /dev/shm + // tmpfs 5120 0 5120 0% /run/lock + // /dev/vda2 192559 11807 180752 7% /boot/efi + // tmpfs 176720 104 176616 1% /run/user/1000 + // ''', ''' Filesystem 1K-blocks Used Available Use% Mounted on udev 16181648 0 16181648 0% /dev diff --git a/test/sensors_test.dart b/test/sensors_test.dart index a2bd4dcf..023524bd 100644 --- a/test/sensors_test.dart +++ b/test/sensors_test.dart @@ -113,15 +113,12 @@ void main() { SensorAdaptor.virtual, SensorAdaptor.pci, ]); - expect( - sensors.map((e) => e.summary), - [ - '+56.0°C (high = +105.0°C, crit = +105.0°C)', - '+27.8°C (crit = +119.0°C)', - '+56.0°C', - '+45.9°C (low = -273.1°C, high = +83.8°C)', - ], - ); + expect(sensors.map((e) => e.summary), [ + '+56.0°C (high = +105.0°C, crit = +105.0°C)', + '+27.8°C (crit = +119.0°C)', + '+56.0°C', + '+45.9°C (low = -273.1°C, high = +83.8°C)', + ]); }); test('parse sensors2', () { @@ -138,14 +135,11 @@ void main() { SensorAdaptor.pci, SensorAdaptor.pci, ]); - expect( - sensors.map((e) => e.summary), - [ - '1.26 V', - '1.19 V (min = +0.00 V, max = +1.74 V)', - '+45.9°C (low = -273.1°C, high = +69.8°C)', - '+44.9°C', - ], - ); + expect(sensors.map((e) => e.summary), [ + '1.26 V', + '1.19 V (min = +0.00 V, max = +1.74 V)', + '+45.9°C (low = -273.1°C, high = +69.8°C)', + '+44.9°C', + ]); }); } diff --git a/test/uptime_test.dart b/test/uptime_test.dart new file mode 100644 index 00000000..1640fb3d --- /dev/null +++ b/test/uptime_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Linux uptime parsing tests', () { + test('should parse uptime with days and hours:minutes', () { + const raw = '19:39:15 up 61 days, 18:16, 1 user, load average: 0.00, 0.00, 0.00'; + final result = _testParseUpTime(raw); + expect(result, '61 days, 18:16'); + }); + + test('should parse uptime with single day and hours:minutes', () { + const raw = '19:39:15 up 1 day, 2:34, 1 user, load average: 0.00, 0.00, 0.00'; + final result = _testParseUpTime(raw); + expect(result, '1 day, 2:34'); + }); + + test('should parse uptime with only hours:minutes', () { + const raw = '19:39:15 up 2:34, 1 user, load average: 0.00, 0.00, 0.00'; + final result = _testParseUpTime(raw); + expect(result, '2:34'); + }); + + test('should parse uptime with only minutes', () { + const raw = '19:39:15 up 34 min, 1 user, load average: 0.00, 0.00, 0.00'; + final result = _testParseUpTime(raw); + expect(result, '34 min'); + }); + + test('should parse uptime with days only (no time part)', () { + const raw = '19:39:15 up 5 days, 1 user, load average: 0.00, 0.00, 0.00'; + final result = _testParseUpTime(raw); + expect(result, '5 days'); + }); + + test('should return null for invalid format', () { + const raw = 'invalid uptime format'; + final result = _testParseUpTime(raw); + expect(result, null); + }); + + test('should handle edge case with empty string', () { + const raw = ''; + final result = _testParseUpTime(raw); + expect(result, null); + }); + }); +} + +// Helper function to test the private _parseUpTime function +String? _testParseUpTime(String raw) { + final splitedUp = raw.split('up '); + if (splitedUp.length == 2) { + 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; +} \ No newline at end of file diff --git a/test/windows_test.dart b/test/windows_test.dart new file mode 100644 index 00000000..cffa7051 --- /dev/null +++ b/test/windows_test.dart @@ -0,0 +1,473 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:server_box/data/model/app/shell_func.dart'; +import 'package:server_box/data/model/server/server_status_update_req.dart'; +import 'package:server_box/data/model/server/system.dart'; +import 'package:server_box/data/res/status.dart'; + +void main() { + group('Windows System Tests', () { + test('should verify Windows segments length matches command types', () { + final systemType = SystemType.windows; + final expectedLength = WindowsStatusCmdType.values.length; + expect(systemType.segmentsLen, equals(expectedLength)); + expect(systemType.isSegmentsLenMatch(expectedLength), isTrue); + }); + + test('should generate Windows PowerShell script correctly', () { + final script = ShellFunc.allScript({'custom_cmd': 'echo "test"'}, systemType: SystemType.windows); + + expect(script, contains('PowerShell script for ServerBox')); + expect(script, contains('function SbStatus')); + expect(script, contains('function SbProcess')); + expect(script, contains('function SbShutdown')); + expect(script, contains('function SbReboot')); + expect(script, contains('function SbSuspend')); + expect(script, contains('switch (\$args[0])')); + expect(script, contains('"-s" { SbStatus }')); + expect(script, contains('echo "test"')); + }); + + test('should handle Windows system parsing with real data', () async { + final segments = _windowsStatusSegments; + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + final result = await getStatus(req); + + // Verify system information was parsed + expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations')); + expect(result.more[StatusCmdType.host], equals('LKH6')); + + // Verify CPU information + expect(result.cpu.now, isNotEmpty); + expect(result.cpu.brand.keys.first, contains('12th Gen Intel(R) Core(TM) i5-12490F')); + + // Verify memory information + expect(result.mem, isNotNull); + expect(result.mem.total, equals(66943944)); + expect(result.mem.free, equals(58912812)); + + // Verify disk information + expect(result.disk, isNotEmpty); + final cDrive = result.disk.firstWhere((disk) => disk.path == 'C:'); + expect(cDrive.fsTyp, equals('NTFS')); + expect(cDrive.size, equals(BigInt.parse('999271952384') ~/ BigInt.from(1024))); + expect(cDrive.avail, equals(BigInt.parse('386084032512') ~/ BigInt.from(1024))); + + // Verify TCP connections + expect(result.tcp, isNotNull); + expect(result.tcp.active, equals(2)); + }); + + test('should parse Windows CPU data correctly', () async { + const cpuJson = ''' + { + "Name": "12th Gen Intel(R) Core(TM) i5-12490F", + "LoadPercentage": 42 + } + '''; + + final segments = ['__windows', '1754151483', '', '', cpuJson]; + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + final result = await getStatus(req); + + expect(result.cpu.now, hasLength(1)); + expect(result.cpu.now.first.user, equals(42)); + expect(result.cpu.now.first.idle, equals(58)); + }); + + test('should parse Windows memory data correctly', () async { + const memoryJson = ''' + { + "TotalVisibleMemorySize": 66943944, + "FreePhysicalMemory": 58912812 + } + '''; + + final segments = ['__windows', '1754151483', '', '', '', '', '', '', memoryJson]; + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + final result = await getStatus(req); + + expect(result.mem, isNotNull); + expect(result.mem.total, equals(66943944)); + expect(result.mem.free, equals(58912812)); + expect(result.mem.avail, equals(58912812)); + }); + + test('should parse Windows disk data correctly', () async { + const diskJson = ''' + { + "DeviceID": "C:", + "Size": 999271952384, + "FreeSpace": 386084032512, + "FileSystem": "NTFS" + } + '''; + + final segments = ['__windows', '1754151483', '', '', '', '', '', diskJson]; + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + final result = await getStatus(req); + + expect(result.disk, hasLength(1)); + final disk = result.disk.first; + expect(disk.path, equals('C:')); + expect(disk.mount, equals('C:')); + expect(disk.fsTyp, equals('NTFS')); + expect(disk.size, equals(BigInt.parse('999271952384') ~/ BigInt.from(1024))); + expect(disk.avail, equals(BigInt.parse('386084032512') ~/ BigInt.from(1024))); + expect(disk.usedPercent, equals(61)); + }); + + test('should parse Windows battery data correctly', () async { + const batteryJson = ''' + { + "EstimatedChargeRemaining": 85, + "BatteryStatus": 6 + } + '''; + + // Create segments with enough elements to reach battery position + final segments = List.filled(WindowsStatusCmdType.values.length, ''); + segments[0] = '__windows'; + segments[WindowsStatusCmdType.battery.index] = batteryJson; + + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + final result = await getStatus(req); + + expect(result.batteries, hasLength(1)); + final battery = result.batteries.first; + expect(battery.name, equals('Battery')); + expect(battery.percent, equals(85)); + expect(battery.status.name, equals('charging')); + }); + + test('should handle Windows uptime parsing correctly', () async { + // Test new format with date line + uptime days + const uptimeNewFormat = 'Friday, July 25, 2025 2:26:42 PM\n2'; + + final segments = List.filled(WindowsStatusCmdType.values.length, ''); + segments[0] = '__windows'; + segments[WindowsStatusCmdType.uptime.index] = uptimeNewFormat; + + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + final result = await getStatus(req); + + expect(result.more[StatusCmdType.uptime], isNotNull); + }); + + test('should handle Windows uptime parsing with old format', () async { + const uptimeDateTime = 'Friday, July 25, 2025 2:26:42 PM'; + + final segments = List.filled(WindowsStatusCmdType.values.length, ''); + segments[0] = '__windows'; + segments[WindowsStatusCmdType.uptime.index] = uptimeDateTime; + + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + final result = await getStatus(req); + + expect(result.more[StatusCmdType.uptime], isNotNull); + }); + + test('should handle Windows script path generation', () { + const serverId = 'test-server'; + + final scriptPath = ShellFunc.getScriptPath(serverId, systemType: SystemType.windows); + expect(scriptPath, contains('.ps1')); + expect(scriptPath, contains('\\')); + + final installCmd = ShellFunc.getInstallShellCmd(serverId, systemType: SystemType.windows); + expect(installCmd, contains('New-Item')); + expect(installCmd, contains('Set-Content')); + // No longer contains 'powershell' prefix as commands now run in PowerShell session + }); + + test('should execute Windows commands correctly', () { + const serverId = 'test-server'; + + final statusCmd = ShellFunc.status.exec(serverId, systemType: SystemType.windows); + expect(statusCmd, contains('powershell')); + expect(statusCmd, contains('-ExecutionPolicy Bypass')); + expect(statusCmd, contains('-s')); + + final processCmd = ShellFunc.process.exec(serverId, systemType: SystemType.windows); + expect(processCmd, contains('powershell')); + expect(processCmd, contains('-p')); + }); + + test('should handle GPU detection on Windows', () async { + const nvidiaNotFound = 'NVIDIA driver not found'; + const amdNotFound = 'AMD driver not found'; + + final segments = List.filled(WindowsStatusCmdType.values.length, ''); + segments[0] = '__windows'; + segments[WindowsStatusCmdType.nvidia.index] = nvidiaNotFound; + segments[WindowsStatusCmdType.amd.index] = amdNotFound; + + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + final result = await getStatus(req); + + // Should not throw errors even when GPU drivers are not found + expect(result.nvidia, anyOf(isNull, isEmpty)); + expect(result.amd, anyOf(isNull, isEmpty)); + }); + + test('should handle Windows error conditions gracefully', () async { + // Test with malformed JSON and error messages + final segments = [ + '__windows', + '1754151483', + 'Network adapter error', + 'Microsoft Windows 11 Pro for Workstations', + 'invalid json {', + 'uptime error', + 'connection error', + 'disk error', + 'memory error', + 'temp error', + 'LKH6', + 'diskio error', + 'battery error', + 'NVIDIA driver not found', + 'AMD driver not found', + 'sensor error', + 'smart error', + '12th Gen Intel(R) Core(TM) i5-12490F', + ]; + + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + // Should not throw exceptions + expect(() async => await getStatus(req), returnsNormally); + + final result = await getStatus(req); + expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations')); + expect(result.more[StatusCmdType.host], equals('LKH6')); + }); + + test('should handle Windows temperature error output gracefully', () async { + // Test with actual error output from win_raw.txt + final segments = [ + '__windows', + '1754151483', + '', // network + 'Microsoft Windows 11 Pro for Workstations', // system + ''' + { + "Name": "12th Gen Intel(R) Core(TM) i5-12490F", + "LoadPercentage": 42 + } + ''', // cpu + 'Friday, July 25, 2025 2:26:42 PM', // uptime + '2', // connections + ''' + { + "DeviceID": "C:", + "Size": 999271952384, + "FreeSpace": 386084032512, + "FileSystem": "NTFS" + } + ''', // disk + ''' + { + "TotalVisibleMemorySize": 66943944, + "FreePhysicalMemory": 58912812 + } + ''', // memory + ''' +The string is missing the terminator: ". + + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException + + FullyQualifiedErrorId : TerminatorExpectedAtEndOfString + ''', // temp (error output) + 'LKH6', // host + '', // diskio + '', // battery + 'NVIDIA driver not found', // nvidia + 'AMD driver not found', // amd + '', // sensors + ''' + { + "DeviceId": "0", + "Temperature": 41, + "TemperatureMax": 70, + "Wear": 0, + "PowerOnHours": null + } + ''', // smart + '12th Gen Intel(R) Core(TM) i5-12490F', // cpu brand + ]; + + final serverStatus = InitStatus.status; + + final req = ServerStatusUpdateReq( + system: SystemType.windows, + ss: serverStatus, + segments: segments, + customCmds: {}, + ); + + // Should not throw exceptions even with error output in temperature values + expect(() async => await getStatus(req), returnsNormally); + + final result = await getStatus(req); + expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations')); + expect(result.more[StatusCmdType.host], equals('LKH6')); + // Temperature should be empty since we got error output + expect(result.temps.isEmpty, isTrue); + }); + }); +} + +// Sample Windows status segments based on real PowerShell output +final _windowsStatusSegments = [ + '__windows', // System type marker + '1754151483', // Unix timestamp + '', // Network data (empty for now) + 'Microsoft Windows 11 Pro for Workstations', // System name + ''' + { + "Name": "12th Gen Intel(R) Core(TM) i5-12490F", + "LoadPercentage": 42 + } + ''', // CPU data + 'Friday, July 25, 2025 2:26:42 PM', // Uptime (boot time) + '2', // Connection count + ''' + { + "DeviceID": "C:", + "Size": 999271952384, + "FreeSpace": 386084032512, + "FileSystem": "NTFS" + } + ''', // Disk data + ''' + { + "TotalVisibleMemorySize": 66943944, + "FreePhysicalMemory": 58912812 + } + ''', // Memory data + '', // Temperature (combined command - empty due to OpenHardwareMonitor error) + 'LKH6', // Hostname + '', // Disk I/O (empty for now) + '', // Battery data (empty) + 'NVIDIA driver not found', // NVIDIA GPU + 'AMD driver not found', // AMD GPU + '', // Sensors (empty due to OpenHardwareMonitor error) + ''' + { + "CimClass": { + "CimSuperClassName": "MSFT_StorageObject", + "CimSuperClass": { + "CimSuperClassName": null, + "CimSuperClass": null, + "CimClassProperties": "ObjectId PassThroughClass PassThroughIds PassThroughNamespace PassThroughServer UniqueId", + "CimClassQualifiers": "Abstract = True locale = 1033", + "CimClassMethods": "", + "CimSystemProperties": "Microsoft.Management.Infrastructure.CimSystemProperties" + }, + "CimClassProperties": [ + "ObjectId", + "PassThroughClass", + "PassThroughIds", + "PassThroughNamespace", + "PassThroughServer", + "UniqueId", + "DeviceId", + "FlushLatencyMax", + "LoadUnloadCycleCount", + "LoadUnloadCycleCountMax", + "ManufactureDate", + "PowerOnHours", + "ReadErrorsCorrected", + "ReadErrorsTotal", + "ReadErrorsUncorrected", + "ReadLatencyMax", + "StartStopCycleCount", + "StartStopCycleCountMax", + "Temperature", + "TemperatureMax", + "Wear", + "WriteErrorsCorrected", + "WriteErrorsTotal", + "WriteErrorsUncorrected", + "WriteLatencyMax" + ] + }, + "Temperature": 46, + "TemperatureMax": 70, + "Wear": 0, + "ReadLatencyMax": 1930, + "WriteLatencyMax": 1903, + "FlushLatencyMax": 262 + } + ''', // Disk SMART data + '12th Gen Intel(R) Core(TM) i5-12490F', // CPU brand +];