diff options
Diffstat (limited to 'web-project/src')
-rw-r--r-- | web-project/src/solo_tool_web.py | 197 |
1 files changed, 141 insertions, 56 deletions
diff --git a/web-project/src/solo_tool_web.py b/web-project/src/solo_tool_web.py index f854e1a..b014061 100644 --- a/web-project/src/solo_tool_web.py +++ b/web-project/src/solo_tool_web.py @@ -1,56 +1,141 @@ -from nicegui import ui - -from solo_tool import SoloTool - -st = SoloTool() -st.loadSession("/home/eddy/music/solos/practice.json") - -def _createSeekHandler(delta): - def f(): - newPosition = st.getPlaybackPosition() + delta - newPosition = min(1.0, max(0.0, newPosition)) - st.setPlaybackPosition(newPosition) - return f - -def main(): - with ui.splitter(value=30) as splitter: - splitter.style('width: 100%; height: 100%;') - with splitter.before: - with ui.list().props('dense separator'): - for song in st.getSongs(): - ui.item(song) - with splitter.after: - ui.slider(min=0, max=1.2, value=1.0, step=0.01, on_change=lambda e: st.setPlaybackVolume(e.value)) - - with ui.row().classes("w-full justify-between no-wrap"): - ui.button('-5%', on_click=lambda: st.setPlaybackRate(max(0.5, st.getPlaybackRate() - 0.05))) - ui.slider(min=0.5, max=1.2, step=0.05, value=st.getPlaybackRate(), on_change=lambda e: st.setPlaybackRate(e.value)) - ui.button('+5%', on_click=lambda: st.setPlaybackRate(min(1.2, st.getPlaybackRate() + 0.05))) - - ui.slider(min=0, max=100, value=0) - - with ui.row().classes("w-full justify-between no-wrap"): - ui.button('Prev', on_click=st.previousSong) - ui.button('-25%', on_click=_createSeekHandler(-0.25)) - ui.button('-5%', on_click=_createSeekHandler(-0.05)) - ui.button('-1%', on_click=_createSeekHandler(-0.01)) - ui.button('+1%', on_click=_createSeekHandler(0.01)) - ui.button('+5%', on_click=_createSeekHandler(0.05)) - ui.button('+25%', on_click=_createSeekHandler(0.25)) - ui.button('Next', on_click=st.nextSong) - - with ui.row().classes("w-full justify-between no-wrap"): - ui.button('Set A') - ui.button('Set B') - ui.button('Previous AB') - ui.button('Next AB') - - with ui.row().classes("w-full justify-between no-wrap"): - ui.button('Toggle AB', on_click=lambda: st.setAbLimitEnable(not st.isAbLimitEnabled())) - ui.button('Stop', on_click=st.stop) - ui.button('Play', on_click=st.play) - ui.button('Jump to A', on_click=st.jumpToA) - ui.run() - -if __name__ in {'__main__', '__mp_main__'}: - main() +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, handlers +from solo_tool.session_manager import SessionManager +from solo_tool.midi_controller_actition import ActitionController + +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 +midiPedal = ActitionController() + +@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] + midiPedal.setSoloTool(st) + + # 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, False, "https://files.0xf7.com") + #start(8080, 0.5, True, "/home/eddy/music/sessions") |