Add comprehensive soundpack manager with security-first design

- **Soundpack Manager**: Full-featured package discovery, download, and installation system
  - Repository management with HTTPS enforcement and validation
  - Directory listing support (auto-discovers .zip files) + soundpacknames.txt fallback
  - Secure download with size limits, timeout protection, and path sanitization
  - Zip bomb protection: file count, individual size, and total extraction limits
  - Audio file validation using magic numbers (not just extensions)
  - Accessible UI with keyboard navigation and screen reader optimization

- **Auto-refresh system**: Smart timeline updates respecting user activity
  - 300-second default interval + 10-second idle buffer
  - Keyboard activity tracking prevents interrupting active users
  - True new content detection using post IDs instead of counts
  - Preserves cursor position during background refreshes

- **Enhanced notifications**: Fixed spam issues and improved targeting
  - Timeline switching now silent (no notifications for actively viewed content)
  - Initial app load notifications disabled for 2 seconds
  - Generic "New content in timeline" messages instead of misleading post counts
  - Separate handling for auto-refresh vs manual refresh scenarios

- **Load more posts improvements**: Better positioning and user experience
  - New posts load below cursor position instead of above
  - Cursor automatically focuses on first new post for natural reading flow
  - Fixed widget hierarchy issues preventing activation

- **Accessibility enhancements**: Workarounds for screen reader quirks
  - Added list headers to fix Orca single-item list reading issues
  - Improved accessible names and descriptions throughout
  - Non-selectable header items with dynamic counts
  - Better error messages and status updates

- **Settings integration**: Corrected soundpack configuration management
  - Fixed inconsistent config keys (now uses [audio] sound_pack consistently)
  - Added soundpack manager to File menu (Ctrl+Shift+P)
  - Repository persistence and validation

🤖 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 15:18:50 -04:00
parent 8661fa67ce
commit f2c4ad1bd6
7 changed files with 1485 additions and 25 deletions

View File

@ -6,8 +6,9 @@ from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QMenuBar, QStatusBar, QPushButton, QTabWidget
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtGui import QKeySequence, QAction, QTextCursor
import time
from config.settings import SettingsManager
from config.accounts import AccountManager
@ -16,6 +17,7 @@ from widgets.compose_dialog import ComposeDialog
from widgets.login_dialog import LoginDialog
from widgets.account_selector import AccountSelector
from widgets.settings_dialog import SettingsDialog
from widgets.soundpack_manager_dialog import SoundpackManagerDialog
from activitypub.client import ActivityPubClient
@ -26,9 +28,15 @@ class MainWindow(QMainWindow):
super().__init__()
self.settings = SettingsManager()
self.account_manager = AccountManager(self.settings)
# Auto-refresh tracking
self.last_activity_time = time.time()
self.is_initial_load = True # Flag to skip notifications on first load
self.setup_ui()
self.setup_menus()
self.setup_shortcuts()
self.setup_auto_refresh()
# Check if we need to show login dialog
if not self.account_manager.has_accounts():
@ -37,6 +45,9 @@ class MainWindow(QMainWindow):
# Play startup sound
if hasattr(self.timeline, 'sound_manager'):
self.timeline.sound_manager.play_startup()
# Mark initial load as complete after startup
QTimer.singleShot(2000, self.mark_initial_load_complete)
def setup_ui(self):
"""Initialize the user interface"""
@ -122,6 +133,12 @@ class MainWindow(QMainWindow):
settings_action.triggered.connect(self.show_settings)
file_menu.addAction(settings_action)
# Soundpack Manager action
soundpack_action = QAction("Sound&pack Manager", self)
soundpack_action.setShortcut(QKeySequence("Ctrl+Shift+P"))
soundpack_action.triggered.connect(self.show_soundpack_manager)
file_menu.addAction(soundpack_action)
file_menu.addSeparator()
# Quit action
@ -206,6 +223,89 @@ class MainWindow(QMainWindow):
# Additional shortcuts that don't need menu items
pass
def setup_auto_refresh(self):
"""Set up auto-refresh timer"""
# Create auto-refresh timer
self.auto_refresh_timer = QTimer()
self.auto_refresh_timer.timeout.connect(self.check_auto_refresh)
# Check every 30 seconds if we should refresh
self.auto_refresh_timer.start(30000) # 30 seconds
def mark_initial_load_complete(self):
"""Mark that initial loading is complete"""
self.is_initial_load = False
# Enable notifications on the timeline
if hasattr(self.timeline, 'enable_notifications'):
self.timeline.enable_notifications()
def keyPressEvent(self, event):
"""Track keyboard activity for auto-refresh"""
self.last_activity_time = time.time()
super().keyPressEvent(event)
def check_auto_refresh(self):
"""Check if we should auto-refresh the timeline"""
# Skip if auto-refresh is disabled
if not self.settings.get_bool('general', 'auto_refresh_enabled', True):
return
# Skip if no account is active
if not self.account_manager.get_active_account():
return
# Get refresh interval from settings
refresh_interval = self.settings.get_int('general', 'timeline_refresh_interval', 300)
# Check if enough time has passed since last activity
time_since_activity = time.time() - self.last_activity_time
required_idle_time = refresh_interval + 10 # refresh_rate + 10 seconds
if time_since_activity >= required_idle_time:
self.auto_refresh_timeline()
def auto_refresh_timeline(self):
"""Automatically refresh the timeline"""
# Store the current scroll position and selected item
current_item = self.timeline.currentItem()
# Store the current newest post ID to detect new content
old_newest_post_id = self.timeline.newest_post_id
# Temporarily disable notifications to prevent double notifications
old_skip_notifications = self.timeline.skip_notifications
self.timeline.skip_notifications = True
# Refresh the timeline
self.timeline.refresh()
# Restore notification setting
self.timeline.skip_notifications = old_skip_notifications
# Check for new content by comparing newest post ID
if (self.timeline.newest_post_id and
old_newest_post_id and
self.timeline.newest_post_id != old_newest_post_id and
not self.is_initial_load):
timeline_name = {
'home': 'home timeline',
'local': 'local timeline',
'federated': 'federated timeline',
'notifications': 'notifications'
}.get(self.timeline.timeline_type, 'timeline')
# Show desktop notification for new content
if hasattr(self.timeline, 'notification_manager'):
self.timeline.notification_manager.notify_new_content(timeline_name)
# Try to restore focus to the previous item
if current_item:
self.timeline.setCurrentItem(current_item)
# Reset activity timer to prevent immediate re-refresh
self.last_activity_time = time.time()
def show_compose_dialog(self):
"""Show the compose post dialog"""
dialog = ComposeDialog(self.account_manager, self)
@ -288,6 +388,11 @@ class MainWindow(QMainWindow):
self.timeline.sound_manager.reload_settings()
self.status_bar.showMessage("Settings saved successfully", 2000)
def show_soundpack_manager(self):
"""Show the soundpack manager dialog"""
dialog = SoundpackManagerDialog(self.settings, self)
dialog.exec()
def refresh_timeline(self):
"""Refresh the current timeline"""
self.timeline.refresh()