Skip to content

Commit 96860e7

Browse files
committed
v3.0
1 parent fb24f38 commit 96860e7

38 files changed

+1296
-881
lines changed

UI/collapsible_sidebar.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
from PyQt6.QtWidgets import (
2+
QFrame, QVBoxLayout, QHBoxLayout, QPushButton,
3+
QLabel, QSpacerItem, QSizePolicy, QButtonGroup
4+
)
5+
from PyQt6.QtCore import (
6+
pyqtSignal, Qt, QPropertyAnimation, QParallelAnimationGroup, QEasingCurve, QSize
7+
)
8+
from PyQt6.QtGui import QIcon
9+
10+
class SidebarButton(QPushButton):
11+
"""
12+
Przycisk w sidebar.
13+
- Może być checkable
14+
- Tekst pojawia się po rozwinięciu sidebar
15+
"""
16+
def __init__(self, icon=None, text="", parent=None, checkable=False):
17+
super().__init__(parent)
18+
self._original_text = text
19+
self.setObjectName("SidebarButton")
20+
self.setCheckable(checkable)
21+
self.setMinimumHeight(30)
22+
23+
if icon is not None:
24+
self.setIcon(icon)
25+
self.setIconSize(QSize(26, 26))
26+
27+
self.show_text(False)
28+
29+
def show_text(self, visible: bool):
30+
"""Włącza/wyłącza oryginalny tekst (obok ikony)."""
31+
self.setText(self._original_text if visible else "")
32+
33+
34+
class CollapsibleSidebar(QFrame):
35+
"""
36+
Rozwijany sidebar (50 -> 200 px).
37+
38+
Parametr 'initial_mode' decyduje, czy mamy tylko
39+
podstawowe przyciski (InitialSetup) czy pełen zestaw (MainWindow).
40+
"""
41+
42+
# Sygnały do MainWindow / Initial
43+
sig_select_lettuce = pyqtSignal()
44+
sig_select_schej = pyqtSignal()
45+
sig_load_csv = pyqtSignal()
46+
sig_export_csv = pyqtSignal()
47+
sig_export_html = pyqtSignal()
48+
sig_export_png = pyqtSignal()
49+
sig_fire_mode = pyqtSignal()
50+
sig_colorize = pyqtSignal()
51+
sig_toggle_params = pyqtSignal()
52+
sig_documentation = pyqtSignal()
53+
sig_go_initial = pyqtSignal()
54+
55+
def __init__(self, parent=None, initial_mode=False):
56+
super().__init__(parent)
57+
self.setObjectName("CollapsibleSidebar")
58+
59+
self._collapsed_width = 50
60+
self._expanded_width = 200
61+
self._expanded = False
62+
63+
self.lettuce_icon_light = "assets/icons/light/lettuce_light.png"
64+
self.lettuce_icon_dark = "assets/icons/dark/lettuce_dark.png"
65+
self.schej_icon_light = "assets/icons/light/schej_light.png"
66+
self.schej_icon_dark = "assets/icons/dark/schej_dark.png"
67+
68+
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
69+
self.setMinimumWidth(self._collapsed_width)
70+
self.setMaximumWidth(self._collapsed_width)
71+
72+
# Layout
73+
self.main_layout = QVBoxLayout(self)
74+
self.main_layout.setContentsMargins(0, 0, 0, 0)
75+
self.main_layout.setSpacing(0)
76+
77+
# --- Pasek górny (toggle + label) ---
78+
top_widget = QFrame()
79+
top_layout = QHBoxLayout(top_widget)
80+
top_layout.setContentsMargins(0, 0, 0, 0)
81+
top_layout.setSpacing(5)
82+
83+
menu_icon = QIcon("assets/icons/light/menu_light.png")
84+
self.toggle_btn = QPushButton()
85+
self.toggle_btn.setObjectName("SidebarToggleBtn")
86+
self.toggle_btn.setIcon(menu_icon)
87+
self.toggle_btn.setIconSize(QSize(24, 24))
88+
self.toggle_btn.clicked.connect(self.toggle_sidebar)
89+
top_layout.addWidget(self.toggle_btn, alignment=Qt.AlignmentFlag.AlignLeft)
90+
91+
self.app_label = QLabel("Harmobot")
92+
self.app_label.setObjectName("SidebarAppLabel")
93+
self.app_label.setVisible(False)
94+
top_layout.addWidget(self.app_label, alignment=Qt.AlignmentFlag.AlignVCenter)
95+
96+
self.main_layout.addWidget(top_widget)
97+
98+
# Separator
99+
sep_top = QFrame()
100+
sep_top.setFrameShape(QFrame.Shape.HLine)
101+
sep_top.setFrameShadow(QFrame.Shadow.Sunken)
102+
self.main_layout.addWidget(sep_top)
103+
104+
# --- Taby Lettuce / Schej ---
105+
engine_group = QButtonGroup(self)
106+
engine_group.setExclusive(True)
107+
108+
# Lettuce
109+
self.btn_lettuce = SidebarButton(
110+
icon=QIcon(self.lettuce_icon_light),
111+
text="lettucemeet",
112+
checkable=True
113+
)
114+
self.btn_lettuce.setChecked(True)
115+
self.btn_lettuce.clicked.connect(self._on_lettuce_clicked)
116+
engine_group.addButton(self.btn_lettuce)
117+
self.main_layout.addWidget(self.btn_lettuce)
118+
119+
# Schej
120+
self.btn_schej = SidebarButton(
121+
icon=QIcon(self.schej_icon_light),
122+
text="schej",
123+
checkable=True
124+
)
125+
self.btn_schej.setChecked(False)
126+
self.btn_schej.clicked.connect(self._on_schej_clicked)
127+
engine_group.addButton(self.btn_schej)
128+
self.main_layout.addWidget(self.btn_schej)
129+
130+
# Wczytaj CSV
131+
csv_icon = QIcon("assets/icons/light/load_csv_light.png")
132+
self.btn_load_csv = SidebarButton(icon=csv_icon, text="Wczytaj CSV", checkable=False)
133+
self.btn_load_csv.clicked.connect(lambda: self.sig_load_csv.emit())
134+
self.main_layout.addWidget(self.btn_load_csv)
135+
136+
# Jeśli main_window => eksporty
137+
if not initial_mode:
138+
exp_csv_icon = QIcon("assets/icons/light/export_csv_light.png")
139+
self.btn_export_csv = SidebarButton(icon=exp_csv_icon, text="Eksport CSV", checkable=False)
140+
self.btn_export_csv.clicked.connect(lambda: self.sig_export_csv.emit())
141+
self.main_layout.addWidget(self.btn_export_csv)
142+
143+
exp_html_icon = QIcon("assets/icons/light/export_html_light.png")
144+
self.btn_export_html = SidebarButton(icon=exp_html_icon, text="Eksport HTML", checkable=False)
145+
self.btn_export_html.clicked.connect(lambda: self.sig_export_html.emit())
146+
self.main_layout.addWidget(self.btn_export_html)
147+
148+
exp_png_icon = QIcon("assets/icons/light/export_png_light.png")
149+
self.btn_export_png = SidebarButton(icon=exp_png_icon, text="Eksport PNG", checkable=False)
150+
self.btn_export_png.clicked.connect(lambda: self.sig_export_png.emit())
151+
self.main_layout.addWidget(self.btn_export_png)
152+
153+
# spacer
154+
self.main_layout.addSpacerItem(QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding))
155+
156+
# FireMode
157+
fire_icon = QIcon("assets/icons/light/theme_toggle_light.png")
158+
self.btn_fire_mode = SidebarButton(icon=fire_icon, text="FireMode", checkable=False)
159+
self.btn_fire_mode.clicked.connect(lambda: self.sig_fire_mode.emit())
160+
self.main_layout.addWidget(self.btn_fire_mode)
161+
162+
# Kolor czipów – checkable
163+
if not initial_mode:
164+
color_icon = QIcon("assets/icons/light/colorize_chips_light.png")
165+
self.btn_colorize = SidebarButton(icon=color_icon, text="Kolor czipów", checkable=True)
166+
self.btn_colorize.setChecked(False)
167+
self.btn_colorize.clicked.connect(lambda: self.sig_colorize.emit())
168+
self.main_layout.addWidget(self.btn_colorize)
169+
170+
# Parametry – checkable
171+
if not initial_mode:
172+
params_icon = QIcon("assets/icons/light/parameters_light.png")
173+
self.btn_params = SidebarButton(icon=params_icon, text="Parametry", checkable=True)
174+
self.btn_params.setChecked(True)
175+
self.btn_params.clicked.connect(lambda: self.sig_toggle_params.emit())
176+
self.main_layout.addWidget(self.btn_params)
177+
178+
# Separator
179+
sep_bottom = QFrame()
180+
sep_bottom.setFrameShape(QFrame.Shape.HLine)
181+
sep_bottom.setFrameShadow(QFrame.Shadow.Sunken)
182+
self.main_layout.addWidget(sep_bottom)
183+
184+
# Dokumentacja
185+
doc_icon = QIcon("assets/icons/light/docs_light.png")
186+
self.btn_doc = SidebarButton(icon=doc_icon, text="Dokumentacja", checkable=False)
187+
self.btn_doc.clicked.connect(lambda: self.sig_documentation.emit())
188+
self.main_layout.addWidget(self.btn_doc)
189+
190+
# Powrót do initial – only main
191+
if not initial_mode:
192+
back_icon = QIcon("assets/icons/light/back_light.png")
193+
self.btn_go_initial = SidebarButton(icon=back_icon, text="Powrót", checkable=False)
194+
self.btn_go_initial.clicked.connect(lambda: self.sig_go_initial.emit())
195+
self.main_layout.addWidget(self.btn_go_initial)
196+
197+
def _on_lettuce_clicked(self):
198+
"""Zaznacz Lettuce, odznacz Schej, emit sig_select_lettuce."""
199+
if not self.btn_lettuce.isChecked():
200+
self.btn_lettuce.setChecked(True)
201+
self.btn_schej.setChecked(False)
202+
self.sig_select_lettuce.emit()
203+
204+
def _on_schej_clicked(self):
205+
"""Zaznacz Schej, odznacz Lettuce, emit sig_select_schej."""
206+
if not self.btn_schej.isChecked():
207+
self.btn_schej.setChecked(True)
208+
self.btn_lettuce.setChecked(False)
209+
self.sig_select_schej.emit()
210+
211+
def toggle_sidebar(self):
212+
"""
213+
Główna metoda rozwijania/zwijania sidebar.
214+
Faktycznie zmieniamy min/maxWidth z animacją
215+
i na końcu ustawiamy setFixedWidth.
216+
"""
217+
end_width = self._expanded_width if not self._expanded else self._collapsed_width
218+
self._expanded = not self._expanded
219+
220+
self._anim_min = QPropertyAnimation(self, b"minimumWidth")
221+
self._anim_min.setDuration(250)
222+
self._anim_min.setEasingCurve(QEasingCurve.Type.InOutQuad)
223+
self._anim_min.setStartValue(self.width())
224+
self._anim_min.setEndValue(end_width)
225+
226+
self._anim_max = QPropertyAnimation(self, b"maximumWidth")
227+
self._anim_max.setDuration(250)
228+
self._anim_max.setEasingCurve(QEasingCurve.Type.InOutQuad)
229+
self._anim_max.setStartValue(self.width())
230+
self._anim_max.setEndValue(end_width)
231+
232+
self._anim_group = QParallelAnimationGroup(self)
233+
self._anim_group.addAnimation(self._anim_min)
234+
self._anim_group.addAnimation(self._anim_max)
235+
self._anim_group.finished.connect(self._on_animation_finished)
236+
self._anim_group.start()
237+
238+
self.app_label.setVisible(self._expanded)
239+
for attr_name in [
240+
"btn_lettuce", "btn_schej", "btn_load_csv",
241+
"btn_export_csv", "btn_export_html", "btn_export_png",
242+
"btn_fire_mode", "btn_colorize", "btn_params",
243+
"btn_doc", "btn_go_initial"
244+
]:
245+
btn = getattr(self, attr_name, None)
246+
if btn is not None:
247+
btn.show_text(self._expanded)
248+
249+
def _on_animation_finished(self):
250+
"""
251+
Po zakończeniu animacji wymuszamy 'setFixedWidth',
252+
co w praktyce zapewnia, że layout rodzica nie narzuci
253+
ponownie starej szerokości.
254+
"""
255+
final_w = self._expanded_width if self._expanded else self._collapsed_width
256+
self.setFixedWidth(final_w)
257+
258+
def disable_api_tabs(self, disabled: bool):
259+
"""Blokuje klikanie w Lettuce i Schej."""
260+
if hasattr(self, "btn_lettuce"):
261+
self.btn_lettuce.setEnabled(not disabled)
262+
if hasattr(self, "btn_schej"):
263+
self.btn_schej.setEnabled(not disabled)
264+
265+
def set_dark_mode_icon(self, dark: bool):
266+
"""
267+
Przełącza WYŁĄCZNIE ikony Lettuce i Schej między wersją light i dark.
268+
Reszta przycisków pozostaje bez zmian.
269+
"""
270+
if dark:
271+
self.btn_lettuce.setIcon(QIcon(self.lettuce_icon_dark))
272+
self.btn_schej.setIcon(QIcon(self.schej_icon_dark))
273+
else:
274+
self.btn_lettuce.setIcon(QIcon(self.lettuce_icon_light))
275+
self.btn_schej.setIcon(QIcon(self.schej_icon_light))

UI/footer.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from PyQt6.QtWidgets import QWidget, QHBoxLayout, QLabel, QSpacerItem, QSizePolicy
2+
from PyQt6.QtCore import Qt
3+
from PyQt6.QtGui import QPixmap, QFont
4+
from core.version import __app_version__
5+
6+
7+
class FooterWidget(QWidget):
8+
"""
9+
Wspólny widget stopki dla całej aplikacji.
10+
- nazwa aplikacji, autor, rok,
11+
- numer wersji,
12+
- ikonka informująca o dostępnej aktualizacji.
13+
"""
14+
def __init__(self, parent=None):
15+
super().__init__(parent)
16+
17+
self._layout = QHBoxLayout(self)
18+
self._layout.setContentsMargins(5, 5, 5, 5)
19+
self._layout.setSpacing(10)
20+
21+
self.appLabel = QLabel("Harmobot © 2025 | mwalas")
22+
self.appLabel.setFont(QFont("Segoe UI", 9))
23+
self._layout.addWidget(self.appLabel)
24+
25+
self._layout.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
26+
27+
self.versionLabel = QLabel(f"v{__app_version__}")
28+
self.versionLabel.setFont(QFont("Segoe UI", 9))
29+
self._layout.addWidget(self.versionLabel)
30+
31+
self.updateIcon = QLabel()
32+
update_pixmap = QPixmap("assets/icons/update.png")
33+
if not update_pixmap.isNull():
34+
self.updateIcon.setPixmap(update_pixmap.scaled(20, 20, Qt.AspectRatioMode.KeepAspectRatio))
35+
self.updateIcon.setToolTip("Nowa wersja jest dostępna.")
36+
self.updateIcon.setVisible(False)
37+
self._layout.addWidget(self.updateIcon)
38+
39+
def setUpdateAvailable(self, is_available: bool):
40+
"""
41+
Pokazuje lub ukrywa ikonę aktualizacji w zależności od wartości is_available.
42+
"""
43+
self.updateIcon.setVisible(is_available)

0 commit comments

Comments
 (0)