mirror of
https://git.srhtnin.garden/sarah/SaraTune.git
synced 2026-10-13 18:18:11 +02:00
147 lines
6.1 KiB
Python
147 lines
6.1 KiB
Python
"""The play bar at the bottom of both modes: what's playing, previous / play /
|
|
next, the seek bar and volume. Each mode adds its own toggles (loop, shuffle,
|
|
repeat) through Extras."""
|
|
|
|
import asyncio
|
|
from typing import Awaitable, Callable, Optional
|
|
|
|
import flet as ft
|
|
|
|
from Core import FormatTime, Palette
|
|
from Player import Player
|
|
|
|
CoverSize = 52
|
|
|
|
|
|
def CoverImage(Data: Optional[bytes], Size: int, Radius: int = 4) -> ft.Control:
|
|
"""A cover, or a pixel-style note on the theme's colours when there's none."""
|
|
if Data:
|
|
return ft.Image(src=Data, width=Size, height=Size, fit=ft.BoxFit.COVER,
|
|
border_radius=Radius, filter_quality=ft.FilterQuality.MEDIUM, gapless_playback=True)
|
|
return ft.Container(
|
|
ft.Icon(ft.Icons.MUSIC_NOTE, color=Palette.Mauve, size=Size * 0.5),
|
|
width=Size, height=Size, bgcolor=Palette.Surface0, border_radius=Radius,
|
|
border=ft.Border.all(1, Palette.Surface1), alignment=ft.Alignment.CENTER,
|
|
)
|
|
|
|
|
|
class Transport(ft.Container):
|
|
def __init__(self, ThePlayer: Player, OnPrevious: Callable[[], Awaitable[None]],
|
|
OnNext: Callable[[], Awaitable[None]], OnVolume: Callable[[float], None],
|
|
Extras: Optional[list[ft.Control]] = None, OnInfoClick: Optional[Callable] = None):
|
|
super().__init__(
|
|
bgcolor=Palette.Mantle,
|
|
border=ft.Border.only(top=ft.BorderSide(1, Palette.Surface1)),
|
|
padding=ft.Padding.symmetric(horizontal=12, vertical=8),
|
|
)
|
|
self.ThePlayer = ThePlayer
|
|
self._Dragging = False
|
|
self.Shown = False # set by Main: only the shown mode's bar is on the page
|
|
|
|
self.Cover = ft.Container(CoverImage(None, CoverSize))
|
|
self.Title = ft.Text("Nothing playing", color=Palette.Text, size=15, max_lines=1,
|
|
overflow=ft.TextOverflow.ELLIPSIS)
|
|
self.Subtitle = ft.Text("", color=Palette.Overlay0, size=12, max_lines=1, overflow=ft.TextOverflow.ELLIPSIS)
|
|
Info = ft.Container(
|
|
ft.Row([self.Cover, ft.Column([self.Title, self.Subtitle], spacing=2, expand=True, tight=True)],
|
|
spacing=10),
|
|
width=280, on_click=OnInfoClick, ink=OnInfoClick is not None, border_radius=4,
|
|
)
|
|
|
|
async def Previous(e) -> None:
|
|
await OnPrevious()
|
|
|
|
async def Next(e) -> None:
|
|
await OnNext()
|
|
|
|
async def Toggle(e) -> None:
|
|
await ThePlayer.Toggle()
|
|
|
|
self.PlayButton = ft.IconButton(
|
|
ft.Icons.PLAY_ARROW_ROUNDED, icon_size=34, icon_color=Palette.Crust, bgcolor=Palette.Mauve,
|
|
tooltip="Play / pause (space)", on_click=Toggle,
|
|
)
|
|
Buttons = ft.Row([
|
|
ft.IconButton(ft.Icons.SKIP_PREVIOUS_ROUNDED, icon_color=Palette.Text, tooltip="Previous", on_click=Previous),
|
|
self.PlayButton,
|
|
ft.IconButton(ft.Icons.SKIP_NEXT_ROUNDED, icon_color=Palette.Text, tooltip="Next", on_click=Next),
|
|
], spacing=4)
|
|
|
|
self.Elapsed = ft.Text("0:00", color=Palette.Overlay0, size=12, width=52, text_align=ft.TextAlign.RIGHT)
|
|
self.Total = ft.Text("0:00", color=Palette.Overlay0, size=12, width=52)
|
|
self.Seek = ft.Slider(
|
|
min=0, max=1, value=0, expand=True, active_color=Palette.Mauve, inactive_color=Palette.Surface1,
|
|
thumb_color=Palette.Mauve, on_change_start=self._DragStart, on_change=self._Dragged,
|
|
on_change_end=self._DragEnd,
|
|
)
|
|
|
|
def VolumeChanged(e) -> None:
|
|
OnVolume(e.control.value)
|
|
self.VolumeIcon.icon = self._VolumeIconFor(e.control.value)
|
|
self.VolumeIcon.update()
|
|
|
|
self.VolumeIcon = ft.Icon(self._VolumeIconFor(ThePlayer.Volume), color=Palette.Overlay0, size=20)
|
|
self.Volume = ft.Slider(min=0, max=1, value=ThePlayer.Volume, width=120, active_color=Palette.Mauve,
|
|
inactive_color=Palette.Surface1, thumb_color=Palette.Mauve, on_change=VolumeChanged)
|
|
|
|
self.content = ft.Row([
|
|
Info,
|
|
Buttons,
|
|
self.Elapsed, self.Seek, self.Total,
|
|
*(Extras or []),
|
|
self.VolumeIcon, self.Volume,
|
|
], spacing=8, vertical_alignment=ft.CrossAxisAlignment.CENTER)
|
|
|
|
@staticmethod
|
|
def _VolumeIconFor(Value: float):
|
|
return ft.Icons.VOLUME_OFF if Value <= 0 else ft.Icons.VOLUME_DOWN if Value < 0.5 else ft.Icons.VOLUME_UP
|
|
|
|
def SetVolume(self, Value: float) -> None:
|
|
"""The other mode's bar changed the shared volume."""
|
|
self.Volume.value = Value
|
|
self.VolumeIcon.icon = self._VolumeIconFor(Value)
|
|
|
|
def ShowTrack(self, Title: str, Subtitle: str, Cover: Optional[bytes]) -> None:
|
|
self.Title.value = Title
|
|
self.Subtitle.value = Subtitle
|
|
self.Cover.content = CoverImage(Cover, CoverSize)
|
|
if self.Shown:
|
|
self.update()
|
|
|
|
# --- Seek bar -----------------------------------------------------------
|
|
|
|
def _DragStart(self, e) -> None:
|
|
self._Dragging = True
|
|
|
|
def _Dragged(self, e) -> None:
|
|
self.Elapsed.value = FormatTime(e.control.value)
|
|
self.Elapsed.update()
|
|
|
|
async def _DragEnd(self, e) -> None:
|
|
self._Dragging = False
|
|
await self.ThePlayer.Seek(int(e.control.value))
|
|
|
|
def Refresh(self) -> None:
|
|
"""Play button, times and seek bar from the player; called by a
|
|
ticker a few times a second while playing."""
|
|
P = self.ThePlayer
|
|
self.PlayButton.icon = ft.Icons.PAUSE_ROUNDED if P.Playing else ft.Icons.PLAY_ARROW_ROUNDED
|
|
Duration = max(P.Duration, 1)
|
|
self.Seek.max = Duration
|
|
self.Seek.disabled = not P.Source
|
|
self.Total.value = FormatTime(P.Duration)
|
|
if not self._Dragging:
|
|
self.Seek.value = min(P.Position, Duration)
|
|
self.Elapsed.value = FormatTime(P.Position)
|
|
if self.Shown:
|
|
self.update()
|
|
|
|
|
|
async def Ticker(Bars: list[Transport], Interval: float = 0.25) -> None:
|
|
"""Keeps the playing bars moving. Runs for the app's lifetime."""
|
|
while True:
|
|
for Bar in Bars:
|
|
if Bar.ThePlayer.Playing and Bar.Shown:
|
|
Bar.Refresh()
|
|
await asyncio.sleep(Interval)
|