pygpt-net 2.7.0__py3-none-any.whl → 2.7.2__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 (37) hide show
  1. pygpt_net/CHANGELOG.txt +14 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/ctx/ctx.py +4 -1
  4. pygpt_net/controller/painter/common.py +43 -11
  5. pygpt_net/core/filesystem/filesystem.py +70 -0
  6. pygpt_net/core/filesystem/packer.py +161 -1
  7. pygpt_net/core/image/image.py +2 -2
  8. pygpt_net/core/platforms/platforms.py +14 -1
  9. pygpt_net/core/updater/updater.py +24 -12
  10. pygpt_net/core/video/video.py +2 -3
  11. pygpt_net/data/config/config.json +3 -3
  12. pygpt_net/data/config/models.json +3 -3
  13. pygpt_net/data/css/style.dark.css +13 -6
  14. pygpt_net/data/css/style.light.css +13 -5
  15. pygpt_net/data/locale/locale.de.ini +2 -0
  16. pygpt_net/data/locale/locale.en.ini +2 -0
  17. pygpt_net/data/locale/locale.es.ini +2 -0
  18. pygpt_net/data/locale/locale.fr.ini +2 -0
  19. pygpt_net/data/locale/locale.it.ini +2 -0
  20. pygpt_net/data/locale/locale.pl.ini +3 -1
  21. pygpt_net/data/locale/locale.uk.ini +2 -0
  22. pygpt_net/data/locale/locale.zh.ini +2 -0
  23. pygpt_net/provider/core/config/patch.py +16 -0
  24. pygpt_net/ui/dialog/preset.py +1 -0
  25. pygpt_net/ui/layout/toolbox/image.py +2 -1
  26. pygpt_net/ui/layout/toolbox/indexes.py +2 -0
  27. pygpt_net/ui/layout/toolbox/video.py +5 -1
  28. pygpt_net/ui/main.py +1 -0
  29. pygpt_net/ui/widget/dialog/update.py +18 -7
  30. pygpt_net/ui/widget/draw/painter.py +238 -51
  31. pygpt_net/ui/widget/filesystem/explorer.py +82 -0
  32. pygpt_net/ui/widget/option/combo.py +179 -13
  33. {pygpt_net-2.7.0.dist-info → pygpt_net-2.7.2.dist-info}/METADATA +44 -4
  34. {pygpt_net-2.7.0.dist-info → pygpt_net-2.7.2.dist-info}/RECORD +37 -37
  35. {pygpt_net-2.7.0.dist-info → pygpt_net-2.7.2.dist-info}/LICENSE +0 -0
  36. {pygpt_net-2.7.0.dist-info → pygpt_net-2.7.2.dist-info}/WHEEL +0 -0
  37. {pygpt_net-2.7.0.dist-info → pygpt_net-2.7.2.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,17 @@
1
+ 2.7.2 (2025-12-29)
2
+
3
+ - Fixed: non-searchable combobox width.
4
+ - Improved updater.
5
+ - Added .AppImage build.
6
+
7
+ 2.7.1 (2025-12-28)
8
+
9
+ - Improved UI elements.
10
+ - Optimized Painter rendering and redraw functions.
11
+ - Added Pack/Unpack feature to File Explorer.
12
+ - Fixed: image restoration in Painter.
13
+ - Fixed: tab title updating upon context deletion.
14
+
1
15
  2.7.0 (2025-12-28)
2
16
 
3
17
  - Added multi-select functionality using CTRL or SHIFT and batch actions to the context list, preset list, attachments list, and other list-based widgets.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.12.28 00:00:00 #
9
+ # Updated Date: 2025.12.29 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.7.0"
17
- __build__ = "2025-12-28"
16
+ __version__ = "2.7.2"
17
+ __build__ = "2025-12-29"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
@@ -622,6 +622,7 @@ class Ctx:
622
622
  )
623
623
  return
624
624
  updated = False
625
+ updated_current = False
625
626
  ids = id if isinstance(id, list) else [id]
626
627
  for id in ids:
627
628
  try:
@@ -633,6 +634,7 @@ class Ctx:
633
634
  if self.window.core.ctx.get_current() == id:
634
635
  items = self.window.core.ctx.all() # TODO: get by meta id(s)
635
636
  self.window.core.history.remove_items(items)
637
+ updated_current = True
636
638
  self.window.core.attachments.context.delete_by_meta_id(id)
637
639
  self.window.core.ctx.remove(id)
638
640
  self.remove_selected(id)
@@ -645,7 +647,8 @@ class Ctx:
645
647
 
646
648
  if updated:
647
649
  self.update_and_restore()
648
- self.window.controller.ui.tabs.update_title_current("...")
650
+ if updated_current:
651
+ self.window.controller.ui.tabs.update_title_current("...")
649
652
 
650
653
  def delete_meta_from_idx(self, id: int):
651
654
  """
@@ -47,7 +47,12 @@ class Common:
47
47
  :param width: Canvas width
48
48
  :param height: Canvas height
49
49
  """
50
- self.window.ui.painter.setFixedSize(QSize(width, height))
50
+ painter = self.window.ui.painter
51
+ if hasattr(painter, "set_canvas_size_pixels"):
52
+ painter.set_canvas_size_pixels(width, height)
53
+ else:
54
+ # required on image open
55
+ self.window.ui.painter.setFixedSize(QSize(width, height))
51
56
 
52
57
  def set_brush_mode(self, enabled: bool):
53
58
  """
@@ -81,14 +86,21 @@ class Common:
81
86
  if self._changing_canvas_size:
82
87
  return
83
88
 
84
- combo: QComboBox = self.window.ui.nodes['painter.select.canvas.size']
89
+ # Be resilient if combobox node is not present in a given UI layout
90
+ combo: Optional[QComboBox] = None
91
+ try:
92
+ if hasattr(self.window.ui, "nodes"):
93
+ combo = self.window.ui.nodes.get('painter.select.canvas.size', None)
94
+ except Exception:
95
+ combo = None
96
+
85
97
  painter = self.window.ui.painter
86
98
 
87
99
  # Heuristic to detect manual UI change vs programmatic call
88
100
  # - manual if: no arg, or int index (Qt int overload), or arg equals currentText/currentData
89
101
  raw_arg = selected
90
- current_text = combo.currentText()
91
- current_data = combo.currentData()
102
+ current_text = combo.currentText() if combo is not None else ""
103
+ current_data = combo.currentData() if combo is not None else None
92
104
  current_data_str = current_data if isinstance(current_data, str) else None
93
105
  is_manual = (
94
106
  raw_arg is None
@@ -105,8 +117,15 @@ class Common:
105
117
  if not selected_norm:
106
118
  return
107
119
 
120
+ # Use true logical canvas size when available
121
+ if hasattr(painter, "get_canvas_size"):
122
+ cur_sz = painter.get_canvas_size()
123
+ cur_val = f"{cur_sz.width()}x{cur_sz.height()}"
124
+ else:
125
+ cur_val = f"{painter.width()}x{painter.height()}"
126
+
108
127
  # Save undo only for manual changes and only if size will change
109
- will_change = selected_norm != f"{painter.width()}x{painter.height()}"
128
+ will_change = selected_norm != cur_val
110
129
  if is_manual and will_change:
111
130
  painter.saveForUndo()
112
131
 
@@ -124,9 +143,10 @@ class Common:
124
143
  self._sticky_custom_value = selected_norm
125
144
 
126
145
  # Ensure combo reflects single custom at index 0 (sticky respected), then select current value
127
- self._sync_canvas_size_combo(combo, selected_norm, sticky_to_keep=self._sticky_custom_value)
146
+ if combo is not None:
147
+ self._sync_canvas_size_combo(combo, selected_norm, sticky_to_keep=self._sticky_custom_value)
128
148
 
129
- # Apply canvas size; PainterWidget handles rescaling in resizeEvent
149
+ # Apply canvas size; PainterWidget handles rescaling in its own logic
130
150
  w, h = self.convert_to_size(selected_norm)
131
151
  self.set_canvas_size(w, h)
132
152
 
@@ -272,10 +292,22 @@ class Common:
272
292
  if self._changing_canvas_size:
273
293
  return
274
294
 
275
- combo: QComboBox = self.window.ui.nodes['painter.select.canvas.size']
295
+ combo: Optional[QComboBox] = None
296
+ try:
297
+ if hasattr(self.window.ui, "nodes"):
298
+ combo = self.window.ui.nodes.get('painter.select.canvas.size', None)
299
+ except Exception:
300
+ combo = None
301
+
276
302
  painter = self.window.ui.painter
277
303
 
278
- canvas_value = f"{painter.width()}x{painter.height()}"
304
+ # Use true logical canvas size, not widget size
305
+ if hasattr(painter, "get_canvas_size"):
306
+ sz = painter.get_canvas_size()
307
+ canvas_value = f"{sz.width()}x{sz.height()}"
308
+ else:
309
+ canvas_value = f"{painter.width()}x{painter.height()}"
310
+
279
311
  canvas_norm = self._normalize_canvas_value(canvas_value)
280
312
  if not canvas_norm:
281
313
  return
@@ -292,7 +324,8 @@ class Common:
292
324
  try:
293
325
  self._changing_canvas_size = True
294
326
  self._sticky_custom_value = sticky
295
- self._sync_canvas_size_combo(combo, canvas_norm, sticky_to_keep=sticky)
327
+ if combo is not None:
328
+ self._sync_canvas_size_combo(combo, canvas_norm, sticky_to_keep=sticky)
296
329
 
297
330
  # Persist canvas size only (do not change sticky config-scope)
298
331
  self.window.core.config.set('painter.canvas.size', canvas_norm)
@@ -409,7 +442,6 @@ class Common:
409
442
  combo.setCurrentText(value)
410
443
  else:
411
444
  # Current value is custom: ensure it exists at index 0 and select it
412
- # If sticky differs or is None, overwrite/create the custom at index 0 to reflect true current value.
413
445
  if not sticky_to_keep or sticky_to_keep != value:
414
446
  self._ensure_custom_index0(combo, value, predef)
415
447
  if combo.currentIndex() != 0:
@@ -480,3 +480,73 @@ class Filesystem:
480
480
  else:
481
481
  files = [os.path.join(path, f) for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
482
482
  return files
483
+
484
+ # ===== Helpers for pack/unpack =====
485
+
486
+ def common_parent_dir(self, paths: List[str]) -> str:
487
+ """
488
+ Return a sensible common parent directory for the given paths.
489
+ For a single directory selection returns that directory; for a single file returns its parent.
490
+ For multiple selections returns a common existing parent, if resolvable, otherwise the parent of the first path.
491
+ """
492
+ if not paths:
493
+ return self.window.core.config.get_user_path()
494
+ norm = []
495
+ for p in paths:
496
+ p = os.path.abspath(p)
497
+ norm.append(p if os.path.isdir(p) else os.path.dirname(p))
498
+ if len(norm) == 1:
499
+ return norm[0]
500
+ try:
501
+ cp = os.path.commonpath(norm)
502
+ if os.path.isdir(cp):
503
+ return cp
504
+ return os.path.dirname(cp)
505
+ except Exception:
506
+ return norm[0]
507
+
508
+ def unique_path(self, directory: str, base_name: str, ext: str) -> str:
509
+ """
510
+ Return a unique file path within 'directory' for 'base_name' and 'ext' (ext should include dot or be empty).
511
+ Uses 'name', 'name (1)', 'name (2)', ... scheme.
512
+ """
513
+ os.makedirs(directory, exist_ok=True)
514
+ candidate = os.path.join(directory, f"{base_name}{ext}")
515
+ if not os.path.exists(candidate):
516
+ return candidate
517
+ i = 1
518
+ while True:
519
+ cand = os.path.join(directory, f"{base_name} ({i}){ext}")
520
+ if not os.path.exists(cand):
521
+ return cand
522
+ i += 1
523
+
524
+ def unique_dir(self, directory: str, base_name: str) -> str:
525
+ """
526
+ Return a unique directory path 'directory/base_name', adding ' (n)' suffix when needed.
527
+ """
528
+ os.makedirs(directory, exist_ok=True)
529
+ candidate = os.path.join(directory, base_name)
530
+ if not os.path.exists(candidate):
531
+ return candidate
532
+ i = 1
533
+ while True:
534
+ cand = os.path.join(directory, f"{base_name} ({i})")
535
+ if not os.path.exists(cand):
536
+ return cand
537
+ i += 1
538
+
539
+ def strip_archive_name(self, filename: str) -> str:
540
+ """
541
+ Strip known archive extensions (.zip, .tar, .tar.gz, .tar.bz2, .tar.xz, .tgz, .tbz2, .txz) from a filename.
542
+ Returns filename without extension(s).
543
+ """
544
+ combos = ['.tar.gz', '.tar.bz2', '.tar.xz', '.tgz', '.tbz2', '.txz']
545
+ lower = filename.lower()
546
+ for suf in combos:
547
+ if lower.endswith(suf):
548
+ return filename[:-len(suf)]
549
+ root, ext = os.path.splitext(filename)
550
+ if ext.lower() in ('.zip', '.tar'):
551
+ return root
552
+ return root
@@ -84,4 +84,164 @@ class Packer:
84
84
  :param path: path to directory
85
85
  """
86
86
  if os.path.exists(path) and os.path.isdir(path):
87
- shutil.rmtree(path)
87
+ shutil.rmtree(path)
88
+
89
+ # ===== New high-level pack/unpack API (non-breaking) =====
90
+
91
+ def can_unpack(self, path: str) -> bool:
92
+ """
93
+ Check using stdlib detectors whether given file is a supported archive.
94
+ """
95
+ if not (path and os.path.isfile(path)):
96
+ return False
97
+ try:
98
+ import zipfile, tarfile
99
+ return zipfile.is_zipfile(path) or tarfile.is_tarfile(path)
100
+ except Exception:
101
+ return False
102
+
103
+ def _detect_kind(self, path: str) -> str:
104
+ """
105
+ Detect archive kind: 'zip' or 'tar'. Returns '' if unknown.
106
+ """
107
+ try:
108
+ import zipfile, tarfile
109
+ if zipfile.is_zipfile(path):
110
+ return 'zip'
111
+ if tarfile.is_tarfile(path):
112
+ return 'tar'
113
+ except Exception:
114
+ pass
115
+ return ''
116
+
117
+ def pack_paths(self, paths: list, fmt: str, dest_dir: str = None, base_name: str = None) -> str:
118
+ """
119
+ Pack given paths into a single archive.
120
+
121
+ :param paths: list of files/dirs to include
122
+ :param fmt: 'zip' or 'tar.gz'
123
+ :param dest_dir: output directory (default: common parent of paths)
124
+ :param base_name: output archive base name without extension (optional)
125
+ :return: created archive path or empty string on error
126
+ """
127
+ if not paths:
128
+ return ""
129
+ fs = self.window.core.filesystem
130
+ paths = [os.path.abspath(p) for p in paths if p]
131
+ dest_dir = dest_dir or fs.common_parent_dir(paths)
132
+
133
+ if base_name is None:
134
+ if len(paths) == 1:
135
+ name = os.path.basename(paths[0].rstrip(os.sep))
136
+ if os.path.isfile(paths[0]):
137
+ root, _ = os.path.splitext(name)
138
+ base_name = root or name
139
+ else:
140
+ base_name = name
141
+ else:
142
+ base_name = "archive"
143
+
144
+ fmt = (fmt or "").lower()
145
+ if fmt == 'zip':
146
+ ext = ".zip"
147
+ out_path = fs.unique_path(dest_dir, base_name, ext)
148
+ try:
149
+ self._pack_zip(paths, out_path)
150
+ return out_path
151
+ except Exception as e:
152
+ try:
153
+ self.window.core.debug.log(e)
154
+ except Exception:
155
+ pass
156
+ return ""
157
+ elif fmt in ('tar.gz', 'tgz'):
158
+ ext = ".tar.gz"
159
+ out_path = fs.unique_path(dest_dir, base_name, ext)
160
+ try:
161
+ self._pack_tar_gz(paths, out_path)
162
+ return out_path
163
+ except Exception as e:
164
+ try:
165
+ self.window.core.debug.log(e)
166
+ except Exception:
167
+ pass
168
+ return ""
169
+ else:
170
+ return ""
171
+
172
+ def _pack_zip(self, paths: list, out_path: str):
173
+ """
174
+ Create ZIP archive with selected paths, preserving top-level names.
175
+ """
176
+ import zipfile
177
+ with zipfile.ZipFile(out_path, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
178
+ for src in paths:
179
+ top = os.path.basename(src.rstrip(os.sep))
180
+ if os.path.isdir(src):
181
+ for root, dirs, files in os.walk(src):
182
+ rel = os.path.relpath(root, src)
183
+ arc_root = top if rel == "." else os.path.join(top, rel)
184
+ # Add empty directories explicitly
185
+ if not files and not dirs:
186
+ zinfo = zipfile.ZipInfo(arc_root + "/")
187
+ zf.writestr(zinfo, "")
188
+ for f in files:
189
+ absf = os.path.join(root, f)
190
+ arcf = os.path.join(arc_root, f)
191
+ zf.write(absf, arcf)
192
+ else:
193
+ zf.write(src, top)
194
+
195
+ def _pack_tar_gz(self, paths: list, out_path: str):
196
+ """
197
+ Create TAR.GZ archive with selected paths, preserving top-level names.
198
+ """
199
+ import tarfile
200
+ with tarfile.open(out_path, 'w:gz') as tf:
201
+ for src in paths:
202
+ top = os.path.basename(src.rstrip(os.sep))
203
+ tf.add(src, arcname=top, recursive=True)
204
+
205
+ def unpack_to_dir(self, path: str, dest_dir: str) -> str:
206
+ """
207
+ Unpack archive at 'path' into 'dest_dir'.
208
+
209
+ :param path: archive file path
210
+ :param dest_dir: destination directory to extract into
211
+ :return: dest_dir on success, empty string otherwise
212
+ """
213
+ if not self.can_unpack(path):
214
+ return ""
215
+ try:
216
+ import zipfile, tarfile
217
+ os.makedirs(dest_dir, exist_ok=True)
218
+ kind = self._detect_kind(path)
219
+ if kind == 'zip':
220
+ with zipfile.ZipFile(path, 'r') as zf:
221
+ zf.extractall(dest_dir)
222
+ elif kind == 'tar':
223
+ with tarfile.open(path, 'r:*') as tf:
224
+ tf.extractall(dest_dir)
225
+ else:
226
+ return ""
227
+ return dest_dir
228
+ except Exception as e:
229
+ try:
230
+ self.window.core.debug.log(e)
231
+ except Exception:
232
+ pass
233
+ return ""
234
+
235
+ def unpack_to_sibling_dir(self, path: str) -> str:
236
+ """
237
+ Unpack archive into directory placed next to the archive, named after archive base name.
238
+
239
+ :param path: archive file path
240
+ :return: created directory path or empty string
241
+ """
242
+ if not (path and os.path.isfile(path)):
243
+ return ""
244
+ parent = os.path.dirname(path)
245
+ base = self.window.core.filesystem.strip_archive_name(os.path.basename(path))
246
+ out_dir = self.window.core.filesystem.unique_dir(parent, base)
247
+ return self.unpack_to_dir(path, out_dir)
@@ -152,7 +152,7 @@ class Image(QObject):
152
152
  """
153
153
  return {
154
154
  "type": "combo",
155
- "slider": True,
155
+ #"search": False,
156
156
  "label": "img_resolution",
157
157
  "value": "1024x1024",
158
158
  "keys": self.get_available_resolutions(),
@@ -166,7 +166,7 @@ class Image(QObject):
166
166
  """
167
167
  return {
168
168
  "type": "combo",
169
- "slider": True,
169
+ #"search": False,
170
170
  "label": "img_mode",
171
171
  "value": "image",
172
172
  "keys": self.get_available_modes(),
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.25 18:00:00 #
9
+ # Updated Date: 2025.12.29 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import platform
@@ -127,6 +127,17 @@ class Platforms:
127
127
  return True
128
128
  return False
129
129
 
130
+ def is_appimage(self) -> bool:
131
+ """
132
+ Return True if app is running as AppImage
133
+
134
+ :return: True if app is running as AppImage
135
+ """
136
+ path = os.path.join(self.window.core.config.get_app_path(), 'data', 'appimage.conf')
137
+ if os.path.exists(path):
138
+ return True
139
+ return False
140
+
130
141
  def get_as_string(self, env_suffix: bool = True) -> str:
131
142
  """
132
143
  Return platform as string
@@ -147,6 +158,8 @@ class Platforms:
147
158
  suffix = ''
148
159
  if self.is_snap():
149
160
  suffix = ' (snap)'
161
+ elif self.is_appimage():
162
+ suffix = ' (AppImage)'
150
163
  elif self.window.core.config.is_compiled():
151
164
  suffix = ' (standalone)'
152
165
  if self.is_windows():
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.11 00:00:00 #
9
+ # Updated Date: 2025.12.29 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -26,13 +26,14 @@ from packaging.version import parse as parse_version, Version
26
26
  from pygpt_net.utils import trans
27
27
 
28
28
 
29
- class Updater:
29
+ class Updater(QObject):
30
30
  def __init__(self, window=None):
31
31
  """
32
32
  Updater core (config data patcher)
33
33
 
34
34
  :param window: Window instance
35
35
  """
36
+ super(Updater, self).__init__()
36
37
  self.window = window
37
38
  self.thanks = None # cache
38
39
 
@@ -261,11 +262,11 @@ class Updater:
261
262
  return self.get_thanks()
262
263
  return self.thanks
263
264
 
264
- def check_silent(self) -> Tuple[bool, str, str, str, str, str]:
265
+ def check_silent(self) -> Tuple[bool, str, str, str, str, str, str]:
265
266
  """
266
267
  Check version in background
267
268
 
268
- :return: (is_new, newest_version, newest_build, changelog, download_windows, download_linux)
269
+ :return: (is_new, newest_version, newest_build, changelog, download_windows, download_linux, download_appimage)
269
270
  """
270
271
  url = self.get_updater_url()
271
272
  is_new = False
@@ -274,6 +275,7 @@ class Updater:
274
275
  changelog = ""
275
276
  download_windows = ""
276
277
  download_linux = ""
278
+ download_appimage = ""
277
279
 
278
280
  try:
279
281
  ctx = ssl.create_default_context()
@@ -315,6 +317,8 @@ class Updater:
315
317
  download_windows = data_json["download_windows"]
316
318
  if "download_linux" in data_json:
317
319
  download_linux = data_json["download_linux"]
320
+ if "download_appimage" in data_json:
321
+ download_appimage = data_json["download_appimage"]
318
322
  if "thanks" in data_json:
319
323
  self.thanks = self.parse_thanks(data_json["thanks"])
320
324
 
@@ -331,7 +335,7 @@ class Updater:
331
335
  self.window.core.debug.log(e)
332
336
  print("Failed to check for updates")
333
337
 
334
- return is_new, newest_version, newest_build, changelog, download_windows, download_linux
338
+ return is_new, newest_version, newest_build, changelog, download_windows, download_linux, download_appimage
335
339
 
336
340
  def parse_thanks(self, people: str) -> str:
337
341
  """
@@ -352,7 +356,7 @@ class Updater:
352
356
  :return: True if force show version dialog
353
357
  """
354
358
  print("Checking for updates...")
355
- is_new, version, build, changelog, download_windows, download_linux = self.check_silent()
359
+ is_new, version, build, changelog, download_windows, download_linux, download_appimage = self.check_silent()
356
360
  if is_new or force:
357
361
  self.show_version_dialog(
358
362
  version,
@@ -360,6 +364,7 @@ class Updater:
360
364
  changelog,
361
365
  download_windows,
362
366
  download_linux,
367
+ download_appimage,
363
368
  is_new
364
369
  )
365
370
  return True
@@ -373,6 +378,7 @@ class Updater:
373
378
  changelog: str,
374
379
  download_windows: str,
375
380
  download_linux: str,
381
+ download_appimage: str = "",
376
382
  is_new: bool = False
377
383
  ):
378
384
  """
@@ -383,6 +389,7 @@ class Updater:
383
389
  :param changelog: changelog
384
390
  :param download_windows: windows download link
385
391
  :param download_linux: linux download link
392
+ :param download_appimage: appimage download link
386
393
  :param is_new: True if is new version available
387
394
  """
388
395
  self.window.ui.dialog['update'].set_data(
@@ -391,7 +398,8 @@ class Updater:
391
398
  build,
392
399
  changelog,
393
400
  download_windows,
394
- download_linux
401
+ download_linux,
402
+ download_appimage
395
403
  )
396
404
  self.window.ui.dialogs.open('update', height=600)
397
405
 
@@ -419,14 +427,15 @@ class Updater:
419
427
 
420
428
  return updated
421
429
 
422
- @Slot(str, str, str, str, str)
430
+ @Slot(str, str, str, str, str, str)
423
431
  def handle_new_version(
424
432
  self,
425
433
  version: str,
426
434
  build: str,
427
435
  changelog: str,
428
- download_windows: str = "",
429
- download_linux: str = ""
436
+ download_windows: str,
437
+ download_linux: str,
438
+ download_appimage: str
430
439
  ):
431
440
  """
432
441
  Handle new version signal
@@ -436,6 +445,7 @@ class Updater:
436
445
  :param changelog: changelog
437
446
  :param download_windows: download link for windows
438
447
  :param download_linux: download link for linux
448
+ :param download_appimage: download link for appimage
439
449
  """
440
450
  if self.window.ui.tray.is_tray:
441
451
  self.window.ui.tray.show_msg(
@@ -450,6 +460,7 @@ class Updater:
450
460
  changelog,
451
461
  download_windows,
452
462
  download_linux,
463
+ download_appimage,
453
464
  True
454
465
  )
455
466
 
@@ -468,7 +479,7 @@ class Updater:
468
479
 
469
480
 
470
481
  class UpdaterSignals(QObject):
471
- version_changed = Signal(str, str, str, str, str)
482
+ version_changed = Signal(str, str, str, str, str, str)
472
483
 
473
484
 
474
485
  class UpdaterWorker(QRunnable):
@@ -498,7 +509,7 @@ class UpdaterWorker(QRunnable):
498
509
  if self.force:
499
510
  print("Checking for updates...")
500
511
 
501
- is_new, version, build, changelog, download_windows, download_linux = self.checker()
512
+ is_new, version, build, changelog, download_windows, download_linux, download_appimage = self.checker()
502
513
  if is_new:
503
514
  if self.force or (parsed_prev_checked is None or parsed_prev_checked < parse_version(version)):
504
515
  self.signals.version_changed.emit(
@@ -507,6 +518,7 @@ class UpdaterWorker(QRunnable):
507
518
  changelog,
508
519
  download_windows,
509
520
  download_linux,
521
+ download_appimage
510
522
  )
511
523
  return
512
524
  if self.force:
@@ -276,7 +276,7 @@ class Video(QObject):
276
276
  """
277
277
  return {
278
278
  "type": "combo",
279
- "slider": True,
279
+ # "search": False,
280
280
  "label": "video.aspect_ratio",
281
281
  "value": "16:9",
282
282
  "keys": self.get_available_aspect_ratio(),
@@ -290,7 +290,7 @@ class Video(QObject):
290
290
  """
291
291
  return {
292
292
  "type": "combo",
293
- "slider": True,
293
+ # "search": False,
294
294
  "label": "video.resolution",
295
295
  "value": "720p",
296
296
  "keys": self.get_available_resolutions(),
@@ -304,7 +304,6 @@ class Video(QObject):
304
304
  """
305
305
  return {
306
306
  "type": "int",
307
- "slider": False,
308
307
  "label": "video.duration",
309
308
  "value": 8,
310
309
  "placeholder": "s",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.7.0",
4
- "app.version": "2.7.0",
5
- "updated_at": "2025-12-28T00:00:00"
3
+ "version": "2.7.2",
4
+ "app.version": "2.7.2",
5
+ "updated_at": "2025-12-29T00:00:00"
6
6
  },
7
7
  "access.audio.event.speech": false,
8
8
  "access.audio.event.speech.disabled": [],