diff --git a/lib/client/client.py b/lib/client/client.py index 53890ed..dac5244 100644 --- a/lib/client/client.py +++ b/lib/client/client.py @@ -1,9 +1,12 @@ +import subprocess + import requests import tkinter as tk import threading import time import os from tkinter import ttk +from PIL import Image, ImageTk from new_job_window import NewJobWindow from server_proxy import RenderServerProxy @@ -36,11 +39,17 @@ class ZordonClient: # Create a Treeview widget self.root = tk.Tk() - self.tree = ttk.Treeview(self.root, show="headings") + self.root.title("Zordon Render Client") self.server_proxy = RenderServerProxy(hostname='localhost') self.job_cache = [] - # Define the columns + # Setup photo preview + self.photo_label = tk.Label(self.root, width=400, height=300) + self.photo_label.pack() + + # Setup the Tree + self.tree = ttk.Treeview(self.root, show="headings") + self.tree.bind("<>", self.on_row_select) self.tree["columns"] = ("id", "Name", "Renderer", "Priority", "Status", "Time Elapsed", "Frames") # Format the columns @@ -57,10 +66,22 @@ class ZordonClient: self.tree.heading(name, text=name) # Pack the Treeview widget - self.tree.pack(fill=tk.BOTH, expand=False) + self.tree.pack(fill=tk.BOTH, expand=True) - new_job_button = tk.Button(self.root, text="New Job", command=self.show_new_job_window) - new_job_button.pack() + button_frame = tk.Frame(self.root) + button_frame.pack(pady=10) + + # 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 Job", command=self.stop_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) + 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") @@ -68,8 +89,57 @@ class ZordonClient: self.root.minsize(width=600, height=600) make_sortable(self.tree) + + self.update_jobs() + selected_item = self.tree.get_children()[0] + self.tree.selection_set(selected_item) self.start_update_thread() + def stop_job(self): + selected_item = self.tree.selection()[0] # Get the selected item + row_data = self.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 on_row_select(self, event): + selected_item = self.tree.selection()[0] # Get the selected item + row_data = self.tree.item(selected_item) # Get the text of the selected item + job_id = row_data['values'][0] + + # update thumb + thumb_url = f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/ui/job/{job_id}/thumbnail' + response = requests.get(thumb_url) + if response.status_code == 200: + import io + image_data = response.content + image = Image.open(io.BytesIO(image_data)) + thumb_image = ImageTk.PhotoImage(image) + if thumb_image: + self.photo_label.configure(image=thumb_image) + self.photo_label.image = thumb_image + + # update button status + job = next((d for d in self.job_cache if d.get('id') == job_id), None) + button_state = 'normal' if job['status'] == 'running' else 'disabled' + self.stop_button.config(state=button_state) + + def reveal_in_finder(self): + selected_item = self.tree.selection()[0] # Get the selected item + row_data = self.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.tree.selection()[0] # Get the selected item + row_data = self.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() @@ -93,12 +163,18 @@ class ZordonClient: break if clear_table: self.tree.delete(*self.tree.get_children()) - self.job_cache = self.server_proxy.get_jobs() - all_jobs = self.job_cache - for job in all_jobs: + 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 job['percent_complete'] - 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']) + 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.tree.exists(job['id']): update_row(self.tree, job['id'], new_values=values) else: @@ -108,7 +184,7 @@ class ZordonClient: new_window = tk.Toplevel(self.root) new_window.title("New Window") new_window.geometry("500x600+300+300") - new_window.resizable(False, False) + new_window.resizable(False, height=True) x = NewJobWindow(parent=new_window, hostname=self.server_proxy.hostname) x.pack() diff --git a/lib/client/new_job_window.py b/lib/client/new_job_window.py index 87f273e..b2b3d77 100755 --- a/lib/client/new_job_window.py +++ b/lib/client/new_job_window.py @@ -73,7 +73,7 @@ class NewJobWindow(Frame): self.renderer_info = {} self.priority = IntVar(value=2) - self.master.title("Schedule Job") + self.master.title("New Job") self.pack(fill=BOTH, expand=True) # project frame diff --git a/lib/render_workers/base_worker.py b/lib/render_workers/base_worker.py index ef4bf9f..a2ed0c0 100644 --- a/lib/render_workers/base_worker.py +++ b/lib/render_workers/base_worker.py @@ -324,7 +324,8 @@ class BaseRenderWorker(Base): 'renderer_version': self.renderer_version, 'errors': getattr(self, 'errors', None), 'total_frames': self.total_frames, - 'last_output': getattr(self, 'last_output', None) + 'last_output': getattr(self, 'last_output', None), + 'log_path': self.log_path() } # convert to json and back to auto-convert dates to iso format