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.
- pygpt_net/CHANGELOG.txt +14 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/ctx/ctx.py +4 -1
- pygpt_net/controller/painter/common.py +43 -11
- pygpt_net/core/filesystem/filesystem.py +70 -0
- pygpt_net/core/filesystem/packer.py +161 -1
- pygpt_net/core/image/image.py +2 -2
- pygpt_net/core/platforms/platforms.py +14 -1
- pygpt_net/core/updater/updater.py +24 -12
- pygpt_net/core/video/video.py +2 -3
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/css/style.dark.css +13 -6
- pygpt_net/data/css/style.light.css +13 -5
- pygpt_net/data/locale/locale.de.ini +2 -0
- pygpt_net/data/locale/locale.en.ini +2 -0
- pygpt_net/data/locale/locale.es.ini +2 -0
- pygpt_net/data/locale/locale.fr.ini +2 -0
- pygpt_net/data/locale/locale.it.ini +2 -0
- pygpt_net/data/locale/locale.pl.ini +3 -1
- pygpt_net/data/locale/locale.uk.ini +2 -0
- pygpt_net/data/locale/locale.zh.ini +2 -0
- pygpt_net/provider/core/config/patch.py +16 -0
- pygpt_net/ui/dialog/preset.py +1 -0
- pygpt_net/ui/layout/toolbox/image.py +2 -1
- pygpt_net/ui/layout/toolbox/indexes.py +2 -0
- pygpt_net/ui/layout/toolbox/video.py +5 -1
- pygpt_net/ui/main.py +1 -0
- pygpt_net/ui/widget/dialog/update.py +18 -7
- pygpt_net/ui/widget/draw/painter.py +238 -51
- pygpt_net/ui/widget/filesystem/explorer.py +82 -0
- pygpt_net/ui/widget/option/combo.py +179 -13
- {pygpt_net-2.7.0.dist-info → pygpt_net-2.7.2.dist-info}/METADATA +44 -4
- {pygpt_net-2.7.0.dist-info → pygpt_net-2.7.2.dist-info}/RECORD +37 -37
- {pygpt_net-2.7.0.dist-info → pygpt_net-2.7.2.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.0.dist-info → pygpt_net-2.7.2.dist-info}/WHEEL +0 -0
- {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.
|
|
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.
|
|
17
|
-
__build__ = "2025-12-
|
|
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"
|
pygpt_net/controller/ctx/ctx.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 !=
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
pygpt_net/core/image/image.py
CHANGED
|
@@ -152,7 +152,7 @@ class Image(QObject):
|
|
|
152
152
|
"""
|
|
153
153
|
return {
|
|
154
154
|
"type": "combo",
|
|
155
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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:
|
pygpt_net/core/video/video.py
CHANGED
|
@@ -276,7 +276,7 @@ class Video(QObject):
|
|
|
276
276
|
"""
|
|
277
277
|
return {
|
|
278
278
|
"type": "combo",
|
|
279
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"app.version": "2.7.
|
|
5
|
-
"updated_at": "2025-12-
|
|
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": [],
|