diff --git a/gui/v2/ui/popups/Database_version.py b/gui/v2/ui/popups/Database_version.py new file mode 100644 index 0000000..d42c2d6 --- /dev/null +++ b/gui/v2/ui/popups/Database_version.py @@ -0,0 +1,290 @@ +from core.Constants import Constants + +from PyQt6.QtWidgets import QApplication, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QScrollArea +from PyQt6.QtCore import Qt, QSize +from PyQt6.QtGui import QIcon, QFont, QColor, QPalette, QPixmap, QPainter +import sys + +APP_VERSION = Constants.DB_VERSION_THIS_APP_WANTS + +class DatabaseConflictDialog(QDialog): + WIPE = 1 + MOVE_DB = 2 + EXIT = 3 + + def __init__(self, check: dict, parent=None): + super().__init__(parent) + self.setWindowTitle("Database Recovery") + self.setModal(True) + self.user_choice = self.EXIT + self.setMinimumWidth(550) + self.setMinimumHeight(400) + self.setMaximumWidth(750) + self.setMaximumHeight(600) + + # Modern dark theme + self.setStyleSheet(""" + DatabaseConflictDialog { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #0f172a, stop:1 #1e293b); + } + """) + + main_layout = QVBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + # Header with gradient and icon + header = QFrame() + header.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #3b82f6, stop:1 #1e40af); + border: none; + } + """) + header.setFixedHeight(100) + header_layout = QHBoxLayout() + header_layout.setContentsMargins(30, 20, 30, 20) + + # Warning icon (using unicode) + icon_label = QLabel("⚠️") + icon_label.setFont(QFont("Segoe UI", 48)) + header_layout.addWidget(icon_label, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + + ######## Title Section. ############## + + # First which messages to display, + if check["status"] == "version_mismatch": + error_title = "Database Compatibility" + error_subtitle = "Your App version and Database don't match" + else: + error_title = "Database Error" + error_subtitle = "The Database can't properly start" + + # then display them: + title_layout = QVBoxLayout() + title_layout.setSpacing(5) + + title = QLabel(error_title) + title.setFont(QFont("Segoe UI", 18, QFont.Weight.Bold)) + title.setStyleSheet("color: white;") + title_layout.addWidget(title) + + subtitle = QLabel(error_subtitle) + subtitle.setFont(QFont("Segoe UI", 11)) + subtitle.setStyleSheet("color: rgba(255, 255, 255, 0.8);") + title_layout.addWidget(subtitle) + + header_layout.addLayout(title_layout, 1) + header.setLayout(header_layout) + main_layout.addWidget(header) + + # Content area with scroll support + content = QFrame() + content.setStyleSheet(""" + QFrame { + background: #1e293b; + border: none; + } + """) + content_layout = QVBoxLayout() + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(0) + + # Scrollable area for dynamic content + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setStyleSheet(""" + QScrollArea { + background: #1e293b; + border: none; + } + QScrollBar:vertical { + background: #1e293b; + width: 12px; + } + QScrollBar::handle:vertical { + background: #475569; + border-radius: 6px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { + background: #64748b; + } + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + border: none; + } + """) + + # Inner scrollable widget + scroll_widget = QFrame() + scroll_widget.setStyleSheet("background: #1e293b; border: none;") + scroll_layout = QVBoxLayout() + scroll_layout.setContentsMargins(40, 30, 40, 30) + scroll_layout.setSpacing(15) + + # Main message + message = QLabel(check["message"]) + message.setFont(QFont("Segoe UI", 12)) + message.setStyleSheet("color: #e2e8f0; line-height: 1.6;") + message.setWordWrap(True) + scroll_layout.addWidget(message) + + # Version info + if check.get("db_version"): + info_frame = QFrame() + info_frame.setStyleSheet(""" + QFrame { + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 6px; + } + """) + info_layout = QVBoxLayout() + info_layout.setContentsMargins(15, 12, 15, 12) + info_layout.setSpacing(5) + + app_ver = QLabel(f"App is looking for database version: {APP_VERSION}") + app_ver.setFont(QFont("Segoe UI", 10)) + app_ver.setStyleSheet("color: #94a3b8;") + info_layout.addWidget(app_ver) + + db_ver = QLabel(f"Your Actual Database version: {check['db_version']}") + db_ver.setFont(QFont("Segoe UI", 10)) + db_ver.setStyleSheet("color: #94a3b8;") + info_layout.addWidget(db_ver) + + info_frame.setLayout(info_layout) + scroll_layout.addWidget(info_frame) + + scroll_layout.addStretch() + scroll_widget.setLayout(scroll_layout) + scroll_area.setWidget(scroll_widget) + content_layout.addWidget(scroll_area, 1) + + content.setLayout(content_layout) + main_layout.addWidget(content, 1) + + # Button area + button_area = QFrame() + button_area.setStyleSheet(""" + QFrame { + background: #0f172a; + border-top: 1px solid #334155; + } + """) + button_layout = QVBoxLayout() + button_layout.setContentsMargins(40, 20, 40, 20) + button_layout.setSpacing(12) + + if check["status"] == "version_mismatch": + btn_backup = self._create_button( + "Create a Fresh Database. Then restart (recommended)", + "primary", + "✅" + ) + btn_backup.clicked.connect(self.on_wipe_create) + button_layout.addWidget(btn_backup) + + btn_different = self._create_button( + "Move the Stale Database for Debug Purposes", + "secondary", + "📁" + ) + btn_different.clicked.connect(self.on_move_db) + button_layout.addWidget(btn_different) + + btn_exit = self._create_button( + "Exit App without action.", + "danger", + "❌" + ) + btn_exit.clicked.connect(self.on_exit) + button_layout.addWidget(btn_exit) + + button_area.setLayout(button_layout) + main_layout.addWidget(button_area) + + self.setLayout(main_layout) + + def _create_button(self, text, style_type, icon): + """Create a styled button""" + btn = QPushButton(f" {icon} {text}") + btn.setFont(QFont("Segoe UI", 11, QFont.Weight.Medium)) + btn.setFixedHeight(45) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + + if style_type == "primary": + btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #3b82f6, stop:1 #2563eb); + color: white; + border: none; + border-radius: 6px; + font-weight: 600; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #2563eb, stop:1 #1d4ed8); + } + QPushButton:pressed { + background: #1d4ed8; + } + """) + elif style_type == "secondary": + btn.setStyleSheet(""" + QPushButton { + background: #334155; + color: #e2e8f0; + border: 1px solid #475569; + border-radius: 6px; + font-weight: 600; + } + QPushButton:hover { + background: #475569; + border: 1px solid #64748b; + } + QPushButton:pressed { + background: #1e293b; + } + """) + elif style_type == "danger": + btn.setStyleSheet(""" + QPushButton { + background: #64748b; + color: #e2e8f0; + border: 1px solid #475569; + border-radius: 6px; + font-weight: 600; + } + QPushButton:hover { + background: #ef4444; + border: 1px solid #dc2626; + } + QPushButton:pressed { + background: #dc2626; + } + """) + + return btn + + def on_wipe_create(self): + self.user_choice = self.WIPE + self.accept() + + def on_move_db(self): + self.user_choice = self.MOVE_DB + self.accept() + + def on_exit(self): + self.user_choice = self.EXIT + self.reject() + +def show_recovery_dialog(check: dict): + """Show recovery dialog as standalone app and return user choice""" + app = QApplication.instance() or QApplication(sys.argv) + dialog = DatabaseConflictDialog(check) + dialog.exec() + return dialog.user_choice diff --git a/gui/v2/ui/popups/pick_folder_to_move.py b/gui/v2/ui/popups/pick_folder_to_move.py new file mode 100644 index 0000000..daff390 --- /dev/null +++ b/gui/v2/ui/popups/pick_folder_to_move.py @@ -0,0 +1,50 @@ +import shutil +import sys +from pathlib import Path +from PyQt6.QtWidgets import QApplication, QFileDialog + +def move_database_file(original_filepath: str) -> bool: + """ + Prompts the user to select a destination for a .db file and moves it there. + + Args: + original_filepath: The current path to the .db file + + Returns: + True if the move was successful, False otherwise + """ + # Create a QApplication if one doesn't already exist + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # Validate that the original file exists + if not Path(original_filepath).exists(): + print(f"Error: File not found at {original_filepath}") + return False + + # Open the file save dialog + destination, _ = QFileDialog.getSaveFileName( + None, + "Select destination for database file", + str(Path(original_filepath).parent), # Start in the original file's directory + "Database Files (*.db);;All Files (*)" + ) + + # If the user cancelled the dialog + if not destination: + print("Operation cancelled by user") + return False + + try: + # Move the file + shutil.move(original_filepath, destination) + print(f"File successfully moved to: {destination}") + return True + except Exception as e: + print(f"Error moving file: {e}") + return False + + +def launch_file_picker(original_path): + move_database_file(original_path)