@@ -150,9 +150,13 @@ def create_windows_exe(cli_binary: Path, version: str, python_bin: str = "python
150150 installer_entry .write_text (
151151 """from __future__ import annotations
152152
153+ import os
153154import shutil
154155import sys
156+ import threading
155157from pathlib import Path
158+ import tkinter as tk
159+ from tkinter import messagebox, ttk
156160
157161
158162def resolve_payload_root() -> Path:
@@ -161,14 +165,17 @@ def resolve_payload_root() -> Path:
161165 return Path(__file__).resolve().parent / 'payload'
162166
163167
164- def main() -> int:
165- payload = resolve_payload_root()
166-
168+ def default_resolve_root() -> Path:
167169 appdata = Path.home() / 'AppData' / 'Roaming'
168- if 'APPDATA' in __import__('os').environ:
169- appdata = Path(__import__('os').environ['APPDATA'])
170+ if 'APPDATA' in os.environ:
171+ appdata = Path(os.environ['APPDATA'])
172+ return appdata / 'Blackmagic Design' / 'DaVinci Resolve' / 'Support' / 'Fusion'
173+
174+
175+ def install_openbeat(destination: Path) -> None:
176+ payload = resolve_payload_root()
177+ resolve_root = destination.expanduser()
170178
171- resolve_root = appdata / 'Blackmagic Design' / 'DaVinci Resolve' / 'Support' / 'Fusion'
172179 utility_target = resolve_root / 'Scripts' / 'Utility' / 'OpenBeat'
173180 module_target = resolve_root / 'Modules' / 'OpenBeat'
174181
@@ -189,8 +196,156 @@ def main() -> int:
189196 config_text = f'return {{\\ n python_bin = "{exe_path}",\\ n}}\\ n'
190197 (module_target / 'OpenBeatConfig.local.lua').write_text(config_text, encoding='utf-8')
191198
192- print(f'OpenBeat installed to: {resolve_root}')
193- print('Restart Resolve if it is open.')
199+
200+ class InstallerWizard(tk.Tk):
201+ def __init__(self) -> None:
202+ super().__init__()
203+ self.title('OpenBeat Setup')
204+ self.geometry('620x420')
205+ self.minsize(560, 380)
206+ self.resizable(False, False)
207+ self.protocol('WM_DELETE_WINDOW', self.cancel)
208+
209+ self.step = 0
210+ self.install_error: str | None = None
211+
212+ self.columnconfigure(0, weight=1)
213+ self.rowconfigure(0, weight=1)
214+ self.configure(background='#f6f7f9')
215+
216+ self.content = ttk.Frame(self, padding=(28, 24, 28, 18))
217+ self.content.grid(row=0, column=0, sticky='nsew')
218+ self.content.columnconfigure(0, weight=1)
219+ self.content.rowconfigure(5, weight=1)
220+
221+ self.nav = ttk.Frame(self, padding=(18, 12))
222+ self.nav.grid(row=1, column=0, sticky='ew')
223+ self.nav.columnconfigure(0, weight=1)
224+
225+ self.back_button = ttk.Button(self.nav, text='< Back', command=self.go_back)
226+ self.next_button = ttk.Button(self.nav, text='Next >', command=self.go_next)
227+ self.cancel_button = ttk.Button(self.nav, text='Cancel', command=self.cancel)
228+ self.back_button.grid(row=0, column=1, padx=(0, 8))
229+ self.next_button.grid(row=0, column=2, padx=(0, 8))
230+ self.cancel_button.grid(row=0, column=3)
231+
232+ self.frames = [
233+ self.welcome_step,
234+ self.ready_step,
235+ self.installing_step,
236+ self.finish_step,
237+ ]
238+ self.show_step(0)
239+
240+ def clear_content(self) -> None:
241+ for child in self.content.winfo_children():
242+ child.destroy()
243+
244+ def add_heading(self, title: str, body: str) -> None:
245+ ttk.Label(self.content, text=title, font=('Segoe UI', 18, 'bold')).grid(
246+ row=0, column=0, sticky='w', pady=(0, 12)
247+ )
248+ ttk.Label(self.content, text=body, wraplength=520, justify='left').grid(
249+ row=1, column=0, sticky='nw'
250+ )
251+
252+ def show_step(self, step: int) -> None:
253+ self.step = step
254+ self.clear_content()
255+ self.frames[step]()
256+
257+ self.back_button.configure(state='normal' if step == 1 else 'disabled')
258+ self.cancel_button.configure(state='normal' if step not in (2, 3) else 'disabled')
259+
260+ if step == 1:
261+ self.next_button.configure(text='Install', state='normal')
262+ elif step == 2:
263+ self.next_button.configure(text='Next >', state='disabled')
264+ elif step == 3:
265+ self.next_button.configure(text='Finish', state='normal')
266+ else:
267+ self.next_button.configure(text='Next >', state='normal')
268+
269+ def welcome_step(self) -> None:
270+ self.add_heading(
271+ 'Welcome to OpenBeat Setup',
272+ 'This wizard will install the OpenBeat scripts and bundled runtime for DaVinci Resolve. Close Resolve before continuing if it is currently open.',
273+ )
274+
275+ def ready_step(self) -> None:
276+ self.add_heading(
277+ 'Ready to Install',
278+ 'Click Install to copy OpenBeat into the standard DaVinci Resolve Fusion support folder and configure the bundled runtime.',
279+ )
280+ summary = ttk.LabelFrame(self.content, text='Install summary', padding=14)
281+ summary.grid(row=2, column=0, sticky='ew', pady=(24, 0))
282+ summary.columnconfigure(1, weight=1)
283+ ttk.Label(summary, text='Destination').grid(row=0, column=0, sticky='nw', padx=(0, 12))
284+ ttk.Label(summary, text=str(default_resolve_root()), wraplength=400).grid(row=0, column=1, sticky='w')
285+
286+ def installing_step(self) -> None:
287+ self.add_heading('Installing OpenBeat', 'Please wait while setup copies files into Resolve.')
288+ self.progress = ttk.Progressbar(self.content, mode='indeterminate')
289+ self.progress.grid(row=2, column=0, sticky='ew', pady=(24, 8))
290+ self.status = ttk.Label(self.content, text='Preparing install...')
291+ self.status.grid(row=3, column=0, sticky='w')
292+ self.progress.start(12)
293+
294+ def finish_step(self) -> None:
295+ if self.install_error:
296+ self.add_heading(
297+ 'Installation Did Not Complete',
298+ f'OpenBeat could not be installed.\\ n\\ n{self.install_error}',
299+ )
300+ return
301+
302+ self.add_heading(
303+ 'OpenBeat Setup Complete',
304+ 'OpenBeat has been installed successfully. Restart DaVinci Resolve if it is open, then use Workspace > Scripts > OpenBeat.',
305+ )
306+
307+ def go_back(self) -> None:
308+ if self.step > 0:
309+ self.show_step(self.step - 1)
310+
311+ def go_next(self) -> None:
312+ if self.step == 3:
313+ self.destroy()
314+ return
315+ if self.step == 1:
316+ self.start_install()
317+ return
318+ self.show_step(self.step + 1)
319+
320+ def cancel(self) -> None:
321+ if self.step == 2:
322+ messagebox.showinfo('OpenBeat Setup', 'Setup is currently installing OpenBeat.')
323+ return
324+ if messagebox.askyesno('Cancel Setup', 'Are you sure you want to cancel OpenBeat Setup?'):
325+ self.destroy()
326+
327+ def start_install(self) -> None:
328+ self.install_error = None
329+ destination = default_resolve_root()
330+ self.show_step(2)
331+
332+ def worker() -> None:
333+ try:
334+ install_openbeat(destination)
335+ except Exception as exc:
336+ self.install_error = str(exc)
337+ self.after(0, self.install_finished)
338+
339+ threading.Thread(target=worker, daemon=True).start()
340+
341+ def install_finished(self) -> None:
342+ if hasattr(self, 'progress'):
343+ self.progress.stop()
344+ self.show_step(3)
345+
346+
347+ def main() -> int:
348+ InstallerWizard().mainloop()
194349 return 0
195350
196351
@@ -207,6 +362,7 @@ def main() -> int:
207362 "--clean" ,
208363 "--noconfirm" ,
209364 "--onefile" ,
365+ "--windowed" ,
210366 "--name" ,
211367 "openbeat-installer" ,
212368 "--add-data" ,
0 commit comments