Add comprehensive content type support with interface settings reorganization

- Rename Accessibility tab to Interface in settings dialog for better organization
- Add default content type setting (Plain Text, Markdown, HTML) in Interface tab
- Add per-post content type selection in compose dialog with user default
- Implement automatic settings migration from accessibility to interface section
- Separate content display vs editing: timeline shows rendered content, editing shows source
- Wire content type selection through all posting paths (new posts, replies, edits)
- Update Post model with get_display_content() for rendered display and get_content_text() for editing
- Support markdown posts that display formatted in timeline but show source when editing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Storm Dragon
2025-07-21 22:57:26 -04:00
parent b80ae3b0e2
commit 3c45932fea
6 changed files with 138 additions and 35 deletions
+42 -8
View File
@@ -24,6 +24,7 @@ class SettingsManager:
self.config_file = self.config_dir / "bifrost.conf"
self.config = configparser.ConfigParser()
self.load_settings()
self.migrate_settings()
def _get_config_dir(self) -> Path:
"""Get XDG config directory"""
@@ -67,19 +68,22 @@ class SettingsManager:
self.config.set('audio', 'sound_pack', 'default')
self.config.set('audio', 'volume', '100')
# Accessibility settings
self.config.add_section('accessibility')
self.config.set('accessibility', 'announce_thread_state', 'true')
self.config.set('accessibility', 'auto_expand_mentions', 'false')
self.config.set('accessibility', 'keyboard_navigation_wrap', 'true')
self.config.set('accessibility', 'focus_follows_mouse', 'false')
# Interface settings
# Interface settings (formerly accessibility + interface)
self.config.add_section('interface')
self.config.set('interface', 'page_step_size', '5')
self.config.set('interface', 'verbose_announcements', 'true')
self.config.set('interface', 'announce_thread_state', 'true')
self.config.set('interface', 'auto_expand_mentions', 'false')
self.config.set('interface', 'keyboard_navigation_wrap', 'true')
self.config.set('interface', 'focus_follows_mouse', 'false')
self.config.set('interface', 'default_timeline', 'home')
self.config.set('interface', 'show_timestamps', 'true')
self.config.set('interface', 'compact_mode', 'false')
# Post composition settings
self.config.add_section('composition')
self.config.set('composition', 'default_content_type', 'text/plain')
# Desktop notification settings
self.config.add_section('notifications')
self.config.set('notifications', 'enabled', 'true')
@@ -92,6 +96,36 @@ class SettingsManager:
self.save_settings()
def migrate_settings(self):
"""Migrate settings from old structure to new"""
# Migrate accessibility section to interface section
if self.config.has_section('accessibility'):
if not self.config.has_section('interface'):
self.config.add_section('interface')
# Migrate accessibility settings to interface
for option in self.config.options('accessibility'):
value = self.config.get('accessibility', option)
self.config.set('interface', option, value)
# Remove old accessibility section
self.config.remove_section('accessibility')
# Ensure composition section exists
if not self.config.has_section('composition'):
self.config.add_section('composition')
self.config.set('composition', 'default_content_type', 'text/plain')
# Ensure interface section has required settings
if not self.config.has_option('interface', 'page_step_size'):
self.config.set('interface', 'page_step_size', '5')
if not self.config.has_option('interface', 'verbose_announcements'):
self.config.set('interface', 'verbose_announcements', 'true')
if not self.config.has_option('interface', 'announce_thread_state'):
self.config.set('interface', 'announce_thread_state', 'true')
self.save_settings()
def save_settings(self):
"""Save settings to config file"""
with open(self.config_file, 'w') as f:
+2
View File
@@ -438,6 +438,7 @@ class MainWindow(QMainWindow):
result = client.post_status(
content=self.post_data['content'],
visibility=self.post_data['visibility'],
content_type=self.post_data.get('content_type', 'text/plain'),
content_warning=self.post_data['content_warning'],
in_reply_to_id=self.post_data.get('in_reply_to_id'),
poll=self.post_data.get('poll'),
@@ -849,6 +850,7 @@ class MainWindow(QMainWindow):
post.id,
content=data['content'],
visibility=data['visibility'],
content_type=data.get('content_type', 'text/plain'),
content_warning=data['content_warning']
)
self.status_bar.showMessage("Post edited successfully", 2000)
+19 -2
View File
@@ -186,7 +186,7 @@ class Post:
return self.account.display_name or self.account.username
def get_content_text(self) -> str:
"""Get plain text content, handling reblogs"""
"""Get plain text content for editing (raw source), handling reblogs"""
if self.reblog:
return self.reblog.get_content_text()
@@ -200,10 +200,27 @@ class Post:
content = content.replace('&lt;', '<').replace('&gt;', '>').replace('&amp;', '&')
return content.strip()
def get_display_content(self) -> str:
"""Get content for display (rendered HTML), handling reblogs"""
if self.reblog:
return self.reblog.get_display_content()
# Always use the rendered HTML content for display
# This is what the server sends back after processing markdown/etc
if self.content:
# Basic HTML entity decoding
content = self.content.replace('&lt;', '<').replace('&gt;', '>').replace('&amp;', '&')
# Strip HTML tags for plain text display in terminal-style interface
import re
content = re.sub(r'<[^>]+>', '', content)
return content.strip()
return ""
def get_summary_for_screen_reader(self) -> str:
"""Get a summary suitable for screen reader announcement"""
author = self.get_display_name()
content = self.get_content_text()
content = self.get_display_content()
summary = f"{author}: {content}"
+33 -7
View File
@@ -24,11 +24,12 @@ class PostThread(QThread):
post_success = Signal(dict) # Emitted with post data on success
post_failed = Signal(str) # Emitted with error message on failure
def __init__(self, account, content, visibility, content_warning=None, poll=None, media_ids=None):
def __init__(self, account, content, visibility, content_type='text/plain', content_warning=None, poll=None, media_ids=None):
super().__init__()
self.account = account
self.content = content
self.visibility = visibility
self.content_type = content_type
self.content_warning = content_warning
self.poll = poll
self.media_ids = media_ids or []
@@ -41,6 +42,7 @@ class PostThread(QThread):
result = client.post_status(
content=self.content,
visibility=self.visibility,
content_type=self.content_type,
content_warning=self.content_warning,
poll=self.poll,
media_ids=self.media_ids
@@ -65,6 +67,7 @@ class ComposeDialog(QDialog):
self.media_upload_widget = None
self.setup_ui()
self.setup_shortcuts()
self.load_default_settings()
def setup_ui(self):
"""Initialize the compose dialog UI"""
@@ -94,10 +97,11 @@ class ComposeDialog(QDialog):
options_group = QGroupBox("Post Options")
options_layout = QVBoxLayout(options_group)
# Visibility settings
visibility_layout = QHBoxLayout()
visibility_layout.addWidget(QLabel("Visibility:"))
# Post settings row 1: Visibility and Content Type
settings_row1 = QHBoxLayout()
# Visibility settings
settings_row1.addWidget(QLabel("Visibility:"))
self.visibility_combo = AccessibleComboBox()
self.visibility_combo.setAccessibleName("Post Visibility")
self.visibility_combo.addItems([
@@ -106,9 +110,19 @@ class ComposeDialog(QDialog):
"Followers Only",
"Direct Message"
])
visibility_layout.addWidget(self.visibility_combo)
visibility_layout.addStretch()
options_layout.addLayout(visibility_layout)
settings_row1.addWidget(self.visibility_combo)
settings_row1.addWidget(QLabel("Content Type:"))
self.content_type_combo = AccessibleComboBox()
self.content_type_combo.setAccessibleName("Content Type")
self.content_type_combo.setAccessibleDescription("Choose the format for this post")
self.content_type_combo.addItem("Plain Text", "text/plain")
self.content_type_combo.addItem("Markdown", "text/markdown")
self.content_type_combo.addItem("HTML", "text/html")
settings_row1.addWidget(self.content_type_combo)
settings_row1.addStretch()
options_layout.addLayout(settings_row1)
# Content warnings
self.cw_checkbox = QCheckBox("Add Content Warning")
@@ -235,6 +249,14 @@ class ComposeDialog(QDialog):
cancel_shortcut = QShortcut(QKeySequence.Cancel, self)
cancel_shortcut.activated.connect(self.reject)
def load_default_settings(self):
"""Load default settings from configuration"""
# Set default content type
default_type = self.settings.get('composition', 'default_content_type', 'text/plain')
index = self.content_type_combo.findData(default_type)
if index >= 0:
self.content_type_combo.setCurrentIndex(index)
def toggle_content_warning(self, enabled: bool):
"""Toggle content warning field visibility"""
if enabled:
@@ -295,6 +317,9 @@ class ComposeDialog(QDialog):
}
visibility = visibility_map.get(visibility_text, "public")
# Get content type
content_type = self.content_type_combo.currentData()
content_warning = None
if self.cw_checkbox.isChecked():
content_warning = self.cw_edit.toPlainText().strip()
@@ -335,6 +360,7 @@ class ComposeDialog(QDialog):
'account': active_account,
'content': content,
'visibility': visibility,
'content_type': content_type,
'content_warning': content_warning,
'poll': poll_data,
'media_ids': media_ids
+39 -15
View File
@@ -43,8 +43,8 @@ class SettingsDialog(QDialog):
# Desktop notifications tab
self.setup_notifications_tab()
# Accessibility settings tab
self.setup_accessibility_tab()
# Interface settings tab
self.setup_interface_tab()
# Button box
button_box = QDialogButtonBox(
@@ -149,10 +149,10 @@ class SettingsDialog(QDialog):
self.tabs.addTab(notifications_widget, "&Notifications")
def setup_accessibility_tab(self):
"""Set up the accessibility settings tab"""
accessibility_widget = QWidget()
layout = QVBoxLayout(accessibility_widget)
def setup_interface_tab(self):
"""Set up the interface settings tab"""
interface_widget = QWidget()
layout = QVBoxLayout(interface_widget)
# Navigation settings
nav_group = QGroupBox("Navigation Settings")
@@ -166,6 +166,20 @@ class SettingsDialog(QDialog):
layout.addWidget(nav_group)
# Post composition settings
post_group = QGroupBox("Post Composition")
post_layout = QFormLayout(post_group)
self.default_content_type = AccessibleComboBox()
self.default_content_type.setAccessibleName("Default Content Type")
self.default_content_type.setAccessibleDescription("Default format for new posts")
self.default_content_type.addItem("Plain Text", "text/plain")
self.default_content_type.addItem("Markdown", "text/markdown")
self.default_content_type.addItem("HTML", "text/html")
post_layout.addRow("Default content type:", self.default_content_type)
layout.addWidget(post_group)
# Screen reader options
sr_group = QGroupBox("Screen Reader Options")
sr_layout = QVBoxLayout(sr_group)
@@ -195,7 +209,7 @@ class SettingsDialog(QDialog):
layout.addWidget(timeline_group)
layout.addStretch()
self.tabs.addTab(accessibility_widget, "A&ccessibility")
self.tabs.addTab(interface_widget, "&Interface")
def load_sound_packs(self):
"""Load available sound packs from the sounds directory"""
@@ -250,10 +264,16 @@ class SettingsDialog(QDialog):
self.notify_follows.setChecked(self.settings.get_bool('notifications', 'follows', True))
self.notify_timeline_updates.setChecked(self.settings.get_bool('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(self.settings.get_bool('accessibility', 'verbose_announcements', True))
self.announce_thread_state.setChecked(self.settings.get_bool('accessibility', 'announce_thread_state', True))
# Interface settings
self.page_step_size.setValue(int(self.settings.get('interface', 'page_step_size', 5) or 5))
self.verbose_announcements.setChecked(self.settings.get_bool('interface', 'verbose_announcements', True))
self.announce_thread_state.setChecked(self.settings.get_bool('interface', 'announce_thread_state', True))
# Post composition settings
default_type = self.settings.get('composition', 'default_content_type', 'text/plain')
index = self.default_content_type.findData(default_type)
if index >= 0:
self.default_content_type.setCurrentIndex(index)
# Timeline settings
self.posts_per_page.setValue(int(self.settings.get('timeline', 'posts_per_page', 40) or 40))
@@ -275,10 +295,14 @@ class SettingsDialog(QDialog):
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())
# Interface settings
self.settings.set('interface', 'page_step_size', self.page_step_size.value())
self.settings.set('interface', 'verbose_announcements', self.verbose_announcements.isChecked())
self.settings.set('interface', 'announce_thread_state', self.announce_thread_state.isChecked())
# Post composition settings
selected_content_type = self.default_content_type.currentData()
self.settings.set('composition', 'default_content_type', selected_content_type)
# Timeline settings
self.settings.set('timeline', 'posts_per_page', self.posts_per_page.value())
+3 -3
View File
@@ -184,7 +184,7 @@ class TimelineView(AccessibleTreeWidget):
# Show desktop notification (skip if this is initial load)
if not self.skip_notifications:
content_preview = post.get_content_text()
content_preview = post.get_display_content()
if notification_type == 'mention':
self.notification_manager.notify_mention(sender, content_preview)
@@ -448,7 +448,7 @@ class TimelineView(AccessibleTreeWidget):
return
# Get the full post content
content = post.get_content_text()
content = post.get_display_content()
author = post.get_display_name()
# Format for clipboard
@@ -471,7 +471,7 @@ class TimelineView(AccessibleTreeWidget):
if not post:
return []
content = post.get_content_text()
content = post.get_display_content()
# URL regex pattern - matches http/https URLs, more comprehensive
url_pattern = r'https?://[^\s<>"\'`\)\]\}]+'