aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMelody Horn <melody@boringcactus.com>2020-12-24 04:56:55 -0700
committerMelody Horn <melody@boringcactus.com>2020-12-24 04:56:55 -0700
commit3364694f14f8a0b92ce70ca608a73966c4043256 (patch)
treede2a073ad2660b094fdf5152cfae353578eb488e
parent940e8754bc13f9f601c5f3db58e4a1ab7d05e2ea (diff)
downloadvidslice-3364694f14f8a0b92ce70ca608a73966c4043256.tar.gz
vidslice-3364694f14f8a0b92ce70ca608a73966c4043256.zip
switch from wxpython to tkinter
-rw-r--r--options.py155
-rw-r--r--output.py174
-rw-r--r--requirements.txt1
-rw-r--r--screenshot.pngbin29314 -> 21013 bytes
-rw-r--r--sources.py133
-rw-r--r--vidslice.py164
6 files changed, 295 insertions, 332 deletions
diff --git a/options.py b/options.py
index 2fc8078..3f7eb0f 100644
--- a/options.py
+++ b/options.py
@@ -1,4 +1,6 @@
-import wx
+import typing
+from tkinter import *
+from tkinter import ttk
HEADERS_ROW = 0
START_ROW = 1
@@ -20,135 +22,113 @@ class FFmpegOptions:
class Property:
- def __init__(self, parent, name, *, label=None, orig=None, edit=None, new_class=None, new=None):
+ def __init__(self, parent, name, row, convert: typing.Callable = int):
self.handlers = []
- if label is None:
- label = wx.StaticText(parent, label=name)
- self.label = label
- if orig is None:
- orig = wx.StaticText(parent, label="N/A")
- self.orig = orig
- if edit is None:
- edit = wx.CheckBox(parent)
- edit.Bind(wx.EVT_CHECKBOX, self.handle_edit)
- self.edit = edit
- if new is None:
- if new_class is None:
- new_class = wx.TextCtrl
- new = new_class(parent)
- new.Bind(wx.EVT_SPINCTRLDOUBLE, self.handle_change)
- new.Bind(wx.EVT_SPINCTRL, self.handle_change)
- new.Disable()
- self.new = new
-
- def add_to(self, sizer, row):
- sizer.Add(self.label, wx.GBPosition(row, LABEL_COL))
- sizer.Add(self.orig, wx.GBPosition(row, ORIG_COL))
- sizer.Add(self.edit, wx.GBPosition(row, EDIT_BOX_COL))
- sizer.Add(self.new, wx.GBPosition(row, NEW_COL), flag=wx.EXPAND)
+ ttk.Label(parent, text=name).grid(column=LABEL_COL, row=row, sticky=(E, W))
+ self.orig = StringVar(parent, value="N/A")
+ ttk.Label(parent, textvariable=self.orig).grid(column=ORIG_COL, row=row, sticky=(E, W))
+ self.edit = BooleanVar(parent)
+ self.edit_widget = ttk.Checkbutton(parent, variable=self.edit, command=self.handle_edit)
+ self.edit_widget.grid(column=EDIT_BOX_COL, row=row, sticky=(E, W))
+ self.new = ttk.Spinbox(parent, command=self.handle_change)
+ self.new.state(['disabled'])
+ self.new.grid(column=NEW_COL, row=row, sticky=(E, W))
+ self.convert = convert
+ self.disable()
def disable(self):
self.enable(False)
def enable(self, enabled=True):
if enabled:
- self.edit.Enable()
+ self.edit_widget.state(['!disabled'])
else:
- self.edit.SetValue(False)
- self.edit.Disable()
- self.new.Disable()
+ self.edit.set(False)
+ self.edit_widget.state(['disabled'])
+ self.new.state(['disabled'])
def is_enabled(self):
- return self.edit.Enabled
+ return self.edit_widget.instate(['!disabled'])
def is_edit(self):
- return self.edit.GetValue()
+ return self.edit.get()
def set_orig(self, val):
- self.orig.SetLabel(str(val))
+ self.orig.set(str(val))
def get_orig(self):
- return self.orig.GetLabel()
+ return self.orig.get()
def set_calc_new(self, val):
if not self.is_edit():
- if self.new.GetMin() > val:
- self.new.SetMin(val)
- if self.new.GetMax() < val:
- self.new.SetMax(val)
- self.new.SetValue(val)
+ if self.convert(self.new['from']) > val:
+ self.new.configure(from_=val)
+ if self.convert(self.new['to']) < val:
+ self.new.configure(to=val)
+ self.new.set(val)
def set_range(self, min, max):
- self.new.SetRange(min, max)
+ self.new.configure(from_=min, to=max)
def get_final(self):
- if self.edit.GetValue():
- return self.new.GetValue()
- else:
- return self.orig.GetLabel()
+ if len(self.new.get()) == 0:
+ return self.orig.get()
+ return self.new.get()
- def handle_edit(self, _event):
- self.new.Enable(self.edit.GetValue())
+ def handle_edit(self, *args):
+ if self.edit.get():
+ self.new.state(['!disabled'])
+ else:
+ self.new.state(['disabled'])
self.handle_change(None)
def on_change(self, callback):
self.handlers.append(callback)
- def handle_change(self, _event):
+ def handle_change(self, *args):
for handler in self.handlers:
handler()
-class OptionsPanel(wx.Panel):
+class OptionsPanel(ttk.LabelFrame):
"""
A Panel displaying ffmpeg options
"""
def __init__(self, *args, **kw):
- super(OptionsPanel, self).__init__(*args, **kw)
-
- root_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label="Options")
- self.SetSizer(root_sizer)
- main = wx.Panel(self)
- root_sizer.Add(main, proportion=1, flag=wx.EXPAND, border=5)
- main_sizer = wx.GridBagSizer(5, 5)
- main.SetSizer(main_sizer)
-
- def make_header(text):
- st = wx.StaticText(main, label=text, style=wx.ALIGN_CENTER_HORIZONTAL)
- st.SetFont(st.GetFont().Bold())
- return st
-
- main_sizer.Add(make_header("Field"), wx.GBPosition(HEADERS_ROW, LABEL_COL), flag=wx.EXPAND)
- main_sizer.Add(make_header("Original Value"), wx.GBPosition(HEADERS_ROW, ORIG_COL), flag=wx.EXPAND)
- main_sizer.Add(make_header("Edit?"), wx.GBPosition(HEADERS_ROW, EDIT_BOX_COL), flag=wx.EXPAND)
- main_sizer.Add(make_header("New Value"), wx.GBPosition(HEADERS_ROW, NEW_COL), flag=wx.EXPAND)
-
- self.start_time = Property(main, "Start time (seconds)", new_class=wx.SpinCtrlDouble)
- self.start_time.add_to(main_sizer, START_ROW)
+ super(OptionsPanel, self).__init__(*args, text="Options", **kw)
+
+ def place_header(text, **kwargs):
+ ttk.Label(self, text=text, font='TkHeadingFont', justify='center', anchor='center'
+ ).grid(sticky=(E, W), **kwargs)
+
+ place_header("Field", column=LABEL_COL, row=HEADERS_ROW)
+ place_header("Original Value", column=ORIG_COL, row=HEADERS_ROW)
+ place_header("Edit?", column=EDIT_BOX_COL, row=HEADERS_ROW)
+ place_header("New Value", column=NEW_COL, row=HEADERS_ROW)
+
+ self.start_time = Property(self, "Start time (seconds)", START_ROW, float)
self.start_time.on_change(self.enforce_constraints)
- self.end_time = Property(main, "End time (seconds)", new_class=wx.SpinCtrlDouble)
- self.end_time.add_to(main_sizer, END_ROW)
+ self.end_time = Property(self, "End time (seconds)", END_ROW, float)
self.end_time.on_change(self.enforce_constraints)
- self.duration = Property(main, "Duration (seconds)", new_class=wx.SpinCtrlDouble)
- self.duration.add_to(main_sizer, DURATION_ROW)
+ self.duration = Property(self, "Duration (seconds)", DURATION_ROW, float)
self.duration.on_change(self.enforce_constraints)
- self.width = Property(main, "Width", new_class=wx.SpinCtrl)
- self.width.add_to(main_sizer, WIDTH_ROW)
+ self.width = Property(self, "Width", WIDTH_ROW, int)
self.width.on_change(self.enforce_constraints)
- self.height = Property(main, "Height", new_class=wx.SpinCtrl)
- self.height.add_to(main_sizer, HEIGHT_ROW)
+ self.height = Property(self, "Height", HEIGHT_ROW, int)
self.height.on_change(self.enforce_constraints)
- self.framerate = Property(main, "Framerate", new_class=wx.SpinCtrlDouble)
- self.framerate.add_to(main_sizer, FRAMERATE_ROW)
+ self.framerate = Property(self, "Framerate", FRAMERATE_ROW, float)
self.framerate.on_change(self.enforce_constraints)
- self.Disable()
+ for child in self.winfo_children():
+ child.grid_configure(padx=2, pady=2)
+
+ self.state(['disabled'])
def enforce_constraints(self):
self.start_time.enable()
@@ -211,11 +191,13 @@ class OptionsPanel(wx.Panel):
self.framerate.set_range(0, orig_framerate)
self.framerate.set_calc_new(orig_framerate)
- def update(self, info):
+ def update_info(self, info):
import fractions
if info is None:
- self.Disable()
+ self.state(['disabled'])
+ for prop in [self.start_time, self.duration, self.end_time, self.width, self.height, self.framerate]:
+ prop.disable()
else:
start_time = float(info['format']['start_time'])
self.start_time.set_orig(start_time)
@@ -229,18 +211,20 @@ class OptionsPanel(wx.Panel):
stream['codec_type'] == 'video' and stream['avg_frame_rate'] != '0/0']
if len(video_streams) > 0:
video_stream = video_streams[0]
+ self.width.enable()
self.width.set_orig(video_stream['width'])
+ self.height.enable()
self.height.set_orig(video_stream['height'])
framerate = round(float(fractions.Fraction(video_stream['avg_frame_rate'])), 3)
+ self.framerate.enable()
self.framerate.set_orig(framerate)
else:
self.width.disable()
self.height.disable()
self.framerate.disable()
- self.Enable()
- self.Layout()
+ self.state(['!disabled'])
self.enforce_constraints()
def ffmpeg_opts(self):
@@ -272,3 +256,6 @@ class OptionsPanel(wx.Panel):
output_opts += ['-r', str(self.framerate.get_final())]
return FFmpegOptions(input_opts, output_opts)
+
+ def frame_count(self):
+ return float(self.duration.get_final()) * float(self.framerate.get_final())
diff --git a/output.py b/output.py
index 2c765ca..5aa6084 100644
--- a/output.py
+++ b/output.py
@@ -1,87 +1,100 @@
import os
import subprocess
import threading
-
-import wx
+from tkinter import *
+from tkinter import filedialog
+from tkinter import ttk
from options import FFmpegOptions
-class OutputPanel(wx.Panel):
- def __init__(self, *args, get_ffmpeg_args=lambda: FFmpegOptions([], []), **kw):
- super(OutputPanel, self).__init__(*args, **kw)
+class OutputPanel(ttk.LabelFrame):
+ def __init__(self, *args, get_ffmpeg_args=lambda: FFmpegOptions([], []), get_frame_count=lambda: 0, **kw):
+ super(OutputPanel, self).__init__(*args, text='Output', **kw)
self.update_listeners = []
self.input_path = None
self.get_ffmpeg_args = get_ffmpeg_args
+ self.get_frame_count = get_frame_count
+
+ ttk.Label(self, text="File").grid(column=0, row=0, sticky=W)
+ self.file_text = StringVar(self)
+ ttk.Entry(self, textvariable=self.file_text, width=30).grid(column=1, row=0, columnspan=2, sticky=(E, W))
+ self.columnconfigure(1, weight=1)
+ self.columnconfigure(2, weight=1)
+ self.file_text.trace_add("write", self.handle_file_changed)
+ ttk.Button(self, text="Browse", command=self.handle_file_browse_pressed).grid(column=3, row=0, sticky=E)
+
+ self.silence = BooleanVar(self)
+ ttk.Checkbutton(self, text="Silence", variable=self.silence, onvalue=True, offvalue=False
+ ).grid(column=0, row=1, columnspan=2, sticky=W)
+ self.deepfry = BooleanVar(self)
+ ttk.Checkbutton(self, text="Compress beyond recognition", variable=self.deepfry, onvalue=True, offvalue=False
+ ).grid(column=2, row=1, columnspan=2, sticky=W)
+
+ run_button = ttk.Button(self, text="Run", command=self.handle_run_pressed)
+ run_button.grid(column=0, row=2, sticky=(E, W))
+ run_preview_button = ttk.Button(self, text="Run & Preview", command=self.handle_run_preview_pressed)
+ run_preview_button.grid(column=1, row=2, columnspan=2, sticky=(E, W))
+ run_quit_button = ttk.Button(self, text="Run & Quit", command=self.handle_run_quit_pressed)
+ run_quit_button.grid(column=3, row=2, sticky=(E, W))
+
+ self.progress = ttk.Progressbar(self, orient=HORIZONTAL, mode='determinate')
+ self.progress.grid(column=0, row=3, columnspan=4, sticky=(N, S, E, W))
+
+ self.logs = StringVar(self)
+ logs_widget = ttk.Label(self, textvariable=self.logs, font="TkFixedFont",
+ justify='left')
+ logs_widget.grid(column=0, row=4, columnspan=4, sticky=(N, S, E, W))
+ self.rowconfigure(4, weight=1)
+
+ for child in self.winfo_children():
+ child.grid_configure(padx=2, pady=2)
+
+ self.enable(False)
+
+ def enable(self, enabled, run_enabled=None):
+ if run_enabled is None:
+ run_enabled = enabled
+ state = 'disabled'
+ if enabled:
+ state = '!' + state
+ run_state = 'disabled'
+ if run_enabled:
+ run_state = '!' + run_state
+ self.state([state])
+ for child in self.winfo_children():
+ if 'text' in child.configure() and child['text'].startswith('Run'):
+ child.state([run_state])
+ else:
+ child.state([state])
- root_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label="Output")
- self.SetSizer(root_sizer)
-
- file_panel = wx.Panel(self)
- root_sizer.Add(file_panel, flag=wx.EXPAND, border=5)
- file_sizer = wx.BoxSizer(wx.HORIZONTAL)
- file_panel.SetSizer(file_sizer)
- file_sizer.Add(wx.StaticText(file_panel, label="File"), flag=wx.EXPAND, border=5)
- self.file_text = wx.TextCtrl(file_panel)
- self.file_text.Bind(wx.EVT_TEXT, self.handle_file_changed)
- file_sizer.Add(self.file_text, proportion=1, flag=wx.EXPAND, border=5)
- self.file_browse_button = wx.Button(file_panel, label="Browse")
- self.file_browse_button.Bind(wx.EVT_BUTTON, self.handle_file_browse_pressed)
- file_sizer.Add(self.file_browse_button, flag=wx.EXPAND, border=5)
-
- options_panel = wx.Panel(self)
- root_sizer.Add(options_panel, flag=wx.EXPAND, border=5)
- options_sizer = wx.BoxSizer(wx.HORIZONTAL)
- options_panel.SetSizer(options_sizer)
- self.silence = wx.CheckBox(options_panel, label="Silence")
- options_sizer.Add(self.silence, proportion=1, flag=wx.EXPAND, border=5)
- self.deepfry = wx.CheckBox(options_panel, label="Compress beyond recognition")
- options_sizer.Add(self.deepfry, proportion=1, flag=wx.EXPAND, border=5)
-
- self.run_panel = wx.Panel(self)
- root_sizer.Add(self.run_panel, flag=wx.EXPAND, border=5)
- run_sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.run_panel.SetSizer(run_sizer)
- run_button = wx.Button(self.run_panel, label="Run")
- run_button.Bind(wx.EVT_BUTTON, self.handle_run_pressed)
- run_sizer.Add(run_button, proportion=1, flag=wx.EXPAND, border=5)
- run_preview_button = wx.Button(self.run_panel, label="Run && Preview")
- run_preview_button.Bind(wx.EVT_BUTTON, self.handle_run_preview_pressed)
- run_sizer.Add(run_preview_button, proportion=1, flag=wx.EXPAND, border=5)
- run_quit_button = wx.Button(self.run_panel, label="Run && Quit")
- run_quit_button.Bind(wx.EVT_BUTTON, self.handle_run_quit_pressed)
- run_sizer.Add(run_quit_button, proportion=1, flag=wx.EXPAND, border=5)
- self.run_panel.Disable()
-
- self.logs = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY)
- root_sizer.Add(self.logs, proportion=1, flag=wx.EXPAND, border=5)
-
- self.Disable()
-
- def handle_file_changed(self, _):
- path = self.file_text.GetValue()
+ def handle_file_changed(self, *args):
+ path = self.file_text.get()
(folder, name) = os.path.split(path)
try:
os.stat(folder)
- self.run_panel.Enable()
+ self.enable(True)
except FileNotFoundError:
- self.run_panel.Disable()
+ self.enable(True, False)
- def handle_file_browse_pressed(self, _):
- dialog = wx.FileDialog(self, style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
- if dialog.ShowModal() == wx.ID_OK:
- self.file_text.SetValue(dialog.GetPath())
+ def handle_file_browse_pressed(self, *args):
+ filename = filedialog.asksaveasfilename(parent=self)
+ if filename != '':
+ self.file_text.set(filename)
- def handle_run_pressed(self, _, callback=lambda _: None):
- self.logs.Clear()
- self.run_panel.Disable()
+ def handle_run_pressed(self, *args, callback=lambda _: None):
+ self.logs.set('')
+ self.progress['value'] = 0
+ self.enable(False)
real_args = self.get_ffmpeg_args()
+ self.progress['maximum'] = float(self.get_frame_count())
+ print(self.get_frame_count())
input_args = real_args.input
output_args = real_args.output
- output_path = self.file_text.GetValue()
+ output_path = self.file_text.get()
(folder, name) = os.path.split(output_path)
(name, ext) = os.path.splitext(name)
- if self.silence.GetValue():
+ if self.silence.get():
output_args += ['-an']
if ext == '.gif':
filter_before = '[0:v] '
@@ -95,12 +108,14 @@ class OutputPanel(wx.Panel):
output_args[i:i + 2] = []
break
output_args += ['-filter_complex', filter_before + filter_during + filter_after]
- if self.deepfry.GetValue():
+ if self.deepfry.get():
if ext == '.mp3':
output_args += ['-q:a', '9']
else:
output_args += ['-q:a', '0.1', '-crf', '51']
- args = ['ffmpeg', '-hide_banner', '-y'] + input_args + ['-i', self.input_path] + output_args + [output_path]
+ args = ['ffmpeg', '-hide_banner', '-v', 'warning', '-stats', '-y'] + input_args + ['-i',
+ self.input_path] + output_args + [
+ output_path]
print(args)
def run():
@@ -110,35 +125,36 @@ class OutputPanel(wx.Panel):
while proc.poll() is None:
out_data = proc.stdout.readline()
if out_data != '':
- wx.CallAfter(lambda: self.add_log(out_data))
- wx.CallAfter(lambda: self.run_panel.Enable())
- wx.CallAfter(lambda: callback(proc.returncode))
+ progress_data = re.match(r'^frame=\s*(\d+)', out_data)
+ print(out_data, end='')
+ if progress_data is not None:
+ self.progress['value'] = float(progress_data.group(1))
+ else:
+ self.logs.set(self.logs.get() + out_data)
+ self.enable(True)
+ callback(proc.returncode)
threading.Thread(target=run).start()
- def handle_run_preview_pressed(self, _event):
+ def handle_run_preview_pressed(self, *args):
def preview(code):
if code == 0:
- out_file = self.file_text.GetValue()
+ out_file = self.file_text.get()
subprocess.Popen(['ffplay', '-autoexit', out_file], creationflags=subprocess.CREATE_NO_WINDOW)
- self.handle_run_pressed(_event, callback=preview)
+ self.handle_run_pressed(*args, callback=preview)
- def handle_run_quit_pressed(self, _event):
+ def handle_run_quit_pressed(self, *args):
def quit(code):
if code == 0:
- parent = self.GetTopLevelParent()
- parent.Close(True)
+ toplevel = self.winfo_toplevel()
+ toplevel.destroy()
- self.handle_run_pressed(_event, callback=quit)
+ self.handle_run_pressed(*args, callback=quit)
def set_input_path(self, path, data):
+ self.enable(data is not None)
if data is None:
self.input_path = None
- self.Disable()
else:
self.input_path = path
- self.Enable()
-
- def add_log(self, data):
- self.logs.AppendText(data)
diff --git a/requirements.txt b/requirements.txt
index 5767b25..c95cfcb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1 @@
-wxPython
cx_Freeze
diff --git a/screenshot.png b/screenshot.png
index d6d0e15..bb8aeb5 100644
--- a/screenshot.png
+++ b/screenshot.png
Binary files differ
diff --git a/sources.py b/sources.py
index d4fab2f..180e51c 100644
--- a/sources.py
+++ b/sources.py
@@ -4,8 +4,10 @@ import os
import subprocess
import tempfile
import threading
-
-import wx
+from tkinter import *
+from tkinter import filedialog
+from tkinter import messagebox
+from tkinter import ttk
def has_ytdl():
@@ -17,7 +19,7 @@ def has_ytdl():
return False
-def update_ytdl(parent_win):
+def update_ytdl(root):
try:
youtube_dl_found = subprocess.run(['where', 'youtube-dl'], stdout=subprocess.PIPE, text=True,
creationflags=subprocess.CREATE_NO_WINDOW)
@@ -25,115 +27,108 @@ def update_ytdl(parent_win):
youtube_dl_found = subprocess.run(['which', 'youtube-dl'], stdout=subprocess.PIPE, text=True,
creationflags=subprocess.CREATE_NO_WINDOW)
if youtube_dl_found.returncode != 0:
- def poll():
- answer = wx.MessageBox("Could not find youtube-dl. Open vidslice README?", "Error", wx.YES_NO, parent_win)
- if answer == wx.YES:
- import webbrowser
- webbrowser.open("https://github.com/boringcactus/vidslice/blob/master/README.md")
- return
-
- wx.CallAfter(poll)
+ answer = messagebox.askyesno(message="Could not find youtube-dl. Open vidslice README?", title="Error",
+ icon='error', parent=root)
+ if answer:
+ import webbrowser
+ webbrowser.open("https://github.com/boringcactus/vidslice/blob/master/README.md")
youtube_dl_path = youtube_dl_found.stdout.split("\n")[0]
old_mtime = os.stat(youtube_dl_path).st_mtime
- proc = subprocess.run(["youtube-dl", "-U"], stdout=subprocess.PIPE, text=True,
+ proc = subprocess.run(["youtube-dl", "-U"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
creationflags=subprocess.CREATE_NO_WINDOW)
- if not proc.stdout.startswith("youtube-dl is up-to-date"):
+ if not proc.stdout.startswith("youtube-dl is up-to-date") and not proc.stdout.startswith("ERROR"):
while os.stat(youtube_dl_path).st_mtime == old_mtime:
from time import sleep
sleep(0.25)
- wx.CallAfter(lambda: wx.MessageBox("Updated youtube-dl successfully", "Complete", wx.OK, parent_win))
+ messagebox.showinfo(message="Updated youtube-dl successfully", title="Complete", parent=root)
-class SourcesPanel(wx.Panel):
+class SourcesPanel(ttk.LabelFrame):
"""
A Panel representing source info
"""
def __init__(self, *args, **kw):
- super(SourcesPanel, self).__init__(*args, **kw)
+ super(SourcesPanel, self).__init__(*args, text='Sources', **kw)
self.update_listeners = []
- root_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label="Sources")
- self.SetSizer(root_sizer)
- main = wx.Panel(self)
- root_sizer.Add(main, proportion=1, flag=wx.EXPAND, border=5)
- main_sizer = wx.GridBagSizer(5, 5)
- main.SetSizer(main_sizer)
-
if has_ytdl():
- main_sizer.Add(wx.StaticText(main, label="URL"), wx.GBPosition(0, 0), flag=wx.EXPAND)
- self.url_text = wx.TextCtrl(main)
- main_sizer.Add(self.url_text, wx.GBPosition(0, 1), flag=wx.EXPAND)
- self.url_download_button = wx.Button(main, label="Download")
- self.url_download_button.Bind(wx.EVT_BUTTON, self.handle_url_download_pressed)
- main_sizer.Add(self.url_download_button, wx.GBPosition(0, 2), flag=wx.EXPAND)
+ ttk.Label(self, text="URL").grid(column=0, row=0, sticky=(E, W))
+ self.url_text = StringVar(self)
+ ttk.Entry(self, textvariable=self.url_text).grid(column=1, row=0, sticky=(E, W))
+ ttk.Button(self, text="Download", command=self.handle_url_download_pressed
+ ).grid(column=2, row=0, sticky=(E, W))
else:
- no_ytdl_label = wx.StaticText(main, label="Could not find youtube-dl, can't download videos automatically")
- main_sizer.Add(no_ytdl_label, wx.GBPosition(0, 0), wx.GBSpan(1, 3), flag=wx.EXPAND)
+ ttk.Label(self, text="Could not find youtube-dl, can't download videos automatically"
+ ).grid(column=0, row=0, columnspan=3, sticky=(E, W))
+ self.url_text = None
- main_sizer.Add(wx.StaticText(main, label="File"), wx.GBPosition(1, 0), flag=wx.EXPAND)
- self.file_text = wx.TextCtrl(main)
- self.file_text.Bind(wx.EVT_TEXT, self.handle_file_changed)
- main_sizer.Add(self.file_text, wx.GBPosition(1, 1), flag=wx.EXPAND)
- self.file_browse_button = wx.Button(main, label="Browse")
- self.file_browse_button.Bind(wx.EVT_BUTTON, self.handle_file_browse_pressed)
- main_sizer.Add(self.file_browse_button, wx.GBPosition(1, 2), flag=wx.EXPAND)
+ ttk.Label(self, text="File").grid(column=0, row=1, sticky=(E, W))
+ self.file_text = StringVar(self)
+ self.file_text.trace_add("write", self.handle_file_changed)
+ ttk.Entry(self, textvariable=self.file_text).grid(column=1, row=1, sticky=(E, W))
+ self.columnconfigure(1, weight=1)
+ ttk.Button(self, text="Browse", command=self.handle_file_browse_pressed).grid(column=2, row=1, sticky=(E, W))
- self.status_label = wx.StaticText(main, label="Status: Select a file")
- main_sizer.Add(self.status_label, wx.GBPosition(2, 0), wx.GBSpan(1, 3))
+ self.status_label = StringVar(self, "Status: Select a file")
+ ttk.Label(self, textvariable=self.status_label).grid(column=0, row=2, columnspan=3, sticky=(E, W))
- main_sizer.AddGrowableCol(1, proportion=1)
+ for child in self.winfo_children():
+ child.grid_configure(padx=2, pady=2)
def set_status(self, text):
- self.status_label.SetLabel("Status: " + text)
+ self.status_label.set("Status: " + text)
- def handle_url_download_pressed(self, _):
+ def handle_url_download_pressed(self, *args):
self.set_status("Downloading...")
def download():
file = tempfile.NamedTemporaryFile(delete=False)
# noinspection PyArgumentList
proc = subprocess.Popen([
- 'youtube-dl', '-o', file.name + '.%(ext)s', self.url_text.GetValue()
+ 'youtube-dl', '-o', file.name + '.%(ext)s', self.url_text.get()
], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
creationflags=subprocess.CREATE_NO_WINDOW)
while proc.poll() is None:
out_data = proc.stdout.readline()
if out_data != '':
- wx.CallAfter(lambda: self.set_status("Downloading: " + out_data.strip()))
+ self.set_status("Downloading: " + out_data.strip())
if proc.returncode == 0:
output_file = glob.glob(glob.escape(file.name) + '.*')[0]
- wx.CallAfter(lambda: self.set_status("Downloaded!"))
- wx.CallAfter(lambda: self.file_text.SetValue(output_file))
+ self.set_status("Downloaded!")
+ self.file_text.set(output_file)
else:
error = ''.join(proc.stderr.readlines()).strip()
- wx.CallAfter(lambda: self.set_status("Couldn't download: " + error))
+ self.set_status("Couldn't download: " + error)
threading.Thread(target=download).start()
- def handle_file_browse_pressed(self, _):
- dialog = wx.FileDialog(self, style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
- if dialog.ShowModal() == wx.ID_OK:
- self.file_text.SetValue(dialog.GetPath())
-
- def handle_file_changed(self, _event):
- result = subprocess.run([
- 'ffprobe', '-v', 'error', '-of', 'json',
- '-show_entries', 'format=start_time,duration:stream=index,codec_type,avg_frame_rate,width,height',
- self.file_text.GetValue()
- ], capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW)
- if result.returncode == 0:
- ffprobe_data = json.loads(result.stdout)
- self.set_status("Successfully loaded media info")
- for listener in self.update_listeners:
- listener(ffprobe_data)
- else:
- self.set_status("Failed to load media info: " + result.stderr)
- for listener in self.update_listeners:
- listener(None)
+ def handle_file_browse_pressed(self, *args):
+ filename = filedialog.askopenfilename(parent=self)
+ if filename != '':
+ self.file_text.set(filename)
+
+ def handle_file_changed(self, *args):
+ def probe():
+ result = subprocess.run([
+ 'ffprobe', '-v', 'error', '-of', 'json',
+ '-show_entries', 'format=start_time,duration:stream=index,codec_type,avg_frame_rate,width,height',
+ self.file_text.get()
+ ], capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0:
+ ffprobe_data = json.loads(result.stdout)
+ self.set_status("Successfully loaded media info")
+ for listener in self.update_listeners:
+ listener(ffprobe_data)
+ else:
+ self.set_status("Failed to load media info: " + result.stderr)
+ for listener in self.update_listeners:
+ listener(None)
+
+ threading.Thread(target=probe).start()
def on_update(self, callback):
self.update_listeners.append(callback)
def get_file(self):
- return self.file_text.GetValue()
+ return self.file_text.get()
diff --git a/vidslice.py b/vidslice.py
index dc37412..632e426 100644
--- a/vidslice.py
+++ b/vidslice.py
@@ -1,10 +1,9 @@
import json
import subprocess
-import sys
import urllib.request
-
-import wx
-import wx.adv
+from tkinter import *
+from tkinter import messagebox
+from tkinter import ttk
from options import OptionsPanel
from output import OutputPanel
@@ -19,8 +18,9 @@ def check_update(parent):
latest_release_obj = json.load(latest_release_response)
newest_version = latest_release_obj['tag_name'].lstrip('v')
if VERSION != newest_version:
- answer = wx.MessageBox("vidslice update available. download?", "Update", wx.YES_NO, parent)
- if answer == wx.YES:
+ open_update = messagebox.askyesno(message="vidslice update available. download?", title="Update",
+ parent=parent)
+ if open_update:
import webbrowser
webbrowser.open("https://github.com/boringcactus/vidslice/releases/latest")
@@ -35,118 +35,84 @@ def has_ffmpeg():
return False
-class VidsliceFrame(wx.Frame):
- """
- A Frame that contains vidslice logic
- """
-
- def __init__(self, *args, **kw):
- # ensure the parent's __init__ is called
- super(VidsliceFrame, self).__init__(*args, **kw)
+class VidsliceFrame:
+ def __init__(self, root: Tk):
+ self.root = root
+ root.title('vidslice')
- root_sizer = wx.BoxSizer(wx.VERTICAL)
- self.SetSizer(root_sizer)
- main = wx.Panel(self)
- root_sizer.Add(main, proportion=1, flag=wx.EXPAND, border=5)
- main_sizer = wx.GridBagSizer(5, 5)
- main.SetSizer(main_sizer)
+ mainframe = ttk.Frame(root, padding="3 3 12 12")
+ mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
+ root.columnconfigure(0, weight=1)
+ root.rowconfigure(0, weight=1)
# set up sources panel
- self.sources_panel = SourcesPanel(main)
- main_sizer.Add(self.sources_panel, wx.GBPosition(0, 0), wx.GBSpan(1, 2), flag=wx.EXPAND)
+ self.sources_panel = SourcesPanel(mainframe)
+ self.sources_panel.grid(column=0, row=0, columnspan=2, sticky=(W, E), padx=5, pady=5)
# set up options panel
- self.options_panel = OptionsPanel(main)
- main_sizer.Add(self.options_panel, wx.GBPosition(1, 0), flag=wx.EXPAND)
- main_sizer.AddGrowableRow(1, proportion=1)
- self.sources_panel.on_update(self.options_panel.update)
+ self.options_panel = OptionsPanel(mainframe)
+ self.options_panel.grid(column=0, row=1, sticky=(W, E, N), padx=5, pady=5)
+ mainframe.rowconfigure(1, weight=1)
+ self.sources_panel.on_update(self.options_panel.update_info)
# set up output panel
- self.output_panel = OutputPanel(main, get_ffmpeg_args=self.options_panel.ffmpeg_opts)
- main_sizer.Add(self.output_panel, wx.GBPosition(1, 1), flag=wx.EXPAND)
- main_sizer.AddGrowableCol(1, proportion=1)
+ self.output_panel = OutputPanel(mainframe, get_ffmpeg_args=self.options_panel.ffmpeg_opts,
+ get_frame_count=self.options_panel.frame_count)
+ self.output_panel.grid(column=1, row=1, sticky=(W, E, N, S), padx=5, pady=5)
+ mainframe.columnconfigure(1, weight=1)
self.sources_panel.on_update(lambda data: self.output_panel.set_input_path(self.sources_panel.get_file(), data))
# create a menu bar
- self.make_menu_bar()
-
- size = root_sizer.GetMinSize()
- self.SetMinClientSize(size)
+ self.make_menu_bar(root)
if len(sys.argv) > 1:
self.sources_panel.file_text.SetValue(sys.argv[1])
- def make_menu_bar(self):
- """
- A menu bar is composed of menus, which are composed of menu items.
- This method builds a set of menus and binds handlers to be called
- when the menu item is selected.
- """
-
- # Make a file menu with Hello and Exit items
- file_menu = wx.Menu()
- # The "\t..." syntax defines an accelerator key that also triggers
- # the same event
- update_item = file_menu.Append(-1, "Update youtube-dl")
- file_menu.AppendSeparator()
- # When using a stock ID we don't need to specify the menu item's
- # label
- exit_item = file_menu.Append(wx.ID_EXIT)
-
- # Now a help menu for the about item
- help_menu = wx.Menu()
- about_item = help_menu.Append(wx.ID_ABOUT)
-
- # Make the menu bar and add the two menus to it. The '&' defines
- # that the next letter is the "mnemonic" for the menu item. On the
- # platforms that support it those letters are underlined and can be
- # triggered from the keyboard.
- menu_bar = wx.MenuBar()
- menu_bar.Append(file_menu, "&File")
- menu_bar.Append(help_menu, "&Help")
-
- # Give the menu bar to the frame
- self.SetMenuBar(menu_bar)
-
- # Finally, associate a handler function with the EVT_MENU event for
- # each of the menu items. That means that when that menu item is
- # activated then the associated handler function will be called.
- self.Bind(wx.EVT_MENU, self.on_update, update_item)
- self.Bind(wx.EVT_MENU, self.on_exit, exit_item)
- self.Bind(wx.EVT_MENU, self.on_about, about_item)
-
- def on_exit(self, event):
- """Close the frame, terminating the application."""
- self.Close(True)
-
- def on_update(self, event):
- import threading
- threading.Thread(target=update_ytdl, args=(self,)).start()
+ def make_menu_bar(self, root):
+ root.option_add('*tearOff', FALSE)
- def on_about(self, event):
- """Display an About Dialog"""
- info = wx.adv.AboutDialogInfo()
- info.SetName("vidslice")
- info.SetVersion(VERSION)
- info.SetDescription("video manipulator wrapping youtube-dl and ffmpeg")
- info.SetWebSite("https://github.com/boringcactus/vidslice")
+ menubar = Menu(root)
+ root['menu'] = menubar
- wx.adv.AboutBox(info)
+ file_menu = Menu(menubar)
+ menubar.add_cascade(menu=file_menu, label='File', underline=0)
+ file_menu.add_command(label="Update youtube-dl", command=self.on_update, underline=0)
+ file_menu.add_separator()
+ file_menu.add_command(label='Exit', command=self.on_exit, underline=1)
+ help_menu = Menu(menubar)
+ menubar.add_cascade(menu=help_menu, label='Help', underline=0)
+ help_menu.add_command(label='About', command=self.on_about, underline=0)
-if __name__ == '__main__':
- # When this module is run (not imported) then create the app, the
- # frame, show it, and start the event loop.
- app = wx.App()
+ def on_exit(self, *args):
+ self.root.destroy()
+
+ def on_update(self, *args):
+ import threading
+ threading.Thread(target=update_ytdl, args=(self.root,)).start()
+
+ def on_about(self, *args):
+ messagebox.showinfo(message=f"vidslice {VERSION}")
+
+
+def check_ffmpeg(root: Tk):
if not has_ffmpeg():
- answer = wx.MessageBox("Could not find ffmpeg. Open vidslice README?", "Error", wx.YES_NO, None)
- if answer == wx.YES:
+ open_readme = messagebox.askyesno(message="Could not find ffmpeg. Open vidslice README?", title="Error",
+ icon='error', parent=root)
+ if open_readme:
import webbrowser
webbrowser.open("https://github.com/boringcactus/vidslice/blob/master/README.md")
- else:
- frm = VidsliceFrame(None, title='vidslice')
- app.SetTopWindow(frm)
- frm.Show()
- check_update(frm)
- app.MainLoop()
+ root.after(1000, root.destroy)
+
+
+def main():
+ root = Tk()
+ frame = VidsliceFrame(root)
+ root.after_idle(check_ffmpeg, root)
+ root.after(1000, check_update, root)
+ root.mainloop()
+
+
+if __name__ == '__main__':
+ main()