Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from dataclasses import dataclass, field
2from datetime import timedelta
3from enum import Enum
4from typing import Any, Callable, Dict, Optional, Set, Tuple, Type
6from ..adapters import AlbumSearchQuery
7from ..adapters.api_objects import Genre, Song
8from ..util import this_decade
11class RepeatType(Enum):
12 NO_REPEAT = 0
13 REPEAT_QUEUE = 1
14 REPEAT_SONG = 2
16 @property
17 def icon(self) -> str:
18 """
19 Get the icon for the repeat type.
21 >>> RepeatType.NO_REPEAT.icon, RepeatType.REPEAT_QUEUE.icon
22 ('media-playlist-repeat-symbolic', 'media-playlist-repeat-symbolic')
23 >>> RepeatType.REPEAT_SONG.icon
24 'media-playlist-repeat-song-symbolic'
25 """
26 song_str = "-song" if self == RepeatType.REPEAT_SONG else ""
27 return f"media-playlist-repeat{song_str}-symbolic"
29 def as_mpris_loop_status(self) -> str:
30 return ["None", "Playlist", "Track"][self.value]
32 @staticmethod
33 def from_mpris_loop_status(loop_status: str) -> "RepeatType":
34 return {
35 "None": RepeatType.NO_REPEAT,
36 "Track": RepeatType.REPEAT_SONG,
37 "Playlist": RepeatType.REPEAT_QUEUE,
38 }[loop_status]
41@dataclass
42class UIState:
43 """Represents the UI state of the application."""
45 @dataclass(unsafe_hash=True)
46 class UINotification:
47 markup: str
48 actions: Tuple[Tuple[str, Callable[[], None]], ...] = field(
49 default_factory=tuple
50 )
51 icon: Optional[str] = None
53 version: int = 1
55 # Play state
56 playing: bool = False
57 current_song_index: int = -1
58 play_queue: Tuple[str, ...] = field(default_factory=tuple)
59 old_play_queue: Tuple[str, ...] = field(default_factory=tuple)
60 _volume: Dict[str, float] = field(default_factory=lambda: {"this device": 100.0})
61 is_muted: bool = False
62 repeat_type: RepeatType = RepeatType.NO_REPEAT
63 shuffle_on: bool = False
64 song_progress: timedelta = timedelta()
65 song_stream_cache_progress: Optional[timedelta] = timedelta()
66 current_device: str = "this device"
67 connecting_to_device: bool = False
68 connected_device_name: Optional[str] = None
69 available_players: Dict[Type, Set[Tuple[str, str]]] = field(default_factory=dict)
71 # UI state
72 current_tab: str = "albums"
73 selected_album_id: Optional[str] = None
74 selected_artist_id: Optional[str] = None
75 selected_browse_element_id: Optional[str] = None
76 selected_playlist_id: Optional[str] = None
77 album_sort_direction: str = "ascending"
78 album_page_size: int = 30
79 album_page: int = 0
80 current_notification: Optional[UINotification] = None
81 playlist_details_expanded: bool = True
82 artist_details_expanded: bool = True
83 loading_play_queue: bool = False
85 # State for Album sort.
86 class _DefaultGenre(Genre):
87 def __init__(self):
88 self.name = "Rock"
90 current_album_search_query: AlbumSearchQuery = AlbumSearchQuery(
91 AlbumSearchQuery.Type.RANDOM,
92 genre=_DefaultGenre(),
93 year_range=this_decade(),
94 )
96 active_playlist_id: Optional[str] = None
98 def __getstate__(self):
99 state = self.__dict__.copy()
100 del state["song_stream_cache_progress"]
101 del state["current_notification"]
102 del state["playing"]
103 del state["available_players"]
104 return state
106 def __setstate__(self, state: Dict[str, Any]):
107 self.__dict__.update(state)
108 self.song_stream_cache_progress = None
109 self.current_notification = None
110 self.playing = False
112 def __init_available_players__(self):
113 from sublime_music.players import PlayerManager
115 self.available_players = {
116 pt: set() for pt in PlayerManager.available_player_types
117 }
119 def migrate(self):
120 pass
122 _current_song: Optional[Song] = None
124 @property
125 def current_song(self) -> Optional[Song]:
126 if not self.play_queue or self.current_song_index < 0:
127 return None
129 from sublime_music.adapters import AdapterManager
131 current_song_id = self.play_queue[self.current_song_index]
133 if not self._current_song or self._current_song.id != current_song_id:
134 self._current_song = AdapterManager.get_song_details(
135 current_song_id
136 ).result()
138 return self._current_song
140 @property
141 def next_song_index(self) -> Optional[int]:
142 # If nothing is playing there is no next song
143 if self.current_song_index < 0:
144 return None
146 if self.repeat_type == RepeatType.REPEAT_SONG:
147 return self.current_song_index
149 # If we are at the end of the play queue
150 if self.current_song_index == len(self.play_queue) - 1:
152 # If we are repeating the queue, jump back to the beginning
153 if self.repeat_type == RepeatType.REPEAT_QUEUE:
154 return 0
156 # Otherwise, there isn't a next song
157 return None
159 # In all other cases, it's the song after the current one
160 return self.current_song_index + 1
162 @property
163 def volume(self) -> float:
164 return self._volume.get(self.current_device, 100.0)
166 @volume.setter
167 def volume(self, value: float):
168 self._volume[self.current_device] = value