Initial commit: Bifrost accessible fediverse client
- Full ActivityPub support for Pleroma, GoToSocial, and Mastodon - Screen reader optimized interface with PySide6 - Timeline switching with tabs and keyboard shortcuts (Ctrl+1-4) - Threaded conversation navigation with expand/collapse - Cross-platform desktop notifications via plyer - Customizable sound pack system with audio feedback - Complete keyboard navigation and accessibility features - XDG Base Directory compliant configuration - Multiple account support with OAuth authentication 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
308
src/widgets/settings_dialog.py
Normal file
308
src/widgets/settings_dialog.py
Normal file
@ -0,0 +1,308 @@
|
||||
"""
|
||||
Settings dialog for Bifrost configuration
|
||||
"""
|
||||
|
||||
import os
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||
QLabel, QComboBox, QPushButton, QDialogButtonBox,
|
||||
QGroupBox, QCheckBox, QSpinBox, QTabWidget, QWidget
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
|
||||
from config.settings import SettingsManager
|
||||
from accessibility.accessible_combo import AccessibleComboBox
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
"""Main settings dialog for Bifrost"""
|
||||
|
||||
settings_changed = Signal() # Emitted when settings are saved
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.settings = SettingsManager()
|
||||
self.setup_ui()
|
||||
self.load_current_settings()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Initialize the settings dialog UI"""
|
||||
self.setWindowTitle("Bifrost Settings")
|
||||
self.setMinimumSize(500, 400)
|
||||
self.setModal(True)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Create tab widget for organized settings
|
||||
self.tabs = QTabWidget()
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
# Audio settings tab
|
||||
self.setup_audio_tab()
|
||||
|
||||
# Desktop notifications tab
|
||||
self.setup_notifications_tab()
|
||||
|
||||
# Accessibility settings tab
|
||||
self.setup_accessibility_tab()
|
||||
|
||||
# Button box
|
||||
button_box = QDialogButtonBox(
|
||||
QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Apply
|
||||
)
|
||||
button_box.accepted.connect(self.save_and_close)
|
||||
button_box.rejected.connect(self.reject)
|
||||
button_box.button(QDialogButtonBox.Apply).clicked.connect(self.apply_settings)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
def setup_audio_tab(self):
|
||||
"""Set up the audio settings tab"""
|
||||
audio_widget = QWidget()
|
||||
layout = QVBoxLayout(audio_widget)
|
||||
|
||||
# Sound pack selection
|
||||
sound_group = QGroupBox("Sound Pack")
|
||||
sound_layout = QFormLayout(sound_group)
|
||||
|
||||
self.sound_pack_combo = AccessibleComboBox()
|
||||
self.sound_pack_combo.setAccessibleName("Sound Pack Selection")
|
||||
self.sound_pack_combo.setAccessibleDescription("Choose which sound pack to use for audio feedback")
|
||||
|
||||
# Populate sound packs
|
||||
self.load_sound_packs()
|
||||
|
||||
sound_layout.addRow("Sound Pack:", self.sound_pack_combo)
|
||||
layout.addWidget(sound_group)
|
||||
|
||||
# Audio volume settings
|
||||
volume_group = QGroupBox("Volume Settings")
|
||||
volume_layout = QFormLayout(volume_group)
|
||||
|
||||
self.master_volume = QSpinBox()
|
||||
self.master_volume.setRange(0, 100)
|
||||
self.master_volume.setSuffix("%")
|
||||
self.master_volume.setAccessibleName("Master Volume")
|
||||
self.master_volume.setAccessibleDescription("Overall volume for all sounds")
|
||||
volume_layout.addRow("Master Volume:", self.master_volume)
|
||||
|
||||
self.notification_volume = QSpinBox()
|
||||
self.notification_volume.setRange(0, 100)
|
||||
self.notification_volume.setSuffix("%")
|
||||
self.notification_volume.setAccessibleName("Notification Volume")
|
||||
self.notification_volume.setAccessibleDescription("Volume for notification sounds")
|
||||
volume_layout.addRow("Notification Volume:", self.notification_volume)
|
||||
|
||||
layout.addWidget(volume_group)
|
||||
|
||||
# Audio enable/disable options
|
||||
options_group = QGroupBox("Audio Options")
|
||||
options_layout = QVBoxLayout(options_group)
|
||||
|
||||
self.enable_sounds = QCheckBox("Enable sound effects")
|
||||
self.enable_sounds.setAccessibleName("Enable Sound Effects")
|
||||
self.enable_sounds.setAccessibleDescription("Turn sound effects on or off globally")
|
||||
options_layout.addWidget(self.enable_sounds)
|
||||
|
||||
self.enable_post_sounds = QCheckBox("Play sounds for post actions")
|
||||
self.enable_post_sounds.setAccessibleName("Post Action Sounds")
|
||||
self.enable_post_sounds.setAccessibleDescription("Play sounds when posting, boosting, or favoriting")
|
||||
options_layout.addWidget(self.enable_post_sounds)
|
||||
|
||||
self.enable_timeline_sounds = QCheckBox("Play sounds for timeline updates")
|
||||
self.enable_timeline_sounds.setAccessibleName("Timeline Update Sounds")
|
||||
self.enable_timeline_sounds.setAccessibleDescription("Play sounds when the timeline refreshes")
|
||||
options_layout.addWidget(self.enable_timeline_sounds)
|
||||
|
||||
layout.addWidget(options_group)
|
||||
layout.addStretch()
|
||||
|
||||
self.tabs.addTab(audio_widget, "&Audio")
|
||||
|
||||
def setup_notifications_tab(self):
|
||||
"""Set up the desktop notifications settings tab"""
|
||||
notifications_widget = QWidget()
|
||||
layout = QVBoxLayout(notifications_widget)
|
||||
|
||||
# Desktop notifications group
|
||||
desktop_group = QGroupBox("Desktop Notifications")
|
||||
desktop_layout = QVBoxLayout(desktop_group)
|
||||
|
||||
self.enable_desktop_notifications = QCheckBox("Enable desktop notifications")
|
||||
self.enable_desktop_notifications.setAccessibleName("Enable Desktop Notifications")
|
||||
self.enable_desktop_notifications.setAccessibleDescription("Show desktop notifications for various events")
|
||||
desktop_layout.addWidget(self.enable_desktop_notifications)
|
||||
|
||||
# Notification types
|
||||
types_group = QGroupBox("Notification Types")
|
||||
types_layout = QVBoxLayout(types_group)
|
||||
|
||||
self.notify_direct_messages = QCheckBox("Direct/Private messages")
|
||||
self.notify_direct_messages.setAccessibleName("Direct Message Notifications")
|
||||
self.notify_direct_messages.setAccessibleDescription("Show notifications for direct messages")
|
||||
types_layout.addWidget(self.notify_direct_messages)
|
||||
|
||||
self.notify_mentions = QCheckBox("Mentions")
|
||||
self.notify_mentions.setAccessibleName("Mention Notifications")
|
||||
self.notify_mentions.setAccessibleDescription("Show notifications when you are mentioned")
|
||||
types_layout.addWidget(self.notify_mentions)
|
||||
|
||||
self.notify_boosts = QCheckBox("Boosts/Reblogs")
|
||||
self.notify_boosts.setAccessibleName("Boost Notifications")
|
||||
self.notify_boosts.setAccessibleDescription("Show notifications when your posts are boosted")
|
||||
types_layout.addWidget(self.notify_boosts)
|
||||
|
||||
self.notify_favorites = QCheckBox("Favorites")
|
||||
self.notify_favorites.setAccessibleName("Favorite Notifications")
|
||||
self.notify_favorites.setAccessibleDescription("Show notifications when your posts are favorited")
|
||||
types_layout.addWidget(self.notify_favorites)
|
||||
|
||||
self.notify_follows = QCheckBox("New followers")
|
||||
self.notify_follows.setAccessibleName("Follow Notifications")
|
||||
self.notify_follows.setAccessibleDescription("Show notifications for new followers")
|
||||
types_layout.addWidget(self.notify_follows)
|
||||
|
||||
self.notify_timeline_updates = QCheckBox("Timeline updates")
|
||||
self.notify_timeline_updates.setAccessibleName("Timeline Update Notifications")
|
||||
self.notify_timeline_updates.setAccessibleDescription("Show notifications for new posts in timeline")
|
||||
types_layout.addWidget(self.notify_timeline_updates)
|
||||
|
||||
layout.addWidget(desktop_group)
|
||||
layout.addWidget(types_group)
|
||||
layout.addStretch()
|
||||
|
||||
self.tabs.addTab(notifications_widget, "&Notifications")
|
||||
|
||||
def setup_accessibility_tab(self):
|
||||
"""Set up the accessibility settings tab"""
|
||||
accessibility_widget = QWidget()
|
||||
layout = QVBoxLayout(accessibility_widget)
|
||||
|
||||
# Navigation settings
|
||||
nav_group = QGroupBox("Navigation Settings")
|
||||
nav_layout = QFormLayout(nav_group)
|
||||
|
||||
self.page_step_size = QSpinBox()
|
||||
self.page_step_size.setRange(1, 20)
|
||||
self.page_step_size.setAccessibleName("Page Step Size")
|
||||
self.page_step_size.setAccessibleDescription("Number of posts to jump when using Page Up/Down")
|
||||
nav_layout.addRow("Page Step Size:", self.page_step_size)
|
||||
|
||||
layout.addWidget(nav_group)
|
||||
|
||||
# Screen reader options
|
||||
sr_group = QGroupBox("Screen Reader Options")
|
||||
sr_layout = QVBoxLayout(sr_group)
|
||||
|
||||
self.verbose_announcements = QCheckBox("Verbose announcements")
|
||||
self.verbose_announcements.setAccessibleName("Verbose Announcements")
|
||||
self.verbose_announcements.setAccessibleDescription("Provide detailed descriptions for screen readers")
|
||||
sr_layout.addWidget(self.verbose_announcements)
|
||||
|
||||
self.announce_thread_state = QCheckBox("Announce thread expand/collapse state")
|
||||
self.announce_thread_state.setAccessibleName("Thread State Announcements")
|
||||
self.announce_thread_state.setAccessibleDescription("Announce when threads are expanded or collapsed")
|
||||
sr_layout.addWidget(self.announce_thread_state)
|
||||
|
||||
layout.addWidget(sr_group)
|
||||
layout.addStretch()
|
||||
|
||||
self.tabs.addTab(accessibility_widget, "A&ccessibility")
|
||||
|
||||
def load_sound_packs(self):
|
||||
"""Load available sound packs from the sounds directory"""
|
||||
self.sound_pack_combo.clear()
|
||||
|
||||
# Add default "None" option
|
||||
self.sound_pack_combo.addItem("None (No sounds)", "none")
|
||||
|
||||
# Look for sound pack directories
|
||||
sounds_dir = "sounds"
|
||||
if os.path.exists(sounds_dir):
|
||||
for item in os.listdir(sounds_dir):
|
||||
pack_dir = os.path.join(sounds_dir, item)
|
||||
if os.path.isdir(pack_dir):
|
||||
pack_json = os.path.join(pack_dir, "pack.json")
|
||||
if os.path.exists(pack_json):
|
||||
# Valid sound pack
|
||||
self.sound_pack_combo.addItem(item, item)
|
||||
|
||||
# Also check in XDG data directory
|
||||
try:
|
||||
data_sounds_dir = self.settings.get_sounds_dir()
|
||||
if os.path.exists(data_sounds_dir) and data_sounds_dir != sounds_dir:
|
||||
for item in os.listdir(data_sounds_dir):
|
||||
pack_dir = os.path.join(data_sounds_dir, item)
|
||||
if os.path.isdir(pack_dir):
|
||||
pack_json = os.path.join(pack_dir, "pack.json")
|
||||
if os.path.exists(pack_json):
|
||||
# Avoid duplicates
|
||||
if self.sound_pack_combo.findData(item) == -1:
|
||||
self.sound_pack_combo.addItem(f"{item} (System)", item)
|
||||
except Exception:
|
||||
pass # Ignore errors in system sound pack detection
|
||||
|
||||
def load_current_settings(self):
|
||||
"""Load current settings into the dialog"""
|
||||
# Audio settings
|
||||
current_pack = self.settings.get('audio', 'sound_pack', 'none')
|
||||
index = self.sound_pack_combo.findData(current_pack)
|
||||
if index >= 0:
|
||||
self.sound_pack_combo.setCurrentIndex(index)
|
||||
|
||||
self.master_volume.setValue(int(self.settings.get('audio', 'master_volume', 100) or 100))
|
||||
self.notification_volume.setValue(int(self.settings.get('audio', 'notification_volume', 100) or 100))
|
||||
|
||||
self.enable_sounds.setChecked(bool(self.settings.get('audio', 'enabled', True)))
|
||||
self.enable_post_sounds.setChecked(bool(self.settings.get('audio', 'post_sounds', True)))
|
||||
self.enable_timeline_sounds.setChecked(bool(self.settings.get('audio', 'timeline_sounds', True)))
|
||||
|
||||
# Desktop notification settings (defaults: DMs, mentions, follows ON; others OFF)
|
||||
self.enable_desktop_notifications.setChecked(bool(self.settings.get('notifications', 'enabled', True)))
|
||||
self.notify_direct_messages.setChecked(bool(self.settings.get('notifications', 'direct_messages', True)))
|
||||
self.notify_mentions.setChecked(bool(self.settings.get('notifications', 'mentions', True)))
|
||||
self.notify_boosts.setChecked(bool(self.settings.get('notifications', 'boosts', False)))
|
||||
self.notify_favorites.setChecked(bool(self.settings.get('notifications', 'favorites', False)))
|
||||
self.notify_follows.setChecked(bool(self.settings.get('notifications', 'follows', True)))
|
||||
self.notify_timeline_updates.setChecked(bool(self.settings.get('notifications', 'timeline_updates', False)))
|
||||
|
||||
# Accessibility settings
|
||||
self.page_step_size.setValue(int(self.settings.get('accessibility', 'page_step_size', 5) or 5))
|
||||
self.verbose_announcements.setChecked(bool(self.settings.get('accessibility', 'verbose_announcements', True)))
|
||||
self.announce_thread_state.setChecked(bool(self.settings.get('accessibility', 'announce_thread_state', True)))
|
||||
|
||||
def apply_settings(self):
|
||||
"""Apply the current settings without closing the dialog"""
|
||||
# Audio settings
|
||||
selected_pack = self.sound_pack_combo.currentData()
|
||||
self.settings.set('audio', 'sound_pack', selected_pack)
|
||||
self.settings.set('audio', 'master_volume', self.master_volume.value())
|
||||
self.settings.set('audio', 'notification_volume', self.notification_volume.value())
|
||||
|
||||
self.settings.set('audio', 'enabled', self.enable_sounds.isChecked())
|
||||
self.settings.set('audio', 'post_sounds', self.enable_post_sounds.isChecked())
|
||||
self.settings.set('audio', 'timeline_sounds', self.enable_timeline_sounds.isChecked())
|
||||
|
||||
# Desktop notification settings
|
||||
self.settings.set('notifications', 'enabled', self.enable_desktop_notifications.isChecked())
|
||||
self.settings.set('notifications', 'direct_messages', self.notify_direct_messages.isChecked())
|
||||
self.settings.set('notifications', 'mentions', self.notify_mentions.isChecked())
|
||||
self.settings.set('notifications', 'boosts', self.notify_boosts.isChecked())
|
||||
self.settings.set('notifications', 'favorites', self.notify_favorites.isChecked())
|
||||
self.settings.set('notifications', 'follows', self.notify_follows.isChecked())
|
||||
self.settings.set('notifications', 'timeline_updates', self.notify_timeline_updates.isChecked())
|
||||
|
||||
# Accessibility settings
|
||||
self.settings.set('accessibility', 'page_step_size', self.page_step_size.value())
|
||||
self.settings.set('accessibility', 'verbose_announcements', self.verbose_announcements.isChecked())
|
||||
self.settings.set('accessibility', 'announce_thread_state', self.announce_thread_state.isChecked())
|
||||
|
||||
# Save to file
|
||||
self.settings.save_settings()
|
||||
|
||||
# Emit signal so other components can update
|
||||
self.settings_changed.emit()
|
||||
|
||||
def save_and_close(self):
|
||||
"""Save settings and close the dialog"""
|
||||
self.apply_settings()
|
||||
self.accept()
|
Reference in New Issue
Block a user