pygpt-net 2.7.7__py3-none-any.whl → 2.7.9__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 (98) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +5 -1
  4. pygpt_net/controller/assistant/batch.py +2 -2
  5. pygpt_net/controller/assistant/files.py +7 -6
  6. pygpt_net/controller/assistant/threads.py +0 -0
  7. pygpt_net/controller/chat/command.py +0 -0
  8. pygpt_net/controller/dialogs/confirm.py +35 -58
  9. pygpt_net/controller/lang/mapping.py +9 -9
  10. pygpt_net/controller/realtime/realtime.py +13 -1
  11. pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
  12. pygpt_net/controller/remote_store/remote_store.py +982 -13
  13. pygpt_net/core/command/command.py +0 -0
  14. pygpt_net/core/db/viewer.py +1 -1
  15. pygpt_net/core/realtime/worker.py +3 -1
  16. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  17. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  18. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  19. pygpt_net/core/remote_store/openai/store.py +5 -4
  20. pygpt_net/core/remote_store/remote_store.py +5 -1
  21. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  22. pygpt_net/core/remote_store/xai/files.py +225 -0
  23. pygpt_net/core/remote_store/xai/store.py +219 -0
  24. pygpt_net/data/config/config.json +10 -6
  25. pygpt_net/data/config/models.json +38 -22
  26. pygpt_net/data/config/settings.json +54 -1
  27. pygpt_net/data/icons/folder_eye.svg +1 -0
  28. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  29. pygpt_net/data/icons/folder_open.svg +1 -0
  30. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  31. pygpt_net/data/locale/locale.de.ini +4 -3
  32. pygpt_net/data/locale/locale.en.ini +14 -4
  33. pygpt_net/data/locale/locale.es.ini +4 -3
  34. pygpt_net/data/locale/locale.fr.ini +4 -3
  35. pygpt_net/data/locale/locale.it.ini +4 -3
  36. pygpt_net/data/locale/locale.pl.ini +5 -4
  37. pygpt_net/data/locale/locale.uk.ini +4 -3
  38. pygpt_net/data/locale/locale.zh.ini +4 -3
  39. pygpt_net/icons.qrc +4 -0
  40. pygpt_net/icons_rc.py +282 -138
  41. pygpt_net/provider/api/anthropic/__init__.py +2 -0
  42. pygpt_net/provider/api/anthropic/chat.py +84 -1
  43. pygpt_net/provider/api/anthropic/store.py +307 -0
  44. pygpt_net/provider/api/anthropic/stream.py +75 -0
  45. pygpt_net/provider/api/anthropic/worker/__init__.py +0 -0
  46. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  47. pygpt_net/provider/api/google/chat.py +59 -2
  48. pygpt_net/provider/api/google/realtime/client.py +70 -24
  49. pygpt_net/provider/api/google/realtime/realtime.py +48 -12
  50. pygpt_net/provider/api/google/store.py +124 -3
  51. pygpt_net/provider/api/google/stream.py +91 -24
  52. pygpt_net/provider/api/google/worker/importer.py +16 -28
  53. pygpt_net/provider/api/openai/assistants.py +2 -2
  54. pygpt_net/provider/api/openai/realtime/realtime.py +26 -6
  55. pygpt_net/provider/api/openai/store.py +4 -1
  56. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  57. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  58. pygpt_net/provider/api/x_ai/__init__.py +27 -6
  59. pygpt_net/provider/api/x_ai/audio.py +43 -11
  60. pygpt_net/provider/api/x_ai/chat.py +92 -4
  61. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  62. pygpt_net/provider/api/x_ai/realtime/client.py +1864 -0
  63. pygpt_net/provider/api/x_ai/realtime/realtime.py +213 -0
  64. pygpt_net/provider/api/x_ai/remote_tools.py +102 -1
  65. pygpt_net/provider/api/x_ai/store.py +610 -0
  66. pygpt_net/provider/api/x_ai/stream.py +30 -9
  67. pygpt_net/provider/api/x_ai/tools.py +51 -0
  68. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  69. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  70. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  71. pygpt_net/provider/core/config/patch.py +29 -3
  72. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  73. pygpt_net/provider/core/model/patch.py +49 -1
  74. pygpt_net/tools/image_viewer/tool.py +334 -34
  75. pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
  76. pygpt_net/ui/dialog/assistant.py +1 -1
  77. pygpt_net/ui/dialog/plugins.py +13 -5
  78. pygpt_net/ui/dialog/remote_store.py +552 -0
  79. pygpt_net/ui/dialogs.py +3 -5
  80. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  81. pygpt_net/ui/menu/tools.py +6 -13
  82. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  83. pygpt_net/ui/widget/element/button.py +4 -4
  84. pygpt_net/ui/widget/image/display.py +2 -2
  85. pygpt_net/ui/widget/lists/context.py +2 -2
  86. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/METADATA +14 -2
  87. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/RECORD +87 -75
  88. pygpt_net/controller/remote_store/google/store.py +0 -615
  89. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  90. pygpt_net/controller/remote_store/openai/store.py +0 -699
  91. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  92. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  93. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  94. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  95. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  96. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/LICENSE +0 -0
  97. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/WHEEL +0 -0
  98. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,7 @@
1
+ # dialog.py
2
+
3
+ # dialog.py
4
+
1
5
  #!/usr/bin/env python3
2
6
  # -*- coding: utf-8 -*-
3
7
  # ================================================== #
@@ -6,12 +10,24 @@
6
10
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
11
  # MIT License #
8
12
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2026.01.05 23:00:00 #
13
+ # Updated Date: 2026.01.06 19:00:00 #
10
14
  # ================================================== #
11
15
 
12
16
  from PySide6.QtCore import Qt, QPoint, QSize, QEvent
13
17
  from PySide6.QtGui import QAction, QIcon
14
- from PySide6.QtWidgets import QMenuBar, QVBoxLayout, QHBoxLayout, QSizePolicy, QScrollArea
18
+ from PySide6.QtWidgets import (
19
+ QMenuBar,
20
+ QVBoxLayout,
21
+ QHBoxLayout,
22
+ QSizePolicy,
23
+ QScrollArea,
24
+ QToolButton,
25
+ QPushButton,
26
+ QWidget,
27
+ QFrame,
28
+ QStyle,
29
+ QLabel,
30
+ )
15
31
 
16
32
  from pygpt_net.ui.widget.dialog.base import BaseDialog
17
33
  from pygpt_net.ui.widget.image.display import ImageLabel
@@ -56,7 +72,18 @@ class DialogSpawner:
56
72
  row.addWidget(scroll)
57
73
 
58
74
  layout = QVBoxLayout()
75
+ # remove extra outer margins to bring the toolbar closer to the top/menu bar
76
+ layout.setContentsMargins(0, 0, 0, 0)
77
+ layout.setSpacing(0)
78
+
79
+ # full-width toolbar frame; icons cluster inside stays compact on the left
80
+ toolbar = dialog.setup_toolbar()
81
+ layout.addWidget(toolbar)
82
+ # image area
59
83
  layout.addLayout(row)
84
+ # bottom status bar
85
+ statusbar = dialog.setup_statusbar()
86
+ layout.addWidget(statusbar)
60
87
 
61
88
  dialog.append_layout(layout)
62
89
  dialog.source = source
@@ -98,6 +125,12 @@ class ImageViewerDialog(BaseDialog):
98
125
  self._icon_save = QIcon(":/icons/save.svg")
99
126
  self._icon_logout = QIcon(":/icons/logout.svg")
100
127
 
128
+ # toolbar icons
129
+ self._icon_prev = QIcon(":/icons/back.svg")
130
+ self._icon_next = QIcon(":/icons/forward.svg")
131
+ self._icon_copy = QIcon(":/icons/copy.svg")
132
+ self._icon_fullscreen = QIcon(":/icons/fullscreen.svg")
133
+
101
134
  # zoom / pan state
102
135
  self._zoom_mode = 'fit' # 'fit' or 'manual'
103
136
  self._zoom_factor = 1.0 # current manual factor (image space)
@@ -112,6 +145,21 @@ class ImageViewerDialog(BaseDialog):
112
145
  self._max_widget_dim = 32768 # max single dimension (px) for the view widget
113
146
  self._max_total_pixels = 80_000_000 # max total pixels of the view widget (about 80MP)
114
147
 
148
+ # buttons map to keep enabled state in sync with actions
149
+ self._tb_btns = {}
150
+
151
+ # status bar widgets and metadata
152
+ self.status_bar = None
153
+ self.lbl_index = None
154
+ self.lbl_resolution = None
155
+ self.lbl_zoom = None
156
+ self.lbl_extra = None
157
+
158
+ self._meta_index = 0 # 1-based index of file in dir
159
+ self._meta_total = 0 # total files in dir
160
+ self._meta_img_size = QSize() # original image size
161
+ self._meta_file_size = "" # formatted file size string
162
+
115
163
  def append_layout(self, layout):
116
164
  """
117
165
  Update layout
@@ -153,6 +201,8 @@ class ImageViewerDialog(BaseDialog):
153
201
  self._fit_factor = self._compute_fit_factor(src.size(), target_size)
154
202
  self._last_src_key = key
155
203
  self._last_target_size = target_size
204
+ # update status bar to reflect new zoom and display size
205
+ self._refresh_statusbar()
156
206
  super(ImageViewerDialog, self).resizeEvent(event)
157
207
 
158
208
  def setup_menu(self) -> QMenuBar:
@@ -197,6 +247,196 @@ class ImageViewerDialog(BaseDialog):
197
247
 
198
248
  return self.menu_bar
199
249
 
250
+ def setup_toolbar(self) -> QFrame:
251
+ """
252
+ Setup top toolbar.
253
+ Full-width frame (Expanding), with a fixed-size icons strip on the left that does not stretch.
254
+ """
255
+ bar = QFrame(self)
256
+ bar.setObjectName("image_viewer_toolbar")
257
+ bar.setFrameShape(QFrame.NoFrame)
258
+ bar.setMinimumHeight(35)
259
+ bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) # full width, fixed height
260
+
261
+ # scoped minimal style for icon-only QPushButtons
262
+ bar.setStyleSheet(
263
+ """
264
+ #image_viewer_toolbar {
265
+ border: none;
266
+ }
267
+ #image_viewer_toolbar QPushButton {
268
+ border: none;
269
+ padding: 0;
270
+ }
271
+ #image_viewer_toolbar QPushButton:disabled {
272
+ opacity: 0.5;
273
+ }
274
+ """
275
+ )
276
+
277
+ # main container layout that fills the width
278
+ lay = QHBoxLayout(bar)
279
+ lay.setContentsMargins(0, 0, 0, 0)
280
+ lay.setSpacing(0)
281
+
282
+ # a compact, non-expanding strip that holds the icons
283
+ strip = QWidget(bar)
284
+ strip.setObjectName("image_viewer_toolbar_strip")
285
+ strip.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
286
+
287
+ strip_lay = QHBoxLayout(strip)
288
+ strip_lay.setContentsMargins(6, 6, 6, 6)
289
+ strip_lay.setSpacing(6)
290
+
291
+ # DPI-aware icon and button sizes; explicit fixed size prevents vertical squashing
292
+ pm = self.style().pixelMetric(QStyle.PM_ToolBarIconSize)
293
+ if not isinstance(pm, int) or pm <= 0:
294
+ pm = self.style().pixelMetric(QStyle.PM_SmallIconSize)
295
+ if not isinstance(pm, int) or pm <= 0:
296
+ pm = 20
297
+ # reduce icon height by ~5px (and width to keep square) as requested
298
+ icon_px = max(16, pm - 5)
299
+ btn_px = max(29, icon_px + 10) # keep compact square buttons; min reduced by 5px
300
+ icon_size = QSize(icon_px, icon_px)
301
+
302
+ def bind_button_to_action(btn: QPushButton, action: QAction):
303
+ """Keep button in sync with the given action."""
304
+ btn.setIcon(action.icon())
305
+ btn.setToolTip(action.toolTip())
306
+ btn.setEnabled(action.isEnabled())
307
+ action.changed.connect(lambda: (
308
+ btn.setIcon(action.icon()),
309
+ btn.setToolTip(action.toolTip()),
310
+ btn.setEnabled(action.isEnabled())
311
+ ))
312
+ btn.clicked.connect(action.trigger)
313
+
314
+ def mk_btn(key: str, icon: QIcon, tooltip: str) -> QPushButton:
315
+ """Create a fixed-size square button with icon-only look."""
316
+ action = QAction(icon, "", self)
317
+ action.setToolTip(tooltip)
318
+ self.actions[key] = action
319
+
320
+ btn = QPushButton(strip)
321
+ btn.setObjectName("ivtb")
322
+ btn.setCursor(Qt.PointingHandCursor)
323
+ btn.setFocusPolicy(Qt.NoFocus)
324
+ btn.setFlat(True)
325
+ btn.setText("")
326
+ btn.setIcon(icon)
327
+ btn.setIconSize(icon_size)
328
+ btn.setFixedSize(btn_px, btn_px)
329
+
330
+ bind_button_to_action(btn, action)
331
+ self._tb_btns[key] = btn
332
+
333
+ strip_lay.addWidget(btn)
334
+ return btn
335
+
336
+ def add_sep():
337
+ sep = QFrame(strip)
338
+ sep.setFrameShape(QFrame.VLine)
339
+ sep.setFrameShadow(QFrame.Sunken)
340
+ sep.setFixedHeight(btn_px - 4)
341
+ strip_lay.addWidget(sep)
342
+
343
+ # prev
344
+ mk_btn("tb_prev", self._icon_prev, "Previous image")
345
+ self.actions["tb_prev"].triggered.connect(
346
+ lambda checked=False: self.window.tools.get("viewer").prev_by_id(self.id)
347
+ )
348
+
349
+ # next
350
+ mk_btn("tb_next", self._icon_next, "Next image")
351
+ self.actions["tb_next"].triggered.connect(
352
+ lambda checked=False: self.window.tools.get("viewer").next_by_id(self.id)
353
+ )
354
+
355
+ add_sep()
356
+
357
+ # open in directory
358
+ mk_btn("tb_open_dir", self._icon_folder, "Open in directory")
359
+ self.actions["tb_open_dir"].triggered.connect(
360
+ lambda checked=False: self.window.tools.get("viewer").open_dir_by_id(self.id)
361
+ )
362
+
363
+ # save as...
364
+ mk_btn("tb_save_as", self._icon_save, "Save as...")
365
+ self.actions["tb_save_as"].triggered.connect(
366
+ lambda checked=False: self.window.tools.get("viewer").save_by_id(self.id)
367
+ )
368
+
369
+ # copy
370
+ mk_btn("tb_copy", self._icon_copy, "Copy to clipboard")
371
+ self.actions["tb_copy"].triggered.connect(
372
+ lambda checked=False: self.window.tools.get("viewer").copy_by_id(self.id)
373
+ )
374
+
375
+ add_sep()
376
+
377
+ # fullscreen
378
+ mk_btn("tb_fullscreen", self._icon_fullscreen, "Toggle fullscreen")
379
+ self.actions["tb_fullscreen"].triggered.connect(
380
+ lambda checked=False: self.window.tools.get("viewer").toggle_fullscreen_by_id(self.id)
381
+ )
382
+
383
+ # initial enabled state; actual state adjusted on image load
384
+ for k in ("tb_prev", "tb_next", "tb_open_dir", "tb_save_as", "tb_copy", "tb_fullscreen"):
385
+ if k in self.actions:
386
+ self.actions[k].setEnabled(k == "tb_fullscreen")
387
+
388
+ # compose: icons strip on the left, stretch fills the rest so frame spans full width
389
+ lay.addWidget(strip)
390
+ lay.addStretch(1)
391
+
392
+ return bar
393
+
394
+ def setup_statusbar(self) -> QFrame:
395
+ """
396
+ Setup bottom status bar with four columns:
397
+ 1) file index in directory (e.g., 1/13)
398
+ 2) displayed size (e.g., 1280x720 px)
399
+ 3) current zoom in percent
400
+ 4) original image resolution and file size on the right
401
+ """
402
+ bar = QFrame(self)
403
+ bar.setObjectName("image_viewer_statusbar")
404
+ bar.setFrameShape(QFrame.NoFrame)
405
+ bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
406
+ bar.setStyleSheet("""
407
+ #image_viewer_statusbar {
408
+ border: none;
409
+ }
410
+ #image_viewer_statusbar QLabel {
411
+ color: #686868;
412
+ font-size: 12px;
413
+ padding: 4px 8px;
414
+ }
415
+ """)
416
+
417
+ lay = QHBoxLayout(bar)
418
+ lay.setContentsMargins(0, 0, 0, 0)
419
+ lay.setSpacing(0)
420
+
421
+ self.lbl_index = QLabel("-", bar)
422
+ self.lbl_resolution = QLabel("-", bar)
423
+ self.lbl_zoom = QLabel("-", bar)
424
+ self.lbl_extra = QLabel("-", bar)
425
+
426
+ # make columns distribute evenly; order updated to show current (displayed) on the left,
427
+ # and original with file size on the right: index | displayed | zoom | original (+ size)
428
+ for w in (self.lbl_index, self.lbl_extra, self.lbl_zoom, self.lbl_resolution):
429
+ w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
430
+
431
+ lay.addWidget(self.lbl_index, 1, Qt.AlignLeft)
432
+ lay.addWidget(self.lbl_extra, 1, Qt.AlignLeft)
433
+ lay.addWidget(self.lbl_zoom, 1, Qt.AlignCenter)
434
+ lay.addWidget(self.lbl_resolution, 1, Qt.AlignRight)
435
+
436
+ self.status_bar = bar
437
+ self._refresh_statusbar()
438
+ return bar
439
+
200
440
  # =========================
201
441
  # Zoom / pan implementation
202
442
  # =========================
@@ -266,10 +506,8 @@ class ImageViewerDialog(BaseDialog):
266
506
  )
267
507
  self._zoom_factor = self._fit_factor
268
508
  self._zoom_mode = 'manual'
269
- # switch to manual rendering path: original pixmap + scaled contents
270
509
  self.scroll_area.setWidgetResizable(False)
271
510
  self.pixmap.setScaledContents(True)
272
- # ensure we display original image for better performance (no giant intermediate pixmaps)
273
511
  self.pixmap.setPixmap(self.source.pixmap())
274
512
 
275
513
  old_w = max(1, self.pixmap.width())
@@ -287,10 +525,8 @@ class ImageViewerDialog(BaseDialog):
287
525
  return False
288
526
 
289
527
  step = self._zoom_step if angle > 0 else (1.0 / self._zoom_step)
290
- # compute tentative new factor and clamp by hard min/max and size guards
291
528
  tentative = self._zoom_factor * step
292
529
  tentative = max(self._min_zoom, min(self._max_zoom, tentative))
293
- # apply size-based guards to avoid extremely huge widget sizes
294
530
  tentative = self._clamp_factor_by_size(tentative)
295
531
 
296
532
  if abs(tentative - self._zoom_factor) < 1e-9:
@@ -301,7 +537,6 @@ class ImageViewerDialog(BaseDialog):
301
537
  hbar = self.scroll_area.horizontalScrollBar()
302
538
  vbar = self.scroll_area.verticalScrollBar()
303
539
 
304
- # position in content coords before zoom (keep point under cursor stable)
305
540
  content_x = hbar.value() + vp_pos.x()
306
541
  content_y = vbar.value() + vp_pos.y()
307
542
  rx = content_x / float(old_w)
@@ -365,23 +600,19 @@ class ImageViewerDialog(BaseDialog):
365
600
  iw = max(1, src.width())
366
601
  ih = max(1, src.height())
367
602
 
368
- # only guard when zooming in; zooming out should not be limited by these caps
369
603
  if factor <= 1.0:
370
604
  return factor
371
605
 
372
- # desired size
373
606
  dw = iw * factor
374
607
  dh = ih * factor
375
608
 
376
609
  scale = 1.0
377
610
 
378
- # total pixel cap
379
611
  total = dw * dh
380
612
  if total > self._max_total_pixels:
381
613
  from math import sqrt
382
614
  scale = min(scale, sqrt(self._max_total_pixels / float(total)))
383
615
 
384
- # dimension caps
385
616
  if dw * scale > self._max_widget_dim:
386
617
  scale = min(scale, self._max_widget_dim / float(dw))
387
618
  if dh * scale > self._max_widget_dim:
@@ -394,10 +625,6 @@ class ImageViewerDialog(BaseDialog):
394
625
  def _set_scaled_pixmap_by_factor(self, factor: float):
395
626
  """
396
627
  Scale and display the image using provided factor relative to the original image.
397
- In manual mode this avoids allocating giant intermediate QPixmaps by:
398
- - drawing the original pixmap;
399
- - enabling scaled contents;
400
- - resizing the label to the required size.
401
628
  """
402
629
  if not self._has_image():
403
630
  return
@@ -406,31 +633,30 @@ class ImageViewerDialog(BaseDialog):
406
633
  iw = max(1, src.width())
407
634
  ih = max(1, src.height())
408
635
 
409
- # target size based on factor (KeepAspectRatio preserved by proportional math)
410
636
  new_w = max(1, int(round(iw * factor)))
411
637
  new_h = max(1, int(round(ih * factor)))
412
638
 
413
- # enforce guards once more to be safe
414
639
  guarded_factor = self._clamp_factor_by_size(factor)
415
640
  if abs(guarded_factor - factor) > 1e-9:
416
641
  new_w = max(1, int(round(iw * guarded_factor)))
417
642
  new_h = max(1, int(round(ih * guarded_factor)))
418
- self._zoom_factor = guarded_factor # keep internal factor in sync
643
+ self._zoom_factor = guarded_factor
419
644
 
420
645
  if self._zoom_mode == 'manual':
421
- # ensure manual path uses original pixmap and scaled contents
422
646
  if self.pixmap.pixmap() is None or self.pixmap.pixmap().cacheKey() != src.cacheKey():
423
647
  self.pixmap.setPixmap(src)
424
648
  self.pixmap.setScaledContents(True)
425
649
  self.pixmap.resize(new_w, new_h)
426
650
  else:
427
- # fallback (not expected here): keep classic high-quality scaling
428
651
  scaled = src.scaled(new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
429
652
  self.pixmap.setScaledContents(False)
430
653
  self.pixmap.setPixmap(scaled)
431
654
  if self.scroll_area is not None:
432
655
  self.scroll_area.setWidgetResizable(True)
433
656
 
657
+ # keep status bar in sync with zoom and current display size
658
+ self._refresh_statusbar()
659
+
434
660
  def _event_pos(self, event) -> QPoint:
435
661
  """
436
662
  Extract integer QPoint from mouse/touchpad event position (supports QPointF in 6.9+).
@@ -439,4 +665,74 @@ class ImageViewerDialog(BaseDialog):
439
665
  return event.position().toPoint()
440
666
  if hasattr(event, "pos"):
441
667
  return event.pos()
442
- return QPoint(0, 0)
668
+ return QPoint(0, 0)
669
+
670
+ # =========================
671
+ # Status bar helpers
672
+ # =========================
673
+
674
+ def set_status_meta(self, index: int = None, total: int = None, img_size: QSize = None, file_size_str: str = None):
675
+ """
676
+ Update persistent metadata shown in status bar (index/total, original image size, file size).
677
+ """
678
+ if index is not None:
679
+ self._meta_index = max(0, int(index))
680
+ if total is not None:
681
+ self._meta_total = max(0, int(total))
682
+ if img_size is not None:
683
+ self._meta_img_size = QSize(max(0, img_size.width()), max(0, img_size.height()))
684
+ if file_size_str is not None:
685
+ self._meta_file_size = file_size_str or ""
686
+ self._refresh_statusbar()
687
+
688
+ def _current_zoom_percent(self) -> int:
689
+ """
690
+ Return current zoom in percent, based on mode.
691
+ """
692
+ factor = self._fit_factor if self._zoom_mode == 'fit' else self._zoom_factor
693
+ try:
694
+ return max(1, int(round(factor * 100.0)))
695
+ except Exception:
696
+ return 100
697
+
698
+ def _displayed_size(self) -> QSize:
699
+ """
700
+ Return the currently displayed image size in pixels.
701
+ """
702
+ if self._zoom_mode == 'manual' and self.pixmap is not None:
703
+ return QSize(max(0, self.pixmap.width()), max(0, self.pixmap.height()))
704
+ # in fit mode or when using a scaled pixmap
705
+ if self.pixmap is not None and self.pixmap.pixmap() is not None:
706
+ pm = self.pixmap.pixmap()
707
+ return QSize(max(0, pm.width()), max(0, pm.height()))
708
+ return QSize(0, 0)
709
+
710
+ def _refresh_statusbar(self):
711
+ """
712
+ Refresh all status bar labels from current metadata and state.
713
+ """
714
+ if self.lbl_index is None:
715
+ return
716
+
717
+ # 1) index/total
718
+ if self._meta_index > 0 and self._meta_total > 0:
719
+ self.lbl_index.setText(f"{self._meta_index}/{self._meta_total}")
720
+ else:
721
+ self.lbl_index.setText("-")
722
+
723
+ # 2) left: displayed size only
724
+ dsz = self._displayed_size()
725
+ dsz_txt = f"{dsz.width()}x{dsz.height()} px" if dsz.width() > 0 and dsz.height() > 0 else "-"
726
+ self.lbl_extra.setText(dsz_txt)
727
+
728
+ # 3) zoom percent (center)
729
+ self.lbl_zoom.setText(f"{self._current_zoom_percent()}%")
730
+
731
+ # 4) right: original resolution with file size appended
732
+ if self._meta_img_size.width() > 0 and self._meta_img_size.height() > 0:
733
+ right_txt = f"{self._meta_img_size.width()}x{self._meta_img_size.height()} px"
734
+ if self._meta_file_size:
735
+ right_txt += f" | {self._meta_file_size}"
736
+ self.lbl_resolution.setText(right_txt)
737
+ else:
738
+ self.lbl_resolution.setText("-")
@@ -46,7 +46,7 @@ class Assistant(BaseConfigDialog):
46
46
  self.window.ui.nodes['assistant.btn.store.editor'] = QPushButton(QIcon(":/icons/db.svg"), "")
47
47
  self.window.ui.nodes['assistant.btn.store.editor'].setToolTip(trans('dialog.remote_store.openai'))
48
48
  self.window.ui.nodes['assistant.btn.store.editor'].clicked.connect(
49
- lambda: self.window.controller.remote_store.openai.toggle_editor()
49
+ lambda: self.window.controller.remote_store.toggle_editor(provider="openai")
50
50
  )
51
51
 
52
52
  self.window.ui.nodes['assistant.btn.store.editor'].setAutoDefault(False)
@@ -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: 2026.01.01 15:00:00 #
9
+ # Updated Date: 2026.01.06 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
@@ -176,8 +176,12 @@ class Plugins:
176
176
  desc_txt = plugin.description
177
177
  if plugin.use_locale:
178
178
  domain = f"plugin.{plugin.id}"
179
- name_txt = trans('plugin.name', False, domain)
180
- desc_txt = trans('plugin.description', False, domain)
179
+ translated_name = trans('plugin.name', False, domain)
180
+ translated_desc = trans('plugin.description', False, domain)
181
+ if translated_name != 'plugin.name':
182
+ name_txt = translated_name
183
+ if translated_desc != 'plugin.description':
184
+ desc_txt = translated_desc
181
185
 
182
186
  self.window.ui.nodes[desc_key] = DescLabel(desc_txt)
183
187
  self.window.ui.nodes[desc_key].setAlignment(Qt.AlignCenter)
@@ -423,8 +427,12 @@ class Plugins:
423
427
  # translate if localization is enabled
424
428
  if plugin.use_locale and allow_locale:
425
429
  domain = f"plugin.{plugin.id}"
426
- txt_title = trans(f"{key}.label", False, domain)
427
- txt_desc = trans(f"{key}.description", False, domain)
430
+ translated_label = trans(f"{key}.label", False, domain)
431
+ translated_description = trans(f"{key}.description", False, domain)
432
+ if translated_label != f"{key}.label":
433
+ txt_title = translated_label
434
+ if translated_description != f"{key}.description":
435
+ txt_desc = translated_description
428
436
  # txt_tooltip = trans(f"{key}.tooltip", False, domain)
429
437
 
430
438
  # if empty tooltip then use description