"""What both modes share: the App context (config, notifications, dialogs, the file picker, switching modes) and small reusable controls.""" import os import subprocess from typing import Callable import flet as ft from Core import AppConfig, ChildEnvironment, ChipButton, IsWindows, Note, NotificationStyle, Palette class App: def __init__(self, Page: ft.Page, Config: AppConfig): self.Page = Page self.Config = Config self.Picker = ft.FilePicker() # registers itself with the page # Set by Main. self.SwitchMode: Callable[[str], None] = lambda Name: None self.OpenInAudio: Callable[[list[str]], None] = lambda Paths: None self.MediaChanged: Callable[[], None] = lambda: None # a mode's NowPlaying changed self.MediaModesChanged: Callable[[], None] = lambda: None # shuffle, repeat or loop changed def Notify(self, Text: str, Kind: str = "info") -> None: Background, Accent = NotificationStyle(Kind) self.Page.show_dialog(ft.SnackBar( content=ft.Text(Text, color=Accent), bgcolor=Background, shape=ft.RoundedRectangleBorder(radius=4, side=ft.BorderSide(1, Accent)), open=True, )) def ShowDialog(self, Title: str, Content: ft.Control, Actions: list[ft.Control], Width: int = 480) -> ft.AlertDialog: Dialog = ft.AlertDialog(title=Title, content=ft.Container(Content, width=Width), actions=Actions) self.Page.show_dialog(Dialog) return Dialog def Close(self) -> None: self.Page.pop_dialog() def Confirm(self, Title: str, Text: str, OnYes: Callable[[], None], YesText: str = "Delete") -> None: def Yes(e) -> None: self.Close() OnYes() self.ShowDialog(Title, ft.Text(Text), [ ChipButton("Cancel", lambda e: self.Close()), ChipButton(YesText, Yes, Accent=Palette.Red), ], Width=420) def Prompt(self, Title: str, Label: str, OnOk: Callable[[str], None], Value: str = "", OkText: str = "OK") -> None: """Asks for one line of text; OnOk gets it stripped (never empty).""" Field = ft.TextField(label=Label, value=Value, autofocus=True) def Ok(e) -> None: Text = (Field.value or "").strip() if Text: self.Close() OnOk(Text) Field.on_submit = Ok self.ShowDialog(Title, Field, [ChipButton("Cancel", lambda e: self.Close()), ChipButton(OkText, Ok)], Width=380) def OpenFolder(self, PathText: str) -> None: try: if IsWindows: os.startfile(PathText) # Explorer else: subprocess.Popen(["xdg-open", PathText], env=ChildEnvironment(), start_new_session=True) except OSError as Error: self.Notify(f"Couldn't open {PathText}: {Error}", "error") def EmptyState(Text: str, Detail: str = "", Icon=ft.Icons.MUSIC_OFF_OUTLINED) -> ft.Control: return ft.Container( ft.Column([ ft.Icon(Icon, color=Palette.Surface1, size=56), ft.Text(Text, color=Palette.Overlay0, size=16), Note(Detail), ], horizontal_alignment=ft.CrossAxisAlignment.CENTER, tight=True, spacing=8), alignment=ft.Alignment.CENTER, expand=True, ) def InfoTable(Rows: list[tuple[str, str]], LabelWidth: int = 110) -> ft.Column: """Label/value rows; values are selectable so tags can be copied.""" return ft.Column([ ft.Row([ ft.Text(Label, color=Palette.Overlay0, size=13, width=LabelWidth), ft.Text(Value, color=Palette.Text, size=13, selectable=True, expand=True), ], vertical_alignment=ft.CrossAxisAlignment.START) for Label, Value in Rows ], spacing=4)