sticker-convert 2.13.3.0__py3-none-any.whl → 2.17.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. sticker_convert/__main__.py +24 -27
  2. sticker_convert/auth/__init__.py +0 -0
  3. sticker_convert/auth/auth_base.py +19 -0
  4. sticker_convert/{utils/auth/get_discord_auth.py → auth/auth_discord.py} +149 -118
  5. sticker_convert/{utils/auth/get_kakao_auth.py → auth/auth_kakao_android_login.py} +331 -330
  6. sticker_convert/auth/auth_kakao_desktop_login.py +327 -0
  7. sticker_convert/{utils/auth/get_kakao_desktop_auth.py → auth/auth_kakao_desktop_memdump.py} +281 -263
  8. sticker_convert/{utils/auth/get_line_auth.py → auth/auth_line.py} +98 -80
  9. sticker_convert/{utils/auth/get_signal_auth.py → auth/auth_signal.py} +139 -135
  10. sticker_convert/auth/auth_telethon.py +161 -0
  11. sticker_convert/{utils/auth/get_viber_auth.py → auth/auth_viber.py} +250 -235
  12. sticker_convert/{utils/auth → auth}/telegram_api.py +736 -675
  13. sticker_convert/cli.py +623 -608
  14. sticker_convert/converter.py +1093 -1084
  15. sticker_convert/definitions.py +4 -0
  16. sticker_convert/downloaders/download_band.py +111 -110
  17. sticker_convert/downloaders/download_base.py +171 -166
  18. sticker_convert/downloaders/download_discord.py +92 -91
  19. sticker_convert/downloaders/download_kakao.py +417 -404
  20. sticker_convert/downloaders/download_line.py +484 -475
  21. sticker_convert/downloaders/download_ogq.py +80 -79
  22. sticker_convert/downloaders/download_signal.py +108 -105
  23. sticker_convert/downloaders/download_telegram.py +56 -55
  24. sticker_convert/downloaders/download_viber.py +121 -120
  25. sticker_convert/gui.py +788 -873
  26. sticker_convert/gui_components/frames/comp_frame.py +180 -166
  27. sticker_convert/gui_components/frames/config_frame.py +156 -113
  28. sticker_convert/gui_components/frames/control_frame.py +32 -30
  29. sticker_convert/gui_components/frames/cred_frame.py +232 -233
  30. sticker_convert/gui_components/frames/input_frame.py +139 -137
  31. sticker_convert/gui_components/frames/output_frame.py +112 -110
  32. sticker_convert/gui_components/frames/right_clicker.py +25 -23
  33. sticker_convert/gui_components/windows/advanced_compression_window.py +757 -757
  34. sticker_convert/gui_components/windows/base_window.py +7 -2
  35. sticker_convert/gui_components/windows/discord_get_auth_window.py +79 -82
  36. sticker_convert/gui_components/windows/kakao_get_auth_window.py +511 -321
  37. sticker_convert/gui_components/windows/line_get_auth_window.py +94 -102
  38. sticker_convert/gui_components/windows/signal_get_auth_window.py +84 -89
  39. sticker_convert/gui_components/windows/viber_get_auth_window.py +168 -168
  40. sticker_convert/ios-message-stickers-template/.github/FUNDING.yml +3 -3
  41. sticker_convert/ios-message-stickers-template/README.md +10 -10
  42. sticker_convert/ios-message-stickers-template/stickers/Info.plist +43 -43
  43. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Info.plist +31 -31
  44. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Contents.json +6 -6
  45. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Contents.json +20 -20
  46. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Contents.json +9 -9
  47. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Contents.json +9 -9
  48. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Contents.json +9 -9
  49. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Contents.json +91 -91
  50. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.pbxproj +364 -364
  51. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -7
  52. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -8
  53. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/xcuserdata/niklaspeterson.xcuserdatad/xcschemes/xcschememanagement.plist +14 -14
  54. sticker_convert/job.py +166 -130
  55. sticker_convert/job_option.py +1 -0
  56. sticker_convert/locales/en_US/LC_MESSAGES/base.mo +0 -0
  57. sticker_convert/locales/ja_JP/LC_MESSAGES/base.mo +0 -0
  58. sticker_convert/locales/zh_CN/LC_MESSAGES/base.mo +0 -0
  59. sticker_convert/locales/zh_TW/LC_MESSAGES/base.mo +0 -0
  60. sticker_convert/py.typed +0 -0
  61. sticker_convert/resources/NotoColorEmoji.ttf +0 -0
  62. sticker_convert/resources/help.ja_JP.json +88 -0
  63. sticker_convert/resources/help.json +10 -7
  64. sticker_convert/resources/help.zh_CN.json +88 -0
  65. sticker_convert/resources/help.zh_TW.json +88 -0
  66. sticker_convert/resources/input.ja_JP.json +74 -0
  67. sticker_convert/resources/input.json +121 -121
  68. sticker_convert/resources/input.zh_CN.json +74 -0
  69. sticker_convert/resources/input.zh_TW.json +74 -0
  70. sticker_convert/resources/output.ja_JP.json +38 -0
  71. sticker_convert/resources/output.zh_CN.json +38 -0
  72. sticker_convert/resources/output.zh_TW.json +38 -0
  73. sticker_convert/uploaders/compress_wastickers.py +186 -177
  74. sticker_convert/uploaders/upload_base.py +44 -35
  75. sticker_convert/uploaders/upload_signal.py +218 -203
  76. sticker_convert/uploaders/upload_telegram.py +353 -338
  77. sticker_convert/uploaders/upload_viber.py +178 -169
  78. sticker_convert/uploaders/xcode_imessage.py +295 -286
  79. sticker_convert/utils/callback.py +238 -6
  80. sticker_convert/utils/emoji.py +16 -4
  81. sticker_convert/utils/files/json_resources_loader.py +24 -19
  82. sticker_convert/utils/files/metadata_handler.py +3 -3
  83. sticker_convert/utils/translate.py +108 -0
  84. sticker_convert/utils/url_detect.py +40 -37
  85. sticker_convert/version.py +1 -1
  86. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/METADATA +89 -74
  87. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/RECORD +91 -74
  88. sticker_convert/utils/auth/telethon_setup.py +0 -97
  89. sticker_convert/utils/singletons.py +0 -18
  90. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/WHEEL +0 -0
  91. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/entry_points.txt +0 -0
  92. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/licenses/LICENSE +0 -0
  93. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/top_level.txt +0 -0
sticker_convert/gui.py CHANGED
@@ -1,873 +1,788 @@
1
- #!/usr/bin/env python3
2
- import locale
3
- import os
4
- import platform
5
- import signal
6
- import sys
7
- from functools import partial
8
- from json.decoder import JSONDecodeError
9
- from math import ceil
10
- from multiprocessing import Event, cpu_count
11
- from pathlib import Path
12
- from threading import Thread
13
- from typing import Any, Callable, Dict, Optional, Union, cast
14
- from urllib.parse import urlparse
15
-
16
- from mergedeep import merge # type: ignore
17
- from PIL import ImageFont
18
- from ttkbootstrap import BooleanVar, DoubleVar, IntVar, StringVar, Toplevel, Window # type: ignore
19
- from ttkbootstrap.dialogs import Messagebox, Querybox # type: ignore
20
-
21
- from sticker_convert.definitions import CONFIG_DIR, DEFAULT_DIR, ROOT_DIR
22
- from sticker_convert.gui_components.frames.comp_frame import CompFrame
23
- from sticker_convert.gui_components.frames.config_frame import ConfigFrame
24
- from sticker_convert.gui_components.frames.control_frame import ControlFrame
25
- from sticker_convert.gui_components.frames.cred_frame import CredFrame
26
- from sticker_convert.gui_components.frames.input_frame import InputFrame
27
- from sticker_convert.gui_components.frames.output_frame import OutputFrame
28
- from sticker_convert.gui_components.frames.progress_frame import ProgressFrame
29
- from sticker_convert.gui_components.gui_utils import GUIUtils
30
- from sticker_convert.job import Job
31
- from sticker_convert.job_option import CompOption, CredOption, InputOption, OutputOption
32
- from sticker_convert.utils.files.json_manager import JsonManager
33
- from sticker_convert.utils.files.metadata_handler import MetadataHandler
34
- from sticker_convert.utils.url_detect import UrlDetect
35
- from sticker_convert.version import __version__
36
-
37
-
38
- class GUI(Window):
39
- def __init__(self) -> None:
40
- super().__init__(themename="darkly", alpha=0) # type: ignore
41
- self.init_done = False
42
- self.load_jsons()
43
-
44
- font_path = ROOT_DIR / "resources/NotoColorEmoji.ttf"
45
- self.emoji_font = ImageFont.truetype(font_path.as_posix(), 109)
46
-
47
- GUIUtils.set_icon(self)
48
-
49
- self.title(f"sticker-convert {__version__}")
50
- self.protocol("WM_DELETE_WINDOW", self.quit)
51
-
52
- (
53
- self.main_frame,
54
- self.horizontal_scrollbar_frame,
55
- self.canvas,
56
- self.x_scrollbar,
57
- self.y_scrollbar,
58
- self.scrollable_frame,
59
- ) = GUIUtils.create_scrollable_frame(self)
60
-
61
- self.declare_variables()
62
- self.apply_config()
63
- self.apply_creds()
64
- self.init_frames()
65
- self.pack_frames()
66
- self.author_info()
67
- self.warn_tkinter_bug()
68
- GUIUtils.finalize_window(self)
69
-
70
- self.bind("<<exec_in_main>>", self.exec_in_main) # type: ignore
71
-
72
- def __enter__(self) -> "GUI":
73
- return self
74
-
75
- def gui(self) -> None:
76
- self.init_done = True
77
- self.highlight_fields()
78
- self.mainloop()
79
-
80
- def quit(self) -> None:
81
- if self.job:
82
- response = self.cb_ask_bool("Job is running, really quit?")
83
- if response is False:
84
- return
85
-
86
- self.cb_msg(msg="Quitting, please wait...")
87
-
88
- self.save_config()
89
- if self.settings_save_cred_var.get() is True:
90
- self.save_creds()
91
- else:
92
- self.delete_creds()
93
-
94
- if self.job:
95
- self.cancel_job()
96
- self.destroy()
97
-
98
- def declare_variables(self) -> None:
99
- # Input
100
- self.input_option_display_var = StringVar(self)
101
- self.input_option_true_var = StringVar(self)
102
- self.input_setdir_var = StringVar(self)
103
- self.input_address_var = StringVar(self)
104
-
105
- # Compression
106
- self.no_compress_var = BooleanVar()
107
- self.comp_preset_var = StringVar(self)
108
- self.fps_min_var = IntVar(self)
109
- self.fps_max_var = IntVar(self)
110
- self.fps_disable_var = BooleanVar()
111
- self.fps_power_var = DoubleVar()
112
- self.res_w_min_var = IntVar(self)
113
- self.res_w_max_var = IntVar(self)
114
- self.res_w_disable_var = BooleanVar()
115
- self.res_h_min_var = IntVar(self)
116
- self.res_h_max_var = IntVar(self)
117
- self.res_h_disable_var = BooleanVar()
118
- self.res_power_var = DoubleVar()
119
- self.res_snap_pow2_var = BooleanVar()
120
- self.quality_min_var = IntVar(self)
121
- self.quality_max_var = IntVar(self)
122
- self.quality_disable_var = BooleanVar()
123
- self.quality_power_var = DoubleVar()
124
- self.color_min_var = IntVar(self)
125
- self.color_max_var = IntVar(self)
126
- self.color_disable_var = BooleanVar()
127
- self.color_power_var = DoubleVar()
128
- self.duration_min_var = IntVar(self)
129
- self.duration_max_var = IntVar(self)
130
- self.duration_disable_var = BooleanVar()
131
- self.padding_percent_var = IntVar(self)
132
- self.img_size_max_var = IntVar(self)
133
- self.vid_size_max_var = IntVar(self)
134
- self.size_disable_var = BooleanVar()
135
- self.bg_color_var = StringVar()
136
- self.img_format_var = StringVar(self)
137
- self.vid_format_var = StringVar(self)
138
- self.fake_vid_var = BooleanVar()
139
- self.scale_filter_var = StringVar(self)
140
- self.quantize_method_var = StringVar(self)
141
- self.chromium_path_var = StringVar(self)
142
- self.cache_dir_var = StringVar(self)
143
- self.default_emoji_var = StringVar(self)
144
- self.steps_var = IntVar(self)
145
- self.processes_var = IntVar(self)
146
-
147
- # Output
148
- self.output_option_display_var = StringVar(self)
149
- self.output_option_true_var = StringVar(self)
150
- self.output_setdir_var = StringVar(self)
151
- self.title_var = StringVar(self)
152
- self.author_var = StringVar(self)
153
-
154
- # Credentials
155
- self.signal_uuid_var = StringVar(self)
156
- self.signal_password_var = StringVar(self)
157
- self.telegram_token_var = StringVar(self)
158
- self.telegram_userid_var = StringVar(self)
159
- self.telethon_api_id_var = IntVar(self)
160
- self.telethon_api_hash_var = StringVar(self)
161
- self.kakao_auth_token_var = StringVar(self)
162
- self.kakao_username_var = StringVar(self)
163
- self.kakao_password_var = StringVar(self)
164
- self.kakao_country_code_var = StringVar(self)
165
- self.kakao_phone_number_var = StringVar(self)
166
- self.kakao_bin_path_var = StringVar(self)
167
- self.line_cookies_var = StringVar(self)
168
- self.viber_auth_var = StringVar(self)
169
- self.viber_bin_path_var = StringVar(self)
170
- self.discord_token_var = StringVar(self)
171
-
172
- # Config
173
- self.settings_save_cred_var = BooleanVar()
174
-
175
- # Other
176
- self.response_event = Event()
177
- self.response = None
178
- self.action: Optional[Callable[..., Any]] = None
179
- self.job: Optional[Job] = None
180
-
181
- def init_frames(self) -> None:
182
- self.input_frame = InputFrame(
183
- self, self.scrollable_frame, borderwidth=1, text="Input"
184
- )
185
- self.comp_frame = CompFrame(
186
- self, self.scrollable_frame, borderwidth=1, text="Compression options"
187
- )
188
- self.output_frame = OutputFrame(
189
- self, self.scrollable_frame, borderwidth=1, text="Output"
190
- )
191
- self.cred_frame = CredFrame(
192
- self, self.scrollable_frame, borderwidth=1, text="Credentials"
193
- )
194
- self.settings_frame = ConfigFrame(
195
- self, self.scrollable_frame, borderwidth=1, text="Config"
196
- )
197
- self.progress_frame = ProgressFrame(
198
- self, self.scrollable_frame, borderwidth=1, text="Progress"
199
- )
200
- self.control_frame = ControlFrame(self, self.scrollable_frame, borderwidth=1)
201
-
202
- def pack_frames(self) -> None:
203
- self.input_frame.grid(column=0, row=0, sticky="w", padx=5, pady=5)
204
- self.comp_frame.grid(column=1, row=0, sticky="news", padx=5, pady=5)
205
- self.output_frame.grid(column=0, row=1, sticky="w", padx=5, pady=5)
206
- self.cred_frame.grid(column=1, row=1, rowspan=2, sticky="w", padx=5, pady=5)
207
- self.settings_frame.grid(column=0, row=2, sticky="news", padx=5, pady=5)
208
- self.progress_frame.grid(
209
- column=0, row=3, columnspan=2, sticky="news", padx=5, pady=5
210
- )
211
- self.control_frame.grid(
212
- column=0, row=4, columnspan=2, sticky="news", padx=5, pady=5
213
- )
214
-
215
- def is_cn(self) -> bool:
216
- cn_locs = ("zh_chs", "zh_cn", "chinese")
217
- loc = locale.getlocale()[0]
218
- winloc = ""
219
- if platform.system() == "Windows":
220
- import ctypes
221
-
222
- windll = ctypes.windll.kernel32 # type: ignore
223
- winloc_id = cast(int, windll.GetUserDefaultUILanguage()) # type: ignore
224
- winloc = cast(str, locale.windows_locale[winloc_id]) # type: ignore
225
-
226
- if loc is not None and (
227
- loc.lower().startswith(cn_locs) or winloc.lower().startswith(cn_locs)
228
- ):
229
- return True
230
- return False
231
-
232
- def author_info(self) -> None:
233
- if self.is_cn():
234
- msg = "sticker-convert是laggykiller创作的免费开源项目,严禁翻售\n"
235
- msg += "https://github.com/laggykiller/sticker-convert\n"
236
- msg += "尊重知识产权,做个文明人,请不要转售贴图!\n"
237
- else:
238
- msg = "sticker-convert is Free and Opensource software by laggykiller\n"
239
- msg += "https://github.com/laggykiller/sticker-convert\n"
240
- msg += "Please use the stickers with your friends only.\n"
241
- msg += "It is illegal and immoral to sell stickers downloaded from this program.\n"
242
-
243
- self.cb_msg(msg)
244
-
245
- def warn_tkinter_bug(self) -> None:
246
- if (
247
- platform.system() == "Darwin"
248
- and platform.mac_ver()[0].split(".")[0] == "14"
249
- and sys.version_info[0] == 3
250
- and sys.version_info[1] == 11
251
- and sys.version_info[2] <= 6
252
- ):
253
- msg = "NOTICE: If buttons are not responsive, try to press "
254
- msg += "on title bar or move mouse cursor away from window for a while.\n"
255
- msg += "(This is a bug in tkinter specific to macOS 14 python <=3.11.6)\n"
256
- msg += "(https://github.com/python/cpython/issues/110218)\n"
257
- self.cb_msg(msg)
258
-
259
- def load_jsons(self) -> None:
260
- try:
261
- from sticker_convert.utils.files.json_resources_loader import COMPRESSION_JSON, EMOJI_JSON, HELP_JSON, INPUT_JSON, OUTPUT_JSON
262
- except RuntimeError as e:
263
- self.cb_msg(str(e))
264
- return
265
-
266
- self.help = HELP_JSON
267
- self.input_presets = INPUT_JSON
268
- self.compression_presets = COMPRESSION_JSON
269
- self.output_presets = OUTPUT_JSON
270
- self.emoji_list = EMOJI_JSON
271
-
272
- if not (
273
- self.compression_presets and self.input_presets and self.output_presets
274
- ):
275
- Messagebox.show_error( # type: ignore
276
- message='Warning: json(s) under "resources" directory cannot be found',
277
- title="sticker-convert",
278
- )
279
- sys.exit()
280
-
281
- self.settings_path = CONFIG_DIR / "config.json"
282
- if self.settings_path.is_file():
283
- try:
284
- self.settings: Dict[Any, Any] = JsonManager.load_json(
285
- self.settings_path
286
- )
287
- except JSONDecodeError:
288
- self.cb_msg("Warning: config.json content is corrupted")
289
- self.settings = {}
290
- else:
291
- self.settings = {}
292
-
293
- self.creds_path = CONFIG_DIR / "creds.json"
294
- if self.creds_path.is_file():
295
- try:
296
- self.creds = JsonManager.load_json(self.creds_path)
297
- except JSONDecodeError:
298
- self.cb_msg("Warning: creds.json content is corrupted")
299
- self.creds = {}
300
- else:
301
- self.creds = {}
302
-
303
- def save_config(self) -> None:
304
- # Only update comp_custom if custom preset is selected
305
- if self.comp_preset_var.get() == "custom":
306
- comp_custom: Dict[Any, Any] = merge( # type: ignore
307
- self.compression_presets.get("custom"), # type: ignore
308
- self.get_opt_comp().to_dict(),
309
- )
310
- comp_custom["format"]["img"] = comp_custom["format"]["img"][0]
311
- comp_custom["format"]["vid"] = comp_custom["format"]["vid"][0]
312
- del comp_custom["preset"]
313
- del comp_custom["no_compress"]
314
- else:
315
- compression_presets_custom = self.compression_presets.get("custom")
316
- if compression_presets_custom is None:
317
- comp_custom = {}
318
- else:
319
- comp_custom = compression_presets_custom
320
-
321
- self.settings = {
322
- "input": self.get_opt_input().to_dict(),
323
- "comp": {
324
- "no_compress": self.no_compress_var.get(),
325
- "preset": self.comp_preset_var.get(),
326
- "chromium_path": self.chromium_path_var.get(),
327
- "cache_dir": self.cache_dir_var.get(),
328
- "processes": self.processes_var.get(),
329
- },
330
- "comp_custom": comp_custom,
331
- "output": self.get_opt_output().to_dict(),
332
- "creds": {"save_cred": self.settings_save_cred_var.get()},
333
- }
334
-
335
- JsonManager.save_json(self.settings_path, self.settings)
336
-
337
- def save_creds(self) -> None:
338
- self.creds = self.get_opt_cred().to_dict()
339
-
340
- JsonManager.save_json(self.creds_path, self.creds)
341
-
342
- def delete_creds(self) -> None:
343
- if self.creds_path.is_file():
344
- os.remove(self.creds_path)
345
-
346
- def delete_config(self) -> None:
347
- if self.settings_path.is_file():
348
- os.remove(self.settings_path)
349
-
350
- def apply_config(self) -> None:
351
- # Input
352
- self.default_input_mode: str = self.settings.get("input", {}).get(
353
- "option", "auto"
354
- )
355
- self.input_address_var.set(self.settings.get("input", {}).get("url", ""))
356
- default_stickers_input_dir = str(DEFAULT_DIR / "stickers_input")
357
- self.input_setdir_var.set(
358
- self.settings.get("input", {}).get("dir", default_stickers_input_dir)
359
- )
360
- if not Path(self.input_setdir_var.get()).is_dir():
361
- self.input_setdir_var.set(default_stickers_input_dir)
362
- self.input_option_display_var.set(
363
- self.input_presets[self.default_input_mode]["full_name"]
364
- )
365
- self.input_option_true_var.set(
366
- self.input_presets[self.default_input_mode]["full_name"]
367
- )
368
-
369
- # Compression
370
- self.no_compress_var.set(
371
- self.settings.get("comp", {}).get("no_compress", False)
372
- )
373
- default_comp_preset = list(self.compression_presets.keys())[0]
374
- self.comp_preset_var.set(
375
- self.settings.get("comp", {}).get("preset", default_comp_preset)
376
- )
377
- comp_custom = self.settings.get("comp_custom")
378
- if comp_custom:
379
- self.compression_presets["custom"] = merge(
380
- self.compression_presets["custom"], comp_custom
381
- )
382
- self.cache_dir_var.set(self.settings.get("comp", {}).get("cache_dir", ""))
383
- self.chromium_path_var.set(
384
- self.settings.get("comp", {}).get("chromium_path", "")
385
- )
386
- self.processes_var.set(
387
- self.settings.get("comp", {}).get("processes", ceil(cpu_count() / 2))
388
- )
389
- self.default_output_mode: str = self.settings.get("output", {}).get(
390
- "option", "signal"
391
- )
392
-
393
- # Output
394
- default_stickers_output_dir = str(DEFAULT_DIR / "stickers_output")
395
- self.output_setdir_var.set(
396
- self.settings.get("output", {}).get("dir", default_stickers_output_dir)
397
- )
398
- if not Path(self.output_setdir_var.get()).is_dir():
399
- self.output_setdir_var.set(default_stickers_output_dir)
400
- self.title_var.set(self.settings.get("output", {}).get("title", ""))
401
- self.author_var.set(self.settings.get("output", {}).get("author", ""))
402
- self.settings_save_cred_var.set(
403
- self.settings.get("creds", {}).get("save_cred", True)
404
- )
405
- self.output_option_display_var.set(
406
- self.output_presets[self.default_output_mode]["full_name"]
407
- )
408
- self.output_option_true_var.set(
409
- self.output_presets[self.default_output_mode]["full_name"]
410
- )
411
-
412
- def apply_creds(self) -> None:
413
- self.signal_uuid_var.set(self.creds.get("signal", {}).get("uuid", ""))
414
- self.signal_password_var.set(self.creds.get("signal", {}).get("password", ""))
415
- self.telegram_token_var.set(self.creds.get("telegram", {}).get("token", ""))
416
- self.telegram_userid_var.set(self.creds.get("telegram", {}).get("userid", ""))
417
- self.telethon_api_id_var.set(self.creds.get("telethon", {}).get("api_id", 0))
418
- self.telethon_api_hash_var.set(
419
- self.creds.get("telethon", {}).get("api_hash", "")
420
- )
421
- self.kakao_auth_token_var.set(self.creds.get("kakao", {}).get("auth_token", ""))
422
- self.kakao_username_var.set(self.creds.get("kakao", {}).get("username", ""))
423
- self.kakao_password_var.set(self.creds.get("kakao", {}).get("password", ""))
424
- self.kakao_country_code_var.set(
425
- self.creds.get("kakao", {}).get("country_code", "")
426
- )
427
- self.kakao_phone_number_var.set(
428
- self.creds.get("kakao", {}).get("phone_number", "")
429
- )
430
- self.line_cookies_var.set(self.creds.get("line", {}).get("cookies", ""))
431
- self.viber_auth_var.set(self.creds.get("viber", {}).get("auth", ""))
432
- self.discord_token_var.set(self.creds.get("discord", {}).get("token", ""))
433
-
434
- def get_input_name(self) -> str:
435
- return [
436
- k
437
- for k, v in self.input_presets.items()
438
- if v["full_name"] == self.input_option_true_var.get()
439
- ][0]
440
-
441
- def get_input_display_name(self) -> str:
442
- return [
443
- k
444
- for k, v in self.input_presets.items()
445
- if v["full_name"] == self.input_option_display_var.get()
446
- ][0]
447
-
448
- def get_output_name(self) -> str:
449
- return [
450
- k
451
- for k, v in self.output_presets.items()
452
- if v["full_name"] == self.output_option_true_var.get()
453
- ][0]
454
-
455
- # def get_output_display_name(self) -> str:
456
- # return [
457
- # k
458
- # for k, v in self.output_presets.items()
459
- # if v["full_name"] == self.output_option_display_var.get()
460
- # ][0]
461
-
462
- def get_preset(self) -> str:
463
- selection = self.comp_preset_var.get()
464
- if selection == "auto":
465
- output_option = self.get_output_name()
466
- if "telegram_emoji" in output_option:
467
- return "telegram_emoji"
468
- if "telegram" in output_option:
469
- return "telegram"
470
- if output_option == "imessage":
471
- return "imessage_small"
472
- if output_option == "local":
473
- return selection
474
- return output_option
475
-
476
- return selection
477
-
478
- def start_job(self) -> None:
479
- self.save_config()
480
- if self.settings_save_cred_var.get() is True:
481
- self.save_creds()
482
- else:
483
- self.delete_creds()
484
-
485
- self.control_frame.start_btn.config(text="Cancel", bootstyle="danger") # type: ignore
486
- self.set_inputs("disabled")
487
-
488
- opt_input = self.get_opt_input()
489
- opt_output = self.get_opt_output()
490
- opt_comp = self.get_opt_comp()
491
- opt_cred = self.get_opt_cred()
492
-
493
- self.job = Job(
494
- opt_input,
495
- opt_comp,
496
- opt_output,
497
- opt_cred,
498
- self.cb_msg,
499
- self.cb_msg_block,
500
- self.cb_bar,
501
- self.cb_ask_bool,
502
- self.cb_ask_str,
503
- )
504
-
505
- signal.signal(signal.SIGINT, self.job.cancel)
506
-
507
- Thread(target=self.start_process, daemon=True).start()
508
-
509
- def get_opt_input(self) -> InputOption:
510
- return InputOption(
511
- option=self.get_input_name(),
512
- url=self.input_address_var.get(),
513
- dir=Path(self.input_setdir_var.get()),
514
- )
515
-
516
- def get_opt_output(self) -> OutputOption:
517
- return OutputOption(
518
- option=self.get_output_name(),
519
- dir=Path(self.output_setdir_var.get()),
520
- title=self.title_var.get(),
521
- author=self.author_var.get(),
522
- )
523
-
524
- def get_opt_comp(self) -> CompOption:
525
- return CompOption(
526
- preset=self.get_preset(),
527
- size_max_img=self.img_size_max_var.get()
528
- if not self.size_disable_var.get()
529
- else None,
530
- size_max_vid=self.vid_size_max_var.get()
531
- if not self.size_disable_var.get()
532
- else None,
533
- format_img=(self.img_format_var.get(),),
534
- format_vid=(self.vid_format_var.get(),),
535
- fps_min=self.fps_min_var.get() if not self.fps_disable_var.get() else None,
536
- fps_max=self.fps_max_var.get() if not self.fps_disable_var.get() else None,
537
- fps_power=self.fps_power_var.get(),
538
- res_w_min=self.res_w_min_var.get()
539
- if not self.res_w_disable_var.get()
540
- else None,
541
- res_w_max=self.res_w_max_var.get()
542
- if not self.res_w_disable_var.get()
543
- else None,
544
- res_h_min=self.res_h_min_var.get()
545
- if not self.res_h_disable_var.get()
546
- else None,
547
- res_h_max=self.res_h_max_var.get()
548
- if not self.res_h_disable_var.get()
549
- else None,
550
- res_power=self.res_power_var.get(),
551
- res_snap_pow2=self.res_snap_pow2_var.get(),
552
- quality_min=self.quality_min_var.get()
553
- if not self.quality_disable_var.get()
554
- else None,
555
- quality_max=self.quality_max_var.get()
556
- if not self.quality_disable_var.get()
557
- else None,
558
- quality_power=self.quality_power_var.get(),
559
- color_min=self.color_min_var.get()
560
- if not self.color_disable_var.get()
561
- else None,
562
- color_max=self.color_max_var.get()
563
- if not self.color_disable_var.get()
564
- else None,
565
- color_power=self.color_power_var.get(),
566
- duration_min=self.duration_min_var.get()
567
- if not self.duration_disable_var.get()
568
- else None,
569
- duration_max=self.duration_max_var.get()
570
- if not self.duration_disable_var.get()
571
- else None,
572
- bg_color=self.bg_color_var.get(),
573
- padding_percent=self.padding_percent_var.get(),
574
- steps=self.steps_var.get(),
575
- fake_vid=self.fake_vid_var.get(),
576
- scale_filter=self.scale_filter_var.get(),
577
- quantize_method=self.quantize_method_var.get(),
578
- chromium_path=self.chromium_path_var.get()
579
- if self.chromium_path_var.get() != ""
580
- else None,
581
- cache_dir=self.cache_dir_var.get()
582
- if self.cache_dir_var.get() != ""
583
- else None,
584
- default_emoji=self.default_emoji_var.get(),
585
- no_compress=self.no_compress_var.get(),
586
- processes=self.processes_var.get(),
587
- )
588
-
589
- def get_opt_cred(self) -> CredOption:
590
- return CredOption(
591
- signal_uuid=self.signal_uuid_var.get(),
592
- signal_password=self.signal_password_var.get(),
593
- telegram_token=self.telegram_token_var.get(),
594
- telegram_userid=self.telegram_userid_var.get(),
595
- telethon_api_id=self.telethon_api_id_var.get(),
596
- telethon_api_hash=self.telethon_api_hash_var.get(),
597
- kakao_auth_token=self.kakao_auth_token_var.get(),
598
- kakao_username=self.kakao_username_var.get(),
599
- kakao_password=self.kakao_password_var.get(),
600
- kakao_country_code=self.kakao_country_code_var.get(),
601
- kakao_phone_number=self.kakao_phone_number_var.get(),
602
- line_cookies=self.line_cookies_var.get(),
603
- viber_auth=self.viber_auth_var.get(),
604
- discord_token=self.discord_token_var.get(),
605
- )
606
-
607
- def start_process(self) -> None:
608
- if self.job:
609
- self.job.start()
610
- self.job = None
611
-
612
- self.stop_job()
613
-
614
- def stop_job(self) -> None:
615
- self.set_inputs("normal")
616
- self.control_frame.start_btn.config(text="Start", bootstyle="default") # type: ignore
617
-
618
- def cancel_job(self) -> None:
619
- if self.job:
620
- # Need to start new thread or else GUI may freeze
621
- Thread(target=self.job.cancel, daemon=True).start()
622
-
623
- def set_inputs(self, state: str) -> None:
624
- # state: 'normal', 'disabled'
625
-
626
- self.input_frame.set_states(state=state)
627
- self.comp_frame.set_states(state=state)
628
- self.output_frame.set_states(state=state)
629
- self.cred_frame.set_states(state=state)
630
- self.settings_frame.set_states(state=state)
631
-
632
- if state == "normal":
633
- self.input_frame.cb_input_option()
634
- self.comp_frame.cb_no_compress()
635
-
636
- def exec_in_main(self, _evt: Any) -> Any:
637
- if self.action:
638
- self.response = self.action()
639
- self.response_event.set()
640
-
641
- def cb_ask_str(
642
- self,
643
- question: str,
644
- initialvalue: Optional[str] = None,
645
- cli_show_initialvalue: bool = True,
646
- parent: Optional[object] = None,
647
- ) -> str:
648
- self.action = partial(
649
- Querybox.get_string, # type: ignore
650
- question,
651
- title="sticker-convert",
652
- initialvalue=initialvalue,
653
- parent=parent,
654
- )
655
- self.event_generate("<<exec_in_main>>")
656
- self.response_event.wait()
657
- self.response_event.clear()
658
-
659
- if self.response is None:
660
- return ""
661
- elif isinstance(self.response, str):
662
- return self.response
663
- else:
664
- raise RuntimeError(f"Invalid response in cb_ask_str: {self.response}")
665
-
666
- def cb_ask_bool(
667
- self, question: str, parent: Union[Window, Toplevel, None] = None
668
- ) -> bool:
669
- self.action = partial(
670
- Messagebox.yesno, # type: ignore
671
- question,
672
- title="sticker-convert",
673
- parent=parent,
674
- )
675
- self.event_generate("<<exec_in_main>>")
676
- self.response_event.wait()
677
- self.response_event.clear()
678
-
679
- if self.response == "Yes":
680
- return True
681
- return False
682
-
683
- def cb_msg(self, *args: Any, **kwargs: Any) -> None:
684
- self.progress_frame.update_message_box(*args, **kwargs)
685
-
686
- def cb_msg_block(
687
- self,
688
- *args: Any,
689
- message: Optional[str] = None,
690
- parent: Optional[object] = None,
691
- **_kwargs: Any,
692
- ) -> Any:
693
- if message is None and len(args) > 0:
694
- message = " ".join(str(i) for i in args)
695
- self.action = partial(
696
- Messagebox.show_info, # type: ignore
697
- message,
698
- title="sticker-convert",
699
- parent=parent,
700
- )
701
- self.event_generate("<<exec_in_main>>")
702
- self.response_event.wait()
703
- self.response_event.clear()
704
-
705
- return self.response
706
-
707
- def cb_bar(
708
- self,
709
- set_progress_mode: Optional[str] = None,
710
- steps: int = 0,
711
- update_bar: int = 0,
712
- *args: Any,
713
- **kwargs: Any,
714
- ) -> None:
715
- self.progress_frame.update_progress_bar(
716
- set_progress_mode, steps, update_bar, *args, **kwargs
717
- )
718
-
719
- def highlight_fields(self) -> bool:
720
- if not self.init_done:
721
- return True
722
-
723
- input_option = self.get_input_name()
724
- input_option_display = self.get_input_display_name()
725
- output_option = self.get_output_name()
726
- # output_option_display = self.get_output_display_name()
727
- url = self.input_address_var.get()
728
-
729
- in_out_dir_same = (
730
- Path(self.input_setdir_var.get()).absolute()
731
- == Path(self.output_setdir_var.get()).absolute()
732
- )
733
-
734
- # Input
735
- if in_out_dir_same is True:
736
- self.input_frame.input_setdir_entry.config(bootstyle="danger") # type: ignore
737
- elif not Path(self.input_setdir_var.get()).is_dir():
738
- self.input_frame.input_setdir_entry.config(bootstyle="warning") # type: ignore
739
- else:
740
- self.input_frame.input_setdir_entry.config(bootstyle="default") # type: ignore
741
-
742
- self.input_frame.address_lbl.config(
743
- text=self.input_presets[input_option_display]["address_lbls"]
744
- )
745
- self.input_frame.address_entry.config(bootstyle="default") # type: ignore
746
-
747
- if input_option == "local":
748
- self.input_frame.address_entry.config(state="disabled")
749
- self.input_frame.address_tip.config(
750
- text=self.input_presets[input_option_display]["example"]
751
- )
752
-
753
- else:
754
- self.input_frame.address_entry.config(state="normal")
755
- self.input_frame.address_tip.config(
756
- text=self.input_presets[input_option_display]["example"]
757
- )
758
- download_option = UrlDetect.detect(url)
759
-
760
- if not url:
761
- self.input_frame.address_entry.config(bootstyle="warning") # type: ignore
762
-
763
- elif (
764
- download_option is None
765
- or input_option.startswith(download_option) is False
766
- and not (
767
- input_option
768
- in ("kakao", "band", "line", "discord", "discord_emoji")
769
- and url.isnumeric()
770
- )
771
- ):
772
- self.input_frame.address_entry.config(bootstyle="danger") # type: ignore
773
- self.input_frame.address_tip.config(
774
- text=f"Invalid URL. {self.input_presets[input_option_display]['example']}"
775
- )
776
-
777
- elif input_option_display == "auto" and download_option:
778
- self.input_frame.address_tip.config(
779
- text=f"Detected URL: {download_option}"
780
- )
781
-
782
- # Output
783
- if in_out_dir_same is True:
784
- self.output_frame.output_setdir_entry.config(bootstyle="danger") # type: ignore
785
- elif not Path(self.output_setdir_var.get()).is_dir():
786
- self.output_frame.output_setdir_entry.config(bootstyle="warning") # type: ignore
787
- else:
788
- self.output_frame.output_setdir_entry.config(bootstyle="default") # type: ignore
789
-
790
- if (
791
- MetadataHandler.check_metadata_required(output_option, "title")
792
- and not MetadataHandler.check_metadata_provided(
793
- Path(self.input_setdir_var.get()), input_option, "title"
794
- )
795
- and not self.title_var.get()
796
- ):
797
- self.output_frame.title_entry.config(bootstyle="warning") # type: ignore
798
- else:
799
- self.output_frame.title_entry.config(bootstyle="default") # type: ignore
800
-
801
- if (
802
- MetadataHandler.check_metadata_required(output_option, "author")
803
- and not MetadataHandler.check_metadata_provided(
804
- Path(self.input_setdir_var.get()), input_option, "author"
805
- )
806
- and not self.author_var.get()
807
- ):
808
- self.output_frame.author_entry.config(bootstyle="warning") # type: ignore
809
- else:
810
- self.output_frame.author_entry.config(bootstyle="default") # type: ignore
811
-
812
- if self.comp_preset_var.get() == "auto":
813
- if output_option == "local":
814
- self.no_compress_var.set(True)
815
- else:
816
- self.no_compress_var.set(False)
817
- self.comp_frame.cb_no_compress()
818
-
819
- # Credentials
820
- if output_option == "signal" and not self.signal_uuid_var.get():
821
- self.cred_frame.signal_uuid_entry.config(bootstyle="warning") # type: ignore
822
- else:
823
- self.cred_frame.signal_uuid_entry.config(bootstyle="default") # type: ignore
824
-
825
- if output_option == "signal" and not self.signal_password_var.get():
826
- self.cred_frame.signal_password_entry.config(bootstyle="warning") # type: ignore
827
- else:
828
- self.cred_frame.signal_password_entry.config(bootstyle="default") # type: ignore
829
-
830
- if (
831
- input_option == "telegram" or output_option == "telegram"
832
- ) and not self.telegram_token_var.get():
833
- self.cred_frame.telegram_token_entry.config(bootstyle="warning") # type: ignore
834
- else:
835
- self.cred_frame.telegram_token_entry.config(bootstyle="default") # type: ignore
836
-
837
- if output_option == "telegram" and not self.telegram_userid_var.get():
838
- self.cred_frame.telegram_userid_entry.config(bootstyle="warning") # type: ignore
839
- else:
840
- self.cred_frame.telegram_userid_entry.config(bootstyle="default") # type: ignore
841
-
842
- if output_option == "viber" and not self.viber_auth_var.get():
843
- self.cred_frame.viber_auth_entry.config(bootstyle="warning") # type: ignore
844
- else:
845
- self.cred_frame.viber_auth_entry.config(bootstyle="default") # type: ignore
846
-
847
- if (
848
- urlparse(url).netloc == "e.kakao.com"
849
- and not self.kakao_auth_token_var.get()
850
- ):
851
- self.cred_frame.kakao_auth_token_entry.config(bootstyle="warning") # type: ignore
852
- else:
853
- self.cred_frame.kakao_auth_token_entry.config(bootstyle="default") # type: ignore
854
-
855
- if input_option.startswith("discord") and not self.discord_token_var.get():
856
- self.cred_frame.discord_token_entry.config(bootstyle="warning") # type: ignore
857
- else:
858
- self.cred_frame.discord_token_entry.config(bootstyle="default") # type: ignore
859
-
860
- # Check for Input and Compression mismatch
861
- if (
862
- not self.no_compress_var.get()
863
- and self.get_output_name() != "local"
864
- and self.comp_preset_var.get() not in ("auto", "custom")
865
- and self.get_output_name() not in self.comp_preset_var.get()
866
- ):
867
- self.comp_frame.comp_preset_opt.config(bootstyle="warning") # type: ignore
868
- self.output_frame.output_option_opt.config(bootstyle="warning") # type: ignore
869
- else:
870
- self.comp_frame.comp_preset_opt.config(bootstyle="secondary") # type: ignore
871
- self.output_frame.output_option_opt.config(bootstyle="secondary") # type: ignore
872
-
873
- return True
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import platform
4
+ import signal
5
+ import sys
6
+ from json.decoder import JSONDecodeError
7
+ from math import ceil
8
+ from multiprocessing import Event, cpu_count
9
+ from pathlib import Path
10
+ from threading import Thread
11
+ from typing import Any, Callable, Dict, Optional
12
+ from urllib.parse import urlparse
13
+
14
+ from mergedeep import merge # type: ignore
15
+ from PIL import ImageFont
16
+ from ttkbootstrap import BooleanVar, DoubleVar, IntVar, StringVar, Window # type: ignore
17
+
18
+ from sticker_convert.definitions import CONFIG_DIR, DEFAULT_DIR, ROOT_DIR, RUNTIME_STATE
19
+ from sticker_convert.gui_components.frames.comp_frame import CompFrame
20
+ from sticker_convert.gui_components.frames.config_frame import ConfigFrame
21
+ from sticker_convert.gui_components.frames.control_frame import ControlFrame
22
+ from sticker_convert.gui_components.frames.cred_frame import CredFrame
23
+ from sticker_convert.gui_components.frames.input_frame import InputFrame
24
+ from sticker_convert.gui_components.frames.output_frame import OutputFrame
25
+ from sticker_convert.gui_components.frames.progress_frame import ProgressFrame
26
+ from sticker_convert.gui_components.gui_utils import GUIUtils
27
+ from sticker_convert.job import Job
28
+ from sticker_convert.job_option import CompOption, CredOption, InputOption, OutputOption
29
+ from sticker_convert.utils.callback import CallbackGui
30
+ from sticker_convert.utils.files.json_manager import JsonManager
31
+ from sticker_convert.utils.files.json_resources_loader import load_resource_json
32
+ from sticker_convert.utils.files.metadata_handler import MetadataHandler
33
+ from sticker_convert.utils.translate import SUPPORTED_LANG, I
34
+ from sticker_convert.utils.url_detect import UrlDetect
35
+ from sticker_convert.version import __version__
36
+
37
+
38
+ class GUI(Window):
39
+ def __init__(self) -> None:
40
+ self.MSG_MORAL = I(
41
+ "sticker-convert is Free and Opensource software by laggykiller\n"
42
+ "{project_url}\n"
43
+ "Please use the stickers with your friends only.\n"
44
+ "It is illegal and immoral to sell stickers downloaded from this program."
45
+ )
46
+ self.PROJECT_URL = "https://github.com/laggykiller/sticker-convert"
47
+
48
+ self.MSG_MAC14_TK_BUG = I(
49
+ "NOTICE: If buttons are not responsive, try to press\n"
50
+ "on title bar or move mouse cursor away from window for a while.\n"
51
+ "(This is a bug in tkinter specific to macOS 14 python <=3.11.6)\n"
52
+ "({mac14_tk_bug_url})"
53
+ )
54
+ self.MAC14_TK_BUG_URL = "https://github.com/python/cpython/issues/110218"
55
+
56
+ super().__init__(themename="darkly", alpha=0) # type: ignore
57
+ self.cb = CallbackGui(self)
58
+ self.init_done = False
59
+ self.load_jsons()
60
+
61
+ font_path = ROOT_DIR / "resources/NotoColorEmoji.ttf"
62
+ self.emoji_font = ImageFont.truetype(font_path.as_posix(), 109)
63
+
64
+ GUIUtils.set_icon(self)
65
+
66
+ self.title(f"sticker-convert {__version__}")
67
+ self.protocol("WM_DELETE_WINDOW", self.quit)
68
+
69
+ (
70
+ self.main_frame,
71
+ self.horizontal_scrollbar_frame,
72
+ self.canvas,
73
+ self.x_scrollbar,
74
+ self.y_scrollbar,
75
+ self.scrollable_frame,
76
+ ) = GUIUtils.create_scrollable_frame(self)
77
+
78
+ self.declare_variables()
79
+ self.apply_config()
80
+ self.apply_creds()
81
+ self.init_frames()
82
+ self.pack_frames()
83
+ self.author_info()
84
+ self.warn_tkinter_bug()
85
+ GUIUtils.finalize_window(self)
86
+
87
+ self.bind("<<exec_in_main>>", self.exec_in_main) # type: ignore
88
+
89
+ def __enter__(self) -> "GUI":
90
+ return self
91
+
92
+ def gui(self) -> None:
93
+ self.init_done = True
94
+ self.highlight_fields()
95
+ self.mainloop()
96
+
97
+ def quit(self) -> None:
98
+ if self.job:
99
+ response = self.cb.put(
100
+ ("ask_bool", (I("Job is running, really quit?"),), None)
101
+ )
102
+ if response is False:
103
+ return
104
+
105
+ self.cb.put(I("Quitting, please wait..."))
106
+
107
+ self.save_config()
108
+ if self.settings_save_cred_var.get() is True:
109
+ self.save_creds()
110
+ else:
111
+ self.delete_creds()
112
+
113
+ if self.job:
114
+ self.cancel_job()
115
+ self.destroy()
116
+
117
+ def reset(self) -> None:
118
+ self.init_done = False
119
+
120
+ # These json have translated version
121
+ RUNTIME_STATE["help_json"] = None
122
+ RUNTIME_STATE["input_json"] = None
123
+ RUNTIME_STATE["output_json"] = None
124
+ self.load_jsons()
125
+ self.apply_config()
126
+
127
+ self.input_frame.destroy()
128
+ self.comp_frame.destroy()
129
+ self.output_frame.destroy()
130
+ self.cred_frame.destroy()
131
+ self.settings_frame.destroy()
132
+ self.progress_frame.destroy()
133
+ self.control_frame.destroy()
134
+
135
+ self.init_frames()
136
+ self.pack_frames()
137
+ self.init_done = True
138
+ self.highlight_fields()
139
+
140
+ def declare_variables(self) -> None:
141
+ # Input
142
+ self.input_option_display_var = StringVar(self)
143
+ self.input_option_true_var = StringVar(self)
144
+ self.input_setdir_var = StringVar(self)
145
+ self.input_address_var = StringVar(self)
146
+
147
+ # Compression
148
+ self.no_compress_var = BooleanVar()
149
+ self.comp_preset_var = StringVar(self)
150
+ self.fps_min_var = IntVar(self)
151
+ self.fps_max_var = IntVar(self)
152
+ self.fps_disable_var = BooleanVar()
153
+ self.fps_power_var = DoubleVar()
154
+ self.res_w_min_var = IntVar(self)
155
+ self.res_w_max_var = IntVar(self)
156
+ self.res_w_disable_var = BooleanVar()
157
+ self.res_h_min_var = IntVar(self)
158
+ self.res_h_max_var = IntVar(self)
159
+ self.res_h_disable_var = BooleanVar()
160
+ self.res_power_var = DoubleVar()
161
+ self.res_snap_pow2_var = BooleanVar()
162
+ self.quality_min_var = IntVar(self)
163
+ self.quality_max_var = IntVar(self)
164
+ self.quality_disable_var = BooleanVar()
165
+ self.quality_power_var = DoubleVar()
166
+ self.color_min_var = IntVar(self)
167
+ self.color_max_var = IntVar(self)
168
+ self.color_disable_var = BooleanVar()
169
+ self.color_power_var = DoubleVar()
170
+ self.duration_min_var = IntVar(self)
171
+ self.duration_max_var = IntVar(self)
172
+ self.duration_disable_var = BooleanVar()
173
+ self.padding_percent_var = IntVar(self)
174
+ self.img_size_max_var = IntVar(self)
175
+ self.vid_size_max_var = IntVar(self)
176
+ self.size_disable_var = BooleanVar()
177
+ self.bg_color_var = StringVar()
178
+ self.img_format_var = StringVar(self)
179
+ self.vid_format_var = StringVar(self)
180
+ self.fake_vid_var = BooleanVar()
181
+ self.scale_filter_var = StringVar(self)
182
+ self.quantize_method_var = StringVar(self)
183
+ self.chromium_path_var = StringVar(self)
184
+ self.cache_dir_var = StringVar(self)
185
+ self.default_emoji_var = StringVar(self)
186
+ self.steps_var = IntVar(self)
187
+ self.processes_var = IntVar(self)
188
+
189
+ # Output
190
+ self.output_option_display_var = StringVar(self)
191
+ self.output_option_true_var = StringVar(self)
192
+ self.output_setdir_var = StringVar(self)
193
+ self.title_var = StringVar(self)
194
+ self.author_var = StringVar(self)
195
+
196
+ # Credentials
197
+ self.signal_uuid_var = StringVar(self)
198
+ self.signal_password_var = StringVar(self)
199
+ self.telegram_token_var = StringVar(self)
200
+ self.telegram_userid_var = StringVar(self)
201
+ self.telethon_api_id_var = IntVar(self)
202
+ self.telethon_api_hash_var = StringVar(self)
203
+ self.kakao_auth_token_var = StringVar(self)
204
+ self.kakao_username_var = StringVar(self)
205
+ self.kakao_password_var = StringVar(self)
206
+ self.kakao_country_code_var = StringVar(self)
207
+ self.kakao_phone_number_var = StringVar(self)
208
+ self.kakao_device_uuid_var = StringVar(self)
209
+ self.kakao_bin_path_var = StringVar(self)
210
+ self.line_cookies_var = StringVar(self)
211
+ self.viber_auth_var = StringVar(self)
212
+ self.viber_bin_path_var = StringVar(self)
213
+ self.discord_token_var = StringVar(self)
214
+
215
+ # Config
216
+ self.settings_save_cred_var = BooleanVar()
217
+
218
+ # Other
219
+ self.lang_display_var = StringVar(self)
220
+ self.lang_true_var = StringVar(self)
221
+ self.response_event = Event()
222
+ self.response = None
223
+ self.action: Optional[Callable[..., Any]] = None
224
+ self.job: Optional[Job] = None
225
+
226
+ def init_frames(self) -> None:
227
+ self.input_frame = InputFrame(
228
+ self, self.scrollable_frame, borderwidth=1, text=I("Input")
229
+ )
230
+ self.comp_frame = CompFrame(
231
+ self, self.scrollable_frame, borderwidth=1, text=I("Compression options")
232
+ )
233
+ self.output_frame = OutputFrame(
234
+ self, self.scrollable_frame, borderwidth=1, text=I("Output")
235
+ )
236
+ self.cred_frame = CredFrame(
237
+ self, self.scrollable_frame, borderwidth=1, text=I("Credentials")
238
+ )
239
+ self.settings_frame = ConfigFrame(
240
+ self, self.scrollable_frame, borderwidth=1, text=I("Config")
241
+ )
242
+ self.progress_frame = ProgressFrame(
243
+ self, self.scrollable_frame, borderwidth=1, text=I("Progress")
244
+ )
245
+ self.control_frame = ControlFrame(self, self.scrollable_frame, borderwidth=1)
246
+
247
+ def pack_frames(self) -> None:
248
+ self.input_frame.grid(column=0, row=0, sticky="w", padx=5, pady=5)
249
+ self.comp_frame.grid(column=1, row=0, sticky="news", padx=5, pady=5)
250
+ self.output_frame.grid(column=0, row=1, sticky="w", padx=5, pady=5)
251
+ self.cred_frame.grid(column=1, row=1, rowspan=2, sticky="w", padx=5, pady=5)
252
+ self.settings_frame.grid(column=0, row=2, sticky="news", padx=5, pady=5)
253
+ self.progress_frame.grid(
254
+ column=0, row=3, columnspan=2, sticky="news", padx=5, pady=5
255
+ )
256
+ self.control_frame.grid(
257
+ column=0, row=4, columnspan=2, sticky="news", padx=5, pady=5
258
+ )
259
+
260
+ def author_info(self) -> None:
261
+ self.cb.put(self.MSG_MORAL.format(project_url=self.PROJECT_URL))
262
+
263
+ def warn_tkinter_bug(self) -> None:
264
+ if (
265
+ platform.system() == "Darwin"
266
+ and platform.mac_ver()[0].split(".")[0] == "14"
267
+ and sys.version_info[0] == 3
268
+ and sys.version_info[1] == 11
269
+ and sys.version_info[2] <= 6
270
+ ):
271
+ self.cb.put(
272
+ self.MSG_MAC14_TK_BUG.format(mac14_tk_bug_url=self.MAC14_TK_BUG_URL)
273
+ )
274
+
275
+ def load_jsons(self) -> None:
276
+ self.help = load_resource_json("help")
277
+ self.input_presets = load_resource_json("input")
278
+ self.compression_presets = load_resource_json("compression")
279
+ self.output_presets = load_resource_json("output")
280
+ self.emoji_list = load_resource_json("emoji")
281
+
282
+ self.settings_path = CONFIG_DIR / "config.json"
283
+ if self.settings_path.is_file():
284
+ try:
285
+ self.settings: Dict[Any, Any] = JsonManager.load_json(
286
+ self.settings_path
287
+ )
288
+ except JSONDecodeError:
289
+ self.cb.put(I("Warning: config.json content is corrupted"))
290
+ self.settings = {}
291
+ else:
292
+ self.settings = {}
293
+
294
+ self.creds_path = CONFIG_DIR / "creds.json"
295
+ if self.creds_path.is_file():
296
+ try:
297
+ self.creds = JsonManager.load_json(self.creds_path)
298
+ except JSONDecodeError:
299
+ self.cb.put(I("Warning: creds.json content is corrupted"))
300
+ self.creds = {}
301
+ else:
302
+ self.creds = {}
303
+
304
+ def save_config(self) -> None:
305
+ # Only update comp_custom if custom preset is selected
306
+ if self.comp_preset_var.get() == "custom":
307
+ comp_custom: Dict[Any, Any] = merge( # type: ignore
308
+ self.compression_presets.get("custom"), # type: ignore
309
+ self.get_opt_comp().to_dict(),
310
+ )
311
+ comp_custom["format"]["img"] = comp_custom["format"]["img"][0]
312
+ comp_custom["format"]["vid"] = comp_custom["format"]["vid"][0]
313
+ del comp_custom["preset"]
314
+ del comp_custom["no_compress"]
315
+ else:
316
+ compression_presets_custom = self.compression_presets.get("custom")
317
+ if compression_presets_custom is None:
318
+ comp_custom = {}
319
+ else:
320
+ comp_custom = compression_presets_custom
321
+
322
+ self.settings = {
323
+ "input": self.get_opt_input().to_dict(),
324
+ "comp": {
325
+ "no_compress": self.no_compress_var.get(),
326
+ "preset": self.comp_preset_var.get(),
327
+ "chromium_path": self.chromium_path_var.get(),
328
+ "cache_dir": self.cache_dir_var.get(),
329
+ "processes": self.processes_var.get(),
330
+ },
331
+ "comp_custom": comp_custom,
332
+ "output": self.get_opt_output().to_dict(),
333
+ "creds": {"save_cred": self.settings_save_cred_var.get()},
334
+ "lang": self.lang_true_var.get(),
335
+ }
336
+
337
+ JsonManager.save_json(self.settings_path, self.settings)
338
+
339
+ def save_creds(self) -> None:
340
+ self.creds = self.get_opt_cred().to_dict()
341
+
342
+ JsonManager.save_json(self.creds_path, self.creds)
343
+
344
+ def delete_creds(self) -> None:
345
+ if self.creds_path.is_file():
346
+ os.remove(self.creds_path)
347
+
348
+ def delete_config(self) -> None:
349
+ if self.settings_path.is_file():
350
+ os.remove(self.settings_path)
351
+
352
+ def apply_config(self) -> None:
353
+ # Input
354
+ self.default_input_mode: str = self.settings.get("input", {}).get(
355
+ "option", "auto"
356
+ )
357
+ self.input_address_var.set(self.settings.get("input", {}).get("url", ""))
358
+ default_stickers_input_dir = str(DEFAULT_DIR / "stickers_input")
359
+ self.input_setdir_var.set(
360
+ self.settings.get("input", {}).get("dir", default_stickers_input_dir)
361
+ )
362
+ if not Path(self.input_setdir_var.get()).is_dir():
363
+ self.input_setdir_var.set(default_stickers_input_dir)
364
+ self.input_option_display_var.set(
365
+ self.input_presets[self.default_input_mode]["full_name"]
366
+ )
367
+ self.input_option_true_var.set(
368
+ self.input_presets[self.default_input_mode]["full_name"]
369
+ )
370
+
371
+ # Compression
372
+ self.no_compress_var.set(
373
+ self.settings.get("comp", {}).get("no_compress", False)
374
+ )
375
+ default_comp_preset = list(self.compression_presets.keys())[0]
376
+ self.comp_preset_var.set(
377
+ self.settings.get("comp", {}).get("preset", default_comp_preset)
378
+ )
379
+ comp_custom = self.settings.get("comp_custom")
380
+ if comp_custom:
381
+ self.compression_presets["custom"] = merge(
382
+ self.compression_presets["custom"], comp_custom
383
+ )
384
+ self.cache_dir_var.set(self.settings.get("comp", {}).get("cache_dir", ""))
385
+ self.chromium_path_var.set(
386
+ self.settings.get("comp", {}).get("chromium_path", "")
387
+ )
388
+ self.processes_var.set(
389
+ self.settings.get("comp", {}).get("processes", ceil(cpu_count() / 2))
390
+ )
391
+ self.default_output_mode: str = self.settings.get("output", {}).get(
392
+ "option", "signal"
393
+ )
394
+
395
+ # Output
396
+ default_stickers_output_dir = str(DEFAULT_DIR / "stickers_output")
397
+ self.output_setdir_var.set(
398
+ self.settings.get("output", {}).get("dir", default_stickers_output_dir)
399
+ )
400
+ if not Path(self.output_setdir_var.get()).is_dir():
401
+ self.output_setdir_var.set(default_stickers_output_dir)
402
+ self.title_var.set(self.settings.get("output", {}).get("title", ""))
403
+ self.author_var.set(self.settings.get("output", {}).get("author", ""))
404
+ self.settings_save_cred_var.set(
405
+ self.settings.get("creds", {}).get("save_cred", True)
406
+ )
407
+ self.output_option_display_var.set(
408
+ self.output_presets[self.default_output_mode]["full_name"]
409
+ )
410
+ self.output_option_true_var.set(
411
+ self.output_presets[self.default_output_mode]["full_name"]
412
+ )
413
+
414
+ # Language
415
+ self.lang_true_var.set(self.settings.get("lang", "auto"))
416
+ self.lang_display_var.set(
417
+ [k for k, v in SUPPORTED_LANG.items() if v == self.lang_true_var.get()][0]
418
+ )
419
+
420
+ def apply_creds(self) -> None:
421
+ self.signal_uuid_var.set(self.creds.get("signal", {}).get("uuid", ""))
422
+ self.signal_password_var.set(self.creds.get("signal", {}).get("password", ""))
423
+ self.telegram_token_var.set(self.creds.get("telegram", {}).get("token", ""))
424
+ self.telegram_userid_var.set(self.creds.get("telegram", {}).get("userid", ""))
425
+ self.telethon_api_id_var.set(self.creds.get("telethon", {}).get("api_id", 0))
426
+ self.telethon_api_hash_var.set(
427
+ self.creds.get("telethon", {}).get("api_hash", "")
428
+ )
429
+ self.kakao_auth_token_var.set(self.creds.get("kakao", {}).get("auth_token", ""))
430
+ self.kakao_username_var.set(self.creds.get("kakao", {}).get("username", ""))
431
+ self.kakao_password_var.set(self.creds.get("kakao", {}).get("password", ""))
432
+ self.kakao_country_code_var.set(
433
+ self.creds.get("kakao", {}).get("country_code", "")
434
+ )
435
+ self.kakao_phone_number_var.set(
436
+ self.creds.get("kakao", {}).get("phone_number", "")
437
+ )
438
+ self.kakao_device_uuid_var.set(
439
+ self.creds.get("kakao", {}).get("device_uuid", "")
440
+ )
441
+ self.line_cookies_var.set(self.creds.get("line", {}).get("cookies", ""))
442
+ self.viber_auth_var.set(self.creds.get("viber", {}).get("auth", ""))
443
+ self.discord_token_var.set(self.creds.get("discord", {}).get("token", ""))
444
+
445
+ def get_input_name(self) -> str:
446
+ return [
447
+ k
448
+ for k, v in self.input_presets.items()
449
+ if v["full_name"] == self.input_option_true_var.get()
450
+ ][0]
451
+
452
+ def get_input_display_name(self) -> str:
453
+ return [
454
+ k
455
+ for k, v in self.input_presets.items()
456
+ if v["full_name"] == self.input_option_display_var.get()
457
+ ][0]
458
+
459
+ def get_output_name(self) -> str:
460
+ return [
461
+ k
462
+ for k, v in self.output_presets.items()
463
+ if v["full_name"] == self.output_option_true_var.get()
464
+ ][0]
465
+
466
+ # def get_output_display_name(self) -> str:
467
+ # return [
468
+ # k
469
+ # for k, v in self.output_presets.items()
470
+ # if v["full_name"] == self.output_option_display_var.get()
471
+ # ][0]
472
+
473
+ def get_preset(self) -> str:
474
+ selection = self.comp_preset_var.get()
475
+ if selection == "auto":
476
+ output_option = self.get_output_name()
477
+ if "telegram_emoji" in output_option:
478
+ return "telegram_emoji"
479
+ if "telegram" in output_option:
480
+ return "telegram"
481
+ if output_option == "imessage":
482
+ return "imessage_small"
483
+ if output_option == "local":
484
+ return selection
485
+ return output_option
486
+
487
+ return selection
488
+
489
+ def start_job(self) -> None:
490
+ self.save_config()
491
+ if self.settings_save_cred_var.get() is True:
492
+ self.save_creds()
493
+ else:
494
+ self.delete_creds()
495
+
496
+ self.control_frame.start_btn.config(text=I("Cancel"), bootstyle="danger") # type: ignore
497
+ self.set_inputs("disabled")
498
+
499
+ opt_input = self.get_opt_input()
500
+ opt_output = self.get_opt_output()
501
+ opt_comp = self.get_opt_comp()
502
+ opt_cred = self.get_opt_cred()
503
+
504
+ self.job = Job(opt_input, opt_comp, opt_output, opt_cred, self.cb)
505
+
506
+ signal.signal(signal.SIGINT, self.job.cancel)
507
+
508
+ Thread(target=self.start_process, daemon=True).start()
509
+
510
+ def get_opt_input(self) -> InputOption:
511
+ return InputOption(
512
+ option=self.get_input_name(),
513
+ url=self.input_address_var.get(),
514
+ dir=Path(self.input_setdir_var.get()),
515
+ )
516
+
517
+ def get_opt_output(self) -> OutputOption:
518
+ return OutputOption(
519
+ option=self.get_output_name(),
520
+ dir=Path(self.output_setdir_var.get()),
521
+ title=self.title_var.get(),
522
+ author=self.author_var.get(),
523
+ )
524
+
525
+ def get_opt_comp(self) -> CompOption:
526
+ return CompOption(
527
+ preset=self.get_preset(),
528
+ size_max_img=self.img_size_max_var.get()
529
+ if not self.size_disable_var.get()
530
+ else None,
531
+ size_max_vid=self.vid_size_max_var.get()
532
+ if not self.size_disable_var.get()
533
+ else None,
534
+ format_img=(self.img_format_var.get(),),
535
+ format_vid=(self.vid_format_var.get(),),
536
+ fps_min=self.fps_min_var.get() if not self.fps_disable_var.get() else None,
537
+ fps_max=self.fps_max_var.get() if not self.fps_disable_var.get() else None,
538
+ fps_power=self.fps_power_var.get(),
539
+ res_w_min=self.res_w_min_var.get()
540
+ if not self.res_w_disable_var.get()
541
+ else None,
542
+ res_w_max=self.res_w_max_var.get()
543
+ if not self.res_w_disable_var.get()
544
+ else None,
545
+ res_h_min=self.res_h_min_var.get()
546
+ if not self.res_h_disable_var.get()
547
+ else None,
548
+ res_h_max=self.res_h_max_var.get()
549
+ if not self.res_h_disable_var.get()
550
+ else None,
551
+ res_power=self.res_power_var.get(),
552
+ res_snap_pow2=self.res_snap_pow2_var.get(),
553
+ quality_min=self.quality_min_var.get()
554
+ if not self.quality_disable_var.get()
555
+ else None,
556
+ quality_max=self.quality_max_var.get()
557
+ if not self.quality_disable_var.get()
558
+ else None,
559
+ quality_power=self.quality_power_var.get(),
560
+ color_min=self.color_min_var.get()
561
+ if not self.color_disable_var.get()
562
+ else None,
563
+ color_max=self.color_max_var.get()
564
+ if not self.color_disable_var.get()
565
+ else None,
566
+ color_power=self.color_power_var.get(),
567
+ duration_min=self.duration_min_var.get()
568
+ if not self.duration_disable_var.get()
569
+ else None,
570
+ duration_max=self.duration_max_var.get()
571
+ if not self.duration_disable_var.get()
572
+ else None,
573
+ bg_color=self.bg_color_var.get(),
574
+ padding_percent=self.padding_percent_var.get(),
575
+ steps=self.steps_var.get(),
576
+ fake_vid=self.fake_vid_var.get(),
577
+ scale_filter=self.scale_filter_var.get(),
578
+ quantize_method=self.quantize_method_var.get(),
579
+ chromium_path=self.chromium_path_var.get()
580
+ if self.chromium_path_var.get() != ""
581
+ else None,
582
+ cache_dir=self.cache_dir_var.get()
583
+ if self.cache_dir_var.get() != ""
584
+ else None,
585
+ default_emoji=self.default_emoji_var.get(),
586
+ no_compress=self.no_compress_var.get(),
587
+ processes=self.processes_var.get(),
588
+ )
589
+
590
+ def get_opt_cred(self) -> CredOption:
591
+ return CredOption(
592
+ signal_uuid=self.signal_uuid_var.get(),
593
+ signal_password=self.signal_password_var.get(),
594
+ telegram_token=self.telegram_token_var.get(),
595
+ telegram_userid=self.telegram_userid_var.get(),
596
+ telethon_api_id=self.telethon_api_id_var.get(),
597
+ telethon_api_hash=self.telethon_api_hash_var.get(),
598
+ kakao_auth_token=self.kakao_auth_token_var.get(),
599
+ kakao_username=self.kakao_username_var.get(),
600
+ kakao_password=self.kakao_password_var.get(),
601
+ kakao_country_code=self.kakao_country_code_var.get(),
602
+ kakao_phone_number=self.kakao_phone_number_var.get(),
603
+ kakao_device_uuid=self.kakao_device_uuid_var.get(),
604
+ line_cookies=self.line_cookies_var.get(),
605
+ viber_auth=self.viber_auth_var.get(),
606
+ discord_token=self.discord_token_var.get(),
607
+ )
608
+
609
+ def start_process(self) -> None:
610
+ if self.job:
611
+ self.job.start()
612
+ self.job = None
613
+
614
+ self.stop_job()
615
+
616
+ def stop_job(self) -> None:
617
+ self.set_inputs("normal")
618
+ self.control_frame.start_btn.config(text=I("Start"), bootstyle="default") # type: ignore
619
+
620
+ def cancel_job(self) -> None:
621
+ if self.job:
622
+ # Need to start new thread or else GUI may freeze
623
+ Thread(target=self.job.cancel, daemon=True).start()
624
+
625
+ def set_inputs(self, state: str) -> None:
626
+ # state: 'normal', 'disabled'
627
+
628
+ self.input_frame.set_states(state=state)
629
+ self.comp_frame.set_states(state=state)
630
+ self.output_frame.set_states(state=state)
631
+ self.cred_frame.set_states(state=state)
632
+ self.settings_frame.set_states(state=state)
633
+
634
+ def exec_in_main(self, _evt: Any) -> Any:
635
+ if self.action:
636
+ self.response = self.action()
637
+ self.response_event.set()
638
+
639
+ def highlight_fields(self) -> bool:
640
+ if not self.init_done:
641
+ return True
642
+
643
+ input_option = self.get_input_name()
644
+ input_option_display = self.get_input_display_name()
645
+ output_option = self.get_output_name()
646
+ # output_option_display = self.get_output_display_name()
647
+ url = self.input_address_var.get()
648
+
649
+ in_out_dir_same = (
650
+ Path(self.input_setdir_var.get()).absolute()
651
+ == Path(self.output_setdir_var.get()).absolute()
652
+ )
653
+
654
+ # Input
655
+ if in_out_dir_same is True:
656
+ self.input_frame.input_setdir_entry.config(bootstyle="danger") # type: ignore
657
+ elif not Path(self.input_setdir_var.get()).is_dir():
658
+ self.input_frame.input_setdir_entry.config(bootstyle="warning") # type: ignore
659
+ else:
660
+ self.input_frame.input_setdir_entry.config(bootstyle="default") # type: ignore
661
+
662
+ self.input_frame.address_lbl.config(
663
+ text=self.input_presets[input_option_display]["address_lbls"]
664
+ )
665
+ self.input_frame.address_entry.config(bootstyle="default") # type: ignore
666
+
667
+ if input_option == "local":
668
+ self.input_frame.address_entry.config(state="disabled")
669
+ self.input_frame.address_tip.config(
670
+ text=self.input_presets[input_option_display]["example"]
671
+ )
672
+
673
+ else:
674
+ self.input_frame.address_entry.config(state="normal")
675
+ self.input_frame.address_tip.config(
676
+ text=self.input_presets[input_option_display]["example"]
677
+ )
678
+ download_option = UrlDetect.detect(url)
679
+
680
+ if not url:
681
+ self.input_frame.address_entry.config(bootstyle="warning") # type: ignore
682
+
683
+ elif (
684
+ download_option is None
685
+ or input_option.startswith(download_option) is False
686
+ and not (
687
+ input_option
688
+ in ("kakao", "band", "line", "discord", "discord_emoji")
689
+ and url.isnumeric()
690
+ )
691
+ ):
692
+ self.input_frame.address_entry.config(bootstyle="danger") # type: ignore
693
+ self.input_frame.address_tip.config(
694
+ text=I("Invalid URL: ")
695
+ + self.input_presets[input_option_display]["example"]
696
+ )
697
+
698
+ elif input_option_display == "auto" and download_option:
699
+ self.input_frame.address_tip.config(
700
+ text=I("Detected URL: ") + download_option
701
+ )
702
+
703
+ # Output
704
+ if in_out_dir_same is True:
705
+ self.output_frame.output_setdir_entry.config(bootstyle="danger") # type: ignore
706
+ elif not Path(self.output_setdir_var.get()).is_dir():
707
+ self.output_frame.output_setdir_entry.config(bootstyle="warning") # type: ignore
708
+ else:
709
+ self.output_frame.output_setdir_entry.config(bootstyle="default") # type: ignore
710
+
711
+ if (
712
+ MetadataHandler.check_metadata_required(output_option, "title")
713
+ and not MetadataHandler.check_metadata_provided(
714
+ Path(self.input_setdir_var.get()), input_option, "title"
715
+ )
716
+ and not self.title_var.get()
717
+ ):
718
+ self.output_frame.title_entry.config(bootstyle="warning") # type: ignore
719
+ else:
720
+ self.output_frame.title_entry.config(bootstyle="default") # type: ignore
721
+
722
+ if (
723
+ MetadataHandler.check_metadata_required(output_option, "author")
724
+ and not MetadataHandler.check_metadata_provided(
725
+ Path(self.input_setdir_var.get()), input_option, "author"
726
+ )
727
+ and not self.author_var.get()
728
+ ):
729
+ self.output_frame.author_entry.config(bootstyle="warning") # type: ignore
730
+ else:
731
+ self.output_frame.author_entry.config(bootstyle="default") # type: ignore
732
+
733
+ # Credentials
734
+ if output_option == "signal" and not self.signal_uuid_var.get():
735
+ self.cred_frame.signal_uuid_entry.config(bootstyle="warning") # type: ignore
736
+ else:
737
+ self.cred_frame.signal_uuid_entry.config(bootstyle="default") # type: ignore
738
+
739
+ if output_option == "signal" and not self.signal_password_var.get():
740
+ self.cred_frame.signal_password_entry.config(bootstyle="warning") # type: ignore
741
+ else:
742
+ self.cred_frame.signal_password_entry.config(bootstyle="default") # type: ignore
743
+
744
+ if (
745
+ input_option == "telegram" or output_option == "telegram"
746
+ ) and not self.telegram_token_var.get():
747
+ self.cred_frame.telegram_token_entry.config(bootstyle="warning") # type: ignore
748
+ else:
749
+ self.cred_frame.telegram_token_entry.config(bootstyle="default") # type: ignore
750
+
751
+ if output_option == "telegram" and not self.telegram_userid_var.get():
752
+ self.cred_frame.telegram_userid_entry.config(bootstyle="warning") # type: ignore
753
+ else:
754
+ self.cred_frame.telegram_userid_entry.config(bootstyle="default") # type: ignore
755
+
756
+ if output_option == "viber" and not self.viber_auth_var.get():
757
+ self.cred_frame.viber_auth_entry.config(bootstyle="warning") # type: ignore
758
+ else:
759
+ self.cred_frame.viber_auth_entry.config(bootstyle="default") # type: ignore
760
+
761
+ if (
762
+ urlparse(url).netloc == "e.kakao.com"
763
+ and not self.kakao_auth_token_var.get()
764
+ ):
765
+ self.cred_frame.kakao_auth_token_entry.config(bootstyle="warning") # type: ignore
766
+ else:
767
+ self.cred_frame.kakao_auth_token_entry.config(bootstyle="default") # type: ignore
768
+
769
+ if input_option.startswith("discord") and not self.discord_token_var.get():
770
+ self.cred_frame.discord_token_entry.config(bootstyle="warning") # type: ignore
771
+ else:
772
+ self.cred_frame.discord_token_entry.config(bootstyle="default") # type: ignore
773
+
774
+ # Check for Input and Compression mismatch
775
+ if (
776
+ not self.no_compress_var.get()
777
+ and self.get_output_name() != "local"
778
+ and self.comp_preset_var.get() not in ("auto", "custom")
779
+ and self.get_output_name().replace("_telethon", "")
780
+ not in self.comp_preset_var.get()
781
+ ):
782
+ self.comp_frame.comp_preset_opt.config(bootstyle="warning") # type: ignore
783
+ self.output_frame.output_option_opt.config(bootstyle="warning") # type: ignore
784
+ else:
785
+ self.comp_frame.comp_preset_opt.config(bootstyle="secondary") # type: ignore
786
+ self.output_frame.output_option_opt.config(bootstyle="secondary") # type: ignore
787
+
788
+ return True