Skip to content

Commit f1b73f7

Browse files
committed
feat: Phase 3 - Polish & Academic Aesthetic
- Add app icon (book with flowing document design) - Add window icon and taskbar icon - Add animated glow effects on drop zone - Add pulse animation for idle drop zone - Add click-to-browse functionality - Add system tray integration with minimize-to-tray - Add tray menu with watch folder toggle - Update PyInstaller spec with icon
1 parent 674f3ce commit f1b73f7

7 files changed

Lines changed: 290 additions & 22 deletions

File tree

biblioflow.spec

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ a = Analysis(
1717
pathex=[project_root],
1818
binaries=[],
1919
datas=[
20-
('src/assets/styles.qss', 'src/assets'),
20+
('src/assets/styles.qss', 'assets'),
21+
('src/assets/icon.png', 'assets'),
22+
('src/assets/icon.ico', 'assets'),
2123
],
2224
hiddenimports=[
2325
'PyQt6.QtCore',
@@ -62,6 +64,6 @@ exe = EXE(
6264
target_arch=None,
6365
codesign_identity=None,
6466
entitlements_file=None,
65-
icon=None, # Add icon path here when available
67+
icon='src/assets/icon.ico',
6668
version=None, # Can add version info file
6769
)

setup.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Write-Host "[3/4] Installing dependencies..." -ForegroundColor Yellow
2121
pip install -r requirements.txt -r requirements-dev.txt --quiet
2222

2323
Write-Host "[4/4] Verifying installation..." -ForegroundColor Yellow
24-
python -c "import PyQt6; print('PyQt6:', PyQt6.__version__)"
24+
python -c "from PyQt6.QtCore import PYQT_VERSION_STR; print('PyQt6:', PYQT_VERSION_STR)"
2525

2626
Write-Host "`n Setup complete!" -ForegroundColor Green
2727
Write-Host "`nTo run the app:" -ForegroundColor Cyan

src/assets/icon.ico

83.1 KB
Binary file not shown.

src/assets/icon.png

400 KB
Loading

src/ui/app.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .settings import SettingsPanel
2626
from .queue_widget import QueueWidget
2727
from .preview_card import PreviewCard
28+
from .system_tray import SystemTray
2829

2930

3031
class UpdateWorker(QThread):
@@ -67,6 +68,7 @@ def __init__(self):
6768
super().__init__()
6869
self.setWindowTitle(f"{APP_NAME} v{__version__}")
6970
self.setMinimumSize(1000, 700)
71+
self._set_window_icon()
7072

7173
# Initialize core components
7274
self.config = ConfigManager()
@@ -82,10 +84,27 @@ def __init__(self):
8284
self._apply_styles()
8385
self._connect_signals()
8486
self._setup_watcher()
87+
self._setup_system_tray()
8588

8689
if self.config.get("check_updates_on_startup", True):
8790
self._check_for_updates()
8891

92+
def _set_window_icon(self):
93+
"""Set the application window icon."""
94+
import sys
95+
from PyQt6.QtGui import QIcon
96+
if getattr(sys, 'frozen', False):
97+
# Running as compiled executable
98+
import os
99+
base_path = sys._MEIPASS
100+
icon_path = os.path.join(base_path, 'assets', 'icon.png')
101+
else:
102+
# Running from source
103+
import os
104+
icon_path = os.path.join(os.path.dirname(__file__), '..', 'assets', 'icon.png')
105+
if os.path.exists(icon_path):
106+
self.setWindowIcon(QIcon(icon_path))
107+
89108
def _setup_ui(self):
90109
"""Initialize UI components."""
91110
central = QWidget()
@@ -235,6 +254,51 @@ def _setup_watcher(self):
235254
self.watcher.start()
236255
self._show_status(f"Watching: {path}")
237256

257+
def _setup_system_tray(self):
258+
"""Setup system tray icon."""
259+
self.system_tray = SystemTray(self)
260+
self.system_tray.show_window.connect(self._show_from_tray)
261+
self.system_tray.quit_app.connect(self._quit_app)
262+
self.system_tray.toggle_watch.connect(self._on_tray_watch_toggled)
263+
self.system_tray.set_watch_enabled(self.config.is_watch_folder_enabled())
264+
if self.system_tray.is_available:
265+
self.system_tray.show()
266+
267+
def _show_from_tray(self):
268+
"""Show window from system tray."""
269+
self.showNormal()
270+
self.activateWindow()
271+
self.raise_()
272+
273+
def _quit_app(self):
274+
"""Quit the application."""
275+
self.system_tray.hide()
276+
QApplication.quit()
277+
278+
def _on_tray_watch_toggled(self, enabled: bool):
279+
"""Handle watch folder toggle from tray."""
280+
if enabled:
281+
path = self.config.get_watch_folder_path()
282+
if path and os.path.isdir(path):
283+
self.watcher.set_watch_path(path)
284+
self.watcher.start()
285+
self.system_tray.show_message("Watch Folder", f"Watching: {path}")
286+
else:
287+
self.watcher.stop()
288+
self.system_tray.show_message("Watch Folder", "Disabled")
289+
290+
def closeEvent(self, event):
291+
"""Minimize to tray instead of closing."""
292+
if self.system_tray.is_available:
293+
event.ignore()
294+
self.hide()
295+
self.system_tray.show_message(
296+
"BiblioFlow",
297+
"Running in background. Double-click tray icon to restore."
298+
)
299+
else:
300+
event.accept()
301+
238302
def _switch_view(self, index: int):
239303
"""Switch to a different view."""
240304
self.content_stack.setCurrentIndex(index)

src/ui/drop_zone.py

Lines changed: 114 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,96 @@
11
"""
22
Drag-and-Drop Zone Widget
33
4-
Handles PDF file drops for processing.
4+
Handles PDF file drops with animated glow effects.
55
"""
6-
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel
7-
from PyQt6.QtCore import Qt, pyqtSignal
8-
from PyQt6.QtGui import QDragEnterEvent, QDropEvent
6+
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QGraphicsDropShadowEffect, QFileDialog
7+
from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve, QTimer, pyqtProperty
8+
from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QColor, QMouseEvent
99
import qtawesome as qta
1010

1111

1212
class DropZone(QWidget):
13-
"""Widget for drag-and-drop PDF files."""
13+
"""Widget for drag-and-drop PDF files with animated glow effects."""
1414

1515
files_dropped = pyqtSignal(list) # List of file paths
1616

1717
def __init__(self):
1818
super().__init__()
1919
self.setAcceptDrops(True)
20+
self._glow_intensity = 0.0
2021
self._setup_ui()
22+
self._setup_glow_effect()
23+
self._setup_animations()
2124

2225
def _setup_ui(self):
2326
"""Initialize UI."""
2427
layout = QVBoxLayout(self)
2528
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
2629

2730
# Icon
28-
icon_label = QLabel()
29-
icon_label.setPixmap(qta.icon('fa5s.cloud-upload-alt', color='#3B82F6').pixmap(64, 64))
30-
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
31-
layout.addWidget(icon_label)
31+
self.icon_label = QLabel()
32+
self.icon_label.setPixmap(qta.icon('fa5s.cloud-upload-alt', color='#3B82F6').pixmap(64, 64))
33+
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
34+
layout.addWidget(self.icon_label)
3235

3336
# Text
34-
text = QLabel("Drop PDF files here")
35-
text.setStyleSheet("font-size: 18px; color: #A0A0A0; margin-top: 10px;")
36-
text.setAlignment(Qt.AlignmentFlag.AlignCenter)
37-
layout.addWidget(text)
37+
self.text_label = QLabel("Drop PDF files here")
38+
self.text_label.setStyleSheet("font-size: 18px; color: #A0A0A0; margin-top: 10px;")
39+
self.text_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
40+
layout.addWidget(self.text_label)
3841

39-
subtext = QLabel("or click to browse")
40-
subtext.setStyleSheet("font-size: 14px; color: #666;")
41-
subtext.setAlignment(Qt.AlignmentFlag.AlignCenter)
42-
layout.addWidget(subtext)
42+
self.subtext_label = QLabel("or click to browse")
43+
self.subtext_label.setStyleSheet("font-size: 14px; color: #666;")
44+
self.subtext_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
45+
layout.addWidget(self.subtext_label)
4346

4447
self._apply_styles()
4548

49+
def _setup_glow_effect(self):
50+
"""Setup the glow drop shadow effect."""
51+
self.glow_effect = QGraphicsDropShadowEffect(self)
52+
self.glow_effect.setBlurRadius(0)
53+
self.glow_effect.setColor(QColor(59, 130, 246, 180)) # #3B82F6 with alpha
54+
self.glow_effect.setOffset(0, 0)
55+
self.setGraphicsEffect(self.glow_effect)
56+
57+
def _setup_animations(self):
58+
"""Setup glow animations."""
59+
# Glow pulse animation for idle state
60+
self.pulse_timer = QTimer(self)
61+
self.pulse_timer.timeout.connect(self._pulse_glow)
62+
self.pulse_direction = 1
63+
self.pulse_timer.start(50) # 50ms intervals for smooth animation
64+
65+
# Drag hover animation
66+
self.glow_animation = QPropertyAnimation(self, b"glowIntensity")
67+
self.glow_animation.setDuration(200)
68+
self.glow_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
69+
70+
def _pulse_glow(self):
71+
"""Subtle idle pulse effect."""
72+
if hasattr(self, '_is_dragging') and self._is_dragging:
73+
return # Don't pulse during drag
74+
75+
current = self.glow_effect.blurRadius()
76+
step = 0.5 * self.pulse_direction
77+
78+
if current >= 15:
79+
self.pulse_direction = -1
80+
elif current <= 5:
81+
self.pulse_direction = 1
82+
83+
self.glow_effect.setBlurRadius(max(0, current + step))
84+
85+
@pyqtProperty(float)
86+
def glowIntensity(self):
87+
return self._glow_intensity
88+
89+
@glowIntensity.setter
90+
def glowIntensity(self, value):
91+
self._glow_intensity = value
92+
self.glow_effect.setBlurRadius(value)
93+
4694
def _apply_styles(self):
4795
"""Apply drop zone styles."""
4896
self.setStyleSheet("""
@@ -54,30 +102,65 @@ def _apply_styles(self):
54102
}
55103
DropZone:hover {
56104
border-color: #60A5FA;
105+
background-color: #1E293B;
106+
}
107+
DropZone[dragging="true"] {
108+
border: 3px solid #60A5FA;
57109
background-color: #1E3A5F;
58110
}
59111
""")
60112

113+
def _animate_glow_in(self):
114+
"""Animate glow when dragging over."""
115+
self.glow_animation.stop()
116+
self.glow_animation.setStartValue(self.glow_effect.blurRadius())
117+
self.glow_animation.setEndValue(40)
118+
self.glow_animation.start()
119+
self.glow_effect.setColor(QColor(96, 165, 250, 220)) # Brighter blue
120+
121+
def _animate_glow_out(self):
122+
"""Animate glow back to normal."""
123+
self.glow_animation.stop()
124+
self.glow_animation.setStartValue(self.glow_effect.blurRadius())
125+
self.glow_animation.setEndValue(10)
126+
self.glow_animation.start()
127+
self.glow_effect.setColor(QColor(59, 130, 246, 180))
128+
61129
def dragEnterEvent(self, event: QDragEnterEvent):
62-
"""Handle drag enter."""
130+
"""Handle drag enter with glow animation."""
63131
if event.mimeData().hasUrls():
64-
# Check if any URLs are PDFs
65132
for url in event.mimeData().urls():
66133
if url.toLocalFile().lower().endswith('.pdf'):
67134
event.acceptProposedAction()
135+
self._is_dragging = True
68136
self.setProperty("dragging", True)
69137
self.style().polish(self)
138+
self._animate_glow_in()
139+
# Update icon color
140+
self.icon_label.setPixmap(
141+
qta.icon('fa5s.cloud-upload-alt', color='#60A5FA').pixmap(72, 72)
142+
)
70143
return
71144

72145
def dragLeaveEvent(self, event):
73146
"""Handle drag leave."""
147+
self._is_dragging = False
74148
self.setProperty("dragging", False)
75149
self.style().polish(self)
150+
self._animate_glow_out()
151+
self.icon_label.setPixmap(
152+
qta.icon('fa5s.cloud-upload-alt', color='#3B82F6').pixmap(64, 64)
153+
)
76154

77155
def dropEvent(self, event: QDropEvent):
78156
"""Handle file drop."""
157+
self._is_dragging = False
79158
self.setProperty("dragging", False)
80159
self.style().polish(self)
160+
self._animate_glow_out()
161+
self.icon_label.setPixmap(
162+
qta.icon('fa5s.cloud-upload-alt', color='#3B82F6').pixmap(64, 64)
163+
)
81164

82165
pdf_files = []
83166
for url in event.mimeData().urls():
@@ -87,3 +170,15 @@ def dropEvent(self, event: QDropEvent):
87170

88171
if pdf_files:
89172
self.files_dropped.emit(pdf_files)
173+
174+
def mousePressEvent(self, event: QMouseEvent):
175+
"""Handle click to open file dialog."""
176+
if event.button() == Qt.MouseButton.LeftButton:
177+
files, _ = QFileDialog.getOpenFileNames(
178+
self,
179+
"Select PDF Files",
180+
"",
181+
"PDF Files (*.pdf)"
182+
)
183+
if files:
184+
self.files_dropped.emit(files)

0 commit comments

Comments
 (0)