Add comprehensive profile editing functionality with accessibility features
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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"""
|
||||
|
@@ -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)
|
||||
|
574
src/widgets/profile_edit_dialog.py
Normal file
574
src/widgets/profile_edit_dialog.py
Normal file
@@ -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()
|
Reference in New Issue
Block a user