fix
This commit is contained in:
parent
7bcac9466a
commit
7c7c54e617
7 changed files with 3616 additions and 6 deletions
|
|
@ -1 +1 @@
|
||||||
{"access_token": "BQBWe7sCCLW9H6GTItLTJ6Fjw7YK6a_quPz3y2RgAXqTLwlQEMFLDO6xCmD8efMEe2OhV4B-MwzbcktuHT67wejexVVcnAJasEFH9RbVHUaVCP7MNgHuirirRazH6QweOd01DL3iieeN6lvGfRJ97BBkfQ-u5StX3foZZuxI4HOhCrALuIKJjWfROcjMRdy-qZeap_8nas0HwKOqowAxzqyjhRki0iZH128vrZe1sfCUM4895t-ULsMBWy2GAjTd", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752303544, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
{"access_token": "BQCzY4NDr26JqIBSxwATrH7MQMG5i9kCZxPvvtjcoVTyFZPRYPOEltaZEqN44ukQhNDo6C8fvkhlSPS6tk2N7zVitnBk9bT2IRAQiQGpxRS67kNlUfxUJSw52YAW83vECziyLfUfOkLs-E2LzoJDQtq_QOx18b_wO883mT1C8OyNFjVGvvjV5Srf8oSMJ4KN6WYwMpllfTbno7YlT7R5oPOtjgNquuUOhLHRWWp9KXEermO-MZWtWl_OxBAlhhmi", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752342994, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||||
3290
logs/app.log
3290
logs/app.log
File diff suppressed because it is too large
Load diff
5
main.py
5
main.py
|
|
@ -137,6 +137,11 @@ class MainWindow(QMainWindow):
|
||||||
self.downloads_page.track_stopped.connect(self.sidebar.media_player.clear_track)
|
self.downloads_page.track_stopped.connect(self.sidebar.media_player.clear_track)
|
||||||
self.downloads_page.track_finished.connect(self.sidebar.media_player.clear_track)
|
self.downloads_page.track_finished.connect(self.sidebar.media_player.clear_track)
|
||||||
|
|
||||||
|
# Connect loading animation signals
|
||||||
|
self.downloads_page.track_loading_started.connect(lambda result: self.sidebar.media_player.show_loading())
|
||||||
|
self.downloads_page.track_loading_finished.connect(lambda result: self.sidebar.media_player.hide_loading())
|
||||||
|
self.downloads_page.track_loading_progress.connect(lambda progress, result: self.sidebar.media_player.set_loading_progress(progress))
|
||||||
|
|
||||||
# Connect sidebar media player signals to downloads page
|
# Connect sidebar media player signals to downloads page
|
||||||
self.sidebar.media_player.play_pause_requested.connect(self.downloads_page.handle_sidebar_play_pause)
|
self.sidebar.media_player.play_pause_requested.connect(self.downloads_page.handle_sidebar_play_pause)
|
||||||
self.sidebar.media_player.stop_requested.connect(self.downloads_page.handle_sidebar_stop)
|
self.sidebar.media_player.stop_requested.connect(self.downloads_page.handle_sidebar_stop)
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -643,6 +643,7 @@ class StreamingThread(QThread):
|
||||||
streaming_started = pyqtSignal(str, object) # Message, search_result
|
streaming_started = pyqtSignal(str, object) # Message, search_result
|
||||||
streaming_finished = pyqtSignal(str, object) # Message, search_result
|
streaming_finished = pyqtSignal(str, object) # Message, search_result
|
||||||
streaming_failed = pyqtSignal(str, object) # Error message, search_result
|
streaming_failed = pyqtSignal(str, object) # Error message, search_result
|
||||||
|
streaming_progress = pyqtSignal(float, object) # Progress percentage (0-100), search_result
|
||||||
|
|
||||||
def __init__(self, soulseek_client, search_result):
|
def __init__(self, soulseek_client, search_result):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -697,12 +698,56 @@ class StreamingThread(QThread):
|
||||||
# Standard streaming - wait for complete download
|
# Standard streaming - wait for complete download
|
||||||
max_wait_time = 45 # Wait up to 45 seconds
|
max_wait_time = 45 # Wait up to 45 seconds
|
||||||
poll_interval = 2 # Check every 2 seconds
|
poll_interval = 2 # Check every 2 seconds
|
||||||
found_file = None
|
|
||||||
|
# Track elapsed time for fallback progress estimation
|
||||||
|
start_time = time.time()
|
||||||
|
estimated_duration = 15.0 # Estimate 15 seconds for typical download
|
||||||
|
last_progress_sent = 0.0
|
||||||
|
found_file = None # Initialize found_file outside the loop
|
||||||
|
|
||||||
for wait_count in range(max_wait_time // poll_interval):
|
for wait_count in range(max_wait_time // poll_interval):
|
||||||
if self._stop_requested:
|
if self._stop_requested:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Calculate fallback progress based on elapsed time
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
estimated_progress = min(95.0, (elapsed_time / estimated_duration) * 95.0) # Cap at 95%
|
||||||
|
|
||||||
|
# Check download progress via slskd API
|
||||||
|
api_progress = None
|
||||||
|
try:
|
||||||
|
download_status = loop.run_until_complete(self._check_download_progress())
|
||||||
|
if download_status:
|
||||||
|
api_progress = download_status.progress
|
||||||
|
print(f"API Download progress: {api_progress:.1f}%")
|
||||||
|
|
||||||
|
# Check if download is complete
|
||||||
|
if download_status.state.lower().startswith('completed') or api_progress >= 100:
|
||||||
|
print(f"✓ Download completed via API status: {download_status.state}")
|
||||||
|
# Try to find the actual file - with retries for file system sync
|
||||||
|
for retry_count in range(5): # Try up to 5 times with delays
|
||||||
|
found_file = self._find_downloaded_file(download_path)
|
||||||
|
if found_file:
|
||||||
|
print(f"✓ Found completed file after {retry_count} retries: {found_file}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print(f"⏳ File not found yet, waiting... (retry {retry_count + 1}/5)")
|
||||||
|
time.sleep(1) # Wait 1 second for file system to sync
|
||||||
|
|
||||||
|
if found_file:
|
||||||
|
break # Exit main polling loop
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not check download progress: {e}")
|
||||||
|
|
||||||
|
# Use API progress if available, otherwise use estimated progress
|
||||||
|
current_progress = api_progress if api_progress is not None and api_progress > 0 else estimated_progress
|
||||||
|
|
||||||
|
# Only emit progress updates if there's a meaningful change (>5%)
|
||||||
|
if current_progress - last_progress_sent >= 5.0:
|
||||||
|
self.streaming_progress.emit(current_progress, self.search_result)
|
||||||
|
print(f"Progress update: {current_progress:.1f}% (API: {api_progress}, Estimated: {estimated_progress:.1f}%)")
|
||||||
|
last_progress_sent = current_progress
|
||||||
|
|
||||||
# Search for the downloaded file in the downloads directory
|
# Search for the downloaded file in the downloads directory
|
||||||
found_file = self._find_downloaded_file(download_path)
|
found_file = self._find_downloaded_file(download_path)
|
||||||
|
|
||||||
|
|
@ -721,7 +766,8 @@ class StreamingThread(QThread):
|
||||||
# Clean up empty directories left behind
|
# Clean up empty directories left behind
|
||||||
self._cleanup_empty_directories(download_path, found_file)
|
self._cleanup_empty_directories(download_path, found_file)
|
||||||
|
|
||||||
# Signal that streaming is ready
|
# Signal that streaming is ready (100% progress)
|
||||||
|
self.streaming_progress.emit(100.0, self.search_result)
|
||||||
self.streaming_finished.emit(f"Stream ready: {os.path.basename(found_file)}", self.search_result)
|
self.streaming_finished.emit(f"Stream ready: {os.path.basename(found_file)}", self.search_result)
|
||||||
self.temp_file_path = stream_path
|
self.temp_file_path = stream_path
|
||||||
print(f"✓ Stream file ready for playback: {stream_path}")
|
print(f"✓ Stream file ready for playback: {stream_path}")
|
||||||
|
|
@ -737,7 +783,39 @@ class StreamingThread(QThread):
|
||||||
time.sleep(poll_interval)
|
time.sleep(poll_interval)
|
||||||
else:
|
else:
|
||||||
# Timed out waiting for file
|
# Timed out waiting for file
|
||||||
|
print(f"❌ Polling loop completed, timeout reached. found_file = {found_file}")
|
||||||
self.streaming_failed.emit("Stream download timed out - file not found", self.search_result)
|
self.streaming_failed.emit("Stream download timed out - file not found", self.search_result)
|
||||||
|
|
||||||
|
# Post-polling file processing
|
||||||
|
print(f"📋 After polling loop: found_file = {found_file}")
|
||||||
|
if found_file:
|
||||||
|
print(f"🎯 Processing found file: {found_file}")
|
||||||
|
|
||||||
|
# Move the file to Stream folder with original filename
|
||||||
|
original_filename = os.path.basename(found_file)
|
||||||
|
stream_path = os.path.join(stream_folder, original_filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Move file to Stream folder
|
||||||
|
print(f"📁 Moving file from {found_file} to {stream_path}")
|
||||||
|
shutil.move(found_file, stream_path)
|
||||||
|
print(f"✅ Successfully moved file to stream folder: {stream_path}")
|
||||||
|
|
||||||
|
# Clean up empty directories left behind
|
||||||
|
self._cleanup_empty_directories(download_path, found_file)
|
||||||
|
|
||||||
|
# Signal that streaming is ready (100% progress)
|
||||||
|
self.streaming_progress.emit(100.0, self.search_result)
|
||||||
|
self.streaming_finished.emit(f"Stream ready: {os.path.basename(found_file)}", self.search_result)
|
||||||
|
self.temp_file_path = stream_path
|
||||||
|
print(f"🎵 Stream file ready for playback: {stream_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error moving file to stream folder: {e}")
|
||||||
|
self.streaming_failed.emit(f"Failed to prepare stream file: {e}", self.search_result)
|
||||||
|
else:
|
||||||
|
print(f"❌ No file found after polling loop completed")
|
||||||
|
self.streaming_failed.emit("Stream download failed - file not found after completion", self.search_result)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.streaming_failed.emit("Streaming failed to start", self.search_result)
|
self.streaming_failed.emit("Streaming failed to start", self.search_result)
|
||||||
|
|
@ -761,6 +839,33 @@ class StreamingThread(QThread):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error cleaning up streaming event loop: {e}")
|
print(f"Error cleaning up streaming event loop: {e}")
|
||||||
|
|
||||||
|
async def _check_download_progress(self):
|
||||||
|
"""Check download progress via slskd API for the current streaming download"""
|
||||||
|
try:
|
||||||
|
# Get all current downloads and find ours by filename match
|
||||||
|
all_downloads = await self.soulseek_client.get_all_downloads()
|
||||||
|
print(f"🔍 Looking for download - Target: {self.search_result.username}:{os.path.basename(self.search_result.filename)}")
|
||||||
|
print(f"🔍 Found {len(all_downloads)} total downloads in API")
|
||||||
|
|
||||||
|
# Look for our specific file by filename and username
|
||||||
|
target_filename = os.path.basename(self.search_result.filename)
|
||||||
|
target_username = self.search_result.username
|
||||||
|
|
||||||
|
for i, download in enumerate(all_downloads):
|
||||||
|
download_filename = os.path.basename(download.filename)
|
||||||
|
print(f"📁 Download {i+1}: {download.username}:{download_filename} - State: {download.state} - Progress: {download.progress}%")
|
||||||
|
|
||||||
|
if (download_filename == target_filename and
|
||||||
|
download.username == target_username):
|
||||||
|
print(f"✅ Found matching download: {download.state} - {download.progress}%")
|
||||||
|
return download
|
||||||
|
|
||||||
|
print(f"❌ No matching download found for {target_username}:{target_filename}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking download progress: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def _find_downloaded_file(self, download_path):
|
def _find_downloaded_file(self, download_path):
|
||||||
"""Find the downloaded audio file in the downloads directory tree"""
|
"""Find the downloaded audio file in the downloads directory tree"""
|
||||||
import os
|
import os
|
||||||
|
|
@ -2896,6 +3001,9 @@ class DownloadsPage(QWidget):
|
||||||
track_stopped = pyqtSignal()
|
track_stopped = pyqtSignal()
|
||||||
track_finished = pyqtSignal()
|
track_finished = pyqtSignal()
|
||||||
track_position_updated = pyqtSignal(float, float) # current_position, duration in seconds
|
track_position_updated = pyqtSignal(float, float) # current_position, duration in seconds
|
||||||
|
track_loading_started = pyqtSignal(object) # Track result object when streaming starts
|
||||||
|
track_loading_finished = pyqtSignal(object) # Track result object when streaming completes
|
||||||
|
track_loading_progress = pyqtSignal(float, object) # Progress percentage (0-100), track result object
|
||||||
|
|
||||||
def __init__(self, soulseek_client=None, parent=None):
|
def __init__(self, soulseek_client=None, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
@ -4342,6 +4450,7 @@ class DownloadsPage(QWidget):
|
||||||
streaming_thread = StreamingThread(self.soulseek_client, search_result)
|
streaming_thread = StreamingThread(self.soulseek_client, search_result)
|
||||||
streaming_thread.streaming_started.connect(self.on_streaming_started, Qt.ConnectionType.QueuedConnection)
|
streaming_thread.streaming_started.connect(self.on_streaming_started, Qt.ConnectionType.QueuedConnection)
|
||||||
streaming_thread.streaming_finished.connect(self.on_streaming_finished, Qt.ConnectionType.QueuedConnection)
|
streaming_thread.streaming_finished.connect(self.on_streaming_finished, Qt.ConnectionType.QueuedConnection)
|
||||||
|
streaming_thread.streaming_progress.connect(self.on_streaming_progress, Qt.ConnectionType.QueuedConnection)
|
||||||
streaming_thread.streaming_failed.connect(self.on_streaming_failed, Qt.ConnectionType.QueuedConnection)
|
streaming_thread.streaming_failed.connect(self.on_streaming_failed, Qt.ConnectionType.QueuedConnection)
|
||||||
streaming_thread.finished.connect(
|
streaming_thread.finished.connect(
|
||||||
functools.partial(self.on_streaming_thread_finished, streaming_thread),
|
functools.partial(self.on_streaming_thread_finished, streaming_thread),
|
||||||
|
|
@ -4372,6 +4481,9 @@ class DownloadsPage(QWidget):
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
# Button was deleted, ignore
|
# Button was deleted, ignore
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Emit signal for media player loading animation
|
||||||
|
self.track_loading_started.emit(search_result)
|
||||||
|
|
||||||
def on_streaming_finished(self, message, search_result):
|
def on_streaming_finished(self, message, search_result):
|
||||||
"""Handle streaming completion - start actual audio playback"""
|
"""Handle streaming completion - start actual audio playback"""
|
||||||
|
|
@ -4419,6 +4531,7 @@ class DownloadsPage(QWidget):
|
||||||
pass
|
pass
|
||||||
# Emit track started signal for sidebar media player
|
# Emit track started signal for sidebar media player
|
||||||
if hasattr(self, 'current_track_result') and self.current_track_result:
|
if hasattr(self, 'current_track_result') and self.current_track_result:
|
||||||
|
self.track_loading_finished.emit(self.current_track_result)
|
||||||
self.track_started.emit(self.current_track_result)
|
self.track_started.emit(self.current_track_result)
|
||||||
else:
|
else:
|
||||||
print(f"❌ Failed to start audio playback")
|
print(f"❌ Failed to start audio playback")
|
||||||
|
|
@ -4452,6 +4565,21 @@ class DownloadsPage(QWidget):
|
||||||
pass
|
pass
|
||||||
self.currently_playing_button = None
|
self.currently_playing_button = None
|
||||||
|
|
||||||
|
def on_streaming_progress(self, progress_percent, search_result):
|
||||||
|
"""Handle streaming progress updates"""
|
||||||
|
print(f"Streaming progress: {progress_percent:.1f}% for {search_result.filename}")
|
||||||
|
|
||||||
|
# Check if this progress is for the currently requested track
|
||||||
|
if hasattr(self, 'current_track_result') and self.current_track_result:
|
||||||
|
current_track_id = f"{self.current_track_result.username}:{self.current_track_result.filename}"
|
||||||
|
progress_track_id = f"{search_result.username}:{search_result.filename}"
|
||||||
|
|
||||||
|
if current_track_id == progress_track_id:
|
||||||
|
# Emit progress signal for media player
|
||||||
|
self.track_loading_progress.emit(progress_percent, search_result)
|
||||||
|
else:
|
||||||
|
print(f"🚫 Ignoring progress for old streaming result: {search_result.filename}")
|
||||||
|
|
||||||
def on_streaming_failed(self, error_msg, search_result):
|
def on_streaming_failed(self, error_msg, search_result):
|
||||||
"""Handle streaming failure"""
|
"""Handle streaming failure"""
|
||||||
print(f"Streaming failed: {error_msg}")
|
print(f"Streaming failed: {error_msg}")
|
||||||
|
|
|
||||||
193
ui/sidebar.py
193
ui/sidebar.py
|
|
@ -1,7 +1,7 @@
|
||||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||||
QLabel, QFrame, QSizePolicy, QSpacerItem, QSlider, QProgressBar)
|
QLabel, QFrame, QSizePolicy, QSpacerItem, QSlider, QProgressBar)
|
||||||
from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect, QTimer
|
from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect, QTimer, pyqtProperty
|
||||||
from PyQt6.QtGui import QFont, QPalette, QIcon, QPixmap, QPainter, QFontMetrics
|
from PyQt6.QtGui import QFont, QPalette, QIcon, QPixmap, QPainter, QFontMetrics, QColor, QLinearGradient
|
||||||
|
|
||||||
class ScrollingLabel(QLabel):
|
class ScrollingLabel(QLabel):
|
||||||
"""A label that smoothly scrolls text horizontally when it's too long to fit"""
|
"""A label that smoothly scrolls text horizontally when it's too long to fit"""
|
||||||
|
|
@ -285,6 +285,173 @@ class StatusIndicator(QWidget):
|
||||||
""")
|
""")
|
||||||
self.service_label.setStyleSheet("color: #b3b3b3; font-weight: 400;")
|
self.service_label.setStyleSheet("color: #b3b3b3; font-weight: 400;")
|
||||||
|
|
||||||
|
class LoadingAnimation(QWidget):
|
||||||
|
"""Thin horizontal loading animation for media player with dual-mode capability"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setFixedHeight(12) # Increased height for text overlay
|
||||||
|
self._progress = 0.0
|
||||||
|
self._is_active = False
|
||||||
|
self._mode = "indefinite" # "indefinite" or "determinate"
|
||||||
|
self._determinate_progress = 0.0 # 0-100% for determinate mode
|
||||||
|
|
||||||
|
# Animation setup for indefinite mode
|
||||||
|
self.animation = QPropertyAnimation(self, b"progress")
|
||||||
|
self.animation.setDuration(1200) # 1.2 second cycle
|
||||||
|
self.animation.setStartValue(0.0)
|
||||||
|
self.animation.setEndValue(1.0)
|
||||||
|
self.animation.setLoopCount(-1) # Infinite loop
|
||||||
|
self.animation.setEasingCurve(QEasingCurve.Type.InOutSine)
|
||||||
|
|
||||||
|
# Progress value animation for smooth transitions in determinate mode
|
||||||
|
self.progress_animation = QPropertyAnimation(self, b"determinate_progress")
|
||||||
|
self.progress_animation.setDuration(300) # Smooth 300ms transitions
|
||||||
|
self.progress_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||||
|
|
||||||
|
# Completion glow effect
|
||||||
|
self._glow_opacity = 0.0
|
||||||
|
self.glow_animation = QPropertyAnimation(self, b"glow_opacity")
|
||||||
|
self.glow_animation.setDuration(800) # Slower glow pulse
|
||||||
|
self.glow_animation.setStartValue(0.0)
|
||||||
|
self.glow_animation.setEndValue(1.0)
|
||||||
|
self.glow_animation.setLoopCount(3) # Pulse 3 times
|
||||||
|
self.glow_animation.setEasingCurve(QEasingCurve.Type.InOutSine)
|
||||||
|
|
||||||
|
self.hide() # Start hidden
|
||||||
|
|
||||||
|
@pyqtProperty(float)
|
||||||
|
def progress(self):
|
||||||
|
return self._progress
|
||||||
|
|
||||||
|
@progress.setter
|
||||||
|
def progress(self, value):
|
||||||
|
self._progress = value
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@pyqtProperty(float)
|
||||||
|
def determinate_progress(self):
|
||||||
|
return self._determinate_progress
|
||||||
|
|
||||||
|
@determinate_progress.setter
|
||||||
|
def determinate_progress(self, value):
|
||||||
|
self._determinate_progress = value
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@pyqtProperty(float)
|
||||||
|
def glow_opacity(self):
|
||||||
|
return self._glow_opacity
|
||||||
|
|
||||||
|
@glow_opacity.setter
|
||||||
|
def glow_opacity(self, value):
|
||||||
|
self._glow_opacity = value
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def start_animation(self):
|
||||||
|
"""Start the indefinite loading animation"""
|
||||||
|
self._is_active = True
|
||||||
|
self._mode = "indefinite"
|
||||||
|
self.show()
|
||||||
|
self.animation.start()
|
||||||
|
|
||||||
|
def set_progress(self, percentage):
|
||||||
|
"""Set determinate progress (0-100%) with smooth animation"""
|
||||||
|
if not self._is_active:
|
||||||
|
self._is_active = True
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
# Switch to determinate mode
|
||||||
|
if self._mode == "indefinite":
|
||||||
|
self._mode = "determinate"
|
||||||
|
self.animation.stop() # Stop indefinite animation
|
||||||
|
|
||||||
|
# Animate to new progress value
|
||||||
|
self.progress_animation.setStartValue(self._determinate_progress)
|
||||||
|
self.progress_animation.setEndValue(percentage)
|
||||||
|
self.progress_animation.start()
|
||||||
|
|
||||||
|
# Trigger completion glow effect when reaching 100%
|
||||||
|
if percentage >= 100 and self._determinate_progress < 100:
|
||||||
|
self.glow_animation.start()
|
||||||
|
|
||||||
|
def stop_animation(self):
|
||||||
|
"""Stop the loading animation"""
|
||||||
|
self._is_active = False
|
||||||
|
self._mode = "indefinite"
|
||||||
|
self.animation.stop()
|
||||||
|
self.progress_animation.stop()
|
||||||
|
self.glow_animation.stop()
|
||||||
|
self.hide()
|
||||||
|
self._progress = 0.0
|
||||||
|
self._determinate_progress = 0.0
|
||||||
|
self._glow_opacity = 0.0
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
"""Custom paint event for dual-mode animation with text overlay"""
|
||||||
|
if not self._is_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
|
||||||
|
|
||||||
|
width = self.width()
|
||||||
|
height = self.height()
|
||||||
|
progress_bar_height = 4 # Bottom 4px for progress bar
|
||||||
|
text_height = height - progress_bar_height # Top area for text
|
||||||
|
|
||||||
|
# Background for progress bar area
|
||||||
|
progress_rect = self.rect()
|
||||||
|
progress_rect.setTop(text_height)
|
||||||
|
painter.fillRect(progress_rect, QColor(40, 40, 40))
|
||||||
|
|
||||||
|
if self._mode == "indefinite":
|
||||||
|
# Indefinite mode: animated gradient wave
|
||||||
|
gradient_width = width * 0.3 # 30% of total width
|
||||||
|
center_x = self._progress * width
|
||||||
|
|
||||||
|
for i in range(int(gradient_width)):
|
||||||
|
alpha = max(0, 255 - (abs(i - gradient_width/2) * 8))
|
||||||
|
color = QColor(29, 185, 84, int(alpha)) # Spotify green with fade
|
||||||
|
x = int(center_x - gradient_width/2 + i)
|
||||||
|
if 0 <= x < width:
|
||||||
|
painter.fillRect(x, text_height, 1, progress_bar_height, color)
|
||||||
|
|
||||||
|
else: # determinate mode
|
||||||
|
# Determinate mode: progress bar with percentage
|
||||||
|
progress_width = (self._determinate_progress / 100.0) * width
|
||||||
|
|
||||||
|
# Progress bar with gradient
|
||||||
|
if progress_width > 0:
|
||||||
|
progress_fill_rect = QRect(0, text_height, int(progress_width), progress_bar_height)
|
||||||
|
|
||||||
|
# Create subtle gradient for progress bar
|
||||||
|
gradient = QLinearGradient(0, text_height, progress_width, text_height)
|
||||||
|
gradient.setColorAt(0, QColor(29, 185, 84)) # Spotify green
|
||||||
|
gradient.setColorAt(1, QColor(30, 215, 96)) # Lighter green
|
||||||
|
|
||||||
|
painter.fillRect(progress_fill_rect, gradient)
|
||||||
|
|
||||||
|
# Add animated glow effect during completion
|
||||||
|
if self._glow_opacity > 0:
|
||||||
|
glow_alpha = int(120 * self._glow_opacity) # Max alpha of 120
|
||||||
|
glow_color = QColor(29, 185, 84, glow_alpha)
|
||||||
|
|
||||||
|
# Expand glow slightly beyond progress bar for effect
|
||||||
|
glow_rect = QRect(0, text_height - 1, width, progress_bar_height + 2)
|
||||||
|
painter.fillRect(glow_rect, glow_color)
|
||||||
|
|
||||||
|
# Percentage text overlay (elegant, small font)
|
||||||
|
if text_height > 0 and self._determinate_progress > 0:
|
||||||
|
font = QFont("Segoe UI", 7, QFont.Weight.Medium) # Small, elegant font
|
||||||
|
painter.setFont(font)
|
||||||
|
painter.setPen(QColor(180, 180, 180)) # Light gray text
|
||||||
|
|
||||||
|
percentage_text = f"{int(self._determinate_progress)}%"
|
||||||
|
text_rect = QRect(0, 0, width, text_height)
|
||||||
|
painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, percentage_text)
|
||||||
|
|
||||||
class MediaPlayer(QWidget):
|
class MediaPlayer(QWidget):
|
||||||
# Signals for media control
|
# Signals for media control
|
||||||
play_pause_requested = pyqtSignal()
|
play_pause_requested = pyqtSignal()
|
||||||
|
|
@ -321,6 +488,10 @@ class MediaPlayer(QWidget):
|
||||||
layout.setContentsMargins(16, 10, 16, 10)
|
layout.setContentsMargins(16, 10, 16, 10)
|
||||||
layout.setSpacing(10)
|
layout.setSpacing(10)
|
||||||
|
|
||||||
|
# Loading animation at the top
|
||||||
|
self.loading_animation = LoadingAnimation()
|
||||||
|
layout.addWidget(self.loading_animation)
|
||||||
|
|
||||||
# Always visible header with basic controls
|
# Always visible header with basic controls
|
||||||
self.header = self.create_header()
|
self.header = self.create_header()
|
||||||
layout.addWidget(self.header)
|
layout.addWidget(self.header)
|
||||||
|
|
@ -581,6 +752,9 @@ class MediaPlayer(QWidget):
|
||||||
# Set to playing state (show pause button since track just started)
|
# Set to playing state (show pause button since track just started)
|
||||||
self.set_playing_state(True)
|
self.set_playing_state(True)
|
||||||
|
|
||||||
|
# Hide loading animation now that track is ready
|
||||||
|
self.hide_loading()
|
||||||
|
|
||||||
# Hide no track message and show player
|
# Hide no track message and show player
|
||||||
self.no_track_label.setVisible(False)
|
self.no_track_label.setVisible(False)
|
||||||
|
|
||||||
|
|
@ -606,8 +780,9 @@ class MediaPlayer(QWidget):
|
||||||
self.current_track = None
|
self.current_track = None
|
||||||
self.is_playing = False
|
self.is_playing = False
|
||||||
|
|
||||||
# Stop any scrolling animation
|
# Stop any animations
|
||||||
self.track_info.stop_scrolling()
|
self.track_info.stop_scrolling()
|
||||||
|
self.hide_loading()
|
||||||
|
|
||||||
# Update UI
|
# Update UI
|
||||||
self.track_info.setText("No track")
|
self.track_info.setText("No track")
|
||||||
|
|
@ -636,6 +811,18 @@ class MediaPlayer(QWidget):
|
||||||
volume = value / 100.0 # Convert to 0.0-1.0
|
volume = value / 100.0 # Convert to 0.0-1.0
|
||||||
self.volume_changed.emit(volume)
|
self.volume_changed.emit(volume)
|
||||||
|
|
||||||
|
def show_loading(self):
|
||||||
|
"""Show and start the loading animation"""
|
||||||
|
self.loading_animation.start_animation()
|
||||||
|
|
||||||
|
def hide_loading(self):
|
||||||
|
"""Hide and stop the loading animation"""
|
||||||
|
self.loading_animation.stop_animation()
|
||||||
|
|
||||||
|
def set_loading_progress(self, percentage):
|
||||||
|
"""Set loading progress percentage (0-100)"""
|
||||||
|
self.loading_animation.set_progress(percentage)
|
||||||
|
|
||||||
|
|
||||||
class ModernSidebar(QWidget):
|
class ModernSidebar(QWidget):
|
||||||
page_changed = pyqtSignal(str)
|
page_changed = pyqtSignal(str)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue