diff --git a/pyproject.toml b/pyproject.toml index 2cf4967..7b7f246 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ include = ["syng/static"] [tool.poetry.scripts] syng-client = "syng.client:main" syng-server = "syng.server:main" +syng-gui = "syng.gui:main" syng-shell = "syng.webclientmockup:main" [tool.poetry.dependencies] @@ -27,6 +28,8 @@ qrcode = "^7.4.2" pymediainfo = "^6.1.0" pyyaml = "^6.0.1" async-tkinter-loop = "^0.9.2" +tkcalendar = "^1.6.1" +tktimepicker = "^2.0.2" [build-system] requires = ["poetry-core"] diff --git a/syng/client.py b/syng/client.py index ea27b23..f5da742 100644 --- a/syng/client.py +++ b/syng/client.py @@ -65,6 +65,8 @@ currentLock: asyncio.Semaphore = asyncio.Semaphore(0) def default_config() -> dict[str, Optional[int | str]]: return { + "server": "http://localhost:8080", + "room": "ABCD", "preview_duration": 3, "secret": None, "last_song": None, @@ -416,6 +418,9 @@ async def aiomain() -> None: """ pass +def create_async_and_start_client(config): + asyncio.run(start_client(config)) + def main() -> None: """Entry point for the syng-client script.""" @@ -443,7 +448,7 @@ def main() -> None: if args.server: config["config"] |= {"server": args.server} - asyncio.run(start_client(config)) + create_async_and_start_client(config) if __name__ == "__main__": diff --git a/syng/gui.py b/syng/gui.py index dda9ebf..3803b87 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -1,19 +1,57 @@ import asyncio +from datetime import datetime, date, time +import os import builtins from functools import partial import webbrowser +import PIL from yaml import load, Loader +import multiprocessing import customtkinter import qrcode import secrets import string -from tkinter import filedialog -from async_tkinter_loop import async_handler, async_mainloop -from async_tkinter_loop.mixins import AsyncCTk +from tkinter import PhotoImage, filedialog +from tkcalendar import Calendar +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 .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): @@ -104,6 +142,35 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.choose_options[name].set(value) 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): super().__init__(parent) self.columnconfigure((1,), weight=1) @@ -112,6 +179,8 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.choose_options = {} self.bool_options = {} self.list_options = {} + self.date_time_options = {} + self.date_and_time_pickers = {} def get_config(self): config = {} @@ -173,9 +242,10 @@ class GeneralConfig(OptionFrame): ["forced", "optional", "none"], str(config["waiting_room_policy"]).lower(), ) - self.add_string_option( - "last_song", "Time of last song\nin ISO-8601", config["last_song"] - ) + # self.add_string_option( + # "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( "preview_duration", "Preview Duration", config["preview_duration"] ) @@ -190,15 +260,37 @@ class GeneralConfig(OptionFrame): return config -class SyngGui(customtkinter.CTk, AsyncCTk): +class SyngGui(customtkinter.CTk): def loadConfig(self): 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): super().__init__(className="Syng") + self.protocol("WM_DELETE_WINDOW", self.on_close) - with open("syng-client.yaml") as cfile: - loaded_config = load(cfile, Loader=Loader) + rel_path = os.path.dirname(__file__) + 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()} if "config" in loaded_config: config["config"] |= loaded_config["config"] @@ -222,10 +314,16 @@ class SyngGui(customtkinter.CTk, AsyncCTk): loadbutton.pack(side="left") startbutton = customtkinter.CTkButton( - fileframe, text="Start", command=self.start + fileframe, text="Start", command=self.start_client ) startbutton.pack(side="right") + startserverbutton = customtkinter.CTkButton( + fileframe, text="Start Server", command=self.start_server + ) + startserverbutton.pack(side="right") + + open_web_button = customtkinter.CTkButton( fileframe, text="Open Web", command=self.open_web ) @@ -266,8 +364,7 @@ class SyngGui(customtkinter.CTk, AsyncCTk): self.updateQr() - @async_handler - async def start(self): + def start_client(self): sources = {} for source, tab in self.tabs.items(): sources[source] = tab.get_config() @@ -275,8 +372,14 @@ class SyngGui(customtkinter.CTk, AsyncCTk): general_config = self.general_config.get_config() config = {"sources": sources, "config": general_config} - print(config) - await start_client(config) + # print(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): config = self.general_config.get_config() @@ -303,11 +406,9 @@ class SyngGui(customtkinter.CTk, AsyncCTk): self.changeQr(server + room) -# async def main(): -# gui = SyngGui() -# await gui.run() +def main(): + SyngGui().mainloop() if __name__ == "__main__": - # asyncio.run(main()) - SyngGui().async_mainloop() + main() diff --git a/syng/sources/__init__.py b/syng/sources/__init__.py index b57aefc..976615f 100644 --- a/syng/sources/__init__.py +++ b/syng/sources/__init__.py @@ -26,5 +26,6 @@ def configure_sources(configs: dict[str, Any]) -> dict[str, Source]: configured_sources = {} for source, config in configs.items(): 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 diff --git a/syng/sources/filebased.py b/syng/sources/filebased.py index 64577e2..dc59be0 100644 --- a/syng/sources/filebased.py +++ b/syng/sources/filebased.py @@ -19,7 +19,7 @@ class FileBasedSource(Source): "extensions": ( list, "List of filename extensions\n(mp3+cdg, mp4, ...)", - "mp3+cdg", + ["mp3+cdg"], ) } diff --git a/syng/static/syng.png b/syng/static/syng.png new file mode 100644 index 0000000..f60bac3 Binary files /dev/null and b/syng/static/syng.png differ