fixed some UI, and temporarily did some better UX for µscan

This commit is contained in:
2025-09-13 16:47:14 +02:00
parent 8d81052d4c
commit cf55db126d
5 changed files with 579 additions and 121 deletions

View File

@@ -16,14 +16,26 @@ import queue
import numpy as np import numpy as np
import shutil import shutil
# @LocalImportStructure ; ref@todo/main.py , Element 3
from PfandApplication.wiki import main as wiki
from PfandApplication.pfand_scanner import launch_pfand_scanner
from PfandApplication.updater import open_updater as open_updater, run_silent_update
from PfandApplication.tgtg_orderchecker import main as tgtg
from PfandApplication.tgtg_orderchecker import setupkey as tgtg_kt
from PfandApplication.todo.main import todo_instance
if __name__ == "__main__":
try:
from wiki import main as wiki
from pfand_scanner import launch_pfand_scanner
from updater import open_updater as open_updater, run_silent_update
from tgtg_orderchecker import main as tgtg
from tgtg_orderchecker import setupkey as tgtg_kt
from todo.main import todo_instance
except ImportError:
print("tried to import without package name. failed, trying to import using package name now")
pass
else:
# @LocalImportStructure ; ref@todo/main.py , Element 3
from PfandApplication.wiki import main as wiki
from PfandApplication.pfand_scanner import launch_pfand_scanner
from PfandApplication.updater import open_updater as open_updater, run_silent_update
from PfandApplication.tgtg_orderchecker import main as tgtg
from PfandApplication.tgtg_orderchecker import setupkey as tgtg_kt
from PfandApplication.todo.main import todo_instance
class Achievement: class Achievement:
def __init__(self, title, description, condition_type, condition_value): def __init__(self, title, description, condition_type, condition_value):
@@ -523,7 +535,8 @@ class PfandCalculator:
about_window, text="Load Todo", command=self.create_todo_list about_window, text="Load Todo", command=self.create_todo_list
) )
) )
todo_button.grid(row=2, column=3, padx=10, pady=10, sticky="ew")
todo_button.grid(row=3, column=0, columnspan=2, padx=10, pady=10, sticky="ew")
# ref@todo/main.py # ref@todo/main.py
def create_todo_list(self): def create_todo_list(self):

View File

@@ -1,4 +1,4 @@
# µScan V2.2.2 # µScan V2.3.0 - Verbesserte Deutsche Version
import tkinter as tk import tkinter as tk
from tkinter import ttk, simpledialog, messagebox from tkinter import ttk, simpledialog, messagebox
import cv2 import cv2
@@ -10,28 +10,39 @@ import queue
import json import json
import os import os
import time import time
import numpy as np
class PfandScanner: class PfandScanner:
def __init__(self, window, window_title): def __init__(self, window, window_title):
self.window = window self.window = window
self.window.title(window_title) self.window.title(window_title)
self.window.geometry("1920x1080") self.window.geometry("1600x900")
self.window.minsize(960, 540) self.window.minsize(1200, 700)
self.window.configure(bg='#f0f0f0')
# Configure main window grid
self.window.columnconfigure(0, weight=1) self.window.columnconfigure(0, weight=1)
self.window.rowconfigure(0, weight=1) self.window.rowconfigure(0, weight=1)
# Style configuration
self.setup_styles()
self.data_file = "quantities.json" self.data_file = "quantities.json"
self.load_json() self.products_file = "products.json"
self.load_data()
self.barcode_times = {} self.barcode_times = {}
self.prompted_barcodes = set() self.prompted_barcodes = set()
self.current_barcodes = [] # Store current frame's barcodes for outlining
self.selected_device_index = tk.IntVar(value=0) self.selected_device_index = tk.IntVar(value=0)
self.last_process_time = time.time() self.last_process_time = time.time()
# FPS Einstellung ist hier! # FPS Setting
# FPS Setting is here! self.process_interval = 0.15 # Improved processing speed
self.process_interval = 0.30 # 30 FPS
# Collapsible scan list state
self.scan_list_collapsed = tk.BooleanVar(value=False)
self.init_gui() self.init_gui()
self.init_camera() self.init_camera()
@@ -47,44 +58,275 @@ class PfandScanner:
self.window.protocol("WM_DELETE_WINDOW", self.on_closing) self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
self.process_queue() self.process_queue()
def setup_styles(self):
"""Setup custom styles for a modern look"""
self.style = ttk.Style()
self.style.theme_use('clam')
# Configure colors
self.colors = {
'primary': '#2c3e50',
'secondary': '#3498db',
'success': '#27ae60',
'warning': '#f39c12',
'danger': '#e74c3c',
'light': '#ecf0f1',
'dark': '#34495e'
}
# Configure custom styles
self.style.configure('Title.TLabel', font=('Segoe UI', 16, 'bold'), foreground=self.colors['primary'])
self.style.configure('Heading.TLabel', font=('Segoe UI', 12, 'bold'), foreground=self.colors['dark'])
self.style.configure('Info.TLabel', font=('Segoe UI', 10), foreground=self.colors['dark'])
self.style.configure('Success.TLabel', font=('Segoe UI', 10, 'bold'), foreground=self.colors['success'])
self.style.configure('Custom.TLabelFrame', relief='solid', borderwidth=1)
self.style.configure('Camera.TFrame', relief='solid', borderwidth=2)
def load_data(self):
"""Load products from products.json and quantities from quantities.json"""
# Load products
if os.path.exists(self.products_file):
try:
with open(self.products_file, 'r', encoding='utf-8') as f:
products_data = json.load(f)
self.products = products_data.get("products", [])
self.prices = products_data.get("prices", {})
except Exception as e:
print(f"Fehler beim Laden der Produkte: {e}")
self.products = []
self.prices = {}
else:
self.products = []
self.prices = {}
# Load quantities
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
self.quantities = json.load(f)
except Exception as e:
print(f"Fehler beim Laden der Mengen: {e}")
self.quantities = {}
else:
self.quantities = {}
def save_json(self):
"""Save quantities to quantities.json"""
try:
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(self.quantities, f, indent=4, ensure_ascii=False)
except Exception as e:
print(f"Fehler beim Speichern der Mengen: {e}")
def init_gui(self): def init_gui(self):
self.main_frame = ttk.Frame(self.window) # Main container
self.main_frame = ttk.Frame(self.window, padding="10")
self.main_frame.grid(sticky="nsew") self.main_frame.grid(sticky="nsew")
self.main_frame.columnconfigure(0, weight=3) self.main_frame.columnconfigure(1, weight=2) # Camera gets more space
self.main_frame.columnconfigure(1, weight=1) self.main_frame.columnconfigure(0, weight=1) # Controls
self.main_frame.columnconfigure(2, weight=3) self.main_frame.columnconfigure(2, weight=1) # Info
self.main_frame.rowconfigure(0, weight=1) self.main_frame.rowconfigure(1, weight=1)
self.camera_frame = ttk.Frame(self.main_frame) # Header
self.camera_frame.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") header_frame = ttk.Frame(self.main_frame)
header_frame.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0, 10))
self.control_frame = ttk.Frame(self.main_frame) title_label = ttk.Label(header_frame, text="µScan V2.3.0", style='Title.TLabel')
self.control_frame.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") title_label.pack(side="left")
self.info_frame = ttk.Frame(self.main_frame) status_label = ttk.Label(header_frame, text="Erweiterte Barcode-Scanner", style='Info.TLabel')
self.info_frame.grid(row=0, column=2, padx=5, pady=5, sticky="nsew") status_label.pack(side="right")
self.camera_label = ttk.Label(self.camera_frame) # Control Panel (Left)
self.camera_label.pack(expand=True, fill="both") self.control_frame = ttk.LabelFrame(self.main_frame, text="Steuerung", padding="10")
self.control_frame.grid(row=1, column=0, padx=(0, 10), sticky="nsew")
# Camera View (Center)
self.camera_frame = ttk.LabelFrame(self.main_frame, text="Kameraansicht", padding="5")
self.camera_frame.grid(row=1, column=1, padx=5, sticky="nsew")
self.camera_frame.columnconfigure(0, weight=1)
self.camera_frame.rowconfigure(0, weight=1)
# Info Panel (Right) - now collapsible
self.info_frame = ttk.LabelFrame(self.main_frame, text="Scan-Ergebnisse", padding="10")
self.info_frame.grid(row=1, column=2, padx=(10, 0), sticky="nsew")
# Camera display
camera_container = ttk.Frame(self.camera_frame, relief='solid', borderwidth=2)
camera_container.grid(sticky="nsew", padx=5, pady=5)
camera_container.columnconfigure(0, weight=1)
camera_container.rowconfigure(0, weight=1)
self.camera_label = ttk.Label(camera_container, text="Kamera wird geladen...", anchor="center")
self.camera_label.grid(sticky="nsew")
self.init_device_selector() self.init_device_selector()
self.init_controls() self.init_controls()
self.init_treeview() self.init_treeview()
self.init_statistics()
def init_device_selector(self): def init_device_selector(self):
device_frame = ttk.LabelFrame(self.control_frame, text="Video Device") device_frame = ttk.LabelFrame(self.control_frame, text="Kamera-Einstellungen", padding="10")
device_frame.pack(pady=5, padx=5, fill="x") device_frame.pack(fill="x", pady=(0, 10))
ttk.Label(device_frame, text="Choose Camera:").pack(anchor="w", padx=5) ttk.Label(device_frame, text="Kamera auswählen:", style='Heading.TLabel').pack(anchor="w")
self.device_combo = ttk.Combobox(device_frame, state="readonly") self.device_combo = ttk.Combobox(device_frame, state="readonly")
self.device_combo.pack(fill="x", padx=5) self.device_combo.pack(fill="x", pady=(5, 10))
available_devices = self.list_video_devices() available_devices = self.list_video_devices()
self.device_combo['values'] = [f"Camera {i}" for i in available_devices] self.device_combo['values'] = [f"Kamera {i}" for i in available_devices]
if available_devices:
self.device_combo.current(0) self.device_combo.current(0)
self.device_combo.bind("<<ComboboxSelected>>", self.change_camera) self.device_combo.bind("<<ComboboxSelected>>", self.change_camera)
# Camera status
self.camera_status = ttk.Label(device_frame, text="Status: Initialisierung...", style='Info.TLabel')
self.camera_status.pack(anchor="w")
def init_controls(self):
# Focus controls
focus_frame = ttk.LabelFrame(self.control_frame, text="Fokus-Steuerung", padding="10")
focus_frame.pack(fill="x", pady=(0, 10))
self.autofocus_var = tk.BooleanVar(value=True)
self.autofocus_check = ttk.Checkbutton(
focus_frame, text="Auto-Fokus", variable=self.autofocus_var, command=self.toggle_autofocus)
self.autofocus_check.pack(anchor="w", pady=(0, 5))
ttk.Label(focus_frame, text="Manueller Fokus:").pack(anchor="w")
self.focus_slider = ttk.Scale(focus_frame, from_=0, to=255, orient="horizontal")
self.focus_slider.set(0)
self.focus_slider.pack(fill="x", pady=(0, 5))
# Image processing controls
process_frame = ttk.LabelFrame(self.control_frame, text="Bildverbesserung", padding="10")
process_frame.pack(fill="x", pady=(0, 10))
ttk.Label(process_frame, text="Helligkeit:").pack(anchor="w")
self.brightness_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal")
self.brightness_slider.set(50)
self.brightness_slider.pack(fill="x", pady=(0, 5))
ttk.Label(process_frame, text="Kontrast:").pack(anchor="w")
self.contrast_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal")
self.contrast_slider.set(50)
self.contrast_slider.pack(fill="x", pady=(0, 10))
# Scan options
scan_frame = ttk.LabelFrame(self.control_frame, text="Scan-Optionen", padding="10")
scan_frame.pack(fill="x")
self.outline_var = tk.BooleanVar(value=True)
ttk.Checkbutton(scan_frame, text="Barcodes umranden", variable=self.outline_var).pack(anchor="w")
self.beep_var = tk.BooleanVar(value=True)
ttk.Checkbutton(scan_frame, text="Ton bei Scan", variable=self.beep_var).pack(anchor="w")
def init_statistics(self):
"""Initialize statistics panel"""
stats_frame = ttk.LabelFrame(self.info_frame, text="Statistiken", padding="10")
stats_frame.pack(fill="x", pady=(0, 10))
self.total_scans_label = ttk.Label(stats_frame, text="Gesamte Scans: 0", style='Info.TLabel')
self.total_scans_label.pack(anchor="w")
self.total_value_label = ttk.Label(stats_frame, text="Gesamtwert: €0,00", style='Success.TLabel')
self.total_value_label.pack(anchor="w")
self.session_time_label = ttk.Label(stats_frame, text="Sitzungsdauer: 00:00", style='Info.TLabel')
self.session_time_label.pack(anchor="w")
self.session_start = datetime.now()
self.total_scans = 0
self.total_value = 0.0
def init_treeview(self):
# Header frame with collapse button
tree_header_frame = ttk.Frame(self.info_frame)
tree_header_frame.pack(fill="x", pady=(0, 5))
# Collapse button
self.collapse_button = ttk.Button(
tree_header_frame,
text="▼ Scan-Liste",
command=self.toggle_scan_list,
width=15
)
self.collapse_button.pack(side="left")
# Clear button
clear_button = ttk.Button(
tree_header_frame,
text="Liste löschen",
command=self.clear_scan_list
)
clear_button.pack(side="right")
# Treeview frame (collapsible)
self.tree_container = ttk.Frame(self.info_frame)
self.tree_container.pack(fill="both", expand=True)
self.tree = ttk.Treeview(
self.tree_container,
columns=("Zeit", "Barcode", "Typ", "Pfand"),
show="headings",
height=15
)
# Configure columns
self.tree.heading("Zeit", text="Zeit")
self.tree.heading("Barcode", text="Barcode")
self.tree.heading("Typ", text="Typ")
self.tree.heading("Pfand", text="Pfand")
self.tree.column("Zeit", width=100, minwidth=80)
self.tree.column("Barcode", width=120, minwidth=100)
self.tree.column("Typ", width=80, minwidth=60)
self.tree.column("Pfand", width=70, minwidth=50)
# Scrollbars
v_scrollbar = ttk.Scrollbar(self.tree_container, orient="vertical", command=self.tree.yview)
h_scrollbar = ttk.Scrollbar(self.tree_container, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
# Grid layout
self.tree.grid(row=0, column=0, sticky="nsew")
v_scrollbar.grid(row=0, column=1, sticky="ns")
h_scrollbar.grid(row=1, column=0, sticky="ew")
self.tree_container.columnconfigure(0, weight=1)
self.tree_container.rowconfigure(0, weight=1)
# Alternate row colors
self.tree.tag_configure('oddrow', background='#f9f9f9')
self.tree.tag_configure('evenrow', background='white')
def toggle_scan_list(self):
"""Toggle the visibility of the scan list"""
if self.scan_list_collapsed.get():
# Expand
self.tree_container.pack(fill="both", expand=True)
self.collapse_button.configure(text="▼ Scan-Liste")
self.scan_list_collapsed.set(False)
# Resize window columns
self.main_frame.columnconfigure(2, weight=1)
else:
# Collapse
self.tree_container.pack_forget()
self.collapse_button.configure(text="▶ Scan-Liste")
self.scan_list_collapsed.set(True)
# Make info panel smaller
self.main_frame.columnconfigure(2, weight=0, minsize=200)
def clear_scan_list(self):
"""Clear all items from the scan list"""
if messagebox.askyesno("Liste löschen", "Möchten Sie wirklich alle Einträge aus der Scan-Liste löschen?"):
for item in self.tree.get_children():
self.tree.delete(item)
def list_video_devices(self, max_devices=10): def list_video_devices(self, max_devices=10):
available = [] available = []
for i in range(max_devices): for i in range(max_devices):
@@ -99,65 +341,21 @@ class PfandScanner:
self.selected_device_index.set(index) self.selected_device_index.set(index)
self.init_camera() self.init_camera()
def init_controls(self):
focus_frame = ttk.LabelFrame(self.control_frame, text="Camera Controls")
focus_frame.pack(pady=5, padx=5, fill="x")
ttk.Label(focus_frame, text="Focus:").pack(pady=2)
self.focus_slider = ttk.Scale(focus_frame, from_=0, to=255, orient="horizontal")
self.focus_slider.set(0)
self.focus_slider.pack(pady=2, padx=5, fill="x")
self.autofocus_var = tk.BooleanVar(value=True)
self.autofocus_check = ttk.Checkbutton(
focus_frame, text="Autofocus", variable=self.autofocus_var, command=self.toggle_autofocus)
self.autofocus_check.pack(pady=2)
process_frame = ttk.LabelFrame(self.control_frame, text="Image Processing")
process_frame.pack(pady=5, padx=5, fill="x")
ttk.Label(process_frame, text="Brightness:").pack(pady=2)
self.brightness_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal")
self.brightness_slider.set(50)
self.brightness_slider.pack(pady=2, padx=5, fill="x")
ttk.Label(process_frame, text="Contrast:").pack(pady=2)
self.contrast_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal")
self.contrast_slider.set(50)
self.contrast_slider.pack(pady=2, padx=5, fill="x")
def init_treeview(self):
self.tree = ttk.Treeview(self.info_frame, columns=("Time", "Barcode", "Type", "Deposit"), show="headings")
for col in ["Time", "Barcode", "Type", "Deposit"]:
self.tree.heading(col, text=col)
self.tree.column("Time", width=150)
self.tree.column("Barcode", width=200)
self.tree.column("Type", width=100)
self.tree.column("Deposit", width=100)
self.tree.pack(fill="both", expand=True)
scrollbar = ttk.Scrollbar(self.info_frame, orient="vertical", command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
def init_camera(self): def init_camera(self):
if hasattr(self, 'cap') and self.cap and self.cap.isOpened(): if hasattr(self, 'cap') and self.cap and self.cap.isOpened():
self.cap.release() self.cap.release()
device_index = self.selected_device_index.get() device_index = self.selected_device_index.get()
self.cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW if os.name == 'nt' else 0) self.cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW if os.name == 'nt' else 0)
if self.cap.isOpened():
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
self.cap.set(cv2.CAP_PROP_FPS, 30)
self.toggle_autofocus() self.toggle_autofocus()
self.camera_status.configure(text="Status: Verbunden ✓", foreground=self.colors['success'])
def load_json(self):
if os.path.exists(self.data_file):
with open(self.data_file, 'r') as f:
self.quantities = json.load(f)
else: else:
self.quantities = {} self.camera_status.configure(text="Status: Fehler ✗", foreground=self.colors['danger'])
def save_json(self):
with open(self.data_file, 'w') as f:
json.dump(self.quantities, f, indent=4)
def toggle_autofocus(self): def toggle_autofocus(self):
if self.cap: if self.cap:
@@ -178,8 +376,53 @@ class PfandScanner:
return cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, return cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2) cv2.THRESH_BINARY, 11, 2)
def draw_barcode_outline(self, frame, barcodes):
if not self.outline_var.get():
return frame
for barcode in barcodes:
# Get barcode polygon points
points = barcode.polygon
if len(points) == 4:
# Convert to numpy array
pts = np.array([[point.x, point.y] for point in points], np.int32)
pts = pts.reshape((-1, 1, 2))
# Draw colored outline based on barcode type
barcode_data = barcode.data.decode('utf-8')
if len(barcode_data) == 13:
color = (0, 255, 0) # Green for EINWEG
elif len(barcode_data) == 8:
color = (255, 0, 0) # Blue for MEHRWEG
else:
color = (0, 165, 255) # Orange for DOSE
# Draw outline
cv2.polylines(frame, [pts], True, color, 3)
# Add label
rect = cv2.boundingRect(pts)
label_pos = (rect[0], rect[1] - 10)
pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE"
deposit = self.pfand_values.get(pfand_type, 0.00)
label = f"{pfand_type}: €{deposit:.2f}"
# Draw label background
(text_width, text_height), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
cv2.rectangle(frame, (label_pos[0], label_pos[1] - text_height - 5),
(label_pos[0] + text_width, label_pos[1] + 5), color, -1)
# Draw label text
cv2.putText(frame, label, label_pos, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
return frame
def update_preview(self): def update_preview(self):
try: try:
if not hasattr(self, 'cap') or not self.cap.isOpened():
self.window.after(100, self.update_preview)
return
ret, frame = self.cap.read() ret, frame = self.cap.read()
if ret: if ret:
if not self.autofocus_var.get(): if not self.autofocus_var.get():
@@ -189,50 +432,192 @@ class PfandScanner:
if current_time - self.last_process_time >= self.process_interval: if current_time - self.last_process_time >= self.process_interval:
processed_frame = self.adjust_image(frame) processed_frame = self.adjust_image(frame)
barcodes = decode(processed_frame) or decode(frame) barcodes = decode(processed_frame) or decode(frame)
self.current_barcodes = barcodes
for barcode in barcodes: for barcode in barcodes:
barcode_data = barcode.data.decode('utf-8') barcode_data = barcode.data.decode('utf-8')
self.queue.put(barcode_data) self.queue.put(barcode_data)
self.last_process_time = current_time self.last_process_time = current_time
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) # Draw barcode outlines
frame_with_outlines = self.draw_barcode_outline(frame.copy(), self.current_barcodes)
# Convert and display
cv2image = cv2.cvtColor(frame_with_outlines, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image) img = Image.fromarray(cv2image)
# Resize to fit label while maintaining aspect ratio
label_width = self.camera_label.winfo_width()
label_height = self.camera_label.winfo_height()
if label_width > 1 and label_height > 1:
img.thumbnail((label_width, label_height), Image.Resampling.LANCZOS)
imgtk = ImageTk.PhotoImage(image=img) imgtk = ImageTk.PhotoImage(image=img)
self.camera_label.imgtk = imgtk self.camera_label.imgtk = imgtk
self.camera_label.configure(image=imgtk) self.camera_label.configure(image=imgtk)
except Exception as e:
print(f"Error in video preview: {e}")
self.window.after(10, self.update_preview) # ~100 FPS preview # Update session time
self.update_statistics()
except Exception as e:
print(f"Fehler in der Video-Vorschau: {e}")
self.window.after(10, self.update_preview)
def update_statistics(self):
session_time = datetime.now() - self.session_start
hours, remainder = divmod(int(session_time.total_seconds()), 3600)
minutes, _ = divmod(remainder, 60)
time_str = f"{hours:02d}:{minutes:02d}"
self.session_time_label.configure(text=f"Sitzungsdauer: {time_str}")
self.total_scans_label.configure(text=f"Gesamte Scans: {self.total_scans}")
self.total_value_label.configure(text=f"Gesamtwert: €{self.total_value:.2f}")
def show_product_selection(self, barcode_data): def show_product_selection(self, barcode_data):
if hasattr(self, 'product_win') and self.product_win.winfo_exists(): if hasattr(self, 'product_win') and self.product_win.winfo_exists():
return return
self.product_win = tk.Toplevel(self.window) self.product_win = tk.Toplevel(self.window)
self.product_win.title("Produktwahl") self.product_win.title("Produkt auswählen")
self.product_win.geometry("500x400")
self.product_win.resizable(True, True)
ttk.Label(self.product_win, text=f"Welches Produkt soll dem Barcode '{barcode_data}' zugeordnet werden?").pack(pady=5) # Center the window
self.product_win.transient(self.window)
self.product_win.grab_set()
# Configure grid weights
self.product_win.columnconfigure(0, weight=1)
self.product_win.rowconfigure(1, weight=1)
# Header frame
header_frame = ttk.Frame(self.product_win, padding="20")
header_frame.grid(row=0, column=0, sticky="ew")
ttk.Label(header_frame, text="Produkt für Barcode auswählen:", style='Heading.TLabel').pack(pady=(0, 5))
ttk.Label(header_frame, text=f"'{barcode_data}'", style='Info.TLabel', font=('Courier', 10)).pack(pady=(0, 10))
# Main content frame
content_frame = ttk.Frame(self.product_win, padding="20")
content_frame.grid(row=1, column=0, sticky="nsew")
content_frame.columnconfigure(0, weight=1)
content_frame.rowconfigure(0, weight=1)
selected_product = tk.StringVar() selected_product = tk.StringVar()
for prod in self.quantities:
ttk.Radiobutton(self.product_win, text=prod, variable=selected_product, value=prod).pack(anchor='w') if self.products:
# Create scrollable frame for products
canvas = tk.Canvas(content_frame, bg='white')
scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
for i, product in enumerate(self.products):
current_quantity = self.quantities.get(product, 0)
price = self.prices.get(product, 0.00)
product_frame = ttk.Frame(scrollable_frame, padding="5")
product_frame.pack(fill="x", pady=2)
ttk.Radiobutton(
product_frame,
text=f"{product}",
variable=selected_product,
value=product
).pack(side="left")
info_text = f"(Aktuell: {current_quantity}, Preis: €{price:.2f})"
ttk.Label(product_frame, text=info_text, style='Info.TLabel').pack(side="right")
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
else:
ttk.Label(content_frame, text="Noch keine Produkte definiert.", style='Info.TLabel').pack(pady=20)
# Button frame
button_frame = ttk.Frame(self.product_win, padding="20")
button_frame.grid(row=2, column=0, sticky="ew")
# Configure button frame columns
button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=0)
button_frame.columnconfigure(2, weight=0)
def confirm(): def confirm():
prod = selected_product.get() product = selected_product.get()
if prod: if product:
self.quantities[prod] = self.quantities.get(prod, 0) + 1 # Update quantity
self.save_json() self.quantities[product] = self.quantities.get(product, 0) + 1
self.product_win.destroy()
else:
messagebox.showwarning("Keine Auswahl", "Bitte ein Produkt auswählen.")
ttk.Button(self.product_win, text="Bestätigen", command=confirm).pack(pady=5) # Update total value with actual product price
product_price = self.prices.get(product, 0.00)
self.total_value += product_price
self.save_json()
self.update_statistics()
self.product_win.destroy()
# Show confirmation message
messagebox.showinfo("Erfolgreich", f"Produkt '{product}' wurde hinzugefügt!")
else:
messagebox.showwarning("Keine Auswahl", "Bitte wählen Sie ein Produkt aus.")
def cancel():
self.product_win.destroy()
# Add new product button
def add_new_product():
new_product = simpledialog.askstring("Neues Produkt", "Name des neuen Produkts:")
if new_product and new_product.strip():
new_product = new_product.strip()
if new_product not in self.products:
# Ask for price
price_str = simpledialog.askstring("Preis", f"Preis für '{new_product}' (€):")
try:
price = float(price_str.replace(',', '.')) if price_str else 0.00
except:
price = 0.00
self.products.append(new_product)
self.prices[new_product] = price
# Save products
try:
products_data = {"products": self.products, "prices": self.prices}
with open(self.products_file, 'w', encoding='utf-8') as f:
json.dump(products_data, f, indent=4, ensure_ascii=False)
except Exception as e:
print(f"Fehler beim Speichern der Produkte: {e}")
# Refresh the dialog
self.product_win.destroy()
self.window.after(0, self.show_product_selection, barcode_data)
else:
messagebox.showwarning("Produkt existiert", "Dieses Produkt ist bereits vorhanden.")
# Buttons with better layout
ttk.Button(button_frame, text="Neues Produkt", command=add_new_product).grid(row=0, column=0, sticky="w", padx=5)
ttk.Button(button_frame, text="Abbrechen", command=cancel).grid(row=0, column=1, padx=5)
ttk.Button(button_frame, text="Bestätigen", command=confirm).grid(row=0, column=2, padx=5)
# Bind Enter key to confirm
self.product_win.bind('<Return>', lambda e: confirm())
self.product_win.bind('<Escape>', lambda e: cancel())
def process_queue(self): def process_queue(self):
try: try:
barcode_data = self.queue.get(timeout=0.1) barcode_data = self.queue.get(timeout=0.1)
now = datetime.now() now = datetime.now()
# Rate limiting
timestamps = self.barcode_times.get(barcode_data, []) timestamps = self.barcode_times.get(barcode_data, [])
timestamps = [t for t in timestamps if now - t <= timedelta(seconds=5)] timestamps = [t for t in timestamps if now - t <= timedelta(seconds=5)]
if len(timestamps) >= 3: if len(timestamps) >= 3:
@@ -240,12 +625,25 @@ class PfandScanner:
timestamps.append(now) timestamps.append(now)
self.barcode_times[barcode_data] = timestamps self.barcode_times[barcode_data] = timestamps
current_time = now.strftime("%Y-%m-%d %H:%M:%S") current_time = now.strftime("%H:%M:%S")
pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE" pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE"
deposit = self.pfand_values.get(pfand_type, 0.00) deposit = self.pfand_values.get(pfand_type, 0.00)
self.tree.insert("", 0, values=(current_time, barcode_data, pfand_type, f"{deposit:.2f}")) # Determine row color
row_count = len(self.tree.get_children())
tag = 'evenrow' if row_count % 2 == 0 else 'oddrow'
self.tree.insert("", 0, values=(current_time, barcode_data, pfand_type, f"{deposit:.2f}"), tags=(tag,))
# Update statistics
self.total_scans += 1
# Note: Total value is updated in product selection dialog
# Sound notification
if self.beep_var.get():
self.window.bell()
# Show product selection dialog
if barcode_data not in self.prompted_barcodes: if barcode_data not in self.prompted_barcodes:
self.prompted_barcodes.add(barcode_data) self.prompted_barcodes.add(barcode_data)
self.window.after(0, self.show_product_selection, barcode_data) self.window.after(0, self.show_product_selection, barcode_data)
@@ -253,16 +651,32 @@ class PfandScanner:
except queue.Empty: except queue.Empty:
pass pass
except Exception as e: except Exception as e:
print(f"Error in queue processing: {e}") print(f"Fehler in der Warteschlangenverarbeitung: {e}")
finally: finally:
self.window.after(100, self.process_queue) self.window.after(100, self.process_queue)
def on_closing(self): def on_closing(self):
if self.cap and self.cap.isOpened(): """Clean shutdown of the application"""
self.running = False
# Wait for camera thread to finish
if self.camera_thread and self.camera_thread.is_alive():
self.camera_thread.join(timeout=1.0)
try:
if hasattr(self, 'cap') and self.cap and self.cap.isOpened():
self.cap.release() self.cap.release()
cv2.destroyAllWindows()
except:
pass
self.window.destroy() self.window.destroy()
if __name__ != "__main__": if __name__ != "__main__":
def launch_pfand_scanner(): def launch_pfand_scanner():
scanner_window = tk.Toplevel() scanner_window = tk.Toplevel()
PfandScanner(scanner_window, "µScan V2.2.2") PfandScanner(scanner_window, "µScan V2.3.0 - Verbesserte Deutsche Version")
else:
# For standalone testing
root = tk.Tk()
app = PfandScanner(root, "µScan V2.3.0 - Verbesserte Deutsche Version")
root.mainloop()

View File

@@ -10,7 +10,7 @@ import tempfile
import traceback import traceback
import threading import threading
GITHUB_REPO_ZIP = "https://github.com/ZockerKatze/pfand_PKG/archive/refs/heads/main.zip" GITHUB_REPO_ZIP = "https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/pfand_PKG/archive/main.zip"
IGNORED_FILES = {"key.py"} IGNORED_FILES = {"key.py"}
class GitHubUpdater(tk.Toplevel): class GitHubUpdater(tk.Toplevel):
@@ -49,7 +49,7 @@ class GitHubUpdater(tk.Toplevel):
header = ttk.Label(self, text="Pfand Updater", style="Header.TLabel") header = ttk.Label(self, text="Pfand Updater", style="Header.TLabel")
header.pack(pady=(20, 5)) header.pack(pady=(20, 5))
self.status_label = ttk.Label(self, text="🔍 Suche nach Updates...", style="Status.TLabel") self.status_label = ttk.Label(self, text="Suche nach Updates", style="Status.TLabel")
self.status_label.pack(pady=(0, 10)) self.status_label.pack(pady=(0, 10))
self.frame = ttk.Frame(self) self.frame = ttk.Frame(self)
@@ -73,14 +73,14 @@ class GitHubUpdater(tk.Toplevel):
button_frame = ttk.Frame(self) button_frame = ttk.Frame(self)
button_frame.pack(pady=15) button_frame.pack(pady=15)
self.back_button = ttk.Button(button_frame, text="⬅️ Zurück", command=self.show_root_view) self.back_button = ttk.Button(button_frame, text="Zurück", command=self.show_root_view)
self.back_button.pack(side="left", padx=10) self.back_button.pack(side="left", padx=10)
self.back_button.pack_forget() self.back_button.pack_forget()
self.update_button = ttk.Button(button_frame, text="⬆️ Dateien aktualisieren", command=self.perform_update, state='disabled') self.update_button = ttk.Button(button_frame, text="Dateien aktualisieren", command=self.perform_update, state='disabled')
self.update_button.pack(side="left", padx=10) self.update_button.pack(side="left", padx=10)
self.toggle_debug_btn = ttk.Button(self, text="🐞 Fehlerdetails anzeigen", command=self.toggle_debug_output) self.toggle_debug_btn = ttk.Button(self, text="Fehlerdetails anzeigen", command=self.toggle_debug_output)
self.toggle_debug_btn.pack() self.toggle_debug_btn.pack()
self.toggle_debug_btn.pack_forget() self.toggle_debug_btn.pack_forget()
@@ -93,10 +93,10 @@ class GitHubUpdater(tk.Toplevel):
self.debug_visible = not self.debug_visible self.debug_visible = not self.debug_visible
if self.debug_visible: if self.debug_visible:
self.debug_output.pack() self.debug_output.pack()
self.toggle_debug_btn.config(text="🔽 Fehlerdetails verbergen") self.toggle_debug_btn.config(text="Fehlerdetails verbergen")
else: else:
self.debug_output.pack_forget() self.debug_output.pack_forget()
self.toggle_debug_btn.config(text="🐞 Fehlerdetails anzeigen") self.toggle_debug_btn.config(text="Fehlerdetails anzeigen")
def show_root_view(self): def show_root_view(self):
self.current_view = "root" self.current_view = "root"
@@ -125,7 +125,7 @@ class GitHubUpdater(tk.Toplevel):
def check_for_updates(self): def check_for_updates(self):
try: try:
self.status_label.config(text="⬇️ Lade Update herunter...", foreground="#ffb300") self.status_label.config(text="Lade Update herunter...", foreground="#ffb300")
self.update_idletasks() self.update_idletasks()
response = requests.get(GITHUB_REPO_ZIP) response = requests.get(GITHUB_REPO_ZIP)
@@ -137,13 +137,13 @@ class GitHubUpdater(tk.Toplevel):
if self.file_differences: if self.file_differences:
self.structure = self.build_structure(self.file_differences) self.structure = self.build_structure(self.file_differences)
self.status_label.config(text="⚠️ Updates verfügbar", foreground="#e53935") self.status_label.config(text="Updates verfügbar", foreground="#e53935")
self.display_structure(self.structure) self.display_structure(self.structure)
self.update_button.config(state='normal') self.update_button.config(state='normal')
else: else:
self.status_label.config(text="Alles ist aktuell", foreground="#43a047") self.status_label.config(text="Alles ist aktuell", foreground="#43a047")
except Exception: except Exception:
self.status_label.config(text="Fehler beim Laden", foreground="#e53935") self.status_label.config(text="Fehler beim Laden", foreground="#e53935")
self.toggle_debug_btn.pack() self.toggle_debug_btn.pack()
self.debug_output.insert("1.0", traceback.format_exc()) self.debug_output.insert("1.0", traceback.format_exc())
@@ -183,7 +183,7 @@ class GitHubUpdater(tk.Toplevel):
def perform_update(self): def perform_update(self):
self.update_button.config(state='disabled') self.update_button.config(state='disabled')
self.status_label.config(text="🚧 Update läuft...", foreground="#fb8c00") self.status_label.config(text="Update läuft", foreground="#fb8c00")
self.update_idletasks() self.update_idletasks()
try: try:
@@ -199,10 +199,10 @@ class GitHubUpdater(tk.Toplevel):
os.makedirs(os.path.dirname(dest_path), exist_ok=True) os.makedirs(os.path.dirname(dest_path), exist_ok=True)
shutil.copy2(src_path, dest_path) shutil.copy2(src_path, dest_path)
messagebox.showinfo("Aktualisiert", "Dateien wurden erfolgreich aktualisiert.") messagebox.showinfo("Aktualisiert", "Dateien wurden erfolgreich aktualisiert.")
self.destroy() self.destroy()
except Exception as e: except Exception as e:
messagebox.showerror("Fehler", str(e)) messagebox.showerror("Fehler", str(e))
self.toggle_debug_btn.pack() self.toggle_debug_btn.pack()
self.debug_output.insert("1.0", traceback.format_exc()) self.debug_output.insert("1.0", traceback.format_exc())
@@ -238,7 +238,7 @@ def run_silent_update(master=None):
file_differences.append(rel_path) file_differences.append(rel_path)
if file_differences: if file_differences:
result = messagebox.askyesno("🔄 Update verfügbar", "Es sind Updates verfügbar. Möchten Sie aktualisieren?") result = messagebox.askyesno("Update verfügbar", "Es sind Updates verfügbar. Möchten Sie aktualisieren?")
if result: if result:
updater = GitHubUpdater(master) updater = GitHubUpdater(master)
updater.grab_set() updater.grab_set()

View File

@@ -1 +1,22 @@
{"products": ["Flaschen", "Bierflasche", "Kasten", "Dose", "Plastikflasche", "Monster", "Joghurt Glas"], "prices": {"Flaschen": 0.25, "Bierflasche": 0.2, "Kasten": 3.0, "Dose": 0.25, "Plastikflasche": 0.25, "Monster": 0.25, "Joghurt Glas": 0.17}} {
"products": [
"Flaschen",
"Bierflasche",
"Kasten",
"Dose",
"Plastikflasche",
"Monster",
"Joghurt Glas",
"nigger"
],
"prices": {
"Flaschen": 0.25,
"Bierflasche": 0.2,
"Kasten": 3.0,
"Dose": 0.25,
"Plastikflasche": 0.25,
"Monster": 0.25,
"Joghurt Glas": 0.17,
"nigger": 200.0
}
}

10
quantities.json Normal file
View File

@@ -0,0 +1,10 @@
{
"Flaschen": 7,
"Bierflasche": 2,
"Kasten": 2,
"Dose": 3,
"Plastikflasche": 2,
"Monster": 2,
"Joghurt Glas": 2,
"nigger": 1
}