from glob import glob from os.path import basename, splitext from nicegui import ui from solo_tool import SoloTool from solo_tool.session_manager import loadSession, saveSession from solo_tool import handlers from solo_tool.midi_controller_launchpad_mini import MidiController SESSION_DIR = "/home/eddy/music/sessions" SONG_POOL = "/home/eddy/music" def fileName(path: str) -> str: return basename(splitext(path)[0]) @ui.refreshable def keyPointTables(st: SoloTool) -> None: for i, song in enumerate(st.songs): with ui.list().props('separator').bind_visibility_from(st, 'song', value=i): for kp in st._keyPoints[i]: ui.item(f"{kp:0.2}", on_click=handlers.setKeyPoint(st, kp)).props('clickable v-ripple').classes('text-lg') sessions = {} for f in glob(f"{SESSION_DIR}/*.json"): sessionName = fileName(f) sessions[sessionName] = loadSession(f, SONG_POOL) sessions[sessionName].registerKeyPointListCallback(lambda new: keyPointTables.refresh()) @ui.page('/{sessionName}') def sessionPage(sessionName: str): if sessionName not in sessions: return fullscreen = ui.fullscreen() ui.dark_mode().enable() ui.colors(secondary='#ffc107') ui.page_title(sessionName) st = sessions[sessionName] # Header with ui.header().classes('items-center justify-between'): with ui.row().classes('items-center justify-start'): ui.button(icon='menu', on_click=lambda: song_drawer.toggle()).props('flat round dense 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'): def save(): saveSession(st, f"{SESSION_DIR}/{sessionName}.json") 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') keyPointTables(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 song_drawer: with ui.list().props('separator'): for i, path in enumerate(st.songs): ui.item(fileName(path), on_click=handlers.setSong(st, i, lambda: song_drawer.hide())).props('clickable v-ripple') ui.button(icon='add', on_click=lambda e: print(e)).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='amber-5').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.changeSong(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 name, soloTool in sessions.items(): ui.link(name, f"/{name}") ui.run(binding_refresh_interval=0.2)