pygpt-net 2.7.6__py3-none-any.whl → 2.7.8__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 (120) hide show
  1. pygpt_net/CHANGELOG.txt +13 -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/chat/remote_tools.py +3 -9
  9. pygpt_net/controller/chat/stream.py +2 -2
  10. pygpt_net/controller/chat/{handler/worker.py → stream_worker.py} +13 -35
  11. pygpt_net/controller/dialogs/confirm.py +35 -58
  12. pygpt_net/controller/lang/mapping.py +9 -9
  13. pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
  14. pygpt_net/controller/remote_store/remote_store.py +982 -13
  15. pygpt_net/core/command/command.py +0 -0
  16. pygpt_net/core/db/viewer.py +1 -1
  17. pygpt_net/core/debug/models.py +2 -2
  18. pygpt_net/core/realtime/worker.py +3 -1
  19. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  20. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  21. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  22. pygpt_net/core/remote_store/openai/store.py +5 -4
  23. pygpt_net/core/remote_store/remote_store.py +5 -1
  24. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  25. pygpt_net/core/remote_store/xai/files.py +225 -0
  26. pygpt_net/core/remote_store/xai/store.py +219 -0
  27. pygpt_net/data/config/config.json +18 -5
  28. pygpt_net/data/config/models.json +193 -4
  29. pygpt_net/data/config/settings.json +179 -36
  30. pygpt_net/data/icons/folder_eye.svg +1 -0
  31. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  32. pygpt_net/data/icons/folder_open.svg +1 -0
  33. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  34. pygpt_net/data/locale/locale.de.ini +6 -3
  35. pygpt_net/data/locale/locale.en.ini +46 -12
  36. pygpt_net/data/locale/locale.es.ini +6 -3
  37. pygpt_net/data/locale/locale.fr.ini +6 -3
  38. pygpt_net/data/locale/locale.it.ini +6 -3
  39. pygpt_net/data/locale/locale.pl.ini +7 -4
  40. pygpt_net/data/locale/locale.uk.ini +6 -3
  41. pygpt_net/data/locale/locale.zh.ini +6 -3
  42. pygpt_net/icons.qrc +4 -0
  43. pygpt_net/icons_rc.py +282 -138
  44. pygpt_net/plugin/cmd_mouse_control/worker.py +2 -1
  45. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +2 -1
  46. pygpt_net/provider/api/anthropic/__init__.py +10 -3
  47. pygpt_net/provider/api/anthropic/chat.py +342 -11
  48. pygpt_net/provider/api/anthropic/computer.py +844 -0
  49. pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
  50. pygpt_net/provider/api/anthropic/store.py +307 -0
  51. pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +99 -10
  52. pygpt_net/provider/api/anthropic/tools.py +32 -77
  53. pygpt_net/provider/api/anthropic/utils.py +30 -0
  54. pygpt_net/{controller/chat/handler → provider/api/anthropic/worker}/__init__.py +0 -0
  55. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  56. pygpt_net/provider/api/google/chat.py +62 -9
  57. pygpt_net/provider/api/google/store.py +124 -3
  58. pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +92 -25
  59. pygpt_net/provider/api/google/utils.py +185 -0
  60. pygpt_net/provider/api/google/worker/importer.py +16 -28
  61. pygpt_net/provider/api/langchain/__init__.py +0 -0
  62. pygpt_net/{controller/chat/handler/langchain_stream.py → provider/api/langchain/stream.py} +1 -1
  63. pygpt_net/provider/api/llama_index/__init__.py +0 -0
  64. pygpt_net/{controller/chat/handler/llamaindex_stream.py → provider/api/llama_index/stream.py} +1 -1
  65. pygpt_net/provider/api/openai/assistants.py +2 -2
  66. pygpt_net/provider/api/openai/image.py +2 -2
  67. pygpt_net/provider/api/openai/store.py +4 -1
  68. pygpt_net/{controller/chat/handler/openai_stream.py → provider/api/openai/stream.py} +1 -1
  69. pygpt_net/provider/api/openai/utils.py +69 -3
  70. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  71. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  72. pygpt_net/provider/api/x_ai/__init__.py +138 -15
  73. pygpt_net/provider/api/x_ai/audio.py +43 -11
  74. pygpt_net/provider/api/x_ai/chat.py +92 -4
  75. pygpt_net/provider/api/x_ai/image.py +149 -47
  76. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  77. pygpt_net/provider/api/x_ai/realtime/client.py +1825 -0
  78. pygpt_net/provider/api/x_ai/realtime/realtime.py +198 -0
  79. pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +183 -70
  80. pygpt_net/provider/api/x_ai/responses.py +507 -0
  81. pygpt_net/provider/api/x_ai/store.py +610 -0
  82. pygpt_net/{controller/chat/handler/xai_stream.py → provider/api/x_ai/stream.py} +42 -10
  83. pygpt_net/provider/api/x_ai/tools.py +59 -8
  84. pygpt_net/{controller/chat/handler → provider/api/x_ai}/utils.py +1 -2
  85. pygpt_net/provider/api/x_ai/vision.py +1 -4
  86. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  87. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  88. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  89. pygpt_net/provider/core/config/patch.py +39 -3
  90. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  91. pygpt_net/provider/core/model/patch.py +39 -1
  92. pygpt_net/tools/image_viewer/tool.py +334 -34
  93. pygpt_net/tools/image_viewer/ui/dialogs.py +319 -22
  94. pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
  95. pygpt_net/tools/text_editor/ui/widgets.py +0 -0
  96. pygpt_net/ui/dialog/assistant.py +1 -1
  97. pygpt_net/ui/dialog/plugins.py +13 -5
  98. pygpt_net/ui/dialog/remote_store.py +552 -0
  99. pygpt_net/ui/dialogs.py +3 -5
  100. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  101. pygpt_net/ui/menu/tools.py +6 -13
  102. pygpt_net/ui/widget/dialog/base.py +16 -5
  103. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  104. pygpt_net/ui/widget/element/button.py +4 -4
  105. pygpt_net/ui/widget/image/display.py +2 -2
  106. pygpt_net/ui/widget/lists/context.py +2 -2
  107. pygpt_net/ui/widget/textarea/editor.py +0 -0
  108. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +15 -2
  109. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +107 -89
  110. pygpt_net/controller/remote_store/google/store.py +0 -615
  111. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  112. pygpt_net/controller/remote_store/openai/store.py +0 -699
  113. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  114. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  115. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  116. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  117. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  118. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
  119. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
  120. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.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.03 17: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
@@ -37,8 +53,9 @@ class DialogSpawner:
37
53
  :return: BaseDialog instance
38
54
  """
39
55
  dialog = ImageViewerDialog(self.window, self.id)
40
- dialog.disable_geometry_store = True # disable geometry store
56
+ dialog.disable_geometry_store = False
41
57
  dialog.id = id
58
+ dialog.shared_id = self.id
42
59
 
43
60
  source = ImageLabel(dialog, self.path)
44
61
  source.setVisible(False)
@@ -55,7 +72,18 @@ class DialogSpawner:
55
72
  row.addWidget(scroll)
56
73
 
57
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
58
83
  layout.addLayout(row)
84
+ # bottom status bar
85
+ statusbar = dialog.setup_statusbar()
86
+ layout.addWidget(statusbar)
59
87
 
60
88
  dialog.append_layout(layout)
61
89
  dialog.source = source
@@ -97,6 +125,12 @@ class ImageViewerDialog(BaseDialog):
97
125
  self._icon_save = QIcon(":/icons/save.svg")
98
126
  self._icon_logout = QIcon(":/icons/logout.svg")
99
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
+
100
134
  # zoom / pan state
101
135
  self._zoom_mode = 'fit' # 'fit' or 'manual'
102
136
  self._zoom_factor = 1.0 # current manual factor (image space)
@@ -111,6 +145,21 @@ class ImageViewerDialog(BaseDialog):
111
145
  self._max_widget_dim = 32768 # max single dimension (px) for the view widget
112
146
  self._max_total_pixels = 80_000_000 # max total pixels of the view widget (about 80MP)
113
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
+
114
163
  def append_layout(self, layout):
115
164
  """
116
165
  Update layout
@@ -152,6 +201,8 @@ class ImageViewerDialog(BaseDialog):
152
201
  self._fit_factor = self._compute_fit_factor(src.size(), target_size)
153
202
  self._last_src_key = key
154
203
  self._last_target_size = target_size
204
+ # update status bar to reflect new zoom and display size
205
+ self._refresh_statusbar()
155
206
  super(ImageViewerDialog, self).resizeEvent(event)
156
207
 
157
208
  def setup_menu(self) -> QMenuBar:
@@ -196,6 +247,196 @@ class ImageViewerDialog(BaseDialog):
196
247
 
197
248
  return self.menu_bar
198
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
+
199
440
  # =========================
200
441
  # Zoom / pan implementation
201
442
  # =========================
@@ -265,10 +506,8 @@ class ImageViewerDialog(BaseDialog):
265
506
  )
266
507
  self._zoom_factor = self._fit_factor
267
508
  self._zoom_mode = 'manual'
268
- # switch to manual rendering path: original pixmap + scaled contents
269
509
  self.scroll_area.setWidgetResizable(False)
270
510
  self.pixmap.setScaledContents(True)
271
- # ensure we display original image for better performance (no giant intermediate pixmaps)
272
511
  self.pixmap.setPixmap(self.source.pixmap())
273
512
 
274
513
  old_w = max(1, self.pixmap.width())
@@ -286,10 +525,8 @@ class ImageViewerDialog(BaseDialog):
286
525
  return False
287
526
 
288
527
  step = self._zoom_step if angle > 0 else (1.0 / self._zoom_step)
289
- # compute tentative new factor and clamp by hard min/max and size guards
290
528
  tentative = self._zoom_factor * step
291
529
  tentative = max(self._min_zoom, min(self._max_zoom, tentative))
292
- # apply size-based guards to avoid extremely huge widget sizes
293
530
  tentative = self._clamp_factor_by_size(tentative)
294
531
 
295
532
  if abs(tentative - self._zoom_factor) < 1e-9:
@@ -300,7 +537,6 @@ class ImageViewerDialog(BaseDialog):
300
537
  hbar = self.scroll_area.horizontalScrollBar()
301
538
  vbar = self.scroll_area.verticalScrollBar()
302
539
 
303
- # position in content coords before zoom (keep point under cursor stable)
304
540
  content_x = hbar.value() + vp_pos.x()
305
541
  content_y = vbar.value() + vp_pos.y()
306
542
  rx = content_x / float(old_w)
@@ -364,23 +600,19 @@ class ImageViewerDialog(BaseDialog):
364
600
  iw = max(1, src.width())
365
601
  ih = max(1, src.height())
366
602
 
367
- # only guard when zooming in; zooming out should not be limited by these caps
368
603
  if factor <= 1.0:
369
604
  return factor
370
605
 
371
- # desired size
372
606
  dw = iw * factor
373
607
  dh = ih * factor
374
608
 
375
609
  scale = 1.0
376
610
 
377
- # total pixel cap
378
611
  total = dw * dh
379
612
  if total > self._max_total_pixels:
380
613
  from math import sqrt
381
614
  scale = min(scale, sqrt(self._max_total_pixels / float(total)))
382
615
 
383
- # dimension caps
384
616
  if dw * scale > self._max_widget_dim:
385
617
  scale = min(scale, self._max_widget_dim / float(dw))
386
618
  if dh * scale > self._max_widget_dim:
@@ -393,10 +625,6 @@ class ImageViewerDialog(BaseDialog):
393
625
  def _set_scaled_pixmap_by_factor(self, factor: float):
394
626
  """
395
627
  Scale and display the image using provided factor relative to the original image.
396
- In manual mode this avoids allocating giant intermediate QPixmaps by:
397
- - drawing the original pixmap;
398
- - enabling scaled contents;
399
- - resizing the label to the required size.
400
628
  """
401
629
  if not self._has_image():
402
630
  return
@@ -405,31 +633,30 @@ class ImageViewerDialog(BaseDialog):
405
633
  iw = max(1, src.width())
406
634
  ih = max(1, src.height())
407
635
 
408
- # target size based on factor (KeepAspectRatio preserved by proportional math)
409
636
  new_w = max(1, int(round(iw * factor)))
410
637
  new_h = max(1, int(round(ih * factor)))
411
638
 
412
- # enforce guards once more to be safe
413
639
  guarded_factor = self._clamp_factor_by_size(factor)
414
640
  if abs(guarded_factor - factor) > 1e-9:
415
641
  new_w = max(1, int(round(iw * guarded_factor)))
416
642
  new_h = max(1, int(round(ih * guarded_factor)))
417
- self._zoom_factor = guarded_factor # keep internal factor in sync
643
+ self._zoom_factor = guarded_factor
418
644
 
419
645
  if self._zoom_mode == 'manual':
420
- # ensure manual path uses original pixmap and scaled contents
421
646
  if self.pixmap.pixmap() is None or self.pixmap.pixmap().cacheKey() != src.cacheKey():
422
647
  self.pixmap.setPixmap(src)
423
648
  self.pixmap.setScaledContents(True)
424
649
  self.pixmap.resize(new_w, new_h)
425
650
  else:
426
- # fallback (not expected here): keep classic high-quality scaling
427
651
  scaled = src.scaled(new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
428
652
  self.pixmap.setScaledContents(False)
429
653
  self.pixmap.setPixmap(scaled)
430
654
  if self.scroll_area is not None:
431
655
  self.scroll_area.setWidgetResizable(True)
432
656
 
657
+ # keep status bar in sync with zoom and current display size
658
+ self._refresh_statusbar()
659
+
433
660
  def _event_pos(self, event) -> QPoint:
434
661
  """
435
662
  Extract integer QPoint from mouse/touchpad event position (supports QPointF in 6.9+).
@@ -438,4 +665,74 @@ class ImageViewerDialog(BaseDialog):
438
665
  return event.position().toPoint()
439
666
  if hasattr(event, "pos"):
440
667
  return event.pos()
441
- 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("-")
@@ -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: 2024.03.26 15:00:00 #
9
+ # Updated Date: 2026.01.05 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtWidgets import QPushButton, QHBoxLayout, QVBoxLayout
@@ -63,8 +63,9 @@ class DialogSpawner:
63
63
  layout.addLayout(bottom_layout)
64
64
 
65
65
  dialog = EditorFileDialog(self.window)
66
- dialog.disable_geometry_store = True # disable geometry store
66
+ dialog.disable_geometry_store = False
67
67
  dialog.id = id
68
+ dialog.shared_id = "text-edit"
68
69
  dialog.append_layout(layout)
69
70
  dialog.setWindowTitle("Text editor")
70
71
 
File without changes
@@ -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