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:
Storm Dragon
2025-07-20 03:39:47 -04:00
commit 460dfc52a5
31 changed files with 5320 additions and 0 deletions

View 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()