import subprocess import logging import requests import tkinter as tk import threading import time import socket import os from tkinter import ttk, messagebox from PIL import Image, ImageTk from new_job_window import NewJobWindow from server_proxy import RenderServerProxy def request_data(server_ip, payload, server_port=8080, timeout=2): try: req = requests.get(f'http://{server_ip}:{server_port}/api/{payload}', timeout=timeout) if req.ok: return req.json() except Exception as e: pass return None def sort_column(tree, col, reverse=False): data = [(tree.set(child, col), child) for child in tree.get_children('')] data.sort(reverse=reverse) for index, (_, child) in enumerate(data): tree.move(child, '', index) def make_sortable(tree): for col in tree["columns"]: tree.heading(col, text=col, command=lambda c=col: sort_column(tree, c)) def available_servers(): return [socket.gethostname()] class ZordonClient: default_image = Image.open("../server/static/images/desktop.png") def __init__(self): servers = available_servers() # Create a Treeview widget self.root = tk.Tk() self.root.title("Zordon Render Client") self.local_host = socket.gethostname() self.server_proxy = RenderServerProxy(hostname=servers[0]) self.job_cache = [] # Setup photo preview photo_pad = tk.Frame(self.root, background="gray") photo_pad.pack(fill=tk.BOTH, pady=5, padx=5) self.photo_label = tk.Label(photo_pad, height=500) self.photo_label.pack(fill=tk.BOTH, expand=True) self.set_image(self.default_image) server_frame = tk.LabelFrame(self.root, text="Server") server_frame.pack(fill=tk.BOTH, pady=5, padx=5, expand=True) # Create server tree self.server_tree = ttk.Treeview(server_frame, show="headings") self.server_tree.pack(side=tk.LEFT, expand=True, fill=tk.BOTH) self.server_tree["columns"] = ("server") self.server_tree.column("server", width=50) self.server_tree.bind("<>", self.server_picked) self.server_tree.column("server", width=200) # Create separator separator = ttk.Separator(server_frame, orient=tk.VERTICAL) separator.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.Y) # Setup the Tree self.job_tree = ttk.Treeview(server_frame, show="headings", height=20) self.job_tree.tag_configure('running', background='lawn green', font=('', 0, 'bold')) self.job_tree.bind("<>", self.on_row_select) self.job_tree["columns"] = ("id", "Name", "Renderer", "Priority", "Status", "Time Elapsed", "Frames", "") # Format the columns self.job_tree.column("id", width=0, stretch=False) self.job_tree.column("Name", width=300) self.job_tree.column("Renderer", width=100, stretch=False) self.job_tree.column("Priority", width=50, stretch=False) self.job_tree.column("Status", width=100, stretch=False) self.job_tree.column("Time Elapsed", width=100, stretch=False) self.job_tree.column("Frames", width=50, stretch=False) # Create the column headings for name in self.job_tree['columns']: self.job_tree.heading(name, text=name) # Pack the Treeview widget self.job_tree.pack(fill=tk.BOTH, expand=True) button_frame = tk.Frame(server_frame) button_frame.pack(pady=5, fill=tk.BOTH, expand=True) # Create buttons logs_button = tk.Button(button_frame, text="Logs", command=self.open_logs) finder_button = tk.Button(button_frame, text="Reveal in Finder", command=self.reveal_in_finder) self.stop_button = tk.Button(button_frame, text="Stop", command=self.stop_job) delete_button = tk.Button(button_frame, text="Delete", command=self.delete_job) add_job_button = tk.Button(button_frame, text="Add Job", command=self.show_new_job_window) # Pack the buttons in the frame self.stop_button.pack(side=tk.LEFT) delete_button.pack(side=tk.LEFT) finder_button.pack(side=tk.LEFT) logs_button.pack(side=tk.LEFT) add_job_button.pack(side=tk.RIGHT) # Start the Tkinter event loop self.root.geometry("500x600+300+300") self.root.maxsize(width=2000, height=1200) self.root.minsize(width=900, height=1000) make_sortable(self.job_tree) # update jobs self.update_jobs() try: selected_job = self.job_tree.get_children()[0] self.job_tree.selection_set(selected_job) except IndexError: pass # update servers self.populate_server_tree() try: selected_server = self.server_tree.get_children()[0] self.server_tree.selection_set(selected_server) except IndexError: pass self.start_update_thread() def populate_server_tree(self): servers = available_servers() self.server_tree.delete(*self.server_tree.get_children()) for hostname in servers: self.server_tree.insert("", tk.END, iid=hostname, values=(hostname,)) def server_picked(self, event): new_hostname = self.server_tree.selection()[0] self.server_proxy.hostname = new_hostname self.job_cache = [] self.update_jobs(clear_table=True) def stop_job(self): selected_item = self.job_tree.selection()[0] # Get the selected item row_data = self.job_tree.item(selected_item) # Get the text of the selected item job_id = row_data['values'][0] self.server_proxy.request_data(f'job/{job_id}/cancel?confirm=true') def delete_job(self): selected_item = self.job_tree.selection()[0] # Get the selected item row_data = self.job_tree.item(selected_item) # Get the text of the selected item job_id = row_data['values'][0] job = next((d for d in self.job_cache if d.get('id') == job_id), None) display_name = job['name'] or os.path.basename(job['input_path']) result = messagebox.askyesno("Confirmation", f"Are you sure you want to delete the job:\n{display_name}?") if result: self.server_proxy.request_data(f'job/{job_id}/delete?confirm=true') self.update_jobs(clear_table=True) def set_image(self, image): thumb_image = ImageTk.PhotoImage(image) if thumb_image: self.photo_label.configure(image=thumb_image) self.photo_label.image = thumb_image def on_row_select(self, event): if self.job_tree.selection(): selected_item = self.job_tree.selection()[0] # Get the selected item row_data = self.job_tree.item(selected_item) # Get the text of the selected item job_id = row_data['values'][0] # update thumb response = self.server_proxy.request(f'job/{job_id}/thumbnail?size=big') if response.ok: try: import io image_data = response.content image = Image.open(io.BytesIO(image_data)) self.set_image(image) except Exception as e: print(f"error getting image: {e}") # update button status job = next((d for d in self.job_cache if d.get('id') == job_id), None) button_state = 'normal' if job and job['status'] == 'running' else 'disabled' self.stop_button.config(state=button_state) else: self.set_image(self.default_image) def reveal_in_finder(self): selected_item = self.job_tree.selection()[0] # Get the selected item row_data = self.job_tree.item(selected_item) # Get the text of the selected item job_id = row_data['values'][0] job = next((d for d in self.job_cache if d.get('id') == job_id), None) output_dir = os.path.dirname(job['output_path']) subprocess.run(['open', output_dir]) def open_logs(self): selected_item = self.job_tree.selection()[0] # Get the selected item row_data = self.job_tree.item(selected_item) # Get the text of the selected item job_id = row_data['values'][0] job = next((d for d in self.job_cache if d.get('id') == job_id), None) subprocess.run(['open', job['log_path']]) def mainloop(self): self.root.mainloop() def start_update_thread(self): x = threading.Thread(target=self.__background_update) x.daemon = True x.start() def __background_update(self): while True: self.update_jobs(clear_table=False) time.sleep(1) def update_jobs(self, clear_table=False): def update_row(tree, id, new_values, tags=None): for item in tree.get_children(): values = tree.item(item, "values") if values[0] == id: tree.item(item, values=new_values, tags=tags) break if clear_table: self.job_tree.delete(*self.job_tree.get_children()) self.job_cache.clear() job_fetch = self.server_proxy.get_jobs() if job_fetch: self.job_cache = job_fetch # update the cache only if its good data for job in self.job_cache: display_status = job['status'] if job['status'] != 'running' else \ ('%.0f%%' % (job['percent_complete'] * 100)) # if running, show percentage, otherwise just show status tags = (job['status'],) values = (job['id'], job['name'] or os.path.basename(job['input_path']), job['renderer'] + "-" + job['renderer_version'], job['priority'], display_status, job['time_elapsed'], job['total_frames']) if self.job_tree.exists(job['id']): update_row(self.job_tree, job['id'], new_values=values, tags=tags) else: self.job_tree.insert("", tk.END, iid=job['id'], values=values, tags=tags) def show_new_job_window(self): new_window = tk.Toplevel(self.root) new_window.title("New Window") new_window.geometry("500x600+300+300") new_window.resizable(False, height=True) x = NewJobWindow(parent=new_window, hostname=self.server_proxy.hostname) x.pack() if __name__ == '__main__': x = ZordonClient() x.mainloop()