From 1bbbb235e833a6f5ab0e6f2e2ec3b48b2b335dc9 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 17 Aug 2025 08:38:28 -0400 Subject: [PATCH] Add comprehensive profile editing functionality with accessibility features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added update_credentials method to ActivityPub client for profile API calls - Created ProfileEditDialog with tabbed interface for editing profile information - Implemented profile fields management with accessible list navigation - Added HTML stripping for clean display of profile field content - Integrated profile editing into File menu with Ctrl+Alt+E shortcut - Fixed Tab key accessibility in biography text field - Updated documentation with new feature and keyboard shortcut 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 2 + README.md | 2 + src/activitypub/client.py | 32 ++ src/main_window.py | 59 +++ src/widgets/profile_edit_dialog.py | 574 +++++++++++++++++++++++++++++ 5 files changed, 669 insertions(+) create mode 100644 src/widgets/profile_edit_dialog.py diff --git a/CLAUDE.md b/CLAUDE.md index fa85d8c..2cd6670 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -433,6 +433,7 @@ Due to Qt's visual display synchronization, thread collapse may require double-o - **Ctrl+S**: Open Search dialog for users, hashtags, and posts - **Ctrl+Shift+F**: Open Timeline Filters dialog - **Ctrl+Shift+E**: Edit selected post (your own posts only) +- **Ctrl+Alt+E**: Edit your profile (display name, bio, fields, privacy settings) - **Shift+Delete**: Delete selected post (your own posts only) - **F5**: Refresh timeline - **Ctrl+,**: Settings @@ -560,6 +561,7 @@ verbose_announcements = true - **Timeline Filtering**: ✅ Filter home timeline by content type (replies, boosts, mentions) and media - **Search Dialog**: ✅ Dedicated search interface with tabbed results and background processing - **Filter Dialog**: ✅ Timeline filter configuration with real-time application +- **Profile Editing**: ✅ Edit your profile information including display name, bio, profile fields, and privacy settings ### Remaining High Priority Features - **User Blocking Management**: Block/unblock users with dedicated management interface diff --git a/README.md b/README.md index 338e0f5..39677e8 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ This project was created through "vibe coding" - a collaborative development app - **Blocked/Muted Management**: Dedicated tabs for managing blocked and muted users - **Custom Emoji Support**: Instance-specific emoji support with caching - **Post Editing**: Edit your own posts and direct messages with full content preservation +- **Profile Editing**: Edit your profile information including display name, bio, profile fields, and privacy settings - **Search Functionality**: Search for users, hashtags, and posts across the fediverse with dedicated search interface - **Timeline Filtering**: Customize your timeline view by hiding/showing replies, boosts, mentions, and media content @@ -128,6 +129,7 @@ Bifrost includes a sophisticated sound system with intelligent notification hand - **Ctrl+Shift+F**: Open Timeline Filters dialog - **Ctrl+,**: Open Settings - **Ctrl+Shift+A**: Add new account +- **Ctrl+Alt+E**: Edit your profile - **Ctrl+Alt+S**: Open Soundpack Manager - **Ctrl+Q**: Quit application diff --git a/src/activitypub/client.py b/src/activitypub/client.py index 0272559..2e4bac5 100644 --- a/src/activitypub/client.py +++ b/src/activitypub/client.py @@ -245,6 +245,38 @@ class ActivityPubClient: """Verify account credentials""" return self._make_request('GET', '/api/v1/accounts/verify_credentials') + def update_credentials(self, display_name: Optional[str] = None, note: Optional[str] = None, + avatar: Optional[str] = None, header: Optional[str] = None, + locked: Optional[bool] = None, bot: Optional[bool] = None, + discoverable: Optional[bool] = None, fields_attributes: Optional[List[Dict]] = None) -> Dict: + """Update account credentials and profile information""" + data = {} + + if display_name is not None: + data['display_name'] = display_name + if note is not None: + data['note'] = note + if avatar is not None: + data['avatar'] = avatar + if header is not None: + data['header'] = header + if locked is not None: + data['locked'] = str(locked).lower() + if bot is not None: + data['bot'] = str(bot).lower() + if discoverable is not None: + data['discoverable'] = str(discoverable).lower() + + # Handle profile fields + if fields_attributes is not None: + for i, field in enumerate(fields_attributes): + if 'name' in field: + data[f'fields_attributes[{i}][name]'] = field['name'] + if 'value' in field: + data[f'fields_attributes[{i}][value]'] = field['value'] + + return self._make_request_form('PATCH', '/api/v1/accounts/update_credentials', data=data) + def get_timeline(self, timeline_type: str = 'home', limit: int = 40, max_id: Optional[str] = None, since_id: Optional[str] = None) -> List[Dict]: """Get timeline posts""" diff --git a/src/main_window.py b/src/main_window.py index 6226db6..e14385b 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -27,6 +27,7 @@ from widgets.account_selector import AccountSelector from widgets.settings_dialog import SettingsDialog from widgets.soundpack_manager_dialog import SoundpackManagerDialog from widgets.profile_dialog import ProfileDialog +from widgets.profile_edit_dialog import ProfileEditDialog from widgets.accessible_text_dialog import AccessibleTextDialog from widgets.search_dialog import SearchDialog from widgets.timeline_filter_dialog import TimelineFilterDialog @@ -182,6 +183,12 @@ class MainWindow(QMainWindow): add_account_action.triggered.connect(self.show_login_dialog) file_menu.addAction(add_account_action) + # Edit Profile action + edit_profile_action = QAction("&Edit Profile", self) + edit_profile_action.setShortcut(QKeySequence("Ctrl+Alt+E")) + edit_profile_action.triggered.connect(self.show_edit_profile) + file_menu.addAction(edit_profile_action) + file_menu.addSeparator() # Settings action @@ -809,6 +816,58 @@ class MainWindow(QMainWindow): self.status_bar.showMessage("Settings saved successfully", 2000) + def show_edit_profile(self): + """Show the profile editing dialog""" + try: + # Check if we have an active account + if not self.account_manager.has_accounts(): + AccessibleTextDialog.show_error( + "No Account", + "You need to be logged in to edit your profile.", + "", + self + ) + return + + active_account = self.account_manager.get_active_account() + if not active_account: + AccessibleTextDialog.show_error( + "No Active Account", + "Please select an active account to edit your profile.", + "", + self + ) + return + + # Create and show dialog + dialog = ProfileEditDialog( + self.account_manager, + self.timeline.sound_manager if hasattr(self.timeline, 'sound_manager') else None, + self + ) + dialog.profile_updated.connect(self.on_profile_updated) + dialog.exec() + + except Exception as e: + self.logger.error(f"Failed to show profile edit dialog: {e}") + AccessibleTextDialog.show_error( + "Profile Edit Error", + "Failed to open profile editor.", + str(e), + self + ) + if hasattr(self.timeline, 'sound_manager'): + self.timeline.sound_manager.play_error() + + def on_profile_updated(self, updated_user): + """Handle successful profile update""" + self.logger.info(f"Profile updated for user: {updated_user.username}") + self.status_bar.showMessage("Profile updated successfully", 3000) + + # Refresh timeline to show any changes (display name, etc.) + if hasattr(self.timeline, 'refresh_timeline'): + self.timeline.refresh_timeline() + def show_soundpack_manager(self): """Show the soundpack manager dialog""" dialog = SoundpackManagerDialog(self.settings, self) diff --git a/src/widgets/profile_edit_dialog.py b/src/widgets/profile_edit_dialog.py new file mode 100644 index 0000000..fe6141e --- /dev/null +++ b/src/widgets/profile_edit_dialog.py @@ -0,0 +1,574 @@ +""" +Profile editing dialog for Bifrost +""" + +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, + QLabel, QPushButton, QDialogButtonBox, QTextEdit, + QLineEdit, QCheckBox, QGroupBox, QTabWidget, QWidget, + QScrollArea, QMessageBox, QListWidget, QListWidgetItem +) +from PySide6.QtCore import Qt, Signal, QThread +from PySide6.QtGui import QFont +from typing import Optional, List, Dict, Any +import logging +import re + +from models.user import User, Field +from activitypub.client import ActivityPubClient +from config.accounts import AccountManager +from audio.sound_manager import SoundManager +from widgets.accessible_text_dialog import AccessibleTextDialog + + +def strip_html_tags(text: str) -> str: + """Strip HTML tags and decode entities for clean text display""" + if not text: + return "" + # Remove HTML tags + clean_text = re.sub(r'<[^>]+>', '', text) + # Decode common HTML entities + clean_text = clean_text.replace('<', '<').replace('>', '>').replace('&', '&') + clean_text = clean_text.replace('"', '"').replace(''', "'") + return clean_text.strip() + + +class ProfileUpdateThread(QThread): + """Thread for updating profile data without blocking UI""" + + profile_updated = Signal(object) # Updated User object + error_occurred = Signal(str) # Error message + + def __init__(self, client: ActivityPubClient, profile_data: Dict[str, Any]): + super().__init__() + self.client = client + self.profile_data = profile_data + self.logger = logging.getLogger('bifrost.profile_edit') + + def run(self): + try: + self.logger.info(f"Updating profile with data: {list(self.profile_data.keys())}") + + # Update profile via API + result = self.client.update_credentials(**self.profile_data) + + # Convert response to User object + updated_user = User.from_api_dict(result) + self.profile_updated.emit(updated_user) + + except Exception as e: + self.logger.error(f"Profile update failed: {e}") + self.error_occurred.emit(str(e)) + + +class ProfileFieldsWidget(QWidget): + """Widget for managing profile fields""" + + def __init__(self, parent=None): + super().__init__(parent) + self.fields = [] # List of field dictionaries + self.setup_ui() + + def setup_ui(self): + """Setup the profile fields UI""" + layout = QVBoxLayout(self) + + # Instructions + instructions = QLabel("Profile fields are name-value pairs displayed on your profile. You can have up to 4 fields.") + instructions.setAccessibleName("Profile Fields Instructions") + instructions.setWordWrap(True) + layout.addWidget(instructions) + + # Fields list + self.fields_list = QListWidget() + self.fields_list.setAccessibleName("Profile Fields List") + layout.addWidget(self.fields_list) + + # Buttons + buttons_layout = QHBoxLayout() + + self.add_button = QPushButton("Add Field") + self.add_button.setAccessibleName("Add Profile Field") + self.add_button.clicked.connect(self.add_field) + buttons_layout.addWidget(self.add_button) + + self.edit_button = QPushButton("Edit Field") + self.edit_button.setAccessibleName("Edit Selected Profile Field") + self.edit_button.clicked.connect(self.edit_field) + self.edit_button.setEnabled(False) + buttons_layout.addWidget(self.edit_button) + + self.remove_button = QPushButton("Remove Field") + self.remove_button.setAccessibleName("Remove Selected Profile Field") + self.remove_button.clicked.connect(self.remove_field) + self.remove_button.setEnabled(False) + buttons_layout.addWidget(self.remove_button) + + buttons_layout.addStretch() + layout.addLayout(buttons_layout) + + # Connect list selection + self.fields_list.itemSelectionChanged.connect(self.on_selection_changed) + + def set_fields(self, fields: List[Field]): + """Set the profile fields""" + self.fields = [] + for field in fields: + # Store the original HTML values (they'll be cleaned for display) + self.fields.append({ + 'name': field.name, + 'value': field.value + }) + self.refresh_list() + + def get_fields(self) -> List[Dict[str, str]]: + """Get the current profile fields as API format""" + return self.fields.copy() + + def refresh_list(self): + """Refresh the fields list display""" + self.fields_list.clear() + + # Add fake header for single-item navigation (accessibility requirement) + field_count = len(self.fields) + header_text = f"Profile fields ({field_count} configured):" + fake_header = QListWidgetItem(header_text) + fake_header.setFlags(Qt.ItemIsEnabled) # Not selectable + fake_header.setData(Qt.AccessibleDescriptionRole, "Profile fields list header") + self.fields_list.addItem(fake_header) + + if not self.fields: + item = QListWidgetItem("No profile fields configured") + item.setData(Qt.UserRole, None) + item.setData(Qt.AccessibleTextRole, "No profile fields have been configured") + self.fields_list.addItem(item) + return + + for i, field in enumerate(self.fields): + # Strip HTML for clean display in list + clean_name = strip_html_tags(field['name']) + clean_value = strip_html_tags(field['value']) + + display_text = f"{clean_name}: {clean_value}" + item = QListWidgetItem(display_text) + item.setData(Qt.UserRole, i) + item.setData(Qt.AccessibleTextRole, f"Profile field {i+1}: {clean_name} equals {clean_value}") + self.fields_list.addItem(item) + + def on_selection_changed(self): + """Handle field selection changes""" + current_item = self.fields_list.currentItem() + # Enable buttons only if it's not the header item and has field data + has_field_data = current_item and current_item.data(Qt.UserRole) is not None + self.edit_button.setEnabled(has_field_data) + self.remove_button.setEnabled(has_field_data) + + def add_field(self): + """Add a new profile field""" + if len(self.fields) >= 4: + QMessageBox.warning(self, "Maximum Fields", "You can only have up to 4 profile fields.") + return + + field_data = self.show_field_dialog() + if field_data: + self.fields.append(field_data) + self.refresh_list() + + def edit_field(self): + """Edit the selected profile field""" + current_item = self.fields_list.currentItem() + if not current_item: + return + + field_index = current_item.data(Qt.UserRole) + if field_index is None: + return + + current_field = self.fields[field_index] + field_data = self.show_field_dialog(current_field) + if field_data: + self.fields[field_index] = field_data + self.refresh_list() + + def remove_field(self): + """Remove the selected profile field""" + current_item = self.fields_list.currentItem() + if not current_item: + return + + field_index = current_item.data(Qt.UserRole) + if field_index is None: + return + + result = QMessageBox.question( + self, + "Remove Field", + f"Are you sure you want to remove the field '{self.fields[field_index]['name']}'?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if result == QMessageBox.Yes: + del self.fields[field_index] + self.refresh_list() + + def show_field_dialog(self, existing_field: Optional[Dict[str, str]] = None) -> Optional[Dict[str, str]]: + """Show dialog for editing a field""" + dialog = QDialog(self) + dialog.setWindowTitle("Edit Profile Field" if existing_field else "Add Profile Field") + dialog.setModal(True) + dialog.setMinimumSize(400, 200) + + layout = QVBoxLayout(dialog) + form_layout = QFormLayout() + + # Field name + name_edit = QLineEdit() + name_edit.setAccessibleName("Field Name") + name_edit.setPlaceholderText("e.g., Website, Location, Pronouns") + if existing_field: + # Strip HTML for clean editing + clean_name = strip_html_tags(existing_field['name']) + name_edit.setText(clean_name) + form_layout.addRow("Field Name:", name_edit) + + # Field value + value_edit = QLineEdit() + value_edit.setAccessibleName("Field Value") + value_edit.setPlaceholderText("e.g., https://example.com, New York, they/them") + if existing_field: + # Strip HTML for clean editing + clean_value = strip_html_tags(existing_field['value']) + value_edit.setText(clean_value) + form_layout.addRow("Field Value:", value_edit) + + layout.addLayout(form_layout) + + # Buttons + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box.accepted.connect(dialog.accept) + button_box.rejected.connect(dialog.reject) + layout.addWidget(button_box) + + # Focus on name field + name_edit.setFocus() + + if dialog.exec() == QDialog.Accepted: + name = name_edit.text().strip() + value = value_edit.text().strip() + + if not name: + QMessageBox.warning(dialog, "Invalid Field", "Field name cannot be empty.") + return None + + return {'name': name, 'value': value} + + return None + + +class ProfileEditDialog(QDialog): + """Dialog for editing user profile information""" + + profile_updated = Signal(object) # Emitted when profile is successfully updated + + def __init__(self, account_manager: AccountManager, sound_manager: SoundManager, parent=None): + super().__init__(parent) + self.account_manager = account_manager + self.sound_manager = sound_manager + self.current_user = None + self.logger = logging.getLogger('bifrost.profile_edit') + + # Get API client + active_account = self.account_manager.get_active_account() + if not active_account: + raise ValueError("No active account for profile editing") + + self.client = ActivityPubClient( + active_account.instance_url, + active_account.access_token + ) + + self.setup_ui() + self.load_current_profile() + + def setup_ui(self): + """Initialize the user interface""" + self.setWindowTitle("Edit Profile") + self.setMinimumSize(600, 500) + self.setModal(True) + + # Main layout + main_layout = QVBoxLayout(self) + + # Loading label + self.loading_label = QLabel("Loading your profile...") + self.loading_label.setAccessibleName("Profile Loading Status") + self.loading_label.setAlignment(Qt.AlignCenter) + main_layout.addWidget(self.loading_label) + + # Content widget (initially hidden) + self.content_widget = QWidget() + self.content_widget.hide() + self.setup_content_ui() + main_layout.addWidget(self.content_widget) + + # Button box + self.button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) + self.button_box.button(QDialogButtonBox.Save).setText("Save Profile") + self.button_box.button(QDialogButtonBox.Save).setAccessibleName("Save Profile Changes") + self.button_box.accepted.connect(self.save_profile) + self.button_box.rejected.connect(self.reject) + main_layout.addWidget(self.button_box) + + def setup_content_ui(self): + """Setup the main content UI (shown after loading)""" + content_layout = QVBoxLayout(self.content_widget) + + # Tab widget for different sections + self.tab_widget = QTabWidget() + self.tab_widget.setAccessibleName("Profile Edit Sections") + + # Basic info tab + self.setup_basic_tab() + self.tab_widget.addTab(self.basic_tab, "Basic Info") + + # Profile fields tab + self.setup_fields_tab() + self.tab_widget.addTab(self.fields_tab, "Profile Fields") + + # Privacy settings tab + self.setup_privacy_tab() + self.tab_widget.addTab(self.privacy_tab, "Privacy") + + content_layout.addWidget(self.tab_widget) + + def setup_basic_tab(self): + """Setup the basic profile information tab""" + self.basic_tab = QScrollArea() + self.basic_tab.setAccessibleName("Basic Profile Information") + self.basic_tab.setWidgetResizable(True) + + basic_content = QWidget() + basic_layout = QVBoxLayout(basic_content) + + # Display name + name_group = QGroupBox("Display Name") + name_group.setAccessibleName("Display Name Section") + name_layout = QVBoxLayout(name_group) + + self.display_name_edit = QLineEdit() + self.display_name_edit.setAccessibleName("Display Name") + self.display_name_edit.setPlaceholderText("Your display name (shown on posts)") + name_layout.addWidget(self.display_name_edit) + + basic_layout.addWidget(name_group) + + # Biography + bio_group = QGroupBox("Biography") + bio_group.setAccessibleName("Biography Section") + bio_layout = QVBoxLayout(bio_group) + + bio_instructions = QLabel("Describe yourself in a few sentences. HTML and links are supported.") + bio_instructions.setAccessibleName("Biography Instructions") + bio_instructions.setWordWrap(True) + bio_layout.addWidget(bio_instructions) + + self.bio_edit = QTextEdit() + self.bio_edit.setAccessibleName("Biography") + self.bio_edit.setPlaceholderText("Tell people about yourself...") + self.bio_edit.setMaximumHeight(150) + # Fix accessibility: Tab should move focus, not insert tab character + self.bio_edit.setTabChangesFocus(True) + bio_layout.addWidget(self.bio_edit) + + basic_layout.addWidget(bio_group) + + basic_layout.addStretch() + self.basic_tab.setWidget(basic_content) + + def setup_fields_tab(self): + """Setup the profile fields tab""" + self.fields_tab = QWidget() + fields_layout = QVBoxLayout(self.fields_tab) + + self.fields_widget = ProfileFieldsWidget() + fields_layout.addWidget(self.fields_widget) + + def setup_privacy_tab(self): + """Setup the privacy settings tab""" + self.privacy_tab = QScrollArea() + self.privacy_tab.setAccessibleName("Privacy Settings") + self.privacy_tab.setWidgetResizable(True) + + privacy_content = QWidget() + privacy_layout = QVBoxLayout(privacy_content) + + # Account privacy + privacy_group = QGroupBox("Account Privacy") + privacy_group.setAccessibleName("Account Privacy Section") + privacy_group_layout = QVBoxLayout(privacy_group) + + self.locked_checkbox = QCheckBox("Private account (require approval for followers)") + self.locked_checkbox.setAccessibleName("Private Account Setting") + privacy_group_layout.addWidget(self.locked_checkbox) + + privacy_layout.addWidget(privacy_group) + + # Account type + type_group = QGroupBox("Account Type") + type_group.setAccessibleName("Account Type Section") + type_group_layout = QVBoxLayout(type_group) + + self.bot_checkbox = QCheckBox("This is a bot account") + self.bot_checkbox.setAccessibleName("Bot Account Setting") + type_group_layout.addWidget(self.bot_checkbox) + + privacy_layout.addWidget(type_group) + + # Discoverability + discovery_group = QGroupBox("Discoverability") + discovery_group.setAccessibleName("Discoverability Section") + discovery_group_layout = QVBoxLayout(discovery_group) + + self.discoverable_checkbox = QCheckBox("Include in directory and search results") + self.discoverable_checkbox.setAccessibleName("Discoverable Account Setting") + discovery_group_layout.addWidget(self.discoverable_checkbox) + + privacy_layout.addWidget(discovery_group) + + privacy_layout.addStretch() + self.privacy_tab.setWidget(privacy_content) + + def load_current_profile(self): + """Load the current user's profile information""" + try: + # Get current user info + user_data = self.client.verify_credentials() + self.current_user = User.from_api_dict(user_data) + self.populate_fields() + + # Hide loading and show content + self.loading_label.hide() + self.content_widget.show() + + self.logger.info("Profile loaded successfully for editing") + self.sound_manager.play_success() + + except Exception as e: + self.logger.error(f"Failed to load profile for editing: {e}") + self.loading_label.setText(f"Error loading profile: {str(e)}") + self.sound_manager.play_error() + + def populate_fields(self): + """Populate the form fields with current profile data""" + if not self.current_user: + return + + # Basic info + self.display_name_edit.setText(self.current_user.display_name or "") + self.bio_edit.setPlainText(self.current_user.note or "") + + # Profile fields + self.fields_widget.set_fields(self.current_user.fields or []) + + # Privacy settings + self.locked_checkbox.setChecked(self.current_user.locked or False) + self.bot_checkbox.setChecked(self.current_user.bot or False) + self.discoverable_checkbox.setChecked(getattr(self.current_user, 'discoverable', True)) + + def save_profile(self): + """Save the profile changes""" + try: + # Collect changes + profile_data = {} + + # Check what changed + new_display_name = self.display_name_edit.text().strip() + if new_display_name != (self.current_user.display_name or ""): + profile_data['display_name'] = new_display_name + + new_bio = self.bio_edit.toPlainText().strip() + if new_bio != (self.current_user.note or ""): + profile_data['note'] = new_bio + + new_locked = self.locked_checkbox.isChecked() + if new_locked != (self.current_user.locked or False): + profile_data['locked'] = new_locked + + new_bot = self.bot_checkbox.isChecked() + if new_bot != (self.current_user.bot or False): + profile_data['bot'] = new_bot + + new_discoverable = self.discoverable_checkbox.isChecked() + current_discoverable = getattr(self.current_user, 'discoverable', True) + if new_discoverable != current_discoverable: + profile_data['discoverable'] = new_discoverable + + # Check fields changes + new_fields = self.fields_widget.get_fields() + current_fields = [{'name': f.name, 'value': f.value} for f in (self.current_user.fields or [])] + if new_fields != current_fields: + profile_data['fields_attributes'] = new_fields + + if not profile_data: + QMessageBox.information(self, "No Changes", "No changes were made to your profile.") + return + + # Disable save button during update + self.button_box.button(QDialogButtonBox.Save).setEnabled(False) + self.button_box.button(QDialogButtonBox.Save).setText("Saving...") + + # Start update thread + self.update_thread = ProfileUpdateThread(self.client, profile_data) + self.update_thread.profile_updated.connect(self.on_profile_updated) + self.update_thread.error_occurred.connect(self.on_update_error) + self.update_thread.start() + + self.logger.info(f"Starting profile update with {len(profile_data)} changes") + + except Exception as e: + self.logger.error(f"Error preparing profile update: {e}") + AccessibleTextDialog.show_error("Profile Update Error", + "Failed to prepare profile update", str(e), self) + self.sound_manager.play_error() + + def on_profile_updated(self, updated_user: User): + """Handle successful profile update""" + self.current_user = updated_user + + # Re-enable save button + self.button_box.button(QDialogButtonBox.Save).setEnabled(True) + self.button_box.button(QDialogButtonBox.Save).setText("Save Profile") + + # Emit signal for other components to update + self.profile_updated.emit(updated_user) + + # Show success message + QMessageBox.information(self, "Profile Updated", "Your profile has been updated successfully!") + + self.logger.info("Profile update completed successfully") + self.sound_manager.play_success() + + # Close dialog + self.accept() + + def on_update_error(self, error_message: str): + """Handle profile update error""" + # Re-enable save button + self.button_box.button(QDialogButtonBox.Save).setEnabled(True) + self.button_box.button(QDialogButtonBox.Save).setText("Save Profile") + + # Show error + if "422" in error_message: + error_msg = "Invalid profile data. Please check your entries and try again." + elif "403" in error_message: + error_msg = "You don't have permission to update your profile." + elif "timeout" in error_message.lower() or "connection" in error_message.lower(): + error_msg = "Network error while updating profile. Please check your connection and try again." + else: + error_msg = f"Failed to update profile: {error_message}" + + AccessibleTextDialog.show_error("Profile Update Error", error_msg, "", self) + + self.logger.error(f"Profile update failed: {error_message}") + self.sound_manager.play_error() \ No newline at end of file