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:
+42
-8
@@ -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:
|
||||
|
||||
@@ -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
@@ -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('<', '<').replace('>', '>').replace('&', '&')
|
||||
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('<', '<').replace('>', '>').replace('&', '&')
|
||||
# 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}"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<>"\'`\)\]\}]+'
|
||||
|
||||
Reference in New Issue
Block a user