Skip to content

GUI + Installer #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c2db59d
Add Functioning GUI
0dm May 11, 2023
8220fe4
Update app.py
0dm May 11, 2023
09d1303
Merge branch '#109' into add-GUI
0dm May 11, 2023
359fbaf
NiceGUI WIP
0dm May 17, 2023
2bee6dd
align dialog buttons
0dm May 17, 2023
a95661f
share button
0dm May 17, 2023
c8ad9fe
deprecate old ui, add import/export recording
0dm May 18, 2023
062cde9
Merge remote-tracking branch 'upstream/main' into add-GUI
0dm May 19, 2023
03b5295
added file picker for import
0dm May 19, 2023
fa43c57
implemented pyinstaller
0dm May 19, 2023
f9b7029
modularization + fixes
0dm May 23, 2023
6237304
Update util.py
0dm May 23, 2023
1551c56
Update util.py
0dm May 23, 2023
8fc3d40
Merge remote-tracking branch 'upstream/main' into add-GUI
0dm May 26, 2023
c5cb169
Merge remote-tracking branch 'upstream/main' into add-GUI
0dm May 27, 2023
bfd4af7
Merge remote-tracking branch 'upstream/main' into add-GUI
0dm May 27, 2023
33f8cc9
Merge branch 'silence-warnings' into add-GUI
0dm May 29, 2023
f6fba8a
added openadapt.renew
0dm May 30, 2023
25d82b5
encapsulated into method "run_app"
0dm May 31, 2023
b728c98
refactor
0dm Jun 1, 2023
77818ec
added changes from review, fixed pyinstaller
0dm Jun 1, 2023
93e9ba1
remove commented line
0dm Jun 1, 2023
f165494
Merge branch 'clear-data' into add-GUI
0dm Jun 1, 2023
56d3cff
remove unused file
0dm Jun 2, 2023
e3732a3
save & sync switch states
0dm Jun 2, 2023
a680c4d
fixed recording stop button
0dm Jun 2, 2023
cc0107f
Merge remote-tracking branch 'upstream/main' into add-GUI
0dm Jun 7, 2023
45a651f
Merge branch 'silence-warnings' into add-GUI
0dm Jun 7, 2023
6789dcd
allow for python3 -m openadapt.app
0dm Jun 7, 2023
0666809
Merge remote-tracking branch 'upstream/main' into add-GUI
0dm Jun 10, 2023
bbb3fca
rename localfilepicker
0dm Jun 10, 2023
df78d57
Merge remote-tracking branch 'upstream/main' into add-GUI
0dm Jun 12, 2023
c54103c
Merge remote-tracking branch 'upstream/main' into add-GUI
0dm Jun 13, 2023
4fb0ba5
add todo for future improvement
0dm Jun 13, 2023
794925c
Merge remote-tracking branch 'upstream/main' into add-GUI
0dm Jun 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions openadapt/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from openadapt.app.main import run_app

run_app()
Binary file added openadapt/app/assets/logo.ico
Binary file not shown.
Binary file added openadapt/app/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions openadapt/app/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os
import subprocess
from pathlib import Path

import nicegui

spec = [
"pyi-makespec",
f"{Path(__file__).parent}/main.py",
f"--icon={Path(__file__).parent}/assets/logo.ico",
"--name",
"OpenAdapt", # name
# "--onefile", # trade startup speed for smaller file size
"--onedir",
"--windowed", # prevent console appearing, only use with ui.run(native=True, ...)
"--add-data",
f"{Path(nicegui.__file__).parent}{os.pathsep}nicegui",
]

subprocess.call(spec)

# add import sys ; sys.setrecursionlimit(sys.getrecursionlimit() * 5) to line 2 of OpenAdapt.spec
with open("OpenAdapt.spec", "r+") as f:
lines = f.readlines()
lines[1] = "import sys ; sys.setrecursionlimit(sys.getrecursionlimit() * 5)\n"
f.seek(0)
f.truncate()
f.writelines(lines)

subprocess.call(["pyinstaller", "OpenAdapt.spec"])
88 changes: 88 additions & 0 deletions openadapt/app/cards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import signal
from nicegui import ui
from subprocess import Popen
from openadapt.app.objects.local_file_picker import LocalFilePicker
from openadapt.app.util import set_dark, sync_switch

PROC = None


def settings(dark_mode):
with ui.dialog() as settings, ui.card():
s = ui.switch("Dark mode", on_change=lambda: set_dark(dark_mode, s.value))
sync_switch(s, dark_mode)
ui.button("Close", on_click=lambda: settings.close())

settings.open()


def select_import(f):
async def pick_file():
result = await LocalFilePicker(".")
ui.notify(f"Selected {result[0]}" if result else "No file selected.")
selected_file.text = result[0] if result else ""
import_button.enabled = True if result else False

with ui.dialog() as import_dialog, ui.card():
with ui.column():
ui.button("Select File", on_click=pick_file).props("icon=folder")
selected_file = ui.label("")
selected_file.visible = False
import_button = ui.button(
"Import", on_click=lambda: f(selected_file.text, delete.value)
)
import_button.enabled = False
delete = ui.checkbox("Delete file after import")

import_dialog.open()


def recording_prompt(options, record_button):
if PROC is None:
with ui.dialog() as dialog, ui.card():
ui.label("Enter a name for the recording: ")
ui.input(
label="Name",
placeholder="test",
autocomplete=options,
on_change=lambda e: result.set_text(e),
)
result = ui.label()

with ui.row():
ui.button("Close", on_click=dialog.close)
ui.button("Enter", on_click=lambda: on_record())

dialog.open()

def terminate():
global PROC
PROC.send_signal(signal.SIGINT)

# wait for process to terminate
PROC.wait()
ui.notify("Stopped recording")
record_button._props["name"] = "radio_button_checked"
record_button.on("click", lambda: recording_prompt(options, record_button))

PROC = None

def begin():
name = result.text.__getattribute__("value")

ui.notify(
f"Recording {name}... Press CTRL + C in terminal window to cancel",
)
PROC = Popen(
"python3 -m openadapt.record " + name,
shell=True,
)
record_button._props["name"] = "stop"
record_button.on("click", lambda: terminate())
record_button.update()
return PROC

def on_record():
global PROC
dialog.close()
PROC = begin()
82 changes: 82 additions & 0 deletions openadapt/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import threading
import base64
import os

from nicegui import app, ui

from openadapt import replay, visualize
from openadapt.app.cards import recording_prompt, select_import, settings
from openadapt.app.util import clear_db, on_export, on_import
from openadapt.app.objects.console import Console

SERVER = "127.0.0.1:8000/upload"


def run_app():
file = os.path.dirname(__file__)
app.native.window_args["resizable"] = False # too many issues with resizing
app.native.start_args["debug"] = False

dark = ui.dark_mode()
logger = None

# Add logo
# right align icon
with ui.row().classes("w-full justify-right"):
# settings

# alignment trick
with ui.avatar(color="white" if dark else "black", size=128):
logo_base64 = base64.b64encode(open(f"{file}/assets/logo.png", "rb").read())
img = bytes(
f"data:image/png;base64,{(logo_base64.decode('utf-8'))}",
encoding="utf-8",
)
ui.image(img.decode("utf-8"))
ui.icon("settings").tooltip("Settings").on("click", lambda: settings(dark))
ui.icon("delete").on("click", lambda: clear_db(log=logger)).tooltip(
"Clear all recorded data"
)
ui.icon("upload").tooltip("Export Data").on("click", lambda: on_export(SERVER))
ui.icon("download").tooltip("Import Data").on(
"click", lambda: select_import(on_import)
)
ui.icon("share").tooltip("Share").on(
"click", lambda: (_ for _ in ()).throw(Exception(NotImplementedError))
)

# Recording description autocomplete
options = ["test"]

with ui.splitter(value=20) as splitter:
splitter.classes("w-full h-full")
with splitter.before:
with ui.column().classes("w-full h-full"):
record_button = (
ui.icon("radio_button_checked", size="64px")
.on("click", lambda: recording_prompt(options, record_button))
.tooltip("Record a new replay / Stop recording")
)
ui.icon("visibility", size="64px").on(
"click", lambda: threading.Thread(target=visualize.main).start()
).tooltip("Visualize the latest replay")

ui.icon("play_arrow", size="64px").on(
"click", lambda: replay.replay("NaiveReplayStrategy")
).tooltip("Play the latest replay")
with splitter.after:
logger = Console()
logger.log.style("height: 250px;, width: 300px;")
splitter.enabled = False

ui.run(
title="OpenAdapt Client",
native=True,
window_size=(400, 400),
fullscreen=False,
reload=False,
)


if __name__ == "__main__":
run_app()
21 changes: 21 additions & 0 deletions openadapt/app/objects/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import sys

from nicegui import ui


class Console(object):
def __init__(self):
self.log = ui.log().classes("w-full h-20")
self.old_stderr = sys.stderr
sys.stderr = self

def write(self, data):
self.log.push(data[:-1])
self.log.update()

def flush(self):
self.log.update()

def reset(self):
self.log.clear()
sys.stderr = self.old_stderr
108 changes: 108 additions & 0 deletions openadapt/app/objects/local_file_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# retrieved from https://github.com/zauberzeug/nicegui/tree/main/examples/local_file_picker

from pathlib import Path
from typing import Dict, Optional

from nicegui import ui


class LocalFilePicker(ui.dialog):
def __init__(
self,
directory: str,
*,
upper_limit: Optional[str] = ...,
multiple: bool = False,
show_hidden_files: bool = False,
dark_mode: bool = False,
) -> None:
"""Local File Picker

This is a simple file picker that allows you to select a file from the local filesystem where NiceGUI is running.

:param directory: The directory to start in.
:param upper_limit: The directory to stop at (None: no limit, default: same as the starting directory).
:param multiple: Whether to allow multiple files to be selected.
:param show_hidden_files: Whether to show hidden files.
"""
super().__init__()

self.path = Path(directory).expanduser()
if upper_limit is None:
self.upper_limit = None
else:
self.upper_limit = Path(
directory if upper_limit == ... else upper_limit
).expanduser()
self.show_hidden_files = show_hidden_files

with self, ui.card():
self.grid = (
ui.aggrid(
{
"columnDefs": [{"field": "name", "headerName": "File"}],
"rowSelection": "multiple" if multiple else "single",
},
html_columns=[0],
)
.classes("w-96")
.on("cellDoubleClicked", self.handle_double_click)
)
with ui.row().classes("w-full justify-end"):
ui.button("Cancel", on_click=self.close).props("outline")
ui.button("Ok", on_click=self._handle_ok)
self.update_grid()

def update_grid(self) -> None:
paths = list(self.path.glob("*"))
if not self.show_hidden_files:
paths = [p for p in paths if not p.name.startswith(".")]
paths.sort(key=lambda p: p.name.lower())
paths.sort(key=lambda p: not p.is_dir())

self.grid.options["rowData"] = [
{
"name": f"📁 <strong>{p.name}</strong>" if p.is_dir() else p.name,
"path": str(p),
}
for p in paths
]
if (
self.upper_limit is None
and self.path != self.path.parent
or self.upper_limit is not None
and self.path != self.upper_limit
):
self.grid.options["rowData"].insert(
0,
{
"name": "📁 <strong>..</strong>",
"path": str(self.path.parent),
},
)

self.grid.update()

async def handle_double_click(self, msg: Dict) -> None:
self.path = Path(msg["args"]["data"]["path"])
if self.path.is_dir():
self.update_grid()
else:
self.submit([str(self.path)])

async def _handle_ok(self):
rows = await ui.run_javascript(
f"getElement({self.grid.id}).gridOptions.api.getSelectedRows()"
)
self.submit([r["path"] for r in rows])


async def pick_file():
result = await LocalFilePicker("~", multiple=True)
ui.notify(f"You chose {result}")


if __name__ in {"__main__", "__mp_main__"}:
ui.button("Choose file", on_click=pick_file).props("icon=folder")

ui.run(native=True)
58 changes: 58 additions & 0 deletions openadapt/app/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import bz2
import os
import sys
from shutil import copyfileobj
from nicegui import ui
from openadapt.scripts.reset_db import reset_db


def clear_db(log=None):
if log:
log.log.clear()
o = sys.stdout
sys.stdout = sys.stderr

reset_db()
ui.notify("Cleared database.")
sys.stdout = o


def on_import(selected_file, delete=False, src="openadapt.db"):
with open(src, "wb") as f:
with bz2.BZ2File(selected_file, "rb") as f2:
copyfileobj(f2, f)

if delete:
os.remove(selected_file)

ui.notify("Imported data.")


def on_export(dest):
# TODO: add ui card for configuration
ui.notify("Exporting data...")

# compress db with bz2
with open("openadapt.db", "rb") as f:
with bz2.BZ2File("openadapt.db.bz2", "wb", compresslevel=9) as f2:
copyfileobj(f, f2)

# TODO: magic wormhole
# # upload to server with requests, and keep file name
# files = {
# "files": open("openadapt.db.bz2", "rb"),
# }
# #requests.post(dest, files=files)

# delete compressed db
os.remove("openadapt.db.bz2")

ui.notify("Exported data.")


def sync_switch(switch, prop):
switch.value = prop.value


def set_dark(dark_mode, value):
dark_mode.value = value
Loading