diff --git a/app.py b/app.py index fcfc654..df5863d 100644 --- a/app.py +++ b/app.py @@ -34,9 +34,9 @@ from textual import on, events, work from textual.binding import Binding from textual.reactive import reactive from textual.app import App, ComposeResult, RenderResult -from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label, Input, Placeholder, Static, Rule +from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label, Input, Placeholder, Static, Rule, Button from textual.widget import Widget -from textual.containers import Horizontal, Vertical, Container +from textual.containers import Horizontal, Vertical, Container, Center from textual.screen import Screen from textual.widgets.option_list import Option from textual.timer import Timer @@ -959,6 +959,99 @@ class MainView(Screen): queue_table.update_queue() app.update_now_playing() +class CurrentPlace(Static): + place: reactive[int] = reactive(0) + + def render(self) -> RenderResult: + # if app.currently_playing: + # duration_min, duration_sec = divmod(app.currently_playing.duration, 60) + # duration_formatted = f"{duration_min:02d}:{duration_sec:02d}" + + place_in_seconds = self.place // 1000 + place_min, place_sec = divmod(place_in_seconds, 60) + place_formatted = f"{place_min:02d}:{place_sec:02d}" + + return place_formatted + +class TotalDuration(Static): + duration: reactive[int] = reactive(0) + + def render(self) -> RenderResult: + duration_min, duration_sec = divmod(self.duration, 60) + duration_formatted = f"{duration_min:02d}:{duration_sec:02d}" + + return duration_formatted + +class Player(Screen): + CSS_PATH = 'main.tcss' + + def update_song(self, song: SubSonic.Song) -> None: + # info = self.query_one(NowPlayingInfo) + # info.song = song + + if song.albumId: + track_title_label: Label = self.query_one('#trackTitle', Label) + track_artist_album_label: Label = self.query_one('#trackAlbumArtist', Label) + + track_title_label.update(song.title) + track_artist_album_label.update(f'{song.album} by {song.artist}') + + cover_art = self.query_one(CoverArtPixels) + + album = conn.getAlbumSongs(song.albumId) + if album: + cover_id: str | None = album[0].coverArtId + if cover_id: + cover_art.get_cover_art(cover_id) + + def compose(self) -> ComposeResult: + yield Header(icon="") + with Horizontal(id="headerbs"): + yield Label(Text("* F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb") + yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb") + yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb") + yield Label(Text("F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb") + yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb") + + with Center(id='playerTrackTitleBox'): + yield Label(id='trackTitle', classes='playerTrackinfo') + with Center(): + yield Label(id='trackAlbumArtist', classes='playerTrackinfo') + with Container(id='playerArtc'): + yield CoverArtPixels(classes='playerArt') + + with Center(): + with Horizontal(id='progressBarWrapper'): + yield CurrentPlace(classes='playerProgressBarTime') + yield ProgressBar(classes='playerProgressBar') + yield TotalDuration(classes='playerProgressBarTotalTime') + + with Horizontal(id='playerButtons'): + yield Button('|<', id='prev_track', classes='playerButton') + yield Button('|>/||', id='play_pause_track', classes='playerButton') + yield Button('>|', id='next_track', classes='playerButton') + + yield Footer() + + @on(events.ScreenResume) + def screen_resumed(self, event: events.ScreenResume) -> None: + app.sub_title = "Overview" + # queue_table = self.query_one(QueueTable) + # queue_table.update_queue() + app.update_now_playing() + + @on(Button.Pressed, "#prev_track") + def prev_track_pressed(self) -> None: + app.action_rewind() + + @on(Button.Pressed, "#play_pause_track") + def play_pause_track_pressed(self) -> None: + app.action_play_pause_song() + + @on(Button.Pressed, "#next_track") + def next_track_pressed(self) -> None: + app.action_skip() + # Startup keyboard listener for media keys def listen_media_keys(queue: multiprocessing.Queue): @@ -998,7 +1091,7 @@ def listen_media_keys(queue: multiprocessing.Queue): pass logger.error(f'Media key listener failed: {e}') -class TextualSubpyPlayer(App): +class SubPy(App): BINDINGS = [ ("r", "rewind()", "⏪"), ("space", "play_pause_song()", "⏯"), @@ -1014,7 +1107,7 @@ class TextualSubpyPlayer(App): ] MODES = { - "main" : MainView, + "main" : Player, "artists" : ArtistsGUI, "albums" : Albums, "favourites" : Favourites, @@ -1081,12 +1174,6 @@ class TextualSubpyPlayer(App): self.update_queue_table() self.update_now_playing() - # playingLabel: Label = self.query_one(Label) - # playingLabel. - - # Set callback loop for progress bar - # self.progress_timer = self.set_interval(1 / 10, self.update_progress, pause=False) - def _music_player_loop(self) -> None: if self.music_command_queue.empty(): return @@ -1160,9 +1247,22 @@ class TextualSubpyPlayer(App): self.update_queue_table() def update_progress(self) -> None: - if isinstance(self.screen, MainView) and self.currently_playing: - now_playing = self.screen.query_one(NowPlaying) - now_playing.set_place(pygame.mixer.music.get_pos()) + if isinstance(self.screen, Player) and self.currently_playing: + current_place_label = self.screen.query_one(CurrentPlace) + place = pygame.mixer.music.get_pos() + current_place_label.place = place + + duration_label = self.screen.query_one(TotalDuration) + duration_label.duration = self.currently_playing.duration + + progressbar: ProgressBar = self.screen.query_one(ProgressBar) + + progress: int = round(place/1000) + + progressbar.update( + total = self.currently_playing.duration, + progress=progress + ) def queue_song(self, song: SubSonic.Song) -> None: logging.debug(f'Queue\'d song "{song.title}" by {song.artist}.') @@ -1193,10 +1293,9 @@ class TextualSubpyPlayer(App): self.is_paused = False def update_now_playing(self) -> None: - if isinstance(self.screen, MainView) and self.currently_playing: - logging.debug("Updating now playing widget") - now_playing = self.screen.query_one(NowPlaying) - now_playing.update_song(self.currently_playing) + if isinstance(self.screen, Player) and self.currently_playing: + logging.debug("Updating CoverArtPixels widget") + self.screen.update_song(self.currently_playing) def update_queue_table(self) -> None: if isinstance(self.screen, MainView): @@ -1236,7 +1335,7 @@ if __name__ == '__main__': p = multiprocessing.Process(target=listen_media_keys, args=(q,)) p.start() - app = TextualSubpyPlayer() + app = SubPy() app.music_command_queue = q app.run() diff --git a/main.tcss b/main.tcss index dc79ffd..ef9149e 100644 --- a/main.tcss +++ b/main.tcss @@ -87,3 +87,73 @@ width: 100%; text-align: center; } + +# Player Screen + +#playerTrackTitleBox { + padding: 1 0 0 0; +} + +#playerArtc { + align: center middle; + # width: 1fr; +} + +.playerTrackinfo { + # text-align: center; + # content-align: center middle; + height: 1; + width: auto; +} + +.playerArt { + content-align: center middle; + height: 1fr; + width: 0.5fr; + padding: 1 0 0 0; +} + +#progressBarWrapper { + height: 3; + width: 0.75fr; + padding: 1 0 0 0; +} + +.playerProgressBarTime { + width: 0.1fr; + text-align: center; +} + +.playerProgressBarTotalTime { + width: 0.1fr; + text-align: center; +} + +#percentage { + visibility: hidden; +} + +#eta { + visibility: hidden; +} + +.playerProgressBar { + height: 3; + width: 0.75fr; + content-align: center middle; +} + +.playerProgressBar #bar { + width: auto; +} + +#playerButtons { + height: 0.25fr; + padding-bottom: 1; + align: center middle; +} + +.playerButton { + width: 10%; +} +