-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKKCleaner.py
More file actions
1535 lines (1343 loc) · 70.9 KB
/
Copy pathKKCleaner.py
File metadata and controls
1535 lines (1343 loc) · 70.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# KKCleaner
# Copyright (c) 2024 GameProgram7777
# Licensed under the MIT License. See the LICENSE file for details.
import os
import csv
import re
import shutil
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, Frame, Label, Entry, Button, Checkbutton, BooleanVar, StringVar
import tkinterdnd2 as tkdnd
import chardet
import sys
import clr
# 添加 Mono.Cecil 的路徑
cecil_path = os.path.dirname(os.path.abspath(__file__)) # 假設 Mono.Cecil.dll 與腳本在同一目錄
sys.path.append(cecil_path)
clr.AddReference(os.path.join(cecil_path, 'Mono.Cecil')) # 明確指定 DLL 檔案路徑
from Mono.Cecil import ModuleDefinition, ReaderParameters, DefaultAssemblyResolver
def load_plugin_info(dll_path):
"""從 DLL 檔案中讀取 plugin 的 GUID"""
try:
# 創建 DefaultAssemblyResolver
resolver = DefaultAssemblyResolver()
resolver.AddSearchDirectory(os.path.dirname(dll_path))
# 讀取模組定義
reader_params = ReaderParameters()
reader_params.AssemblyResolver = resolver
module = ModuleDefinition.ReadModule(dll_path, reader_params)
# 查找 BepInEx.BepInPlugin 特性
bepinex_plugin_type = None
for type_ref in module.GetTypeReferences():
if type_ref.FullName == "BepInEx.BepInPlugin":
bepinex_plugin_type = type_ref
break
if not bepinex_plugin_type:
print(f"No BepInEx.BepInPlugin type found in {dll_path}")
return None
# 遍歷所有類型,查找帶有 BepInEx.BepInPlugin 特性的類
for type_def in module.Types:
for custom_attr in type_def.CustomAttributes:
if custom_attr.AttributeType.FullName == "BepInEx.BepInPlugin":
# 提取 GUID
if len(custom_attr.ConstructorArguments) >= 1:
guid = custom_attr.ConstructorArguments[0].Value # 直接使用 Value,不需要 ToString()
return guid
print(f"No GUID found in {dll_path}")
return None
except Exception as e:
print(f"Error reading DLL {dll_path}: {str(e)}")
return None
finally:
# 確保模組資源被釋放
if 'module' in locals():
module.Dispose()
class CSVType:
MOD = "mod"
PLUGIN = "plugin"
class Localizations:
LANGUAGES = {
'en': {
'title': 'KK Cleaner',
'upload_text': 'Drag and drop your CSV file here or click to upload',
'detect': 'Detect',
'action': 'Action',
'min_usages': 'Minimum Card Usages',
'run': 'Run',
'browse': 'Browse',
'action_pack': 'Pack',
'action_undo_pack': 'Undo Pack',
'action_remove': 'Remove',
'language': 'Language:',
'error_csv_upload': 'Only .csv files are allowed.',
'error_upload': 'Upload Error',
'error_no_csv': 'Please upload a CSV file first.',
'error_mod_path': 'Please enter path',
'error_invalid_path': 'The entered path is invalid.',
'error_no_min_usage': 'Please enter minimum card usages.',
'error_no_detection': 'Please detect first.',
'error_unknown_action': 'Unknown action selected.',
'error_no_data': 'No data found in CSV.',
'nothing_to_undo': 'Nothing to undo.',
'success_csv_upload': 'CSV uploaded successfully!',
# messagebox 標題
'result_title': 'Detection Results',
'success_title': 'Success',
'nothing_to_pack_title': 'Nothing to Pack',
'nothing_to_remove_title': 'Nothing to Remove',
'nothing_to_undo_title': 'Nothing to Undo',
'error_pack_title': 'Pack Error',
'error_remove_title': 'Remove Error',
'error_undo_pack_title': 'Undo Pack Error',
# 初始組合狀態的鍵值
'mod_plugin_path': 'Mod/Plugin Path',
'custom_mod_plugin': 'Custom Mods/Plugins Only',
# Mod 專用鍵值
'mod_path': 'Mod Path',
'custom_mods': 'Custom Mods Only',
'result_all_used': 'All mods have card usages above the minimum.',
'result_low_usage_found': '{} mods found with card usages at or below the minimum.',
'nothing_to_remove': 'No mods to remove.',
'nothing_to_pack': 'No mods to pack.',
'confirm_remove': 'Are you sure you want to remove {0} mod(s) with usage count at or below {1}?',
'confirm_remove_title': 'Confirm Remove',
'success_pack': 'Pack executed successfully! {} mods moved.',
'success_remove': 'Remove executed successfully! {} mods removed.',
'success_undo_pack': 'Undo Pack executed successfully! {} mods restored.',
'error_pack': 'Failed to pack mods: {}',
'error_remove': 'Failed to remove mods: {}',
'error_undo_pack': 'Failed to undo pack: {}',
# Plugin 專用鍵值
'plugin_path': 'Plugin Path',
'custom_plugins': 'Custom Plugins Only',
'result_all_plugins_used': 'All plugins have card usages above the minimum.',
'result_low_usage_plugins_found': '{} plugins found with card usages at or below the minimum.',
'nothing_to_remove_plugin': 'No plugins to remove.',
'nothing_to_pack_plugin': 'No plugins to pack.',
'confirm_remove_plugin': 'Are you sure you want to remove {0} plugin(s) with usage count at or below {1}?',
'success_pack_plugin': 'Pack executed successfully! {} plugins moved.',
'success_remove_plugin': 'Remove executed successfully! {} plugins removed.',
'success_undo_pack_plugin': 'Undo Pack executed successfully! {} plugins restored.',
'error_pack_plugin': 'Failed to pack plugins: {}',
'error_remove_plugin': 'Failed to remove plugins: {}',
'error_undo_pack_plugin': 'Failed to undo pack: {}'
},
'zh-tw': {
'title': 'KK 清理器',
'upload_text': '將 CSV 檔案拖曳至此處或點擊上傳',
'detect': '偵測',
'action': '操作',
'min_usages': '最小卡片使用次數',
'run': '執行',
'browse': '瀏覽',
'action_pack': '打包',
'action_undo_pack': '復原打包',
'action_remove': '移除',
'language': '語言:',
'error_csv_upload': '只能上傳 .csv 檔案',
'error_upload': '上傳錯誤',
'error_no_csv': '請先上傳 CSV 檔案',
'error_mod_path': '請輸入路徑',
'error_invalid_path': '輸入的路徑無效',
'error_no_min_usage': '請輸入最小卡片使用次數',
'error_no_detection': '請先進行偵測',
'error_unknown_action': '選擇了未知的操作',
'error_no_data': 'CSV 檔案中沒有發現任何資料',
'nothing_to_undo': '沒有可復原的項目',
'success_csv_upload': 'CSV 上傳成功!',
# messagebox 標題
'result_title': '偵測結果',
'success_title': '成功',
'nothing_to_pack_title': '沒有可打包的項目',
'nothing_to_remove_title': '沒有可移除的項目',
'nothing_to_undo_title': '沒有可復原的項目',
'error_pack_title': '打包錯誤',
'error_remove_title': '移除錯誤',
'error_undo_pack_title': '復原打包錯誤',
# 初始組合狀態的鍵值
'mod_plugin_path': '模組/插件路徑',
'custom_mod_plugin': '僅限自訂模組/插件',
# Mod 專用鍵值
'mod_path': '模組路徑',
'custom_mods': '僅限自訂模組',
'result_all_used': '所有模組使用次數皆高於最小值',
'result_low_usage_found': '找到 {} 個使用次數低於或等於最小值的模組',
'nothing_to_remove': '沒有可移除的模組',
'nothing_to_pack': '沒有可打包的模組',
'confirm_remove': '您確定要移除 {0} 個使用次數在{1}次以下的模組嗎?',
'confirm_remove_title': '確認移除',
'success_pack': '打包執行成功!已移動 {} 個模組',
'success_remove': '移除執行成功!已移除 {} 個模組',
'success_undo_pack': '復原打包執行成功!已還原 {} 個模組',
'error_pack': '打包模組失敗:{}',
'error_remove': '移除模組失敗:{}',
'error_undo_pack': '復原打包失敗:{}',
# Plugin 專用鍵值
'plugin_path': '插件路徑',
'custom_plugins': '僅限自訂插件',
'result_all_plugins_used': '所有插件使用次數皆高於最小值',
'result_low_usage_plugins_found': '找到 {} 個使用次數低於或等於最小值的插件',
'nothing_to_remove_plugin': '沒有可移除的插件',
'nothing_to_pack_plugin': '沒有可打包的插件',
'confirm_remove_plugin': '您確定要移除 {0} 個使用次數在{1}次以下的插件嗎?',
'success_pack_plugin': '打包執行成功!已移動 {} 個插件',
'success_remove_plugin': '移除執行成功!已移除 {} 個插件',
'success_undo_pack_plugin': '復原打包執行成功!已還原 {} 個插件',
'error_pack_plugin': '打包插件失敗:{}',
'error_remove_plugin': '移除插件失敗:{}',
'error_undo_pack_plugin': '復原打包失敗:{}'
},
'zh-cn': {
'title': 'KK 清理器',
'upload_text': '将 CSV 文件拖拽至此处或点击上传',
'detect': '检测',
'action': '操作',
'min_usages': '最小卡片使用次数',
'run': '执行',
'browse': '浏览',
'action_pack': '打包',
'action_undo_pack': '撤销打包',
'action_remove': '移除',
'language': '语言:',
'error_csv_upload': '只能上传 .csv 文件',
'error_upload': '上传错误',
'error_no_csv': '请先上传 CSV 文件',
'error_mod_path': '请输入路径',
'error_invalid_path': '输入的路径无效',
'error_no_min_usage': '请输入最小卡片使用次数',
'error_no_detection': '请先进行检测',
'error_unknown_action': '选择了未知的操作',
'error_no_data': 'CSV 文件中没有发现任何数据',
'nothing_to_undo': '没有可撤销的项目',
'success_csv_upload': 'CSV 上传成功!',
# messagebox 標題
'result_title': '检测结果',
'success_title': '成功',
'nothing_to_pack_title': '没有可打包的项目',
'nothing_to_remove_title': '没有可移除的项目',
'nothing_to_undo_title': '没有可撤销的项目',
'error_pack_title': '打包错误',
'error_remove_title': '移除错误',
'error_undo_pack_title': '撤销打包错误',
# 初始组合状态的键值
'mod_plugin_path': '模组/插件路径',
'custom_mod_plugin': '仅限自定义模组/插件',
# Mod 专用键值
'mod_path': '模组路径',
'custom_mods': '仅限自定义模组',
'result_all_used': '所有模组使用次数均高于最小值',
'result_low_usage_found': '找到 {} 个使用次数低于或等于最小值的模组',
'nothing_to_remove': '没有可移除的模组',
'nothing_to_pack': '没有可打包的模组',
'confirm_remove': '您确定要移除 {0} 个使用次数在{1}次以下的模组吗?',
'confirm_remove_title': '确认移除',
'success_pack': '打包执行成功!已移动 {} 个模组',
'success_remove': '移除执行成功!已移除 {} 个模组',
'success_undo_pack': '撤销打包执行成功!已还原 {} 个模组',
'error_pack': '打包模组失败:{}',
'error_remove': '移除模组失败:{}',
'error_undo_pack': '撤销打包失败:{}',
# Plugin 专用键值
'plugin_path': '插件路径',
'custom_plugins': '仅限自定义插件',
'result_all_plugins_used': '所有插件使用次数均高于最小值',
'result_low_usage_plugins_found': '找到 {} 个使用次数低于或等于最小值的插件',
'nothing_to_remove_plugin': '没有可移除的插件',
'nothing_to_pack_plugin': '没有可打包的插件',
'confirm_remove_plugin': '您确定要移除 {0} 个使用次数在{1}次以下的插件吗?',
'success_pack_plugin': '打包执行成功!已移动 {} 个插件',
'success_remove_plugin': '移除执行成功!已移除 {} 个插件',
'success_undo_pack_plugin': '撤销打包执行成功!已还原 {} 个插件',
'error_pack_plugin': '打包插件失败:{}',
'error_remove_plugin': '移除插件失败:{}',
'error_undo_pack_plugin': '撤销打包失败:{}'
},
'ja': {
'title': 'KK クリーナー',
'upload_text': 'CSVファイルをここにドラッグ&ドロップするかアップロード',
'detect': '検出',
'action': 'アクション',
'min_usages': '最小カード使用回数',
'run': '実行',
'browse': '参照',
'action_pack': 'パック',
'action_undo_pack': 'パックを取り消す',
'action_remove': '削除',
'language': '言語:',
'error_csv_upload': '.csvファイルのみ許可されています',
'error_upload': 'アップロードエラー',
'error_no_csv': 'まずCSVファイルをアップロードしてください',
'error_mod_path': 'パスを入力してください',
'error_invalid_path': '入力されたパスは無効です',
'error_no_min_usage': '最小カード使用回数を入力してください',
'error_no_detection': '先に検出を実行してください',
'error_unknown_action': '未知のアクションが選択されました',
'error_no_data': 'CSVにデータが見つかりません',
'nothing_to_undo': '取り消すものがありません',
'success_csv_upload': 'CSVのアップロードに成功しました!',
# messagebox 標題
'result_title': '検出結果',
'success_title': '成功',
'nothing_to_pack_title': 'パックするものがありません',
'nothing_to_remove_title': '削除するものがありません',
'nothing_to_undo_title': '取り消すものがありません',
'error_pack_title': 'パックエラー',
'error_remove_title': '削除エラー',
'error_undo_pack_title': 'パック取り消しエラー',
# 初期組み合わせ状態のキー値
'mod_plugin_path': 'MOD/プラグインパス',
'custom_mod_plugin': 'カスタムMOD/プラグインのみ',
# MOD専用キー値
'mod_path': 'MODパス',
'custom_mods': 'カスタムMODのみ',
'result_all_used': 'すべてのMODの使用回数が最小値を上回っています',
'result_low_usage_found': '使用回数が最小値以下のMODが {} 個見つかりました',
'nothing_to_remove': '削除するMODがありません',
'nothing_to_pack': 'パックするMODがありません',
'confirm_remove': '使用回数が{1}回以下のMOD {0} 個を削除してもよろしいですか?',
'confirm_remove_title': '削除の確認',
'success_pack': 'パックが正常に実行されました! {} 個のMODが移動されました',
'success_remove': '削除が正常に実行されました! {} 個のMODが削除されました',
'success_undo_pack': 'パックの取り消しが正常に実行されました! {} 個のMODが復元されました',
'error_pack': 'MODのパックに失敗しました:{}',
'error_remove': 'MODの削除に失敗しました:{}',
'error_undo_pack': 'パックの取り消しに失敗しました:{}',
# プラグイン専用キー値
'plugin_path': 'プラグインパス',
'custom_plugins': 'カスタムプラグインのみ',
'result_all_plugins_used': 'すべてのプラグインの使用回数が最小値を上回っています',
'result_low_usage_plugins_found': '使用回数が最小値以下のプラグインが {} 個見つかりました',
'nothing_to_remove_plugin': '削除するプラグインがありません',
'nothing_to_pack_plugin': 'パックするプラグインがありません',
'confirm_remove_plugin': '使用回数が{1}回以下のプラグイン {0} 個を削除してもよろしいですか?',
'success_pack_plugin': 'パックが正常に実行されました! {} 個のプラグインが移動されました',
'success_remove_plugin': '削除が正常に実行されました! {} 個のプラグインが削除されました',
'success_undo_pack_plugin': 'パックの取り消しが正常に実行されました! {} 個のプラグインが復元されました',
'error_pack_plugin': 'プラグインのパックに失敗しました:{}',
'error_remove_plugin': 'プラグインの削除に失敗しました:{}',
'error_undo_pack_plugin': 'パックの取り消しに失敗しました:{}'
},
'ko': {
'title': 'KK 클리너',
'upload_text': 'CSV 파일을 여기에 끌어다 놓거나 클릭하여 업로드',
'detect': '감지',
'action': '작업',
'min_usages': '최소 카드 사용 횟수',
'run': '실행',
'browse': '찾아보기',
'action_pack': '패킹',
'action_undo_pack': '패킹 취소',
'action_remove': '제거',
'language': '언어:',
'error_csv_upload': '.csv 파일만 허용됩니다',
'error_upload': '업로드 오류',
'error_no_csv': '먼저 CSV 파일을 업로드하세요',
'error_mod_path': '경로를 입력하세요',
'error_invalid_path': '입력한 경로가 잘못되었습니다',
'error_no_min_usage': '최소 카드 사용 횟수를 입력하세요',
'error_no_detection': '먼저 감지를 실행하세요',
'error_unknown_action': '알 수 없는 작업이 선택되었습니다',
'error_no_data': 'CSV 파일에 데이터가 없습니다',
'nothing_to_undo': '취소할 항목이 없습니다',
'success_csv_upload': 'CSV 업로드 성공!',
# messagebox 標題
'result_title': '감지 결과',
'success_title': '성공',
'nothing_to_pack_title': '패킹할 항목 없음',
'nothing_to_remove_title': '제거할 항목 없음',
'nothing_to_undo_title': '취소할 항목 없음',
'error_pack_title': '패킹 오류',
'error_remove_title': '제거 오류',
'error_undo_pack_title': '패킹 취소 오류',
# 초기 결합 상태 키값
'mod_plugin_path': '모드/플러그인 경로',
'custom_mod_plugin': '사용자 지정 모드/플러그인만',
# 모드 전용 키값
'mod_path': '모드 경로',
'custom_mods': '사용자 지정 모드만',
'result_all_used': '모든 모드의 사용 횟수가 최소값보다 높습니다',
'result_low_usage_found': '사용 횟수가 최소값 이하인 모드 {} 개 발견',
'nothing_to_remove': '제거할 모드가 없습니다',
'nothing_to_pack': '패킹할 모드가 없습니다',
'confirm_remove': '사용 횟수가 {1}회 이하인 모드 {0} 개를 제거하시겠습니까?',
'confirm_remove_title': '제거 확인',
'success_pack': '패킹 실행 성공! {} 개 모드 이동됨',
'success_remove': '제거 실행 성공! {} 개 모드 제거됨',
'success_undo_pack': '패킹 취소 실행 성공! {} 개 모드 복원됨',
'error_pack': '모드 패킹 실패: {}',
'error_remove': '모드 제거 실패: {}',
'error_undo_pack': '패킹 취소 실패: {}',
# 플러그인 전용 키값
'plugin_path': '플러그인 경로',
'custom_plugins': '사용자 지정 플러그인만',
'result_all_plugins_used': '모든 플러그인의 사용 횟수가 최소값보다 높습니다',
'result_low_usage_plugins_found': '사용 횟수가 최소값 이하인 플러그인 {} 개 발견',
'nothing_to_remove_plugin': '제거할 플러그인이 없습니다',
'nothing_to_pack_plugin': '패킹할 플러그인이 없습니다',
'confirm_remove_plugin': '사용 횟수가 {1}회 이하인 플러그인 {0} 개를 제거하시겠습니까?',
'success_pack_plugin': '패킹 실행 성공! {} 개 플러그인 이동됨',
'success_remove_plugin': '제거 실행 성공! {} 개 플러그인 제거됨',
'success_undo_pack_plugin': '패킹 취소 실행 성공! {} 개 플러그인 복원됨',
'error_pack_plugin': '플러그인 패킹 실패: {}',
'error_remove_plugin': '플러그인 제거 실패: {}',
'error_undo_pack_plugin': '패킹 취소 실패: {}'
},
'ru': {
'title': 'KK Cleaner',
'upload_text': 'Перетащите CSV-файл сюда или нажмите для загрузки',
'detect': 'Обнаружить',
'action': 'Действие',
'min_usages': 'Минимальное количество использований карт',
'run': 'Выполнить',
'browse': 'Обзор',
'action_pack': 'Упаковка',
'action_undo_pack': 'Отмена упаковки',
'action_remove': 'Удаление',
'language': 'Язык:',
'error_csv_upload': 'Разрешены только файлы .csv',
'error_upload': 'Ошибка загрузки',
'error_no_csv': 'Сначала загрузите CSV-файл',
'error_mod_path': 'Введите путь',
'error_invalid_path': 'Введён неверный путь',
'error_no_min_usage': 'Введите минимальное количество использований карт',
'error_no_detection': 'Сначала выполните обнаружение',
'error_unknown_action': 'Выбрано неизвестное действие',
'error_no_data': 'В CSV не найдены данные',
'nothing_to_undo': 'Нечего отменять',
'success_csv_upload': 'CSV успешно загружен!',
# messagebox 標題
'result_title': 'Результаты обнаружения',
'success_title': 'Успех',
'nothing_to_pack_title': 'Нечего упаковывать',
'nothing_to_remove_title': 'Нечего удалять',
'nothing_to_undo_title': 'Нечего отменять',
'error_pack_title': 'Ошибка упаковки',
'error_remove_title': 'Ошибка удаления',
'error_undo_pack_title': 'Ошибка отмены упаковки',
# Ключи для начального комбинированного состояния
'mod_plugin_path': 'Путь к модам/плагинам',
'custom_mod_plugin': 'Только пользовательские моды/плагины',
# Ключи для модов
'mod_path': 'Путь к модам',
'custom_mods': 'Только пользовательские моды',
'result_all_used': 'Все моды используются выше минимума',
'result_low_usage_found': 'Найдено {} модов с использованием не выше минимума',
'nothing_to_remove': 'Нечего удалять из модов',
'nothing_to_pack': 'Нечего упаковывать из модов',
'confirm_remove': 'Вы уверены, что хотите удалить {0} модов с количеством использований не более {1}?',
'confirm_remove_title': 'Подтвердить удаление',
'success_pack': 'Упаковка выполнена успешно! Перемещено {} модов',
'success_remove': 'Удаление выполнено успешно! Удалено {} модов',
'success_undo_pack': 'Отмена упаковки выполнена успешно! Восстановлено {} модов',
'error_pack': 'Не удалось упаковать моды: {}',
'error_remove': 'Не удалось удалить моды: {}',
'error_undo_pack': 'Не удалось отменить упаковку: {}',
# Ключи для плагинов
'plugin_path': 'Путь к плагинам',
'custom_plugins': 'Только пользовательские плагины',
'result_all_plugins_used': 'Все плагины используются выше минимума',
'result_low_usage_plugins_found': 'Найдено {} плагинов с использованием не выше минимума',
'nothing_to_remove_plugin': 'Нечего удалять из плагинов',
'nothing_to_pack_plugin': 'Нечего упаковывать из плагинов',
'confirm_remove_plugin': 'Вы уверены, что хотите удалить {0} плагинов с количеством использований не более {1}?',
'success_pack_plugin': 'Упаковка выполнена успешно! Перемещено {} плагинов',
'success_remove_plugin': 'Удаление выполнено успешно! Удалено {} плагинов',
'success_undo_pack_plugin': 'Отмена упаковки выполнена успешно! Восстановлено {} плагинов',
'error_pack_plugin': 'Не удалось упаковать плагины: {}',
'error_remove_plugin': 'Не удалось удалить плагины: {}',
'error_undo_pack_plugin': 'Не удалось отменить упаковку: {}'
}
}
class KKCleanerTool:
def __init__(self, master):
self.master = master
self.current_language = StringVar(value='en')
self.current_language.trace_add('write', self.change_language)
# 初始化變數
self.csv_file = None
self.csv_type = None
self.detection_performed = False
self.low_usage_items = []
self.base_path = None
self.moved_files = {}
self.current_action_index = 0
# 配置主窗口
self.setup_window()
self.create_widgets()
# 配置拖放功能
self.setup_drag_drop()
def change_language(self, *args, **kwargs):
"""語言切換處理"""
if hasattr(self, 'action_combo'):
self.current_action_index = self.action_combo.current()
self.update_ui_elements()
def on_action_change(self, *args):
"""操作選項變更處理"""
self.current_action_index = self.action_combo.current()
def update_ui_elements(self):
"""更新UI元素"""
self.master.title(self.get_localized_text('title'))
self.language_label.config(text=self.get_localized_text('language'))
self.drop_area.config(text=self.get_localized_text('upload_text'))
self.detect_button.config(text=self.get_localized_text('detect'))
self.path_label.config(text=self.get_localized_text('mod_plugin_path'))
self.browse_button.config(text=self.get_localized_text('browse'))
self.action_label.config(text=self.get_localized_text('action'))
self.custom_checkbox.config(text=self.get_localized_text('custom_mod_plugin'))
self.min_usages_label.config(text=self.get_localized_text('min_usages'))
self.run_button.config(text=self.get_localized_text('run'))
actions = [
self.get_localized_text('action_pack'),
self.get_localized_text('action_undo_pack'),
self.get_localized_text('action_remove')
]
self.action_combo.config(values=actions)
self.action_combo.current(self.current_action_index)
self.action_combo_var.set(actions[self.current_action_index])
def update_usage_range(self, usages):
"""更新使用次數範圍顯示"""
if usages:
min_usage = min(usages)
max_usage = max(usages)
self.usage_range_label.config(text=f"({min_usage}, {max_usage})")
else:
self.usage_range_label.config(text="")
def update_ui_for_csv_type(self):
"""根據CSV類型更新UI"""
self.path_label.config(text=self.get_localized_text('mod_plugin_path'))
self.custom_checkbox.config(text=self.get_localized_text('custom_mod_plugin'))
def show_detection_results(self):
"""顯示檢測結果"""
if not self.low_usage_items:
key = "result_all_plugins_used" if self.csv_type == CSVType.PLUGIN else "result_all_used"
messagebox.showinfo(
self.get_localized_text("result_title"),
self.get_localized_text(key)
)
else:
key = "result_low_usage_plugins_found" if self.csv_type == CSVType.PLUGIN else "result_low_usage_found"
messagebox.showinfo(
self.get_localized_text("result_title"),
self.get_localized_text(key, len(self.low_usage_items))
)
def upload_csv(self, event=None):
file_path = filedialog.askopenfilename(
title="Upload CSV",
filetypes=[("CSV Files", "*.csv")]
)
if file_path and file_path.endswith('.csv'):
self.load_csv(file_path)
def manual_mod_path_input(self, event=None):
"""處理手動輸入模組路徑"""
path = self.path_input.get().strip()
if path and os.path.exists(path) and os.path.isdir(path):
self.base_path = path
print(f"Path manually set to: {path}")
else:
print(f"Invalid path entered: {path}")
def browse_mod_path(self):
"""開啟資料夾選擇對話框"""
path = filedialog.askdirectory(title="Select Mod/Plugin Directory")
if path:
self.path_input.delete(0, tk.END)
self.path_input.insert(0, path)
self.base_path = path
print(f"Path set to: {path}")
def create_widgets(self):
"""建立 UI 元件"""
# Main frame
main_frame = Frame(self.master, bg='#f0f0f0')
main_frame.pack(padx=20, pady=20, fill='both', expand=True)
# Language Selector
language_frame = Frame(main_frame, bg='#f0f0f0')
language_frame.pack(fill='x', pady=(0, 10))
self.language_label = Label(
language_frame,
text=self.get_localized_text('language'),
bg='#f0f0f0'
)
self.language_label.pack(side='left', padx=(0, 10))
# 顯示語言名稱對應的母語
language_display_names = {
'en': 'English',
'zh-tw': '繁體中文',
'zh-cn': '简体中文',
'ja': '日本語',
'ko': '한국어',
'ru': 'Русский'
}
# 建立語言選擇下拉選單
self.language_combo = ttk.Combobox(
language_frame,
state='readonly',
values=[language_display_names[lang] for lang in Localizations.LANGUAGES.keys()],
width=10
)
current_language_index = list(Localizations.LANGUAGES.keys()).index(self.current_language.get())
self.language_combo.current(current_language_index)
self.language_combo.pack(side='left')
# 綁定語言切換事件
def on_language_change(event):
selected_index = self.language_combo.current()
selected_language = list(Localizations.LANGUAGES.keys())[selected_index]
self.current_language.set(selected_language)
self.language_combo.bind("<<ComboboxSelected>>", on_language_change)
# Minimum Card Usages Selection
min_usages_frame = Frame(main_frame, bg='#f0f0f0')
min_usages_frame.pack(pady=5)
self.min_usages_label = Label(
min_usages_frame,
text=self.get_localized_text('min_usages'),
bg='#f0f0f0'
)
self.min_usages_label.pack(side='left')
self.min_usages_var = StringVar(value='0')
self.min_usages_entry = Entry(
min_usages_frame,
textvariable=self.min_usages_var,
width=5
)
self.min_usages_entry.pack(side='left', padx=(0, 10))
self.usage_range_label = Label(
min_usages_frame,
text='',
bg='#f0f0f0'
)
self.usage_range_label.pack(side='left')
# CSV Upload Area
self.drop_area = tk.Label(
main_frame,
text=self.get_localized_text('upload_text'),
bg='white',
fg='gray',
font=('Arial', 12),
width=50,
height=10,
relief='groove',
borderwidth=2
)
self.drop_area.pack(pady=10, padx=10, fill='both', expand=True)
self.drop_area.bind("<Button-1>", self.upload_csv)
# Detect Button
self.detect_button = Button(
main_frame,
text=self.get_localized_text('detect'),
command=self.detect_low_usage_items
)
self.detect_button.pack(pady=5)
# Path Input
path_frame = Frame(main_frame, bg='#f0f0f0')
path_frame.pack(pady=5, padx=10, fill='x')
self.path_label = Label(
path_frame,
text=self.get_localized_text('mod_plugin_path'),
bg='#f0f0f0'
)
self.path_label.pack(side='top', anchor='w')
self.path_input = Entry(path_frame, width=40)
self.path_input.pack(side='left', expand=True, fill='x', padx=(0, 10))
self.path_input.bind('<KeyRelease>', self.manual_mod_path_input)
self.browse_button = Button(
path_frame,
text=self.get_localized_text('browse'),
command=self.browse_mod_path
)
self.browse_button.pack(side='right')
# Action Selection
action_frame = Frame(main_frame, bg='#f0f0f0')
action_frame.pack(pady=5)
self.action_label = Label(
action_frame,
text=self.get_localized_text('action'),
bg='#f0f0f0'
)
self.action_label.pack(side='top')
# Action Combo Box
self.action_combo_var = StringVar()
self.action_combo = ttk.Combobox(
action_frame,
textvariable=self.action_combo_var,
state="readonly"
)
self.action_combo.pack(pady=5)
actions = [
self.get_localized_text('action_pack'),
self.get_localized_text('action_undo_pack'),
self.get_localized_text('action_remove')
]
self.action_combo.config(values=actions)
self.action_combo.current(0)
self.current_action_index = 0
# Custom Checkbox
custom_mods_frame = Frame(main_frame, bg='#f0f0f0')
custom_mods_frame.pack(pady=5)
self.custom_mods_var = BooleanVar()
self.custom_checkbox = Checkbutton(
custom_mods_frame,
text=self.get_localized_text('custom_mod_plugin'),
variable=self.custom_mods_var,
bg='#f0f0f0'
)
self.custom_checkbox.pack()
# Run Button
self.run_button = Button(
main_frame,
text=self.get_localized_text('run'),
command=self.execute_action
)
self.run_button.pack(pady=5)
def setup_window(self):
"""設置主窗口"""
self.master.title("KK Cleaner")
self.master.geometry("600x550")
self.master.configure(bg='#f0f0f0')
self.style = ttk.Style()
self.style.theme_use('clam')
self.style.configure('TButton', background='#4CAF50', foreground='white')
self.style.configure('TCombobox', background='white')
def setup_drag_drop(self):
"""設置拖放功能"""
self.master.drop_target_register(tkdnd.DND_FILES)
self.master.dnd_bind('<<Drop>>', self.handle_drop)
def handle_drop(self, event):
"""處理文件拖放"""
dropped_file = event.data
dropped_file = dropped_file.strip('{}').strip('"')
if dropped_file.lower().endswith('.csv'):
self.load_csv(dropped_file)
else:
messagebox.showerror(
self.get_localized_text('error_upload'),
self.get_localized_text('error_csv_upload')
)
def get_localized_text(self, key, *args):
"""Get localized text for the current language"""
lang = self.current_language.get()
text = Localizations.LANGUAGES.get(lang, Localizations.LANGUAGES['en']).get(key, key)
return text.format(*args) if args else text
# 輔助方法
def get_target_folder(self):
"""獲取目標資料夾名稱"""
return "low_usage_plugins" if self.csv_type == CSVType.PLUGIN else "low_usage_mods"
def get_target_folder_path(self):
"""獲取目標資料夾完整路徑"""
return os.path.normpath(os.path.join(self.base_path, self.get_target_folder()))
def walk_files(self, base_path, exclude_dir=None):
"""遍歷文件的輔助方法,排除指定目錄"""
for root, dirs, files in os.walk(base_path):
if exclude_dir and os.path.abspath(root) == os.path.abspath(exclude_dir):
dirs.clear() # 不遍歷被排除的目錄
continue
yield from ((root, file) for file in files)
def safe_remove_file(self, file_path):
"""安全地刪除文件"""
try:
if os.path.exists(file_path):
os.remove(file_path)
return True
return False
except Exception as e:
print(f"Error removing file {file_path}: {str(e)}")
return False
def safe_move_file(self, source, dest):
"""安全地移動文件"""
try:
if os.path.exists(source):
os.makedirs(os.path.dirname(dest), exist_ok=True)
shutil.move(source, dest)
return True
return False
except Exception as e:
print(f"Error moving file from {source} to {dest}: {str(e)}")
return False
def clean_empty_folder(self, folder_path):
"""清理空資料夾"""
try:
if os.path.exists(folder_path) and not os.listdir(folder_path):
os.rmdir(folder_path)
print(f"Removed empty folder: {folder_path}")
return True
return False
except Exception as e:
print(f"Error removing empty folder {folder_path}: {str(e)}")
return False
def validate_detection(self):
"""驗證檢測條件"""
if self.csv_file is None:
messagebox.showerror(
self.get_localized_text("error_upload"),
self.get_localized_text("error_no_csv")
)
return False
if not self.min_usages_var.get().strip():
messagebox.showerror(
self.get_localized_text("error_upload"),
self.get_localized_text("error_no_min_usage")
)
return False
try:
int(self.min_usages_var.get())
except ValueError:
messagebox.showerror(
self.get_localized_text("error_upload"),
self.get_localized_text("error_no_min_usage")
)
return False
return True
def validate_action(self):
"""驗證執行操作的條件"""
if not self.detection_performed:
messagebox.showerror(
self.get_localized_text("error_upload"),
self.get_localized_text("error_no_detection")
)
return False
path_input = self.path_input.get().strip()
if not path_input:
messagebox.showerror(
self.get_localized_text("error_upload"),
self.get_localized_text("error_mod_path")
)
return False
if not os.path.exists(path_input) or not os.path.isdir(path_input):
messagebox.showerror(
self.get_localized_text("error_upload"),
self.get_localized_text("error_invalid_path")
)
return False
self.base_path = path_input
return True
def detect_csv_encoding(self, file_path):
"""檢測 CSV 文件的編碼"""
with open(file_path, 'rb') as f:
raw_data = f.read()
result = chardet.detect(raw_data)
return result['encoding'] or 'utf-8'
def detect_csv_type(self, file_path):
"""檢測 CSV 文件類型"""
try:
encoding = self.detect_csv_encoding(file_path)
with open(file_path, 'r', encoding=encoding) as f:
reader = csv.reader(f)
first_row = next(reader) # 讀取第一行(標題行)
header_row = next(reader) # 讀取第二行(欄位名稱行)
# 通過檢查標題行和欄位名稱來判斷類型
first_row_text = ' '.join(first_row).lower()
# 檢查是否為插件CSV
if "plugins used by" in first_row_text:
return CSVType.PLUGIN
# 檢查是否為Mod CSV
if "chara zipmods" in first_row_text:
# 驗證Mod CSV的欄位名稱
expected_mod_fields = {"guid", "cards with usages", "is installed", "zipmod filename"}
header_fields = {field.lower().strip() for field in header_row}
if expected_mod_fields.issubset(header_fields):
return CSVType.MOD
return None
except Exception as e:
print(f"Error detecting CSV type: {str(e)}")
return None
def load_csv(self, file_path):
"""載入並解析 CSV 文件"""
try:
self.csv_type = self.detect_csv_type(file_path)
if not self.csv_type:
raise ValueError("Invalid CSV format")
encoding = self.detect_csv_encoding(file_path)
self.csv_file = []
with open(file_path, 'r', encoding=encoding) as f:
lines = f.readlines()
if self.csv_type == CSVType.PLUGIN:
print("Loading plugin CSV")
for line in lines[2:]: # 跳過標題行和欄位名稱行
if line.strip():
parts = line.strip().split(',', 1)
if len(parts) >= 2:
plugin_name = parts[0].strip()
usage_str = parts[1].strip()
# 從插件名稱中提取 GUID
guid = plugin_name.replace(".dll", "").strip()
# 從使用次數字串中提取第一段連續數字
# (不可串接所有數字:"3 (2 disabled)" 會變成 32)
match = re.search(r'\d+', usage_str)
usage = int(match.group()) if match else 0
if guid: # 只有在有效的 GUID 時才添加
self.csv_file.append({
'GUID': guid,
'Cards with usages': usage,
'Is installed': 'yes', # 假設列在 CSV 中的插件都是已安裝的
'Filename': f"{guid}.dll"
})
print(f"Loaded plugin: GUID={guid}, usage={usage}")
else:
# Mod CSV 解析邏輯
csv_reader = csv.reader(lines)
next(csv_reader) # 跳過標題行
header_row = next(csv_reader) # 讀取欄位名稱行
header_map = {col.lower().strip(): idx for idx, col in enumerate(header_row)}
for row in csv_reader:
if len(row) >= len(header_map): # 確保行有足夠的列
try:
cleaned_row = {
'GUID': row[header_map['guid']].strip(),
'Cards with usages': row[header_map['cards with usages']].strip(),
'Is installed': row[header_map['is installed']].strip(),