toast
This commit is contained in:
parent
3b0adc3e4f
commit
091f3b3833
13 changed files with 338 additions and 1 deletions
11
main.py
11
main.py
|
|
@ -20,6 +20,7 @@ from ui.pages.sync import SyncPage
|
||||||
from ui.pages.downloads import DownloadsPage
|
from ui.pages.downloads import DownloadsPage
|
||||||
from ui.pages.artists import ArtistsPage
|
from ui.pages.artists import ArtistsPage
|
||||||
from ui.pages.settings import SettingsPage
|
from ui.pages.settings import SettingsPage
|
||||||
|
from ui.components.toast_manager import ToastManager
|
||||||
|
|
||||||
logger = get_logger("main")
|
logger = get_logger("main")
|
||||||
|
|
||||||
|
|
@ -154,6 +155,9 @@ class MainWindow(QMainWindow):
|
||||||
# Create stacked widget for pages
|
# Create stacked widget for pages
|
||||||
self.stacked_widget = QStackedWidget()
|
self.stacked_widget = QStackedWidget()
|
||||||
|
|
||||||
|
# Create toast manager
|
||||||
|
self.toast_manager = ToastManager(self)
|
||||||
|
|
||||||
# Create and add pages
|
# Create and add pages
|
||||||
self.dashboard_page = DashboardPage()
|
self.dashboard_page = DashboardPage()
|
||||||
self.downloads_page = DownloadsPage(self.soulseek_client)
|
self.downloads_page = DownloadsPage(self.soulseek_client)
|
||||||
|
|
@ -161,10 +165,17 @@ class MainWindow(QMainWindow):
|
||||||
self.artists_page = ArtistsPage(downloads_page=self.downloads_page)
|
self.artists_page = ArtistsPage(downloads_page=self.downloads_page)
|
||||||
self.settings_page = SettingsPage()
|
self.settings_page = SettingsPage()
|
||||||
|
|
||||||
|
# Set toast manager for pages that need direct access
|
||||||
|
self.downloads_page.set_toast_manager(self.toast_manager)
|
||||||
|
self.sync_page.set_toast_manager(self.toast_manager)
|
||||||
|
self.artists_page.set_toast_manager(self.toast_manager)
|
||||||
|
self.settings_page.set_toast_manager(self.toast_manager)
|
||||||
|
|
||||||
# Configure dashboard with service clients and page references
|
# Configure dashboard with service clients and page references
|
||||||
self.dashboard_page.set_service_clients(self.spotify_client, self.plex_client, self.soulseek_client)
|
self.dashboard_page.set_service_clients(self.spotify_client, self.plex_client, self.soulseek_client)
|
||||||
self.dashboard_page.set_page_references(self.downloads_page, self.sync_page)
|
self.dashboard_page.set_page_references(self.downloads_page, self.sync_page)
|
||||||
self.dashboard_page.set_app_start_time(self.app_start_time)
|
self.dashboard_page.set_app_start_time(self.app_start_time)
|
||||||
|
self.dashboard_page.set_toast_manager(self.toast_manager)
|
||||||
|
|
||||||
# Connect download completion signal for session tracking
|
# Connect download completion signal for session tracking
|
||||||
self.downloads_page.download_session_completed.connect(
|
self.downloads_page.download_session_completed.connect(
|
||||||
|
|
|
||||||
BIN
ui/components/__pycache__/toast_manager.cpython-312.pyc
Normal file
BIN
ui/components/__pycache__/toast_manager.cpython-312.pyc
Normal file
Binary file not shown.
260
ui/components/toast_manager.py
Normal file
260
ui/components/toast_manager.py
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
from PyQt6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QGraphicsOpacityEffect
|
||||||
|
from PyQt6.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve, pyqtSignal, QRect
|
||||||
|
from PyQt6.QtGui import QFont, QPainter, QPaintEvent
|
||||||
|
import time
|
||||||
|
from typing import List, Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class ToastType(Enum):
|
||||||
|
SUCCESS = "success"
|
||||||
|
INFO = "info"
|
||||||
|
WARNING = "warning"
|
||||||
|
ERROR = "error"
|
||||||
|
|
||||||
|
class Toast(QWidget):
|
||||||
|
"""Individual toast notification widget"""
|
||||||
|
closed = pyqtSignal(object) # Emits self when closing
|
||||||
|
|
||||||
|
def __init__(self, message: str, toast_type: ToastType = ToastType.INFO, duration: int = 4000, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.message = message
|
||||||
|
self.toast_type = toast_type
|
||||||
|
self.duration = duration
|
||||||
|
self.created_time = time.time()
|
||||||
|
|
||||||
|
self.setup_ui()
|
||||||
|
self.setup_animations()
|
||||||
|
self.setup_auto_dismiss()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup the toast UI"""
|
||||||
|
self.setFixedHeight(60)
|
||||||
|
self.setMinimumWidth(300)
|
||||||
|
self.setMaximumWidth(400)
|
||||||
|
|
||||||
|
# Make the widget click-through for the background but clickable for the content
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
|
||||||
|
|
||||||
|
# Main layout
|
||||||
|
layout = QHBoxLayout(self)
|
||||||
|
layout.setContentsMargins(12, 12, 12, 12)
|
||||||
|
layout.setSpacing(12)
|
||||||
|
|
||||||
|
# Icon label
|
||||||
|
self.icon_label = QLabel()
|
||||||
|
self.icon_label.setFont(QFont("Segoe UI", 14))
|
||||||
|
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.icon_label.setFixedSize(24, 24)
|
||||||
|
|
||||||
|
# Message label
|
||||||
|
self.message_label = QLabel(self.message)
|
||||||
|
self.message_label.setFont(QFont("Segoe UI", 10))
|
||||||
|
self.message_label.setWordWrap(True)
|
||||||
|
self.message_label.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
|
||||||
|
layout.addWidget(self.icon_label)
|
||||||
|
layout.addWidget(self.message_label, 1)
|
||||||
|
|
||||||
|
# Apply styling based on toast type
|
||||||
|
self.apply_styling()
|
||||||
|
|
||||||
|
def apply_styling(self):
|
||||||
|
"""Apply styling based on toast type"""
|
||||||
|
if self.toast_type == ToastType.SUCCESS:
|
||||||
|
icon = "✅"
|
||||||
|
accent_color = "#1db954" # Spotify green
|
||||||
|
bg_color = "rgba(29, 185, 84, 0.15)"
|
||||||
|
border_color = "rgba(29, 185, 84, 0.3)"
|
||||||
|
elif self.toast_type == ToastType.ERROR:
|
||||||
|
icon = "❌"
|
||||||
|
accent_color = "#f04747"
|
||||||
|
bg_color = "rgba(240, 71, 71, 0.15)"
|
||||||
|
border_color = "rgba(240, 71, 71, 0.3)"
|
||||||
|
elif self.toast_type == ToastType.WARNING:
|
||||||
|
icon = "⚠️"
|
||||||
|
accent_color = "#ffa500"
|
||||||
|
bg_color = "rgba(255, 165, 0, 0.15)"
|
||||||
|
border_color = "rgba(255, 165, 0, 0.3)"
|
||||||
|
else: # INFO
|
||||||
|
icon = "ℹ️"
|
||||||
|
accent_color = "#5865f2"
|
||||||
|
bg_color = "rgba(88, 101, 242, 0.15)"
|
||||||
|
border_color = "rgba(88, 101, 242, 0.3)"
|
||||||
|
|
||||||
|
self.icon_label.setText(icon)
|
||||||
|
|
||||||
|
self.setStyleSheet(f"""
|
||||||
|
Toast {{
|
||||||
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 rgba(45, 45, 45, 0.95),
|
||||||
|
stop:1 rgba(35, 35, 35, 0.95));
|
||||||
|
border: 1px solid {border_color};
|
||||||
|
border-left: 3px solid {accent_color};
|
||||||
|
border-radius: 8px;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.message_label.setStyleSheet(f"""
|
||||||
|
color: #ffffff;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
def setup_animations(self):
|
||||||
|
"""Setup slide-in and fade-out animations"""
|
||||||
|
# Opacity effect for fade animations
|
||||||
|
self.opacity_effect = QGraphicsOpacityEffect()
|
||||||
|
self.setGraphicsEffect(self.opacity_effect)
|
||||||
|
|
||||||
|
# Slide-in animation (from right)
|
||||||
|
self.slide_animation = QPropertyAnimation(self, b"geometry")
|
||||||
|
self.slide_animation.setDuration(300)
|
||||||
|
self.slide_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||||
|
|
||||||
|
# Fade-out animation
|
||||||
|
self.fade_animation = QPropertyAnimation(self.opacity_effect, b"opacity")
|
||||||
|
self.fade_animation.setDuration(200)
|
||||||
|
self.fade_animation.setEasingCurve(QEasingCurve.Type.OutQuad)
|
||||||
|
|
||||||
|
# Connect fade animation to close
|
||||||
|
self.fade_animation.finished.connect(self._on_fade_complete)
|
||||||
|
|
||||||
|
def setup_auto_dismiss(self):
|
||||||
|
"""Setup auto-dismiss timer"""
|
||||||
|
if self.duration > 0:
|
||||||
|
self.dismiss_timer = QTimer()
|
||||||
|
self.dismiss_timer.setSingleShot(True)
|
||||||
|
self.dismiss_timer.timeout.connect(self.dismiss)
|
||||||
|
self.dismiss_timer.start(self.duration)
|
||||||
|
|
||||||
|
def show_at_position(self, target_rect: QRect):
|
||||||
|
"""Show the toast with slide-in animation at the specified position"""
|
||||||
|
# Start position (off-screen to the right)
|
||||||
|
start_rect = QRect(target_rect.x() + 50, target_rect.y(), target_rect.width(), target_rect.height())
|
||||||
|
|
||||||
|
# Set initial position and show
|
||||||
|
self.setGeometry(start_rect)
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
# Animate to target position
|
||||||
|
self.slide_animation.setStartValue(start_rect)
|
||||||
|
self.slide_animation.setEndValue(target_rect)
|
||||||
|
self.slide_animation.start()
|
||||||
|
|
||||||
|
def dismiss(self):
|
||||||
|
"""Dismiss the toast with fade-out animation"""
|
||||||
|
if hasattr(self, 'dismiss_timer'):
|
||||||
|
self.dismiss_timer.stop()
|
||||||
|
|
||||||
|
self.fade_animation.setStartValue(1.0)
|
||||||
|
self.fade_animation.setEndValue(0.0)
|
||||||
|
self.fade_animation.start()
|
||||||
|
|
||||||
|
def _on_fade_complete(self):
|
||||||
|
"""Called when fade animation completes"""
|
||||||
|
self.closed.emit(self)
|
||||||
|
self.hide()
|
||||||
|
self.deleteLater()
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
"""Handle click to dismiss"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.dismiss()
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
|
class ToastManager(QWidget):
|
||||||
|
"""Manages multiple toast notifications"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent_widget = parent
|
||||||
|
self.active_toasts: List[Toast] = []
|
||||||
|
self.toast_spacing = 10
|
||||||
|
self.margin_from_edge = 20
|
||||||
|
|
||||||
|
# Make this widget transparent and non-interactive
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
def show_toast(self, message: str, toast_type: ToastType = ToastType.INFO, duration: int = 4000):
|
||||||
|
"""Show a new toast notification"""
|
||||||
|
toast = Toast(message, toast_type, duration, self.parent_widget)
|
||||||
|
toast.closed.connect(self._on_toast_closed)
|
||||||
|
|
||||||
|
# Calculate position for this toast
|
||||||
|
position = self._calculate_toast_position(len(self.active_toasts))
|
||||||
|
|
||||||
|
# Add to active toasts list
|
||||||
|
self.active_toasts.append(toast)
|
||||||
|
|
||||||
|
# Show the toast
|
||||||
|
toast.show_at_position(position)
|
||||||
|
|
||||||
|
# Reposition existing toasts if needed
|
||||||
|
self._reposition_existing_toasts()
|
||||||
|
|
||||||
|
def _calculate_toast_position(self, index: int) -> QRect:
|
||||||
|
"""Calculate position for a toast at the given index"""
|
||||||
|
if not self.parent_widget:
|
||||||
|
return QRect(0, 0, 350, 60)
|
||||||
|
|
||||||
|
parent_rect = self.parent_widget.rect()
|
||||||
|
toast_height = 60
|
||||||
|
toast_width = 350
|
||||||
|
|
||||||
|
x = parent_rect.width() - toast_width - self.margin_from_edge
|
||||||
|
y = self.margin_from_edge + (index * (toast_height + self.toast_spacing))
|
||||||
|
|
||||||
|
return QRect(x, y, toast_width, toast_height)
|
||||||
|
|
||||||
|
def _reposition_existing_toasts(self):
|
||||||
|
"""Reposition existing toasts to make room for new ones"""
|
||||||
|
for i, toast in enumerate(self.active_toasts[:-1]): # Exclude the newest toast
|
||||||
|
new_position = self._calculate_toast_position(i)
|
||||||
|
|
||||||
|
# Animate to new position if needed
|
||||||
|
current_geo = toast.geometry()
|
||||||
|
if current_geo.y() != new_position.y():
|
||||||
|
toast.slide_animation.stop()
|
||||||
|
toast.slide_animation.setStartValue(current_geo)
|
||||||
|
toast.slide_animation.setEndValue(new_position)
|
||||||
|
toast.slide_animation.start()
|
||||||
|
|
||||||
|
def _on_toast_closed(self, toast: Toast):
|
||||||
|
"""Handle when a toast is closed"""
|
||||||
|
if toast in self.active_toasts:
|
||||||
|
self.active_toasts.remove(toast)
|
||||||
|
|
||||||
|
# Reposition remaining toasts
|
||||||
|
for i, remaining_toast in enumerate(self.active_toasts):
|
||||||
|
new_position = self._calculate_toast_position(i)
|
||||||
|
current_geo = remaining_toast.geometry()
|
||||||
|
|
||||||
|
if current_geo != new_position:
|
||||||
|
remaining_toast.slide_animation.stop()
|
||||||
|
remaining_toast.slide_animation.setStartValue(current_geo)
|
||||||
|
remaining_toast.slide_animation.setEndValue(new_position)
|
||||||
|
remaining_toast.slide_animation.start()
|
||||||
|
|
||||||
|
def clear_all_toasts(self):
|
||||||
|
"""Dismiss all active toasts"""
|
||||||
|
for toast in self.active_toasts.copy():
|
||||||
|
toast.dismiss()
|
||||||
|
|
||||||
|
# Convenience methods for different toast types
|
||||||
|
def success(self, message: str, duration: int = 4000):
|
||||||
|
"""Show a success toast"""
|
||||||
|
self.show_toast(message, ToastType.SUCCESS, duration)
|
||||||
|
|
||||||
|
def error(self, message: str, duration: int = 6000):
|
||||||
|
"""Show an error toast (longer duration)"""
|
||||||
|
self.show_toast(message, ToastType.ERROR, duration)
|
||||||
|
|
||||||
|
def warning(self, message: str, duration: int = 5000):
|
||||||
|
"""Show a warning toast"""
|
||||||
|
self.show_toast(message, ToastType.WARNING, duration)
|
||||||
|
|
||||||
|
def info(self, message: str, duration: int = 4000):
|
||||||
|
"""Show an info toast"""
|
||||||
|
self.show_toast(message, ToastType.INFO, duration)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -2378,6 +2378,10 @@ class ArtistsPage(QWidget):
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.setup_clients()
|
self.setup_clients()
|
||||||
|
|
||||||
|
def set_toast_manager(self, toast_manager):
|
||||||
|
"""Set the toast manager for showing notifications"""
|
||||||
|
self.toast_manager = toast_manager
|
||||||
|
|
||||||
def setup_clients(self):
|
def setup_clients(self):
|
||||||
"""Initialize client connections"""
|
"""Initialize client connections"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -3004,6 +3008,10 @@ class ArtistsPage(QWidget):
|
||||||
existing_session = self.active_album_sessions[album.id]
|
existing_session = self.active_album_sessions[album.id]
|
||||||
existing_modal = existing_session.get('modal')
|
existing_modal = existing_session.get('modal')
|
||||||
|
|
||||||
|
# Show toast notification for already active session
|
||||||
|
if hasattr(self, 'toast_manager') and self.toast_manager:
|
||||||
|
self.toast_manager.info(f"Downloads already in progress for '{album.name}'")
|
||||||
|
|
||||||
# Check if the modal still exists and is valid
|
# Check if the modal still exists and is valid
|
||||||
if existing_modal and not existing_modal.isVisible():
|
if existing_modal and not existing_modal.isVisible():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1206,6 +1206,10 @@ class DashboardPage(QWidget):
|
||||||
"""Called from main window to provide app start time for uptime calculation"""
|
"""Called from main window to provide app start time for uptime calculation"""
|
||||||
self.data_provider.set_app_start_time(start_time)
|
self.data_provider.set_app_start_time(start_time)
|
||||||
|
|
||||||
|
def set_toast_manager(self, toast_manager):
|
||||||
|
"""Set the toast manager for showing notifications"""
|
||||||
|
self.toast_manager = toast_manager
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
DashboardPage {
|
DashboardPage {
|
||||||
|
|
@ -1615,7 +1619,11 @@ class DashboardPage(QWidget):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_activity_item(self, icon: str, title: str, subtitle: str, time_ago: str = "Now"):
|
def add_activity_item(self, icon: str, title: str, subtitle: str, time_ago: str = "Now"):
|
||||||
"""Add new activity item to the feed"""
|
"""Add new activity item to the feed and potentially show a toast"""
|
||||||
|
# Show toast for immediate user actions (if toast manager is available)
|
||||||
|
if hasattr(self, 'toast_manager') and self.toast_manager:
|
||||||
|
self._maybe_show_toast(icon, title, subtitle)
|
||||||
|
|
||||||
# Remove placeholder if it exists
|
# Remove placeholder if it exists
|
||||||
if self.has_placeholder:
|
if self.has_placeholder:
|
||||||
# Clear the entire layout
|
# Clear the entire layout
|
||||||
|
|
@ -1642,6 +1650,44 @@ class DashboardPage(QWidget):
|
||||||
if item.widget():
|
if item.widget():
|
||||||
item.widget().deleteLater()
|
item.widget().deleteLater()
|
||||||
|
|
||||||
|
def _maybe_show_toast(self, icon: str, title: str, subtitle: str):
|
||||||
|
"""Determine if this activity should show a toast notification"""
|
||||||
|
from ui.components.toast_manager import ToastType
|
||||||
|
|
||||||
|
# Success activities that deserve toasts
|
||||||
|
if icon == "✅" and any(keyword in title.lower() for keyword in ["download started", "sync completed", "complete"]):
|
||||||
|
self.toast_manager.success(f"{title}: {subtitle}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if icon == "📥" and "Download Started" in title:
|
||||||
|
self.toast_manager.success(f"{subtitle}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if icon == "🔍" and "Search Complete" in title:
|
||||||
|
self.toast_manager.info(f"{subtitle}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Error activities that need immediate attention
|
||||||
|
if icon == "❌":
|
||||||
|
# Skip routine background errors
|
||||||
|
if any(skip_term in title.lower() for skip_term in ["metadata", "connection test", "routine"]):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show errors for user-initiated actions
|
||||||
|
if any(keyword in title.lower() for keyword in ["download failed", "sync failed", "search failed"]):
|
||||||
|
self.toast_manager.error(f"{title}: {subtitle}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Warning activities
|
||||||
|
if icon == "⚠️":
|
||||||
|
self.toast_manager.warning(f"{title}: {subtitle}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Info activities for searches and connections
|
||||||
|
if icon == "🔍" and "Search Started" in title:
|
||||||
|
self.toast_manager.info(f"{subtitle}")
|
||||||
|
return
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Clean up threads when dashboard is closed"""
|
"""Clean up threads when dashboard is closed"""
|
||||||
self.cleanup_threads()
|
self.cleanup_threads()
|
||||||
|
|
|
||||||
|
|
@ -4923,6 +4923,10 @@ class DownloadsPage(QWidget):
|
||||||
|
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
|
def set_toast_manager(self, toast_manager):
|
||||||
|
"""Set the toast manager for showing notifications"""
|
||||||
|
self.toast_manager = toast_manager
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,10 @@ class SettingsPage(QWidget):
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.load_config_values()
|
self.load_config_values()
|
||||||
|
|
||||||
|
def set_toast_manager(self, toast_manager):
|
||||||
|
"""Set the toast manager for showing notifications"""
|
||||||
|
self.toast_manager = toast_manager
|
||||||
|
|
||||||
def on_test_completed(self, service, success, message):
|
def on_test_completed(self, service, success, message):
|
||||||
"""Handle test completion from background thread"""
|
"""Handle test completion from background thread"""
|
||||||
# Re-enable the test button
|
# Re-enable the test button
|
||||||
|
|
|
||||||
|
|
@ -1973,6 +1973,10 @@ class SyncPage(QWidget):
|
||||||
self.show_initial_state()
|
self.show_initial_state()
|
||||||
self.playlists_loaded = False
|
self.playlists_loaded = False
|
||||||
|
|
||||||
|
def set_toast_manager(self, toast_manager):
|
||||||
|
"""Set the toast manager for showing notifications"""
|
||||||
|
self.toast_manager = toast_manager
|
||||||
|
|
||||||
|
|
||||||
def _update_and_save_sync_status(self, playlist_id, result, snapshot_id):
|
def _update_and_save_sync_status(self, playlist_id, result, snapshot_id):
|
||||||
"""Updates the sync status for a given playlist and saves to file."""
|
"""Updates the sync status for a given playlist and saves to file."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue