gui can now launch client and server (almost there)
This commit is contained in:
parent
785354032e
commit
f5a8b16a7f
6 changed files with 133 additions and 23 deletions
|
@ -10,6 +10,7 @@ include = ["syng/static"]
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
syng-client = "syng.client:main"
|
syng-client = "syng.client:main"
|
||||||
syng-server = "syng.server:main"
|
syng-server = "syng.server:main"
|
||||||
|
syng-gui = "syng.gui:main"
|
||||||
syng-shell = "syng.webclientmockup:main"
|
syng-shell = "syng.webclientmockup:main"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
|
@ -27,6 +28,8 @@ qrcode = "^7.4.2"
|
||||||
pymediainfo = "^6.1.0"
|
pymediainfo = "^6.1.0"
|
||||||
pyyaml = "^6.0.1"
|
pyyaml = "^6.0.1"
|
||||||
async-tkinter-loop = "^0.9.2"
|
async-tkinter-loop = "^0.9.2"
|
||||||
|
tkcalendar = "^1.6.1"
|
||||||
|
tktimepicker = "^2.0.2"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|
|
@ -65,6 +65,8 @@ currentLock: asyncio.Semaphore = asyncio.Semaphore(0)
|
||||||
|
|
||||||
def default_config() -> dict[str, Optional[int | str]]:
|
def default_config() -> dict[str, Optional[int | str]]:
|
||||||
return {
|
return {
|
||||||
|
"server": "http://localhost:8080",
|
||||||
|
"room": "ABCD",
|
||||||
"preview_duration": 3,
|
"preview_duration": 3,
|
||||||
"secret": None,
|
"secret": None,
|
||||||
"last_song": None,
|
"last_song": None,
|
||||||
|
@ -416,6 +418,9 @@ async def aiomain() -> None:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def create_async_and_start_client(config):
|
||||||
|
asyncio.run(start_client(config))
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Entry point for the syng-client script."""
|
"""Entry point for the syng-client script."""
|
||||||
|
@ -443,7 +448,7 @@ def main() -> None:
|
||||||
if args.server:
|
if args.server:
|
||||||
config["config"] |= {"server": args.server}
|
config["config"] |= {"server": args.server}
|
||||||
|
|
||||||
asyncio.run(start_client(config))
|
create_async_and_start_client(config)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
141
syng/gui.py
141
syng/gui.py
|
@ -1,19 +1,57 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from datetime import datetime, date, time
|
||||||
|
import os
|
||||||
import builtins
|
import builtins
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
import PIL
|
||||||
from yaml import load, Loader
|
from yaml import load, Loader
|
||||||
|
import multiprocessing
|
||||||
import customtkinter
|
import customtkinter
|
||||||
import qrcode
|
import qrcode
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
from tkinter import filedialog
|
from tkinter import PhotoImage, filedialog
|
||||||
from async_tkinter_loop import async_handler, async_mainloop
|
from tkcalendar import Calendar
|
||||||
from async_tkinter_loop.mixins import AsyncCTk
|
from tktimepicker import SpinTimePickerOld, AnalogPicker, AnalogThemes
|
||||||
|
from tktimepicker import constants
|
||||||
|
|
||||||
from .client import default_config, start_client
|
from .client import create_async_and_start_client, default_config, start_client
|
||||||
|
|
||||||
from .sources import available_sources
|
from .sources import available_sources
|
||||||
|
from .server import main as server_main
|
||||||
|
|
||||||
|
class DateAndTimePickerWindow(customtkinter.CTkToplevel):
|
||||||
|
def __init__(self, parent, input_field):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.calendar = Calendar(self)
|
||||||
|
self.calendar.pack(expand=True, fill="both")
|
||||||
|
self.timepicker = AnalogPicker(self, type=constants.HOURS12)
|
||||||
|
theme = AnalogThemes(self.timepicker)
|
||||||
|
theme.setDracula()
|
||||||
|
# self.timepicker.addAll(constants.HOURS24)
|
||||||
|
self.timepicker.pack(expand=True, fill="both")
|
||||||
|
|
||||||
|
button = customtkinter.CTkButton(self, text="Ok", command=partial(self.insert, input_field))
|
||||||
|
button.pack(expand=True, fill='x')
|
||||||
|
|
||||||
|
def insert(self, input_field: customtkinter.CTkTextbox):
|
||||||
|
input_field.delete("0.0", "end")
|
||||||
|
selected_date = self.calendar.selection_get()
|
||||||
|
print(type(selected_date))
|
||||||
|
if not isinstance(selected_date, date):
|
||||||
|
return
|
||||||
|
hours, minutes, ampm = self.timepicker.time()
|
||||||
|
if ampm == "PM":
|
||||||
|
hours = (hours + 12) % 24
|
||||||
|
|
||||||
|
selected_datetime = datetime.combine(selected_date, time(hours, minutes))
|
||||||
|
input_field.insert("0.0", selected_datetime.isoformat())
|
||||||
|
self.withdraw()
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OptionFrame(customtkinter.CTkScrollableFrame):
|
class OptionFrame(customtkinter.CTkScrollableFrame):
|
||||||
|
@ -104,6 +142,35 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
|
||||||
self.choose_options[name].set(value)
|
self.choose_options[name].set(value)
|
||||||
self.number_of_options += 1
|
self.number_of_options += 1
|
||||||
|
|
||||||
|
def open_date_and_time_picker(self, name, input_field):
|
||||||
|
if name not in self.date_and_time_pickers or not self.date_and_time_pickers[name].winfo_exists():
|
||||||
|
self.date_and_time_pickers[name] = DateAndTimePickerWindow(self, input_field)
|
||||||
|
else:
|
||||||
|
self.date_and_time_pickers[name].focus()
|
||||||
|
|
||||||
|
|
||||||
|
def add_date_time_option(self, name, description, value):
|
||||||
|
self.add_option_label(description)
|
||||||
|
self.date_time_options[name] = None
|
||||||
|
input_and_button = customtkinter.CTkFrame(self)
|
||||||
|
input_and_button.grid(column=1, row=self.number_of_options, sticky="EW")
|
||||||
|
input_field = customtkinter.CTkTextbox(input_and_button, wrap="none", height=1)
|
||||||
|
input_field.pack(side="left", fill="x", expand=True)
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(value)
|
||||||
|
except TypeError:
|
||||||
|
value = ""
|
||||||
|
input_field.insert("0.0", value)
|
||||||
|
|
||||||
|
button = customtkinter.CTkButton(
|
||||||
|
input_and_button,
|
||||||
|
text="...",
|
||||||
|
width=40,
|
||||||
|
command=partial(self.open_date_and_time_picker, name, input_field),
|
||||||
|
)
|
||||||
|
button.pack(side="right")
|
||||||
|
self.number_of_options += 1
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.columnconfigure((1,), weight=1)
|
self.columnconfigure((1,), weight=1)
|
||||||
|
@ -112,6 +179,8 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
|
||||||
self.choose_options = {}
|
self.choose_options = {}
|
||||||
self.bool_options = {}
|
self.bool_options = {}
|
||||||
self.list_options = {}
|
self.list_options = {}
|
||||||
|
self.date_time_options = {}
|
||||||
|
self.date_and_time_pickers = {}
|
||||||
|
|
||||||
def get_config(self):
|
def get_config(self):
|
||||||
config = {}
|
config = {}
|
||||||
|
@ -173,9 +242,10 @@ class GeneralConfig(OptionFrame):
|
||||||
["forced", "optional", "none"],
|
["forced", "optional", "none"],
|
||||||
str(config["waiting_room_policy"]).lower(),
|
str(config["waiting_room_policy"]).lower(),
|
||||||
)
|
)
|
||||||
self.add_string_option(
|
# self.add_string_option(
|
||||||
"last_song", "Time of last song\nin ISO-8601", config["last_song"]
|
# "last_song", "Time of last song\nin ISO-8601", config["last_song"]
|
||||||
)
|
# )
|
||||||
|
self.add_date_time_option("last_song", "Time of last song", config["last_song"])
|
||||||
self.add_string_option(
|
self.add_string_option(
|
||||||
"preview_duration", "Preview Duration", config["preview_duration"]
|
"preview_duration", "Preview Duration", config["preview_duration"]
|
||||||
)
|
)
|
||||||
|
@ -190,15 +260,37 @@ class GeneralConfig(OptionFrame):
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
class SyngGui(customtkinter.CTk, AsyncCTk):
|
class SyngGui(customtkinter.CTk):
|
||||||
def loadConfig(self):
|
def loadConfig(self):
|
||||||
filedialog.askopenfilename()
|
filedialog.askopenfilename()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
if self.server is not None:
|
||||||
|
self.server.kill()
|
||||||
|
|
||||||
|
if self.client is not None:
|
||||||
|
self.client.kill()
|
||||||
|
|
||||||
|
self.withdraw()
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(className="Syng")
|
super().__init__(className="Syng")
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.on_close)
|
||||||
|
|
||||||
with open("syng-client.yaml") as cfile:
|
rel_path = os.path.dirname(__file__)
|
||||||
loaded_config = load(cfile, Loader=Loader)
|
img = PIL.ImageTk.PhotoImage(file=os.path.join(rel_path,"static/syng.png"))
|
||||||
|
self.wm_iconbitmap()
|
||||||
|
self.iconphoto(False, img)
|
||||||
|
|
||||||
|
self.server = None
|
||||||
|
self.client = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open("syng-client.yaml") as cfile:
|
||||||
|
loaded_config = load(cfile, Loader=Loader)
|
||||||
|
except FileNotFoundError:
|
||||||
|
loaded_config = {}
|
||||||
config = {"sources": {}, "config": default_config()}
|
config = {"sources": {}, "config": default_config()}
|
||||||
if "config" in loaded_config:
|
if "config" in loaded_config:
|
||||||
config["config"] |= loaded_config["config"]
|
config["config"] |= loaded_config["config"]
|
||||||
|
@ -222,10 +314,16 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
|
||||||
loadbutton.pack(side="left")
|
loadbutton.pack(side="left")
|
||||||
|
|
||||||
startbutton = customtkinter.CTkButton(
|
startbutton = customtkinter.CTkButton(
|
||||||
fileframe, text="Start", command=self.start
|
fileframe, text="Start", command=self.start_client
|
||||||
)
|
)
|
||||||
startbutton.pack(side="right")
|
startbutton.pack(side="right")
|
||||||
|
|
||||||
|
startserverbutton = customtkinter.CTkButton(
|
||||||
|
fileframe, text="Start Server", command=self.start_server
|
||||||
|
)
|
||||||
|
startserverbutton.pack(side="right")
|
||||||
|
|
||||||
|
|
||||||
open_web_button = customtkinter.CTkButton(
|
open_web_button = customtkinter.CTkButton(
|
||||||
fileframe, text="Open Web", command=self.open_web
|
fileframe, text="Open Web", command=self.open_web
|
||||||
)
|
)
|
||||||
|
@ -266,8 +364,7 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
|
||||||
|
|
||||||
self.updateQr()
|
self.updateQr()
|
||||||
|
|
||||||
@async_handler
|
def start_client(self):
|
||||||
async def start(self):
|
|
||||||
sources = {}
|
sources = {}
|
||||||
for source, tab in self.tabs.items():
|
for source, tab in self.tabs.items():
|
||||||
sources[source] = tab.get_config()
|
sources[source] = tab.get_config()
|
||||||
|
@ -275,8 +372,14 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
|
||||||
general_config = self.general_config.get_config()
|
general_config = self.general_config.get_config()
|
||||||
|
|
||||||
config = {"sources": sources, "config": general_config}
|
config = {"sources": sources, "config": general_config}
|
||||||
print(config)
|
# print(config)
|
||||||
await start_client(config)
|
self.client = multiprocessing.Process(target=create_async_and_start_client, args=(config,))
|
||||||
|
self.client.start()
|
||||||
|
|
||||||
|
def start_server(self):
|
||||||
|
self.server = multiprocessing.Process(target=server_main)
|
||||||
|
self.server.start()
|
||||||
|
|
||||||
|
|
||||||
def open_web(self):
|
def open_web(self):
|
||||||
config = self.general_config.get_config()
|
config = self.general_config.get_config()
|
||||||
|
@ -303,11 +406,9 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
|
||||||
self.changeQr(server + room)
|
self.changeQr(server + room)
|
||||||
|
|
||||||
|
|
||||||
# async def main():
|
def main():
|
||||||
# gui = SyngGui()
|
SyngGui().mainloop()
|
||||||
# await gui.run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# asyncio.run(main())
|
main()
|
||||||
SyngGui().async_mainloop()
|
|
||||||
|
|
|
@ -26,5 +26,6 @@ def configure_sources(configs: dict[str, Any]) -> dict[str, Source]:
|
||||||
configured_sources = {}
|
configured_sources = {}
|
||||||
for source, config in configs.items():
|
for source, config in configs.items():
|
||||||
if source in available_sources:
|
if source in available_sources:
|
||||||
configured_sources[source] = available_sources[source](config)
|
if config["enabled"]:
|
||||||
|
configured_sources[source] = available_sources[source](config)
|
||||||
return configured_sources
|
return configured_sources
|
||||||
|
|
|
@ -19,7 +19,7 @@ class FileBasedSource(Source):
|
||||||
"extensions": (
|
"extensions": (
|
||||||
list,
|
list,
|
||||||
"List of filename extensions\n(mp3+cdg, mp4, ...)",
|
"List of filename extensions\n(mp3+cdg, mp4, ...)",
|
||||||
"mp3+cdg",
|
["mp3+cdg"],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
syng/static/syng.png
Normal file
BIN
syng/static/syng.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
Loading…
Add table
Reference in a new issue