1
0
mirror of https://github.com/TheFunny/ArisuAutoSweeper synced 2026-06-23 20:34:51 +00:00

204 Commits

Author SHA1 Message Date
YoursFunny b606108871 fix: update assets file 2026-04-24 19:30:05 +08:00
YoursFunny be09b39bbe fix: update ui assets for jp 2026-04-24 19:26:36 +08:00
YoursFunny fafb9d5a5c fix: update assets file 2026-01-20 19:45:18 +08:00
YoursFunny 998d52b56c fix: update ui assets for jp 2026-01-20 19:43:55 +08:00
YoursFunny 3e6a82764a fix(login): disable clicking back when login for jp 2025-10-27 14:30:45 +08:00
YoursFunny 3cd8161afb feat(momotalk): add a single tool page for momotalk,
remove its scheduler,
and some minor improvements
2025-10-13 11:41:48 +08:00
YoursFunny 02b8de6d18 fix: update assets file 2025-09-23 20:02:23 +08:00
YoursFunny 7a321eae48 fix: update configs to enable new bounty stage for OVERSEA server 2025-09-23 20:01:32 +08:00
YoursFunny ad19268e5a fix: update ui assets for zht 2025-09-23 19:59:49 +08:00
YoursFunny 8bc170e170 fix: update ui assets for en 2025-09-23 19:59:26 +08:00
YoursFunny 1d618d1624 fix: update assets file 2025-08-09 13:19:41 +08:00
YoursFunny 9eb3bde649 fix: update ui assets for OVERSEA server 2025-08-09 13:18:23 +08:00
YoursFunny 62ef4c733c fix: update assets file 2025-07-22 20:13:27 +08:00
YoursFunny 27dbd31434 fix: update ui assets for jp 2025-07-22 20:12:46 +08:00
YoursFunny 74ef1a7b4d fix: update configs 2025-05-18 19:57:13 +08:00
YoursFunny 43282354cd fix(bounty): update stage select option 2025-05-18 19:53:27 +08:00
YoursFunny f92d92aec0 fix: update assets file 2025-05-13 21:54:43 +08:00
YoursFunny 3a00c47125 fix(cafe): update ui assets for zht 2025-05-13 21:53:18 +08:00
YoursFunny bca3395cb9 fix(cafe): update ui assets for jp & en 2025-05-13 21:53:08 +08:00
YoursFunny 766cb78377 fix(cafe): synchronize Cafe No.2 for global server 2025-05-13 21:52:07 +08:00
YoursFunny 84acb67061 fix: update assets file 2025-04-22 19:53:10 +08:00
YoursFunny 9c07251669 fix: update ui assets for jp 2025-04-22 19:52:59 +08:00
YoursFunny 19272d6cbe fix(cafe): correct invitation detection of Cafe No.2 in OVERSEA server 2025-03-08 18:43:25 +08:00
YoursFunny f268cfeda8 fix: update assets file 2025-01-29 16:41:31 +08:00
YoursFunny 37b5d4e4d2 fix(cafe): update ui assets for zht 2025-01-29 16:40:32 +08:00
YoursFunny 29bd71ccc8 fix(cafe): adjust timer of Cafe No.2 2025-01-22 15:01:49 +08:00
YoursFunny d5f4891d4e fix(cafe): adjust click template offset 2025-01-22 15:01:20 +08:00
YoursFunny 9a500b9191 fix: update assets file 2025-01-20 23:20:29 +08:00
YoursFunny 180fa6aa7d fix(cafe): update ui assets for jp 2025-01-20 23:19:46 +08:00
YoursFunny 4325118562 fix(cafe): update latest Cafe No.2 switch for jp 2025-01-20 23:19:45 +08:00
YoursFunny 2adf3f9efe fix: update assets file 2024-11-12 18:16:27 +08:00
YoursFunny 648bbb8a63 fix(cafe): update ui assets for zht 2024-11-12 18:16:05 +08:00
YoursFunny f73bba3e20 fix(cafe): update ui assets for en 2024-11-12 18:16:05 +08:00
YoursFunny e321fd7e28 lang(cafe): correct Cafe No.2 description 2024-11-12 18:16:05 +08:00
YoursFunny 6215f061c8 fix(cafe): enable Cafe No.2 for OVERSEA server 2024-11-12 18:09:13 +08:00
YoursFunny 72a948da68 fix: update assets file 2024-10-23 23:35:14 +08:00
YoursFunny c69a8543c6 fix: update ui assets for en
(cherry picked from commit a5d20a95be)
2024-10-23 23:34:54 +08:00
YoursFunny c16e50157e fix: update ui assets for zht 2024-10-23 23:29:51 +08:00
YoursFunny 2cd705b5ba fix: update assets file 2024-09-24 14:08:29 +08:00
YoursFunny f70e75f1a6 fix: update ui assets for en
(cherry picked from commit daf55b76e2)
2024-09-24 14:07:36 +08:00
YoursFunny 861cf9a2da fix: update ui assets for zht 2024-09-24 13:58:49 +08:00
YoursFunny 437977ef63 fix(circle): improve circle enter 2024-09-03 15:10:36 +08:00
YoursFunny 123c248f43 fix: update assets file 2024-09-03 15:07:40 +08:00
YoursFunny 1461cc41c6 fix(circle): update ui assets 2024-09-03 15:07:11 +08:00
YoursFunny 34af75245e fix(tc): improve stability of reward 2024-08-10 17:49:26 +08:00
YoursFunny 6e3d5fc4c5 fix(popup): update assets files 2024-08-10 17:48:25 +08:00
YoursFunny 16d5f214d8 fix(popup): update ui assets 2024-08-10 17:48:07 +08:00
YoursFunny f669130e33 fix(cafe): improve stability of closing reward window 2024-08-10 17:10:46 +08:00
YoursFunny 8fe8ed5f86 fix: update assets file 2024-07-24 17:32:37 +08:00
YoursFunny e070bb839e fix: update ui assets for zht 2024-07-24 17:32:28 +08:00
YoursFunny 90c47745d7 fix: update ui assets and circle for en
(cherry picked from commit 2ec7e45172)
2024-07-24 17:28:34 +08:00
YoursFunny e832e3c27f fix: update assets file 2024-07-11 18:04:45 +08:00
YoursFunny 3391874151 fix: update jp ui assets 2024-07-11 18:03:54 +08:00
YoursFunny b02528f14b fix: update assets file 2024-07-09 16:09:29 +08:00
YoursFunny addb984c8e fix: update zht ui assets 2024-07-09 16:08:52 +08:00
YoursFunny 1d837b5441 fix: update en ui assets 2024-07-09 16:05:38 +08:00
YoursFunny 99a82242d7 fix: correct tray tooltip 2024-05-07 16:40:28 +08:00
YoursFunny b2ae30b45c fix(ocr): force substitute % with / in DigitCounter 2024-05-05 16:08:35 +08:00
YoursFunny bd0749c058 fix: update assets file 2024-05-04 20:06:26 +08:00
YoursFunny d87827a098 fix: update en ui assets 2024-05-04 20:04:25 +08:00
YoursFunny 92feadc228 fix: update assets file 2024-05-04 19:54:04 +08:00
YoursFunny 4c020dbfd3 fix: update en ui assets 2024-05-04 19:52:49 +08:00
YoursFunny 67b9cfee2d fix: update zht ui assets 2024-05-04 19:44:39 +08:00
YoursFunny 76f80bb6b1 fix: update ui assets for jp 2024-04-24 19:34:46 +08:00
YoursFunny ac2cfe3974 fix: update ui assets for jp 2024-03-27 20:21:40 +08:00
YoursFunny f4fd4a4d86 fix: update ui assets for jp 2024-02-21 19:51:10 +08:00
YoursFunny 07261d5438 fix: update zht assets 2024-01-30 23:02:10 +08:00
YoursFunny 5d9c14b5d4 fix: change network reconnect button 2024-01-26 23:25:05 +08:00
YoursFunny daca32d4fe fix: update ui assets and circle for jp 2024-01-25 19:50:04 +08:00
YoursFunny a5d478ce56 fix(task): add correct complete check for jp 2024-01-15 21:57:51 +08:00
YoursFunny 830bc8d211 lang: update zh gui 2024-01-15 21:51:57 +08:00
YoursFunny 08bb139476 fix: disable year detection for jp temporarily 2024-01-15 21:49:23 +08:00
YoursFunny 92702fdc04 feat: add log when sending back 2024-01-15 21:36:32 +08:00
YoursFunny 55b4dc9748 feat: add assets for JP 2024-01-15 20:55:50 +08:00
YoursFunny e6a3b79733 style: format 2024-01-15 19:25:15 +08:00
RedDeadDepresso e5f91e0c0a chore: updated GUI 2024-01-15 17:45:36 +08:00
RedDeadDepresso 8287822fe4 feat: momotalk zht 2024-01-15 17:45:36 +08:00
RedDeadDepresso 1c2ebf6b8b docs: updated gui 2024-01-15 17:45:36 +08:00
RedDeadDepresso d713121a86 fix: ui 2024-01-15 17:45:36 +08:00
RedDeadDepresso de73446f73 fix: distinguish task and campaign in zht 2024-01-15 17:45:35 +08:00
RedDeadDepresso e7878d63b0 chore: added comments in ui 2024-01-15 17:45:35 +08:00
RedDeadDepresso 9927f0550b fix: ui 2024-01-15 17:45:35 +08:00
RedDeadDepresso c4c7df1f21 feat: start and exit ldplayer9 2024-01-15 17:45:35 +08:00
RedDeadDepresso 4fa4478d77 feat: added quit for zht 2024-01-15 17:45:35 +08:00
RedDeadDepresso ccd9466b77 fix: shop 2024-01-15 17:45:34 +08:00
RedDeadDepresso a8caafb292 fix: login 2024-01-15 17:45:34 +08:00
RedDeadDepresso 7e1070e740 perf: mission 2024-01-15 17:45:34 +08:00
RedDeadDepresso 01a3fdfce5 feat: 7 choices in lesson 2024-01-15 17:45:34 +08:00
RedDeadDepresso 9d0c276db5 perf: mission 2024-01-15 17:45:34 +08:00
RedDeadDepresso fd4ec3aff0 fix: back in ui 2024-01-15 17:45:34 +08:00
RedDeadDepresso 0cbde8077e fix: exit aas 2024-01-15 17:45:33 +08:00
RedDeadDepresso 7c1620c0f0 fix: exit aas 2024-01-15 17:45:33 +08:00
RedDeadDepresso 6cca8f1082 perf: login 2024-01-15 17:45:33 +08:00
RedDeadDepresso a74d05aeda fix: login 2024-01-15 17:45:33 +08:00
RedDeadDepresso 31a69f11ed fix: login 2024-01-15 17:45:33 +08:00
RedDeadDepresso 726662d8b7 fix: schedule 2024-01-15 17:45:33 +08:00
RedDeadDepresso 1aa9a50e62 fix: lesson 2024-01-15 17:45:32 +08:00
RedDeadDepresso 08ee060f34 fix: player selection in tactical challenge 2024-01-15 17:45:32 +08:00
RedDeadDepresso 4bb95b61ff fix: login 2024-01-15 17:45:32 +08:00
RedDeadDepresso 17ee226a5a perf: tasks 2024-01-15 17:45:32 +08:00
RedDeadDepresso a1af2b0b74 perf: lesson 2024-01-15 17:45:32 +08:00
RedDeadDepresso e5fe0d096c fix: commissions 2024-01-15 17:45:32 +08:00
RedDeadDepresso f4ad80d17e feat: traditional chinese 2024-01-15 17:45:31 +08:00
RedDeadDepresso e28b2f1e2e chore: updated lesson help 2024-01-15 17:45:31 +08:00
RedDeadDepresso 7d03615916 chore: updated gui cafe and lesson help 2024-01-15 17:45:31 +08:00
RedDeadDepresso 0184376076 perf: shop 2024-01-15 17:45:31 +08:00
RedDeadDepresso 17851b86de fix: schedule popups 2024-01-15 17:45:31 +08:00
RedDeadDepresso b5d2c13259 feat: added level up and location level up popup 2024-01-15 17:45:30 +08:00
RedDeadDepresso 659172cdd3 fix: exit emulator, aas, shutdown
allow tactical challenge to be completed
2024-01-15 17:45:30 +08:00
RedDeadDepresso ec543c6db2 chore: updated gui task queue empty 2024-01-15 17:45:30 +08:00
RedDeadDepresso a02c05bd69 fix: exit aas & emulator 2024-01-15 17:45:30 +08:00
RedDeadDepresso c674c7a53b feat: exit AAS & emulator 2024-01-15 17:45:30 +08:00
RedDeadDepresso 1ab7c5c40f fix: bluestacks start emulator 2024-01-15 17:45:30 +08:00
RedDeadDepresso 64d63bdd24 test: exit emulator 2024-01-15 17:45:29 +08:00
RedDeadDepresso f932b2ac10 feat: shutdown when task queue empty 2024-01-15 17:45:29 +08:00
RedDeadDepresso 73568fe48c feat: add up in MCE manager 2024-01-15 17:45:29 +08:00
RedDeadDepresso 0d35587940 fix: MCE Manager tooltip 2024-01-15 17:45:29 +08:00
RedDeadDepresso ee87f92252 perf: momotalk 2024-01-15 17:45:29 +08:00
RedDeadDepresso a627f76197 chore: updated requirements.txt 2024-01-15 17:45:28 +08:00
RedDeadDepresso d7722c044e fix: momotalk 2024-01-15 17:45:28 +08:00
RedDeadDepresso 9bf970e5fb fix: lesson classroom order 2024-01-15 17:45:28 +08:00
RedDeadDepresso c4e12c4194 fix: mission AP calculations 2024-01-15 17:45:28 +08:00
RedDeadDepresso 92aaac7b5a fix: mission event 2024-01-15 17:45:28 +08:00
RedDeadDepresso 782e61ad9a fix: shop 2024-01-15 17:45:28 +08:00
RedDeadDepresso 149e6ea1ef chore: updated lesson gui section 2024-01-15 17:45:27 +08:00
RedDeadDepresso 69bff3f757 feat: 7 hours mission delay 2024-01-15 17:45:27 +08:00
RedDeadDepresso 9b9d0a5bcd fix: mission ocr 2024-01-15 17:45:27 +08:00
RedDeadDepresso 10607f9c3d fix: shop 2024-01-15 17:45:27 +08:00
RedDeadDepresso 390082fa50 fix: shop 2024-01-15 17:45:27 +08:00
RedDeadDepresso ca1b1b2efd fix: lesson 2024-01-15 17:45:27 +08:00
RedDeadDepresso 4817b6768f chore: added licenses 2024-01-15 17:45:26 +08:00
RedDeadDepresso 9c7fc247a1 refactor: mission 2024-01-15 17:45:26 +08:00
RedDeadDepresso fe7c6f92a8 feat:lesson 2024-01-15 17:45:26 +08:00
RedDeadDepresso 93b7ca8cfc feat: input helper
Just a small program for users to quickly find students name.
2024-01-15 17:45:26 +08:00
RedDeadDepresso 151a085a9c fix: task 2024-01-15 17:45:25 +08:00
RedDeadDepresso a241484e24 fix: sweep event 2024-01-15 17:45:25 +08:00
RedDeadDepresso 3ac2009737 feat: added assets event 2024-01-15 17:45:25 +08:00
RedDeadDepresso 59b310b6df fix: task for EN 2024-01-15 17:45:25 +08:00
RedDeadDepresso fb02cedd8d refactor: MCE Manager
changed queue thread into daemon thread
2024-01-15 17:45:25 +08:00
RedDeadDepresso 204c56efac fix: close random popup in main after login 2024-01-15 17:45:25 +08:00
RedDeadDepresso fc1edefa79 feat: auto-generate MCE/config.json 2024-01-15 17:45:24 +08:00
RedDeadDepresso a915ce396b revert changes in task 2024-01-15 17:45:24 +08:00
RedDeadDepresso e4afe9a112 Update .gitignore 2024-01-15 17:45:24 +08:00
RedDeadDepresso d34a8a39d5 fix: updated regex for mission 2024-01-15 17:45:24 +08:00
RedDeadDepresso 9aaf1e8f3b fix: tasks 2024-01-15 17:45:24 +08:00
RedDeadDepresso bcbe10d291 feat: mission/commissions/event 2024-01-15 17:45:23 +08:00
YoursFunny 69a612dc55 fix(tc): change refresh time 2024-01-15 17:45:23 +08:00
YoursFunny b8b59e7dbc feat: add tasks assets for jp 2024-01-15 17:45:23 +08:00
YoursFunny 41e2d188b9 fix(momotalk): correct sort switch 2024-01-15 17:45:23 +08:00
YoursFunny 107392a900 fix(momotalk): correct sidebar switch 2024-01-15 17:45:23 +08:00
YoursFunny 8ccb3d3d22 fix(task): check all claimed 2024-01-15 17:45:22 +08:00
YoursFunny 694d88a339 lang: add zh for new settings 2024-01-15 17:45:22 +08:00
RedDeadDepresso 17964cbd9a Added Tasks, Shop, MomoTalk (#11)
* feat: tasks

Added module tasks for EN

* refactor: gui

added tree view Farm and Reward.

* feat: shop

* feat: momotalk

---------

Co-authored-by: YoursFunny <admin@yoursfunny.top>
2024-01-15 17:45:22 +08:00
YoursFunny f26dffa221 perf: change tc priority 2024-01-15 17:44:59 +08:00
Cheong Sik Feng 2f44074400 Fix Tactical Challenge button mask image
With the addition of the Grand Assault, the Joint Firing Drill button was moved to the top half of where the Tactical Challenge button previously was, and the new Tactical Challenge button is only the lower half.
2023-12-25 16:26:57 +08:00
YoursFunny 0f6dd93608 fix(sweep): remove index if equal 0 2023-12-09 21:22:11 +08:00
YoursFunny f5cf0a7fbe fix(tc): add handle ui_additional 2023-12-07 18:24:09 +08:00
YoursFunny 16d78e1e16 fix(sweep): correct load index situation 2023-12-07 13:04:05 +08:00
YoursFunny 7b707be841 refactor(scrimmage): use stage ap count 2023-12-06 17:11:50 +08:00
YoursFunny 93bf1f73e2 feat: separate stage ap count 2023-12-05 16:21:19 +08:00
YoursFunny 71da6fd996 feat: add auto select for bounty and scrimmage 2023-11-30 14:21:19 +08:00
YoursFunny ecd4ba0a7c doc: update readme 2023-11-29 18:38:59 +08:00
YoursFunny d684fd79f6 feat(sweep): support finding max sweepable index 2023-11-29 14:55:55 +08:00
YoursFunny 6ae785634c refactor(sweep): separate sweepable check and search_box generate 2023-11-29 14:11:51 +08:00
YoursFunny a0d3fd75af refactor(sweep): change current_indexes 2023-11-29 14:05:50 +08:00
YoursFunny 257e092936 refactor(sweep): use regex for index match 2023-11-29 13:54:55 +08:00
YoursFunny 32cee3f6b5 fix(cafe): adjust name of invitation 2023-11-29 12:39:51 +08:00
YoursFunny 659f58db38 fix(cafe): add check of null invitation name 2023-11-28 21:46:21 +08:00
YoursFunny 6dac8a1de2 feat(cafe): add invitation assets for en 2023-11-28 20:53:24 +08:00
YoursFunny b1aeb64768 lang: change option description 2023-11-28 20:53:01 +08:00
YoursFunny 88e8a98b76 fix(cafe): apply config of choice only when not set before 2023-11-28 16:18:34 +08:00
YoursFunny 39d00ac549 fix(cafe): correct target name check 2023-11-28 16:11:50 +08:00
YoursFunny 11d2a6ef7e feat(cafe): add invitation condition check 2023-11-28 14:04:37 +08:00
YoursFunny 1f5b68d095 feat(webui): add invitation condition options 2023-11-28 14:04:07 +08:00
YoursFunny 5f3ff140dd feat(cafe): add invitation 2023-11-27 22:02:50 +08:00
YoursFunny 8698fa20c2 feat(webui): add config of invitation options 2023-11-27 20:48:37 +08:00
YoursFunny b426c6caac feat(cafe): add invitation assets for jp 2023-11-27 15:44:56 +08:00
YoursFunny 8d83ec1657 fix: add swipe name 2023-11-27 15:26:27 +08:00
YoursFunny 7fcda15329 perf(sweep): improve list insight logic 2023-11-27 13:17:29 +08:00
YoursFunny 54aa1bafb5 refactor(sweep): simplify list enter match 2023-11-26 18:54:26 +08:00
YoursFunny d756b0dc3f perf: reduce config save times 2023-11-25 15:28:57 +08:00
YoursFunny e71118c09e fix: improve data update stability 2023-11-25 15:28:07 +08:00
YoursFunny 44b6d5cdf8 fix: expand ticket ocr region 2023-11-25 14:27:01 +08:00
YoursFunny f92dba92c9 fix(sweep): use swipe instead of drag in list 2023-11-24 14:30:43 +08:00
YoursFunny 86ce04cff9 fix(sweep): extend timer stable 2023-11-24 13:59:39 +08:00
YoursFunny f2065507c2 fix(ui): change task order 2023-11-23 16:37:16 +08:00
YoursFunny 15bf77da3d fix(cafe): expand search box 2023-11-23 16:21:58 +08:00
YoursFunny 4ce8073096 perf(sweep): simplify sweep num ocr 2023-11-23 16:08:51 +08:00
YoursFunny aa872c890d doc: update readme 2023-11-23 14:45:31 +08:00
YoursFunny 25e0559171 feat: add oversea servers 2023-11-23 14:23:52 +08:00
YoursFunny df6da1f77a fix(cafe): adjust property of second cafe setting 2023-11-23 14:06:38 +08:00
YoursFunny fc49adc859 doc: update readme 2023-11-22 22:23:09 +08:00
YoursFunny 4582406ef2 perf(tc): improve status check stability 2023-11-22 22:04:50 +08:00
YoursFunny 256dc96598 fix(tc): restrict count frequency of claim reward 2023-11-22 22:01:07 +08:00
YoursFunny 04744d6f8c fix(scrimmage): add missing multiply of ap count 2023-11-22 22:01:06 +08:00
YoursFunny 8fe578615d fix: change webui port 2023-11-22 21:02:00 +08:00
YoursFunny 5e9615542c fix: set repo when update 2023-11-22 20:47:18 +08:00
YoursFunny 36c5f60eb3 fix(cafe): adjust property of second cafe setting 2023-11-22 19:05:36 +08:00
YoursFunny 91650cc584 feat: add en assets for bounty scrimmage and sweep 2023-11-22 19:04:50 +08:00
YoursFunny 67881568dd perf(button): combine shared assets 2023-11-22 13:13:39 +08:00
YoursFunny f8404edd9e fix: set repo of build-in update 2023-11-21 23:15:06 +08:00
YoursFunny ff3ec041d2 refactor(cafe): separate ui operation and simplify template extraction 2023-11-21 22:18:28 +08:00
YoursFunny 99074a1575 feat(cafe): add template search area 2023-11-21 21:31:01 +08:00
YoursFunny 7862fa6cb8 fix(scrimmage): fix typo 2023-11-21 20:56:39 +08:00
615 changed files with 10788 additions and 1172 deletions
+1
View File
@@ -20,6 +20,7 @@ config/reloadalas
test.py
test/
note.md
MCE/config.json
# Created by .ignore support plugin (hsz.mobi)
+102
View File
@@ -0,0 +1,102 @@
import customtkinter
import json
from MCE.custom_widgets.ctk_scrollable_dropdown import CTkScrollableDropdown
import os
from tkinter import END
class InputHelper(customtkinter.CTk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.create_tabview()
self.create_invite_student_widgets()
self.create_lessons_widgets()
def create_tabview(self):
self.tabview = customtkinter.CTkTabview(master=self)
self.tabview.grid(row=0, column=0)
self.cafe_tab = self.tabview.add("Cafe") # add tab at the end
self.lessons_tab = self.tabview.add("Lessons") # add tab at the end
self.tabview.set("Cafe") # set currently visible
def create_invite_student_widgets(self):
self.invitation_label = customtkinter.CTkLabel(master=self.cafe_tab, text="Copy and paste this in AAS Invitation Settings:")
self.invitation_label.grid(row=0, column=0, padx=60)
self.invitation_entry = customtkinter.CTkEntry(master=self.cafe_tab, width=500)
self.invitation_entry.grid(row=1, column=0)
self.invite_copy_button = customtkinter.CTkButton(master=self.cafe_tab, text="Copy", width=40, command=lambda : self.copy_entry(self.invitation_entry, self.invite_copy_button))
self.invite_copy_button.grid(row=1, column=3, padx=5)
self.invite_clear_button = customtkinter.CTkButton(master=self.cafe_tab, text="Clear", width=40, fg_color="crimson", command=lambda : self.invitation_entry.delete(0, END))
self.invite_clear_button.grid(row=1, column=4, padx=5)
self.invite_frame = customtkinter.CTkFrame(master=self.cafe_tab, fg_color="transparent")
self.invite_frame.grid(row=2, column=0, padx=20, pady=20)
self.server_dropdown = customtkinter.CTkOptionMenu(master=self.invite_frame, values=self.find_json_files("MCE/student_list"), command=self.switch_server, width=40)
self.server_dropdown.grid(row=0, column=0)
self.student_entry = customtkinter.CTkComboBox(master=self.invite_frame, width=300)
self.student_entry.grid(row=0, column=1, padx=(50,0))
self.student_dropdown = CTkScrollableDropdown(self.student_entry, width=300, height=550, autocomplete=True, command=lambda choice: self.insert(choice, self.invitation_entry), values=[""])
self.server_dropdown.set("EN")
self.switch_server("EN")
def create_lessons_widgets(self):
self.lessons_label = customtkinter.CTkLabel(master=self.lessons_tab, text="Copy and paste this in AAS Lessons Settings:")
self.lessons_label.grid(row=0, column=0, padx=60)
self.lessons_entry = customtkinter.CTkEntry(master=self.lessons_tab, width=500)
self.lessons_entry.grid(row=1, column=0)
self.lessons_copy_button = customtkinter.CTkButton(master=self.lessons_tab, text="Copy", width=40, command=lambda : self.copy_entry(self.lessons_entry, self.lessons_copy_button))
self.lessons_copy_button.grid(row=1, column=1, padx=5)
self.lessons_clear_button = customtkinter.CTkButton(master=self.lessons_tab, text="Clear", width=40, fg_color="crimson", command=lambda : self.lessons_entry.delete(0, END))
self.lessons_clear_button.grid(row=1, column=2, padx=5)
self.lessons_buttons_frame = customtkinter.CTkFrame(master=self.lessons_tab, fg_color="transparent")
self.lessons_buttons_frame.grid(row=2, column=0, padx=20, pady=20)
for i in range(9):
self.lesson_button = customtkinter.CTkButton(master=self.lessons_buttons_frame, text=str(i+1), command=lambda choice=str(i+1): self.insert(choice, self.lessons_entry), width=40)
self.lesson_button.grid(row=0, column=i, padx=5)
def find_json_files(self,folder_path):
json_files = []
for root, dirs, files in os.walk(folder_path):
for file in files:
if file.endswith(".json"):
json_files.append(os.path.splitext(file)[0])
return json_files
def switch_server(self, server):
with open(f"MCE/student_list/{server}.json", "r") as f:
student_list = json.load(f)
self.student_dropdown.configure(values=student_list)
def insert(self, value, entry):
entry.insert(index=END, string=value + " > ")
def copy_entry(self, entry, button):
text_to_copy = entry.get()
# Check if there is text to copy
if text_to_copy:
# Clear the clipboard and set the new text
self.clipboard_clear()
self.clipboard_append(text_to_copy)
self.update() # This is necessary on some systems to update the clipboard
button_color = button.cget("fg_color")
button.configure(fg_color="green")
self.after(2000, lambda : button.configure(fg_color=['#3B8ED0', '#1F6AA5']))
if __name__ == "__main__":
app = InputHelper()
app.title("Input Helper")
app.mainloop()
+442
View File
@@ -0,0 +1,442 @@
import customtkinter
import tkinter as tk
import random
import re
from MCE.custom_widgets.ctkmessagebox import CTkMessagebox
from MCE.custom_widgets.ctk_tooltip import CTkToolTip
from MCE.custom_widgets.ctk_timeentry import CTkTimeEntry
from MCE.custom_widgets.ctk_integerspinbox import CTkIntegerSpinbox
from MCE.custom_widgets.ctk_templatedialog import CTkTemplateDialog
from MCE.custom_widgets.ctk_notification import CTkNotification
from MCE.custom_widgets.ctk_add_button import CTkAddButton
from MCE.utils import Linker, Config
from filelock import FileLock, Timeout
import threading
import time
class MCE_Manager(customtkinter.CTk):
def __init__(self, linker, config, **kwargs):
super().__init__(**kwargs)
self.linker = linker
self.config = config
self.create_widgets()
# Load Template Data
self.load_template_data()
# Load queue Data
self.load_queue_data()
def create_widgets(self):
self.create_mission_commissions_widgets()
def create_mission_commissions_widgets(self):
# Create Mission/Commissions/Event Checkbox
self.create_mission_commissions_checkbox()
# Create Reset Daily Widgets
self.create_reset_daily_widgets()
# Create Recharge AP and Event Checkboxes
self.create_recharge_and_event_checkboxes()
# Create Preferred Template Selection
self.create_preferred_template_selection()
# Create Mission Tabview with Template and Queue Tabs
self.create_mission_tabview()
# Create Top-Level Window for Template Editing
self.create_template_queue_editor()
# Create Template Frame and Queue Frame
self.create_template_and_queue_frames()
# Create Lists to Store Frame Widgets
self.create_frame_lists()
# Initialize Preferred Template and Templates List
self.initialize_preferred_template()
# Create OptionMenu for Selecting a Template
self.create_template_option_menu()
# Create Delete Template Button
self.create_delete_template_button()
# Helper method to create Mission/Commissions/Event Checkbox
def create_mission_commissions_checkbox(self):
self.mission_commissions_checkbox = customtkinter.CTkLabel(self, text="Mission/Commissions/Event", width=60, font=customtkinter.CTkFont(family="Inter", size=20, weight="bold"))
self.mission_commissions_checkbox.grid(row=11, column=0, sticky="nw", padx=20, pady=20)
self.notification = CTkNotification(master=self, text="Config saved")
self.notification.grid(row=11, column=1)
# Helper method to create Reset Daily Widgets
def create_reset_daily_widgets(self):
self.reset_daily = customtkinter.CTkCheckBox(self, text="Reset Daily", font=customtkinter.CTkFont(family="Inter", size=16, underline=True), command=lambda x=["ResetDaily"]: self.config.save_to_json(x))
self.reset_daily.grid(row=12, column=0, sticky="nw", padx=80)
self.reset_daily_tooltip = CTkToolTip(self.reset_daily, wraplength=400,
message="If enabled and if current time >= reset time,\
the queue will automatically be cleared and repopulated with preferred template stages. Only activated once a day.")
self.reset_daily_sub_label = customtkinter.CTkLabel(self, text="hh/mm/ss", font=customtkinter.CTkFont(family="Inter", size=12))
self.reset_daily_sub_label.grid(row=13, column=0, padx=80)
self.reset_time = CTkTimeEntry(self)
self.reset_time.grid(row=12, column=1)
self.reset_time.hour_entry.bind("<KeyRelease>", lambda event, x=["ResetTime"]: self.config.save_to_json(x))
self.reset_time.minute_entry.bind("<KeyRelease>", lambda event, x=["ResetTime"]: self.config.save_to_json(x))
self.reset_time.second_entry.bind("<KeyRelease>", lambda event, x=["ResetTime"]: self.config.save_to_json(x))
self.linker.widgets["ResetDaily"] = self.reset_daily
self.linker.widgets["ResetTime"] = self.reset_time
# Helper method to create Recharge AP and Event Checkboxes
def create_recharge_and_event_checkboxes(self):
self.recharge_checkbox = customtkinter.CTkCheckBox(self, text="Recharge AP", command=lambda x=["RechargeAP"]: self.config.save_to_json(x), font=customtkinter.CTkFont(family="Inter", size=16, underline=True))
self.recharge_checkbox.grid(row=14, column=0, sticky="nw", padx=80, pady=20)
self.linker.widgets["RechargeAP"] = self.recharge_checkbox
self.recharge_tooltip = CTkToolTip(self.recharge_checkbox, wraplength=400,
message="When enabled, recharge AP when low via cafe earnings, tasks, club and mailbox if they are enabled in their respective sections.")
self.event_checkbox = customtkinter.CTkCheckBox(self, text="Sweep Event Stages", command=lambda x=["Event"]: self.config.save_to_json(x), font=customtkinter.CTkFont(family="Inter", size=16, underline=True))
self.event_tooltip = CTkToolTip(self.event_checkbox, wraplength=400, message="When enabled, the script will sweep event stages. Otherwise, it will ignore them.")
self.event_checkbox.grid(row=15, column=0, sticky="nw", padx=80)
self.linker.widgets["Event"] = self.event_checkbox
# Helper method to create Preferred Template Selection
def create_preferred_template_selection(self):
self.templates = self.config.config_data["Templates"]
self.templates_list = list(self.templates.keys())
self.preferred_template_label = customtkinter.CTkLabel(self, text="Preferred Template:", font=customtkinter.CTkFont(family="Inter", size=16, underline=True))
self.preferred_template_label.grid(row=16, column=0, pady=20)
self.preferred_template_tooltip = CTkToolTip(self.preferred_template_label, wraplength=400,
message="The template from which to repopulate the queue when it is empty or reset daily is activated")
self.preferred_template_optionmenu = customtkinter.CTkOptionMenu(self, values=self.templates_list, command=lambda x, y=["PreferredTemplate"]: self.config.save_to_json(y))
self.preferred_template_optionmenu.grid(row=16, column=1, pady=20)
self.linker.widgets["PreferredTemplate"] = self.preferred_template_optionmenu
# Helper method to create Mission Tabview with Template and Queue Tabs
def create_mission_tabview(self):
self.mission_tabview = customtkinter.CTkTabview(self)
self.mission_tabview.grid(row=17, column=0, columnspan=3, padx=20)
self.tab_template = self.mission_tabview.add('Template')
self.tab_queue = self.mission_tabview.add('Queue')
# Helper method to create Template Queue Editor
def create_template_queue_editor(self):
self.queue_buttons = []
for i in [self.tab_queue, self.tab_template]:
queue = True if i == self.tab_queue else False
self.template_labels = customtkinter.CTkFrame(i)
self.template_labels.grid(row=0, column=0, sticky="ew")
self.mode_label = customtkinter.CTkLabel(self.template_labels, text="Mode:", font=customtkinter.CTkFont(underline=True))
self.mode_tooltip = CTkToolTip(self.mode_label, message="N : Mission Normal\nH : Mission Hard\nE : Event Quest\nXP : Commissions EXP\nCR : Commissions Credits\n", justify=tk.LEFT)
self.mode_label.grid(row=1, column=0, padx=(130, 0), pady=5)
self.stage_label = customtkinter.CTkLabel(self.template_labels, text="Stage:", font=customtkinter.CTkFont(underline=True))
self.stage_tooltip = CTkToolTip(self.stage_label, message="Valid format\nMission: 1-1, 3-A\nCommissions & Event: 01", justify=tk.LEFT)
self.stage_label.grid(row=1, column=1, padx=(40, 20), pady=5)
self.run_times_label = customtkinter.CTkLabel(self.template_labels, text="Number of Sweeps:", font=customtkinter.CTkFont(underline=True))
self.run_times_tooltip = CTkToolTip(self.run_times_label, message="How many times do you want to sweep the stage?")
self.run_times_label.grid(row=1, column=2, pady=5)
self.template_buttons_frame = customtkinter.CTkFrame(i)
self.template_buttons_frame.grid(row=3, column=0)
self.highlight_label = customtkinter.CTkLabel(self.template_buttons_frame, text="*You can double click an entry and press up or down arrow to change its position", font=customtkinter.CTkFont(family="Inter", size=12))
self.highlight_label.grid(row=0, column=0, columnspan=3)
self.add_button = CTkAddButton(master=self.template_buttons_frame)
self.add_button.button.configure(command=lambda queue=queue, button=self.add_button.button: self.add_frame(queue=queue, button=button))
self.add_button.grid(row=1, column=0, padx=5, pady=5)
# Clear button to clear all frames
self.clear_button = customtkinter.CTkButton(self.template_buttons_frame, text="Clear All", command=lambda queue=queue: self.clear_frames(queue=queue), fg_color="crimson")
self.clear_button.grid(row=1, column=1, padx=5, pady=5)
# Save button to save data
self.save_button = customtkinter.CTkButton(self.template_buttons_frame, text="Save", command=lambda queue=queue: self.save_data(queue=queue), fg_color="#DC621D")
self.save_button.grid(row=1, column=2, padx=5, pady=5)
if queue:
self.queue_buttons = [self.add_button, self.clear_button, self.save_button]
# Helper method to create Template Frame and Queue Frame
def create_template_and_queue_frames(self):
self.template_frame = customtkinter.CTkScrollableFrame(self.tab_template, width=435, height=350)
self.template_frame.grid(row=1, column=0, sticky="nsew")
self.queue_frame = customtkinter.CTkScrollableFrame(self.tab_queue, width=435, height=350)
self.queue_frame.grid(row=1, column=0, sticky="nsew")
# Helper method to create Lists to Store Frame Widgets
def create_frame_lists(self):
self.template_frames = []
self.queue_frames = []
self.highlighted_frame = None
# Helper method to initialize Preferred Template and Templates List
def initialize_preferred_template(self):
self.preferred_template = self.config.config_data["PreferredTemplate"]
self.templates_list.append("Add New Template")
# Helper method to create OptionMenu for Selecting a Template
def create_template_option_menu(self):
self.selected_template = tk.StringVar(self.template_frame)
self.selected_template.set(self.preferred_template) # Set the initial value to the preferred template
self.template_optionmenu = customtkinter.CTkOptionMenu(self.template_labels, values=self.templates_list, variable=self.selected_template, command=lambda *args: self.load_template_data())
self.template_optionmenu.grid(row=0, column=0, padx=5, pady=5)
# Helper method to create Delete Template Button
def create_delete_template_button(self):
self.delete_template_button = customtkinter.CTkButton(self.template_labels, width=40, text="Delete", command=self.delete_template)
self.delete_template_button.grid(row=0, column=1)
# Helper method to add frames from Configuration Data
def load_queue_data(self, state="normal"):
for entry in self.config.config_data['Queue']:
self.add_frame(entry, queue=True, state=state)
# Function to load template data into frames
def load_template_data(self):
selected = self.selected_template.get()
if selected == "Add New Template":
dialog = CTkTemplateDialog(text="Type in new template name. Template name MUST be different from other templates!", title="Template Name", values=self.templates_list[:-1])
template_name, template_import = dialog.get_input()
if template_name.replace(" ", "") == "":
self.template_optionmenu.set(self.previous_selected)
return
elif template_name in self.templates_list:
CTkMessagebox(title="Error", message="Name is invalid.", icon="MCE\icons\cancel.png")
self.template_optionmenu.set(self.previous_selected)
return
else:
if template_import != "":
self.templates[template_name] = self.templates[template_import]
else:
self.templates[template_name] = []
self.templates_list[-1] = template_name
self.preferred_template_optionmenu.configure(values=self.templates_list)
selected = template_name
self.template_optionmenu.set(selected)
self.templates_list.append("Add New Template")
self.template_optionmenu.configure(values=self.templates_list)
self.clear_frames()
for entry in self.templates[selected]:
self.add_frame(entry)
self.previous_selected = selected
def delete_template(self):
msg = CTkMessagebox(title="Template Deletetion", message=f"Are you sure you want to delete Template {self.previous_selected}?",
icon="MCE\icons\question.png", option_1="No", option_2="Yes")
response = msg.get()
if response=="Yes":
if len(self.templates) != 1:
del self.templates[self.previous_selected]
self.templates_list = list(self.templates.keys())
self.preferred_template_optionmenu.configure(values=self.templates_list)
if self.preferred_template == self.previous_selected:
self.preferred_template = random.choice(self.templates_list)
self.config.config_data["PreferredTemplate"] = self.preferred_template
self.selected_template.set(self.preferred_template) # Set the initial value to the preferred template
self.preferred_template_optionmenu.set(self.preferred_template)
self.load_template_data()
self.config.save_file()
self.templates_list.append("Add New Template")
self.template_optionmenu.configure(values=self.templates_list)
self.template_optionmenu.set(self.preferred_template)
else:
CTkMessagebox(title="Error", message="At least one template must exist!!!", icon="MCE\icons\cancel.png")
return
# Function to add a frame with widgets
def add_frame(self, inner_list=None, queue=False, state="normal", button=None):
position = button.cget("text") if button else "Add Down"
frames = self.queue_frames if queue else self.template_frames
parent_frame = self.queue_frame if queue else self.template_frame
row_index = len(frames) + 1 # Calculate the row for the new frame
# Create a frame
frame = tk.Frame(parent_frame, bg="gray17")
frame.grid(row=row_index, column=0, columnspan=4, padx=10, pady=10, sticky="w")
frames.append(frame) if position == "Add Down" else frames.insert(0, frame)
# "Up" button to move the frame up
up_button = customtkinter.CTkButton(frame, text="Up", width=5, command=lambda f=frame, queue=queue: self.move_frame_up(f, queue), state=state)
up_button.grid(row=0, column=0, padx=5, pady=5, sticky="w")
# "Down" button to move the frame down
down_button = customtkinter.CTkButton(frame, text="Down", width=5, command=lambda f=frame, queue=queue: self.move_frame_down(f, queue), state=state)
down_button.grid(row=0, column=1, padx=5, pady=5, sticky="w")
# Dropdown menu for mode
mode_optionmenu = customtkinter.CTkOptionMenu(frame, width=60, values=["N", "H", "E", "XP", "CR"], state=state)
mode_optionmenu.set(inner_list[0] if inner_list else "N")
mode_optionmenu.grid(row=0, column=2, padx=5, pady=5, sticky="w")
# Entry widget for stage
stage_var = tk.StringVar(value=inner_list[1] if inner_list else "")
stage_entry = customtkinter.CTkEntry(frame, width=60, textvariable=stage_var, state=state)
stage_entry.grid(row=0, column=3, padx=5, pady=5, sticky="w")
mode_optionmenu.configure(command=lambda choice, x=mode_optionmenu, y=stage_entry : self.check_entry(x,y))
stage_entry.bind('<KeyRelease>', command=lambda event, x=mode_optionmenu, y=stage_entry : self.check_entry(x,y))
self.check_entry(mode_optionmenu, stage_entry)
# Entry widget for run times (only accepts numbers)
run_times_spinbox = CTkIntegerSpinbox(frame, step_size=1, min_value=1)
run_times_spinbox.set(value=inner_list[2] if inner_list else 1)
run_times_spinbox.grid(row=0, column=4, padx=5, pady=5, sticky="w")
# Delete button to delete the frame
delete_button = customtkinter.CTkButton(frame, text="Delete", width=5, command=lambda f=frame, queue=queue: self.delete_frame(f, queue), state=state)
delete_button.grid(row=0, column=5, padx=5, pady=5, sticky="w")
frame.bind("<Double-Button-1>", lambda event, f=frame: self.highlight_frame(f))
if position == "Add Up":
self.update_frame_positions(queue=queue)
# Function to clear all frames
def clear_frames(self, queue=False):
frames = self.queue_frames if queue else self.template_frames
for frame in frames:
frame.destroy()
frames.clear()
# Function to save frames as data
def save_data(self, queue=False):
entries = []
frames = self.queue_frames if queue else self.template_frames
name = "Queue" if queue else "Template"
for frame in frames:
mode_optionmenu = frame.winfo_children()[2]
stage_entry = frame.winfo_children()[3]
if not self.check_entry(mode_optionmenu, stage_entry):
CTkMessagebox(title="Error", message="Configuration not saved. Some entries are incomplete or have incorect input.", icon="MCE\icons\cancel.png")
return
mode = frame.winfo_children()[2].get()
stage = frame.winfo_children()[3].get().strip()
run_times = frame.winfo_children()[4].get()
entries.append([mode, stage, int(run_times)])
if queue:
self.config.config_data['Queue'] = entries
else:
selected = self.selected_template.get()
self.templates[selected] = entries
self.config.save_file(name)
def check_entry(self, mode_dropdown, stage_entry):
mode = mode_dropdown.get()
stage = stage_entry.get()
if mode in ["N", "H"]:
pattern = r'\d{1,2}-[0-9A-Z]'
else:
pattern = r"^\d{2}$"
if re.match(pattern, stage):
stage_entry.configure(border_color=['#979DA2', '#565B5E'])
return True
else:
stage_entry.configure(border_color='crimson')
return False
# Function to move a frame up
def move_frame_up(self, frame, queue=False):
frames = self.queue_frames if queue else self.template_frames
index = frames.index(frame)
if index > 0:
frames[index], frames[index - 1] = frames[index - 1], frames[index]
self.update_frame_positions(queue=queue)
# Function to move a frame down
def move_frame_down(self, frame, queue=False):
frames = self.queue_frames if queue else self.template_frames
index = frames.index(frame)
if index < len(frames) - 1:
frames[index], frames[index + 1] = frames[index + 1], frames[index]
self.update_frame_positions(queue=queue)
# Function to update frame positions in the grid
def update_frame_positions(self, queue=False):
frames = self.queue_frames if queue else self.template_frames
for index, frame in enumerate(frames):
frame.grid(row=index, column=0, columnspan=4, padx=10, pady=10, sticky="w")
# Function to delete a frame
def delete_frame(self, frame, queue=False):
if queue:
self.queue_frames.remove(frame)
else:
self.template_frames.remove(frame)
frame.destroy()
# Update the positions of remaining frames
self.update_frame_positions(queue=queue)
def highlight_frame(self, frame):
try:
if self.highlighted_frame is not None:
self.highlighted_frame.unbind("<Up>")
self.highlighted_frame.unbind("<Down>")
self.highlighted_frame.config(bg="gray17")
except:
pass
if self.highlighted_frame == frame:
self.highlighted_frame = None
else:
up_button = frame.winfo_children()[0]
down_button = frame.winfo_children()[1]
frame.config(bg="yellow")
frame.bind("<Up>", lambda event: up_button.invoke())
frame.bind("<Down>", lambda event: down_button.invoke())
frame.focus_set()
self.highlighted_frame = frame
def check_lock(self):
while 1:
try:
lock = FileLock("MCE\config.json.lock")
lock.acquire(timeout=1)
except Timeout:
if not self.config.locked:
self.after(10, lambda : (self.queue_changed(), self.update_queue(), self.switch_queue_state("disabled")))
self.config.locked = True
elif self.config.locked and self.queue_changed():
self.after(10, lambda : (self.update_queue(), self.switch_queue_state("disabled")))
else:
lock.release()
if self.config.locked:
self.after(10, lambda : (self.queue_changed(), self.update_queue(), self.switch_queue_state("normal")))
self.config.locked = False
finally:
time.sleep(2)
def switch_queue_state(self, state):
for button in self.queue_buttons:
button.configure(state=state)
for frame in self.queue_frames:
for widget in frame.winfo_children():
widget.configure(state=state)
def update_queue(self):
self.clear_frames(queue=True)
for entry in self.config.config_data['Queue']:
self.add_frame(entry, queue=True)
def queue_changed(self):
new_config_data = self.config.read()
changed = self.config.config_data["Queue"] != new_config_data["Queue"] or self.config.config_data["LastRun"] != new_config_data["LastRun"]
if changed:
self.config.config_data["LastRun"] = new_config_data["LastRun"]
self.config.config_data['Queue'] = new_config_data['Queue']
return changed
if __name__ == "__main__":
linker = Linker()
config = Config(linker, "MCE\config.json")
app = MCE_Manager(linker, config)
app.title("MCE Manager")
linker.sidebar = app
config.load_config()
daemon_thread = threading.Thread(target=app.check_lock, daemon=True)
daemon_thread.start()
app.mainloop()
View File
+19
View File
@@ -0,0 +1,19 @@
import customtkinter
class CTkAddButton(customtkinter.CTkFrame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.button = customtkinter.CTkButton(self, text="Add Down", corner_radius=0, width=120)
self.button.grid(row=0, column=0)
self.option_menu = customtkinter.CTkOptionMenu(
self, values= ["Add Up", "Add Down"], width=10, command=self.set_button, corner_radius=0
)
self.option_menu.set("")
self.option_menu.grid(row=0, column=1)
def set_button(self, value):
self.option_menu.set("")
self.button.configure(text=value)
def configure(self, *args, **kwargs):
self.button.configure(*args, **kwargs)
+95
View File
@@ -0,0 +1,95 @@
import customtkinter
import re
from typing import Callable
class CTkIntegerSpinbox(customtkinter.CTkFrame):
def __init__(self, *args,
width: int = 100,
height: int = 32,
step_size: int = 1,
min_value: int = 0,
command: Callable = None,
**kwargs):
super().__init__(*args, width=width, height=height, **kwargs)
self.step_size = step_size
self.min_value = min_value
self.command = command
self.after_id = None
self.grid_columnconfigure((0, 2), weight=0)
self.grid_columnconfigure(1, weight=1)
self.subtract_button = customtkinter.CTkButton(self, text="-", width=height-6, height=height-6)
self.subtract_button.grid(row=0, column=0, padx=(3, 0), pady=3)
self.subtract_button.bind('<ButtonPress-1>', self.start_decrementing)
self.subtract_button.bind('<ButtonRelease-1>', self.stop_decrementing)
self.entry = customtkinter.CTkEntry(self, width=width-(2*height), height=height-6, border_width=0)
self.entry.grid(row=0, column=1, columnspan=1, padx=3, pady=3, sticky="ew")
self.add_button = customtkinter.CTkButton(self, text="+", width=height-6, height=height-6)
self.add_button.grid(row=0, column=2, padx=(0, 3), pady=3)
self.add_button.bind('<ButtonPress-1>', self.start_incrementing)
self.add_button.bind('<ButtonRelease-1>', self.stop_incrementing)
self.entry.insert(0, "0")
# Configure validatecommand to allow only integers
vcmd = (self.entry.register(self.validate_input), '%P')
self.entry.configure(validate='key', validatecommand=vcmd)
def start_incrementing(self, event):
self.increment()
self.after_id = self.after(150, self.start_incrementing, event)
def stop_incrementing(self, event):
if self.after_id:
self.after_cancel(self.after_id)
self.after_id = None
def start_decrementing(self, event):
self.decrement()
self.after_id = self.after(150, self.start_decrementing, event)
def stop_decrementing(self, event):
if self.after_id:
self.after_cancel(self.after_id)
self.after_id = None
def increment(self):
value = int(self.entry.get()) + self.step_size
self.entry.delete(0, "end")
self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 1
if self.command is not None:
self.command()
def decrement(self):
value = int(self.entry.get()) - self.step_size
self.entry.delete(0, "end")
self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 0
if self.command is not None:
self.command()
def validate_input(self, new_value):
# Validate that the input is a non-negative integer
return re.match(r'^\d*$', new_value) is not None
def get(self) -> int:
try:
return int(self.entry.get())
except ValueError:
return 0
def set(self, value: int):
self.entry.delete(0, "end")
self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 0
def configure(self, **kwargs):
state = kwargs.get("state", None)
if state is not None:
self.subtract_button.configure(state=state)
self.add_button.configure(state=state)
self.entry.configure(state=state)
kwargs.pop("state")
super().configure(**kwargs)
+27
View File
@@ -0,0 +1,27 @@
import customtkinter
class CTkNotification(customtkinter.CTkFrame):
def __init__(self, text, master, **kwargs):
self.master_color = master.cget("fg_color")
super().__init__(master=master, **kwargs, fg_color=self.master_color)
self.label = customtkinter.CTkLabel(self, text=text, text_color=self.master_color, width=200, wraplength=200, font=("Inter", 16))
self.label.grid(row=0, column=0, sticky="nsew")
self.close_button = customtkinter.CTkButton(
self, width=40, text="X", text_color_disabled=self.master_color, command=self.hide, fg_color="transparent", state="disabled")
self.close_button.grid(row=0, column=1)
self.progress_bar = customtkinter.CTkProgressBar(self, determinate_speed=0.4, fg_color=self.master_color, progress_color=self.master_color)
self.progress_bar.grid(row=1, column=0, columnspan=2, sticky="nsew")
def hide(self):
self.configure(fg_color="transparent")
self.progress_bar.stop()
self.progress_bar.configure(progress_color=self.master_color)
self.close_button.configure(state="disabled")
self.label.configure(text_color=self.master_color)
def show(self):
self.configure(fg_color="green")
self.progress_bar.configure(progress_color="white")
self.progress_bar.set(0)
self.progress_bar.start()
self.close_button.configure(state="normal")
self.label.configure(text_color="white")
@@ -0,0 +1,337 @@
'''
Advanced Scrollable Dropdown class for customtkinter widgets
Author: Akash Bora
'''
import customtkinter
import sys
import time
import difflib
class CTkScrollableDropdown(customtkinter.CTkToplevel):
def __init__(self, attach, x=None, y=None, button_color=None, height: int = 200, width: int = None,
fg_color=None, button_height: int = 20, justify="center", scrollbar_button_color=None,
scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=2, values=[],
command=None, image_values=[], alpha: float = 0.97, frame_corner_radius=20, double_click=False,
resize=True, frame_border_color=None, text_color=None, autocomplete=False, **button_kwargs):
super().__init__(takefocus=1)
self.focus()
self.lift()
self.alpha = alpha
self.attach = attach
self.corner = frame_corner_radius
self.padding = 0
self.focus_something = False
self.disable = True
self.update()
if sys.platform.startswith("win"):
self.after(100, lambda: self.overrideredirect(True))
self.transparent_color = self._apply_appearance_mode(self._fg_color)
self.attributes("-transparentcolor", self.transparent_color)
elif sys.platform.startswith("darwin"):
self.overrideredirect(True)
self.transparent_color = 'systemTransparent'
self.attributes("-transparent", True)
self.focus_something = True
else:
self.overrideredirect(True)
self.transparent_color = '#000001'
self.corner = 0
self.padding = 18
self.withdraw()
self.hide = True
self.attach.bind('<Configure>', lambda e: self._withdraw() if not self.disable else None, add="+")
self.attach.winfo_toplevel().bind('<Configure>', lambda e: self._withdraw() if not self.disable else None, add="+")
self.attach.winfo_toplevel().bind("<ButtonPress>", lambda e: self._withdraw() if not self.disable else None, add="+")
self.attributes('-alpha', 0)
self.disable = False
self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color
self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color
self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color
self.frame_border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if frame_border_color is None else frame_border_color
self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color
self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color
if scrollbar is False:
self.scroll_button_color = self.fg_color
self.scroll_hover_color = self.fg_color
self.frame = customtkinter.CTkScrollableFrame(self, bg_color=self.transparent_color, fg_color=self.fg_color,
scrollbar_button_hover_color=self.scroll_hover_color,
corner_radius=self.corner, border_width=frame_border_width,
scrollbar_button_color=self.scroll_button_color,
border_color=self.frame_border_color)
self.frame._scrollbar.grid_configure(padx=3)
self.frame.pack(expand=True, fill="both")
self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1)
self.no_match = customtkinter.CTkLabel(self.frame, text="No Match")
self.height = height
self.height_new = height
self.width = width
self.command = command
self.fade = False
self.resize = resize
self.autocomplete = autocomplete
self.var_update = customtkinter.StringVar()
self.appear = False
if justify.lower()=="left":
self.justify = "w"
elif justify.lower()=="right":
self.justify = "e"
else:
self.justify = "c"
self.button_height = button_height
self.values = values
self.button_num = len(self.values)
self.image_values = None if len(image_values)!=len(self.values) else image_values
self.resizable(width=False, height=False)
self.transient(self.master)
self._init_buttons(**button_kwargs)
# Add binding for different ctk widgets
if double_click or self.attach.winfo_name().startswith("!ctkentry") or self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach.bind('<Double-Button-1>', lambda e: self._iconify(), add="+")
else:
self.attach.bind('<Button-1>', lambda e: self._iconify(), add="+")
if self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach._canvas.tag_bind("right_parts", "<Button-1>", lambda e: self._iconify())
self.attach._canvas.tag_bind("dropdown_arrow", "<Button-1>", lambda e: self._iconify())
if self.command is None:
self.command = self.attach.set
if self.attach.winfo_name().startswith("!ctkoptionmenu"):
self.attach._canvas.bind("<Button-1>", lambda e: self._iconify())
self.attach._text_label.bind("<Button-1>", lambda e: self._iconify())
if self.command is None:
self.command = self.attach.set
self.attach.bind("<Destroy>", lambda _: self._destroy(), add="+")
self.update_idletasks()
self.x = x
self.y = y
if self.autocomplete:
self.bind_autocomplete()
self.deiconify()
self.withdraw()
self.attributes("-alpha", self.alpha)
def _destroy(self):
self.after(500, self.destroy_popup)
def _withdraw(self):
if self.winfo_viewable() and self.hide:
self.withdraw()
self.event_generate("<<Closed>>")
self.hide = True
def _update(self, a, b, c):
self.live_update(self.attach._entry.get())
def bind_autocomplete(self, ):
def appear(x):
self.appear = True
if self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach._entry.configure(textvariable=self.var_update)
self.attach._entry.bind("<Key>", appear)
self.attach.set(self.values[0])
self.var_update.trace_add('write', self._update)
if self.attach.winfo_name().startswith("!ctkentry"):
self.attach.configure(textvariable=self.var_update)
self.attach.bind("<Key>", appear)
self.var_update.trace_add('write', self._update)
def fade_out(self):
for i in range(100,0,-10):
if not self.winfo_exists():
break
self.attributes("-alpha", i/100)
self.update()
time.sleep(1/100)
def fade_in(self):
for i in range(0,100,10):
if not self.winfo_exists():
break
self.attributes("-alpha", i/100)
self.update()
time.sleep(1/100)
def _init_buttons(self, **button_kwargs):
self.i = 0
self.widgets = {}
for row in self.values:
self.widgets[self.i] = customtkinter.CTkButton(self.frame,
text=row,
height=self.button_height,
fg_color=self.button_color,
text_color=self.text_color,
image=self.image_values[i] if self.image_values is not None else None,
anchor=self.justify,
command=lambda k=row: self._attach_key_press(k), **button_kwargs)
self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0))
self.i+=1
self.hide = False
def destroy_popup(self):
self.destroy()
self.disable = True
def place_dropdown(self):
self.x_pos = self.attach.winfo_rootx() if self.x is None else self.x + self.attach.winfo_rootx()
self.y_pos = self.attach.winfo_rooty() + self.attach.winfo_reqheight() + 5 if self.y is None else self.y + self.attach.winfo_rooty()
self.width_new = self.attach.winfo_width() if self.width is None else self.width
if self.resize:
if self.button_num<=5:
self.height_new = self.button_height * self.button_num + 55
else:
self.height_new = self.button_height * self.button_num + 35
if self.height_new>self.height:
self.height_new = self.height
self.geometry('{}x{}+{}+{}'.format(self.width_new, self.height_new,
self.x_pos, self.y_pos))
self.fade_in()
self.attributes('-alpha', self.alpha)
self.attach.focus()
def _iconify(self):
if self.disable: return
if self.hide:
self.event_generate("<<Opened>>")
self._deiconify()
self.focus()
self.hide = False
self.place_dropdown()
if self.focus_something:
self.dummy_entry.pack()
self.dummy_entry.focus_set()
self.after(100, self.dummy_entry.pack_forget)
else:
self.withdraw()
self.hide = True
def _attach_key_press(self, k):
self.event_generate("<<Selected>>")
self.fade = True
if self.command:
self.command(k)
self.fade = False
self.fade_out()
self.withdraw()
self.hide = True
def live_update(self, string=None):
if not self.appear: return
if self.disable: return
if self.fade: return
if string:
string = string.lower()
self._deiconify()
i=1
for key in self.widgets.keys():
s = self.widgets[key].cget("text").lower()
text_similarity = difflib.SequenceMatcher(None, s[0:len(string)], string).ratio()
similar = s.startswith(string) or text_similarity > 0.75
if not similar:
self.widgets[key].pack_forget()
else:
self.widgets[key].pack(fill="x", pady=2, padx=(self.padding, 0))
i+=1
if i==1:
self.no_match.pack(fill="x", pady=2, padx=(self.padding, 0))
else:
self.no_match.pack_forget()
self.button_num = i
self.place_dropdown()
else:
self.no_match.pack_forget()
self.button_num = len(self.values)
for key in self.widgets.keys():
self.widgets[key].destroy()
self._init_buttons()
self.place_dropdown()
self.frame._parent_canvas.yview_moveto(0.0)
self.appear = False
def insert(self, value, **kwargs):
self.widgets[self.i] = customtkinter.CTkButton(self.frame,
text=value,
height=self.button_height,
fg_color=self.button_color,
text_color=self.text_color,
anchor=self.justify,
command=lambda k=value: self._attach_key_press(k), **kwargs)
self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0))
self.i+=1
self.values.append(value)
def _deiconify(self):
if len(self.values)>0:
self.deiconify()
def popup(self, x=None, y=None):
self.x = x
self.y = y
self.hide = True
self._iconify()
def configure(self, **kwargs):
if "height" in kwargs:
self.height = kwargs.pop("height")
self.height_new = self.height
if "alpha" in kwargs:
self.alpha = kwargs.pop("alpha")
if "width" in kwargs:
self.width = kwargs.pop("width")
if "fg_color" in kwargs:
self.frame.configure(fg_color=kwargs.pop("fg_color"))
if "values" in kwargs:
self.values = kwargs.pop("values")
self.image_values = None
self.button_num = len(self.values)
for key in self.widgets.keys():
self.widgets[key].destroy()
self._init_buttons()
if "image_values" in kwargs:
self.image_values = kwargs.pop("image_values")
self.image_values = None if len(self.image_values)!=len(self.values) else self.image_values
if self.image_values is not None:
i=0
for key in self.widgets.keys():
self.widgets[key].configure(image=self.image_values[i])
i+=1
if "button_color" in kwargs:
for key in self.widgets.keys():
self.widgets[key].configure(fg_color=kwargs.pop("button_color"))
for key in self.widgets.keys():
self.widgets[key].configure(**kwargs)
@@ -0,0 +1,291 @@
'''
Advanced Scrollable Dropdown Frame class for customtkinter widgets
Author: Akash Bora
'''
import customtkinter
import sys
import difflib
class CTkScrollableDropdownFrame(customtkinter.CTkFrame):
def __init__(self, attach, x=None, y=None, button_color=None, height: int = 200, width: int = None,
fg_color=None, button_height: int = 20, justify="center", scrollbar_button_color=None,
scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=2, values=[],
command=None, image_values=[], double_click=False, frame_corner_radius=True, resize=True, frame_border_color=None,
text_color=None, autocomplete=False, **button_kwargs):
super().__init__(master=attach.winfo_toplevel(), bg_color=attach.cget("bg_color"))
self.attach = attach
self.corner = 11 if frame_corner_radius else 0
self.padding = 0
self.disable = True
self.hide = True
self.attach.bind('<Configure>', lambda e: self._withdraw() if not self.disable else None, add="+")
self.attach.winfo_toplevel().bind("<ButtonPress>", lambda e: self._withdraw() if not self.disable else None, add="+")
self.disable = False
self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color
self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color
self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color
self.frame_border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if frame_border_color is None else frame_border_color
self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color
self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color
if scrollbar is False:
self.scroll_button_color = self.fg_color
self.scroll_hover_color = self.fg_color
self.frame = customtkinter.CTkScrollableFrame(self, fg_color=self.fg_color, bg_color=attach.cget("bg_color"),
scrollbar_button_hover_color=self.scroll_hover_color,
corner_radius=self.corner, border_width=frame_border_width,
scrollbar_button_color=self.scroll_button_color,
border_color=self.frame_border_color)
self.frame._scrollbar.grid_configure(padx=3)
self.frame.pack(expand=True, fill="both")
if self.corner==0:
self.corner = 21
self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1)
self.no_match = customtkinter.CTkLabel(self.frame, text="No Match")
self.height = height
self.height_new = height
self.width = width
self.command = command
self.fade = False
self.resize = resize
self.autocomplete = autocomplete
self.var_update = customtkinter.StringVar()
self.appear = False
if justify.lower()=="left":
self.justify = "w"
elif justify.lower()=="right":
self.justify = "e"
else:
self.justify = "c"
self.button_height = button_height
self.values = values
self.button_num = len(self.values)
self.image_values = None if len(image_values)!=len(self.values) else image_values
self._init_buttons(**button_kwargs)
# Add binding for different ctk widgets
if double_click or self.attach.winfo_name().startswith("!ctkentry") or self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach.bind('<Double-Button-1>', lambda e: self._iconify(), add="+")
self.attach._entry.bind('<FocusOut>', lambda e: self._withdraw() if not self.disable else None, add="+")
else:
self.attach.bind('<Button-1>', lambda e: self._iconify(), add="+")
if self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach._canvas.tag_bind("right_parts", "<Button-1>", lambda e: self._iconify())
self.attach._canvas.tag_bind("dropdown_arrow", "<Button-1>", lambda e: self._iconify())
if self.command is None:
self.command = self.attach.set
if self.attach.winfo_name().startswith("!ctkoptionmenu"):
self.attach._canvas.bind("<Button-1>", lambda e: self._iconify())
self.attach._text_label.bind("<Button-1>", lambda e: self._iconify())
if self.command is None:
self.command = self.attach.set
self.x = x
self.y = y
self.attach.bind("<Destroy>", lambda _: self._destroy(), add="+")
if self.autocomplete:
self.bind_autocomplete()
def _destroy(self):
self.after(500, self.destroy_popup)
def _withdraw(self):
if self.winfo_viewable() and self.hide:
self.place_forget()
self.event_generate("<<Closed>>")
self.hide = True
def _update(self, a, b, c):
self.live_update(self.attach._entry.get())
def bind_autocomplete(self, ):
def appear(x):
self.appear = True
if self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach._entry.configure(textvariable=self.var_update)
self.attach.set(self.values[0])
self.attach._entry.bind("<Key>", appear)
self.var_update.trace_add('write', self._update)
if self.attach.winfo_name().startswith("!ctkentry"):
self.attach.configure(textvariable=self.var_update)
self.attach.bind("<Key>", appear)
self.var_update.trace_add('write', self._update)
def _init_buttons(self, **button_kwargs):
self.i = 0
self.widgets = {}
for row in self.values:
self.widgets[self.i] = customtkinter.CTkButton(self.frame,
text=row,
height=self.button_height,
fg_color=self.button_color,
text_color=self.text_color,
image=self.image_values[i] if self.image_values is not None else None,
anchor=self.justify,
command=lambda k=row: self._attach_key_press(k), **button_kwargs)
self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0))
self.i+=1
self.hide = False
def destroy_popup(self):
self.destroy()
self.disable = True
def place_dropdown(self):
self.x_pos = self.attach.winfo_x() if self.x is None else self.x + self.attach.winfo_rootx()
self.y_pos = self.attach.winfo_y() + self.attach.winfo_reqheight() + 5 if self.y is None else self.y + self.attach.winfo_rooty()
self.width_new = self.attach.winfo_width()-45+self.corner if self.width is None else self.width
if self.resize:
if self.button_num<=5:
self.height_new = self.button_height * self.button_num + 55
else:
self.height_new = self.button_height * self.button_num + 35
if self.height_new>self.height:
self.height_new = self.height
self.frame.configure(width=self.width_new, height=self.height_new)
self.place(x=self.x_pos, y=self.y_pos)
if sys.platform.startswith("darwin"):
self.dummy_entry.pack()
self.after(100, self.dummy_entry.pack_forget())
self.lift()
self.attach.focus()
def _iconify(self):
if self.disable: return
if self.hide:
self.event_generate("<<Opened>>")
self.hide = False
self.place_dropdown()
else:
self.place_forget()
self.hide = True
def _attach_key_press(self, k):
self.event_generate("<<Selected>>")
self.fade = True
if self.command:
self.command(k)
self.fade = False
self.place_forget()
self.hide = True
def live_update(self, string=None):
if not self.appear: return
if self.disable: return
if self.fade: return
if string:
string = string.lower()
self._deiconify()
i=1
for key in self.widgets.keys():
s = self.widgets[key].cget("text").lower()
text_similarity = difflib.SequenceMatcher(None, s[0:len(string)], string).ratio()
similar = s.startswith(string) or text_similarity > 0.75
if not similar:
self.widgets[key].pack_forget()
else:
self.widgets[key].pack(fill="x", pady=2, padx=(self.padding, 0))
i+=1
if i==1:
self.no_match.pack(fill="x", pady=2, padx=(self.padding, 0))
else:
self.no_match.pack_forget()
self.button_num = i
self.place_dropdown()
else:
self.no_match.pack_forget()
self.button_num = len(self.values)
for key in self.widgets.keys():
self.widgets[key].destroy()
self._init_buttons()
self.place_dropdown()
self.frame._parent_canvas.yview_moveto(0.0)
self.appear = False
def insert(self, value, **kwargs):
self.widgets[self.i] = customtkinter.CTkButton(self.frame,
text=value,
height=self.button_height,
fg_color=self.button_color,
text_color=self.text_color,
anchor=self.justify,
command=lambda k=value: self._attach_key_press(k), **kwargs)
self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0))
self.i+=1
self.values.append(value)
def _deiconify(self):
if len(self.values)>0:
self.pack_forget()
def popup(self, x=None, y=None):
self.x = x
self.y = y
self.hide = True
self._iconify()
def configure(self, **kwargs):
if "height" in kwargs:
self.height = kwargs.pop("height")
self.height_new = self.height
if "alpha" in kwargs:
self.alpha = kwargs.pop("alpha")
if "width" in kwargs:
self.width = kwargs.pop("width")
if "fg_color" in kwargs:
self.frame.configure(fg_color=kwargs.pop("fg_color"))
if "values" in kwargs:
self.values = kwargs.pop("values")
self.image_values = None
self.button_num = len(self.values)
for key in self.widgets.keys():
self.widgets[key].destroy()
self._init_buttons()
if "image_values" in kwargs:
self.image_values = kwargs.pop("image_values")
self.image_values = None if len(self.image_values)!=len(self.values) else self.image_values
if self.image_values is not None:
i=0
for key in self.widgets.keys():
self.widgets[key].configure(image=self.image_values[i])
i+=1
if "button_color" in kwargs:
for key in self.widgets.keys():
self.widgets[key].configure(fg_color=kwargs.pop("button_color"))
for key in self.widgets.keys():
self.widgets[key].configure(**kwargs)
+133
View File
@@ -0,0 +1,133 @@
from typing import Union, Tuple, Optional
from customtkinter import CTkLabel, CTkEntry, CTkButton, ThemeManager, CTkToplevel, CTkFont, CTkOptionMenu
class CTkTemplateDialog(CTkToplevel):
"""
Dialog with extra window, message, entry widget, cancel and ok button.
For detailed information check out the documentation.
"""
def __init__(self,
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
button_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
button_text_color: Optional[Union[str, Tuple[str, str]]] = None,
entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
entry_border_color: Optional[Union[str, Tuple[str, str]]] = None,
entry_text_color: Optional[Union[str, Tuple[str, str]]] = None,
title: str = "CTkDialog",
font: Optional[Union[tuple, CTkFont]] = None,
text: str = "CTkDialog",
values = []):
super().__init__(fg_color=fg_color)
self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color)
self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color)
self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color)
self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color)
self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color)
self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color)
self._user_input = ("", "")
self._running: bool = False
self._title = title
self._text = text
self._font = font
self._values = [""] + values
self.title(self._title)
self.lift() # lift window on top
self.attributes("-topmost", True) # stay on top
self.protocol("WM_DELETE_WINDOW", self._on_closing)
self.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background
self.resizable(False, False)
self.grab_set() # make other windows not clickable
def _create_widgets(self):
self.grid_columnconfigure((0, 1), weight=1)
self.rowconfigure(0, weight=1)
self._label = CTkLabel(master=self,
width=300,
wraplength=300,
fg_color="transparent",
text_color=self._text_color,
text=self._text,
font=self._font)
self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew")
self._entry = CTkEntry(master=self,
width=230,
fg_color=self._entry_fg_color,
border_color=self._entry_border_color,
text_color=self._entry_text_color,
font=self._font)
self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew")
self._label2 = CTkLabel(master=self,
width=100,
wraplength=100,
fg_color="transparent",
text_color=self._text_color,
text="Import stages from: ",
font=self._font)
self._label2.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew")
self._template_optionmenu = CTkOptionMenu(master=self,
width=100,
fg_color=self._button_fg_color,
button_hover_color=self._button_hover_color,
text_color=self._button_text_color,
font=self._font,
values=self._values
)
self._template_optionmenu.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew")
self._ok_button = CTkButton(master=self,
width=100,
border_width=0,
fg_color=self._button_fg_color,
hover_color=self._button_hover_color,
text_color=self._button_text_color,
text='Ok',
font=self._font,
command=self._ok_event)
self._ok_button.grid(row=3, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew")
self._cancel_button = CTkButton(master=self,
width=100,
border_width=0,
fg_color=self._button_fg_color,
hover_color=self._button_hover_color,
text_color=self._button_text_color,
text='Cancel',
font=self._font,
command=self._cancel_event)
self._cancel_button.grid(row=3, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew")
self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work
self._entry.bind("<Return>", self._ok_event)
def _ok_event(self, event=None):
self._user_input = self._entry.get(), self._template_optionmenu.get()
self.grab_release()
self.destroy()
def _on_closing(self):
self.grab_release()
self.destroy()
def _cancel_event(self):
self.grab_release()
self.destroy()
def get_input(self):
self.master.wait_window(self)
return self._user_input
+36
View File
@@ -0,0 +1,36 @@
import customtkinter
import tkinter as tk
class CTkTimeEntry(customtkinter.CTkFrame):
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.hour = tk.StringVar()
self.minute = tk.StringVar()
self.second = tk.StringVar()
self.hour_entry = customtkinter.CTkEntry(self, width=50, textvariable=self.hour, validate="key", validatecommand=(self.register(self.validate_hour), '%P'))
self.hour_entry.pack(side=tk.LEFT)
self.minute_entry = customtkinter.CTkEntry(self,width=50, textvariable=self.minute, validate="key", validatecommand=(self.register(self.validate_min_sec), '%P'))
self.minute_entry.pack(side=tk.LEFT)
self.second_entry = customtkinter.CTkEntry(self, width=50, textvariable=self.second, validate="key", validatecommand=(self.register(self.validate_min_sec), '%P'))
self.second_entry.pack(side=tk.LEFT)
def validate_hour(self, P):
return len(P) <= 2 and (P.isdigit() and int(P) <= 23 or P == "")
def validate_min_sec(self, P):
return len(P) <= 2 and (P.isdigit() and int(P) <= 59 or P == "")
def set(self, time_str):
h, m, s = map(str, time_str.split(':'))
self.hour.set(h)
self.minute.set(m)
self.second.set(s)
def get(self):
h = self.hour.get() if self.hour.get() else "00"
m = self.minute.get() if self.minute.get() else "00"
s = self.second.get() if self.second.get() else "00"
return f"{h.zfill(2)}:{m.zfill(2)}:{s.zfill(2)}"
+212
View File
@@ -0,0 +1,212 @@
"""
CTkToolTip Widget
version: 0.8
"""
import time
import sys
import customtkinter
from tkinter import Toplevel, Frame
class CTkToolTip(Toplevel):
"""
Creates a ToolTip (pop-up) widget for customtkinter.
"""
def __init__(
self,
widget: any = None,
message: str = None,
delay: float = 0.2,
follow: bool = True,
x_offset: int = +20,
y_offset: int = +10,
bg_color: str = None,
corner_radius: int = 10,
border_width: int = 0,
border_color: str = None,
alpha: float = 0.95,
padding: tuple = (10, 2),
**message_kwargs):
super().__init__()
self.widget = widget
self.withdraw()
# Disable ToolTip's title bar
self.overrideredirect(True)
if sys.platform.startswith("win"):
self.transparent_color = self.widget._apply_appearance_mode(
customtkinter.ThemeManager.theme["CTkToplevel"]["fg_color"])
self.attributes("-transparentcolor", self.transparent_color)
self.transient()
elif sys.platform.startswith("darwin"):
self.transparent_color = 'systemTransparent'
self.attributes("-transparent", True)
self.transient(self.master)
else:
self.transparent_color = '#000001'
corner_radius = 0
self.transient()
self.resizable(width=True, height=True)
# Make the background transparent
self.config(background=self.transparent_color)
# StringVar instance for msg string
self.messageVar = customtkinter.StringVar()
self.message = message
self.messageVar.set(self.message)
self.delay = delay
self.follow = follow
self.x_offset = x_offset
self.y_offset = y_offset
self.corner_radius = corner_radius
self.alpha = alpha
self.border_width = border_width
self.padding = padding
self.bg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if bg_color is None else bg_color
self.border_color = border_color
self.disable = False
# visibility status of the ToolTip inside|outside|visible
self.status = "outside"
self.last_moved = 0
self.attributes('-alpha', self.alpha)
if sys.platform.startswith("win"):
if self.widget._apply_appearance_mode(self.bg_color) == self.transparent_color:
self.transparent_color = "#000001"
self.config(background=self.transparent_color)
self.attributes("-transparentcolor", self.transparent_color)
# Add the message widget inside the tooltip
self.transparent_frame = Frame(self, bg=self.transparent_color)
self.transparent_frame.pack(padx=0, pady=0, fill="both", expand=True)
self.frame = customtkinter.CTkFrame(self.transparent_frame, bg_color=self.transparent_color,
corner_radius=self.corner_radius,
border_width=self.border_width, fg_color=self.bg_color,
border_color=self.border_color)
self.frame.pack(padx=0, pady=0, fill="both", expand=True)
self.message_label = customtkinter.CTkLabel(self.frame, textvariable=self.messageVar, **message_kwargs)
self.message_label.pack(fill="both", padx=self.padding[0] + self.border_width,
pady=self.padding[1] + self.border_width, expand=True)
if self.widget.winfo_name() != "tk":
if self.frame.cget("fg_color") == self.widget.cget("bg_color"):
if not bg_color:
self._top_fg_color = self.frame._apply_appearance_mode(
customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"])
if self._top_fg_color != self.transparent_color:
self.frame.configure(fg_color=self._top_fg_color)
# Add bindings to the widget without overriding the existing ones
self.widget.bind("<Enter>", self.on_enter, add="+")
self.widget.bind("<Leave>", self.on_leave, add="+")
self.widget.bind("<Motion>", self.on_enter, add="+")
self.widget.bind("<B1-Motion>", self.on_enter, add="+")
self.widget.bind("<Destroy>", lambda _: self.hide(), add="+")
def show(self) -> None:
"""
Enable the widget.
"""
self.disable = False
def on_enter(self, event) -> None:
"""
Processes motion within the widget including entering and moving.
"""
if self.disable:
return
self.last_moved = time.time()
# Set the status as inside for the very first time
if self.status == "outside":
self.status = "inside"
# If the follow flag is not set, motion within the widget will make the ToolTip dissapear
if not self.follow:
self.status = "inside"
self.withdraw()
# Calculate available space on the right side of the widget relative to the screen
root_width = self.winfo_screenwidth()
widget_x = event.x_root
space_on_right = root_width - widget_x
# Calculate the width of the tooltip's text based on the length of the message string
text_width = self.message_label.winfo_reqwidth()
# Calculate the offset based on available space and text width to avoid going off-screen on the right side
offset_x = self.x_offset
if space_on_right < text_width + 20: # Adjust the threshold as needed
offset_x = -text_width - 20 # Negative offset when space is limited on the right side
# Offsets the ToolTip using the coordinates od an event as an origin
self.geometry(f"+{event.x_root + offset_x}+{event.y_root + self.y_offset}")
# Time is in integer: milliseconds
self.after(int(self.delay * 1000), self._show)
def on_leave(self, event=None) -> None:
"""
Hides the ToolTip temporarily.
"""
if self.disable: return
self.status = "outside"
self.withdraw()
def _show(self) -> None:
"""
Displays the ToolTip.
"""
if not self.widget.winfo_exists():
self.hide()
self.destroy()
if self.status == "inside" and time.time() - self.last_moved >= self.delay:
self.status = "visible"
self.deiconify()
def hide(self) -> None:
"""
Disable the widget from appearing.
"""
if not self.winfo_exists():
return
self.withdraw()
self.disable = True
def is_disabled(self) -> None:
"""
Return the window state
"""
return self.disable
def get(self) -> None:
"""
Returns the text on the tooltip.
"""
return self.messageVar.get()
def configure(self, message: str = None, delay: float = None, bg_color: str = None, **kwargs):
"""
Set new message or configure the label parameters.
"""
if delay: self.delay = delay
if bg_color: self.frame.configure(fg_color=bg_color)
self.messageVar.set(message)
self.message_label.configure(**kwargs)
+448
View File
@@ -0,0 +1,448 @@
"""
CustomTkinter Messagebox
Author: Akash Bora
Version: 2.5
"""
import customtkinter
from PIL import Image, ImageTk
import os
import sys
import time
from typing import Literal
class CTkMessagebox(customtkinter.CTkToplevel):
ICONS = {
"check": None,
"cancel": None,
"info": None,
"question": None,
"warning": None
}
ICON_BITMAP = {}
def __init__(self,
master: any = None,
width: int = 400,
height: int = 200,
title: str = "CTkMessagebox",
message: str = "This is a CTkMessagebox!",
option_1: str = "OK",
option_2: str = None,
option_3: str = None,
options: list = [],
border_width: int = 1,
border_color: str = "default",
button_color: str = "default",
bg_color: str = "default",
fg_color: str = "default",
text_color: str = "default",
title_color: str = "default",
button_text_color: str = "default",
button_width: int = None,
button_height: int = None,
cancel_button_color: str = None,
cancel_button: str = None, # types: circle, cross or none
button_hover_color: str = "default",
icon: str = "info",
icon_size: tuple = None,
corner_radius: int = 15,
justify: str = "right",
font: tuple = None,
header: bool = False,
topmost: bool = True,
fade_in_duration: int = 0,
sound: bool = False,
option_focus: Literal[1, 2, 3] = None):
super().__init__()
self.master_window = master
self.width = 250 if width<250 else width
self.height = 150 if height<150 else height
if self.master_window is None:
self.spawn_x = int((self.winfo_screenwidth()-self.width)/2)
self.spawn_y = int((self.winfo_screenheight()-self.height)/2)
else:
self.spawn_x = int(self.master_window.winfo_width() * .5 + self.master_window.winfo_x() - .5 * self.width + 7)
self.spawn_y = int(self.master_window.winfo_height() * .5 + self.master_window.winfo_y() - .5 * self.height + 20)
self.after(10)
self.geometry(f"{self.width}x{self.height}+{self.spawn_x}+{self.spawn_y}")
self.title(title)
self.resizable(width=False, height=False)
self.fade = fade_in_duration
if self.fade:
self.fade = 20 if self.fade<20 else self.fade
self.attributes("-alpha", 0)
if not header:
self.overrideredirect(1)
if topmost:
self.attributes("-topmost", True)
else:
self.transient(self.master_window)
if sys.platform.startswith("win"):
self.transparent_color = self._apply_appearance_mode(self.cget("fg_color"))
self.attributes("-transparentcolor", self.transparent_color)
default_cancel_button = "cross"
elif sys.platform.startswith("darwin"):
self.transparent_color = 'systemTransparent'
self.attributes("-transparent", True)
default_cancel_button = "circle"
else:
self.transparent_color = '#000001'
corner_radius = 0
default_cancel_button = "cross"
self.lift()
self.config(background=self.transparent_color)
self.protocol("WM_DELETE_WINDOW", self.button_event)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
self.x = self.winfo_x()
self.y = self.winfo_y()
self._title = title
self.message = message
self.font = font
self.justify = justify
self.sound = sound
self.cancel_button = cancel_button if cancel_button else default_cancel_button
self.round_corners = corner_radius if corner_radius<=30 else 30
self.button_width = button_width if button_width else self.width/4
self.button_height = button_height if button_height else 28
if self.fade: self.attributes("-alpha", 0)
if self.button_height>self.height/4: self.button_height = self.height/4 -20
self.dot_color = cancel_button_color
self.border_width = border_width if border_width<6 else 5
if type(options) is list and len(options)>0:
try:
option_1 = options[-1]
option_2 = options[-2]
option_3 = options[-3]
except IndexError: None
if bg_color=="default":
self.bg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"])
else:
self.bg_color = bg_color
if fg_color=="default":
self.fg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"])
else:
self.fg_color = fg_color
default_button_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["fg_color"])
if sys.platform.startswith("win"):
if self.bg_color==self.transparent_color or self.fg_color==self.transparent_color:
self.configure(fg_color="#000001")
self.transparent_color = "#000001"
self.attributes("-transparentcolor", self.transparent_color)
if button_color=="default":
self.button_color = (default_button_color, default_button_color, default_button_color)
else:
if type(button_color) is tuple:
if len(button_color)==2:
self.button_color = (button_color[0], button_color[1], default_button_color)
elif len(button_color)==1:
self.button_color = (button_color[0], default_button_color, default_button_color)
else:
self.button_color = button_color
else:
self.button_color = (button_color, button_color, button_color)
if text_color=="default":
self.text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"])
else:
self.text_color = text_color
if title_color=="default":
self.title_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"])
else:
self.title_color = title_color
if button_text_color=="default":
self.bt_text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["text_color"])
else:
self.bt_text_color = button_text_color
if button_hover_color=="default":
self.bt_hv_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["hover_color"])
else:
self.bt_hv_color = button_hover_color
if border_color=="default":
self.border_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["border_color"])
else:
self.border_color = border_color
if icon_size:
self.size_height = icon_size[1] if icon_size[1]<=self.height-100 else self.height-100
self.size = (icon_size[0], self.size_height)
else:
self.size = (self.height/4, self.height/4)
self.icon = self.load_icon(icon, icon_size) if icon else None
self.frame_top = customtkinter.CTkFrame(self, corner_radius=self.round_corners, width=self.width, border_width=self.border_width,
bg_color=self.transparent_color, fg_color=self.bg_color, border_color=self.border_color)
self.frame_top.grid(sticky="nswe")
if button_width:
self.frame_top.grid_columnconfigure(0, weight=1)
else:
self.frame_top.grid_columnconfigure((1,2,3), weight=1)
if button_height:
self.frame_top.grid_rowconfigure((0,1,3), weight=1)
else:
self.frame_top.grid_rowconfigure((0,1,2), weight=1)
self.frame_top.bind("<B1-Motion>", self.move_window)
self.frame_top.bind("<ButtonPress-1>", self.oldxyset)
if self.cancel_button=="cross":
self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=0, height=0, hover=False, border_width=0,
text_color=self.dot_color if self.dot_color else self.title_color,
text="", fg_color="transparent", command=self.button_event)
self.button_close.grid(row=0, column=5, sticky="ne", padx=5+self.border_width, pady=5+self.border_width)
elif self.cancel_button=="circle":
self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=10, height=10, hover=False, border_width=0,
text="", fg_color=self.dot_color if self.dot_color else "#c42b1c", command=self.button_event)
self.button_close.grid(row=0, column=5, sticky="ne", padx=10, pady=10)
self.title_label = customtkinter.CTkLabel(self.frame_top, width=1, text=self._title, text_color=self.title_color, font=self.font)
self.title_label.grid(row=0, column=0, columnspan=6, sticky="nw", padx=(15,30), pady=5)
self.title_label.bind("<B1-Motion>", self.move_window)
self.title_label.bind("<ButtonPress-1>", self.oldxyset)
self.info = customtkinter.CTkButton(self.frame_top, width=1, height=self.height/2, corner_radius=0, text=self.message, font=self.font,
fg_color=self.fg_color, hover=False, text_color=self.text_color, image=self.icon)
self.info._text_label.configure(wraplength=self.width/2, justify="left")
self.info.grid(row=1, column=0, columnspan=6, sticky="nwes", padx=self.border_width)
if self.info._text_label.winfo_reqheight()>self.height/2:
height_offset = int((self.info._text_label.winfo_reqheight())-(self.height/2) + self.height)
self.geometry(f"{self.width}x{height_offset}")
self.option_text_1 = option_1
self.button_1 = customtkinter.CTkButton(self.frame_top, text=self.option_text_1, fg_color=self.button_color[0],
width=self.button_width, font=self.font, text_color=self.bt_text_color,
hover_color=self.bt_hv_color, height=self.button_height,
command=lambda: self.button_event(self.option_text_1))
self.option_text_2 = option_2
if option_2:
self.button_2 = customtkinter.CTkButton(self.frame_top, text=self.option_text_2, fg_color=self.button_color[1],
width=self.button_width, font=self.font, text_color=self.bt_text_color,
hover_color=self.bt_hv_color, height=self.button_height,
command=lambda: self.button_event(self.option_text_2))
self.option_text_3 = option_3
if option_3:
self.button_3 = customtkinter.CTkButton(self.frame_top, text=self.option_text_3, fg_color=self.button_color[2],
width=self.button_width, font=self.font, text_color=self.bt_text_color,
hover_color=self.bt_hv_color, height=self.button_height,
command=lambda: self.button_event(self.option_text_3))
if self.justify=="center":
if button_width:
columns = [4,3,2]
span = 1
else:
columns = [4,2,0]
span = 2
if option_3:
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10)
self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10)
elif option_2:
self.frame_top.columnconfigure((0,5), weight=1)
columns = [2,3]
self.button_1.grid(row=2, column=columns[0], sticky="news", padx=(0,5), pady=10)
self.button_2.grid(row=2, column=columns[1], sticky="news", padx=(5,0), pady=10)
else:
if button_width:
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
else:
self.frame_top.columnconfigure((0,2,4), weight=2)
self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=(0,10), pady=10)
elif self.justify=="left":
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
if button_width:
columns = [0,1,2]
span = 1
else:
columns = [0,2,4]
span = 2
if option_3:
self.button_1.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(0,10), pady=10)
self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
self.button_3.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10)
elif option_2:
self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
self.button_2.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10)
else:
self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10)
else:
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
if button_width:
columns = [5,4,3]
span = 1
else:
columns = [4,2,0]
span = 2
self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10)
if option_2:
self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
if option_3:
self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10)
if header:
self.title_label.configure(text="")
self.title_label.grid_configure(pady=0)
self.button_close.configure(text_color=self.bg_color)
self.frame_top.configure(corner_radius=0)
if self.winfo_exists():
self.grab_set()
if self.sound:
self.bell()
if self.fade:
self.fade_in()
if option_focus:
self.option_focus = option_focus
self.focus_button(self.option_focus)
else:
if not self.option_text_2 and not self.option_text_3:
self.button_1.focus()
self.button_1.bind("<Return>", lambda event: self.button_event(self.option_text_1))
self.bind("<Escape>", lambda e: self.button_event())
def focus_button(self, option_focus):
try:
self.selected_button = getattr(self, "button_"+str(option_focus))
self.selected_button.focus()
self.selected_button.configure(border_color=self.bt_hv_color, border_width=3)
self.selected_option = getattr(self, "option_text_"+str(option_focus))
self.selected_button.bind("<Return>", lambda event: self.button_event(self.selected_option))
except AttributeError:
return
self.bind("<Left>", lambda e: self.change_left())
self.bind("<Right>", lambda e: self.change_right())
def change_left(self):
if self.option_focus==3:
return
self.selected_button.unbind("<Return>")
self.selected_button.configure(border_width=0)
if self.option_focus==1:
if self.option_text_2:
self.option_focus = 2
elif self.option_focus==2:
if self.option_text_3:
self.option_focus = 3
self.focus_button(self.option_focus)
def change_right(self):
if self.option_focus==1:
return
self.selected_button.unbind("<Return>")
self.selected_button.configure(border_width=0)
if self.option_focus==2:
self.option_focus = 1
elif self.option_focus==3:
self.option_focus = 2
self.focus_button(self.option_focus)
def load_icon(self, icon, icon_size):
if icon not in self.ICONS or self.ICONS[icon] is None:
if icon in ["check", "cancel", "info", "question", "warning"]:
image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons', icon + '.png')
else:
image_path = icon
if icon_size:
size_height = icon_size[1] if icon_size[1] <= self.height - 100 else self.height - 100
size = (icon_size[0], size_height)
else:
size = (self.height / 4, self.height / 4)
self.ICONS[icon] = customtkinter.CTkImage(Image.open(image_path), size=size)
self.ICON_BITMAP[icon] = ImageTk.PhotoImage(file=image_path)
self.after(200, lambda: self.iconphoto(False, self.ICON_BITMAP[icon]))
return self.ICONS[icon]
def fade_in(self):
for i in range(0,110,10):
if not self.winfo_exists():
break
self.attributes("-alpha", i/100)
self.update()
time.sleep(1/self.fade)
def fade_out(self):
for i in range(100,0,-10):
if not self.winfo_exists():
break
self.attributes("-alpha", i/100)
self.update()
time.sleep(1/self.fade)
def get(self):
if self.winfo_exists():
self.master.wait_window(self)
return self.event
def oldxyset(self, event):
self.oldx = event.x
self.oldy = event.y
def move_window(self, event):
self.y = event.y_root - self.oldy
self.x = event.x_root - self.oldx
self.geometry(f'+{self.x}+{self.y}')
def button_event(self, event=None):
try:
self.button_1.configure(state="disabled")
self.button_2.configure(state="disabled")
self.button_3.configure(state="disabled")
except AttributeError:
pass
if self.fade:
self.fade_out()
self.grab_release()
self.destroy()
self.event = event
if __name__ == "__main__":
app = CTkMessagebox()
app.mainloop()
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+158
View File
@@ -0,0 +1,158 @@
[
"Airi",
"Akane",
"Akane (Bunny Girl)",
"Akari",
"Ako",
"Arisu",
"Arisu (Maid)",
"Aru",
"Aru (New Year)",
"Asuna",
"Asuna (Bunny Girl)",
"Atsuko",
"Ayane",
"Ayane (Swimsuit)",
"Azusa",
"Azusa (Swimsuit)",
"Cherino",
"Cherino (Hot Spring)",
"Chihiro",
"Chinatsu",
"Chinatsu (Hot Spring)",
"Chise",
"Chise (Swimsuit)",
"Eimi",
"Fubuki",
"Fuuka",
"Fuuka (New Year)",
"Hanae",
"Hanae (Christmas)",
"Hanako",
"Hanako (Swimsuit)",
"Hare",
"Haruka",
"Haruka (New Year)",
"Haruna",
"Haruna (New Year)",
"Haruna (Sportswear)",
"Hasumi",
"Hasumi (Sportswear)",
"Hatsune Miku",
"Hibiki",
"Hibiki (Cheerleader)",
"Hifumi",
"Hifumi (Swimsuit)",
"Himari",
"Hina",
"Hina (Swimsuit)",
"Hinata",
"Hinata (Swimsuit)",
"Hiyori",
"Hoshino",
"Hoshino (Swimsuit)",
"Iori",
"Iori (Swimsuit)",
"Iroha",
"Izumi",
"Izumi (Swimsuit)",
"Izuna",
"Izuna (Swimsuit)",
"Junko",
"Junko (New Year)",
"Juri",
"Kaede",
"Kaho",
"Kanna",
"Karin",
"Karin (Bunny Girl)",
"Kayoko",
"Kayoko (New Year)",
"Kazusa",
"Kirino",
"Koharu",
"Koharu (Swimsuit)",
"Kokona",
"Kotama",
"Kotori",
"Kotori (Cheerleader)",
"Koyuki",
"Maki",
"Mari",
"Mari (Sportswear)",
"Marina",
"Mashiro",
"Mashiro (Swimsuit)",
"Megu",
"Meru",
"Michiru",
"Midori",
"Mika",
"Mimori",
"Mimori (Swimsuit)",
"Mina",
"Mine",
"Minori",
"Misaki",
"Miyako",
"Miyako (Swimsuit)",
"Miyu",
"Miyu (Swimsuit)",
"Moe",
"Momiji",
"Momoi",
"Mutsuki",
"Mutsuki (New Year)",
"Nagisa",
"Natsu",
"Neru",
"Neru (Bunny Girl)",
"Noa",
"Nodoka",
"Nodoka (Hot Spring)",
"Nonomi",
"Nonomi (Swimsuit)",
"Pina",
"Reisa",
"Rumi",
"Saki",
"Saki (Swimsuit)",
"Sakurako",
"Saori",
"Saya",
"Saya (Casual)",
"Sena",
"Serika",
"Serika (New Year)",
"Serina",
"Serina (Christmas)",
"Shigure",
"Shimiko",
"Shiroko",
"Shiroko (Riding)",
"Shiroko (Swimsuit)",
"Shizuko",
"Shizuko (Swimsuit)",
"Shun",
"Shun (Kid)",
"Sumire",
"Suzumi",
"Toki",
"Toki (Bunny Girl)",
"Tomoe",
"Tsubaki",
"Tsukuyo",
"Tsurugi",
"Tsurugi (Swimsuit)",
"Ui",
"Ui (Swimsuit)",
"Utaha",
"Utaha (Cheerleader)",
"Wakamo",
"Wakamo (Swimsuit)",
"Yoshimi",
"Yuuka",
"Yuuka (Sportswear)",
"Yuzu",
"Yuzu (Maid)"
]
+168
View File
@@ -0,0 +1,168 @@
[
"\u30a2\u30a4\u30ea",
"\u30a2\u30ab\u30cd",
"\u30a2\u30ab\u30cd\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30a2\u30ab\u30ea",
"\u30a2\u30b3",
"\u30a2\u30b9\u30ca",
"\u30a2\u30b9\u30ca\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30a2\u30ba\u30b5",
"\u30a2\u30ba\u30b5\uff08\u6c34\u7740\uff09",
"\u30a2\u30c4\u30b3",
"\u30a2\u30e4\u30cd",
"\u30a2\u30e4\u30cd\uff08\u6c34\u7740\uff09",
"\u30a2\u30ea\u30b9",
"\u30a2\u30ea\u30b9\uff08\u30e1\u30a4\u30c9\uff09",
"\u30a2\u30eb",
"\u30a2\u30eb\uff08\u6b63\u6708\uff09",
"\u30a4\u30aa\u30ea",
"\u30a4\u30aa\u30ea\uff08\u6c34\u7740\uff09",
"\u30a4\u30ba\u30ca",
"\u30a4\u30ba\u30ca\uff08\u6c34\u7740\uff09",
"\u30a4\u30ba\u30df",
"\u30a4\u30ba\u30df\uff08\u6c34\u7740\uff09",
"\u30a4\u30c1\u30ab",
"\u30a4\u30ed\u30cf",
"\u30a6\u30a4",
"\u30a6\u30a4\uff08\u6c34\u7740\uff09",
"\u30a6\u30bf\u30cf",
"\u30a6\u30bf\u30cf\uff08\u5fdc\u63f4\u56e3\uff09",
"\u30a8\u30a4\u30df",
"\u30a8\u30a4\u30df\uff08\u6c34\u7740\uff09",
"\u30ab\u30a8\u30c7",
"\u30ab\u30b9\u30df",
"\u30ab\u30ba\u30b5",
"\u30ab\u30db",
"\u30ab\u30e8\u30b3",
"\u30ab\u30e8\u30b3\uff08\u6b63\u6708\uff09",
"\u30ab\u30ea\u30f3",
"\u30ab\u30ea\u30f3\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30ab\u30f3\u30ca",
"\u30ad\u30ad\u30e7\u30a6",
"\u30ad\u30ea\u30ce",
"\u30b3\u30b3\u30ca",
"\u30b3\u30bf\u30de",
"\u30b3\u30c8\u30ea",
"\u30b3\u30c8\u30ea\uff08\u5fdc\u63f4\u56e3\uff09",
"\u30b3\u30cf\u30eb",
"\u30b3\u30cf\u30eb\uff08\u6c34\u7740\uff09",
"\u30b3\u30e6\u30ad",
"\u30b5\u30aa\u30ea",
"\u30b5\u30ad",
"\u30b5\u30ad\uff08\u6c34\u7740\uff09",
"\u30b5\u30af\u30e9\u30b3",
"\u30b5\u30e4",
"\u30b5\u30e4\uff08\u79c1\u670d\uff09",
"\u30b7\u30b0\u30ec",
"\u30b7\u30b0\u30ec\uff08\u6e29\u6cc9\uff09",
"\u30b7\u30ba\u30b3",
"\u30b7\u30ba\u30b3\uff08\u6c34\u7740\uff09",
"\u30b7\u30df\u30b3",
"\u30b7\u30e5\u30f3",
"\u30b7\u30e5\u30f3\uff08\u5e7c\u5973\uff09",
"\u30b7\u30ed\u30b3",
"\u30b7\u30ed\u30b3\n\uff08\u30e9\u30a4\u30c7\u30a3\u30f3\u30b0\uff09",
"\u30b7\u30ed\u30b3\uff08\u6c34\u7740\uff09",
"\u30b8\u30e5\u30ea",
"\u30b8\u30e5\u30f3\u30b3",
"\u30b8\u30e5\u30f3\u30b3\uff08\u6b63\u6708\uff09",
"\u30b9\u30ba\u30df",
"\u30b9\u30df\u30ec",
"\u30bb\u30ca",
"\u30bb\u30ea\u30ab",
"\u30bb\u30ea\u30ab\uff08\u6b63\u6708\uff09",
"\u30bb\u30ea\u30ca",
"\u30bb\u30ea\u30ca\n\uff08\u30af\u30ea\u30b9\u30de\u30b9\uff09",
"\u30c1\u30a7\u30ea\u30ce",
"\u30c1\u30a7\u30ea\u30ce\uff08\u6e29\u6cc9\uff09",
"\u30c1\u30bb",
"\u30c1\u30bb\uff08\u6c34\u7740\uff09",
"\u30c1\u30ca\u30c4",
"\u30c1\u30ca\u30c4\uff08\u6e29\u6cc9\uff09",
"\u30c1\u30d2\u30ed",
"\u30c4\u30af\u30e8",
"\u30c4\u30d0\u30ad",
"\u30c4\u30eb\u30ae",
"\u30c4\u30eb\u30ae\uff08\u6c34\u7740\uff09",
"\u30c8\u30ad",
"\u30c8\u30ad\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30c8\u30e2\u30a8",
"\u30ca\u30ae\u30b5",
"\u30ca\u30c4",
"\u30cd\u30eb",
"\u30cd\u30eb\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30ce\u30a2",
"\u30ce\u30c9\u30ab",
"\u30ce\u30c9\u30ab\uff08\u6e29\u6cc9\uff09",
"\u30ce\u30ce\u30df",
"\u30ce\u30ce\u30df\uff08\u6c34\u7740\uff09",
"\u30cf\u30b9\u30df",
"\u30cf\u30b9\u30df\uff08\u4f53\u64cd\u670d\uff09",
"\u30cf\u30ca\u30a8",
"\u30cf\u30ca\u30a8\n\uff08\u30af\u30ea\u30b9\u30de\u30b9\uff09",
"\u30cf\u30ca\u30b3",
"\u30cf\u30ca\u30b3\uff08\u6c34\u7740\uff09",
"\u30cf\u30eb\u30ab",
"\u30cf\u30eb\u30ab\uff08\u6b63\u6708\uff09",
"\u30cf\u30eb\u30ca",
"\u30cf\u30eb\u30ca\uff08\u4f53\u64cd\u670d\uff09",
"\u30cf\u30eb\u30ca\uff08\u6b63\u6708\uff09",
"\u30cf\u30ec",
"\u30d2\u30ca",
"\u30d2\u30ca\u30bf",
"\u30d2\u30ca\u30bf\uff08\u6c34\u7740\uff09",
"\u30d2\u30ca\uff08\u6c34\u7740\uff09",
"\u30d2\u30d3\u30ad",
"\u30d2\u30d3\u30ad\uff08\u5fdc\u63f4\u56e3\uff09",
"\u30d2\u30d5\u30df",
"\u30d2\u30d5\u30df\uff08\u6c34\u7740\uff09",
"\u30d2\u30de\u30ea",
"\u30d2\u30e8\u30ea",
"\u30d5\u30a3\u30fc\u30ca",
"\u30d5\u30a6\u30ab",
"\u30d5\u30a6\u30ab\uff08\u6b63\u6708\uff09",
"\u30d5\u30d6\u30ad",
"\u30db\u30b7\u30ce",
"\u30db\u30b7\u30ce\uff08\u6c34\u7740\uff09",
"\u30de\u30ad",
"\u30de\u30b7\u30ed",
"\u30de\u30b7\u30ed\uff08\u6c34\u7740\uff09",
"\u30de\u30ea\u30ca",
"\u30de\u30ea\u30fc",
"\u30de\u30ea\u30fc\uff08\u4f53\u64cd\u670d\uff09",
"\u30df\u30ab",
"\u30df\u30b5\u30ad",
"\u30df\u30c1\u30eb",
"\u30df\u30c9\u30ea",
"\u30df\u30ca",
"\u30df\u30cd",
"\u30df\u30ce\u30ea",
"\u30df\u30e2\u30ea",
"\u30df\u30e2\u30ea\uff08\u6c34\u7740\uff09",
"\u30df\u30e4\u30b3",
"\u30df\u30e4\u30b3\uff08\u6c34\u7740\uff09",
"\u30df\u30e6",
"\u30df\u30e6\uff08\u6c34\u7740\uff09",
"\u30e0\u30c4\u30ad",
"\u30e0\u30c4\u30ad\uff08\u6b63\u6708\uff09",
"\u30e1\u30b0",
"\u30e1\u30eb",
"\u30e2\u30a8",
"\u30e2\u30df\u30b8",
"\u30e2\u30e2\u30a4",
"\u30e6\u30a6\u30ab",
"\u30e6\u30a6\u30ab\uff08\u4f53\u64cd\u670d\uff09",
"\u30e6\u30ab\u30ea",
"\u30e6\u30ba",
"\u30e6\u30ba\uff08\u30e1\u30a4\u30c9\uff09",
"\u30e8\u30b7\u30df",
"\u30eb\u30df",
"\u30ec\u30a4\u30b5",
"\u30ec\u30f3\u30b2",
"\u30ef\u30ab\u30e2",
"\u30ef\u30ab\u30e2\uff08\u6c34\u7740\uff09",
"\u4f50\u5929\u6d99\u5b50",
"\u521d\u97f3\u30df\u30af",
"\u5fa1\u5742\u7f8e\u7434",
"\u98df\u8702\u64cd\u7948"
]
+109
View File
@@ -0,0 +1,109 @@
import customtkinter
import json
import sys
import os
class Config:
def __init__(self, linker, config_file):
self.default_config = {
"ResetDaily": False,
"LastRun": "2023-12-24 21:41:55",
"ResetTime": "11:21:30",
"RechargeAP": False,
"PreferredTemplate": "template1",
"Queue": [],
"Event": False,
"Templates": {
"template1": []
}
}
self.linker = linker
self.config_file = config_file
if not os.path.exists(self.config_file):
with open(self.config_file, "w") as f:
json.dump(self.default_config, f, indent=2)
self.config_data = self.read()
self.linker.widgets = self.set_values_to_none(self.config_data)
self.locked = False
linker.config = self
def read(self):
# Read the JSON file
try:
with open(self.config_file, 'r') as json_file:
config_data = json.load(json_file)
return config_data
except FileNotFoundError:
print(f"Config file '{self.config_file}' not found.")
sys.exit(1)
except json.JSONDecodeError:
print(f"Invalid JSON format in '{self.config_file}'.")
sys.exit(1)
def set_values_to_none(self, input_dict):
result = {}
for key, value in input_dict.items():
if isinstance(value, dict):
result[key] = self.set_values_to_none(value)
else:
result[key] = None
return result
def load_config(self, widgets=None, config_data=None):
if widgets == None:
widgets = self.linker.widgets
config_data = self.config_data
for key in widgets:
if isinstance(widgets[key], dict) and isinstance(config_data[key], dict):
self.load_config(widgets[key], config_data[key])
else:
if widgets[key] is not None:
if isinstance(widgets[key], customtkinter.CTkCheckBox):
if config_data[key] == True:
widgets[key].select()
else:
widgets[key].deselect()
elif isinstance(widgets[key], customtkinter.CTkEntry):
widgets[key].insert(0, config_data[key])
else:
widgets[key].set(config_data[key])
def save_to_json(self, list_keys):
widget = self.linker.widgets
data = self.config_data
for i in list_keys[:-1]:
widget = widget[i]
data = data[i]
widget = widget[list_keys[-1]]
value = widget.get()
if isinstance(widget, customtkinter.CTkCheckBox):
value = True if value==1 else False
data[list_keys[-1]] = value
self.save_file("Configuration")
def save_file(self, name=None):
if self.locked:
with open("MCE\config.json", "r") as config_file:
new_config = json.load(config_file)
self.config_data["Queue"] = new_config["Queue"]
self.config_data["LastRun"] = new_config["LastRun"]
with open("MCE\config.json", "w") as config_file:
json.dump(self.config_data, config_file, indent=2)
if name:
self.linker.show_notification(name)
class Linker:
def __init__(self):
self.capitalise = lambda word: " ".join(x.title() for x in word.split("_"))
self.config = None
self.widgets = {}
self.sidebar = None
self.event_id = None
def show_notification(self, text):
if self.event_id:
self.sidebar.after_cancel(self.event_id)
self.sidebar.notification.show()
self.event_id = self.sidebar.after(2500, self.sidebar.notification.hide)
+7 -2
View File
@@ -12,7 +12,7 @@
The script is still under active development. The following features have been implemented:
- [x] **Cafe** Claim rewards / Interact / Second floor
- [x] **Cafe** Claim rewards / Interact / Invitation / Second floor
- [x] **Club** Claim AP
- [x] **Mailbox** Claim rewards
- [x] **Bounty** Auto sweep
@@ -22,7 +22,12 @@ The script is still under active development. The following features have been i
Supported servers:
- [x] JP
- [x] OVERSEA - Global
- [x] OVERSEA
Supported in-game languages:
- [x] Japanese
- [x] English
## Relative projects
+9 -3
View File
@@ -12,7 +12,7 @@
当前脚本还在活跃开发中,已经实现的功能有:
- [x] **咖啡厅** 领取奖励 / 互动 / 第二咖啡厅
- [x] **咖啡厅** 领取奖励 / 互动 / 邀请 / 第二咖啡厅
- [x] **公会** 领取体力
- [x] **邮箱** 领取奖励
- [x] **悬赏通缉** 自动扫荡
@@ -22,12 +22,18 @@
目前支持的服务器:
- [x] 日服
- [x] 国际服 - 全球
- [x] 国际服
目前支持的游戏内语言:
- [x] 日语
- [x] 英语
## 已知问题
若愿意提供其他服务器支持,请开 PR 或 Issue。
若愿意提供其他语言或国服支持,请开 PR 或 Issue。
- **国际服登录的全屏通知**:未实现自动关闭,正在研究中
- **大小月卡**:未实现自动领取,~~因为没买过~~,可能不影响使用。愿意提供图片的请开 Issue
- **月卡的额外悬赏券和学院交流券**:不太清楚月卡领取额外券的机制,~~因为没买过~~,可能影响相关任务使用券和体力的计算。愿意提供相关信息的请开
Issue
+20 -1
View File
@@ -46,11 +46,30 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
from tasks.tactical_challenge.tactical_challenge import TacticalChallenge
TacticalChallenge(config=self.config, device=self.device).run()
def task(self):
from tasks.task.task import Task
Task(config=self.config, device=self.device).run()
def shop(self):
from tasks.shop.shop import Shop
Shop(config=self.config, device=self.device).run()
def momotalk(self):
from tasks.momotalk.momotalk import MomoTalk
MomoTalk(config=self.config, device=self.device).run()
def mission(self):
from tasks.mission.mission import Mission
Mission(config=self.config, device=self.device).run()
def schedule(self):
from tasks.schedule.schedule import Schedule
Schedule(config=self.config, device=self.device).run()
def data_update(self):
from tasks.item.data_update import DataUpdate
DataUpdate(config=self.config, device=self.device).run()
if __name__ == '__main__':
aas = ArisuAutoSweeper('aas')
aas.loop()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

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