embedingHF commited on
Commit
b108abe
Β·
verified Β·
1 Parent(s): 1decbc8

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +959 -791
main.py CHANGED
@@ -1,792 +1,960 @@
1
- import sys
2
- import os
3
- import json
4
- import threading
5
- import shutil
6
- from pathlib import Path
7
- from datetime import datetime
8
- from PyQt6.QtWidgets import (
9
- QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
10
- QPushButton, QLabel, QFileDialog, QComboBox, QProgressBar,
11
- QTextEdit, QTabWidget, QGroupBox, QGridLayout, QMessageBox,
12
- QListWidget, QListWidgetItem, QCheckBox, QSpinBox, QLineEdit,
13
- QSplitter, QFrame, QToolButton, QSizePolicy
14
- )
15
- from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize, QPropertyAnimation, QEasingCurve, QRect
16
- from PyQt6.QtGui import QFont, QIcon, QDragEnterEvent, QDropEvent, QPalette, QColor, QAction
17
-
18
- # Fix FFmpeg before any imports
19
- import warnings
20
-
21
- warnings.filterwarnings("ignore", message="Couldn't find ffmpeg or avconv")
22
-
23
- # Setup FFmpeg
24
- ffmpeg_path = None
25
- for path in [
26
- shutil.which('ffmpeg'),
27
- shutil.which('ffmpeg.exe'),
28
- r"C:\ffmpeg\bin\ffmpeg.exe",
29
- r"C:\Program Files\ffmpeg\bin\ffmpeg.exe"
30
- ]:
31
- if path and os.path.exists(path):
32
- ffmpeg_path = path
33
- os.environ["FFMPEG_BINARY"] = ffmpeg_path
34
- break
35
-
36
- # Import converters
37
- from converters.document_converter import DocumentConverter
38
- from converters.image_converter import ImageConverter
39
- from converters.video_converter import VideoConverter
40
- from converters.audio_converter import AudioConverter
41
- from ai.ai_assistant import AIAssistant
42
-
43
-
44
- class ConversionThread(QThread):
45
- progress_update = pyqtSignal(int)
46
- status_update = pyqtSignal(str)
47
- conversion_complete = pyqtSignal(bool, str)
48
-
49
- def __init__(self, converter, input_path, output_path, options=None):
50
- super().__init__()
51
- self.converter = converter
52
- self.input_path = input_path
53
- self.output_path = output_path
54
- self.options = options or {}
55
-
56
- def run(self):
57
- try:
58
- self.status_update.emit(f"Converting: {os.path.basename(self.input_path)}")
59
- success = self.converter.convert(
60
- self.input_path,
61
- self.output_path,
62
- self.options,
63
- self.progress_update
64
- )
65
-
66
- if success:
67
- self.conversion_complete.emit(True, f"βœ… Successfully converted: {os.path.basename(self.input_path)}")
68
- else:
69
- self.conversion_complete.emit(False, f"❌ Failed to convert: {os.path.basename(self.input_path)}")
70
- except Exception as e:
71
- self.conversion_complete.emit(False, f"❌ Error: {str(e)}")
72
-
73
-
74
- class CollapsibleSidebar(QWidget):
75
- """Collapsible sidebar widget"""
76
-
77
- def __init__(self, parent=None):
78
- super().__init__(parent)
79
- self.setVisible(True)
80
- self.animation_duration = 300
81
-
82
- def toggle(self):
83
- """Toggle sidebar visibility with animation"""
84
- self.animated_toggle()
85
-
86
- def animated_toggle(self):
87
- """Animated toggle of sidebar"""
88
- if self.isVisible():
89
- self.animation = QPropertyAnimation(self, b"maximumWidth")
90
- self.animation.setDuration(self.animation_duration)
91
- self.animation.setStartValue(self.width())
92
- self.animation.setEndValue(0)
93
- self.animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
94
- self.animation.finished.connect(lambda: self.setVisible(False))
95
- self.animation.start()
96
- else:
97
- self.setVisible(True)
98
- self.animation = QPropertyAnimation(self, b"maximumWidth")
99
- self.animation.setDuration(self.animation_duration)
100
- self.animation.setStartValue(0)
101
- self.animation.setEndValue(400) # Fixed width when expanded
102
- self.animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
103
- self.animation.start()
104
-
105
-
106
- class FileConverterApp(QMainWindow):
107
- def __init__(self):
108
- super().__init__()
109
- self.setWindowTitle("🎯 AI File Converter Pro")
110
- self.setGeometry(100, 100, 1400, 900)
111
-
112
- # Sidebar state
113
- self.sidebar_visible = True
114
-
115
- # Set modern style
116
- self.setStyleSheet("""
117
- QMainWindow {
118
- background-color: #1e1e2e;
119
- }
120
- QGroupBox {
121
- font-weight: bold;
122
- border: 2px solid #313244;
123
- border-radius: 8px;
124
- margin-top: 10px;
125
- padding-top: 10px;
126
- color: #cdd6f4;
127
- font-size: 13px;
128
- }
129
- QGroupBox::title {
130
- subcontrol-origin: margin;
131
- left: 10px;
132
- padding: 0 5px 0 5px;
133
- color: #89b4fa;
134
- }
135
- QPushButton {
136
- background-color: #89b4fa;
137
- border: none;
138
- padding: 8px;
139
- border-radius: 6px;
140
- font-weight: bold;
141
- color: #1e1e2e;
142
- }
143
- QPushButton:hover {
144
- background-color: #b4befe;
145
- }
146
- QPushButton:pressed {
147
- background-color: #6c7086;
148
- }
149
- QToolButton {
150
- background-color: #313244;
151
- border: 1px solid #45475a;
152
- border-radius: 6px;
153
- padding: 8px;
154
- color: #cdd6f4;
155
- }
156
- QToolButton:hover {
157
- background-color: #45475a;
158
- }
159
- QListWidget, QTextEdit, QLineEdit, QComboBox {
160
- background-color: #313244;
161
- border: 1px solid #45475a;
162
- border-radius: 6px;
163
- padding: 5px;
164
- color: #cdd6f4;
165
- }
166
- QProgressBar {
167
- border: 2px solid #45475a;
168
- border-radius: 6px;
169
- text-align: center;
170
- color: #cdd6f4;
171
- }
172
- QProgressBar::chunk {
173
- background-color: #89b4fa;
174
- border-radius: 4px;
175
- }
176
- QTabWidget::pane {
177
- background-color: #1e1e2e;
178
- border: 1px solid #313244;
179
- border-radius: 6px;
180
- }
181
- QTabBar::tab {
182
- background-color: #313244;
183
- padding: 8px;
184
- margin: 2px;
185
- border-radius: 4px;
186
- color: #cdd6f4;
187
- }
188
- QTabBar::tab:selected {
189
- background-color: #89b4fa;
190
- color: #1e1e2e;
191
- }
192
- QLabel {
193
- color: #cdd6f4;
194
- }
195
- QCheckBox {
196
- color: #cdd6f4;
197
- }
198
- QSplitter::handle {
199
- background-color: #313244;
200
- margin: 2px;
201
- }
202
- QSplitter::handle:hover {
203
- background-color: #89b4fa;
204
- }
205
- """)
206
-
207
- # Initialize converters
208
- self.doc_converter = DocumentConverter()
209
- self.img_converter = ImageConverter()
210
- self.video_converter = VideoConverter()
211
- self.audio_converter = AudioConverter()
212
- self.ai_assistant = AIAssistant()
213
-
214
- # Conversion queue
215
- self.conversion_queue = []
216
- self.current_conversion = None
217
- self.active_threads = []
218
- self.converted_count = 0
219
- self.failed_count = 0
220
-
221
- self.setup_ui()
222
- self.setup_drag_drop()
223
- self.setup_menu_bar()
224
-
225
- def setup_menu_bar(self):
226
- """Setup menu bar with toggle option"""
227
- menubar = self.menuBar()
228
- menubar.setStyleSheet("""
229
- QMenuBar {
230
- background-color: #1e1e2e;
231
- color: #cdd6f4;
232
- border-bottom: 1px solid #313244;
233
- }
234
- QMenuBar::item:selected {
235
- background-color: #313244;
236
- }
237
- QMenu {
238
- background-color: #1e1e2e;
239
- color: #cdd6f4;
240
- border: 1px solid #313244;
241
- }
242
- QMenu::item:selected {
243
- background-color: #89b4fa;
244
- color: #1e1e2e;
245
- }
246
- """)
247
-
248
- view_menu = menubar.addMenu("πŸ‘οΈ View")
249
-
250
- toggle_action = QAction("πŸ€– Toggle AI Assistant", self)
251
- toggle_action.setShortcut("Ctrl+Shift+A")
252
- toggle_action.triggered.connect(self.toggle_sidebar)
253
- view_menu.addAction(toggle_action)
254
-
255
- # Add separator and other options
256
- view_menu.addSeparator()
257
-
258
- reset_view_action = QAction("πŸ”„ Reset Layout", self)
259
- reset_view_action.setShortcut("Ctrl+Shift+R")
260
- reset_view_action.triggered.connect(self.reset_layout)
261
- view_menu.addAction(reset_view_action)
262
-
263
- def setup_ui(self):
264
- central_widget = QWidget()
265
- self.setCentralWidget(central_widget)
266
- main_layout = QHBoxLayout(central_widget)
267
- main_layout.setContentsMargins(0, 0, 0, 0)
268
- main_layout.setSpacing(0)
269
-
270
- # Create splitter for resizable panels
271
- self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
272
-
273
- # Left panel - Main content
274
- self.left_panel = self.create_left_panel()
275
- self.main_splitter.addWidget(self.left_panel)
276
-
277
- # Right panel - AI Sidebar (collapsible)
278
- self.sidebar_widget = self.create_sidebar()
279
- self.main_splitter.addWidget(self.sidebar_widget)
280
-
281
- # Set initial sizes (70% left, 30% right)
282
- self.main_splitter.setSizes([980, 420])
283
-
284
- main_layout.addWidget(self.main_splitter)
285
-
286
- def create_left_panel(self):
287
- """Create the main left panel with file list and controls"""
288
- left_panel = QWidget()
289
- left_layout = QVBoxLayout(left_panel)
290
- left_layout.setSpacing(10)
291
- left_layout.setContentsMargins(10, 10, 10, 10)
292
-
293
- # File selection area
294
- file_group = QGroupBox("πŸ“ Files to Convert")
295
- file_layout = QVBoxLayout()
296
-
297
- self.file_list = QListWidget()
298
- self.file_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
299
- self.file_list.setMinimumHeight(200)
300
- file_layout.addWidget(self.file_list)
301
-
302
- btn_layout = QHBoxLayout()
303
- add_btn = QPushButton("βž• Add Files")
304
- add_btn.clicked.connect(self.add_files)
305
- add_folder_btn = QPushButton("πŸ“‚ Add Folder")
306
- add_folder_btn.clicked.connect(self.add_folder)
307
- remove_btn = QPushButton("❌ Remove Selected")
308
- remove_btn.clicked.connect(self.remove_selected)
309
- clear_btn = QPushButton("πŸ—‘οΈ Clear All")
310
- clear_btn.clicked.connect(self.clear_files)
311
-
312
- btn_layout.addWidget(add_btn)
313
- btn_layout.addWidget(add_folder_btn)
314
- btn_layout.addWidget(remove_btn)
315
- btn_layout.addWidget(clear_btn)
316
- file_layout.addLayout(btn_layout)
317
-
318
- file_group.setLayout(file_layout)
319
- left_layout.addWidget(file_group)
320
-
321
- # Converter settings
322
- converter_group = QGroupBox("βš™οΈ Converter Settings")
323
- converter_layout = QGridLayout()
324
- converter_layout.setSpacing(10)
325
-
326
- converter_layout.addWidget(QLabel("File Type:"), 0, 0)
327
- self.file_type_combo = QComboBox()
328
- self.file_type_combo.addItems(["Document", "Image", "Video", "Audio"])
329
- self.file_type_combo.currentTextChanged.connect(self.on_file_type_changed)
330
- converter_layout.addWidget(self.file_type_combo, 0, 1)
331
-
332
- converter_layout.addWidget(QLabel("Output Format:"), 1, 0)
333
- self.output_format_combo = QComboBox()
334
- converter_layout.addWidget(self.output_format_combo, 1, 1)
335
-
336
- converter_layout.addWidget(QLabel("Quality:"), 2, 0)
337
- self.quality_combo = QComboBox()
338
- self.quality_combo.addItems(["High", "Medium", "Low"])
339
- converter_layout.addWidget(self.quality_combo, 2, 1)
340
-
341
- converter_layout.addWidget(QLabel("Output Folder:"), 3, 0)
342
- self.output_folder = QLineEdit()
343
- self.output_folder.setText(str(Path.home() / "Downloads" / "Converted"))
344
- converter_layout.addWidget(self.output_folder, 3, 1)
345
-
346
- browse_btn = QPushButton("πŸ“ Browse")
347
- browse_btn.clicked.connect(self.browse_output_folder)
348
- converter_layout.addWidget(browse_btn, 3, 2)
349
-
350
- # Create output folder if not exists
351
- Path(self.output_folder.text()).mkdir(parents=True, exist_ok=True)
352
-
353
- self.ai_checkbox = QCheckBox("πŸ€– Enable AI Enhancement")
354
- self.ai_checkbox.setChecked(False)
355
- converter_layout.addWidget(self.ai_checkbox, 4, 0, 1, 2)
356
-
357
- converter_group.setLayout(converter_layout)
358
- left_layout.addWidget(converter_group)
359
-
360
- # Progress section
361
- progress_group = QGroupBox("πŸ“Š Conversion Progress")
362
- progress_layout = QVBoxLayout()
363
-
364
- self.progress_bar = QProgressBar()
365
- progress_layout.addWidget(self.progress_bar)
366
-
367
- self.status_label = QLabel("βœ… Ready to convert")
368
- self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
369
- progress_layout.addWidget(self.status_label)
370
-
371
- self.stats_label = QLabel("πŸ“ˆ Converted: 0 | ❌ Failed: 0")
372
- self.stats_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
373
- progress_layout.addWidget(self.stats_label)
374
-
375
- progress_group.setLayout(progress_layout)
376
- left_layout.addWidget(progress_group)
377
-
378
- # Convert button
379
- self.convert_btn = QPushButton("πŸš€ Start Conversion")
380
- self.convert_btn.setMinimumHeight(50)
381
- self.convert_btn.setStyleSheet("""
382
- QPushButton {
383
- background-color: #a6e3a1;
384
- font-size: 16px;
385
- font-weight: bold;
386
- }
387
- QPushButton:hover {
388
- background-color: #94e2d5;
389
- }
390
- """)
391
- self.convert_btn.clicked.connect(self.start_conversion)
392
- left_layout.addWidget(self.convert_btn)
393
-
394
- return left_panel
395
-
396
- def create_sidebar(self):
397
- """Create the collapsible AI sidebar"""
398
- sidebar_container = QWidget()
399
- sidebar_layout = QVBoxLayout(sidebar_container)
400
- sidebar_layout.setContentsMargins(0, 0, 0, 0)
401
- sidebar_layout.setSpacing(0)
402
-
403
- # Header with toggle button
404
- header = QWidget()
405
- header.setStyleSheet("""
406
- QWidget {
407
- background-color: #313244;
408
- border-bottom: 1px solid #45475a;
409
- }
410
- """)
411
- header_layout = QHBoxLayout(header)
412
- header_layout.setContentsMargins(10, 10, 10, 10)
413
-
414
- title_label = QLabel("πŸ€– AI Assistant")
415
- title_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #89b4fa;")
416
- header_layout.addWidget(title_label)
417
-
418
- header_layout.addStretch()
419
-
420
- # Close button
421
- self.close_sidebar_btn = QToolButton()
422
- self.close_sidebar_btn.setText("β—€")
423
- self.close_sidebar_btn.setToolTip("Close Sidebar (Ctrl+Shift+A)")
424
- self.close_sidebar_btn.setStyleSheet("""
425
- QToolButton {
426
- background-color: #45475a;
427
- border: none;
428
- font-size: 14px;
429
- padding: 5px;
430
- }
431
- QToolButton:hover {
432
- background-color: #6c7086;
433
- }
434
- """)
435
- self.close_sidebar_btn.clicked.connect(self.toggle_sidebar)
436
- header_layout.addWidget(self.close_sidebar_btn)
437
-
438
- sidebar_layout.addWidget(header)
439
-
440
- # Tab widget for AI and Log
441
- right_panel = QTabWidget()
442
- right_panel.setStyleSheet("""
443
- QTabWidget::pane {
444
- border: none;
445
- }
446
- """)
447
-
448
- # AI Assistant tab
449
- ai_widget = QWidget()
450
- ai_layout = QVBoxLayout(ai_widget)
451
- ai_layout.setSpacing(10)
452
-
453
- # Suggestions panel
454
- suggestions_group = QGroupBox("πŸ’‘ Quick Commands")
455
- suggestions_layout = QGridLayout()
456
- suggestions_group.setMaximumHeight(220)
457
-
458
- suggestions = [
459
- ("πŸ–ΌοΈ To PNG", "convert this to png"),
460
- ("πŸ“„ To PDF", "convert to pdf"),
461
- ("🎡 To MP3", "make it mp3"),
462
- ("🎬 Web format", "best format for web"),
463
- ("πŸ“Š Analyze", "analyze this file"),
464
- ("πŸ’Ύ Compress", "how to compress video"),
465
- ("πŸ“§ Email size", "best format for email"),
466
- ("πŸ–¨οΈ Print", "best format for printing"),
467
- ]
468
-
469
- row, col = 0, 0
470
- for text, cmd in suggestions:
471
- btn = QPushButton(text)
472
- btn.setMaximumHeight(30)
473
- btn.clicked.connect(lambda checked, c=cmd: self.ai_input.setText(c))
474
- suggestions_layout.addWidget(btn, row, col)
475
- col += 1
476
- if col >= 2:
477
- col = 0
478
- row += 1
479
-
480
- suggestions_group.setLayout(suggestions_layout)
481
- ai_layout.addWidget(suggestions_group)
482
-
483
- # Input area
484
- self.ai_input = QTextEdit()
485
- self.ai_input.setPlaceholderText("""πŸ’¬ Ask me anything about file conversion...
486
-
487
- Examples:
488
- β€’ "Convert this to PNG"
489
- β€’ "What's the best format for web images?"
490
- β€’ "How to compress video for email?"
491
- β€’ "Analyze this file"
492
- β€’ "Make it smaller without losing quality" """)
493
- self.ai_input.setMaximumHeight(100)
494
- ai_layout.addWidget(self.ai_input)
495
-
496
- # Buttons
497
- ai_buttons_layout = QHBoxLayout()
498
- ai_send_btn = QPushButton("πŸ’­ Ask AI")
499
- ai_send_btn.clicked.connect(self.ask_ai_assistant)
500
- ai_clear_btn = QPushButton("πŸ—‘οΈ Clear")
501
- ai_clear_btn.clicked.connect(lambda: self.ai_input.clear())
502
- ai_buttons_layout.addWidget(ai_send_btn)
503
- ai_buttons_layout.addWidget(ai_clear_btn)
504
- ai_layout.addLayout(ai_buttons_layout)
505
-
506
- # Response area
507
- self.ai_response = QTextEdit()
508
- self.ai_response.setReadOnly(True)
509
- self.ai_response.setPlaceholderText("AI response will appear here...")
510
- self.ai_response.setMinimumHeight(200)
511
- ai_layout.addWidget(self.ai_response)
512
-
513
- right_panel.addTab(ai_widget, "πŸ’¬ Assistant")
514
-
515
- # Log tab
516
- log_widget = QWidget()
517
- log_layout = QVBoxLayout(log_widget)
518
- self.log_text = QTextEdit()
519
- self.log_text.setReadOnly(True)
520
- log_layout.addWidget(self.log_text)
521
-
522
- log_btn_layout = QHBoxLayout()
523
- clear_log_btn = QPushButton("πŸ—‘οΈ Clear Log")
524
- clear_log_btn.clicked.connect(lambda: self.log_text.clear())
525
- log_btn_layout.addStretch()
526
- log_btn_layout.addWidget(clear_log_btn)
527
- log_layout.addLayout(log_btn_layout)
528
-
529
- right_panel.addTab(log_widget, "πŸ“ Log")
530
-
531
- sidebar_layout.addWidget(right_panel)
532
-
533
- return sidebar_container
534
-
535
- def toggle_sidebar(self):
536
- """Toggle the AI sidebar visibility"""
537
- self.sidebar_visible = not self.sidebar_visible
538
-
539
- if self.sidebar_visible:
540
- self.sidebar_widget.setVisible(True)
541
- self.close_sidebar_btn.setText("β—€")
542
- self.main_splitter.setSizes([980, 420])
543
- else:
544
- self.sidebar_widget.setVisible(False)
545
- self.close_sidebar_btn.setText("β–Ά")
546
- self.main_splitter.setSizes([1400, 0])
547
-
548
- # Update tooltip
549
- self.close_sidebar_btn.setToolTip(
550
- "Open Sidebar (Ctrl+Shift+A)" if not self.sidebar_visible else "Close Sidebar (Ctrl+Shift+A)")
551
-
552
- def reset_layout(self):
553
- """Reset layout to default"""
554
- self.main_splitter.setSizes([980, 420])
555
- if not self.sidebar_visible:
556
- self.toggle_sidebar()
557
- self.log_message("πŸ”„ Layout reset to default")
558
-
559
- def setup_drag_drop(self):
560
- self.setAcceptDrops(True)
561
-
562
- def dragEnterEvent(self, event: QDragEnterEvent):
563
- if event.mimeData().hasUrls():
564
- event.acceptProposedAction()
565
-
566
- def dropEvent(self, event: QDropEvent):
567
- for url in event.mimeData().urls():
568
- file_path = url.toLocalFile()
569
- if os.path.isfile(file_path):
570
- self.add_file_to_list(file_path)
571
-
572
- def add_files(self):
573
- files, _ = QFileDialog.getOpenFileNames(
574
- self, "Select Files to Convert", "",
575
- "All Files (*.*)"
576
- )
577
- for file in files:
578
- self.add_file_to_list(file)
579
-
580
- def add_folder(self):
581
- folder = QFileDialog.getExistingDirectory(self, "Select Folder")
582
- if folder:
583
- for root, dirs, files in os.walk(folder):
584
- for file in files:
585
- self.add_file_to_list(os.path.join(root, file))
586
-
587
- def remove_selected(self):
588
- for item in self.file_list.selectedItems():
589
- self.file_list.takeItem(self.file_list.row(item))
590
-
591
- def add_file_to_list(self, file_path):
592
- # Check if already in list
593
- for i in range(self.file_list.count()):
594
- if self.file_list.item(i).data(Qt.ItemDataRole.UserRole) == file_path:
595
- return
596
- item = QListWidgetItem(f"πŸ“„ {os.path.basename(file_path)}")
597
- item.setToolTip(file_path)
598
- item.setData(Qt.ItemDataRole.UserRole, file_path)
599
- self.file_list.addItem(item)
600
-
601
- def clear_files(self):
602
- self.file_list.clear()
603
-
604
- def on_file_type_changed(self, file_type):
605
- self.output_format_combo.clear()
606
-
607
- formats = {
608
- "Document": ["pdf", "docx", "txt", "html", "md"],
609
- "Image": ["png", "jpg", "webp", "bmp", "tiff"],
610
- "Video": ["mp4", "avi", "mkv", "mov", "webm", "gif"],
611
- "Audio": ["mp3", "wav", "ogg", "m4a", "flac"]
612
- }
613
- self.output_format_combo.addItems(formats.get(file_type, []))
614
-
615
- def browse_output_folder(self):
616
- folder = QFileDialog.getExistingDirectory(self, "Select Output Folder")
617
- if folder:
618
- self.output_folder.setText(folder)
619
- Path(folder).mkdir(parents=True, exist_ok=True)
620
-
621
- def get_converter(self):
622
- file_type = self.file_type_combo.currentText()
623
- converters = {
624
- "Document": self.doc_converter,
625
- "Image": self.img_converter,
626
- "Video": self.video_converter,
627
- "Audio": self.audio_converter
628
- }
629
- return converters.get(file_type)
630
-
631
- def start_conversion(self):
632
- if self.file_list.count() == 0:
633
- QMessageBox.warning(self, "Warning", "Please add files to convert")
634
- return
635
-
636
- # Reset stats
637
- self.converted_count = 0
638
- self.failed_count = 0
639
- self.update_stats()
640
-
641
- # Build queue
642
- self.conversion_queue = []
643
- selected_items = self.file_list.selectedItems()
644
- if selected_items:
645
- for item in selected_items:
646
- self.conversion_queue.append(item.data(Qt.ItemDataRole.UserRole))
647
- else:
648
- for i in range(self.file_list.count()):
649
- self.conversion_queue.append(self.file_list.item(i).data(Qt.ItemDataRole.UserRole))
650
-
651
- self.convert_btn.setEnabled(False)
652
- self.process_next_conversion()
653
-
654
- def process_next_conversion(self):
655
- if not self.conversion_queue:
656
- self.log_message("πŸŽ‰ All conversions completed!")
657
- self.status_label.setText("βœ… All conversions completed!")
658
- self.progress_bar.setValue(100)
659
- self.convert_btn.setEnabled(True)
660
- QMessageBox.information(self, "Success",
661
- f"Conversion complete!\nβœ… Success: {self.converted_count}\n❌ Failed: {self.failed_count}")
662
- return
663
-
664
- input_path = self.conversion_queue.pop(0)
665
- self.current_conversion = input_path
666
-
667
- # Get output path
668
- output_format = self.output_format_combo.currentText()
669
- output_folder = self.output_folder.text()
670
- base_name = Path(input_path).stem
671
- output_path = Path(output_folder) / f"{base_name}.{output_format}"
672
-
673
- # Create output directory if needed
674
- Path(output_folder).mkdir(parents=True, exist_ok=True)
675
-
676
- # Quality settings
677
- quality_map = {
678
- "High": {"video": "18", "audio": "320k", "image": 95},
679
- "Medium": {"video": "23", "audio": "192k", "image": 85},
680
- "Low": {"video": "28", "audio": "128k", "image": 70}
681
- }
682
- quality = self.quality_combo.currentText()
683
-
684
- # Prepare options
685
- options = {
686
- "ai_enhancement": self.ai_checkbox.isChecked(),
687
- "quality": quality,
688
- "quality_settings": quality_map[quality]
689
- }
690
-
691
- # Start conversion thread
692
- converter = self.get_converter()
693
- if converter:
694
- self.conversion_thread = ConversionThread(
695
- converter, input_path, str(output_path), options
696
- )
697
- self.conversion_thread.progress_update.connect(self.update_progress)
698
- self.conversion_thread.status_update.connect(self.update_status)
699
- self.conversion_thread.conversion_complete.connect(self.on_conversion_complete)
700
- self.conversion_thread.start()
701
- self.active_threads.append(self.conversion_thread)
702
- else:
703
- self.log_message(f"❌ Invalid converter for: {input_path}")
704
- self.process_next_conversion()
705
-
706
- def update_progress(self, value):
707
- self.progress_bar.setValue(value)
708
-
709
- def update_status(self, status):
710
- self.status_label.setText(status)
711
- self.log_message(status)
712
-
713
- def on_conversion_complete(self, success, message):
714
- self.log_message(message)
715
- if success:
716
- self.converted_count += 1
717
- else:
718
- self.failed_count += 1
719
- self.update_stats()
720
- self.process_next_conversion()
721
-
722
- def update_stats(self):
723
- self.stats_label.setText(f"πŸ“ˆ Converted: {self.converted_count} | ❌ Failed: {self.failed_count}")
724
-
725
- def log_message(self, message):
726
- timestamp = datetime.now().strftime("%H:%M:%S")
727
- self.log_text.append(f"[{timestamp}] {message}")
728
-
729
- def ask_ai_assistant(self):
730
- """Process AI query and potentially auto-configure settings"""
731
- question = self.ai_input.toPlainText()
732
- if not question.strip():
733
- return
734
-
735
- self.ai_response.setPlainText("πŸ€” AI Assistant is thinking...")
736
-
737
- # Get AI response
738
- response = self.ai_assistant.ask(question, self.get_conversion_context())
739
-
740
- # Check if AI suggested a format change
741
- suggested_format = self.extract_format_from_response(response)
742
- if suggested_format:
743
- # Auto-update the output format combo box
744
- index = self.output_format_combo.findText(suggested_format.lower())
745
- if index >= 0:
746
- self.output_format_combo.setCurrentIndex(index)
747
- response += f"\n\nβœ… **Auto-configured**: Output format changed to {suggested_format.upper()}"
748
-
749
- # Check if AI suggested a file type change
750
- suggested_type = self.extract_file_type_from_response(response)
751
- if suggested_type:
752
- index = self.file_type_combo.findText(suggested_type.title())
753
- if index >= 0:
754
- self.file_type_combo.setCurrentIndex(index)
755
- response += f"\n\nβœ… **Auto-configured**: File type changed to {suggested_type.title()}"
756
-
757
- self.ai_response.setPlainText(response)
758
-
759
- def extract_format_from_response(self, response: str) -> str:
760
- """Extract suggested format from AI response"""
761
- formats = ['pdf', 'docx', 'txt', 'html', 'md', 'png', 'jpg', 'jpeg',
762
- 'webp', 'bmp', 'tiff', 'mp4', 'avi', 'mkv', 'mov', 'webm',
763
- 'gif', 'mp3', 'wav', 'ogg', 'm4a', 'flac']
764
-
765
- for fmt in formats:
766
- if f" {fmt.upper()}" in response.upper() or f"to {fmt}" in response.lower():
767
- return fmt
768
- return ""
769
-
770
- def extract_file_type_from_response(self, response: str) -> str:
771
- """Extract suggested file type from AI response"""
772
- types = ['document', 'image', 'video', 'audio']
773
- for t in types:
774
- if t in response.lower():
775
- return t
776
- return ""
777
-
778
- def get_conversion_context(self):
779
- return {
780
- "file_type": self.file_type_combo.currentText(),
781
- "output_format": self.output_format_combo.currentText(),
782
- "file_count": self.file_list.count(),
783
- "ai_enhancement": self.ai_checkbox.isChecked(),
784
- "quality": self.quality_combo.currentText()
785
- }
786
-
787
-
788
- if __name__ == "__main__":
789
- app = QApplication(sys.argv)
790
- window = FileConverterApp()
791
- window.show()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  sys.exit(app.exec())
 
1
+ import sys
2
+ import os
3
+ import json
4
+ import threading
5
+ import shutil
6
+ from pathlib import Path
7
+ from datetime import datetime
8
+
9
+ from PyQt6.QtWidgets import (
10
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
11
+ QPushButton, QLabel, QFileDialog, QComboBox, QProgressBar,
12
+ QTextEdit, QTabWidget, QGroupBox, QGridLayout, QMessageBox,
13
+ QListWidget, QListWidgetItem, QCheckBox, QSpinBox, QLineEdit,
14
+ QSplitter, QFrame, QToolButton, QSizePolicy
15
+ )
16
+ from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize, QPropertyAnimation, QEasingCurve, QRect
17
+ from PyQt6.QtGui import QFont, QIcon, QDragEnterEvent, QDropEvent, QPalette, QColor, QAction, QBrush, QPen, QPainter, \
18
+ QPixmap
19
+
20
+ # Fix FFmpeg before any imports
21
+ import warnings
22
+
23
+ warnings.filterwarnings("ignore", message="Couldn't find ffmpeg or avconv")
24
+
25
+ # Setup FFmpeg
26
+ ffmpeg_path = None
27
+ for path in [
28
+ shutil.which('ffmpeg'),
29
+ shutil.which('ffmpeg.exe'),
30
+ r"C:\ffmpeg\bin\ffmpeg.exe",
31
+ r"C:\Program Files\ffmpeg\bin\ffmpeg.exe"
32
+ ]:
33
+ if path and os.path.exists(path):
34
+ ffmpeg_path = path
35
+ os.environ["FFMPEG_BINARY"] = ffmpeg_path
36
+ break
37
+
38
+ # Import converters
39
+ from converters.document_converter import DocumentConverter
40
+ from converters.image_converter import ImageConverter
41
+ from converters.video_converter import VideoConverter
42
+ from converters.audio_converter import AudioConverter
43
+ from ai.ai_assistant import AIAssistant
44
+
45
+
46
+ class ConversionThread(QThread):
47
+ progress_update = pyqtSignal(int)
48
+ status_update = pyqtSignal(str)
49
+ conversion_complete = pyqtSignal(bool, str)
50
+
51
+ def __init__(self, converter, input_path, output_path, options=None):
52
+ super().__init__()
53
+ self.converter = converter
54
+ self.input_path = input_path
55
+ self.output_path = output_path
56
+ self.options = options or {}
57
+
58
+ def run(self):
59
+ try:
60
+ self.status_update.emit(f"Converting: {os.path.basename(self.input_path)}")
61
+ success = self.converter.convert(
62
+ self.input_path,
63
+ self.output_path,
64
+ self.options,
65
+ self.progress_update
66
+ )
67
+
68
+ if success:
69
+ self.conversion_complete.emit(True, f"βœ… Successfully converted: {os.path.basename(self.input_path)}")
70
+ else:
71
+ self.conversion_complete.emit(False, f"❌ Failed to convert: {os.path.basename(self.input_path)}")
72
+ except Exception as e:
73
+ self.conversion_complete.emit(False, f"❌ Error: {str(e)}")
74
+
75
+
76
+ class FileConverterApp(QMainWindow):
77
+ def __init__(self):
78
+ super().__init__()
79
+ self.setWindowTitle("AI File Converter Pro")
80
+ self.setGeometry(100, 100, 1400, 900)
81
+
82
+ # ===== ADD LOGO SUPPORT =====
83
+ self.setup_logo()
84
+ # ============================
85
+
86
+ # Sidebar state
87
+ self.sidebar_visible = True
88
+
89
+ # Set modern style
90
+ self.setStyleSheet("""
91
+ QMainWindow {
92
+ background-color: #1e1e2e;
93
+ }
94
+ QGroupBox {
95
+ font-weight: bold;
96
+ border: 2px solid #313244;
97
+ border-radius: 8px;
98
+ margin-top: 10px;
99
+ padding-top: 10px;
100
+ color: #cdd6f4;
101
+ font-size: 13px;
102
+ }
103
+ QGroupBox::title {
104
+ subcontrol-origin: margin;
105
+ left: 10px;
106
+ padding: 0 5px 0 5px;
107
+ color: #89b4fa;
108
+ }
109
+ QPushButton {
110
+ background-color: #89b4fa;
111
+ border: none;
112
+ padding: 8px;
113
+ border-radius: 6px;
114
+ font-weight: bold;
115
+ color: #1e1e2e;
116
+ }
117
+ QPushButton:hover {
118
+ background-color: #b4befe;
119
+ }
120
+ QPushButton:pressed {
121
+ background-color: #6c7086;
122
+ }
123
+ QToolButton {
124
+ background-color: #313244;
125
+ border: 1px solid #45475a;
126
+ border-radius: 6px;
127
+ padding: 8px;
128
+ color: #cdd6f4;
129
+ }
130
+ QToolButton:hover {
131
+ background-color: #45475a;
132
+ }
133
+ QListWidget, QTextEdit, QLineEdit, QComboBox {
134
+ background-color: #313244;
135
+ border: 1px solid #45475a;
136
+ border-radius: 6px;
137
+ padding: 5px;
138
+ color: #cdd6f4;
139
+ }
140
+ QProgressBar {
141
+ border: 2px solid #45475a;
142
+ border-radius: 6px;
143
+ text-align: center;
144
+ color: #cdd6f4;
145
+ }
146
+ QProgressBar::chunk {
147
+ background-color: #89b4fa;
148
+ border-radius: 4px;
149
+ }
150
+ QTabWidget::pane {
151
+ background-color: #1e1e2e;
152
+ border: 1px solid #313244;
153
+ border-radius: 6px;
154
+ }
155
+ QTabBar::tab {
156
+ background-color: #313244;
157
+ padding: 8px;
158
+ margin: 2px;
159
+ border-radius: 4px;
160
+ color: #cdd6f4;
161
+ }
162
+ QTabBar::tab:selected {
163
+ background-color: #89b4fa;
164
+ color: #1e1e2e;
165
+ }
166
+ QLabel {
167
+ color: #cdd6f4;
168
+ }
169
+ QCheckBox {
170
+ color: #cdd6f4;
171
+ }
172
+ QSplitter::handle {
173
+ background-color: #313244;
174
+ margin: 2px;
175
+ }
176
+ QSplitter::handle:hover {
177
+ background-color: #89b4fa;
178
+ }
179
+ """)
180
+
181
+ # Initialize converters and other components...
182
+ self.doc_converter = DocumentConverter()
183
+ self.img_converter = ImageConverter()
184
+ self.video_converter = VideoConverter()
185
+ self.audio_converter = AudioConverter()
186
+ self.ai_assistant = AIAssistant()
187
+
188
+ # Conversion queue
189
+ self.conversion_queue = []
190
+ self.current_conversion = None
191
+ self.active_threads = []
192
+ self.converted_count = 0
193
+ self.failed_count = 0
194
+
195
+ self.setup_ui()
196
+ self.setup_drag_drop()
197
+ self.setup_menu_bar()
198
+
199
+ def setup_logo(self):
200
+ """Setup window icon and UI logo"""
201
+ # Try multiple locations for the logo file
202
+ logo_paths = [
203
+ Path(__file__).parent / "logo.png",
204
+ Path(__file__).parent / "logo.ico",
205
+ Path(__file__).parent / "assets" / "logo.png",
206
+ Path(__file__).parent / "assets" / "logo.ico",
207
+ # For PyInstaller bundled app
208
+ Path(sys.executable).parent / "logo.png",
209
+ Path(sys.executable).parent / "logo.ico",
210
+ Path(sys.executable).parent / "assets" / "logo.png",
211
+ ]
212
+
213
+ icon_loaded = False
214
+ for logo_path in logo_paths:
215
+ if logo_path.exists():
216
+ # Set window icon (appears in title bar and taskbar)
217
+ self.setWindowIcon(QIcon(str(logo_path)))
218
+ icon_loaded = True
219
+ print(f"βœ… Logo loaded from: {logo_path}")
220
+ break
221
+
222
+ if not icon_loaded:
223
+ # Create default icon programmatically
224
+ self.setWindowIcon(self.create_default_icon())
225
+ print("⚠️ Using default generated icon. Place 'logo.png' in the application directory for custom logo.")
226
+
227
+ def create_default_icon(self):
228
+ """Create a default icon programmatically"""
229
+ pixmap = QPixmap(256, 256)
230
+ pixmap.fill(Qt.GlobalColor.transparent)
231
+
232
+ painter = QPainter(pixmap)
233
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
234
+
235
+ # Draw circular background
236
+ painter.setBrush(QBrush(QColor(137, 180, 250))) # #89b4fa
237
+ painter.setPen(Qt.PenStyle.NoPen)
238
+ painter.drawEllipse(20, 20, 216, 216)
239
+
240
+ # Draw inner circle
241
+ painter.setBrush(QBrush(QColor(30, 30, 46))) # #1e1e2e
242
+ painter.drawEllipse(40, 40, 176, 176)
243
+
244
+ # Draw "AI" text
245
+ painter.setPen(QPen(QColor(137, 180, 250), 2))
246
+ font = QFont("Arial", 80, QFont.Weight.Bold)
247
+ painter.setFont(font)
248
+ painter.drawText(QRect(40, 70, 176, 100), Qt.AlignmentFlag.AlignCenter, "AI")
249
+
250
+ # Draw conversion arrows
251
+ painter.setPen(QPen(QColor(166, 227, 161), 5)) # #a6e3a1
252
+ painter.setBrush(Qt.BrushStyle.NoBrush)
253
+
254
+ # Right arrow
255
+ painter.drawLine(180, 180, 210, 195)
256
+ painter.drawLine(210, 195, 180, 210)
257
+
258
+ # Left arrow
259
+ painter.drawLine(76, 180, 46, 195)
260
+ painter.drawLine(46, 195, 76, 210)
261
+
262
+ painter.end()
263
+
264
+ return QIcon(pixmap)
265
+
266
+ def create_ui_logo(self, size=48):
267
+ """Create a logo widget for the UI"""
268
+ logo_label = QLabel()
269
+
270
+ # Try to load from file first
271
+ logo_path = Path(__file__).parent / "logo.png"
272
+ if not logo_path.exists() and hasattr(sys, 'frozen'):
273
+ # If running as bundled executable
274
+ logo_path = Path(sys.executable).parent / "logo.png"
275
+
276
+ if logo_path.exists():
277
+ pixmap = QPixmap(str(logo_path))
278
+ scaled_pixmap = pixmap.scaled(size, size,
279
+ Qt.AspectRatioMode.KeepAspectRatio,
280
+ Qt.TransformationMode.SmoothTransformation)
281
+ logo_label.setPixmap(scaled_pixmap)
282
+ else:
283
+ # Create programmatic logo
284
+ pixmap = QPixmap(size, size)
285
+ pixmap.fill(Qt.GlobalColor.transparent)
286
+
287
+ painter = QPainter(pixmap)
288
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
289
+
290
+ # Draw circle
291
+ painter.setBrush(QBrush(QColor(137, 180, 250)))
292
+ painter.setPen(Qt.PenStyle.NoPen)
293
+ painter.drawEllipse(4, 4, size - 8, size - 8)
294
+
295
+ # Draw inner circle
296
+ painter.setBrush(QBrush(QColor(30, 30, 46)))
297
+ painter.drawEllipse(8, 8, size - 16, size - 16)
298
+
299
+ # Draw text
300
+ painter.setPen(QPen(QColor(137, 180, 250), 1))
301
+ font = QFont("Arial", size // 3, QFont.Weight.Bold)
302
+ painter.setFont(font)
303
+ painter.drawText(QRect(8, 8, size - 16, size - 16),
304
+ Qt.AlignmentFlag.AlignCenter, "AI")
305
+
306
+ painter.end()
307
+ logo_label.setPixmap(pixmap)
308
+
309
+ logo_label.setFixedSize(size, size)
310
+ logo_label.setToolTip("AI File Converter Pro")
311
+ return logo_label
312
+
313
+ def setup_ui(self):
314
+ central_widget = QWidget()
315
+ self.setCentralWidget(central_widget)
316
+ main_layout = QHBoxLayout(central_widget)
317
+ main_layout.setContentsMargins(0, 0, 0, 0)
318
+ main_layout.setSpacing(0)
319
+
320
+ # Create splitter for resizable panels
321
+ self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
322
+
323
+ # Left panel - Main content (WITH LOGO IN TOP-LEFT CORNER)
324
+ self.left_panel = self.create_left_panel_with_logo()
325
+ self.main_splitter.addWidget(self.left_panel)
326
+
327
+ # Right panel - AI Sidebar (collapsible)
328
+ self.sidebar_widget = self.create_sidebar()
329
+ self.main_splitter.addWidget(self.sidebar_widget)
330
+
331
+ # Set initial sizes (70% left, 30% right)
332
+ self.main_splitter.setSizes([980, 420])
333
+
334
+ main_layout.addWidget(self.main_splitter)
335
+
336
+ def setup_menu_bar(self):
337
+ """Setup menu bar with toggle option"""
338
+ menubar = self.menuBar()
339
+ menubar.setStyleSheet("""
340
+ QMenuBar {
341
+ background-color: #1e1e2e;
342
+ color: #cdd6f4;
343
+ border-bottom: 1px solid #313244;
344
+ }
345
+ QMenuBar::item:selected {
346
+ background-color: #313244;
347
+ }
348
+ QMenu {
349
+ background-color: #1e1e2e;
350
+ color: #cdd6f4;
351
+ border: 1px solid #313244;
352
+ }
353
+ QMenu::item:selected {
354
+ background-color: #89b4fa;
355
+ color: #1e1e2e;
356
+ }
357
+ """)
358
+
359
+ # File menu
360
+ file_menu = menubar.addMenu("πŸ“ File")
361
+
362
+ exit_action = QAction("πŸšͺ Exit", self)
363
+ exit_action.setShortcut("Ctrl+Q")
364
+ exit_action.triggered.connect(self.close)
365
+ file_menu.addAction(exit_action)
366
+
367
+ # View menu
368
+ view_menu = menubar.addMenu("πŸ‘οΈ View")
369
+
370
+ toggle_action = QAction("πŸ€– Toggle AI Assistant", self)
371
+ toggle_action.setShortcut("Ctrl+Shift+A")
372
+ toggle_action.triggered.connect(self.toggle_sidebar)
373
+ view_menu.addAction(toggle_action)
374
+
375
+ view_menu.addSeparator()
376
+
377
+ reset_view_action = QAction("πŸ”„ Reset Layout", self)
378
+ reset_view_action.setShortcut("Ctrl+Shift+R")
379
+ reset_view_action.triggered.connect(self.reset_layout)
380
+ view_menu.addAction(reset_view_action)
381
+
382
+ # Help menu
383
+ help_menu = menubar.addMenu("❓ Help")
384
+
385
+ about_action = QAction("πŸ“– About", self)
386
+ about_action.setShortcut("F1")
387
+ about_action.triggered.connect(self.show_about)
388
+ help_menu.addAction(about_action)
389
+
390
+ def show_about(self):
391
+ """Show about dialog with logo"""
392
+ about_text = """<div style='text-align: center;'>
393
+ <h2 style='color: #89b4fa;'>🎯 AI File Converter Pro</h2>
394
+ <p><b>Version 2.0</b></p>
395
+ <p>Advanced file conversion tool with AI assistance</p>
396
+ <br>
397
+ <p><b>Features:</b></p>
398
+ <p>β€’ Convert documents, images, videos, and audio</p>
399
+ <p>β€’ AI-powered format recommendations</p>
400
+ <p>β€’ Natural language commands</p>
401
+ <p>β€’ Batch processing support</p>
402
+ <p>β€’ Quality optimization</p>
403
+ <br>
404
+ <p>Β© 2024 AI File Converter Pro</p>
405
+ <p>All rights reserved</p>
406
+ </div>"""
407
+
408
+ about_dialog = QMessageBox(self)
409
+ about_dialog.setWindowTitle("About AI File Converter Pro")
410
+ about_dialog.setText(about_text)
411
+
412
+ # Add logo to about dialog
413
+ logo_path = Path(__file__).parent / "logo.png"
414
+ if not logo_path.exists() and hasattr(sys, 'frozen'):
415
+ logo_path = Path(sys.executable).parent / "logo.png"
416
+
417
+ if logo_path.exists():
418
+ logo_pixmap = QPixmap(str(logo_path)).scaled(64, 64,
419
+ Qt.AspectRatioMode.KeepAspectRatio,
420
+ Qt.TransformationMode.SmoothTransformation)
421
+ about_dialog.setIconPixmap(logo_pixmap)
422
+
423
+ about_dialog.exec()
424
+
425
+ def create_left_panel_with_logo(self):
426
+ """Create the main left panel with logo in top-left corner"""
427
+ left_panel = QWidget()
428
+ left_layout = QVBoxLayout(left_panel)
429
+ left_layout.setSpacing(10)
430
+ left_layout.setContentsMargins(10, 10, 10, 10)
431
+
432
+ # ===== ADD LOGO HEADER SECTION =====
433
+ header_widget = QWidget()
434
+ header_layout = QHBoxLayout(header_widget)
435
+ header_layout.setContentsMargins(0, 0, 0, 10)
436
+
437
+ # Add logo to top-left corner
438
+ logo_widget = self.create_ui_logo(48)
439
+ header_layout.addWidget(logo_widget)
440
+
441
+ # Add title next to logo
442
+ title_label = QLabel("AI File Converter Pro")
443
+ title_label.setStyleSheet("""
444
+ font-size: 20px;
445
+ font-weight: bold;
446
+ color: #89b4fa;
447
+ padding: 5px;
448
+ """)
449
+ header_layout.addWidget(title_label)
450
+
451
+ header_layout.addStretch()
452
+
453
+ # Optional: Add version label
454
+ version_label = QLabel("v2.0")
455
+ version_label.setStyleSheet("color: #6c7086; font-size: 12px;")
456
+ header_layout.addWidget(version_label)
457
+
458
+ left_layout.addWidget(header_widget)
459
+ # ==================================
460
+
461
+ # File selection area
462
+ file_group = QGroupBox("πŸ“ Files to Convert")
463
+ file_layout = QVBoxLayout()
464
+
465
+ self.file_list = QListWidget()
466
+ self.file_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
467
+ self.file_list.setMinimumHeight(200)
468
+ file_layout.addWidget(self.file_list)
469
+
470
+ btn_layout = QHBoxLayout()
471
+ add_btn = QPushButton("βž• Add Files")
472
+ add_btn.clicked.connect(self.add_files)
473
+ add_folder_btn = QPushButton("πŸ“‚ Add Folder")
474
+ add_folder_btn.clicked.connect(self.add_folder)
475
+ remove_btn = QPushButton("❌ Remove Selected")
476
+ remove_btn.clicked.connect(self.remove_selected)
477
+ clear_btn = QPushButton("πŸ—‘οΈ Clear All")
478
+ clear_btn.clicked.connect(self.clear_files)
479
+
480
+ btn_layout.addWidget(add_btn)
481
+ btn_layout.addWidget(add_folder_btn)
482
+ btn_layout.addWidget(remove_btn)
483
+ btn_layout.addWidget(clear_btn)
484
+ file_layout.addLayout(btn_layout)
485
+
486
+ file_group.setLayout(file_layout)
487
+ left_layout.addWidget(file_group)
488
+
489
+ # Converter settings
490
+ converter_group = QGroupBox("βš™οΈ Converter Settings")
491
+ converter_layout = QGridLayout()
492
+ converter_layout.setSpacing(10)
493
+
494
+ converter_layout.addWidget(QLabel("File Type:"), 0, 0)
495
+ self.file_type_combo = QComboBox()
496
+ self.file_type_combo.addItems(["Document", "Image", "Video", "Audio"])
497
+ self.file_type_combo.currentTextChanged.connect(self.on_file_type_changed)
498
+ converter_layout.addWidget(self.file_type_combo, 0, 1)
499
+
500
+ converter_layout.addWidget(QLabel("Output Format:"), 1, 0)
501
+ self.output_format_combo = QComboBox()
502
+ converter_layout.addWidget(self.output_format_combo, 1, 1)
503
+
504
+ converter_layout.addWidget(QLabel("Quality:"), 2, 0)
505
+ self.quality_combo = QComboBox()
506
+ self.quality_combo.addItems(["High", "Medium", "Low"])
507
+ converter_layout.addWidget(self.quality_combo, 2, 1)
508
+
509
+ converter_layout.addWidget(QLabel("Output Folder:"), 3, 0)
510
+ self.output_folder = QLineEdit()
511
+ self.output_folder.setText(str(Path.home() / "Downloads" / "Converted"))
512
+ converter_layout.addWidget(self.output_folder, 3, 1)
513
+
514
+ browse_btn = QPushButton("πŸ“ Browse")
515
+ browse_btn.clicked.connect(self.browse_output_folder)
516
+ converter_layout.addWidget(browse_btn, 3, 2)
517
+
518
+ # Create output folder if not exists
519
+ Path(self.output_folder.text()).mkdir(parents=True, exist_ok=True)
520
+
521
+ self.ai_checkbox = QCheckBox("πŸ€– Enable AI Enhancement")
522
+ self.ai_checkbox.setChecked(False)
523
+ converter_layout.addWidget(self.ai_checkbox, 4, 0, 1, 2)
524
+
525
+ converter_group.setLayout(converter_layout)
526
+ left_layout.addWidget(converter_group)
527
+
528
+ # Progress section
529
+ progress_group = QGroupBox("πŸ“Š Conversion Progress")
530
+ progress_layout = QVBoxLayout()
531
+
532
+ self.progress_bar = QProgressBar()
533
+ progress_layout.addWidget(self.progress_bar)
534
+
535
+ self.status_label = QLabel("βœ… Ready to convert")
536
+ self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
537
+ progress_layout.addWidget(self.status_label)
538
+
539
+ self.stats_label = QLabel("πŸ“ˆ Converted: 0 | ❌ Failed: 0")
540
+ self.stats_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
541
+ progress_layout.addWidget(self.stats_label)
542
+
543
+ progress_group.setLayout(progress_layout)
544
+ left_layout.addWidget(progress_group)
545
+
546
+ # Convert button
547
+ self.convert_btn = QPushButton("πŸš€ Start Conversion")
548
+ self.convert_btn.setMinimumHeight(50)
549
+ self.convert_btn.setStyleSheet("""
550
+ QPushButton {
551
+ background-color: #a6e3a1;
552
+ font-size: 16px;
553
+ font-weight: bold;
554
+ }
555
+ QPushButton:hover {
556
+ background-color: #94e2d5;
557
+ }
558
+ """)
559
+ self.convert_btn.clicked.connect(self.start_conversion)
560
+ left_layout.addWidget(self.convert_btn)
561
+
562
+ return left_panel
563
+
564
+ def create_sidebar(self):
565
+ """Create the collapsible AI sidebar"""
566
+ sidebar_container = QWidget()
567
+ sidebar_layout = QVBoxLayout(sidebar_container)
568
+ sidebar_layout.setContentsMargins(0, 0, 0, 0)
569
+ sidebar_layout.setSpacing(0)
570
+
571
+ # Header with toggle button
572
+ header = QWidget()
573
+ header.setStyleSheet("""
574
+ QWidget {
575
+ background-color: #313244;
576
+ border-bottom: 1px solid #45475a;
577
+ }
578
+ """)
579
+ header_layout = QHBoxLayout(header)
580
+ header_layout.setContentsMargins(10, 10, 10, 10)
581
+
582
+ title_label = QLabel("πŸ€– AI Assistant")
583
+ title_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #89b4fa;")
584
+ header_layout.addWidget(title_label)
585
+
586
+ header_layout.addStretch()
587
+
588
+ # Close button
589
+ self.close_sidebar_btn = QToolButton()
590
+ self.close_sidebar_btn.setText("β—€")
591
+ self.close_sidebar_btn.setToolTip("Close Sidebar (Ctrl+Shift+A)")
592
+ self.close_sidebar_btn.setStyleSheet("""
593
+ QToolButton {
594
+ background-color: #45475a;
595
+ border: none;
596
+ font-size: 14px;
597
+ padding: 5px;
598
+ }
599
+ QToolButton:hover {
600
+ background-color: #6c7086;
601
+ }
602
+ """)
603
+ self.close_sidebar_btn.clicked.connect(self.toggle_sidebar)
604
+ header_layout.addWidget(self.close_sidebar_btn)
605
+
606
+ sidebar_layout.addWidget(header)
607
+
608
+ # Tab widget for AI and Log
609
+ right_panel = QTabWidget()
610
+ right_panel.setStyleSheet("""
611
+ QTabWidget::pane {
612
+ border: none;
613
+ }
614
+ """)
615
+
616
+ # AI Assistant tab
617
+ ai_widget = QWidget()
618
+ ai_layout = QVBoxLayout(ai_widget)
619
+ ai_layout.setSpacing(10)
620
+
621
+ # Suggestions panel
622
+ suggestions_group = QGroupBox("πŸ’‘ Quick Commands")
623
+ suggestions_layout = QGridLayout()
624
+ suggestions_group.setMaximumHeight(220)
625
+
626
+ suggestions = [
627
+ ("πŸ–ΌοΈ To PNG", "convert this to png"),
628
+ ("πŸ“„ To PDF", "convert to pdf"),
629
+ ("🎡 To MP3", "make it mp3"),
630
+ ("🎬 Web format", "best format for web"),
631
+ ("πŸ“Š Analyze", "analyze this file"),
632
+ ("πŸ’Ύ Compress", "how to compress video"),
633
+ ("πŸ“§ Email size", "best format for email"),
634
+ ("πŸ–¨οΈ Print", "best format for printing"),
635
+ ]
636
+
637
+ row, col = 0, 0
638
+ for text, cmd in suggestions:
639
+ btn = QPushButton(text)
640
+ btn.setMaximumHeight(30)
641
+ btn.clicked.connect(lambda checked, c=cmd: self.ai_input.setText(c))
642
+ suggestions_layout.addWidget(btn, row, col)
643
+ col += 1
644
+ if col >= 2:
645
+ col = 0
646
+ row += 1
647
+
648
+ suggestions_group.setLayout(suggestions_layout)
649
+ ai_layout.addWidget(suggestions_group)
650
+
651
+ # Input area
652
+ self.ai_input = QTextEdit()
653
+ self.ai_input.setPlaceholderText("""πŸ’¬ Ask me anything about file conversion...
654
+
655
+ Examples:
656
+ β€’ "Convert this to PNG"
657
+ β€’ "What's the best format for web images?"
658
+ β€’ "How to compress video for email?"
659
+ β€’ "Analyze this file"
660
+ β€’ "Make it smaller without losing quality" """)
661
+ self.ai_input.setMaximumHeight(100)
662
+ ai_layout.addWidget(self.ai_input)
663
+
664
+ # Buttons
665
+ ai_buttons_layout = QHBoxLayout()
666
+ ai_send_btn = QPushButton("πŸ’­ Ask AI")
667
+ ai_send_btn.clicked.connect(self.ask_ai_assistant)
668
+ ai_clear_btn = QPushButton("πŸ—‘οΈ Clear")
669
+ ai_clear_btn.clicked.connect(lambda: self.ai_input.clear())
670
+ ai_buttons_layout.addWidget(ai_send_btn)
671
+ ai_buttons_layout.addWidget(ai_clear_btn)
672
+ ai_layout.addLayout(ai_buttons_layout)
673
+
674
+ # Response area
675
+ self.ai_response = QTextEdit()
676
+ self.ai_response.setReadOnly(True)
677
+ self.ai_response.setPlaceholderText("AI response will appear here...")
678
+ self.ai_response.setMinimumHeight(200)
679
+ ai_layout.addWidget(self.ai_response)
680
+
681
+ right_panel.addTab(ai_widget, "πŸ’¬ Assistant")
682
+
683
+ # Log tab
684
+ log_widget = QWidget()
685
+ log_layout = QVBoxLayout(log_widget)
686
+ self.log_text = QTextEdit()
687
+ self.log_text.setReadOnly(True)
688
+ log_layout.addWidget(self.log_text)
689
+
690
+ log_btn_layout = QHBoxLayout()
691
+ clear_log_btn = QPushButton("πŸ—‘οΈ Clear Log")
692
+ clear_log_btn.clicked.connect(lambda: self.log_text.clear())
693
+ log_btn_layout.addStretch()
694
+ log_btn_layout.addWidget(clear_log_btn)
695
+ log_layout.addLayout(log_btn_layout)
696
+
697
+ right_panel.addTab(log_widget, "πŸ“ Log")
698
+
699
+ sidebar_layout.addWidget(right_panel)
700
+
701
+ return sidebar_container
702
+
703
+ def toggle_sidebar(self):
704
+ """Toggle the AI sidebar visibility"""
705
+ self.sidebar_visible = not self.sidebar_visible
706
+
707
+ if self.sidebar_visible:
708
+ self.sidebar_widget.setVisible(True)
709
+ self.close_sidebar_btn.setText("β—€")
710
+ self.main_splitter.setSizes([980, 420])
711
+ else:
712
+ self.sidebar_widget.setVisible(False)
713
+ self.close_sidebar_btn.setText("β–Ά")
714
+ self.main_splitter.setSizes([1400, 0])
715
+
716
+ # Update tooltip
717
+ self.close_sidebar_btn.setToolTip(
718
+ "Open Sidebar (Ctrl+Shift+A)" if not self.sidebar_visible else "Close Sidebar (Ctrl+Shift+A)")
719
+
720
+ def reset_layout(self):
721
+ """Reset layout to default"""
722
+ self.main_splitter.setSizes([980, 420])
723
+ if not self.sidebar_visible:
724
+ self.toggle_sidebar()
725
+ self.log_message("πŸ”„ Layout reset to default")
726
+
727
+ def setup_drag_drop(self):
728
+ self.setAcceptDrops(True)
729
+
730
+ def dragEnterEvent(self, event: QDragEnterEvent):
731
+ if event.mimeData().hasUrls():
732
+ event.acceptProposedAction()
733
+
734
+ def dropEvent(self, event: QDropEvent):
735
+ for url in event.mimeData().urls():
736
+ file_path = url.toLocalFile()
737
+ if os.path.isfile(file_path):
738
+ self.add_file_to_list(file_path)
739
+
740
+ def add_files(self):
741
+ files, _ = QFileDialog.getOpenFileNames(
742
+ self, "Select Files to Convert", "",
743
+ "All Files (*.*)"
744
+ )
745
+ for file in files:
746
+ self.add_file_to_list(file)
747
+
748
+ def add_folder(self):
749
+ folder = QFileDialog.getExistingDirectory(self, "Select Folder")
750
+ if folder:
751
+ for root, dirs, files in os.walk(folder):
752
+ for file in files:
753
+ self.add_file_to_list(os.path.join(root, file))
754
+
755
+ def remove_selected(self):
756
+ for item in self.file_list.selectedItems():
757
+ self.file_list.takeItem(self.file_list.row(item))
758
+
759
+ def add_file_to_list(self, file_path):
760
+ # Check if already in list
761
+ for i in range(self.file_list.count()):
762
+ if self.file_list.item(i).data(Qt.ItemDataRole.UserRole) == file_path:
763
+ return
764
+ item = QListWidgetItem(f"πŸ“„ {os.path.basename(file_path)}")
765
+ item.setToolTip(file_path)
766
+ item.setData(Qt.ItemDataRole.UserRole, file_path)
767
+ self.file_list.addItem(item)
768
+
769
+ def clear_files(self):
770
+ self.file_list.clear()
771
+
772
+ def on_file_type_changed(self, file_type):
773
+ self.output_format_combo.clear()
774
+
775
+ formats = {
776
+ "Document": ["pdf", "docx", "txt", "html", "md"],
777
+ "Image": ["png", "jpg", "webp", "bmp", "tiff"],
778
+ "Video": ["mp4", "avi", "mkv", "mov", "webm", "gif"],
779
+ "Audio": ["mp3", "wav", "ogg", "m4a", "flac"]
780
+ }
781
+ self.output_format_combo.addItems(formats.get(file_type, []))
782
+
783
+ def browse_output_folder(self):
784
+ folder = QFileDialog.getExistingDirectory(self, "Select Output Folder")
785
+ if folder:
786
+ self.output_folder.setText(folder)
787
+ Path(folder).mkdir(parents=True, exist_ok=True)
788
+
789
+ def get_converter(self):
790
+ file_type = self.file_type_combo.currentText()
791
+ converters = {
792
+ "Document": self.doc_converter,
793
+ "Image": self.img_converter,
794
+ "Video": self.video_converter,
795
+ "Audio": self.audio_converter
796
+ }
797
+ return converters.get(file_type)
798
+
799
+ def start_conversion(self):
800
+ if self.file_list.count() == 0:
801
+ QMessageBox.warning(self, "Warning", "Please add files to convert")
802
+ return
803
+
804
+ # Reset stats
805
+ self.converted_count = 0
806
+ self.failed_count = 0
807
+ self.update_stats()
808
+
809
+ # Build queue
810
+ self.conversion_queue = []
811
+ selected_items = self.file_list.selectedItems()
812
+ if selected_items:
813
+ for item in selected_items:
814
+ self.conversion_queue.append(item.data(Qt.ItemDataRole.UserRole))
815
+ else:
816
+ for i in range(self.file_list.count()):
817
+ self.conversion_queue.append(self.file_list.item(i).data(Qt.ItemDataRole.UserRole))
818
+
819
+ self.convert_btn.setEnabled(False)
820
+ self.process_next_conversion()
821
+
822
+ def process_next_conversion(self):
823
+ if not self.conversion_queue:
824
+ self.log_message("πŸŽ‰ All conversions completed!")
825
+ self.status_label.setText("βœ… All conversions completed!")
826
+ self.progress_bar.setValue(100)
827
+ self.convert_btn.setEnabled(True)
828
+ QMessageBox.information(self, "Success",
829
+ f"Conversion complete!\nβœ… Success: {self.converted_count}\n❌ Failed: {self.failed_count}")
830
+ return
831
+
832
+ input_path = self.conversion_queue.pop(0)
833
+ self.current_conversion = input_path
834
+
835
+ # Get output path
836
+ output_format = self.output_format_combo.currentText()
837
+ output_folder = self.output_folder.text()
838
+ base_name = Path(input_path).stem
839
+ output_path = Path(output_folder) / f"{base_name}.{output_format}"
840
+
841
+ # Create output directory if needed
842
+ Path(output_folder).mkdir(parents=True, exist_ok=True)
843
+
844
+ # Quality settings
845
+ quality_map = {
846
+ "High": {"video": "18", "audio": "320k", "image": 95},
847
+ "Medium": {"video": "23", "audio": "192k", "image": 85},
848
+ "Low": {"video": "28", "audio": "128k", "image": 70}
849
+ }
850
+ quality = self.quality_combo.currentText()
851
+
852
+ # Prepare options
853
+ options = {
854
+ "ai_enhancement": self.ai_checkbox.isChecked(),
855
+ "quality": quality,
856
+ "quality_settings": quality_map[quality]
857
+ }
858
+
859
+ # Start conversion thread
860
+ converter = self.get_converter()
861
+ if converter:
862
+ self.conversion_thread = ConversionThread(
863
+ converter, input_path, str(output_path), options
864
+ )
865
+ self.conversion_thread.progress_update.connect(self.update_progress)
866
+ self.conversion_thread.status_update.connect(self.update_status)
867
+ self.conversion_thread.conversion_complete.connect(self.on_conversion_complete)
868
+ self.conversion_thread.start()
869
+ self.active_threads.append(self.conversion_thread)
870
+ else:
871
+ self.log_message(f"❌ Invalid converter for: {input_path}")
872
+ self.process_next_conversion()
873
+
874
+ def update_progress(self, value):
875
+ self.progress_bar.setValue(value)
876
+
877
+ def update_status(self, status):
878
+ self.status_label.setText(status)
879
+ self.log_message(status)
880
+
881
+ def on_conversion_complete(self, success, message):
882
+ self.log_message(message)
883
+ if success:
884
+ self.converted_count += 1
885
+ else:
886
+ self.failed_count += 1
887
+ self.update_stats()
888
+ self.process_next_conversion()
889
+
890
+ def update_stats(self):
891
+ self.stats_label.setText(f"πŸ“ˆ Converted: {self.converted_count} | ❌ Failed: {self.failed_count}")
892
+
893
+ def log_message(self, message):
894
+ timestamp = datetime.now().strftime("%H:%M:%S")
895
+ self.log_text.append(f"[{timestamp}] {message}")
896
+
897
+ def ask_ai_assistant(self):
898
+ """Process AI query and potentially auto-configure settings"""
899
+ question = self.ai_input.toPlainText()
900
+ if not question.strip():
901
+ return
902
+
903
+ self.ai_response.setPlainText("πŸ€” AI Assistant is thinking...")
904
+
905
+ # Get AI response
906
+ response = self.ai_assistant.ask(question, self.get_conversion_context())
907
+
908
+ # Check if AI suggested a format change
909
+ suggested_format = self.extract_format_from_response(response)
910
+ if suggested_format:
911
+ # Auto-update the output format combo box
912
+ index = self.output_format_combo.findText(suggested_format.lower())
913
+ if index >= 0:
914
+ self.output_format_combo.setCurrentIndex(index)
915
+ response += f"\n\nβœ… **Auto-configured**: Output format changed to {suggested_format.upper()}"
916
+
917
+ # Check if AI suggested a file type change
918
+ suggested_type = self.extract_file_type_from_response(response)
919
+ if suggested_type:
920
+ index = self.file_type_combo.findText(suggested_type.title())
921
+ if index >= 0:
922
+ self.file_type_combo.setCurrentIndex(index)
923
+ response += f"\n\nβœ… **Auto-configured**: File type changed to {suggested_type.title()}"
924
+
925
+ self.ai_response.setPlainText(response)
926
+
927
+ def extract_format_from_response(self, response: str) -> str:
928
+ """Extract suggested format from AI response"""
929
+ formats = ['pdf', 'docx', 'txt', 'html', 'md', 'png', 'jpg', 'jpeg',
930
+ 'webp', 'bmp', 'tiff', 'mp4', 'avi', 'mkv', 'mov', 'webm',
931
+ 'gif', 'mp3', 'wav', 'ogg', 'm4a', 'flac']
932
+
933
+ for fmt in formats:
934
+ if f" {fmt.upper()}" in response.upper() or f"to {fmt}" in response.lower():
935
+ return fmt
936
+ return ""
937
+
938
+ def extract_file_type_from_response(self, response: str) -> str:
939
+ """Extract suggested file type from AI response"""
940
+ types = ['document', 'image', 'video', 'audio']
941
+ for t in types:
942
+ if t in response.lower():
943
+ return t
944
+ return ""
945
+
946
+ def get_conversion_context(self):
947
+ return {
948
+ "file_type": self.file_type_combo.currentText(),
949
+ "output_format": self.output_format_combo.currentText(),
950
+ "file_count": self.file_list.count(),
951
+ "ai_enhancement": self.ai_checkbox.isChecked(),
952
+ "quality": self.quality_combo.currentText()
953
+ }
954
+
955
+
956
+ if __name__ == "__main__":
957
+ app = QApplication(sys.argv)
958
+ window = FileConverterApp()
959
+ window.show()
960
  sys.exit(app.exec())