mirror of
https://git.srhtnin.garden/sarah/SaraTune.git
synced 2026-10-15 10:23:17 +02:00
Initial commit
This commit is contained in:
commit
0eeff23589
25 files changed
+3328
No files matched your search
@@ -0,0 +1,246 @@
|
||||
"""SaraTune: a music player with two modes.
|
||||
|
||||
- Audio: open a file or folder and listen right away; shows the file's
|
||||
details and tags. Nothing is saved.
|
||||
- Music: a library of your music folders, a queue, and saved playlists.
|
||||
|
||||
Files given on the command line (e.g. "Open with SaraTune") open in Audio mode."""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
import flet as ft
|
||||
|
||||
from AudioMode import AudioMode
|
||||
from Core import (
|
||||
AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, ChipButtonStyle, DataHome, EffectiveSettings,
|
||||
IsWindows, LoadConfig, Palette,
|
||||
)
|
||||
from MusicMode import MusicMode
|
||||
from Player import Player
|
||||
from Settings import ShowSettingsDialog
|
||||
from Transport import Ticker
|
||||
from Widgets import App
|
||||
|
||||
Modes = [("Audio", ft.Icons.HEADPHONES_OUTLINED), ("Music", ft.Icons.LIBRARY_MUSIC_OUTLINED)]
|
||||
|
||||
# When running as an AppImage, SaraTune can put itself in the app menu (like SaraSuite).
|
||||
_AppImage = os.environ.get("APPIMAGE", "")
|
||||
_MenuEntry = DataHome / "applications" / "SaraTune.desktop"
|
||||
_MenuIcon = DataHome / "SaraTune" / "SaraTune.png"
|
||||
MimeTypes = "audio/mpeg;audio/flac;audio/x-flac;audio/ogg;audio/x-vorbis+ogg;audio/opus;audio/x-opus+ogg;" \
|
||||
"audio/wav;audio/x-wav;audio/mp4;audio/x-m4a;audio/aac;audio/x-aiff;audio/x-ms-wma;audio/x-matroska;" \
|
||||
"audio/webm;"
|
||||
|
||||
# The Flet client's audio plugin is only in its "full" build, which on Linux
|
||||
# needs the system's libmpv (Fedora: mpv-libs). Windows' full client brings
|
||||
# everything itself.
|
||||
def _HasMpv() -> bool:
|
||||
"""Loads the exact library the client links against (like the client
|
||||
will), so LD_LIBRARY_PATH and odd install paths count too."""
|
||||
try:
|
||||
ctypes.CDLL("libmpv.so.2")
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
NeedsMpv = sys.platform.startswith("linux") and not _HasMpv()
|
||||
|
||||
|
||||
def _SetMenuEntry(Enabled: bool) -> None:
|
||||
if not Enabled:
|
||||
_MenuEntry.unlink(missing_ok=True)
|
||||
_MenuIcon.unlink(missing_ok=True)
|
||||
return
|
||||
_MenuIcon.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(AssetsDir / "Icon.png", _MenuIcon)
|
||||
_MenuEntry.parent.mkdir(parents=True, exist_ok=True)
|
||||
_MenuEntry.write_text(
|
||||
"[Desktop Entry]\nType=Application\nName=SaraTune\n"
|
||||
"Comment=Play audio files and your music library\n"
|
||||
f"Exec={_AppImage} %F\nIcon={_MenuIcon}\nTerminal=false\n"
|
||||
f"Categories=AudioVideo;Audio;Player;\nMimeType={MimeTypes}\nStartupNotify=true\n"
|
||||
)
|
||||
|
||||
|
||||
def _SetUpPage(Page: ft.Page, Appearance) -> None:
|
||||
Page.title = AppName
|
||||
Page.theme_mode = ft.ThemeMode.DARK
|
||||
Page.theme = BuildTheme()
|
||||
Page.dark_theme = BuildTheme()
|
||||
Page.fonts = {BundledFontFamily: "fonts/Fixedsys_Excelsior_Regular.ttf"}
|
||||
Page.window.title_bar_hidden = Appearance.HideWindowTitleBar
|
||||
Page.padding = 0
|
||||
Page.bgcolor = Palette.Base
|
||||
if IsWindows:
|
||||
Page.window.icon = str(AssetsDir / "Icon.ico") # taskbar/title bar (Linux: the desktop entry)
|
||||
|
||||
|
||||
def MissingMpv(Page: ft.Page) -> None:
|
||||
"""Shown instead of the player when libmpv isn't installed."""
|
||||
ApplyAppearance(EffectiveSettings(LoadConfig()))
|
||||
_SetUpPage(Page, EffectiveSettings(LoadConfig()))
|
||||
Page.window.width, Page.window.height = 620, 360
|
||||
Command = "sudo dnf install mpv-libs"
|
||||
Page.add(ft.Container(ft.Column([
|
||||
ft.Text(AppName, size=24, color=Palette.Mauve),
|
||||
ft.Text("SaraTune needs libmpv to play audio.", size=16, color=Palette.Text),
|
||||
ft.Text("Flet's audio player is part of its full desktop client, which links against the system's "
|
||||
"libmpv. Install it, then start SaraTune again:", color=Palette.Overlay0),
|
||||
ft.Container(ft.Text(Command, selectable=True, color=Palette.Green), bgcolor=Palette.Mantle, padding=10,
|
||||
border_radius=4, border=ft.Border.all(1, Palette.Surface1)),
|
||||
ft.Text("(On other distributions the package is usually called libmpv2 or mpv-libs.)", size=12,
|
||||
color=Palette.Overlay0),
|
||||
], spacing=12), padding=24))
|
||||
|
||||
|
||||
def Main(Page: ft.Page) -> None:
|
||||
Config = LoadConfig()
|
||||
Appearance = EffectiveSettings(Config)
|
||||
ApplyAppearance(Appearance) # before anything reads Palette.X
|
||||
_SetUpPage(Page, Appearance)
|
||||
Page.window.width = 1180
|
||||
Page.window.height = 760
|
||||
Page.window.min_width = 820
|
||||
Page.window.min_height = 520
|
||||
|
||||
TheApp = App(Page, Config)
|
||||
|
||||
def SetVolume(Value: float) -> None:
|
||||
Config.Volume = round(Value, 3)
|
||||
for Each in Players.values():
|
||||
Each.SetVolume(Value)
|
||||
for Name, Mode in ModeViews.items():
|
||||
if Mode is not ModeViews[State["Mode"]]:
|
||||
Mode.Bar.SetVolume(Value)
|
||||
|
||||
Players = {Name: Player(Page, Config.Volume) for Name, _ in Modes}
|
||||
ModeViews = {
|
||||
"Audio": AudioMode(TheApp, Players["Audio"], SetVolume),
|
||||
"Music": MusicMode(TheApp, Players["Music"], SetVolume),
|
||||
}
|
||||
# One sound at a time: starting one mode's player pauses the other's.
|
||||
for Name, ThePlayer in Players.items():
|
||||
Others = [Other for OtherName, Other in Players.items() if OtherName != Name]
|
||||
ThePlayer.OnStarted = lambda Others=Others: [Page.run_task(Other.Pause) for Other in Others if Other.Playing]
|
||||
|
||||
Body = ft.Container(expand=True)
|
||||
State = {"Mode": ""}
|
||||
|
||||
def SwitchMode(Name: str) -> None:
|
||||
if Name not in ModeViews:
|
||||
return
|
||||
State["Mode"] = Name
|
||||
for ModeName, Mode in ModeViews.items():
|
||||
Mode.Bar.Shown = ModeName == Name
|
||||
Switcher.selected = [Name]
|
||||
Body.content = ModeViews[Name]
|
||||
Page.update()
|
||||
ModeViews[Name].Bar.Refresh()
|
||||
|
||||
def OpenInAudio(Paths: list[str]) -> None:
|
||||
SwitchMode("Audio")
|
||||
Page.run_task(ModeViews["Audio"].Open, Paths)
|
||||
|
||||
TheApp.SwitchMode = SwitchMode
|
||||
TheApp.OpenInAudio = OpenInAudio
|
||||
|
||||
Switcher = ft.SegmentedButton(
|
||||
segments=[ft.Segment(value=Name, label=Name, icon=Icon) for Name, Icon in Modes],
|
||||
selected=["Music"],
|
||||
show_selected_icon=False,
|
||||
style=ft.ButtonStyle(
|
||||
color={ft.ControlState.SELECTED: Palette.Crust, ft.ControlState.DEFAULT: Palette.Text},
|
||||
bgcolor={ft.ControlState.SELECTED: Palette.Mauve, ft.ControlState.DEFAULT: Palette.Surface0},
|
||||
side=ft.BorderSide(1, Palette.Mauve),
|
||||
shape=ft.RoundedRectangleBorder(radius=4),
|
||||
),
|
||||
on_change=lambda e: SwitchMode(e.control.selected[0]) if e.control.selected else None,
|
||||
)
|
||||
|
||||
def ToggleMenuEntry(e) -> None:
|
||||
_SetMenuEntry(not _MenuEntry.exists())
|
||||
MenuButton.content = "Remove from app menu" if _MenuEntry.exists() else "Add to app menu"
|
||||
MenuButton.update()
|
||||
|
||||
MenuButton = ft.OutlinedButton(
|
||||
content="Remove from app menu" if _MenuEntry.exists() else "Add to app menu",
|
||||
style=ChipButtonStyle(),
|
||||
visible=bool(_AppImage),
|
||||
on_click=ToggleMenuEntry,
|
||||
)
|
||||
|
||||
def AfterSettings(FoldersChanged: bool) -> None:
|
||||
if FoldersChanged:
|
||||
ModeViews["Music"].StartScan()
|
||||
|
||||
def ClearCache() -> None:
|
||||
Music = ModeViews["Music"]
|
||||
Music.Library.ClearCache()
|
||||
Music.ShowView("Songs")
|
||||
Music.StartScan()
|
||||
|
||||
SettingsButton = ft.IconButton(
|
||||
icon=ft.Icons.SETTINGS_OUTLINED,
|
||||
icon_color=Palette.Mauve,
|
||||
tooltip="Settings",
|
||||
on_click=lambda e: ShowSettingsDialog(TheApp, AfterSettings, ClearCache),
|
||||
)
|
||||
|
||||
async def OnKey(e: ft.KeyboardEvent) -> None:
|
||||
# Not while a dialog is open (its text fields take the keys), and
|
||||
# only in Audio mode: Music mode has a search field.
|
||||
if any(Dialog.open and not isinstance(Dialog, ft.SnackBar) for Dialog in Page._dialogs.controls):
|
||||
return
|
||||
if State["Mode"] == "Audio" and not (e.ctrl or e.alt or e.meta):
|
||||
await ModeViews["Audio"].HandleKey(e)
|
||||
|
||||
Page.on_keyboard_event = OnKey
|
||||
|
||||
async def OnWindowEvent(e: ft.WindowEvent) -> None:
|
||||
if e.type == ft.WindowEventType.CLOSE:
|
||||
ModeViews["Music"].SaveSession()
|
||||
Config.Save() # the volume
|
||||
await Page.window.destroy()
|
||||
|
||||
Page.window.prevent_close = True
|
||||
Page.window.on_event = OnWindowEvent
|
||||
|
||||
Page.add(ft.Column([
|
||||
ft.Container(
|
||||
content=ft.Row([
|
||||
ft.Text(AppName, size=24, color=Palette.Mauve),
|
||||
ft.Container(width=16),
|
||||
Switcher,
|
||||
ft.Container(expand=True),
|
||||
MenuButton,
|
||||
SettingsButton,
|
||||
]),
|
||||
padding=ft.Padding.symmetric(horizontal=16, vertical=8),
|
||||
bgcolor=Palette.Mantle,
|
||||
border=ft.Border.only(bottom=ft.BorderSide(1, Palette.Surface1)),
|
||||
),
|
||||
Body,
|
||||
], expand=True, spacing=0))
|
||||
|
||||
Files = [Argument for Argument in sys.argv[1:] if not Argument.startswith("-")]
|
||||
if Files:
|
||||
SwitchMode("Audio")
|
||||
Page.run_task(ModeViews["Audio"].Open, Files)
|
||||
else:
|
||||
SwitchMode(Config.StartMode if Config.StartMode in ModeViews else "Music")
|
||||
Page.run_task(ModeViews["Music"].Started)
|
||||
Page.run_task(Ticker, [Mode.Bar for Mode in ModeViews.values()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if NeedsMpv:
|
||||
os.environ["FLET_DESKTOP_FLAVOR"] = "light"
|
||||
os.environ.pop("FLET_VIEW_PATH", None) # Launch.sh's client copy is the full one
|
||||
ft.run(MissingMpv, assets_dir=str(AssetsDir))
|
||||
else:
|
||||
os.environ.setdefault("FLET_DESKTOP_FLAVOR", "full")
|
||||
ft.run(Main, assets_dir=str(AssetsDir))
|
||||
Reference in new issue
Block a user