Compare commits

...

311 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
fc8e9b4bb1 bump: v1241 2025-09-03 09:24:33 +08:00
lollipopkit🏳️‍⚧️
ec4b633889 fix: watchOS app cfg (#890) 2025-09-03 01:41:08 +08:00
lollipopkit🏳️‍⚧️
e51804fa70 new: custom tabs (#889) 2025-09-03 01:05:03 +08:00
lollipopkit🏳️‍⚧️
2466341999 feat: server conn statistics (#888) 2025-09-02 19:41:56 +08:00
lollipopkit🏳️‍⚧️
929061213f refactor: docker status parsing (#886) 2025-09-02 13:22:54 +08:00
lollipopkit🏳️‍⚧️
6b52679942 fix: resolve Docker interface blank issue caused by LateInitializationError (#884) 2025-09-02 12:44:05 +08:00
lollipopkit🏳️‍⚧️
efc0315c93 new: CLAUDE.md 2025-09-01 23:32:44 +08:00
lollipopkit🏳️‍⚧️
8e4c2a7cde fix: fallback to df on incompatible system (#880) 2025-09-01 23:32:20 +08:00
lollipopkit🏳️‍⚧️
4ec7f5895e fix: imported servers from ssh config are the same (#882) 2025-09-01 23:06:58 +08:00
lollipopkit🏳️‍⚧️
ee22cdb55f fix: private key can't be selected in edit page (#879) 2025-09-01 13:05:54 +08:00
lollipopkit🏳️‍⚧️
b1b0d9a18f bump: v1231 2025-09-01 01:19:23 +08:00
lollipopkit🏳️‍⚧️
56e67f4725 fix: sync will refresh the entire app (#877) 2025-09-01 01:18:06 +08:00
lollipopkit🏳️‍⚧️
3b7fdf36fb opt. 2025-08-31 23:59:53 +08:00
lollipopkit🏳️‍⚧️
5291d316a2 fix: ensure unique IDs for bulk server import to prevent overwriting (#875) 2025-08-31 21:20:27 +08:00
lollipopkit🏳️‍⚧️
4c369546da fix: replace String.fromCharCodes with utf8.decode for proper Chinese character handling in JSON import (#874) 2025-08-31 20:06:47 +08:00
lollipopkit🏳️‍⚧️
12a243d139 feat: import servers from ~/.ssh/config (#873) 2025-08-31 19:33:29 +08:00
lollipopkit🏳️‍⚧️
a97b3cf43e opt.: bak pwd is optional (#872) 2025-08-31 11:11:47 +08:00
lollipopkit🏳️‍⚧️
53a7c0d8ff migrate: riverpod + freezed (#870) 2025-08-31 00:55:54 +08:00
lollipopkit🏳️‍⚧️
9cb705f8dd fix: parsing hostname (#865) 2025-08-22 09:18:21 +08:00
lollipopkit🏳️‍⚧️
8270674b7d chore: tests 2025-08-22 00:25:26 +08:00
lxdklp
24fd4b782d fix: GBK decoding fallback (#863)
* fix #757

* fix #757

* apply the code recommendations from sourcery ai

* Make sure raw is non-empty data

* Modified the way to judge gbk, fixed the problem that null cannot throw an error
2025-08-21 23:28:06 +08:00
lollipopkit🏳️‍⚧️
fcb3d7e2b3 bump: v1220 2025-08-18 12:21:52 +08:00
lollipopkit🏳️‍⚧️
f5634d6e88 bump: v1218 2025-08-18 12:08:29 +08:00
lollipopkit🏳️‍⚧️
5497ad83e0 opt.: bio auth settings 2025-08-17 17:56:26 +08:00
dsvf
4a7827f41a Delay bio auth (#642) 2025-08-17 14:06:24 +08:00
lollipopkit🏳️‍⚧️
60671fe461 feat: native widget url settings dialog (#856) 2025-08-16 23:07:19 +08:00
lollipopkit🏳️‍⚧️
bc1b6e5a4a feat: GitHub Gist sync (#854) 2025-08-14 23:21:33 +08:00
lollipopkit🏳️‍⚧️
1d553eccd5 rm: claude pr review 2025-08-13 23:33:03 +08:00
lollipopkit🏳️‍⚧️
68734a9e52 fix: disable command menu doesnt work (#852) 2025-08-13 23:32:22 +08:00
lollipopkit🏳️‍⚧️
ed8a1d18b9 opt.: systemd page (#851) 2025-08-13 22:16:55 +08:00
shamnad-sherief
e4a9875620 fix: Systemd shows nothing (#850) 2025-08-13 20:19:28 +08:00
lollipopkit🏳️‍⚧️
6f9aa2ece9 add: Claude Code GitHub Workflow (#849)
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"
2025-08-13 15:23:30 +08:00
lollipopkit🏳️‍⚧️
13e28675af opt.: watchOS & iOS widget (#847) 2025-08-13 01:44:02 +08:00
lollipopkit🏳️‍⚧️
8c0e0f89d5 fix: term opening on Linux (#845) 2025-08-12 23:55:31 +08:00
lollipopkit🏳️‍⚧️
9b01da5a23 feat: term session mgr (#846) 2025-08-12 23:43:42 +08:00
lollipopkit🏳️‍⚧️
584af5423a bump: v1206 2025-08-09 12:45:45 +08:00
lollipopkit🏳️‍⚧️
95f8e571c1 feat: ability to disable monitoring cmds (#840) 2025-08-09 12:37:30 +08:00
lollipopkit🏳️‍⚧️
9c9648656d fix: macOS ssh term unusable (#838) 2025-08-08 18:59:25 +08:00
lollipopkit🏳️‍⚧️
6880bcc192 opt.: m3 layout breakpoints (#837) 2025-08-08 17:12:13 +08:00
lollipopkit🏳️‍⚧️
3a615449e3 feat: Windows compatibility (#836)
* feat: win compatibility

* fix

* fix: uptime parse

* opt.: linux uptime accuracy

* fix: windows temperature fetching

* opt.

* opt.: powershell exec

* refactor: address PR review feedback and improve code quality

### Major Improvements:
- **Refactored Windows status parsing**: Broke down large `_getWindowsStatus` method into 13 smaller, focused helper methods for better maintainability and readability
- **Extracted system detection logic**: Created dedicated `SystemDetector` helper class to separate OS detection concerns from ServerProvider
- **Improved concurrency handling**: Implemented proper synchronization for server updates using Future-based locks to prevent race conditions

### Bug Fixes:
- **Fixed CPU percentage parsing**: Removed incorrect '*100' multiplication in BSD CPU parsing (values were already percentages)
- **Enhanced memory parsing**: Added validation and error handling to BSD memory fallback parsing with proper logging
- **Improved uptime parsing**: Added support for multiple Windows date formats and robust error handling with validation
- **Fixed division by zero**: Added safety checks in Swap.usedPercent getter

### Code Quality Enhancements:
- **Added comprehensive documentation**: Documented Windows CPU counter limitations and approach
- **Strengthened error handling**: Added detailed logging and validation throughout parsing methods
- **Improved robustness**: Enhanced BSD CPU parsing with percentage validation and warnings
- **Better separation of concerns**: Each parsing method now has single responsibility

### Files Changed:
- `lib/data/helper/system_detector.dart` (new): System detection helper
- `lib/data/model/server/cpu.dart`: Fixed percentage parsing and added validation
- `lib/data/model/server/memory.dart`: Enhanced fallback parsing and division-by-zero protection
- `lib/data/model/server/server_status_update_req.dart`: Refactored into 13 focused parsing methods
- `lib/data/provider/server.dart`: Improved synchronization and extracted system detection

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: parse & shell fn struct

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-08 16:56:36 +08:00
lollipopkit🏳️‍⚧️
46a12bc844 bump: v1201 2025-07-28 22:27:56 +08:00
lollipopkit🏳️‍⚧️
8d597294a4 feat: amd gpu (#831) 2025-07-28 22:26:29 +08:00
lollipopkit🏳️‍⚧️
682a6e4f2d feat: custom pwd of bak (#827) 2025-07-25 16:38:28 +08:00
lollipopkit🏳️‍⚧️
8c3302cf0d chore: update script location in Attention notice (#825)
Fixes #824
2025-07-21 16:42:16 +08:00
lollipopkit🏳️‍⚧️
ec4bf3df24 opt.: sftp dl 2025-07-21 16:20:27 +08:00
lollipopkit🏳️‍⚧️
263d4eabb4 feat: store critical data in secure store (#821) 2025-07-17 18:26:34 +08:00
lollipopkit🏳️‍⚧️
c6439673b8 feat: shift key in ssh term (#819) 2025-07-17 18:18:18 +08:00
lollipopkit🏳️‍⚧️
a35d21981b opt.: watch sync mechanism (#817)
* opt.: watch sync mechanism
Fixes #816

* opt.
2025-07-17 16:55:56 +08:00
Tom
dbc873c0c0 feat: enhance server card layout and add logo display functionality (#804) 2025-06-27 18:55:48 +08:00
Integral
e69808a2f6 fix: disable APK signing block to resolve F-Droid build issues (#793)
Thanks for the patch from @linsui.
2025-06-16 01:51:30 +08:00
lollipopkit🏳️‍⚧️
55b3ba63ec opt.: ui 2025-06-12 22:04:03 +08:00
ИEØ_ΙΙØZ
006e66d825 update: app_zh_tw.arb (#790) 2025-06-11 17:07:22 +08:00
lollipopkit🏳️‍⚧️
c556c0f1b5 bump: v1189 2025-06-11 17:02:47 +08:00
lollipopkit🏳️‍⚧️
c42c701ffc bug: incorrect disk smart info (#789) 2025-06-11 16:45:25 +08:00
lollipopkit🏳️‍⚧️
e6db2db320 fix: container not working (#787) 2025-06-11 14:53:43 +08:00
lollipopkit🏳️‍⚧️
66ecb02d9e migrate: freezed v3 2025-06-10 14:27:27 +08:00
lollipopkit🏳️‍⚧️
8e7de604ee fix: linux build 2025-06-09 18:50:01 +08:00
lollipopkit🏳️‍⚧️
6f2a58ce18 bump: v1184 2025-06-09 16:01:39 +08:00
lollipopkit🏳️‍⚧️
066629d7e0 fix: android build 2025-06-08 20:59:58 +08:00
lollipopkit🏳️‍⚧️
4b3953e0d2 readd: serverTabPreferDiskAmount (#780)
Fixes #643
2025-06-08 11:15:54 +08:00
lollipopkit🏳️‍⚧️
b5aec55106 fix android reload when physical keyboard changes (#779) 2025-06-08 10:46:47 +08:00
lollipopkit🏳️‍⚧️
ba686db847 fix: ssh terminal ui 2025-06-07 17:18:42 +08:00
lollipopkit🏳️‍⚧️
4d52023982 opt.: ssh terminal ux (#778) 2025-06-07 17:07:13 +08:00
lollipopkit🏳️‍⚧️
7a71a96442 fix: examples UI of importing (#777)
Fixes #601
2025-06-05 09:22:54 +08:00
lollipopkit🏳️‍⚧️
79c515c903 new: bio_auth -> local_auth (#776)
Fixes #722
2025-06-05 09:07:28 +08:00
lollipopkit🏳️‍⚧️
4701757857 feat: SSH page background (#775) 2025-06-05 08:53:00 +08:00
lollipopkit🏳️‍⚧️
176cb7da03 feat: disk smart info (#773) 2025-06-05 07:31:45 +08:00
lollipopkit🏳️‍⚧️
741a6442e0 fix: batch delete servers (#772) 2025-06-04 19:28:58 +08:00
lollipopkit🏳️‍⚧️
f6d394c71e opt.: custom terminal emulator (#771) 2025-06-04 19:13:31 +08:00
lollipopkit🏳️‍⚧️
7127c960f7 opt.: server detail page columns 2025-06-04 17:29:03 +08:00
lollipopkit🏳️‍⚧️
1084c49a5f opt.: ui 2025-06-04 01:52:27 +08:00
lollipopkit🏳️‍⚧️
bc824691e0 opt.: server card loading UI 2025-06-04 00:47:18 +08:00
lollipopkit🏳️‍⚧️
0c1ada0067 fix: cloud sync (#769) 2025-06-04 00:11:31 +08:00
lollipopkit🏳️‍⚧️
9547d92ac5 migrate: flutter 3.32 2025-05-25 17:05:46 +08:00
lollipopkit🏳️‍⚧️
7e16d2f159 new: parse disk info via lsblk output Fixes #709 (#760) 2025-05-17 00:45:38 +08:00
lollipopkit🏳️‍⚧️
d88e97e699 new: use generated ids for servers (#765)
* new: use generated ids for servers
Fixes #743

* fix: deps.

* fix: migrate related settings

* fix: restore servers from json
2025-05-16 21:50:44 +08:00
lollipopkit🏳️‍⚧️
d29bd1d806 opt.: ssh page sftp path checking (#763) 2025-05-15 21:01:10 +08:00
lollipopkit🏳️‍⚧️
2b2f1ddb60 opt.: handle esc btn in ssh page (#761) 2025-05-15 20:35:45 +08:00
lollipopkit🏳️‍⚧️
4f16d510c8 fix: code editor page popping (#759)
Fixes #713
2025-05-14 18:58:58 +08:00
lollipopkit🏳️‍⚧️
94cded39a6 fix: horizontal ssh virt keys ui (#758)
Fixes #737
2025-05-14 18:21:18 +08:00
lollipopkit🏳️‍⚧️
12082e1235 chore: README 2025-05-14 16:54:28 +08:00
lollipopkit🏳️‍⚧️
28e34e2183 opt.: editor lang parse 2025-05-14 16:19:26 +08:00
lollipopkit🏳️‍⚧️
4d45d01074 feat: searching in editor page (#756)
* feat: searching in editor page
Fixes #734

* opt.: editor searching ui
2025-05-14 05:09:32 +08:00
lollipopkit🏳️‍⚧️
f6b3ec2a62 opt.: editor (#755)
Fixes #753
2025-05-14 04:07:21 +08:00
lollipopkit🏳️‍⚧️
d6cf33fb70 bug: can't select file on macOS (#754)
Fixes #750
2025-05-14 04:04:19 +08:00
lollipopkit🏳️‍⚧️
1eea133b69 opt.: appbar scrolledUnderElevation (#752)
Fixes #751
2025-05-14 04:02:30 +08:00
lollipopkit🏳️‍⚧️
2b46cb6dcc fix: android monochrome icon (#749)
Fixes #732
2025-05-14 03:22:02 +08:00
lollipopkit🏳️‍⚧️
8627ff823f optimization: desktop UI (#747) 2025-05-13 04:57:37 +08:00
lollipopkit🏳️‍⚧️
e520929411 chore: migrate fl_lib 2025-04-28 23:15:54 +08:00
lollipopkit🏳️‍⚧️
8f09085cf3 Merge remote-tracking branch 'origin/lollipopkit/issue727' 2025-04-25 18:33:36 +08:00
lollipopkit🏳️‍⚧️
9e66071cb0 opt. 2025-04-25 18:32:29 +08:00
Noo6
fa90c1ef31 opt: navigation bar (#740) 2025-04-22 11:54:29 +08:00
Noo6
ede238c647 feat: adaptive navigation bar (#739) 2025-04-22 11:19:19 +08:00
lollipopkit🏳️‍⚧️
6e7fee20b8 opt.: routes 2025-04-10 15:28:47 +08:00
lollipopkit🏳️‍⚧️
391e4f6b65 opt.: page struct 2025-04-09 12:15:42 +08:00
lollipopkit🏳️‍⚧️
e185414355 fix: logo url dist null check 2025-04-08 14:49:26 +08:00
lollipopkit🏳️‍⚧️
2a2f348063 migrate: fl_lib 2025-03-24 23:07:52 +08:00
moli765
95ca6bcfc9 reslove issue 717 about logo url and add coreelec support (#718) 2025-03-22 23:19:54 +08:00
lollipopkit🏳️‍⚧️
275041247a migrate: webdav_client_plus (#729)
Fixes #723
2025-03-22 01:27:17 +08:00
lollipopkit🏳️‍⚧️
24d64b835d opt.: app bar
Fixes #727
2025-03-20 20:20:13 +08:00
lollipopkit🏳️‍⚧️
dd5fea09b1 opt.: skip updating home widget on desktop 2025-03-13 16:07:45 +08:00
𝗛𝗼𝗹𝗶
0a404e035e Improve Turkish Language (#721) 2025-03-10 15:21:17 +08:00
Noo6
b5ab5b1cab fix: window title bar might not be displayed (#701) 2025-02-12 13:33:49 +08:00
Noo6
5cb83001c6 opt: windows app icon (#700) 2025-02-12 13:17:54 +08:00
Noo6
20a39f0292 feat: record window position (#692) 2025-02-05 20:59:04 +08:00
Noo6
900686f955 chore: Fns & FnRes (#690) 2025-02-04 22:41:03 +08:00
Calvin lin
a10321e3de Update release.yml
It may work now :)
2025-02-03 16:22:18 +08:00
Calvin lin
0691ab2213 Update release.yml 2025-02-03 16:16:45 +08:00
Calvin lin
ef05203ea3 Update release.yml 2025-02-03 16:08:31 +08:00
Calvin lin
28410707a8 Update release.yml 2025-02-03 16:03:49 +08:00
Calvin lin
06b966caa8 Merge pull request #687 from lollipopkit/fix_action_linux
fix github action build linux
2025-02-03 16:01:36 +08:00
calvin
11b0806083 fix github action build linux 2025-02-03 15:48:19 +08:00
lollipopkit🏳️‍⚧️
749fd4d800 fix: ci 2025-01-29 23:56:27 +08:00
lollipopkit🏳️‍⚧️
bec4a3b314 chore: bump version 2025-01-29 23:44:44 +08:00
lollipopkit🏳️‍⚧️
9e5babec76 opt.: close after saving (#684) 2025-01-29 15:10:50 +08:00
lollipopkit🏳️‍⚧️
dbbb10364b fix: webdav settings (#683) 2025-01-29 13:13:12 +08:00
lollipopkit🏳️‍⚧️
16948c3e0f new: provide .deb & .rpm 2025-01-29 12:56:03 +08:00
lollipopkit🏳️‍⚧️
e39fb23b66 bug: unix perm switcher (#674) 2025-01-14 11:19:47 +08:00
lollipopkit🏳️‍⚧️
4777166dd9 migrate: fl_lib v235 2025-01-13 21:57:12 +08:00
Noo6
0ae0241800 opt: window title bar (#672)
* opt: window title bar

* rm: `VirtualWindowFrame` on `SettingsPage`
2025-01-10 15:19:03 +08:00
lollipopkit🏳️‍⚧️
e7a5f43cc4 fix: disabled android service related (#670)
Fixes #662
2025-01-07 20:36:12 +08:00
lollipopkit🏳️‍⚧️
7f58237589 fix: catch crash of fg service (#669) 2025-01-04 16:22:20 +08:00
lollipopkit🏳️‍⚧️
0bbd0b43b3 opt.: display settings btn in fullscreen mode (#660) 2024-12-15 23:53:31 +08:00
lollipopkit🏳️‍⚧️
aaa1eddeaf opt.: display err if home widget fails (#659) 2024-12-15 23:39:38 +08:00
lollipopkit🏳️‍⚧️
2f6db2961f fix: crash while opening terminal (#658)
Fixes #639
2024-12-14 21:06:37 +08:00
lollipopkit
831efa833b fix: file_picker err 2024-12-14 16:21:25 +08:00
lollipopkit
867fcbfc0d chore: migrate to flutter 3.27 2024-12-14 14:45:04 +08:00
lollipopkit
41886be649 opt.: home top bar 2024-12-14 14:11:26 +08:00
lollipopkit
029b4e0dba chore: README 2024-12-03 00:13:58 +08:00
lollipopkit🏳️‍⚧️
3a3c29764a bug: can't share server via qr_code (#651)
Fixes #650
2024-12-02 22:22:14 +08:00
lollipopkit
4ace4af7da opt.: home ui
- new: top left settings btn
- opt.: top logo
2024-12-02 21:41:17 +08:00
lollipopkit🏳️‍⚧️
ddd32e82d4 opt.: migrate to new fl_lib (#649)
Fixes #648
2024-12-02 21:06:44 +08:00
dsvf
b882baeafa Added Function keys (F1-F12) to SSH virtual keyboard options (#641) 2024-11-24 13:19:20 +08:00
fei1025
046f2c06d0 修复路径在windos下读取不到的问题 (#630)
* 修复路径在windos下读取不到的问题

* opt: `local.dart` fmt

---------

Co-authored-by: Noo6 <72285529+No06@users.noreply.github.com>
2024-11-14 14:42:15 +08:00
Noo6
d706886343 fix: sftp open file on windows (#633) 2024-11-14 14:24:57 +08:00
Noo6
7dda63af8a fix: add file on windows 2024-11-14 11:20:45 +08:00
lollipopkit🏳️‍⚧️
00d303ac36 fix: editing pref store (#618) 2024-10-29 19:37:19 +08:00
lollipopkit🏳️‍⚧️
229983d82e chore: bump version 2024-10-17 14:06:45 +08:00
Mased
4928ca600d [Translation] some Russian fixes (#604) 2024-10-07 21:04:36 +08:00
lollipopkit🏳️‍⚧️
89ec2d94d6 opt.: virt keys loading 2024-10-05 10:18:00 +08:00
lollipopkit🏳️‍⚧️
393c3e6388 Merge branch 'main' of github.com:lollipopkit/flutter_server_box 2024-10-05 10:00:41 +08:00
Gitro
dee458e926 new: material you icon (#599) 2024-10-04 12:19:10 +08:00
lollipopkit🏳️‍⚧️
f89228db40 opt.: ssh page 2024-09-28 17:09:35 +08:00
lollipopkit🏳️‍⚧️
3b6fb6194b opt.: more virtual keys (#596) 2024-09-28 14:40:22 +08:00
lollipopkit🏳️‍⚧️
02444fc2f0 new: ios18 dark app icon (#594)
Fixes #593
2024-09-25 19:16:14 +08:00
lollipopkit🏳️‍⚧️
aef317a140 opt.: dismiss notification if no ssh conn (#592) 2024-09-24 22:01:35 +08:00
lollipopkit🏳️‍⚧️
47aedb2f2e fix: rename file 2024-09-22 16:06:09 +08:00
lollipopkit🏳️‍⚧️
eab06abcaf opt. 2024-09-21 23:12:15 +08:00
lollipopkit🏳️‍⚧️
c062c12a0e opt.: redesigned settings page (#587) 2024-09-21 22:37:42 +08:00
lollipopkit🏳️‍⚧️
d7669c94b8 opt.: spi with same id (#585) 2024-09-21 11:54:56 +08:00
lollipopkit🏳️‍⚧️
50af289574 migrate: fl_lib 2024-09-21 11:01:41 +08:00
lollipopkit🏳️‍⚧️
90b88ed795 opt.: sync immediately after changes (#577) 2024-09-14 17:08:51 +08:00
CakesTwix
d611fdcd50 l10n: Added Ukrainian lang (#574) 2024-09-14 14:48:23 +08:00
lollipopkit🏳️‍⚧️
db9b2dd818 fix: snippet fmt (#570) 2024-09-01 21:07:32 +08:00
lollipopkit🏳️‍⚧️
edb49ead67 opt.: split webdav & other settings (#569) 2024-08-31 21:45:09 +08:00
lollipopkit🏳️‍⚧️
7f0dc656b8 Merge pull request #568 from lollipopkit/lollipopkit/issue564 2024-08-31 19:36:50 +08:00
lollipopkit🏳️‍⚧️
b33d0bbc3e opt.: back btn on scan page
Fixes #564
2024-08-31 19:33:57 +08:00
lollipopkit🏳️‍⚧️
7d0ea8a58b Merge branch 'android_service' 2024-08-31 19:28:04 +08:00
Noo6
c18732d8f3 fix: backup on windows (#563) 2024-08-30 22:44:08 +08:00
Shin
157af0a354 l10n: fixed Japanese translations. (#558) 2024-08-30 11:35:13 +08:00
lollipopkit🏳️‍⚧️
2d9dc044f9 new: custom foreground service on Android (#556) 2024-08-28 16:42:09 +08:00
lollipopkit🏳️‍⚧️
479250c207 init 2024-08-28 13:44:54 +08:00
Noo6
aef7ec911f opt: ssh tab page 2024-08-28 13:18:21 +08:00
lollipopkit🏳️‍⚧️
4f9ee7781f fix: ssh tab page UI (#555) 2024-08-27 17:20:47 +08:00
lollipopkit🏳️‍⚧️
eb83d05c81 fix: ssh alter url (#554) 2024-08-27 15:22:26 +08:00
lollipopkit🏳️‍⚧️
329fd33b69 fix: lang switch (#553) 2024-08-27 14:36:00 +08:00
lollipopkit🏳️‍⚧️
931c5f0bf6 opt.: alterUrl (#550) 2024-08-25 22:52:47 +08:00
lollipopkit🏳️‍⚧️
bcbf1fbc17 fix: fdroid build (#548) 2024-08-25 22:06:55 +08:00
Noo6
3e7315dac6 fix: ssh page displays the CustomAppBar on desktop 2024-08-18 20:04:39 +08:00
Noo6
4cecfdf7a8 opt: ssh card text overflow 2024-08-18 13:57:20 +08:00
Noo6
0346821cf5 opt: windows and linux drag area 2024-08-18 13:27:16 +08:00
lollipopkit🏳️‍⚧️
966a60a82d migrate: appleos 2024-08-17 22:59:32 +08:00
lollipopkit🏳️‍⚧️
76e98c6468 feat: custom shell script install path (#545) 2024-08-17 22:44:35 +08:00
lollipopkit🏳️‍⚧️
d7ae8b75b8 feat: custom net dev (#543) 2024-08-17 21:57:39 +08:00
lollipopkit🏳️‍⚧️
b5329e2692 fix: tags dialog 2024-08-16 21:02:05 +08:00
lollipopkit🏳️‍⚧️
ef297673f3 fix: privatekey update actually creates a new key (#541)
Fixes #540
2024-08-16 21:00:34 +08:00
lollipopkit🏳️‍⚧️
7558b4806d opt.: TagSwitcher related 2024-08-16 19:09:54 +08:00
Noo6
f7ef8a3915 opt: hidden at launch on linux and macos 2024-08-16 16:19:58 +08:00
lollipopkit🏳️‍⚧️
38366a2ef3 refactors (#539) 2024-08-16 01:24:43 +08:00
lollipopkit🏳️‍⚧️
7e5bb54c98 opt.: hide logo if distribution == null (#536) 2024-08-15 18:02:31 +08:00
lollipopkit🏳️‍⚧️
7ce3854392 opt.: use json_serializable 2024-08-15 16:44:13 +08:00
lollipopkit🏳️‍⚧️
195ddd2bcc refactor: SSHClientX.exec 2024-08-15 11:27:22 +08:00
lollipopkit🏳️‍⚧️
267b0b0a69 opt.: sftp home & back (#533) 2024-08-14 19:01:44 +08:00
lollipopkit🏳️‍⚧️
41e3fcb23a feat: systemd management (#532) 2024-08-14 14:29:03 +08:00
lollipopkit🏳️‍⚧️
46d5840276 feat: share server via qrcode (#530) 2024-08-13 20:04:01 +08:00
lollipopkit🏳️‍⚧️
fe566e97ca chore: README 2024-08-11 23:29:11 +08:00
lollipopkit🏳️‍⚧️
ddd1524d63 opt.: macos icon (#527) 2024-08-11 22:40:11 +08:00
lollipopkit🏳️‍⚧️
4d8268c614 fix: ssh tab focus mgmt (#525)
Fixes #522
2024-08-11 22:36:52 +08:00
lollipopkit🏳️‍⚧️
568b97606a opt.: split single list into multiples on desktop (#524) 2024-08-11 20:53:25 +08:00
lollipopkit🏳️‍⚧️
42cc2416a1 chore: README 2024-08-09 11:59:45 +08:00
lollipopkit🏳️‍⚧️
aaa69f0f95 chore: bump version 2024-08-04 19:26:31 +08:00
lollipopkit🏳️‍⚧️
64676bc5cb chore: l10n 2024-08-04 13:05:29 +08:00
lollipopkit🏳️‍⚧️
a15c04956c opt.: TipText 2024-08-04 12:02:57 +08:00
lollipopkit🏳️‍⚧️
e3c885483b opt.: use ssh term to decompress (#519) 2024-08-04 11:40:38 +08:00
lollipopkit🏳️‍⚧️
493c86cacb fix: home widget url (#517) 2024-08-04 00:17:21 +08:00
lollipopkit🏳️‍⚧️
ea7c8caf14 bug: color seed setting not working (#516) 2024-08-03 23:17:18 +08:00
lollipopkit🏳️‍⚧️
9db04a60c2 opt.: l10n & fix: write script (#514) 2024-08-03 22:44:21 +08:00
lollipopkit🏳️‍⚧️
610f46da0d opt.: bulk import servers (#512) 2024-08-03 14:52:39 +08:00
lollipopkit🏳️‍⚧️
b8e5418ff2 feat: import snippets from network (#510)
Fixes #507
2024-08-03 14:25:58 +08:00
lollipopkit🏳️‍⚧️
0e21755acb opt.: remove internal SharePreference keys while using KvEditor (#509)
Fixes #508
2024-08-03 12:37:16 +08:00
lollipopkit🏳️‍⚧️
73248011a1 migrate: fl_lib 2024-08-01 13:44:32 +08:00
lollipopkit🏳️‍⚧️
969643d3df opt.: debug page copy logs 2024-07-28 22:12:07 +08:00
lollipopkit🏳️‍⚧️
c90d0e4b3b fix: manual restore 2024-07-28 20:37:34 +08:00
lollipopkit🏳️‍⚧️
f9aadc6b0f opt.: Btn 2024-07-28 20:26:08 +08:00
lollipopkit🏳️‍⚧️
8fd4cc1fe1 opt.: TagsEditor & Btn 2024-07-28 19:05:31 +08:00
lollipopkit🏳️‍⚧️
432d76f024 fix: builtin editor (#503) 2024-07-28 15:06:34 +08:00
𝗦𝗵𝗟𝗲𝗿𝗣
ca8211e1a4 Add TR Locales (#497) 2024-07-27 16:42:09 +08:00
lollipopkit🏳️‍⚧️
a3b48fc01c chore: bump version 2024-07-26 23:35:59 +08:00
lollipopkit🏳️‍⚧️
8be94aa09c feat: use $EDITOR to edit files (#496)
Fixes #489
2024-07-26 23:32:57 +08:00
lollipopkit🏳️‍⚧️
5db1253ab8 fix: termux compatibility (#495) 2024-07-26 22:31:17 +08:00
lollipopkit🏳️‍⚧️
ceedd86310 rm: pkg (#494)
Fixes #470
2024-07-26 21:31:45 +08:00
lollipopkit🏳️‍⚧️
6a0254623f opt.: json input experience 2024-07-26 20:22:30 +08:00
lollipopkit🏳️‍⚧️
1c6ec56032 Create FUNDING.yml 2024-07-25 10:42:18 +08:00
lollipopkit🏳️‍⚧️
287869ed45 opt.: simplify settings page (#488) 2024-07-24 00:30:17 +08:00
lollipopkit🏳️‍⚧️
e4dbc3ba12 feat: set envs in term (#485) 2024-07-23 21:34:34 +08:00
lollipopkit🏳️‍⚧️
426e5689f8 fix: uploaded file's path on windows (#484) 2024-07-23 19:59:58 +08:00
lollipopkit🏳️‍⚧️
afda5fd4a4 fix: bio auth (#482) 2024-07-23 19:26:03 +08:00
lollipopkit🏳️‍⚧️
0a21b2820c fix: letterCacheTip translation 2024-07-23 12:14:31 +08:00
lollipopkit🏳️‍⚧️
87b3b76b0b feat: display cpu model (#477) 2024-07-23 12:03:10 +08:00
lollipopkit🏳️‍⚧️
41ec46f1d3 opt.: show loading dialog 2024-07-22 23:33:21 +08:00
lollipopkit🏳️‍⚧️
7a359588db opt.: input field suggestion 2024-07-22 22:03:56 +08:00
lollipopkit🏳️‍⚧️
255abe8b11 rollback: write script to /dev/shm (#474) 2024-07-21 17:58:14 +08:00
lollipopkit🏳️‍⚧️
b0936c5e6e fix: termux compatibility (#472) 2024-07-20 20:35:30 +08:00
lollipopkit🏳️‍⚧️
2907ac74d4 chore: bump version 2024-07-18 23:44:01 +08:00
lollipopkit🏳️‍⚧️
ea678f37b0 chore: bump version 2024-07-18 23:15:40 +08:00
lollipopkit🏳️‍⚧️
076082c945 feat: sftp perm setting & path copy (#467) 2024-07-18 21:40:40 +08:00
lollipopkit🏳️‍⚧️
5ee98f90e8 fix: update changelog (#466) 2024-07-18 20:53:22 +08:00
lollipopkit🏳️‍⚧️
c988dd88d7 fix: linux duplicated title bar (#462)
Fixes #459
2024-07-15 17:38:30 +08:00
lollipopkit🏳️‍⚧️
f7d6c461dc fix: version display (#458)
Fixes #457
2024-07-10 15:12:05 +08:00
lollipopkit🏳️‍⚧️
14771ae946 chore: README 2024-07-09 14:36:04 +08:00
lollipopkit🏳️‍⚧️
7e9086b20e fix: chmod perm 2024-07-09 14:02:23 +08:00
Noo6
4b3c4870ba fix: desktop window appbar (#450) 2024-07-05 12:42:38 +08:00
Noo6
43cebd0c04 fix: window blink on startup (#447) 2024-07-04 12:23:20 +08:00
Noo6
5ce13109b0 fix: ssh card tap area (#448) 2024-07-04 12:22:19 +08:00
lollipopkit🏳️‍⚧️
6e428c91d1 feat: disable letter cache (#446)
Fixes #445
2024-07-03 19:55:33 +08:00
lollipopkit🏳️‍⚧️
4430045550 feat: write script into /dev/shm (#444)
Fixes #443
2024-07-03 19:14:27 +08:00
lollipopkit🏳️‍⚧️
282cb06091 fix: picker dialog (#442)
Fixes #440
2024-07-03 18:19:09 +08:00
lollipopkit🏳️‍⚧️
772c2743b5 fix: no appBar in server detail page (#441)
Fixes #435
2024-07-03 16:42:52 +08:00
lollipopkit🏳️‍⚧️
90199b89a5 chore: README 2024-07-03 00:52:28 +08:00
Integral
e1d2e3f3e5 l10n: fix remaining translations (#439)
* l10n: Url -> URL

* l10n: Logo Address -> Logo URL

* l10n: fix ttl translation

* l10n: fix all zh-tw translations

* l10n: fix all zh-cn translations

* l10n: fix all en translations
2024-07-02 15:02:49 +00:00
Integral
3f9fe1f2c6 l10n: alterUrl -> fallbackSshDest (#437) 2024-07-02 13:15:19 +00:00
Integral
c79bbc5756 l10n: fix two acronyms (MAC, URL) (#436)
* l10n: Mac Address -> MAC Address

* l10n: PVE addr -> URL
2024-07-01 14:56:38 +00:00
Integral
63e1bec2b9 l10n: fix translation of net/network (#433) 2024-07-01 07:01:51 +00:00
Integral
d26b7c6f75 l10n: add noLineChartForCpu field (#431) 2024-07-01 05:24:31 +00:00
Integral
da8dc4fa54 docs(README): add f-droid & reorganize other installation methods (#429)
* docs(README): add f-droid & reorganize other installation methods

* docs(README): update Chinese README from English
2024-06-30 11:51:36 +00:00
Integral
413b81c47f l10n: fix Japanese translations (#425) 2024-06-25 16:00:42 +00:00
Integral
9ef419e3df l10n: fix English translations (#424) 2024-06-25 16:00:26 +00:00
Integral
39893912d9 Merge pull request #423 from Integral-Tech/fix-fdroid-typo
chore: fix typo of "F-Droid"
2024-06-25 14:04:16 +00:00
Integral
49456ca6c3 chore: fix typo of "F-Droid" 2024-06-25 22:00:46 +08:00
lollipopkit🏳️‍⚧️
6b9b8f0dbb chore: bump version 2024-06-25 13:15:29 +08:00
lollipopkit🏳️‍⚧️
5eb48b2717 fix: linux build (#422)
Fixes #421
2024-06-25 11:48:56 +08:00
lollipopkit🏳️‍⚧️
5339cfca70 chore: lib vers bump (#420)
Fixes #419
2024-06-25 11:16:34 +08:00
lollipopkit🏳️‍⚧️
1462b2d0b8 fix: always enter intro page (#418)
Fixes #417
2024-06-24 16:32:37 +08:00
lollipopkit🏳️‍⚧️
3798a23183 chore: bump version 2024-06-24 00:56:45 +08:00
lollipopkit🏳️‍⚧️
ddaf916170 feat: intro page (#416)
Fixes #415
2024-06-24 00:43:52 +08:00
lollipopkit🏳️‍⚧️
d6e37b058f fix: docker parse (#414)
Fixes #395
2024-06-23 18:37:05 +08:00
lollipopkit🏳️‍⚧️
2e9ad7d7cb chore: missing l10n noLineChart (#413)
Fixes #412
2024-06-23 15:39:30 +08:00
lollipopkit🏳️‍⚧️
190da74f66 chore: add participants (#410)
Fixes #409
2024-06-23 00:27:26 +08:00
lollipopkit🏳️‍⚧️
f1315dda7f fix: batch delete servers (#408)
Fixes #393
2024-06-23 00:24:30 +08:00
lollipopkit🏳️‍⚧️
43e6105eb3 fix: hideTitleBar doesn't work (#407)
Fixes #406
2024-06-22 22:58:22 +08:00
lollipopkit🏳️‍⚧️
d785209eb6 opt.: share on desktop (#405) 2024-06-22 22:50:17 +08:00
lollipopkit🏳️‍⚧️
da8b6a9010 feat: remember window size (#404)
Fixes #398
2024-06-22 21:52:48 +08:00
lollipopkit🏳️‍⚧️
1fd68722da fix: watchos settings (#403)
Fixes #401
2024-06-22 21:09:05 +08:00
lollipopkit🏳️‍⚧️
c9a2c1d0e4 Android - default to English (#402) 2024-06-18 14:39:18 +08:00
lollipopkit
161f536a62 fix: conform fdroid version fmt
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 23:51:10 +08:00
lollipopkit
1a32e9944e opt.: put version logic somewhere else (#381)
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 22:40:18 +08:00
lollipopkit
6deb753198 fix: version change logic (#381) 2024-06-13 21:29:03 +08:00
lollipopkit
4e33a98631 chore: fl_build 2024-06-13 21:22:44 +08:00
lollipopkit
dcb9464d8f fix
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 20:39:06 +08:00
lollipopkit
b94936b29f fix: reproducible (#381)
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 20:28:22 +08:00
lollipopkit
108d0a5a5b chore: bump version
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 00:45:24 +08:00
Integral
4814a2de28 Merge pull request #391 from Integral-Tech/add-abiCodes
Add abiCodes
2024-06-12 16:21:13 +00:00
Integral
5d8eeff502 Add abiCodes 2024-06-13 00:18:04 +08:00
lollipopkit
0bc4087266 chore: correct version for fdroid 2024-06-13 00:00:20 +08:00
lollipopkit
921209b901 chore: specify flutter version (#381) 2024-06-12 19:51:04 +08:00
lollipopkit
fa9d754470 rm: android wear settings 2024-06-12 19:42:38 +08:00
Integral
1f50a1f0f4 Merge pull request #389 from Integral-Tech/clean-up-proguard-rules
fix: clean up proguard-rules to remove depends on google libraries
2024-06-11 18:03:44 +00:00
Integral
80e84c0421 fix: clean up proguard-rules to remove depends on google libraries 2024-06-12 01:36:29 +08:00
lollipopkit
5059872c3f chore: README 2024-06-11 22:54:48 +08:00
lollipopkit
8add244776 fix: fdroid version fmt 2024-06-11 22:47:48 +08:00
lollipopkit
04e23fd7e4 fix: pubspec version 2024-06-11 22:18:27 +08:00
lollipopkit
336b11b808 chore: change dart pkg id 2024-06-11 22:06:29 +08:00
lollipopkit
8d9dba361c chore: completely rm countly 2024-06-11 22:03:17 +08:00
Integral
64ce3638cb chore: add distributionSha256Sum in gradle (#385) 2024-06-11 18:43:41 +08:00
lollipopkit
f3ceb73f0e fix: rm gms (#379 #381) 2024-06-11 16:40:05 +08:00
Integral
131dbe934c chore: setup fastlane for F-Droid submission (#384) 2024-06-11 16:18:39 +08:00
lollipopkit
58288e89bd Merge branch 'main' of github.com:lollipopkit/flutter_server_box 2024-06-10 21:35:59 +08:00
lollipopkit🏳️‍⚧️
22c43c7124 Lollipopkit/issue382 (#383) 2024-06-10 21:34:56 +08:00
lollipopkit
2bf0b25ee5 fix: podman term 2024-06-10 21:32:45 +08:00
lollipopkit
3bc03c1364 opt.: add tip (#380 #382) 2024-06-10 21:29:41 +08:00
lollipopkit
490d71f8c9 feat: snippet with term ctrl (#380 #382) 2024-06-10 21:08:33 +08:00
lollipopkit
edceb5900e opt.: write script to server tip 2024-06-09 13:57:02 +08:00
lollipopkit
e5ef28415b opt.: ssh tab 2024-06-08 21:39:42 +08:00
lollipopkit
46b98df153 Merge branch 'dev' 2024-06-08 20:58:41 +08:00
lollipopkit
9f34021c90 fix: docker ps parse if id/name is too long 2024-06-08 20:57:56 +08:00
lollipopkit
8121eef839 opt.: RNode 2024-06-08 15:35:19 +08:00
lollipopkit
da48d1f66c opt.: IME popup after opening drawer if ssh term is focusing 2024-06-08 13:53:23 +08:00
lollipopkit
b167287c5b opt.: ssh tab page's tab bar 2024-06-07 23:53:13 +08:00
lollipopkit
41f9da6bf8 fix: ssh tab PageView animteToPage 2024-06-07 21:51:00 +08:00
lollipopkit
e7c7fc8186 fix: ssh tab name generaton 2024-06-07 21:43:38 +08:00
lollipopkit
b950dd2d68 fix: ssh tab 2024-06-07 21:09:17 +08:00
lollipopkit
6d34de14d3 Merge branch 'dev' 2024-06-06 21:26:03 +08:00
lollipopkit
a5a84c0cdd fix: podman log 2024-06-06 18:52:20 +08:00
lollipopkit
701b1b811f feat: beta program 2024-06-06 16:18:10 +08:00
lollipopkit
97267cdfbf fix: docker container status (#374) 2024-06-05 21:58:19 +08:00
lollipopkit
40ce37d230 opt.: sftp del dir 2024-06-05 18:48:44 +08:00
lollipopkit
8a9ade355c fix: update check 2024-06-05 18:26:02 +08:00
lollipopkit
9bffec64b5 fix: wol cfg (#373) 2024-06-05 18:16:17 +08:00
lollipopkit
a03ee2ae0e fix: term help 2024-06-05 11:12:43 +08:00
PaperCube
ee889235fe Fixed UI representation of server reorder page (#372) 2024-06-04 18:57:57 +01:00
lollipopkit
94d6d80497 chore: README 2024-06-04 23:17:32 +08:00
415 changed files with 54385 additions and 15033 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: ['https://cdn.lpkt.cn/donate']

66
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
github.actor == 'lollipopkit' && (
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
# model: "claude-opus-4-1-20250805"
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test

View File

@@ -1,6 +1,7 @@
name: Flutter Release name: Flutter Release
on: on:
workflow_dispatch:
push: push:
tags: tags:
- "v*" - "v*"
@@ -9,33 +10,64 @@ permissions:
contents: write contents: write
jobs: jobs:
releaseAL: releaseAndroid:
name: Release android and linux name: Release android
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: '0'
- name: Install Flutter - name: Install Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.35.1"
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: "zulu"
java-version: '17' java-version: "17"
- name: Fetch secrets - name: Fetch secrets
run: | run: |
curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key
curl -u ${{ secrets.BASIC_AUTH }} -o android/key.properties ${{ secrets.URL_PREFIX }}key.properties curl -u ${{ secrets.BASIC_AUTH }} -o android/key.properties ${{ secrets.URL_PREFIX }}key.properties
- name: Build - name: Build
run: dart run fl_build -p android,linux run: dart run fl_build -p android
- name: Rename for fdroid
run: |
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
releaseLinux:
name: Release linux
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Flutter
uses: subosito/flutter-action@v2
- name: Install dependencies
run: |
sudo apt update
# Basic
sudo apt install -y clang cmake ninja-build pkg-config libgtk-3-dev mesa-utils libvulkan-dev desktop-file-utils wget
# App Specific
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev libsecret-1-dev
- name: Build
run: |
dart run fl_build -p linux
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.apk
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -46,8 +78,6 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: '0'
- name: Install Flutter - name: Install Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
- name: Build - name: Build
@@ -68,6 +98,9 @@ jobs:
# uses: actions/checkout@v4 # uses: actions/checkout@v4
# - name: Install Flutter # - name: Install Flutter
# uses: subosito/flutter-action@v2 # uses: subosito/flutter-action@v2
# with:
# channel: 'stable'
# flutter-version: '3.32.1'
# - name: Build # - name: Build
# run: dart run fl_build -p ios,mac # run: dart run fl_build -p ios,mac
# - name: Create Release # - name: Create Release

5
.gitignore vendored
View File

@@ -46,6 +46,7 @@ app.*.map.json
/android/app/release /android/app/release
/android/app/fjy.androidstudio.key /android/app/fjy.androidstudio.key
/android/app/app.key
/release /release
test.dart test.dart
@@ -57,9 +58,11 @@ test.dart
# Linux release # Linux release
linux.AppDir linux.AppDir
ServerBox-x86_64.AppImage **/*.AppImage
untranlated.json untranlated.json
.vscode/settings.json .vscode/settings.json
more_build_data.json more_build_data.json
trans.txt
android/app/.cxx

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited. # This file should be version controlled and should not be manually edited.
version: version:
revision: "bae5e49bc2a867403c43b2aae2de8f8c33b037e4" revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
channel: "stable" channel: "stable"
project_type: app project_type: app
@@ -13,26 +13,26 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: android - platform: android
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: ios - platform: ios
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: linux - platform: linux
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: macos - platform: macos
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: web - platform: web
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: windows - platform: windows
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
# User provided section # User provided section

4
.vscode/launch.json vendored
View File

@@ -8,6 +8,10 @@
"name": "debug", "name": "debug",
"request": "launch", "request": "launch",
"type": "dart", "type": "dart",
"env": {
// Comment this line to use the default display
"DISPLAY": ":1"
}
// "args": [ // "args": [
// "-v" // "-v"
// ] // ]

95
CLAUDE.md Normal file
View File

@@ -0,0 +1,95 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Development
- `flutter run` - Run the app in development mode
- `dart run fl_build -p PLATFORM` - Build the app for specific platform (see fl_build package)
- `dart run build_runner build --delete-conflicting-outputs` - Generate code for models with annotations (json_serializable, freezed, hive, riverpod)
- Every time you change model files, run this command to regenerate code (Hive adapters, Riverpod providers, etc.)
- Generated files include: `*.g.dart`, `*.freezed.dart` files
### Testing
- `flutter test` - Run unit tests
- `flutter test test/battery_test.dart` - Run specific test file
## Architecture
This is a Flutter application for managing Linux servers with the following key architectural components:
### Project Structure
- `lib/core/` - Core utilities, extensions, and routing
- `lib/data/` - Data layer with models, providers, and storage
- `model/` - Data models organized by feature (server, container, ssh, etc.)
- `provider/` - Riverpod providers for state management
- `store/` - Local storage implementations using Hive
- `lib/view/` - UI layer with pages and widgets
- `lib/generated/` - Generated localization files
- `lib/hive/` - Hive adapters for local storage
### Key Technologies
- **State Management**: Riverpod with code generation (riverpod_annotation)
- **Local Storage**: Hive for persistent data with generated adapters
- **SSH/SFTP**: Custom dartssh2 fork for server connections
- **Terminal**: Custom xterm.dart fork for SSH terminal interface
- **Networking**: dio for HTTP requests
- **Charts**: fl_chart for server status visualization
- **Localization**: Flutter's built-in i18n with ARB files
- **Code Generation**: Uses build_runner with json_serializable, freezed, hive_generator, riverpod_generator
### Data Models
- Server management models in `lib/data/model/server/`
- Container/Docker models in `lib/data/model/container/`
- SSH and SFTP models in respective directories
- Most models use freezed for immutability and json_annotation for serialization
### Features
- Server status monitoring (CPU, memory, disk, network)
- SSH terminal with virtual keyboard
- SFTP file browser
- Docker container management
- Process and systemd service management
- Server snippets and custom commands
- Multi-language support (12+ languages)
- Cross-platform support (iOS, Android, macOS, Linux, Windows)
### State Management Pattern
- Uses Riverpod providers for dependency injection and state management
- Uses Freezed for immutable state models
- Providers are organized by feature in `lib/data/provider/`
- State is often persisted using Hive stores in `lib/data/store/`
### Build System
- Uses custom `fl_build` package for cross-platform building
- `make.dart` script handles pre/post build tasks (metadata generation)
- Supports building for multiple platforms with platform-specific configurations
- Many dependencies are custom forks hosted on GitHub (dartssh2, xterm, fl_lib, etc.)
### Important Notes
- **Never run code formatting commands** - The codebase has specific formatting that should not be changed
- **Always run code generation** after modifying models with annotations (freezed, json_serializable, hive, riverpod)
- Generated files (`*.g.dart`, `*.freezed.dart`) should not be manually edited
- AGAIN, NEVER run code formatting commands.
- USE dependency injection via GetIt for services like Stores, Services and etc.
- Generate all l10n files using `flutter gen-l10n` command after modifying ARB files.
- USE `hive_ce` not `hive` package for Hive integration.
- Which no need to config `HiveField` and `HiveType` manually.
- USE widgets and utilities from `fl_lib` package for common functionalities.
- Such as `CustomAppBar`, `context.showRoundDialog`, `Input`, `Btnx.cancelOk`, etc.
- You can use context7 MCP to search `lppcg fl_lib KEYWORD` to find relevant widgets and utilities.
- USE `libL10n` and `l10n` for localization strings.
- `libL10n` is from `fl_lib` package, and `l10n` is from this project.
- Before adding new strings, check if it already exists in `libL10n`.
- Prioritize using strings from `libL10n` to avoid duplication, even if the meaning is not 100% exact, just use the substitution of `libL10n`.
- Split UI into Widget build, Actions, Utils. use `extension on` to achieve this

View File

@@ -2,68 +2,87 @@ English | [简体中文](README_zh.md)
<h2 align="center">Flutter Server Box</h2> <h2 align="center">Flutter Server Box</h2>
<p align="center"> <div align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink"> <a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a>
<img alt="countly" src="https://img.shields.io/badge/analysis-countly-pink"> <img alt="lang" src="https://img.shields.io/badge/lang-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink"> <img alt="license" src="https://img.shields.io/badge/license-GPLv3-yellow">
</p> <a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
</div>
<p align="center"> <p align="center">
A Flutter project which provide charts to display <a href="../../issues/43">Linux</a> 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.
<br> <br>
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>. Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
</p> </p>
## Download ## 🏙 Screenshots
🎉 **The `Android / Linux / Windows` version are now built via GitHub Actions**
[iOS & macOS](https://apps.apple.com/app/id6476033062) / [Android & Linux & Windows](https://github.com/lollipopkit/flutter_gpt_box/releases) <table>
<tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/2.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
</tr>
</table>
## 📥 Installation
## 🔖 Feature |Platform| From|
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Pkg & Process`... |--|--|
| iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) |
| Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) |
| Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) |
Please only download pkgs from the source that **you trust**!
## 🔖 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`... - Platform specific: `Bio auth``Msg push``Home widget``watchOS App`...
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic); Español, Русский язык, Português, 日本語 (Generated by GPT) - English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT)
## 🏙️ ScreenShots
<table>
<tr>
<td><img width="277px" src="imgs/server.png"></td>
<td><img width="277px" src="imgs/detail.png"></td>
<td><img width="277px" src="imgs/sftp.png"></td>
</tr>
</table>
<table>
<tr>
<td><img width="277px" src="imgs/editor.png"> </td>
<td><img width="277px" src="imgs/ssh.png"></td>
<td><img width="277px" src="imgs/docker.png"></td>
</tr>
</table>
## 🆘 Help ## 🆘 Help
<div align="center">
<a href="https://qm.qq.com/q/daCGa7eShG"><img alt="qq" src="https://img.shields.io/badge/QQ-Group-pink"></a>
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div>
- In order to push server status to your portable device without opening ServerBox app (Such as **message push** and **home widget**), you need to install [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor) on your servers, and config it correctly. See [wiki](https://github.com/lollipopkit/server_box_monitor/wiki) for more details. - In order to push server status to your portable device without opening ServerBox app (Such as **message push** and **home widget**), you need to install [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor) on your servers, and config it correctly. See [wiki](https://github.com/lollipopkit/server_box_monitor/wiki) for more details.
- **Common issues** can be found in [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki). - **Common issues** can be found in [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki).
Before you open an issue, please read the following: Before you open an issue, please read the following:
1. Paste the **entire log** (click the top right of the home page) in the issue template. 1. Paste the **entire log** (click the top right of the home page) in the issue template.
2. Make sure whether the issue is caused by ServerBox app. 2. Make sure whether the issue is caused by ServerBox app.
3. Welcome all valid and positive feedback, subjective feedback (such as you think other UI is better) may not be accepted. 3. Welcome all valid and positive feedback, subjective feedback (such as you think other UI is better) may not be accepted.
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new). After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
## 🧱 Contributions
## 🧱 Contribution Any positive contribution is welcome.
- Any positive contribution is welcome.
- [l10n guide](https://blog.lolli.tech/faq/) can be found in my blog.
If I forgot to add your name to the contributors list, please add a comment in the issue or PR you opened to let me know, I will add it as soon as possible.
### Development
1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.
2. Clone this repo, run `flutter run` to start the app.
3. Run `dart run fl_build -p PLATFORM` to build the app.
### Translation
- [Guide](https://blog.lpkt.cn/posts/faq/) can be found in my blog.
- We need your help! Just feel free to open a PR.
## 💡 My other apps ## 💡 My other apps
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - A third-party GPT Client for OpenAI API on all platforms. - [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - A third-party GPT Client for OpenAI API on all platforms.
- [More](https://github.com/lollipopkit) - Tools & etc. - [More](https://github.com/lollipopkit) - Tools & etc.
## 📝 License ## 📝 License
`GPL v3 lollipopkit` `GPL v3 lollipopkit`

View File

@@ -2,74 +2,88 @@
<h2 align="center">Flutter Server Box</h2> <h2 align="center">Flutter Server Box</h2>
<p align="center"> <div align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink"> <a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a>
<img alt="countly" src="https://img.shields.io/badge/analysis-countly-pink"> <img alt="语言" src="https://img.shields.io/badge/语言-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink"> <img alt="license" src="https://img.shields.io/badge/证书-GPLv3-yellow">
</p> <a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
</div>
<p align="center"> <p align="center">
使用 Flutter 开发的 <a href="../../issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。 使用 Flutter 开发的 Linux, Unix, Windows 服务器工具箱,提供服务器状态图表和管理工具。
<br> <br>
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a> 特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>
</p> </p>
## 🏙️ 截屏
## ⬇️ Download <table>
🎉 **现在 `Android / Linux / Windows` 版本使用 GitHub Actions 构建** <tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/2.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
</tr>
</table>
[iOS & macOS](https://apps.apple.com/app/id1586449703) / [Android & Linux & Windows](https://github.com/lollipopkit/flutter_gpt_box/releases) ## 📥 安装
平台|下载
--|--
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid)
请从 **信任** 的来源下载!
## 🔖 特点 ## 🔖 特点
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 包 & 进程` 管理器...
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理,`S.M.A.R.T`...
- 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`... - 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`...
- 本地化 - 本地化
- English, 简体中文 - English, 简体中文
- Español, Русский язык, Português, 日本語 (Generated by GPT) - Español, Русский язык, Português, 日本語 (Generated by GPT)
- Deutsch (@its-tom) / 繁體中文 (@kalashnikov) / Indonesian (@azkadev) / Français (@FrancXPT) / Dutch (@QazCetelic) - Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix);
- 感谢贡献者们!
## 🏙️ 截屏
<table>
<tr>
<td><img width="277px" src="imgs/server.png"></td>
<td><img width="277px" src="imgs/detail.png"></td>
<td><img width="277px" src="imgs/sftp.png"></td>
</tr>
</table>
<table>
<tr>
<td><img width="277px" src="imgs/editor.png"> </td>
<td><img width="277px" src="imgs/ssh.png"></td>
<td><img width="277px" src="imgs/docker.png"></td>
</tr>
</table>
## 🆘 帮助 ## 🆘 帮助
- 吹水、参与开发、了解如何使用QQ群 **762870488** <div align="center">
- 为了可以在不使用 ServerBox app 时获取服务器状态(例如:桌面小部件、推送服务),你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor),并且正确配置,详情可见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。 <a href="https://qm.qq.com/q/daCGa7eShG"><img alt="qq" src="https://img.shields.io/badge/QQ-群-pink"></a>
- **常见问题**可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。 <a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div>
- 为了可以在不使用 ServerBox app 时获取服务器状态(例如:桌面小部件、推送服务),你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor),详情见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。
- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
反馈前须知: 反馈前须知:
1. 反馈问题请附带 log点击首页右上角并以 bug 模版提交。 1. 反馈问题请附带 log点击首页右上角并以 bug 模版提交。
2. 反馈问题前请检查是否是 serverbox 的问题。 2. 反馈问题前请检查是否是 serverbox 的问题。
3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受 3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受
确认了解上述内容后,请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
## 🧱 贡献 ## 🧱 贡献
- 任何正面的贡献都欢迎。
- [本地化翻译指南](https://blog.lolli.tech/faq/) 可在我的博客中找到。
任何正面的贡献都欢迎。
如果我忘记在贡献者列表中添加你的名字,请在你打开的 issue 或 PR 中添加评论让我知道,我会尽快添加。
### 开发
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
2. 克隆这个仓库, 运行 `flutter run` 启动应用
3. 运行 `dart run fl_build -p PLATFORM` 构建应用
### 翻译
[指南](https://blog.lpkt.cn/faq/) 可在我的博客中找到。
## 💡 我的其它 Apps ## 💡 我的其它 Apps
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。 - [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。
- [更多](https://github.com/lollipopkit) - 工具 & etc. - [更多](https://github.com/lollipopkit) - 工具 & etc.
## 📝 协议 ## 📝 协议
`GPL v3 lollipopkit` `GPL v3 lollipopkit`

View File

@@ -11,11 +11,13 @@ include: package:flutter_lints/flutter.yaml
analyzer: analyzer:
exclude: exclude:
- '**/*.g.dart' - "**/*.g.dart"
language: language:
# strict-casts: true # strict-casts: true
# strict-inference: true # strict-inference: true
# strict-raw-types: true # strict-raw-types: true
errors:
invalid_annotation_target: ignore
linter: linter:
# The lint rules applied to this project can be customized in the # The lint rules applied to this project can be customized in the
@@ -30,14 +32,20 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file # `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint. # producing the lint.
rules: rules:
library_private_types_in_public_api: false library_private_types_in_public_api: true
use_build_context_synchronously: false use_build_context_synchronously: false
depend_on_referenced_packages: false depend_on_referenced_packages: false
prefer_final_locals: true prefer_final_locals: true
unnecessary_parenthesis: true unnecessary_parenthesis: true
implicit_call_tearoffs: true implicit_call_tearoffs: true
always_declare_return_types: true
always_use_package_imports: true
annotate_overrides: true
avoid_empty_else: true
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
avoid_return_types_on_setters: true
directives_ordering: true # Enable sorting of imports
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@@ -53,7 +53,6 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "tech.lolli.toolbox" applicationId "tech.lolli.toolbox"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
@@ -86,13 +85,20 @@ android {
} }
debug { debug {
applicationIdSuffix '.debug' // No applicationIdSuffix or resValue here
} }
profile { profile {
applicationIdSuffix '.debug' // No applicationIdSuffix or resValue here
} }
} }
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
} }
flutter { flutter {
@@ -100,3 +106,14 @@ flutter {
} }
dependencies {} dependencies {}
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]
import com.android.build.OutputFile
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (abiVersionCode != null) {
output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode
}
}
}

View File

@@ -1,7 +1 @@
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-keep class com.jcraft.** { *; } -keep class com.jcraft.** { *; }

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -10,18 +11,20 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:label="ServerBox" android:label="@string/app_name"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:hasFragileUserData="true" android:hasFragileUserData="true"
android:restoreAnyVersion="true"> android:restoreAnyVersion="true"
tools:targetApi="q">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|layoutDirection|fontScale|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
@@ -29,12 +32,12 @@
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
@@ -43,11 +46,15 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<service <activity
android:name="id.flutter.flutter_background_service.BackgroundService" android:name=".widget.WidgetConfigureActivity"
android:foregroundServiceType="dataSync" android:exported="false"
/> android:theme="@android:style/Theme.Material.Light.Dialog">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver <receiver
android:name=".widget.HomeWidget" android:name=".widget.HomeWidget"
android:exported="false" android:exported="false"
@@ -67,7 +74,12 @@
android:resource="@xml/home_widget" /> android:resource="@xml/home_widget" />
</receiver> </receiver>
<service android:name=".KeepAliveService"/> <service
android:name=".ForegroundService"
android:enabled="true"
android:foregroundServiceType="dataSync"
android:exported="false" />
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and https://developer.android.com/training/package-visibility?hl=en and
@@ -76,8 +88,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain" />
</intent> </intent>
</queries> </queries>
</manifest> </manifest>

View File

@@ -0,0 +1,288 @@
package tech.lolli.toolbox
import android.app.*
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.*
class ForegroundService : Service() {
companion object {
@Volatile
var isRunning: Boolean = false
}
private val chanId = "ForegroundServiceChannel"
private val GROUP_KEY = "ssh_sessions_group"
private val SUMMARY_ID = 1000
private val ACTION_STOP_FOREGROUND = "ACTION_STOP_FOREGROUND"
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
private val ACTION_DISCONNECT_SESSION = "tech.lolli.toolbox.ACTION_DISCONNECT_SESSION"
private var isFgStarted = false
private val postedIds = mutableSetOf<Int>()
// Stable mapping from session-id -> notification-id to avoid hash collisions
private val notificationIdMap = mutableMapOf<String, Int>()
private val nextNotificationId = java.util.concurrent.atomic.AtomicInteger(2001)
private fun logError(message: String, error: Throwable? = null) {
Log.e("ForegroundService", message, error)
try {
val logFile = File(getExternalFilesDir(null), "server_box.log")
val timestamp = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
val logMessage = "$timestamp [ForegroundService] ERROR: $message\n${error?.stackTraceToString() ?: ""}\n"
logFile.appendText(logMessage)
} catch (e: Exception) {
Log.e("ForegroundService", "Failed to write log", e)
}
}
override fun onCreate() {
super.onCreate()
Log.d("ForegroundService", "Service onCreate")
isRunning = true
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
androidx.core.content.ContextCompat.checkSelfPermission(
this, android.Manifest.permission.POST_NOTIFICATIONS
) != android.content.pm.PackageManager.PERMISSION_GRANTED
) {
Log.w("ForegroundService", "Notification permission denied. Stopping service.")
stopForegroundService()
return START_NOT_STICKY
}
if (intent == null) {
Log.w("ForegroundService", "onStartCommand called with null intent")
stopForegroundService()
return START_NOT_STICKY
}
val action = intent.action
Log.d("ForegroundService", "onStartCommand action=$action")
return when (action) {
ACTION_STOP_FOREGROUND -> {
clearAll()
stopForegroundService()
START_NOT_STICKY
}
ACTION_UPDATE_SESSIONS -> {
val payload = intent.getStringExtra("payload") ?: "{}"
handleUpdateSessions(payload)
START_STICKY
}
else -> {
// Default bring up foreground with placeholder
ensureForeground(createSummaryNotification(0, emptyList()))
START_STICKY
}
}
} catch (e: Exception) {
logError("Error in onStartCommand", e)
stopSelf()
return START_NOT_STICKY
}
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
if (manager == null) {
Log.e("ForegroundService", "Failed to get NotificationManager")
return
}
val serviceChannel = NotificationChannel(
chanId,
"ForegroundServiceChannel",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "For foreground service"
}
manager.createNotificationChannel(serviceChannel)
}
}
private fun ensureForeground(notification: Notification) {
try {
if (!isFgStarted) {
startForeground(SUMMARY_ID, notification)
isFgStarted = true
} else {
val nm = getSystemService(NotificationManager::class.java)
nm?.notify(SUMMARY_ID, notification)
}
} catch (e: Exception) {
logError("Failed to start/update foreground", e)
}
}
private fun createSummaryNotification(count: Int, lines: List<String>): Notification {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
)
val stopIntent = Intent(this, ForegroundService::class.java).apply { action = ACTION_STOP_FOREGROUND }
val stopPending = PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId)
} else {
Notification.Builder(this)
}
val inbox = Notification.InboxStyle()
lines.forEach { inbox.addLine(it) }
return builder
.setContentTitle("SSH sessions: $count active")
.setContentText(if (lines.isNotEmpty()) lines.first() else "Running")
.setSmallIcon(R.mipmap.ic_launcher)
.setStyle(inbox)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setGroup(GROUP_KEY)
.setGroupSummary(true)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_delete, "Stop", stopPending)
.build()
}
private fun handleUpdateSessions(payload: String) {
val nm = getSystemService(NotificationManager::class.java)
if (nm == null) {
logError("NotificationManager null")
return
}
val sessions = mutableListOf<SessionItem>()
try {
val obj = JSONObject(payload)
val arr: JSONArray = obj.optJSONArray("sessions") ?: JSONArray()
for (i in 0 until arr.length()) {
val s = arr.optJSONObject(i) ?: continue
val id = s.optString("id")
val title = s.optString("title")
val sub = s.optString("subtitle")
val whenMs = s.optLong("startTimeMs", System.currentTimeMillis())
val status = s.optString("status", "connected")
if (id.isNotEmpty()) {
sessions.add(SessionItem(id, title, sub, whenMs, status))
}
}
} catch (e: Exception) {
logError("Failed to parse payload", e)
}
// Clear if empty
if (sessions.isEmpty()) {
clearAll()
return
}
// Build per-session notifications
val currentIds = mutableSetOf<Int>()
val summaryLines = mutableListOf<String>()
sessions.forEach { s ->
// Assign a stable, collision-resistant id per session for this service lifecycle
val nid = notificationIdMap.getOrPut(s.id) { nextNotificationId.getAndIncrement() }
currentIds.add(nid)
summaryLines.add("${s.title}: ${s.status}")
val disconnectIntent = Intent(this, MainActivity::class.java).apply {
action = ACTION_DISCONNECT_SESSION
putExtra("session_id", s.id)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
val disconnectPending = PendingIntent.getActivity(
this, nid, disconnectIntent, PendingIntent.FLAG_IMMUTABLE
)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId)
} else {
Notification.Builder(this)
}
val noti = builder
.setContentTitle(s.title)
.setContentText("${s.subtitle} · ${s.status}")
.setSmallIcon(R.mipmap.ic_launcher)
.setWhen(s.startWhen)
.setUsesChronometer(true)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setGroup(GROUP_KEY)
.addAction(android.R.drawable.ic_media_pause, "Disconnect", disconnectPending)
.build()
nm.notify(nid, noti)
}
// Cancel stale ones
val toCancel = postedIds - currentIds
toCancel.forEach { nm.cancel(it) }
// Clean up id mappings for canceled notifications to prevent growth
if (toCancel.isNotEmpty()) {
val keysToRemove = notificationIdMap.filterValues { it in toCancel }.keys
keysToRemove.forEach { notificationIdMap.remove(it) }
}
postedIds.clear()
postedIds.addAll(currentIds)
// Post/update summary and ensure foreground
val maxSummaryLines = 5
val truncated = summaryLines.size > maxSummaryLines
val displaySummaryLines = if (truncated) {
summaryLines.take(maxSummaryLines) + "...and ${summaryLines.size - maxSummaryLines} more"
} else {
summaryLines
}
val summary = createSummaryNotification(sessions.size, displaySummaryLines)
ensureForeground(summary)
}
private fun clearAll() {
val nm = getSystemService(NotificationManager::class.java)
nm?.cancel(SUMMARY_ID)
postedIds.forEach { id -> nm?.cancel(id) }
postedIds.clear()
isFgStarted = false
}
data class SessionItem(
val id: String,
val title: String,
val subtitle: String,
val startWhen: Long,
val status: String,
)
private fun stopForegroundService() {
try {
stopForeground(true)
} catch (e: Exception) {
logError("Error stopping foreground", e)
}
stopSelf()
Log.d("ForegroundService", "ForegroundService stopped")
}
override fun onDestroy() {
super.onDestroy()
Log.d("ForegroundService", "Service onDestroy")
isRunning = false
}
}

View File

@@ -1,18 +0,0 @@
package tech.lolli.toolbox
import android.app.Service
import android.content.Intent
import android.os.IBinder
import org.jetbrains.annotations.Nullable
class KeepAliveService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
@Nullable
override fun onBind(intent: Intent?): IBinder? {
return null
}
}

View File

@@ -1,30 +1,143 @@
package tech.lolli.toolbox package tech.lolli.toolbox
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.Manifest
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import android.appwidget.AppWidgetManager
import tech.lolli.toolbox.widget.HomeWidget
class MainActivity: FlutterFragmentActivity() { class MainActivity: FlutterFragmentActivity() {
private lateinit var channel: MethodChannel
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
private val ACTION_DISCONNECT_SESSION = "tech.lolli.toolbox.ACTION_DISCONNECT_SESSION"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
val binaryMessenger = flutterEngine.dartExecutor.binaryMessenger val binaryMessenger = flutterEngine.dartExecutor.binaryMessenger
MethodChannel(binaryMessenger, "tech.lolli.toolbox/app_retain").apply { channel = MethodChannel(binaryMessenger, "tech.lolli.toolbox/main_chan")
setMethodCallHandler { method, result -> channel.setMethodCallHandler { method, result ->
when (method.method) { when (method.method) {
"sendToBackground" -> { "sendToBackground" -> {
moveTaskToBack(true) moveTaskToBack(true)
result.success(null) result.success(null)
} }
"isServiceRunning" -> {
result.success(ForegroundService.isRunning)
}
"startService" -> { "startService" -> {
val intent = Intent(this@MainActivity, KeepAliveService::class.java) try {
startService(intent) reqPerm()
if (!notificationsAllowed()) {
// Don't start foreground service without notification permission on API 33+
result.error("NOTIFICATION_PERMISSION_DENIED", "Notification permission not granted", null)
return@setMethodCallHandler
}
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
result.success(null)
} catch (e: Exception) {
// Log error but don't crash
android.util.Log.e("MainActivity", "Failed to start service: ${e.message}")
result.error("SERVICE_ERROR", e.message, null)
}
}
"stopService" -> {
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
stopService(serviceIntent)
result.success(null)
}
"updateHomeWidget" -> {
val intent = Intent(this@MainActivity, HomeWidget::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
sendBroadcast(intent)
result.success(null)
}
"updateSessions" -> {
try {
if (!notificationsAllowed()) {
// Avoid starting/continuing service updates when notifications are blocked
result.error("NOTIFICATION_PERMISSION_DENIED", "Notification permission not granted", null)
return@setMethodCallHandler
}
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
serviceIntent.action = ACTION_UPDATE_SESSIONS
serviceIntent.putExtra("payload", method.arguments as String)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
result.success(null)
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to update sessions: ${e.message}")
result.error("SERVICE_ERROR", e.message, null)
}
} }
else -> { else -> {
result.notImplemented() result.notImplemented()
} }
} }
}
// Handle intent if launched via notification action
handleActionIntent(intent)
}
private fun reqPerm() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
// Check if we already have the permission to avoid unnecessary prompts
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
try {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
123,
)
} catch (e: Exception) {
// Log error but don't crash
android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}")
}
}
}
private fun notificationsAllowed(): Boolean {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
true
} else {
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleActionIntent(intent)
}
private fun handleActionIntent(intent: Intent?) {
if (intent == null) return
when (intent.action) {
ACTION_DISCONNECT_SESSION -> {
val sessionId = intent.getStringExtra("session_id")
if (sessionId != null && ::channel.isInitialized) {
try {
channel.invokeMethod("disconnectSession", mapOf("id" to sessionId))
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to invoke disconnect: ${e.message}")
}
}
} }
} }
} }

View File

@@ -6,106 +6,216 @@ import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.util.Log
import android.view.View import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.json.JSONObject import org.json.JSONObject
import org.json.JSONException
import tech.lolli.toolbox.R import tech.lolli.toolbox.R
import java.net.URL import java.net.URL
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.io.FileNotFoundException
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
class HomeWidget : AppWidgetProvider() { class HomeWidget : AppWidgetProvider() {
companion object {
private const val TAG = "HomeWidget"
private const val NETWORK_TIMEOUT = 10_000L // 10 seconds
private const val COROUTINE_TIMEOUT = 15_000L // 15 seconds
private val activeUpdates = ConcurrentHashMap<Int, Boolean>()
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
for (appWidgetId in appWidgetIds) { for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId) updateAppWidget(context, appWidgetManager, appWidgetId)
} }
} }
@OptIn(DelicateCoroutinesApi::class)
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) { private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.home_widget) // Prevent concurrent updates for the same widget
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) if (activeUpdates.putIfAbsent(appWidgetId, true) == true) {
var url = sp.getString("$appWidgetId", null) Log.d(TAG, "Widget $appWidgetId is already updating, skipping")
val gUrl = sp.getString("*", null)
if (url.isNullOrEmpty()) {
url = gUrl
}
val intentUpdate = Intent(context, HomeWidget::class.java)
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = intArrayOf(appWidgetId)
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
var flag = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
flag = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
}
val pendingUpdate: PendingIntent = PendingIntent.getBroadcast(
context,
appWidgetId,
intentUpdate,
flag)
views.setOnClickPendingIntent(R.id.widget_container, pendingUpdate)
if (url.isNullOrEmpty()) {
views.setViewVisibility(R.id.widget_cpu_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_mem_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_disk_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_net_label, View.INVISIBLE)
views.setTextViewText(R.id.widget_name, "ID: $appWidgetId")
appWidgetManager.updateAppWidget(appWidgetId, views)
return return
} else {
views.setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
views.setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
views.setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
views.setViewVisibility(R.id.widget_net_label, View.VISIBLE)
} }
GlobalScope.launch(Dispatchers.IO) { val views = RemoteViews(context.packageName, R.layout.home_widget)
try { val url = getWidgetUrl(context, appWidgetId)
val jsonStr = URL(url).readText()
val jsonObject = JSONObject(jsonStr)
val data = jsonObject.getJSONObject("data")
val server = data.getString("name")
val cpu = data.getString("cpu")
val mem = data.getString("mem")
val disk = data.getString("disk")
val net = data.getString("net")
GlobalScope.launch(Dispatchers.Main) main@ { if (url.isNullOrEmpty()) {
// mem or disk is empty -> get status failed Log.w(TAG, "URL not found for widget $appWidgetId")
// (cpu | net) isEmpty -> data is not ready showErrorState(views, appWidgetManager, appWidgetId, "Please configure the widget URL.")
if (mem.isEmpty() || disk.isEmpty()) { activeUpdates.remove(appWidgetId)
return@main return
}
setupClickIntent(context, views, appWidgetId)
showLoadingState(views, appWidgetManager, appWidgetId)
CoroutineScope(Dispatchers.IO).launch {
withTimeoutOrNull(COROUTINE_TIMEOUT) {
try {
val serverData = fetchServerData(url)
if (serverData != null) {
withContext(Dispatchers.Main) {
showSuccessState(views, appWidgetManager, appWidgetId, serverData)
}
} else {
withContext(Dispatchers.Main) {
showErrorState(views, appWidgetManager, appWidgetId, "Invalid server data received.")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error updating widget $appWidgetId: ${e.message}", e)
withContext(Dispatchers.Main) {
val errorMessage = when (e) {
is SocketTimeoutException -> "Connection timeout. Please check your network."
is IOException -> "Network error. Please check your connection."
is JSONException -> "Invalid data format received from server."
else -> "Failed to retrieve data: ${e.message}"
}
showErrorState(views, appWidgetManager, appWidgetId, errorMessage)
} }
views.setTextViewText(R.id.widget_name, server)
views.setTextViewText(R.id.widget_cpu, cpu)
views.setTextViewText(R.id.widget_mem, mem)
views.setTextViewText(R.id.widget_disk, disk)
views.setTextViewText(R.id.widget_net, net)
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
views.setTextViewText(R.id.widget_time, timeStr)
appWidgetManager.updateAppWidget(appWidgetId, views)
} }
} catch (e: Exception) { } ?: run {
println("ServerBoxHomeWidget: ${e.localizedMessage}") Log.w(TAG, "Widget update timed out for widget $appWidgetId")
GlobalScope.launch(Dispatchers.Main) main@ { withContext(Dispatchers.Main) {
views.setViewVisibility(R.id.widget_cpu_label, View.INVISIBLE) showErrorState(views, appWidgetManager, appWidgetId, "Update timed out. Please try again.")
views.setViewVisibility(R.id.widget_mem_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_disk_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_net_label, View.INVISIBLE)
views.setTextViewText(R.id.widget_name, "ID: $appWidgetId")
views.setTextViewText(R.id.widget_mem, e.localizedMessage)
appWidgetManager.updateAppWidget(appWidgetId, views)
} }
} }
activeUpdates.remove(appWidgetId)
} }
} }
private fun getWidgetUrl(context: Context, appWidgetId: Int): String? {
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
return sp.getString("widget_$appWidgetId", null)
?: sp.getString("$appWidgetId", null)
?: sp.getString("widget_*", null)
}
private fun setupClickIntent(context: Context, views: RemoteViews, appWidgetId: Int) {
val intentConfigure = Intent(context, WidgetConfigureActivity::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
}
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val pendingConfigure = PendingIntent.getActivity(context, appWidgetId, intentConfigure, flag)
views.setOnClickPendingIntent(R.id.widget_container, pendingConfigure)
}
private suspend fun fetchServerData(url: String): ServerData? = withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
try {
connection = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
connectTimeout = NETWORK_TIMEOUT.toInt()
readTimeout = NETWORK_TIMEOUT.toInt()
setRequestProperty("User-Agent", "ServerBox-Widget/1.0")
setRequestProperty("Accept", "application/json")
}
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw IOException("HTTP ${connection.responseCode}: ${connection.responseMessage}")
}
val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
parseServerData(jsonStr)
} finally {
connection?.disconnect()
}
}
private fun parseServerData(jsonStr: String): ServerData? {
return try {
val jsonObject = JSONObject(jsonStr)
val data = jsonObject.getJSONObject("data")
val server = data.optString("name", "Unknown Server")
val cpu = data.optString("cpu", "").takeIf { it.isNotBlank() } ?: "N/A"
val mem = data.optString("mem", "").takeIf { it.isNotBlank() } ?: "N/A"
val disk = data.optString("disk", "").takeIf { it.isNotBlank() } ?: "N/A"
val net = data.optString("net", "").takeIf { it.isNotBlank() } ?: "N/A"
// Return data even if some fields are missing, providing defaults
// Only reject if we can't parse the JSON structure properly
ServerData(server, cpu, mem, disk, net)
} catch (e: JSONException) {
Log.e(TAG, "JSON parsing error: ${e.message}", e)
null
}
}
private fun showLoadingState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
views.apply {
setTextViewText(R.id.widget_name, "Loading...")
setViewVisibility(R.id.error_message, View.GONE)
setViewVisibility(R.id.widget_content, View.VISIBLE)
setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
setViewVisibility(R.id.widget_net_label, View.VISIBLE)
setViewVisibility(R.id.widget_progress, View.VISIBLE)
setFloat(R.id.widget_name, "setAlpha", 0.7f)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
private fun showSuccessState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, data: ServerData) {
views.apply {
setTextViewText(R.id.widget_name, data.name)
setTextViewText(R.id.widget_cpu, data.cpu)
setTextViewText(R.id.widget_mem, data.mem)
setTextViewText(R.id.widget_disk, data.disk)
setTextViewText(R.id.widget_net, data.net)
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
setTextViewText(R.id.widget_time, timeStr)
setViewVisibility(R.id.error_message, View.GONE)
setViewVisibility(R.id.widget_content, View.VISIBLE)
setViewVisibility(R.id.widget_progress, View.GONE)
// Smooth fade-in animation
setFloat(R.id.widget_name, "setAlpha", 1f)
setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
setFloat(R.id.widget_mem_label, "setAlpha", 1f)
setFloat(R.id.widget_disk_label, "setAlpha", 1f)
setFloat(R.id.widget_net_label, "setAlpha", 1f)
setFloat(R.id.widget_time, "setAlpha", 1f)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
private fun showErrorState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, errorMessage: String) {
views.apply {
setTextViewText(R.id.widget_name, "Error")
setViewVisibility(R.id.error_message, View.VISIBLE)
setTextViewText(R.id.error_message, errorMessage)
setViewVisibility(R.id.widget_content, View.GONE)
setViewVisibility(R.id.widget_progress, View.GONE)
setFloat(R.id.widget_name, "setAlpha", 1f)
setFloat(R.id.error_message, "setAlpha", 1f)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
data class ServerData(
val name: String,
val cpu: String,
val mem: String,
val disk: String,
val net: String
)
} }

View File

@@ -0,0 +1,82 @@
package tech.lolli.toolbox.widget
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import android.util.Patterns
import android.widget.Button
import android.widget.EditText
import tech.lolli.toolbox.R
class WidgetConfigureActivity : Activity() {
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
private lateinit var urlEditText: EditText
private lateinit var saveButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.widget_configure)
// 设置结果为取消,以防用户在完成配置前退出
setResult(RESULT_CANCELED)
// 获取 widget ID
val extras = intent.extras
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
}
// 如果没有有效的 widget ID完成 activity
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
return
}
// 初始化 UI 元素
urlEditText = findViewById(R.id.url_edit_text)
saveButton = findViewById(R.id.save_button)
// 从 SharedPreferences 加载现有配置
val sp = getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE)
val existingUrl = sp.getString("widget_$appWidgetId", "")
urlEditText.setText(existingUrl)
// 设置保存按钮点击事件
saveButton.setOnClickListener {
val url = urlEditText.text.toString().trim()
if (url.isEmpty()) {
urlEditText.error = "Please enter a URL"
return@setOnClickListener
}
// 验证 URL 格式
if (!Patterns.WEB_URL.matcher(url).matches()) {
urlEditText.error = "Please enter a valid URL"
return@setOnClickListener
}
// 保存 URL 到 SharedPreferences
val editor = sp.edit()
editor.putString("widget_$appWidgetId", url)
editor.apply()
// 更新 widget 使用 AppWidgetManager
val appWidgetManager = AppWidgetManager.getInstance(this)
val updateIntent = Intent(this, HomeWidget::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
}
sendBroadcast(updateIntent)
// 设置结果并结束 activity
val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, resultValue)
finish()
}
}
}

View File

@@ -10,139 +10,204 @@
<TextView <TextView
android:id="@+id/widget_name" android:id="@+id/widget_name"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/widgetText" android:textColor="@color/widgetText"
android:textSize="23sp" android:textSize="20sp"
android:textStyle="bold" android:textStyle="bold"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end"
android:alpha="0"
android:animateLayoutChanges="true"
android:fadingEdge="horizontal"
android:singleLine="true"
tools:text="Server Name" /> tools:text="Server Name" />
<RelativeLayout <!-- Wrap the content in a LinearLayout for easy visibility management -->
android:id="@+id/widget_container_inner" <LinearLayout
android:id="@+id/widget_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:gravity="center_vertical" android:orientation="vertical"
android:paddingTop="13dp"> android:layout_below="@id/widget_name"
android:layout_marginTop="8dp">
<LinearLayout <RelativeLayout
android:id="@+id/widget_cpu_label" android:id="@+id/widget_container_inner"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="2.7dp" android:animateLayoutChanges="true">
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView <LinearLayout
android:layout_width="17dp" android:id="@+id/widget_cpu_label"
android:layout_height="17dp"
android:src="@drawable/speed_24">
</ImageView>
<TextView
android:id="@+id/widget_cpu"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="11dp" android:layout_marginBottom="4dp"
android:singleLine="true" android:gravity="center_vertical"
android:ellipsize = "marquee" android:orientation="horizontal"
android:textColor="@color/widgetSummaryText" android:alpha="0"
android:textSize="12.7sp" android:animateLayoutChanges="true">
tools:text="CPU" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/speed_24"
android:layout_gravity="center_vertical"
android:contentDescription="CPU usage" />
<TextView
android:id="@+id/widget_cpu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:singleLine="true"
android:ellipsize="end"
android:textColor="@color/widgetSummaryText"
android:textSize="12sp"
tools:text="CPU: 25.6%" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/widget_mem_label" android:id="@+id/widget_mem_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="2.7dp"
android:layout_below="@id/widget_cpu_label"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/memory_24">
</ImageView>
<TextView
android:id="@+id/widget_mem"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="11dp" android:layout_marginBottom="4dp"
android:maxLines="1" android:layout_below="@id/widget_cpu_label"
android:textColor="@color/widgetSummaryText" android:gravity="center_vertical"
android:textSize="12.7sp" android:orientation="horizontal"
tools:text="Mem" /> android:alpha="0"
android:animateLayoutChanges="true">
</LinearLayout> <ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/memory_24"
android:layout_gravity="center_vertical"
android:contentDescription="Memory usage" />
<LinearLayout <TextView
android:id="@+id/widget_disk_label" android:id="@+id/widget_mem"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="2.7dp" android:layout_weight="1"
android:layout_below="@id/widget_mem_label" android:layout_marginStart="8dp"
android:gravity="center_vertical" android:maxLines="1"
android:orientation="horizontal"> android:ellipsize="end"
android:textColor="@color/widgetSummaryText"
android:textSize="12sp"
tools:text="Memory: 4.2GB / 8GB" />
<ImageView </LinearLayout>
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/storage_24">
</ImageView>
<TextView <LinearLayout
android:id="@+id/widget_disk" android:id="@+id/widget_disk_label"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="11dp" android:layout_marginBottom="4dp"
android:maxLines="1" android:layout_below="@id/widget_mem_label"
android:textColor="@color/widgetSummaryText" android:gravity="center_vertical"
android:textSize="12.7sp" android:orientation="horizontal"
tools:text="Disk" /> android:alpha="0"
android:animateLayoutChanges="true">
</LinearLayout> <ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/storage_24"
android:layout_gravity="center_vertical"
android:contentDescription="Disk usage" />
<LinearLayout <TextView
android:id="@+id/widget_net_label" android:id="@+id/widget_disk"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/widget_disk_label" android:layout_weight="1"
android:gravity="center_vertical" android:layout_marginStart="8dp"
android:orientation="horizontal"> android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/widgetSummaryText"
android:textSize="12sp"
tools:text="Disk: 125GB / 250GB" />
<ImageView </LinearLayout>
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/net_24">
</ImageView>
<TextView <LinearLayout
android:id="@+id/widget_net" android:id="@+id/widget_net_label"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="11dp" android:layout_below="@id/widget_disk_label"
android:maxLines="1" android:gravity="center_vertical"
android:textColor="@color/widgetSummaryText" android:orientation="horizontal"
android:textSize="12.7sp" android:alpha="0"
tools:text="Net" /> android:animateLayoutChanges="true">
</LinearLayout> <ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/net_24"
android:layout_gravity="center_vertical"
android:contentDescription="Network usage" />
</RelativeLayout> <TextView
android:id="@+id/widget_net"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/widgetSummaryText"
android:textSize="12sp"
tools:text="Network: 15MB/s ↓ 8MB/s ↑" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<!-- Error message display -->
<TextView
android:id="@+id/error_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/widget_name"
android:layout_marginTop="8dp"
android:textColor="@color/widgetSummaryText"
android:textSize="11sp"
android:visibility="gone"
android:alpha="0"
android:animateLayoutChanges="true"
android:lineSpacingMultiplier="1.2"
android:maxLines="3"
android:ellipsize="end"
tools:text="Error message text that might be longer than usual" />
<TextView <TextView
android:id="@+id/widget_time" android:id="@+id/widget_time"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:maxLines="2" android:layout_alignParentEnd="true"
android:maxLines="1"
android:textColor="@color/widgetSummaryText" android:textColor="@color/widgetSummaryText"
android:textSize="11sp" android:textSize="10sp"
tools:text="UpdateTime" /> android:alpha="0"
android:animateLayoutChanges="true"
android:fontFamily="monospace"
tools:text="12:34" />
<!-- Progress indicator for loading state -->
<ProgressBar
android:id="@+id/widget_progress"
style="?android:attr/progressBarStyleLarge"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerInParent="true"
android:visibility="gone"
android:indeterminate="true" />
</RelativeLayout> </RelativeLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Widget URL"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/url_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="https://server/status"
android:inputType="textUri"
android:layout_marginBottom="16dp"
android:background="@android:drawable/edit_text"
android:padding="12dp"
android:textColor="@android:color/black"
android:textColorHint="@android:color/darker_gray" />
<Button
android:id="@+id/save_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save"
android:background="#8b2252"
android:textColor="@android:color/white"
android:padding="12dp" />
</LinearLayout>

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ServerBox</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
</full-backup-content>

View File

@@ -6,6 +6,7 @@
android:minHeight="110dp" android:minHeight="110dp"
android:updatePeriodMillis="1800001" android:updatePeriodMillis="1800001"
android:initialLayout="@layout/home_widget" android:initialLayout="@layout/home_widget"
android:configure="tech.lolli.toolbox.widget.WidgetConfigureActivity"
android:resizeMode="none" android:resizeMode="none"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">
</appwidget-provider> </appwidget-provider>

View File

@@ -9,6 +9,23 @@ rootProject.buildDir = '../build'
subprojects { subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}" project.buildDir = "${rootProject.buildDir}/${project.name}"
} }
subprojects { subproject ->
// Only works on com.android.application(the main app module)
if (subproject.plugins.hasPlugin('com.android.application')) {
subproject.afterEvaluate {
android.buildTypes.matching { it.name == 'profile' }.all { buildType ->
buildType.applicationIdSuffix = ".profile"
buildTypes.profile.resValue 'string', 'app_name', 'SrvBxP'
}
android.buildTypes.matching { it.name == 'debug' }.all { buildType ->
buildType.applicationIdSuffix = ".debug"
buildTypes.debug.resValue 'string', 'app_name', 'SrvBxD'
}
}
}
}
subprojects { subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }

View File

@@ -1,3 +1,6 @@
org.gradle.jvmargs=-Xmx4G org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@@ -19,8 +19,8 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.4.2" apply false id "com.android.application" version '8.6.0' apply false
id "org.jetbrains.kotlin.android" version "1.8.10" apply false id "org.jetbrains.kotlin.android" version "2.1.21" apply false
} }
include ":app" include ":app"

6505
coverage/lcov.info Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions: extensions:

View File

@@ -0,0 +1,6 @@
A Flutter project which provide charts to display Linux server status and tools to manage server.
Especially thanks to dartssh2 & xterm.dart.
* Status chart (CPU, Sensors, GPU...), SSH Term, SFTP, Docker & Pkg & Process...
* Platform specific: Bio auth、Msg push、Home widget、watchOS App...
* English, 简体中文; Deutsch, 繁體中文, Indonesian, Français, Dutch; Español, Русский язык, Português, 日本語

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View File

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -0,0 +1 @@
A server status & toolbox app using Flutter

View File

@@ -0,0 +1 @@
ServerBox

View File

@@ -0,0 +1,7 @@
使用 Flutter 开发的 Linux 服务器工具箱,提供服务器状态图表和管理工具。
特别感谢 dartssh2 & xterm.dart。
特点:
* 状态图表CPU、传感器、GPU 等), SSH 终端, SFTP, Docker & 包 & 进程管理器...
* 特殊支持生物认证、推送、桌面小部件、watchOS App、跟随系统颜色...
* 本地化 (English, 简体中文, Español, Русский язык, Português, 日本語, Deutsch, 繁體中文, Indonesian, Français, Dutch

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -0,0 +1 @@
使用 Flutter 开发的服务器状态和工具箱应用

View File

@@ -0,0 +1 @@
ServerBox

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>12.0</string> <string>13.0</string>
</dict> </dict>
</plist> </plist>

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '12.0' # platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -1,26 +1,25 @@
PODS: PODS:
- countly_flutter (24.4.1): - app_links (6.4.1):
- Flutter - Flutter
- device_info_plus (0.0.1): - camera_avfoundation (0.0.1):
- Flutter - Flutter
- file_picker (0.0.1): - file_picker (0.0.1):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_background_service_ios (0.0.3): - flutter_native_splash (2.4.3):
- Flutter - Flutter
- flutter_native_splash (0.0.1): - flutter_secure_storage (6.0.0):
- Flutter - Flutter
- icloud_storage (0.0.1): - icloud_storage (0.0.1):
- Flutter - Flutter
- local_auth_darwin (0.0.1): - local_auth_darwin (0.0.1):
- Flutter - Flutter
- FlutterMacOS
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- plain_notification_token (0.0.1): - plain_notification_token (0.0.1):
- Flutter - Flutter
- share_plus (0.0.1): - share_plus (0.0.1):
@@ -36,17 +35,16 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- countly_flutter (from `.symlinks/plugins/countly_flutter/ios`) - app_links (from `.symlinks/plugins/app_links/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- icloud_storage (from `.symlinks/plugins/icloud_storage/ios`) - icloud_storage (from `.symlinks/plugins/icloud_storage/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- plain_notification_token (from `.symlinks/plugins/plain_notification_token/ios`) - plain_notification_token (from `.symlinks/plugins/plain_notification_token/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -55,18 +53,18 @@ DEPENDENCIES:
- watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`) - watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
countly_flutter: app_links:
:path: ".symlinks/plugins/countly_flutter/ios" :path: ".symlinks/plugins/app_links/ios"
device_info_plus: camera_avfoundation:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/camera_avfoundation/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_background_service_ios:
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
icloud_storage: icloud_storage:
:path: ".symlinks/plugins/icloud_storage/ios" :path: ".symlinks/plugins/icloud_storage/ios"
local_auth_darwin: local_auth_darwin:
@@ -75,8 +73,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
plain_notification_token: plain_notification_token:
:path: ".symlinks/plugins/plain_notification_token/ios" :path: ".symlinks/plugins/plain_notification_token/ios"
share_plus: share_plus:
@@ -91,24 +87,23 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/watch_connectivity/ios" :path: ".symlinks/plugins/watch_connectivity/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
countly_flutter: 56233d921c6b4e0a720774a39b8ee8110d6f8d91 app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287 file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
local_auth_darwin: 4d56c90c2683319835a61274b57620df9c4520ab local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 plain_notification_token: 047876b9d80a5b93565ddcc13a487a7e7b906f7d
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
watch_connectivity: 715eb484685e05846eab74795348a44bb2809b82
PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8 PODFILE CHECKSUM: 5a0fb6438066e44ab2c77bd223668d351b8d8461
COCOAPODS: 1.15.2 COCOAPODS: 1.16.2

View File

@@ -9,6 +9,10 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
4A2DCD6B2E4B127100CF68B7 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD692E4B127100CF68B7 /* LiveActivityManager.swift */; };
4A2DCD6C2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD6A2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift */; };
4A2DCD6F2E4B128100CF68B7 /* TerminalLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD6D2E4B128100CF68B7 /* TerminalLiveActivity.swift */; };
4A2DCD702E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD6E2E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7538AEC32BB83FAB002AB82A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7538AEC22BB83FAB002AB82A /* PrivacyInfo.xcprivacy */; }; 7538AEC32BB83FAB002AB82A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7538AEC22BB83FAB002AB82A /* PrivacyInfo.xcprivacy */; };
7538AEC52BB83FC8002AB82A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7538AEC42BB83FC8002AB82A /* PrivacyInfo.xcprivacy */; }; 7538AEC52BB83FC8002AB82A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7538AEC42BB83FC8002AB82A /* PrivacyInfo.xcprivacy */; };
@@ -36,6 +40,8 @@
E3AE8AEB2AB601DB000A6459 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AE8AE92AB601DB000A6459 /* Utils.swift */; }; E3AE8AEB2AB601DB000A6459 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AE8AE92AB601DB000A6459 /* Utils.swift */; };
E3AE8AEC2AB601DB000A6459 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AE8AE92AB601DB000A6459 /* Utils.swift */; }; E3AE8AEC2AB601DB000A6459 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AE8AE92AB601DB000A6459 /* Utils.swift */; };
E3DB67ED2A31FE200027B8CB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E3DB67EB2A31FE200027B8CB /* LaunchScreen.storyboard */; }; E3DB67ED2A31FE200027B8CB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E3DB67EB2A31FE200027B8CB /* LaunchScreen.storyboard */; };
F0A1B2C31A2B3C4D5E6F0005 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F0A1B2C31A2B3C4D5E6F0001 /* Localizable.strings */; };
F0A1B2C31A2B3C4D5E6F1005 /* Localizable.strings (StatusWidget) in Resources */ = {isa = PBXBuildFile; fileRef = F0A1B2C31A2B3C4D5E6F1001 /* Localizable.strings */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -95,6 +101,10 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
278C1EB3935F9285537B0516 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 278C1EB3935F9285537B0516 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4A2DCD692E4B127100CF68B7 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
4A2DCD6A2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalLiveActivityAttributes.swift; sourceTree = "<group>"; };
4A2DCD6D2E4B128100CF68B7 /* TerminalLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalLiveActivity.swift; sourceTree = "<group>"; };
4A2DCD6E2E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalLiveActivityAttributes.swift; sourceTree = "<group>"; };
5A4B3EB10512B2EB8E10213B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 5A4B3EB10512B2EB8E10213B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -156,6 +166,26 @@
E3D26BD22B9966EC00D83425 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Main.strings; sourceTree = "<group>"; }; E3D26BD22B9966EC00D83425 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Main.strings; sourceTree = "<group>"; };
E3D26BD32B9966EC00D83425 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/LaunchScreen.strings; sourceTree = "<group>"; }; E3D26BD32B9966EC00D83425 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/LaunchScreen.strings; sourceTree = "<group>"; };
E3DB67EC2A31FE200027B8CB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; E3DB67EC2A31FE200027B8CB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F0002 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F0003 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F0004 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F0006 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F0007 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F0008 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F0009 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F000A /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F000B /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F000C /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F1002 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F1003 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F1004 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F1006 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F1007 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F1008 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F1009 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F100A /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F100B /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
F0A1B2C31A2B3C4D5E6F100C /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -233,6 +263,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
F0A1B2C31A2B3C4D5E6F0001 /* Localizable.strings */,
7538AEC22BB83FAB002AB82A /* PrivacyInfo.xcprivacy */, 7538AEC22BB83FAB002AB82A /* PrivacyInfo.xcprivacy */,
E398BF6A29BDB34500FE4FD5 /* Runner.entitlements */, E398BF6A29BDB34500FE4FD5 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
@@ -242,6 +273,8 @@
E39A76AD2AB9A2F70067C641 /* Info-Profile.plist */, E39A76AD2AB9A2F70067C641 /* Info-Profile.plist */,
E39A76AC2AB9A2F70067C641 /* Info-Release.plist */, E39A76AC2AB9A2F70067C641 /* Info-Release.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
4A2DCD692E4B127100CF68B7 /* LiveActivityManager.swift */,
4A2DCD6A2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
E3AE8AE92AB601DB000A6459 /* Utils.swift */, E3AE8AE92AB601DB000A6459 /* Utils.swift */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
@@ -263,8 +296,11 @@
E33A3E3A2A626DCE009744AB /* StatusWidget */ = { E33A3E3A2A626DCE009744AB /* StatusWidget */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
F0A1B2C31A2B3C4D5E6F1001 /* Localizable.strings */,
7538AEC42BB83FC8002AB82A /* PrivacyInfo.xcprivacy */, 7538AEC42BB83FC8002AB82A /* PrivacyInfo.xcprivacy */,
E33A3E3B2A626DCE009744AB /* StatusWidgetBundle.swift */, E33A3E3B2A626DCE009744AB /* StatusWidgetBundle.swift */,
4A2DCD6D2E4B128100CF68B7 /* TerminalLiveActivity.swift */,
4A2DCD6E2E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift */,
E33A3E3F2A626DCE009744AB /* StatusWidget.swift */, E33A3E3F2A626DCE009744AB /* StatusWidget.swift */,
E37C48ED2B9C30EE00E542D2 /* StatusWidget.intentdefinition */, E37C48ED2B9C30EE00E542D2 /* StatusWidget.intentdefinition */,
E33A3E442A626DD0009744AB /* Info.plist */, E33A3E442A626DD0009744AB /* Info.plist */,
@@ -302,7 +338,6 @@
E33A3E4A2A626DD0009744AB /* Embed Foundation Extensions */, E33A3E4A2A626DD0009744AB /* Embed Foundation Extensions */,
E39515D52AB5AD64003602C1 /* Embed Watch Content */, E39515D52AB5AD64003602C1 /* Embed Watch Content */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
955896919A10AA2BEC131F36 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -413,6 +448,7 @@
E39A76B02AB9A2F70067C641 /* Info-Profile.plist in Resources */, E39A76B02AB9A2F70067C641 /* Info-Profile.plist in Resources */,
7538AEC32BB83FAB002AB82A /* PrivacyInfo.xcprivacy in Resources */, 7538AEC32BB83FAB002AB82A /* PrivacyInfo.xcprivacy in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
F0A1B2C31A2B3C4D5E6F0005 /* Localizable.strings in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -421,6 +457,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7538AEC52BB83FC8002AB82A /* PrivacyInfo.xcprivacy in Resources */, 7538AEC52BB83FC8002AB82A /* PrivacyInfo.xcprivacy in Resources */,
F0A1B2C31A2B3C4D5E6F1005 /* Localizable.strings (StatusWidget) in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -452,23 +489,6 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
955896919A10AA2BEC131F36 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@@ -534,6 +554,8 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
E37C48EA2B9C30EE00E542D2 /* StatusWidget.intentdefinition in Sources */, E37C48EA2B9C30EE00E542D2 /* StatusWidget.intentdefinition in Sources */,
E3AE8AEA2AB601DB000A6459 /* Utils.swift in Sources */, E3AE8AEA2AB601DB000A6459 /* Utils.swift in Sources */,
4A2DCD6B2E4B127100CF68B7 /* LiveActivityManager.swift in Sources */,
4A2DCD6C2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -543,6 +565,8 @@
files = ( files = (
E33A3E402A626DCE009744AB /* StatusWidget.swift in Sources */, E33A3E402A626DCE009744AB /* StatusWidget.swift in Sources */,
E37C48EB2B9C30EE00E542D2 /* StatusWidget.intentdefinition in Sources */, E37C48EB2B9C30EE00E542D2 /* StatusWidget.intentdefinition in Sources */,
4A2DCD6F2E4B128100CF68B7 /* TerminalLiveActivity.swift in Sources */,
4A2DCD702E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */,
E33A3E3C2A626DCE009744AB /* StatusWidgetBundle.swift in Sources */, E33A3E3C2A626DCE009744AB /* StatusWidgetBundle.swift in Sources */,
E3AE8AEB2AB601DB000A6459 /* Utils.swift in Sources */, E3AE8AEB2AB601DB000A6459 /* Utils.swift in Sources */,
); );
@@ -628,6 +652,40 @@
name = LaunchScreen.storyboard; name = LaunchScreen.storyboard;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
F0A1B2C31A2B3C4D5E6F0001 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
F0A1B2C31A2B3C4D5E6F0002 /* en */,
F0A1B2C31A2B3C4D5E6F0003 /* zh-Hans */,
F0A1B2C31A2B3C4D5E6F0004 /* zh-Hant */,
F0A1B2C31A2B3C4D5E6F0006 /* fr */,
F0A1B2C31A2B3C4D5E6F0007 /* ru */,
F0A1B2C31A2B3C4D5E6F0008 /* es */,
F0A1B2C31A2B3C4D5E6F0009 /* de */,
F0A1B2C31A2B3C4D5E6F000A /* pt-BR */,
F0A1B2C31A2B3C4D5E6F000B /* id */,
F0A1B2C31A2B3C4D5E6F000C /* ja */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
F0A1B2C31A2B3C4D5E6F1001 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
F0A1B2C31A2B3C4D5E6F1002 /* en */,
F0A1B2C31A2B3C4D5E6F1003 /* zh-Hans */,
F0A1B2C31A2B3C4D5E6F1004 /* zh-Hant */,
F0A1B2C31A2B3C4D5E6F1006 /* fr */,
F0A1B2C31A2B3C4D5E6F1007 /* ru */,
F0A1B2C31A2B3C4D5E6F1008 /* es */,
F0A1B2C31A2B3C4D5E6F1009 /* de */,
F0A1B2C31A2B3C4D5E6F100A /* pt-BR */,
F0A1B2C31A2B3C4D5E6F100B /* id */,
F0A1B2C31A2B3C4D5E6F100C /* ja */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */ /* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
@@ -673,7 +731,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -690,17 +748,17 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 923; CURRENT_PROJECT_VERSION = 1241;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.923; MARKETING_VERSION = 1.0.1241;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -757,7 +815,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -807,7 +865,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -826,17 +884,17 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 923; CURRENT_PROJECT_VERSION = 1241;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.923; MARKETING_VERSION = 1.0.1241;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -854,17 +912,17 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 923; CURRENT_PROJECT_VERSION = 1241;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.923; MARKETING_VERSION = 1.0.1241;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -885,7 +943,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 923; CURRENT_PROJECT_VERSION = 1241;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -898,7 +956,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.923; MARKETING_VERSION = 1.0.1241;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
@@ -924,7 +982,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 923; CURRENT_PROJECT_VERSION = 1241;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -937,7 +995,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.923; MARKETING_VERSION = 1.0.1241;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -960,7 +1018,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 923; CURRENT_PROJECT_VERSION = 1241;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -973,7 +1031,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.923; MARKETING_VERSION = 1.0.1241;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -996,7 +1054,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 923; CURRENT_PROJECT_VERSION = 1241;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1008,7 +1066,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.923; MARKETING_VERSION = 1.0.1241;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
@@ -1037,7 +1095,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 923; CURRENT_PROJECT_VERSION = 1241;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1049,7 +1107,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.923; MARKETING_VERSION = 1.0.1241;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox; PRODUCT_NAME = ServerBox;
@@ -1075,7 +1133,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 923; CURRENT_PROJECT_VERSION = 1241;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1087,7 +1145,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.923; MARKETING_VERSION = 1.0.1241;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox; PRODUCT_NAME = ServerBox;

View File

@@ -26,6 +26,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
@@ -43,11 +44,13 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">

View File

@@ -1,8 +1,9 @@
import UIKit import UIKit
import WidgetKit import WidgetKit
import Flutter import Flutter
import ActivityKit
@UIApplicationMain @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
@@ -11,14 +12,48 @@ import Flutter
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: "tech.lolli.toolbox/home_widget", binaryMessenger: controller.binaryMessenger) // Home widget channel (legacy)
methodChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in let homeWidgetChannel = FlutterMethodChannel(name: "tech.lolli.toolbox/home_widget", binaryMessenger: controller.binaryMessenger)
homeWidgetChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "update" { if call.method == "update" {
if #available(iOS 14.0, *) { if #available(iOS 14.0, *) {
WidgetCenter.shared.reloadTimelines(ofKind: "StatusWidget") WidgetCenter.shared.reloadTimelines(ofKind: "StatusWidget")
} }
} }
}) })
// Main channel for cross-platform calls (incl. Live Activities)
let mainChannel = FlutterMethodChannel(name: "tech.lolli.toolbox/main_chan", binaryMessenger: controller.binaryMessenger)
mainChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "updateHomeWidget":
if #available(iOS 14.0, *) {
WidgetCenter.shared.reloadTimelines(ofKind: "StatusWidget")
}
result(nil)
case "startLiveActivity":
if #available(iOS 16.2, *) {
if let payload = call.arguments as? String {
LiveActivityManager.start(json: payload)
}
}
result(nil)
case "updateLiveActivity":
if #available(iOS 16.2, *) {
if let payload = call.arguments as? String {
LiveActivityManager.update(json: payload)
}
}
result(nil)
case "stopLiveActivity":
if #available(iOS 16.2, *) {
LiveActivityManager.stop()
}
result(nil)
default:
result(FlutterMethodNotImplemented)
}
})
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
@@ -30,4 +65,11 @@ import Flutter
} }
return true return true
} }
override func applicationWillTerminate(_ application: UIApplication) {
// Stop Live Activity when app is about to terminate
if #available(iOS 16.2, *) {
LiveActivityManager.stop()
}
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1 +1,37 @@
{"images":[{"scale":"3x","idiom":"universal","filename":"AppIcon-29.0x29.0@3x.png","size":"29x29","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-29.0x29.0@2x.png","size":"29x29","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-64.0x64.0@3x.png","size":"64x64","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-20.0x20.0@2x.png","size":"20x20","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-60.0x60.0@2x.png","size":"60x60","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-40.0x40.0@3x.png","size":"40x40","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-76.0x76.0@2x.png","size":"76x76","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-38.0x38.0@3x.png","size":"38x38","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-68.0x68.0@2x.png","size":"68x68","platform":"ios"},{"scale":"1x","idiom":"universal","filename":"AppIcon-1024.0x1024.0@1x.png","size":"1024x1024","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-64.0x64.0@2x.png","size":"64x64","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-40.0x40.0@2x.png","size":"40x40","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-83.5x83.5@2x.png","size":"83.5x83.5","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-20.0x20.0@3x.png","size":"20x20","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-38.0x38.0@2x.png","size":"38x38","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-60.0x60.0@3x.png","size":"60x60","platform":"ios"}],"info":{"version":1,"author":"appicon"}} {
"images" : [
{
"filename" : "Icon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "icon-1024 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View File

@@ -1,76 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true />
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>en</string> <string>en</string>
<string>zh</string> <string>zh</string>
</array> </array>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>ServerBox</string> <string>ServerBox</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false />
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true />
<key>LSSupportsOpeningDocumentsInPlace</key> <key>LSSupportsOpeningDocumentsInPlace</key>
<true/> <true />
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_dartobservatory._tcp</string> <string>_dartobservatory._tcp</string>
</array> </array>
<key>NSFaceIDUsageDescription</key> <key>NSUserActivityTypes</key>
<string>Required for auth</string> <array>
<key>NSLocalNetworkUsageDescription</key> <string>ConfigurationIntent</string>
<string>ServerBox needs to access your local network to discover and connect to your server.</string> </array>
<key>NSUserActivityTypes</key> <key>NSSupportsLiveActivities</key>
<array> <true/>
<string>ConfigurationIntent</string> <key>UIApplicationSupportsIndirectInputEvents</key>
</array> <true />
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIBackgroundModes</key>
<true/> <array>
<key>UIBackgroundModes</key> <string>fetch</string>
<array> </array>
<string>fetch</string> <key>UILaunchStoryboardName</key>
</array> <string>LaunchScreen</string>
<key>UILaunchStoryboardName</key> <key>UIMainStoryboardFile</key>
<string>LaunchScreen</string> <string>Main</string>
<key>UIMainStoryboardFile</key> <key>UIStatusBarHidden</key>
<string>Main</string> <false />
<key>UIStatusBarHidden</key> <key>UISupportedInterfaceOrientations</key>
<false/> <array>
<key>UISupportedInterfaceOrientations</key> <string>UIInterfaceOrientationPortrait</string>
<array> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string> </array>
<string>UIInterfaceOrientationLandscapeRight</string> <key>UISupportedInterfaceOrientations~ipad</key>
</array> <array>
<key>UISupportedInterfaceOrientations~ipad</key> <string>UIInterfaceOrientationPortrait</string>
<array> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string> </array>
<string>UIInterfaceOrientationLandscapeRight</string> <key>UIViewControllerBasedStatusBarAppearance</key>
</array> <false />
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <key>NSLocalNetworkUsageDescription</key>
</dict> <string>Access your local network to discover and connect to your server.</string>
<key>NSFaceIDUsageDescription</key>
<string>Required for auth</string>
<key>NSCameraUsageDescription</key>
<string>Scan QR codes and etc.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Get QR code and etc.</string>
</dict>
</plist> </plist>

View File

@@ -17,6 +17,8 @@
<string>en</string> <string>en</string>
<string>zh</string> <string>zh</string>
</array> </array>
<key>NSSupportsLiveActivities</key>
<true/>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>ServerBox</string> <string>ServerBox</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
@@ -64,9 +66,14 @@
<array> <array>
<string>_dartobservatory._tcp</string> <string>_dartobservatory._tcp</string>
</array> </array>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>ServerBox needs to access your local network to discover and connect to your server.</string> <string>Access your local network to discover and connect to your server.</string>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Required for auth</string> <string>Required for auth</string>
<key>NSCameraUsageDescription</key>
<string>Scan QR codes and etc.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Get QR code and etc.</string>
</dict> </dict>
</plist> </plist>

View File

@@ -3,7 +3,7 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true />
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
@@ -17,6 +17,8 @@
<string>en</string> <string>en</string>
<string>zh</string> <string>zh</string>
</array> </array>
<key>NSSupportsLiveActivities</key>
<true/>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>ServerBox</string> <string>ServerBox</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
@@ -28,13 +30,13 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false />
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true />
<key>LSSupportsOpeningDocumentsInPlace</key> <key>LSSupportsOpeningDocumentsInPlace</key>
<true/> <true />
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true />
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
@@ -44,7 +46,7 @@
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIStatusBarHidden</key> <key>UIStatusBarHidden</key>
<false/> <false />
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
@@ -59,8 +61,13 @@
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false />
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Required for auth</string> <string>Required for auth</string>
<key>NSCameraUsageDescription</key>
<string>Scan QR codes and etc.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Get QR code and etc.</string>
</dict> </dict>
</plist> </plist>

View File

@@ -0,0 +1,95 @@
//
// LiveActivityManager.swift
// Runner
//
// Handles starting/updating/stopping Terminal Live Activities from Flutter via MethodChannel.
//
import Foundation
import ActivityKit
@available(iOS 16.2, *)
class LiveActivityManager {
static var current: Activity<TerminalAttributes>?
struct Payload: Decodable {
let id: String
let title: String
let subtitle: String
let startTimeMs: Int
let status: String
let hasTerminal: Bool?
let connectionCount: Int?
}
private static func parse(_ json: String) -> Payload? {
guard let data = json.data(using: .utf8) else { return nil }
return try? JSONDecoder().decode(Payload.self, from: data)
}
static func start(json: String) {
guard #available(iOS 16.2, *) else { return }
guard let p = parse(json) else { return }
let attributes = TerminalAttributes(id: p.id)
let date = Date(timeIntervalSince1970: TimeInterval(p.startTimeMs) / 1000.0)
// Localize multi-connection title/subtitle on iOS side
let isMulti = (p.id == "multi_connections")
let title = isMulti
? String(format: NSLocalizedString("%d connections", comment: "Title for multiple connections"), p.connectionCount ?? 1)
: p.title
let subtitle = isMulti
? NSLocalizedString("Multiple SSH sessions active", comment: "Subtitle for multiple connections")
: p.subtitle
let state = TerminalAttributes.ContentState(
id: p.id,
title: title,
subtitle: subtitle,
status: p.status,
startTime: date,
hasTerminal: p.hasTerminal ?? true,
connectionCount: p.connectionCount ?? 1
)
let content = ActivityContent(state: state, staleDate: nil)
do {
current = try Activity<TerminalAttributes>.request(attributes: attributes, content: content, pushType: nil)
} catch {
// ignore
}
}
static func update(json: String) {
guard #available(iOS 16.2, *) else { return }
guard let p = parse(json) else { return }
let date = Date(timeIntervalSince1970: TimeInterval(p.startTimeMs) / 1000.0)
// Localize multi-connection title/subtitle on iOS side
let isMulti = (p.id == "multi_connections")
let title = isMulti
? String(format: NSLocalizedString("%d connections", comment: "Title for multiple connections"), p.connectionCount ?? 1)
: p.title
let subtitle = isMulti
? NSLocalizedString("Multiple SSH sessions active", comment: "Subtitle for multiple connections")
: p.subtitle
let state = TerminalAttributes.ContentState(
id: p.id,
title: title,
subtitle: subtitle,
status: p.status,
startTime: date,
hasTerminal: p.hasTerminal ?? true,
connectionCount: p.connectionCount ?? 1
)
if let activity = current {
Task { await activity.update(ActivityContent(state: state, staleDate: nil)) }
} else {
start(json: json)
}
}
static func stop() {
guard #available(iOS 16.2, *) else { return }
if let activity = current {
Task { await activity.end(dismissalPolicy: .immediate) }
current = nil
}
}
}

View File

@@ -0,0 +1,39 @@
//
// TerminalLiveActivityAttributes.swift
// Runner
//
// Mirror of the ActivityKit attributes used in the extension.
//
import Foundation
import ActivityKit
@available(iOS 16.1, *)
public struct TerminalAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
public var id: String
public var title: String
public var subtitle: String
public var status: String
public var startTime: Date
public var hasTerminal: Bool
public var connectionCount: Int
public init(id: String, title: String, subtitle: String, status: String, startTime: Date, hasTerminal: Bool, connectionCount: Int = 1) {
self.id = id
self.title = title
self.subtitle = subtitle
self.status = status
self.startTime = startTime
self.hasTerminal = hasTerminal
self.connectionCount = connectionCount
}
}
public var id: String
public init(id: String) {
self.id = id
}
}

View File

@@ -0,0 +1,8 @@
"Terminal" = "Terminal";
"Connected" = "Verbunden";
"Connecting" = "Verbindung wird hergestellt";
"Disconnected" = "Getrennt";
"Multiple SSH sessions active" = "Mehrere aktive SSH-Sitzungen";
"1 connection" = "1 Verbindung";
"%d connections" = "%d Verbindungen";

View File

@@ -0,0 +1,8 @@
"Terminal" = "Terminal";
"Connected" = "Connected";
"Connecting" = "Connecting";
"Disconnected" = "Disconnected";
"Multiple SSH sessions active" = "Multiple SSH sessions active";
"1 connection" = "1 connection";
"%d connections" = "%d connections";

View File

@@ -0,0 +1,8 @@
"Terminal" = "Terminal";
"Connected" = "Conectado";
"Connecting" = "Conectando";
"Disconnected" = "Desconectado";
"Multiple SSH sessions active" = "Varias sesiones SSH activas";
"1 connection" = "1 conexión";
"%d connections" = "%d conexiones";

View File

@@ -0,0 +1,8 @@
"Terminal" = "Terminal";
"Connected" = "Connecté";
"Connecting" = "Connexion en cours";
"Disconnected" = "Déconnecté";
"Multiple SSH sessions active" = "Plusieurs sessions SSH actives";
"1 connection" = "1 connexion";
"%d connections" = "%d connexions";

View File

@@ -0,0 +1,8 @@
"Terminal" = "Terminal";
"Connected" = "Terhubung";
"Connecting" = "Menghubungkan";
"Disconnected" = "Terputus";
"Multiple SSH sessions active" = "Beberapa sesi SSH aktif";
"1 connection" = "1 koneksi";
"%d connections" = "%d koneksi";

View File

@@ -0,0 +1,8 @@
"Terminal" = "ターミナル";
"Connected" = "接続済み";
"Connecting" = "接続中";
"Disconnected" = "切断";
"Multiple SSH sessions active" = "複数の SSH セッションがアクティブ";
"1 connection" = "1 件の接続";
"%d connections" = "%d 件の接続";

View File

@@ -0,0 +1,8 @@
"Terminal" = "Terminal";
"Connected" = "Conectado";
"Connecting" = "Conectando";
"Disconnected" = "Desconectado";
"Multiple SSH sessions active" = "Várias sessões SSH ativas";
"1 connection" = "1 conexão";
"%d connections" = "%d conexões";

View File

@@ -0,0 +1,8 @@
"Terminal" = "Терминал";
"Connected" = "Подключено";
"Connecting" = "Подключение";
"Disconnected" = "Отключено";
"Multiple SSH sessions active" = "Несколько активных сеансов SSH";
"1 connection" = "1 подключение";
"%d connections" = "%d подключений";

View File

@@ -0,0 +1,8 @@
"Terminal" = "终端";
"Connected" = "已连接";
"Connecting" = "连接中";
"Disconnected" = "已断开连接";
"Multiple SSH sessions active" = "多个 SSH 会话正在活动";
"1 connection" = "1 个连接";
"%d connections" = "%d 个连接";

View File

@@ -0,0 +1,8 @@
"Terminal" = "終端機";
"Connected" = "已連線";
"Connecting" = "連線中";
"Disconnected" = "已中斷連線";
"Multiple SSH sessions active" = "多個 SSH 連線運行中";
"1 connection" = "1 個連線";
"%d connections" = "%d 個連線";

View File

@@ -4,6 +4,15 @@
<dict> <dict>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionAttributes</key>
<dict>
<key>IntentsSupportedIntents</key>
<array>
<string>ConfigurationIntent</string>
</array>
<key>NSSupportsLiveActivities</key>
<true/>
</dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string> <string>com.apple.widgetkit-extension</string>
</dict> </dict>

View File

@@ -15,6 +15,142 @@ let demoStatus = Status(name: "Server", cpu: "31.7%", mem: "1.3g / 1.9g", disk:
let domain = "com.lollipopkit.toolbox" let domain = "com.lollipopkit.toolbox"
let bgColor = DynamicColor(dark: UIColor.black, light: UIColor.white) let bgColor = DynamicColor(dark: UIColor.black, light: UIColor.white)
// Widget-specific constants
enum WidgetConstants {
enum Dimensions {
static let smallGauge: CGFloat = 56
static let mediumGauge: CGFloat = 64
static let largeGauge: CGFloat = 76
static let refreshIconSmall: CGFloat = 12
static let refreshIconLarge: CGFloat = 14
static let cornerRadius: CGFloat = 12
static let shadowRadius: CGFloat = 2
}
enum Thresholds {
static let warningThreshold: Double = 0.6
static let criticalThreshold: Double = 0.85
}
enum Spacing {
static let tight: CGFloat = 4
static let normal: CGFloat = 8
static let loose: CGFloat = 12
static let extraLoose: CGFloat = 16
}
enum Colors {
static let cardBackground = Color(.systemBackground)
static let secondaryText = Color(.secondaryLabel)
static let success = Color(.systemGreen)
static let warning = Color(.systemOrange)
static let critical = Color(.systemRed)
static let accent = Color(.systemBlue)
}
static let appGroupId = "group.com.lollipopkit.toolbox"
}
// Performance optimization: cache parsed values
struct ParseCache {
private static var percentCache: [String: Double] = [:]
private static var usagePercentCache: [String: Double] = [:]
static func parsePercent(_ text: String) -> Double {
if let cached = percentCache[text] { return cached }
let trimmed = text.trimmingCharacters(in: CharacterSet(charactersIn: "% "))
let result = Double(trimmed).map { max(0, min(1, $0 / 100.0)) } ?? 0
percentCache[text] = result
return result
}
static func parseUsagePercent(_ text: String) -> Double {
if let cached = usagePercentCache[text] { return cached }
let parts = text.split(separator: "/").map { String($0).trimmingCharacters(in: .whitespaces) }
guard parts.count == 2 else { return 0 }
let used = PerformanceUtils.parseSizeToBytes(parts[0])
let total = PerformanceUtils.parseSizeToBytes(parts[1])
let result = total <= 0 ? 0 : max(0, min(1, used / total))
usagePercentCache[text] = result
return result
}
static func parseNetworkTotal(_ text: String) -> (totalBytes: Double, displayText: String) {
let parts = text.split(separator: "/").map { String($0).trimmingCharacters(in: .whitespaces) }
guard parts.count == 2 else { return (0, "0 B") }
let upload = PerformanceUtils.parseSizeToBytes(parts[0])
let download = PerformanceUtils.parseSizeToBytes(parts[1])
let total = upload + download
let displayText = PerformanceUtils.formatSize(total)
return (total, displayText)
}
static func parseNetworkPercent(_ text: String) -> Double {
let parts = text.split(separator: "/").map { String($0).trimmingCharacters(in: .whitespaces) }
guard parts.count == 2 else { return 0 }
let upload = PerformanceUtils.parseSizeToBytes(parts[0])
let download = PerformanceUtils.parseSizeToBytes(parts[1])
let total = upload + download
// Return upload percentage of total traffic
return total <= 0 ? 0 : max(0, min(1, upload / total))
}
}
struct PerformanceUtils {
// Precomputed multipliers for performance
private static let sizeMultipliers: [Character: Double] = [
"k": 1024,
"m": pow(1024, 2),
"g": pow(1024, 3),
"t": pow(1024, 4),
"p": pow(1024, 5)
]
static func parseSizeToBytes(_ text: String) -> Double {
let lower = text.lowercased().replacingOccurrences(of: "b", with: "")
let unitChar = lower.trimmingCharacters(in: .whitespaces).last
let numberPart: String
let multiplier: Double
if let u = unitChar, let mult = sizeMultipliers[u] {
multiplier = mult
numberPart = String(lower.dropLast())
} else {
multiplier = 1.0
numberPart = lower
}
let value = Double(numberPart.trimmingCharacters(in: .whitespaces)) ?? 0
return value * multiplier
}
static func percentStr(_ value: Double) -> String {
let pct = max(0, min(1, value)) * 100
let rounded = (pct * 10).rounded() / 10
return rounded.truncatingRemainder(dividingBy: 1) == 0
? String(format: "%.0f%%", rounded)
: String(format: "%.1f%%", rounded)
}
static func thresholdColor(_ value: Double) -> Color {
let v = max(0, min(1, value))
switch v {
case ..<WidgetConstants.Thresholds.warningThreshold: return WidgetConstants.Colors.success
case ..<WidgetConstants.Thresholds.criticalThreshold: return WidgetConstants.Colors.warning
default: return WidgetConstants.Colors.critical
}
}
static func formatSize(_ bytes: Double) -> String {
let units = ["B", "KB", "MB", "GB", "TB"]
var size = bytes
var unitIndex = 0
while size >= 1024 && unitIndex < units.count - 1 {
size /= 1024
unitIndex += 1
}
return String(format: "%.1f %@", size, units[unitIndex])
}
}
struct Provider: IntentTimelineProvider { struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry { func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent(), state: .normal(demoStatus)) SimpleEntry(date: Date(), configuration: ConfigurationIntent(), state: .normal(demoStatus))
@@ -29,11 +165,13 @@ struct Provider: IntentTimelineProvider {
var url = configuration.url var url = configuration.url
let family = context.family let family = context.family
#if os(iOS)
if #available(iOSApplicationExtension 16.0, *) { if #available(iOSApplicationExtension 16.0, *) {
if family == .accessoryInline || family == .accessoryRectangular { if family == .accessoryInline || family == .accessoryRectangular {
url = UserDefaults.standard.string(forKey: accessoryKey) url = UserDefaults(suiteName: WidgetConstants.appGroupId)?.string(forKey: "accessory_widget_url")
} }
} }
#endif
let currentDate = Date() let currentDate = Date()
let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)! let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!
@@ -111,7 +249,7 @@ struct StatusWidgetEntryView : View {
Button(intent: RefreshIntent()) { Button(intent: RefreshIntent()) {
Image(systemName: "arrow.clockwise") Image(systemName: "arrow.clockwise")
.resizable() .resizable()
.frame(width: 10, height: 12.7) .frame(width: WidgetConstants.Dimensions.refreshIconSmall, height: WidgetConstants.Dimensions.refreshIconSmall * 1.27)
}.tint(.gray) }.tint(.gray)
} }
} }
@@ -123,6 +261,37 @@ struct StatusWidgetEntryView : View {
case .normal(let data): case .normal(let data):
let sumColor: Color = .primary.opacity(0.7) let sumColor: Color = .primary.opacity(0.7)
switch family { switch family {
case .systemMedium:
VStack(alignment: .leading, spacing: WidgetConstants.Spacing.normal) {
// Title + refresh
if #available(iOS 17.0, *) {
HStack {
Text(data.name).font(.system(.title3, design: .monospaced))
Spacer()
Button(intent: RefreshIntent()) {
Image(systemName: "arrow.clockwise")
.resizable()
.frame(width: WidgetConstants.Dimensions.refreshIconSmall, height: WidgetConstants.Dimensions.refreshIconSmall * 1.27)
}.tint(.gray)
}
} else {
Text(data.name).font(.system(.title3, design: .monospaced))
}
Spacer(minLength: WidgetConstants.Spacing.normal)
// Gauges row
HStack(spacing: WidgetConstants.Spacing.tight) {
GaugeTile(label: "CPU", value: ParseCache.parsePercent(data.cpu), display: data.cpu, diameter: WidgetConstants.Dimensions.smallGauge)
GaugeTile(label: "MEM", value: ParseCache.parseUsagePercent(data.mem), display: PerformanceUtils.percentStr(ParseCache.parseUsagePercent(data.mem)), diameter: WidgetConstants.Dimensions.smallGauge)
GaugeTile(label: "DISK", value: ParseCache.parseUsagePercent(data.disk), display: PerformanceUtils.percentStr(ParseCache.parseUsagePercent(data.disk)), diameter: WidgetConstants.Dimensions.smallGauge)
GaugeTile(label: "NET", value: ParseCache.parseNetworkPercent(data.net), display: ParseCache.parseNetworkTotal(data.net).displayText, diameter: WidgetConstants.Dimensions.smallGauge)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 3)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.autoPadding()
.widgetBackground()
#if os(iOS)
case .accessoryRectangular: case .accessoryRectangular:
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
HStack { HStack {
@@ -142,6 +311,7 @@ struct StatusWidgetEntryView : View {
.widgetBackground() .widgetBackground()
case .accessoryInline: case .accessoryInline:
Text("\(data.name) \(data.cpu)").widgetBackground() Text("\(data.name) \(data.cpu)").widgetBackground()
#endif
default: default:
VStack(alignment: .leading, spacing: 3.7) { VStack(alignment: .leading, spacing: 3.7) {
if #available(iOS 17.0, *) { if #available(iOS 17.0, *) {
@@ -151,7 +321,7 @@ struct StatusWidgetEntryView : View {
Button(intent: RefreshIntent()) { Button(intent: RefreshIntent()) {
Image(systemName: "arrow.clockwise") Image(systemName: "arrow.clockwise")
.resizable() .resizable()
.frame(width: 10, height: 12.7) .frame(width: WidgetConstants.Dimensions.refreshIconSmall, height: WidgetConstants.Dimensions.refreshIconSmall * 1.27)
}.tint(.gray) }.tint(.gray)
} }
} else { } else {
@@ -162,9 +332,6 @@ struct StatusWidgetEntryView : View {
DetailItem(icon: "memorychip", text: data.mem, color: sumColor) DetailItem(icon: "memorychip", text: data.mem, color: sumColor)
DetailItem(icon: "externaldrive", text: data.disk, color: sumColor) DetailItem(icon: "externaldrive", text: data.disk, color: sumColor)
DetailItem(icon: "network", text: data.net, color: sumColor) DetailItem(icon: "network", text: data.net, color: sumColor)
Spacer()
DetailItem(icon: "clock", text: entry.date.toStr(), color: sumColor)
.padding(.bottom, 3)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.autoPadding() .autoPadding()
@@ -177,8 +344,16 @@ struct StatusWidgetEntryView : View {
extension View { extension View {
@ViewBuilder @ViewBuilder
func widgetBackground() -> some View { func widgetBackground() -> some View {
// Set bg to black in Night, white in Day // Modern card-style background with subtle effects
let backgroundView = Color(bgColor.resolve()) let backgroundView = LinearGradient(
gradient: Gradient(colors: [
Color(bgColor.resolve()),
Color(bgColor.resolve()).opacity(0.95)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
if #available(iOS 17.0, *) { if #available(iOS 17.0, *) {
containerBackground(for: .widget) { containerBackground(for: .widget) {
backgroundView backgroundView
@@ -188,14 +363,29 @@ extension View {
} }
} }
// iOS 17 will auto add a SafeArea, so when iOS < 17, add .padding(.all, 17) // Enhanced padding with improved spacing
func autoPadding() -> some View { func autoPadding() -> some View {
if #available(iOS 17.0, *) { if #available(iOS 17.0, *) {
return self return self.padding(.all, WidgetConstants.Spacing.tight)
} else { } else {
return self.padding(.all, 17) return self.padding(.all, WidgetConstants.Spacing.extraLoose + 1)
} }
} }
// Modern card container with shadow and rounded corners
func modernCard(cornerRadius: CGFloat = WidgetConstants.Dimensions.cornerRadius) -> some View {
self
.background(
RoundedRectangle(cornerRadius: cornerRadius)
.fill(WidgetConstants.Colors.cardBackground)
.shadow(
color: .black.opacity(0.08),
radius: WidgetConstants.Dimensions.shadowRadius,
x: 0,
y: 1
)
)
}
} }
struct StatusWidget: Widget { struct StatusWidget: Widget {
@@ -207,11 +397,15 @@ struct StatusWidget: Widget {
} }
.configurationDisplayName("Status") .configurationDisplayName("Status")
.description("Status of your servers.") .description("Status of your servers.")
if #available(iOSApplicationExtension 16.0, *) { #if os(iOS)
return cfg.supportedFamilies([.systemSmall, .accessoryRectangular, .accessoryInline]) if #available(iOSApplicationExtension 16.0, *) {
return cfg.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular, .accessoryInline])
} else { } else {
return cfg.supportedFamilies([.systemSmall]) return cfg.supportedFamilies([.systemSmall, .systemMedium])
} }
#else
return cfg.supportedFamilies([.systemSmall, .systemMedium])
#endif
} }
} }
@@ -228,31 +422,176 @@ struct DetailItem: View {
let color: Color let color: Color
var body: some View { var body: some View {
HStack(spacing: 6.7) { HStack(spacing: WidgetConstants.Spacing.normal) {
Image(systemName: icon).resizable().foregroundColor(color).frame(width: 11, height: 11, alignment: .center) Image(systemName: icon)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(color.opacity(0.8))
.frame(width: 12, height: 12)
.background(
Circle()
.fill(color.opacity(0.1))
.frame(width: 20, height: 20)
)
Text(text) Text(text)
.font(.system(size: 11, design: .monospaced)) .font(.system(size: 12, weight: .medium, design: .rounded))
.foregroundColor(color) .foregroundColor(color)
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.padding(.horizontal, WidgetConstants.Spacing.tight)
.padding(.vertical, 2)
}
}
// Enhanced circular progress indicator
struct CirclePercent: View {
// eg: 31.7%
let percent: String
@State private var animatedProgress: Double = 0
var body: some View {
let percentD = Double(percent.trimmingCharacters(in: .init(charactersIn: "%")))
let progress = (percentD ?? 0) / 100
ZStack {
// Background circle
Circle()
.stroke(Color.primary.opacity(0.15), lineWidth: 2.5)
// Progress circle with gradient
Circle()
.trim(from: 0, to: CGFloat(max(0, min(1, animatedProgress))))
.stroke(
AngularGradient(
gradient: Gradient(colors: [
PerformanceUtils.thresholdColor(progress).opacity(0.7),
PerformanceUtils.thresholdColor(progress)
]),
center: .center
),
style: StrokeStyle(lineWidth: 3, lineCap: .round)
)
.rotationEffect(.degrees(-90))
// Percentage text
Text(percent)
.font(.system(size: 8, weight: .bold, design: .rounded))
.foregroundColor(.primary.opacity(0.8))
}
.frame(width: 24, height: 24)
.onAppear {
withAnimation(.easeOut(duration: 0.8).delay(0.2)) {
animatedProgress = progress
}
}
.onChange(of: progress) { newProgress in
withAnimation(.easeInOut(duration: 0.5)) {
animatedProgress = newProgress
}
} }
} }
} }
// // Modern gauge tile with enhanced visual design
struct CirclePercent: View { struct GaugeTile: View {
// eg: 31.7% let label: String
let percent: String // 0..1
let value: Double
// eg: "31.7%"
let display: String
let diameter: CGFloat
@State private var animatedValue: Double = 0
var body: some View { var body: some View {
// 31.7% -> 0.317 VStack(spacing: WidgetConstants.Spacing.normal) {
let percentD = Double(percent.trimmingCharacters(in: .init(charactersIn: "%"))) ZStack {
let double = (percentD ?? 0) / 100 // Background circle with subtle shadow effect
Circle() Circle()
.trim(from: 0, to: CGFloat(double)) .stroke(Color.primary.opacity(0.1), lineWidth: 4)
.stroke(Color.primary, lineWidth: 3) .background(
.animation(.easeInOut(duration: 0.5)) Circle()
.fill(WidgetConstants.Colors.cardBackground)
.shadow(color: .black.opacity(0.05), radius: WidgetConstants.Dimensions.shadowRadius, x: 0, y: 1)
)
// Progress arc with gradient effect
Circle()
.trim(from: 0, to: CGFloat(max(0, min(1, animatedValue))))
.stroke(
AngularGradient(
gradient: Gradient(colors: [
PerformanceUtils.thresholdColor(value).opacity(0.8),
PerformanceUtils.thresholdColor(value)
]),
center: .center,
startAngle: .degrees(-90),
endAngle: .degrees(270)
),
style: StrokeStyle(lineWidth: 5, lineCap: .round)
)
.rotationEffect(.degrees(-90))
// Center value text with improved typography
Text(display)
.font(.system(size: diameter < 60 ? 11 : 13, weight: .bold, design: .rounded))
.foregroundColor(.primary)
.minimumScaleFactor(0.8)
.lineLimit(1)
}
.frame(width: diameter, height: diameter)
.onAppear {
withAnimation(.easeOut(duration: 0.8).delay(0.1)) {
animatedValue = value
}
}
.onChange(of: value) { newValue in
withAnimation(.easeInOut(duration: 0.6)) {
animatedValue = newValue
}
}
// Label with enhanced styling
if #available(iOS 16.0, *) {
Text(label)
.font(.system(size: 11, weight: .medium, design: .rounded))
.foregroundColor(WidgetConstants.Colors.secondaryText)
.textCase(.uppercase)
.tracking(0.5)
} else {
Text(label)
.font(.system(size: 11, weight: .medium, design: .rounded))
.foregroundColor(WidgetConstants.Colors.secondaryText)
.textCase(.uppercase)
}
}
.frame(maxWidth: .infinity)
} }
} }
// Legacy functions maintained for compatibility - now delegate to optimized versions
func parsePercent(_ text: String) -> Double {
return ParseCache.parsePercent(text)
}
func parseUsagePercent(_ text: String) -> Double {
return ParseCache.parseUsagePercent(text)
}
func parseSizeToBytes(_ text: String) -> Double {
return PerformanceUtils.parseSizeToBytes(text)
}
func percentStr(_ value: Double) -> String {
return PerformanceUtils.percentStr(value)
}
func thresholdColor(_ value: Double) -> Color {
return PerformanceUtils.thresholdColor(value)
}
struct DynamicColor { struct DynamicColor {
let dark: UIColor let dark: UIColor
let light: UIColor let light: UIColor

View File

@@ -12,5 +12,8 @@ import SwiftUI
struct StatusWidgetBundle: WidgetBundle { struct StatusWidgetBundle: WidgetBundle {
var body: some Widget { var body: some Widget {
StatusWidget() StatusWidget()
if #available(iOSApplicationExtension 16.1, *) {
TerminalLiveActivity()
}
} }
} }

View File

@@ -0,0 +1,185 @@
//
// TerminalLiveActivity.swift
// StatusWidget
//
// Renders the Live Activity UI for SSH/Terminal sessions.
//
import SwiftUI
import WidgetKit
import ActivityKit
// Helper to map status strings to a color dot (case-insensitive).
@inline(__always)
private func getStatusDotColor(_ status: String) -> Color {
switch status.lowercased() {
case "connected":
return .green
case "connecting":
return .yellow
case "disconnected":
return .red
default:
return .secondary
}
}
// Normalize status for display: capitalize first letter only.
@inline(__always)
private func formatStatus(_ status: String) -> String {
let trimmed = status.trimmingCharacters(in: .whitespacesAndNewlines)
guard let first = trimmed.first else { return status }
let head = String(first).uppercased()
let tail = String(trimmed.dropFirst()).lowercased()
return head + tail
}
// Localize known statuses; fall back to formatted original.
@inline(__always)
private func localizedStatus(_ status: String) -> String {
switch status.lowercased() {
case "connected":
return NSLocalizedString("Connected", comment: "Session connected status")
case "connecting":
return NSLocalizedString("Connecting", comment: "Session connecting status")
case "disconnected":
return NSLocalizedString("Disconnected", comment: "Session disconnected status")
default:
return formatStatus(status)
}
}
@available(iOS 16.1, *)
struct TerminalLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TerminalAttributes.self) { context in
let state = context.state
HStack(alignment: .center, spacing: 12) {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 6) {
Text(state.hasTerminal ? NSLocalizedString("Terminal", comment: "Terminal label") : "SSH")
.font(.caption)
.foregroundStyle(.secondary)
if state.connectionCount > 1 {
Text("(\(state.connectionCount))")
.font(.caption)
.foregroundStyle(.secondary)
}
}
Text(state.title)
.font(.headline)
.lineLimit(1)
.truncationMode(.tail)
Text(state.subtitle)
.font(.subheadline)
.lineLimit(1)
.foregroundStyle(.secondary)
HStack(spacing: 8) {
Circle()
.fill(getStatusDotColor(state.status))
.frame(width: 6, height: 6)
Text(localizedStatus(state.status))
.font(.caption)
.foregroundStyle(.secondary)
}
}
Spacer(minLength: 8)
Image(systemName: state.hasTerminal ? "terminal" : "bolt.horizontal.circle")
.font(.title3)
.foregroundStyle(.secondary)
}
.padding(.horizontal)
.padding(.vertical, 10)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Text(context.state.hasTerminal ? NSLocalizedString("Terminal", comment: "Terminal label") : "SSH")
.font(.caption2)
.foregroundStyle(.secondary)
if context.state.connectionCount > 1 {
Text("(\(context.state.connectionCount))")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
Text(context.state.title)
.font(.subheadline)
.lineLimit(1)
.truncationMode(.tail)
}
.padding(.vertical, 8)
.padding(.horizontal, 8)
}
DynamicIslandExpandedRegion(.trailing) {
VStack(alignment: .trailing, spacing: 6) {
HStack(spacing: 6) {
Circle()
.fill(getStatusDotColor(context.state.status))
.frame(width: 6, height: 6)
Text(localizedStatus(context.state.status))
.font(.caption2)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 8)
.padding(.horizontal, 8)
}
DynamicIslandExpandedRegion(.bottom) {
Text(context.state.subtitle)
.font(.caption)
.lineLimit(1)
.foregroundStyle(.secondary)
}
} compactLeading: {
Image(systemName: context.state.hasTerminal ? "terminal" : "bolt.horizontal.circle")
} compactTrailing: {
EmptyView()
} minimal: {
Image(systemName: context.state.hasTerminal ? "terminal" : "bolt.horizontal.circle")
}
}
}
}
#if DEBUG
@available(iOS 16.2, *)
struct TerminalLiveActivity_Previews: PreviewProvider {
static let attributes = TerminalAttributes(id: "preview")
static let contentState = TerminalAttributes.ContentState(
id: "preview",
title: "root@server-01",
subtitle: "CPU 37% • Mem 1.3G/2.0G",
status: "Connected",
startTime: Date().addingTimeInterval(-1234),
hasTerminal: true,
connectionCount: 2
)
static var previews: some View {
Group {
// /
attributes
.previewContext(contentState, viewKind: .content)
.previewDisplayName("Lock Screen")
// 屿
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
.previewDisplayName("Dynamic Island • Expanded")
// 屿
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
.previewDisplayName("Dynamic Island • Compact")
// 屿
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.minimal))
.previewDisplayName("Dynamic Island • Minimal")
}
}
}
#endif

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