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:
@ -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()
|
||||
|
Reference in New Issue
Block a user