Added uploads, part renaming, bulk data import acceptance

This commit is contained in:
2025-12-17 13:57:47 +11:00
parent aaa1f7520a
commit ae9e1d6e7e
14 changed files with 3325 additions and 11 deletions

View File

@@ -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
View 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