import sys from os import getenv from os.path import basename, splitext from functools import partial from nicegui import ui, events import click from fastapi import HTTPException from urllib.parse import unquote from solo_tool import SoloTool from solo_tool.session_manager import SessionManager from solo_tool import handlers def fileName(path: str) -> str: return unquote(basename(splitext(path)[0])) @ui.refreshable def keyPointList(st: SoloTool) -> None: with ui.list().props('separator'): if st.keyPoints is not None: for kp in st.keyPoints: ui.item(f"{kp:0.2}", on_click=handlers.keyPointAbsolute(st, kp)).props('clickable v-ripple').classes('text-lg') @ui.refreshable def songList(st: SoloTool, songDrawer) -> None: with ui.list().props('separator'): for i, path in enumerate(st.songs): ui.item(fileName(path), on_click=handlers.songAbsolute(st, i, lambda: songDrawer.hide())).props('clickable v-ripple') sessions = {} sessionManager = None @ui.page('/{sessionId}') def sessionPage(sessionId: str): if sessionId not in sessions: raise HTTPException(status_code=404, detail=f"No session with ID {sessionId}") fullscreen = ui.fullscreen() ui.dark_mode().enable() ui.colors(secondary='#ffc107') ui.page_title(sessionId) st = sessions[sessionId] # Manage songs dialog with ui.dialog() as manageSongsDialog: ui.label("Under construction") # Header with ui.header().classes('items-center justify-between'): with ui.row().classes('items-center justify-start'): ui.button(icon='menu', on_click=lambda: songDrawer.toggle()).props('flat dense round color=white') ui.label().bind_text_from(st, 'song', lambda index: fileName(st.songs[index]) if index is not None else "Select a song").classes('text-lg') with ui.row().classes('items-center justify-start'): ui.button(icon='home', on_click=lambda: ui.navigate.to("/")).props('flat dense round color=white') def save(): sessionManager.saveSession(st, sessionId) ui.button(icon='save', on_click=save).props('flat dense round color=white') ui.button(icon='fullscreen', on_click=fullscreen.toggle).props('flat dense round color=white') # Key points list with ui.right_drawer(top_corner=True, bottom_corner=True).props('width=120 behavior=desktop'): ui.label("Key Points").classes('text-lg') keyPointList(st) def addKeyPoint() -> None: st.keyPoints += [st.keyPoint] ui.button(icon='add', on_click=addKeyPoint).props('flat round dense color=white') # Song list with ui.left_drawer(bottom_corner=True).props('overlay breakpoint=8000') as songDrawer: songList(st, songDrawer) ui.button(icon='add', on_click=manageSongsDialog.open).props('flat round dense color=white') # Playback position def setPosition(e) -> None: st.position = e.args ui.slider(min=0, max=1.0, step=0.001) \ .bind_value_from(st, 'position') \ .on('change', setPosition) \ .props('thumb-size=0px track-size=16px') # Key point position ui.slider(min=0, max=1.0, step=0.001).bind_value(st, 'keyPoint').props('selection-color=transparent color=secondary') # Play control with ui.button_group().classes('w-full').style('height: 80px'): buttonSize = "20px" ui.button(icon='skip_previous', on_click=handlers.restartOrPreviousSong(st, 0.01)).props(f"size={buttonSize}").style('flex: 1') ui.button(color='positive', on_click=handlers.playPause(st)).bind_icon_from(st, "playing", lambda playing: "pause" if playing else "play_arrow").props(f"size={buttonSize}").style('flex: 1') ui.button(icon='vertical_align_bottom', on_click=handlers.positionToKeyPoint(st), color='negative').props(f"size={buttonSize}").style('flex: 2') ui.button(icon='undo', on_click=st.jump, color='secondary').props(f"size={buttonSize}").style('flex: 2') ui.button(icon='skip_next', on_click=handlers.songRelative(st, 1)).props(f"size={buttonSize}").style('flex: 1') # Playback rate with ui.row().classes('w-full justify-between no-wrap items-center'): markerLabels = ",".join([f"{v}:'{v}x'" for v in [0.4, 0.6, 0.8, 1.0, 1.2]]) ui.slider(min=0.4, max=1.2, step=0.05).bind_value(st, 'rate').props(f'snap markers :marker-labels="{{{markerLabels}}}"').classes('q-px-md') # Volume with ui.row().classes('w-full justify-between no-wrap items-center'): volumeLabels = ",".join([f"{v}:'{int(v*100)}%'" for v in [0.0, 0.25, 0.5, 0.75, 1.0, 1.25]]) ui.slider(min=0, max=1.25, step=0.01).bind_value(st, 'volume').props(f':marker-labels="{{{volumeLabels}}}"').classes('q-px-md') @ui.page('/') def landingPage(): ui.dark_mode().enable() ui.page_title("Solo Tool") # Header with ui.header().classes('items-center'): ui.label("Choose a session").classes('text-lg') for id, soloTool in sessions.items(): ui.button(id, on_click=partial(ui.navigate.to, f"/{id}")) def start(port, refresh, reload, session_path): global sessionManager sessionManager = SessionManager(session_path) for id in sessionManager.getSessions(): songTool = sessionManager.loadSession(id) songTool.registerKeyPointListCallback(lambda new: keyPointList.refresh()) songTool.registerSongSelectionCallback(lambda new: keyPointList.refresh()) songTool.registerSongListCallback(lambda new: songList.refresh()) sessions[id] = songTool try: ui.run(reload=reload, binding_refresh_interval=refresh, port=port) except KeyboardInterrupt: pass @click.command() @click.option("--port", type=int, default=8080, help="Port on which to bind.") @click.option("--refresh", type=float, default=0.5, help="Refresh interval in seconds.") @click.option("--reload/--no-reload", default=True, help="Auto-reload when files change.") @click.option("--session_path", default="https://files.0xf7.com", help="Look for sessions in this location.") def main(port, refresh, reload, session_path): start(port, refresh, reload, session_path) # Hardcoded dev settings if __name__ in {"__main__", "__mp_main__"}: start(8080, 0.5, True, "https://files.0xf7.com") #start(8080, 0.5, True, "/home/eddy/music/sessions")