Added uploads, part renaming, bulk data import acceptance
This commit is contained in:
178
ui/app_tk.py
178
ui/app_tk.py
@@ -15,10 +15,175 @@ from apis.digikey_api import suggest_category_from_digikey
|
||||
# ------------ Pages ------------
|
||||
|
||||
class HomePage(ttk.Frame):
|
||||
"""Always listening; on scan -> ask Part-DB; route to View or Create."""
|
||||
"""Home page with tools and settings."""
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master)
|
||||
self.app = app
|
||||
|
||||
ttk.Label(self, text="Part-DB Helper", font=("Segoe UI", 16, "bold")).pack(pady=(18,8))
|
||||
|
||||
# Settings section
|
||||
settings_frame = ttk.LabelFrame(self, text="Settings", padding=10)
|
||||
settings_frame.pack(fill="x", padx=20, pady=(0,10))
|
||||
|
||||
# Mode toggle (Live/Dry Run)
|
||||
mode_frame = ttk.Frame(settings_frame)
|
||||
mode_frame.pack(fill="x", pady=2)
|
||||
ttk.Label(mode_frame, text="Standardization Mode:", width=20).pack(side="left")
|
||||
self.mode_var = tk.StringVar(value="live")
|
||||
ttk.Radiobutton(mode_frame, text="Live (Make Changes)", variable=self.mode_var, value="live").pack(side="left", padx=5)
|
||||
ttk.Radiobutton(mode_frame, text="Dry Run (Preview Only)", variable=self.mode_var, value="dry").pack(side="left", padx=5)
|
||||
|
||||
# Provider update toggle
|
||||
provider_frame = ttk.Frame(settings_frame)
|
||||
provider_frame.pack(fill="x", pady=2)
|
||||
ttk.Label(provider_frame, text="Provider Updates:", width=20).pack(side="left")
|
||||
self.provider_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(provider_frame, text="Update from information providers (Digi-Key, etc.)", variable=self.provider_var).pack(side="left", padx=5)
|
||||
|
||||
# Tools section
|
||||
ttk.Separator(self, orient="horizontal").pack(fill="x", pady=10, padx=20)
|
||||
ttk.Label(self, text="Tools", font=("Segoe UI", 14, "bold")).pack(pady=(0,10))
|
||||
|
||||
tools_frame = ttk.Frame(self)
|
||||
tools_frame.pack(pady=(0,10))
|
||||
ttk.Button(tools_frame, text="Scanner", command=self.goto_scanner, width=25).pack(pady=4)
|
||||
ttk.Button(tools_frame, text="Accept Import Jobs", command=self.run_accept_jobs, width=25).pack(pady=4)
|
||||
ttk.Button(tools_frame, text="Run Bulk Add", command=self.run_bulk_add, width=25).pack(pady=4)
|
||||
ttk.Button(tools_frame, text="Import from CSV Files", command=self.run_import_csv, width=25).pack(pady=4)
|
||||
ttk.Button(tools_frame, text="Update Components", command=self.run_standardize_components, width=25).pack(pady=4)
|
||||
|
||||
def goto_scanner(self):
|
||||
"""Navigate to scanner page."""
|
||||
self.app.goto_scanner()
|
||||
|
||||
def run_accept_jobs(self):
|
||||
"""Launch the accept import jobs automation in a new thread."""
|
||||
from workflows.accept_import_jobs import run_accept_import_jobs
|
||||
|
||||
def work():
|
||||
run_accept_import_jobs(auto_close=True)
|
||||
|
||||
t = threading.Thread(target=work, daemon=True)
|
||||
t.start()
|
||||
messagebox.showinfo("Started", "Import job acceptance automation started in browser.\nCheck the browser window for progress.")
|
||||
|
||||
def run_bulk_add(self):
|
||||
"""Launch the bulk add workflow."""
|
||||
from workflows.bulk_add import run_bulk_add
|
||||
|
||||
def work():
|
||||
run_bulk_add()
|
||||
|
||||
t = threading.Thread(target=work, daemon=True)
|
||||
t.start()
|
||||
messagebox.showinfo("Started", "Bulk add workflow started in browser.\nCheck the browser window for progress.")
|
||||
|
||||
def run_import_csv(self):
|
||||
"""Launch the CSV import workflow."""
|
||||
update_providers = self.provider_var.get()
|
||||
|
||||
from workflows.import_from_csv import run_import_from_csv
|
||||
from ui.progress_dialog import ProgressDialog
|
||||
|
||||
# Create progress dialog
|
||||
progress = ProgressDialog(self.app, "Importing from CSV")
|
||||
|
||||
def progress_callback(current, total, status):
|
||||
progress.update(current, total, status)
|
||||
return progress.is_cancelled()
|
||||
|
||||
def work():
|
||||
try:
|
||||
run_import_from_csv(update_providers=update_providers, progress_callback=progress_callback)
|
||||
finally:
|
||||
progress.close()
|
||||
# Show completion message
|
||||
self.app.after(0, lambda: messagebox.showinfo("Import Complete", "CSV import finished!\nCheck console for details."))
|
||||
|
||||
t = threading.Thread(target=work, daemon=True)
|
||||
t.start()
|
||||
|
||||
def run_standardize_components(self):
|
||||
"""Launch the component standardization workflow."""
|
||||
dry_run = (self.mode_var.get() == "dry")
|
||||
|
||||
from workflows.standardize_components import run_standardize_components
|
||||
from ui.progress_dialog import ProgressDialog
|
||||
|
||||
# Create progress dialog - self.app is the Tk root
|
||||
progress = ProgressDialog(self.app, "Standardizing Components")
|
||||
|
||||
def progress_callback(current, total, status):
|
||||
progress.update(current, total, status)
|
||||
return progress.is_cancelled()
|
||||
|
||||
def work():
|
||||
try:
|
||||
run_standardize_components(dry_run=dry_run, progress_callback=progress_callback)
|
||||
finally:
|
||||
progress.close()
|
||||
|
||||
t = threading.Thread(target=work, daemon=True)
|
||||
t.start()
|
||||
|
||||
def run_standardize_passives(self):
|
||||
"""Launch the passive components standardization workflow."""
|
||||
dry_run = (self.mode_var.get() == "dry")
|
||||
|
||||
from workflows.standardize_passives import run_standardize_passives
|
||||
from ui.progress_dialog import ProgressDialog
|
||||
|
||||
# Create progress dialog - self.app is the Tk root
|
||||
progress = ProgressDialog(self.app, "Standardizing Passives")
|
||||
|
||||
def progress_callback(current, total, status):
|
||||
progress.update(current, total, status)
|
||||
return progress.is_cancelled()
|
||||
|
||||
def work():
|
||||
try:
|
||||
run_standardize_passives(category_name="Passives", dry_run=dry_run, progress_callback=progress_callback)
|
||||
finally:
|
||||
progress.close()
|
||||
|
||||
t = threading.Thread(target=work, daemon=True)
|
||||
t.start()
|
||||
|
||||
def run_standardize_asdmb(self):
|
||||
"""Launch the ASDMB crystal standardization workflow."""
|
||||
dry_run = (self.mode_var.get() == "dry")
|
||||
provider_update = self.provider_var.get() and not dry_run
|
||||
|
||||
from workflows.standardize_asdmb import run_standardize_asdmb
|
||||
from ui.progress_dialog import ProgressDialog
|
||||
|
||||
# Create progress dialog - self.app is the Tk root
|
||||
progress = ProgressDialog(self.app, "Standardizing ASDMB Crystals")
|
||||
|
||||
def progress_callback(current, total, status):
|
||||
progress.update(current, total, status)
|
||||
return progress.is_cancelled()
|
||||
|
||||
def work():
|
||||
try:
|
||||
run_standardize_asdmb(category_name="Clock - ASDMB", dry_run=dry_run, update_providers=provider_update, progress_callback=progress_callback)
|
||||
finally:
|
||||
progress.close()
|
||||
|
||||
t = threading.Thread(target=work, daemon=True)
|
||||
t.start()
|
||||
|
||||
|
||||
class ScannerPage(ttk.Frame):
|
||||
"""Scanner page for scanning Digi-Key labels."""
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master)
|
||||
self.app = app
|
||||
|
||||
# Back button
|
||||
ttk.Button(self, text="← Back to Home", command=self.app.goto_home).pack(anchor="w", padx=10, pady=10)
|
||||
|
||||
ttk.Label(self, text="Scan a Digi-Key code", font=("Segoe UI", 16, "bold")).pack(pady=(18,8))
|
||||
ttk.Label(self, text=f"Listening on {COM_PORT} @ {BAUD_RATE}").pack(pady=(0,12))
|
||||
|
||||
@@ -29,7 +194,7 @@ class HomePage(ttk.Frame):
|
||||
ttk.Entry(wrap, textvariable=self.last_var, state="readonly").pack(side="left", fill="x", expand=True, padx=(8,0))
|
||||
|
||||
ttk.Label(self, text="(This page listens continuously. Just scan another label.)", foreground="#666").pack()
|
||||
|
||||
|
||||
def on_scan(self, raw: str):
|
||||
self.last_var.set(raw)
|
||||
fields = parse_digikey(raw)
|
||||
@@ -50,6 +215,7 @@ class HomePage(ttk.Frame):
|
||||
# No part -> prepare creation
|
||||
self.app.goto_create(fields, raw)
|
||||
|
||||
|
||||
class ViewPage(ttk.Frame):
|
||||
"""Shows existing part summary + button to force Digi-Key provider update."""
|
||||
def __init__(self, master, app):
|
||||
@@ -275,10 +441,11 @@ class App(tk.Tk):
|
||||
self.pdb = PartDB(PARTDB_BASE, PARTDB_TOKEN)
|
||||
|
||||
self.home = HomePage(self, self)
|
||||
self.scanner = ScannerPage(self, self)
|
||||
self.view = ViewPage(self, self)
|
||||
self.create = CreatePage(self, self)
|
||||
|
||||
for f in (self.home, self.view, self.create):
|
||||
for f in (self.home, self.scanner, self.view, self.create):
|
||||
f.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
# start listening thread once
|
||||
@@ -290,10 +457,13 @@ class App(tk.Tk):
|
||||
def on_scan(self, flat: str):
|
||||
if self.busy:
|
||||
return # ignore scans during provider update
|
||||
self.home.on_scan(flat)
|
||||
self.scanner.on_scan(flat)
|
||||
|
||||
def goto_home(self):
|
||||
self.home.tkraise()
|
||||
|
||||
def goto_scanner(self):
|
||||
self.scanner.tkraise()
|
||||
|
||||
def goto_view(self, summary: dict):
|
||||
self.view.set_summary(summary)
|
||||
|
||||
127
ui/progress_dialog.py
Normal file
127
ui/progress_dialog.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Progress dialog with progress bar, ETA, and cancel button.
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class ProgressDialog:
|
||||
"""
|
||||
A modal progress dialog with progress bar, status text, ETA, and cancel button.
|
||||
Thread-safe for updating from background threads.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, title="Progress"):
|
||||
self.parent = parent
|
||||
self.cancelled = False
|
||||
self._start_time = time.time()
|
||||
|
||||
# Create dialog
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title(title)
|
||||
self.dialog.geometry("500x150")
|
||||
self.dialog.resizable(False, False)
|
||||
|
||||
# Make it modal
|
||||
self.dialog.transient(parent)
|
||||
self.dialog.grab_set()
|
||||
|
||||
# Center on parent
|
||||
self.dialog.update_idletasks()
|
||||
x = parent.winfo_x() + (parent.winfo_width() - 500) // 2
|
||||
y = parent.winfo_y() + (parent.winfo_height() - 150) // 2
|
||||
self.dialog.geometry(f"+{x}+{y}")
|
||||
|
||||
# Status label
|
||||
self.status_var = tk.StringVar(value="Starting...")
|
||||
ttk.Label(self.dialog, textvariable=self.status_var, font=("Segoe UI", 10)).pack(pady=(15, 5))
|
||||
|
||||
# Progress bar
|
||||
self.progress = ttk.Progressbar(self.dialog, length=450, mode='determinate')
|
||||
self.progress.pack(pady=(5, 5), padx=25)
|
||||
|
||||
# ETA label
|
||||
self.eta_var = tk.StringVar(value="Calculating...")
|
||||
ttk.Label(self.dialog, textvariable=self.eta_var, font=("Segoe UI", 9), foreground="#666").pack(pady=(0, 10))
|
||||
|
||||
# Cancel button
|
||||
self.cancel_btn = ttk.Button(self.dialog, text="Cancel", command=self.cancel)
|
||||
self.cancel_btn.pack(pady=(0, 15))
|
||||
|
||||
# Handle window close
|
||||
self.dialog.protocol("WM_DELETE_WINDOW", self.cancel)
|
||||
|
||||
def update(self, current, total, status_text=None):
|
||||
"""
|
||||
Update progress bar and ETA.
|
||||
Thread-safe - can be called from background threads.
|
||||
|
||||
Args:
|
||||
current: Current item number (0-based or 1-based)
|
||||
total: Total number of items
|
||||
status_text: Optional status text to display
|
||||
"""
|
||||
def _update():
|
||||
if self.cancelled:
|
||||
return
|
||||
|
||||
# Update progress bar
|
||||
if total > 0:
|
||||
percentage = (current / total) * 100
|
||||
self.progress['value'] = percentage
|
||||
|
||||
# Update status text
|
||||
if status_text:
|
||||
self.status_var.set(status_text)
|
||||
|
||||
# Calculate and update ETA
|
||||
if current > 0 and total > 0:
|
||||
elapsed = time.time() - self._start_time
|
||||
avg_time_per_item = elapsed / current
|
||||
remaining_items = total - current
|
||||
eta_seconds = remaining_items * avg_time_per_item
|
||||
|
||||
if eta_seconds < 60:
|
||||
eta_text = f"ETA: {int(eta_seconds)}s"
|
||||
elif eta_seconds < 3600:
|
||||
eta_text = f"ETA: {int(eta_seconds / 60)}m {int(eta_seconds % 60)}s"
|
||||
else:
|
||||
hours = int(eta_seconds / 3600)
|
||||
minutes = int((eta_seconds % 3600) / 60)
|
||||
eta_text = f"ETA: {hours}h {minutes}m"
|
||||
|
||||
self.eta_var.set(f"{current}/{total} items - {eta_text}")
|
||||
else:
|
||||
self.eta_var.set(f"{current}/{total} items")
|
||||
|
||||
# Schedule update on main thread
|
||||
if threading.current_thread() == threading.main_thread():
|
||||
_update()
|
||||
else:
|
||||
self.dialog.after(0, _update)
|
||||
|
||||
def cancel(self):
|
||||
"""Mark as cancelled and close dialog."""
|
||||
self.cancelled = True
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
"""Close the dialog."""
|
||||
def _close():
|
||||
try:
|
||||
self.dialog.grab_release()
|
||||
self.dialog.destroy()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Schedule on main thread
|
||||
if threading.current_thread() == threading.main_thread():
|
||||
_close()
|
||||
else:
|
||||
self.dialog.after(0, _close)
|
||||
|
||||
def is_cancelled(self):
|
||||
"""Check if user cancelled the operation."""
|
||||
return self.cancelled
|
||||
Reference in New Issue
Block a user