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_file = self.config_dir / "bifrost.conf"
|
||||||
self.config = configparser.ConfigParser()
|
self.config = configparser.ConfigParser()
|
||||||
self.load_settings()
|
self.load_settings()
|
||||||
|
self.migrate_settings()
|
||||||
|
|
||||||
def _get_config_dir(self) -> Path:
|
def _get_config_dir(self) -> Path:
|
||||||
"""Get XDG config directory"""
|
"""Get XDG config directory"""
|
||||||
@@ -67,19 +68,22 @@ class SettingsManager:
|
|||||||
self.config.set('audio', 'sound_pack', 'default')
|
self.config.set('audio', 'sound_pack', 'default')
|
||||||
self.config.set('audio', 'volume', '100')
|
self.config.set('audio', 'volume', '100')
|
||||||
|
|
||||||
# Accessibility settings
|
# Interface settings (formerly accessibility + interface)
|
||||||
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
|
|
||||||
self.config.add_section('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', 'default_timeline', 'home')
|
||||||
self.config.set('interface', 'show_timestamps', 'true')
|
self.config.set('interface', 'show_timestamps', 'true')
|
||||||
self.config.set('interface', 'compact_mode', 'false')
|
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
|
# Desktop notification settings
|
||||||
self.config.add_section('notifications')
|
self.config.add_section('notifications')
|
||||||
self.config.set('notifications', 'enabled', 'true')
|
self.config.set('notifications', 'enabled', 'true')
|
||||||
@@ -92,6 +96,36 @@ class SettingsManager:
|
|||||||
|
|
||||||
self.save_settings()
|
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):
|
def save_settings(self):
|
||||||
"""Save settings to config file"""
|
"""Save settings to config file"""
|
||||||
with open(self.config_file, 'w') as f:
|
with open(self.config_file, 'w') as f:
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ class MainWindow(QMainWindow):
|
|||||||
result = client.post_status(
|
result = client.post_status(
|
||||||
content=self.post_data['content'],
|
content=self.post_data['content'],
|
||||||
visibility=self.post_data['visibility'],
|
visibility=self.post_data['visibility'],
|
||||||
|
content_type=self.post_data.get('content_type', 'text/plain'),
|
||||||
content_warning=self.post_data['content_warning'],
|
content_warning=self.post_data['content_warning'],
|
||||||
in_reply_to_id=self.post_data.get('in_reply_to_id'),
|
in_reply_to_id=self.post_data.get('in_reply_to_id'),
|
||||||
poll=self.post_data.get('poll'),
|
poll=self.post_data.get('poll'),
|
||||||
@@ -849,6 +850,7 @@ class MainWindow(QMainWindow):
|
|||||||
post.id,
|
post.id,
|
||||||
content=data['content'],
|
content=data['content'],
|
||||||
visibility=data['visibility'],
|
visibility=data['visibility'],
|
||||||
|
content_type=data.get('content_type', 'text/plain'),
|
||||||
content_warning=data['content_warning']
|
content_warning=data['content_warning']
|
||||||
)
|
)
|
||||||
self.status_bar.showMessage("Post edited successfully", 2000)
|
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
|
return self.account.display_name or self.account.username
|
||||||
|
|
||||||
def get_content_text(self) -> str:
|
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:
|
if self.reblog:
|
||||||
return self.reblog.get_content_text()
|
return self.reblog.get_content_text()
|
||||||
|
|
||||||
@@ -200,10 +200,27 @@ class Post:
|
|||||||
content = content.replace('<', '<').replace('>', '>').replace('&', '&')
|
content = content.replace('<', '<').replace('>', '>').replace('&', '&')
|
||||||
return content.strip()
|
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:
|
def get_summary_for_screen_reader(self) -> str:
|
||||||
"""Get a summary suitable for screen reader announcement"""
|
"""Get a summary suitable for screen reader announcement"""
|
||||||
author = self.get_display_name()
|
author = self.get_display_name()
|
||||||
content = self.get_content_text()
|
content = self.get_display_content()
|
||||||
|
|
||||||
summary = f"{author}: {content}"
|
summary = f"{author}: {content}"
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,12 @@ class PostThread(QThread):
|
|||||||
post_success = Signal(dict) # Emitted with post data on success
|
post_success = Signal(dict) # Emitted with post data on success
|
||||||
post_failed = Signal(str) # Emitted with error message on failure
|
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__()
|
super().__init__()
|
||||||
self.account = account
|
self.account = account
|
||||||
self.content = content
|
self.content = content
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
|
self.content_type = content_type
|
||||||
self.content_warning = content_warning
|
self.content_warning = content_warning
|
||||||
self.poll = poll
|
self.poll = poll
|
||||||
self.media_ids = media_ids or []
|
self.media_ids = media_ids or []
|
||||||
@@ -41,6 +42,7 @@ class PostThread(QThread):
|
|||||||
result = client.post_status(
|
result = client.post_status(
|
||||||
content=self.content,
|
content=self.content,
|
||||||
visibility=self.visibility,
|
visibility=self.visibility,
|
||||||
|
content_type=self.content_type,
|
||||||
content_warning=self.content_warning,
|
content_warning=self.content_warning,
|
||||||
poll=self.poll,
|
poll=self.poll,
|
||||||
media_ids=self.media_ids
|
media_ids=self.media_ids
|
||||||
@@ -65,6 +67,7 @@ class ComposeDialog(QDialog):
|
|||||||
self.media_upload_widget = None
|
self.media_upload_widget = None
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.setup_shortcuts()
|
self.setup_shortcuts()
|
||||||
|
self.load_default_settings()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
"""Initialize the compose dialog UI"""
|
"""Initialize the compose dialog UI"""
|
||||||
@@ -94,10 +97,11 @@ class ComposeDialog(QDialog):
|
|||||||
options_group = QGroupBox("Post Options")
|
options_group = QGroupBox("Post Options")
|
||||||
options_layout = QVBoxLayout(options_group)
|
options_layout = QVBoxLayout(options_group)
|
||||||
|
|
||||||
# Visibility settings
|
# Post settings row 1: Visibility and Content Type
|
||||||
visibility_layout = QHBoxLayout()
|
settings_row1 = QHBoxLayout()
|
||||||
visibility_layout.addWidget(QLabel("Visibility:"))
|
|
||||||
|
|
||||||
|
# Visibility settings
|
||||||
|
settings_row1.addWidget(QLabel("Visibility:"))
|
||||||
self.visibility_combo = AccessibleComboBox()
|
self.visibility_combo = AccessibleComboBox()
|
||||||
self.visibility_combo.setAccessibleName("Post Visibility")
|
self.visibility_combo.setAccessibleName("Post Visibility")
|
||||||
self.visibility_combo.addItems([
|
self.visibility_combo.addItems([
|
||||||
@@ -106,9 +110,19 @@ class ComposeDialog(QDialog):
|
|||||||
"Followers Only",
|
"Followers Only",
|
||||||
"Direct Message"
|
"Direct Message"
|
||||||
])
|
])
|
||||||
visibility_layout.addWidget(self.visibility_combo)
|
settings_row1.addWidget(self.visibility_combo)
|
||||||
visibility_layout.addStretch()
|
|
||||||
options_layout.addLayout(visibility_layout)
|
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
|
# Content warnings
|
||||||
self.cw_checkbox = QCheckBox("Add Content Warning")
|
self.cw_checkbox = QCheckBox("Add Content Warning")
|
||||||
@@ -235,6 +249,14 @@ class ComposeDialog(QDialog):
|
|||||||
cancel_shortcut = QShortcut(QKeySequence.Cancel, self)
|
cancel_shortcut = QShortcut(QKeySequence.Cancel, self)
|
||||||
cancel_shortcut.activated.connect(self.reject)
|
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):
|
def toggle_content_warning(self, enabled: bool):
|
||||||
"""Toggle content warning field visibility"""
|
"""Toggle content warning field visibility"""
|
||||||
if enabled:
|
if enabled:
|
||||||
@@ -295,6 +317,9 @@ class ComposeDialog(QDialog):
|
|||||||
}
|
}
|
||||||
visibility = visibility_map.get(visibility_text, "public")
|
visibility = visibility_map.get(visibility_text, "public")
|
||||||
|
|
||||||
|
# Get content type
|
||||||
|
content_type = self.content_type_combo.currentData()
|
||||||
|
|
||||||
content_warning = None
|
content_warning = None
|
||||||
if self.cw_checkbox.isChecked():
|
if self.cw_checkbox.isChecked():
|
||||||
content_warning = self.cw_edit.toPlainText().strip()
|
content_warning = self.cw_edit.toPlainText().strip()
|
||||||
@@ -335,6 +360,7 @@ class ComposeDialog(QDialog):
|
|||||||
'account': active_account,
|
'account': active_account,
|
||||||
'content': content,
|
'content': content,
|
||||||
'visibility': visibility,
|
'visibility': visibility,
|
||||||
|
'content_type': content_type,
|
||||||
'content_warning': content_warning,
|
'content_warning': content_warning,
|
||||||
'poll': poll_data,
|
'poll': poll_data,
|
||||||
'media_ids': media_ids
|
'media_ids': media_ids
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ class SettingsDialog(QDialog):
|
|||||||
# Desktop notifications tab
|
# Desktop notifications tab
|
||||||
self.setup_notifications_tab()
|
self.setup_notifications_tab()
|
||||||
|
|
||||||
# Accessibility settings tab
|
# Interface settings tab
|
||||||
self.setup_accessibility_tab()
|
self.setup_interface_tab()
|
||||||
|
|
||||||
# Button box
|
# Button box
|
||||||
button_box = QDialogButtonBox(
|
button_box = QDialogButtonBox(
|
||||||
@@ -149,10 +149,10 @@ class SettingsDialog(QDialog):
|
|||||||
|
|
||||||
self.tabs.addTab(notifications_widget, "&Notifications")
|
self.tabs.addTab(notifications_widget, "&Notifications")
|
||||||
|
|
||||||
def setup_accessibility_tab(self):
|
def setup_interface_tab(self):
|
||||||
"""Set up the accessibility settings tab"""
|
"""Set up the interface settings tab"""
|
||||||
accessibility_widget = QWidget()
|
interface_widget = QWidget()
|
||||||
layout = QVBoxLayout(accessibility_widget)
|
layout = QVBoxLayout(interface_widget)
|
||||||
|
|
||||||
# Navigation settings
|
# Navigation settings
|
||||||
nav_group = QGroupBox("Navigation Settings")
|
nav_group = QGroupBox("Navigation Settings")
|
||||||
@@ -166,6 +166,20 @@ class SettingsDialog(QDialog):
|
|||||||
|
|
||||||
layout.addWidget(nav_group)
|
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
|
# Screen reader options
|
||||||
sr_group = QGroupBox("Screen Reader Options")
|
sr_group = QGroupBox("Screen Reader Options")
|
||||||
sr_layout = QVBoxLayout(sr_group)
|
sr_layout = QVBoxLayout(sr_group)
|
||||||
@@ -195,7 +209,7 @@ class SettingsDialog(QDialog):
|
|||||||
layout.addWidget(timeline_group)
|
layout.addWidget(timeline_group)
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
self.tabs.addTab(accessibility_widget, "A&ccessibility")
|
self.tabs.addTab(interface_widget, "&Interface")
|
||||||
|
|
||||||
def load_sound_packs(self):
|
def load_sound_packs(self):
|
||||||
"""Load available sound packs from the sounds directory"""
|
"""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_follows.setChecked(self.settings.get_bool('notifications', 'follows', True))
|
||||||
self.notify_timeline_updates.setChecked(self.settings.get_bool('notifications', 'timeline_updates', False))
|
self.notify_timeline_updates.setChecked(self.settings.get_bool('notifications', 'timeline_updates', False))
|
||||||
|
|
||||||
# Accessibility settings
|
# Interface settings
|
||||||
self.page_step_size.setValue(int(self.settings.get('accessibility', 'page_step_size', 5) or 5))
|
self.page_step_size.setValue(int(self.settings.get('interface', 'page_step_size', 5) or 5))
|
||||||
self.verbose_announcements.setChecked(self.settings.get_bool('accessibility', 'verbose_announcements', True))
|
self.verbose_announcements.setChecked(self.settings.get_bool('interface', 'verbose_announcements', True))
|
||||||
self.announce_thread_state.setChecked(self.settings.get_bool('accessibility', 'announce_thread_state', 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
|
# Timeline settings
|
||||||
self.posts_per_page.setValue(int(self.settings.get('timeline', 'posts_per_page', 40) or 40))
|
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', 'follows', self.notify_follows.isChecked())
|
||||||
self.settings.set('notifications', 'timeline_updates', self.notify_timeline_updates.isChecked())
|
self.settings.set('notifications', 'timeline_updates', self.notify_timeline_updates.isChecked())
|
||||||
|
|
||||||
# Accessibility settings
|
# Interface settings
|
||||||
self.settings.set('accessibility', 'page_step_size', self.page_step_size.value())
|
self.settings.set('interface', 'page_step_size', self.page_step_size.value())
|
||||||
self.settings.set('accessibility', 'verbose_announcements', self.verbose_announcements.isChecked())
|
self.settings.set('interface', 'verbose_announcements', self.verbose_announcements.isChecked())
|
||||||
self.settings.set('accessibility', 'announce_thread_state', self.announce_thread_state.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
|
# Timeline settings
|
||||||
self.settings.set('timeline', 'posts_per_page', self.posts_per_page.value())
|
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)
|
# Show desktop notification (skip if this is initial load)
|
||||||
if not self.skip_notifications:
|
if not self.skip_notifications:
|
||||||
content_preview = post.get_content_text()
|
content_preview = post.get_display_content()
|
||||||
|
|
||||||
if notification_type == 'mention':
|
if notification_type == 'mention':
|
||||||
self.notification_manager.notify_mention(sender, content_preview)
|
self.notification_manager.notify_mention(sender, content_preview)
|
||||||
@@ -448,7 +448,7 @@ class TimelineView(AccessibleTreeWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Get the full post content
|
# Get the full post content
|
||||||
content = post.get_content_text()
|
content = post.get_display_content()
|
||||||
author = post.get_display_name()
|
author = post.get_display_name()
|
||||||
|
|
||||||
# Format for clipboard
|
# Format for clipboard
|
||||||
@@ -471,7 +471,7 @@ class TimelineView(AccessibleTreeWidget):
|
|||||||
if not post:
|
if not post:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
content = post.get_content_text()
|
content = post.get_display_content()
|
||||||
|
|
||||||
# URL regex pattern - matches http/https URLs, more comprehensive
|
# URL regex pattern - matches http/https URLs, more comprehensive
|
||||||
url_pattern = r'https?://[^\s<>"\'`\)\]\}]+'
|
url_pattern = r'https?://[^\s<>"\'`\)\]\}]+'
|
||||||
|
|||||||
Reference in New Issue
Block a user