From 383cad8808ca250c3152a4145c8d044834bb61b2 Mon Sep 17 00:00:00 2001 From: trav Date: Tue, 6 Aug 2024 22:55:44 -0400 Subject: [PATCH] backup while laptop is on the fritz --- .gitignore | 7 +- __pycache__/addtoDB.cpython-38.pyc | Bin 1387 -> 0 bytes __pycache__/tozpl.cpython-38.pyc | Bin 3915 -> 0 bytes addtoDB.py | 46 +- freeze_frame.jpg | Bin 29843 -> 0 bytes kiosk.py | 1361 ++++++++++++++++++++++ kiosk6.py => kiosk6-backup-pre-claude.py | 13 +- ssb-custodisco-plugin.js | 26 + ssb-post.sh | 79 ++ 9 files changed, 1528 insertions(+), 4 deletions(-) delete mode 100644 __pycache__/addtoDB.cpython-38.pyc delete mode 100644 __pycache__/tozpl.cpython-38.pyc delete mode 100644 freeze_frame.jpg create mode 100644 kiosk.py rename kiosk6.py => kiosk6-backup-pre-claude.py (98%) create mode 100644 ssb-custodisco-plugin.js diff --git a/.gitignore b/.gitignore index ff7e515..b89ecb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ -freeze_frame.jpg +\freeze_frame.jpg qr.jpg users.json merged_image.jpg drawing.png to_print.zpl +tmp/* +merged_image.png +__pycache__/* +freeze_frame.png +image-temp.jpg diff --git a/__pycache__/addtoDB.cpython-38.pyc b/__pycache__/addtoDB.cpython-38.pyc deleted file mode 100644 index 302e9d6c4b0435e9fd5b4bd320e3a1aeaf449b8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1387 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 diff --git a/addtoDB.py b/addtoDB.py index 3be2c59..a9af16c 100644 --- a/addtoDB.py +++ b/addtoDB.py @@ -9,7 +9,7 @@ import traceback import os, sys import json import subprocess - +import logging def main(): @@ -26,7 +26,6 @@ def main(): exit(1) - def addToSSB(pathToImage,description,mintOrGive): # mint @@ -68,5 +67,48 @@ def addToSSB(pathToImage,description,mintOrGive): return key +def get_message_content(message_id): + try: + print(f"Executing command: ./ssb-post.sh get_message_content {message_id}") + result = subprocess.run(['./ssb-post.sh', 'get_message_content', message_id], + capture_output=True, text=True, check=True) + + print(f"Command output:\n{result.stdout}") + + # Split the output into lines + lines = result.stdout.split('\n') + + # Extract the image path + image_path = None + for line in lines: + if line.startswith("IMAGE_PATH:"): + image_path = line.split(":", 1)[1].strip() + break + + # Find the start of the JSON content + json_start = next(i for i, line in enumerate(lines) if line.strip().startswith("{")) + + # Join the JSON lines and parse + json_content = "\n".join(lines[json_start:]) + message_content = json.loads(json_content) + + print(f"Parsed message content: {message_content}") + print(f"Image path: {image_path}") + + return message_content, image_path + except subprocess.CalledProcessError as e: + print(f"Error: subprocess.CalledProcessError - {e}") + print(f"Debug: stdout = {e.stdout}") + print(f"Debug: stderr = {e.stderr}") + return None, None + except json.JSONDecodeError as e: + print(f"Error: json.JSONDecodeError - {e}") + print(f"Debug: stdout = {result.stdout}") + return None, None + except Exception as e: + print(f"Error: Unexpected exception - {e}") + return None, None + + if __name__ == '__main__': main() diff --git a/freeze_frame.jpg b/freeze_frame.jpg deleted file mode 100644 index eb23dff08251d373c8b6d2c66ae718ba100c0762..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29843 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 Y{oOEswoyBXn50`V@hbd<#c+TB+4TKe+5i9m diff --git a/kiosk.py b/kiosk.py new file mode 100644 index 0000000..e0bde6e --- /dev/null +++ b/kiosk.py @@ -0,0 +1,1361 @@ +## this is the custodisco kiosk + +import tkinter as tk +from tkinter import font as tkfont +from tkinter import Canvas, ttk, Text, filedialog, messagebox +from PIL import Image, ImageTk, ImageDraw +from qreader import QReader +from pyzbar.pyzbar import decode +import numpy as np +import tozpl +import subprocess +import threading +import json +import os +import cv2 +import time +import qrcode +import addtoDB +import re # Import re for regular expression matching +import os +import shutil +import hashlib + + + +# Configuration +MIGRATION_ITEMS_DIR = "/home/trav/Documents/migration_items" + +# Global variables +class GlobalVars: + qr_code_value = None + print_type = "neither" + migration_ticket = "" + selected_user = None + BUTTON_FONT = None + TEXT_FONT = None + + +class Kiosk(tk.Tk): + def __init__(self, *args, **kwargs): + tk.Tk.__init__(self, *args, **kwargs) + self.frame = None + self.frames_history = [] + self.geometry('1366x768') + self.attributes('-fullscreen', True) + self.config(cursor="crosshair") + self.QRX = None + self.QRY = None + self.QRscale = 1 + + # Initialize fonts + GlobalVars.BUTTON_FONT = tkfont.Font(size=24, family='Helvetica') + GlobalVars.TEXT_FONT = tkfont.Font(size=30, family='Helvetica') + + self.switch_frame(Screen0) + + + def switch_frame(self, frame_class, keep_history=True): + if keep_history and self.frame: + self.frames_history.append(type(self.frame)) + new_frame = frame_class(self) + if self.frame is not None: + self.frame.destroy() + self.frame = new_frame + self.frame.pack(fill="both", expand=True) + + def add_home_button(self, frame): + # Create the "Start Over" button + home_button = tk.Button(frame, text="Start Over from the beginning", command=self.show_warning_dialog, bg='peach puff', width=24, font=GlobalVars.BUTTON_FONT) + home_button.place(x=0, y=0) # top-left corner + + def show_warning_dialog(self): + result = messagebox.askokcancel("Warning", "If you start over, you will lose all information entered so far. Are you sure you want to start over?", icon='warning') + if result: + self.start_over() + + def start_over(self): + global migration_ticket + global MIGRATION_ITEMS_DIR + + # Check if migration_ticket is defined + if 'migration_ticket' in globals(): + # Delete any existing migration files + if migration_ticket: + text_file = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.txt") + image_file = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.jpg") + + if os.path.exists(text_file): + os.remove(text_file) + if os.path.exists(image_file): + os.remove(image_file) + + # Reset migration_ticket + migration_ticket = "" + + # Reset other global variables + GlobalVars.qr_code_value = None + GlobalVars.print_type = "neither" + GlobalVars.selected_user = None + + # Switch to the home screen + self.switch_frame(Screen0) + +# home screen +class Screen0(tk.Frame): + def __init__(self, master): + tk.Frame.__init__(self, master, bg='#bcfef9') + title_font = tkfont.Font(size=42, family='Helvetica') # 30% bigger + + # Split the screen into two frames + left_frame = tk.Frame(self, bg='#bcfef9') + right_frame = tk.Frame(self, bg='#bcfef9', padx=40) # 40px padding on the right side + left_frame.grid(row=0, column=0, sticky='nsew') + right_frame.grid(row=0, column=1, sticky='nsew') + + self.grid_columnconfigure(0, weight=1, minsize=800) # For left frame + self.grid_columnconfigure(1, weight=1) # For right frame + + # Title and buttons on the left side + title_label = tk.Label(left_frame, text="Custodisco", bg='#bcfef9', fg='#800080', font=('Helvetica', 64)) # dark purple color + title_label.pack(side='top', pady=200) # adjust to your needs + + # Welcome message on the left side + # welcome_text = """""" + # welcome_label = tk.Label(left_frame, text=welcome_text, bg='#bcfef9', font=GlobalVars.TEXT_FONT, justify='left', wraplength=650) + # welcome_label.pack(side='top', padx=20, pady=20) + + + + # tk.Button(right_frame, text="Create Item", command=lambda: master.switch_frame(Screen1), height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30) + + # Button for Migration mode + tk.Button(right_frame, text="Create Item", command=lambda: master.switch_frame(Screen15), height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30) + tk.Button(right_frame, text="Lookup Item", command=lambda: master.switch_frame(Screen14), height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30) + + # Create the quit button + tk.Button(right_frame, text="Quit", command=self.quit_program, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + + def quit_program(self): + self.master.destroy() + + +# do you have ssb? +class Screen1(tk.Frame): + def __init__(self, master): + + tk.Frame.__init__(self, master, bg='#bcfef9') + master.add_home_button(self) + + # Create the label widget with the text + label = tk.Label(self, text="Would you like to associate your item with a Scuttlebutt account?", font=GlobalVars.TEXT_FONT) + label.pack(pady=90) + + tk.Button(self, text="Yes, and I already have an account", command=lambda: master.switch_frame(Screen2), height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20) + tk.Button(self, text="Yes, but I need to create an account now", command=lambda: master.switch_frame(Screen7), height=3, width=50, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20) + tk.Button(self, text="No thanks", command=lambda: master.switch_frame(Screen3), height=3, width=50, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20) + + + +# find yourself in list of ssb users +class Screen2(tk.Frame): + selected_user = None # This is the global variable to store selected user + + def __init__(self, master): + tk.Frame.__init__(self, master, bg='#bcfef9') + self.selected_label = None # This is the global variable to store selected label + + # Create a new frame at the top for the label and text box + self.top_frame = tk.Frame(self, bg='#bcfef9') + self.top_frame.pack(side="top", fill="x", pady=60) + + # Add a label with text wrapping + self.label = tk.Label(self.top_frame, + text="Start typing your public key to find yourself in the list then click on your key to select it.", + font=GlobalVars.TEXT_FONT, + wraplength=800, # Adjust this value as needed + bg='#bcfef9') + self.label.pack(side="top", pady=(0, 20)) # Add some padding below the label + + # Add text box to the top frame + self.entry = tk.Entry(self.top_frame, font=GlobalVars.TEXT_FONT) + self.entry.bind('', lambda e: self.update_users_list()) + self.entry.pack(side="top", fill="x", padx=20) + + # Focus on the entry box + self.entry.focus_set() + + # Create container for user list + self.container = ttk.Frame(self, height=500) # Define a height here + self.container.pack(fill='both', expand=True, padx=20, pady=20) + + # Initialize users list from users.json + self.users = self.get_users_from_file() + self.update_users_list() + + # Highlight selected user if one exists + if GlobalVars.selected_user is not None: + for widget in self.container.winfo_children(): + if isinstance(widget, tk.Button) and widget['text'] == GlobalVars.selected_user: + widget.configure(relief=tk.SUNKEN, bg="light blue") + self.selected_label = widget + + + # The 'Done' button to navigate to next screen + self.done_button = tk.Button(self, text="Done", command=lambda: master.switch_frame(Screen3), height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT) + self.done_button.pack(side="bottom", padx=20, pady=10) + + # The 'Refresh List' button + self.refresh_button = tk.Button(self, text="Refresh List (takes 30 seconds or so)", command=self.refresh_users, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT) + self.refresh_button.pack(side="bottom", padx=20, pady=10) + + master.add_home_button(self) + + def refresh_users(self): + try: + # Update users list from Scuttlebutt and display it + self.users = self.get_scuttlebutt_users() + self.update_users_list() + except Exception as e: + print(f"An error occurred while refreshing the user list: {e}") + messagebox.showerror("Error", "An error occurred while refreshing the user list. Please try again later.") + + def get_scuttlebutt_users(self): + result = subprocess.run(['node', 'scuttlebot.js'], capture_output=True, text=True) + if result.returncode == 0: + users = json.loads(result.stdout) + return users + else: + raise Exception("Command failed: " + result.stderr) + + def get_users_from_file(self): + with open('users.json') as f: + users = json.load(f) + return users + + def update_users_list(self): + # Remove all widgets from container + for widget in self.container.winfo_children(): + widget.destroy() + + # Filter users based on search text + search_text = self.entry.get().lower() + filtered_users = [user for user in self.users if search_text in user['id'].lower()] + + # Display filtered users + self.display_users(filtered_users) + + def display_users(self, users): + # Scrollable list of users + canvas = tk.Canvas(self.container, width=460) # Decrease the width by scrollbar's width + style = ttk.Style() + style.configure("Vertical.TScrollbar", gripcount=0, + arrowsize=50, width=50) # Adjust width here + scrollbar = ttk.Scrollbar(self.container, orient='vertical', command=canvas.yview, style="Vertical.TScrollbar") + scrollable_frame = ttk.Frame(canvas) + + scrollable_frame.bind( + "", + lambda e: canvas.configure( + scrollregion=canvas.bbox("all") + ) + ) + + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + # Add user buttons to scrollable frame + for user in users: + button = tk.Button(scrollable_frame, text=user['id'], font=('Liberation Mono', 24), bd=0) # Adjust font size here + button.pack(anchor="w") + button.config(command=lambda button=button, user=user: self.on_user_clicked(button, user)) + + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + def widget_exists(self, widget): + try: + widget.winfo_exists() + return True + except tk.TclError: + return False + + def on_user_clicked(self, button, user): + # Remove highlight from previously selected user + if self.selected_label is not None and self.widget_exists(self.selected_label): + self.selected_label.configure(relief=tk.FLAT, bg='SystemButtonFace') # default color + + # Store selected user and button + GlobalVars.selected_user = user['id'] + self.selected_label = button + + # Highlight clicked label + self.selected_label.configure(relief=tk.SUNKEN, bg="light blue") + + + + +#take photo of item +class Screen3(tk.Frame): + def __init__(self, master=None): + tk.Frame.__init__(self, master, bg='#bcfef9') + + # Create the "Start Over" button + home_button = tk.Button(text="Start Over", command=self.show_warning_dialog, bg='peach puff', font=GlobalVars.BUTTON_FONT) + home_button.place(x=0, y=0) # top-left corner + + self.vid = cv2.VideoCapture(0) + self.is_capturing = True + self.freeze_frame = None + + # Video feed + self.canvas = tk.Canvas(self, width=self.vid.get(cv2.CAP_PROP_FRAME_WIDTH), height=self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT)) + self.canvas.pack(side="left") + + # Info and button on the right + self.text_frame = tk.Frame(self, bg='#bcfef9') + self.text_frame.pack(side="right", fill="both", expand=True) + tk.Label(self.text_frame, text="Now we will take a picture of your item to show up on Scuttlebutt.\n\nIf you tap Take Photo a second time it will re-take the photo\nbut wont show you a preview during the countdown (this is a bug)", font=("Helvetica", 16), bg='#bcfef9').pack(pady=10) + self.button = tk.Button(self.text_frame, text="Take Photo", command=self.take_photo, height=3, width=37, bg='peach puff', font=GlobalVars.BUTTON_FONT) + self.button.pack(pady=10) + + self.done_button = tk.Button(self, text="Done", command=self.done, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT) + self.done_button.place(relx=0.9, rely=0.9, anchor='se') + + self.update_image() + + def show_warning_dialog(self): + result = messagebox.askokcancel("Warning", "If you start over, you will lose all information entered so far. Are you sure you want to start over?", icon='warning') + if result: + self.homer() + + def homer(self): + self.__del__() + self.master.switch_frame(Screen0) + + self.vid = cv2.VideoCapture(0) + self.is_capturing = True + self.freeze_frame = None + + # Video feed + self.canvas = tk.Canvas(self, width=self.vid.get(cv2.CAP_PROP_FRAME_WIDTH), height=self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT)) + self.canvas.pack(side="left") + + # Info and button on the right + self.text_frame = tk.Frame(self, bg='#bcfef9') + self.text_frame.pack(side="right", fill="both", expand=True) + tk.Label(self.text_frame, text="Now we will take a picture of your item to show up on Scuttlebutt.\n\nIf you tap Take Photo a second time it will re-take the photo\nbut wont show you a preview during the countdown (this is a bug)", font=("Helvetica", 16), bg='#bcfef9').pack(pady=10) + self.button = tk.Button(self.text_frame, text="Take Photo", command=self.take_photo, height=3, width=37, bg='peach puff', font=GlobalVars.BUTTON_FONT) + self.button.pack(pady=10) + + self.done_button = tk.Button(self, text="Done", command=self.done, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT) + self.done_button.place(relx=0.9, rely=0.9, anchor='se') + + self.update_image() + + def homer(self): + self.__del__() + self.master.switch_frame(Screen0) + +#take photo of item +class Screen3(tk.Frame): + def __init__(self, master=None): + tk.Frame.__init__(self, master, bg='#bcfef9') + + # Create the "Start Over" button + home_button = tk.Button(text="Start Over", command=self.show_warning_dialog, bg='peach puff', font=GlobalVars.BUTTON_FONT) + home_button.place(x=0, y=0) # top-left corner + + self.vid = cv2.VideoCapture(0) + self.is_capturing = True + self.freeze_frame = None + self.countdown_text = None + self.last_photo = None + + # Video feed + self.canvas = tk.Canvas(self, width=self.vid.get(cv2.CAP_PROP_FRAME_WIDTH), height=self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT)) + self.canvas.pack(side="left") + + # Info and button on the right + self.text_frame = tk.Frame(self, bg='#bcfef9') + self.text_frame.pack(side="right", fill="both", expand=True) + # tk.Label(self.text_frame, text="Take a photo of your item", font=("Helvetica", 16), bg='#bcfef9').pack(pady=10) + self.button = tk.Button(self.text_frame, text="Take Photo", command=self.take_photo, height=3, width=37, bg='peach puff', font=GlobalVars.BUTTON_FONT) + self.button.pack(pady=10) + + self.done_button = tk.Button(self, text="Done", command=self.done, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT) + self.done_button.place(relx=0.9, rely=0.9, anchor='se') + + self.update_image() + + # Bind the destroy event to release resources + self.bind("", self.on_destroy) + + def show_warning_dialog(self): + result = messagebox.askokcancel("Warning", "If you start over, you will lose all information entered so far. Are you sure you want to start over?", icon='warning') + if result: + self.start_over() + + def start_over(self): + self.release_resources() + self.master.switch_frame(Screen0) + + def update_image(self): + if self.is_capturing and self.vid.isOpened(): + ret, frame = self.vid.read() + if ret: + self.cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) + self.img = Image.fromarray(self.cv2image) + self.imgtk = ImageTk.PhotoImage(image=self.img) + self.canvas.delete("all") # Clear previous image + self.canvas.create_image(0, 0, image=self.imgtk, anchor='nw') + elif self.last_photo: + self.canvas.delete("all") + self.canvas.create_image(0, 0, image=self.last_photo, anchor='nw') + + if self.countdown_text: + self.canvas.create_text(self.canvas.winfo_width() // 2, self.canvas.winfo_height() // 2, + text=self.countdown_text, fill="white", font=("Helvetica", 120)) + + self.after(10, self.update_image) + + def take_photo(self): + self.is_capturing = True + self.countdown_text = None + countdown_thread = threading.Thread(target=self.countdown) + countdown_thread.start() + + def countdown(self): + for i in range(3, 0, -1): + self.countdown_text = str(i) + time.sleep(1) + + self.countdown_text = None + + # Capture the current frame + ret, frame = self.vid.read() + if ret: + rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + pil_image = Image.fromarray(rgb_image) + pil_image.save('freeze_frame.jpg') + self.display_taken_photo() + + self.is_capturing = False + + def display_taken_photo(self): + image = Image.open('freeze_frame.jpg') + self.last_photo = ImageTk.PhotoImage(image) + self.canvas.delete("all") # Clear the canvas + self.canvas.create_image(0, 0, image=self.last_photo, anchor='nw') + + def done(self): + global migration_ticket + global MIGRATION_ITEMS_DIR + + # If migration_ticket is set, copy the photo to MIGRATION_ITEMS_DIR + if migration_ticket: + source_path = 'freeze_frame.jpg' + destination_path = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.jpg") + try: + shutil.copy2(source_path, destination_path) + print(f"Photo saved as {destination_path}") + except Exception as e: + print(f"Error saving photo: {e}") + + self.release_resources() + self.master.switch_frame(Screen5) + + def release_resources(self): + self.is_capturing = False + if self.vid.isOpened(): + self.vid.release() + + def on_destroy(self, event): + self.release_resources() + + def __del__(self): + self.release_resources() + + + +# draw a sticker +class Screen4(tk.Frame): + def __init__(self, master): + tk.Frame.__init__(self, master, bg='#bcfef9') + + # Configure column minsizes + self.grid_columnconfigure(0, minsize=675) + self.grid_columnconfigure(1, minsize=100) + self.grid_columnconfigure(2, minsize=100) + + # Creating a frame for the left side of the screen for drawing and instructions + self.left_frame = tk.Frame(self, bg='#bcfef9') + self.left_frame.grid(row=0, column=0, padx=2) + + # Frame for the tools + self.right_frame = tk.Frame(self, bg='#bcfef9') + self.right_frame.grid(row=0, column=2, padx=70) + + # Add Import Image Button + tk.Button(self.left_frame, text="Import Image", command=self.import_image, height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + + # Add instructions + self.label = tk.Label(self.left_frame, text="You may now draw your sticker :)", wraplength=650, font=GlobalVars.TEXT_FONT) + self.label.pack(pady=2) + + # Drawing area + self.drawing = Image.new('1', (650, 360), 1) + self.draw = ImageDraw.Draw(self.drawing) + self.last_draw = None + + # Set initial drawing color to black and size to 1 + self.draw_color = 'black' + self.draw_size = 1 + + # Creating the Canvas for drawing + self.canvas = Canvas(self.left_frame, width=650, height=360, bg='white') + self.canvas.bind("", self.draw_line) + self.image_on_canvas = None + self.canvas.pack(pady=20) + self.canvas.bind("", self.reset_last_draw) + self.add_qr_box() + + # Create frames for pen size and color tools + self.pen_size_frame = tk.Frame(self.right_frame, bg='#bcfef9') + self.pen_size_frame.pack(pady=(0, 20)) + self.pen_color_frame = tk.Frame(self.right_frame, bg='#bcfef9') + self.pen_color_frame.pack(pady=(0, 20)) + + # Pen size label + tk.Label(self.pen_size_frame, text="Pen Size", font=GlobalVars.TEXT_FONT, bg='#bcfef9').pack() + + # Add Draw Size buttons + pen_sizes = [(".", 1), ("*", 2), ("⚬", 3), ("⬤", 4), ("⬛", 5)] + for i, (text, size) in enumerate(pen_sizes): + tk.Button(self.pen_size_frame, text=text, command=lambda s=size: self.set_draw_size(s), + height=2, width=5, bg='peach puff').pack(pady=2) + + # Pen color label + tk.Label(self.pen_color_frame, text="Pen Color", font=GlobalVars.TEXT_FONT, bg='#bcfef9').pack() + + # Creating color buttons + colors = ['black', 'gray', 'white'] + for color in colors: + tk.Button(self.pen_color_frame, height=2, width=5, bg=color, + command=lambda c=color: self.set_draw_color(c)).pack(pady=2) + + # Add Clear Drawing Button + tk.Button(self.right_frame, text="Clear Drawing", command=self.clear_drawing, + height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + + # Done button + tk.Button(self.right_frame, text="Done", command=self.next, + height=3, width=10, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + + # Adding a home button + master.add_home_button(self) + + # Define the info_label to display QRX, QRY, and QRscale values + self.info_label = tk.Label(self.right_frame, text="", bg='#bcfef9', font=GlobalVars.TEXT_FONT) + self.info_label.pack(pady=5) + + def draw_line(self, event): + x, y = event.x, event.y + if self.last_draw: + points = self.get_points_on_line(*self.last_draw, x, y) + for px, py in points: + if self.draw_color == 'gray': + self.draw_dithered_point(px, py) + else: + self.draw_point(px, py) + self.last_draw = (x, y) + + def get_points_on_line(self, x0, y0, x1, y1): + points = [] + dx = abs(x1 - x0) + dy = abs(y1 - y0) + sx = 1 if x0 < x1 else -1 + sy = 1 if y0 < y1 else -1 + err = dx - dy + + while True: + points.append((x0, y0)) + if x0 == x1 and y0 == y1: + break + e2 = 2 * err + if e2 > -dy: + err -= dy + x0 += sx + if e2 < dx: + err += dx + y0 += sy + + return points + + def draw_point(self, x, y): + color = 0 if self.draw_color == 'black' else 1 + for dx in range(self.draw_size): + for dy in range(self.draw_size): + self.canvas.create_rectangle(x+dx, y+dy, x+dx+1, y+dy+1, fill=self.draw_color, outline='') + self.draw.point((x+dx, y+dy), fill=color) + + + def draw_dithered_point(self, x, y): + for dx in range(self.draw_size): + for dy in range(self.draw_size): + if (x+dx+y+dy) % 2 == 0: + self.canvas.create_rectangle(x+dx, y+dy, x+dx+1, y+dy+1, fill='black', outline='') + self.draw.point((x+dx, y+dy), fill=0) + else: + self.canvas.create_rectangle(x+dx, y+dy, x+dx+1, y+dy+1, fill='white', outline='') + self.draw.point((x+dx, y+dy), fill=1) + + def next(self): + self.drawing.save("drawing.png") + self.master.switch_frame(Screen13) + + def reset_last_draw(self, event): + self.last_draw = None + + def set_draw_color(self, color): + self.draw_color = color + + def set_draw_size(self, size): + self.draw_size = size + + def clear_drawing(self): + self.canvas.delete("all") + self.drawing = Image.new('1', (650, 360), 1) + self.draw = ImageDraw.Draw(self.drawing) + self.add_qr_box() + + def add_qr_box(self, x=506, y=217, size=1): + box_size = 37 * size + self.canvas.create_rectangle(x, y, x + box_size, y + box_size, outline='black', fill='white') + self.canvas.create_text(x + box_size/2, y + box_size/2, text="QR", fill="black") + + def import_image(self): + file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.jpeg *.png")]) + + if not file_path: + return + + file_parts = file_path.split('-')[-4:] + + if len(file_parts) < 4: + messagebox.showerror("Filename Error", "The filename does not follow the expected pattern.") + return + + try: + QRX = int(file_parts[1]) + QRY = int(file_parts[2]) + QRscale = int(file_parts[3].split('.')[0]) + + self.master.QRX = QRX + self.master.QRY = QRY + self.master.QRscale = QRscale + + self.info_label.config(text=f"QRX: {QRX}, QRY: {QRY}, QRscale: {QRscale}") + except ValueError as e: + messagebox.showerror("Filename Error", "The filename does not follow the expected pattern.") + return + + self.canvas.delete("all") + img = Image.open(file_path) + img = img.convert('1') + if img.size[0] > 675 or img.size[1] > 375: + img = img.crop((0, 0, 650, 360)) + self.add_qr_box(QRX, QRY, QRscale) + + self.drawing.paste(img) + + self.imported_img = ImageTk.PhotoImage(img) + + self.image_on_canvas = self.canvas.create_image(0, 0, image=self.imported_img, anchor='nw') + + self.add_qr_box(QRX, QRY, QRscale) + + + + +# typed description +class Screen5(tk.Frame): + def __init__(self, master): + tk.Frame.__init__(self, master, bg='#bcfef9') + master.add_home_button(self) + + + # Adding the information label + self.info_label = tk.Label(self, text="Please enter a description of your item.", font=GlobalVars.TEXT_FONT, wraplength=500) + self.info_label.pack(pady=70) + + # Adding the text entry field + self.info_entry = tk.Text(self, height=10, width=50, font=("Helvetica", 16)) + self.info_entry.pack(pady=10) + + # Adding the done button + self.done_button = tk.Button(self, text="Done", command=self.save_info_and_switch, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT) + self.done_button.pack(pady=10) + + # Setting the focus to the text entry field + self.info_entry.focus_set() + + def save_info_and_switch(self): + global info_text + global migration_ticket + global MIGRATION_ITEMS_DIR + + info_text = self.info_entry.get("1.0", "end-1c") + info_text = info_text.replace('\n', '\\n') + + # If migration_ticket is set (not empty), save the text and image to MIGRATION_ITEMS_DIR + if migration_ticket: + # Save text + text_file_path = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.txt") + try: + with open(text_file_path, 'w') as f: + f.write(info_text) + print(f"Text saved as {text_file_path}") + except Exception as e: + print(f"Error saving text: {e}") + + # Save image + source_image_path = 'freeze_frame.jpg' + destination_image_path = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.jpg") + try: + shutil.copy2(source_image_path, destination_image_path) + print(f"Photo saved as {destination_image_path}") + except Exception as e: + print(f"Error saving photo: {e}") + + self.master.switch_frame(Screen11) + +# I understand +class Screen6(tk.Frame): + def __init__(self, master): + tk.Frame.__init__(self, master, bg='#bcfef9') + + master.add_home_button(self) + tk.Button(self, text="I Understand", command=lambda: master.switch_frame(Screen3), height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + + + +# create user not implemented lol +class Screen7(tk.Frame): + def __init__(self, master): + tk.Frame.__init__(self, master, bg='#bcfef9') + + master.add_home_button(self) + # Assume there's a method to manage the text entry + self.info_label = tk.Label(self, text="Hiii sorry this hasn't been implemented yet!", font=("Helvetica", 16), wraplength=500) + self.info_label.pack() + #tk.Button(self, text="Done", command=lambda: master.switch_frame(Screen6), height=3, width=30, bg='peach puff').pack(pady=10) + + + + + + + +# draw a ribbon tag +class Screen8(tk.Frame): + def __init__(self, master): + tk.Frame.__init__(self, master, bg='#bcfef9') + + # Original dimensions + self.original_width = 325 + self.original_height = 179 + + # Doubled dimensions for display + self.display_width = self.original_width * 2 + self.display_height = self.original_height * 2 + + # Grid size (2x2 pixels) + self.grid_size = 2 + + # Add the home button + master.add_home_button(self) + + # Main container to hold all elements + main_container = tk.Frame(self, bg='#bcfef9') + main_container.place(relx=0.5, rely=0.5, anchor='center') + + # Left frame for drawing area and import button + left_frame = tk.Frame(main_container, bg='#bcfef9') + left_frame.pack(side='left', padx=(0, 20)) + + # Right frame for tools and buttons + right_frame = tk.Frame(main_container, bg='#bcfef9') + right_frame.pack(side='right') + + # Import Image Button + tk.Button(left_frame, text="Import Image", command=self.import_image, height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + + # Simplified instructions + self.label = tk.Label(left_frame, text="You may now draw your ribbon :)", wraplength=300, font=GlobalVars.TEXT_FONT, bg='#bcfef9') + self.label.pack(pady=10) + + # Drawing area (doubled size for display) + self.drawing = Image.new('1', (self.original_width, self.original_height), 1) + self.draw = ImageDraw.Draw(self.drawing) + self.last_draw = None + + # Set initial drawing color to black and size to 1 + self.draw_color = 'black' + self.draw_size = 1 + + # Canvas for drawing (doubled size for display) + self.canvas = Canvas(left_frame, width=self.display_width, height=self.display_height, bg='white') + self.canvas.bind("", self.draw_line) + self.canvas.bind("", self.reset_last_draw) + self.canvas.pack(pady=10) + + # Pen size frame + pen_size_frame = tk.Frame(right_frame, bg='#bcfef9') + pen_size_frame.pack(pady=10) + + tk.Label(pen_size_frame, text="Pen Size", font=GlobalVars.TEXT_FONT, bg='#bcfef9').pack() + + # Pen size buttons + pen_sizes = [(".", 1), ("*", 2), ("⚬", 3), ("⬤", 4), ("⬛", 5)] + for text, size in pen_sizes: + tk.Button(pen_size_frame, text=text, command=lambda s=size: self.set_draw_size(s), + height=2, width=5, bg='peach puff').pack(pady=2) + + # Pen color frame + pen_color_frame = tk.Frame(right_frame, bg='#bcfef9') + pen_color_frame.pack(pady=10) + + tk.Label(pen_color_frame, text="Pen Color", font=GlobalVars.TEXT_FONT, bg='#bcfef9').pack() + + # Color buttons + colors = ['black', 'gray', 'white'] + for color in colors: + tk.Button(pen_color_frame, height=2, width=5, bg=color, + command=lambda c=color: self.set_draw_color(c)).pack(pady=2) + + # Clear Drawing Button + tk.Button(right_frame, text="Clear Drawing", command=self.clear_drawing, + height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + + # Done button + tk.Button(right_frame, text="Done", command=self.next, + height=3, width=10, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + + def draw_line(self, event): + x, y = event.x // self.grid_size, event.y // self.grid_size + if self.last_draw: + points = self.get_points_on_line(*self.last_draw, x, y) + for px, py in points: + if self.draw_color == 'gray': + self.draw_dithered_point(px, py) + else: + self.draw_point(px, py) + self.last_draw = (x, y) + + def get_points_on_line(self, x0, y0, x1, y1): + points = [] + dx = abs(x1 - x0) + dy = abs(y1 - y0) + sx = 1 if x0 < x1 else -1 + sy = 1 if y0 < y1 else -1 + err = dx - dy + + while True: + points.append((x0, y0)) + if x0 == x1 and y0 == y1: + break + e2 = 2 * err + if e2 > -dy: + err -= dy + x0 += sx + if e2 < dx: + err += dx + y0 += sy + + return points + + def draw_point(self, x, y): + color = 0 if self.draw_color == 'black' else 1 + for dx in range(self.draw_size): + for dy in range(self.draw_size): + # Draw on the display canvas (2x2 pixels) + self.canvas.create_rectangle( + (x+dx)*self.grid_size, (y+dy)*self.grid_size, + (x+dx+1)*self.grid_size, (y+dy+1)*self.grid_size, + fill=self.draw_color, outline='' + ) + # Draw on the actual image (1x1 pixel) + self.draw.point((x+dx, y+dy), fill=color) + + def draw_dithered_point(self, x, y): + for dx in range(self.draw_size): + for dy in range(self.draw_size): + if (x+dx+y+dy) % 2 == 0: + # Draw on the display canvas (2x2 pixels) + self.canvas.create_rectangle( + (x+dx)*self.grid_size, (y+dy)*self.grid_size, + (x+dx+1)*self.grid_size, (y+dy+1)*self.grid_size, + fill='black', outline='' + ) + # Draw on the actual image (1x1 pixel) + self.draw.point((x+dx, y+dy), fill=0) + else: + # Draw on the display canvas (2x2 pixels) + self.canvas.create_rectangle( + (x+dx)*self.grid_size, (y+dy)*self.grid_size, + (x+dx+1)*self.grid_size, (y+dy+1)*self.grid_size, + fill='white', outline='' + ) + # Draw on the actual image (1x1 pixel) + self.draw.point((x+dx, y+dy), fill=1) + + def reset_last_draw(self, event): + self.last_draw = None + + def set_draw_color(self, color): + self.draw_color = color + + def set_draw_size(self, size): + self.draw_size = size + + def clear_drawing(self): + self.canvas.delete("all") + self.drawing = Image.new('1', (self.original_width, self.original_height), 1) + self.draw = ImageDraw.Draw(self.drawing) + + def import_image(self): + file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.jpeg *.png")]) + + if not file_path: + return + + img = Image.open(file_path) + img = img.convert('1') + if img.size != (self.original_width, self.original_height): + img = img.resize((self.original_width, self.original_height), Image.LANCZOS) + + self.drawing = img + self.draw = ImageDraw.Draw(self.drawing) + + # Display the image at 2x size + display_img = img.resize((self.display_width, self.display_height), Image.NEAREST) + self.imported_img = ImageTk.PhotoImage(display_img) + + self.canvas.delete("all") + self.canvas.create_image(0, 0, image=self.imported_img, anchor='nw') + + def next(self): + # The drawing is already at the correct size, so we can save it directly + self.drawing.save("drawing.png") + self.master.switch_frame(Screen13) + + +# txt update +class Screen9(tk.Frame): + def __init__(self, master): + tk.Frame.__init__(self, master, bg='#bcfef9') + master.add_home_button(self) + # Assume there's a method to manage the text entry + tk.Button(self, text="Done", command=lambda: master.switch_frame(Screen10), height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + +#THX BYE +class Screen10(tk.Frame): + def __init__(self, master): + + GlobalVars.selected_user = None # Reset the selected user + tk.Frame.__init__(self, master, bg='#bcfef9') + tk.Label(self, text="Thank you!", bg='#bcfef9', font=('Helvetica', 48)).pack() + tk.Button(self, text="Done", command=lambda: master.switch_frame(Screen0), height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10) + +# Sticker or tag? +class Screen11(tk.Frame): + def __init__(self, master): + + tk.Frame.__init__(self, master, bg='#bcfef9') + master.add_home_button(self) + # Instructions + self.label = tk.Label(self, text="Which type of tag would you like to design?", + wraplength=400, # adjust to suit needs + font=GlobalVars.TEXT_FONT) + self.label.pack(pady=50) + + # Button functions + def select_ribbon(): + global print_type + print_type = 'ribbon' + master.switch_frame(Screen8) + + def select_sticker(): + global print_type + print_type = 'sticker' + master.switch_frame(Screen4) + + + # Buttons + tk.Button(self, text="Sticker", command=select_sticker, height=4, width=39, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30) + tk.Button(self, text="Ribbon tag", command=select_ribbon, height=4, width=39, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30) + + +# after QR scanned for lookup +class Screen12(tk.Frame): + def __init__(self, master): + tk.Frame.__init__(self, master, bg='#bcfef9') + + print(f"Initializing Screen12 with QR code value: {GlobalVars.qr_code_value}") + + if GlobalVars.qr_code_value is None: + self.display_error("Error: QR code value is None") + return + + try: + qr_code_str = GlobalVars.qr_code_value if isinstance(GlobalVars.qr_code_value, str) else GlobalVars.qr_code_value.decode('utf-8') + print(f"Attempting to get message content for: {qr_code_str}") + message_content, image_path = addtoDB.get_message_content(qr_code_str) + print(f"Received message content: {message_content}") + print(f"Received image path: {image_path}") + + if message_content is None: + self.display_error(f"Error: Failed to retrieve message content for {qr_code_str}") + else: + self.display_content(message_content, image_path) + except Exception as e: + self.display_error(f"An error occurred while processing message: {str(e)}") + + def display_error(self, message): + print(f"Displaying error: {message}") + error_frame = tk.Frame(self, bg='#bcfef9') + error_frame.pack(expand=True, fill='both', padx=20, pady=20) + + tk.Label(error_frame, text=message, bg='#bcfef9', font=GlobalVars.TEXT_FONT, wraplength=500).pack(pady=50) + + # Add "Go Back" button for error cases + tk.Button(error_frame, text="Go Back", command=lambda: self.master.switch_frame(Screen0), + height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20) + + def display_content(self, message_content, image_path): + # Main content frame + content_frame = tk.Frame(self, bg='#bcfef9') + content_frame.pack(expand=True, fill='both', padx=20, pady=10) + + # Left column: Image + left_column = tk.Frame(content_frame, bg='#bcfef9') + left_column.pack(side='left', fill='both', expand=False) + + if image_path and os.path.exists(image_path): + try: + img = Image.open(image_path) + img.thumbnail((550, 550)) # Slightly smaller than before + photo = ImageTk.PhotoImage(img) + img_label = tk.Label(left_column, image=photo, bg='#bcfef9') + img_label.image = photo # Keep a reference + img_label.pack(expand=True, fill='both') + print(f"Displayed image: {image_path}") + except Exception as e: + print(f"Error displaying image {image_path}: {e}") + elif image_path: + print(f"Image file not found: {image_path}") + else: + print("No image path provided") + + # Right column: Scrollable text + right_column = tk.Frame(content_frame, bg='#bcfef9') + right_column.pack(side='right', fill='both', padx=10, expand=True) + + canvas = tk.Canvas(right_column, bg='#bcfef9') + scrollbar = ttk.Scrollbar(right_column, orient="vertical", command=canvas.yview) + scrollable_frame = tk.Frame(canvas, bg='#bcfef9') + + scrollable_frame.bind( + "", + lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + text_content = message_content.get('content', {}).get('text', '') + # Remove markdown image syntax + text_content = re.sub(r'!\[.*?\]\(.*?\)', '', text_content).strip() + tk.Label(scrollable_frame, text=text_content, wraplength=650, justify='left', bg='#bcfef9', font=("Helvetica", 28)).pack(pady=5, padx=(5, 0)) + + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # Configure the scrollbar style to make it larger + style = ttk.Style() + style.configure("Vertical.TScrollbar", arrowsize=48, width=48) + + # Add "Done" button for successful content display + tk.Button(self, text="Done", command=lambda: self.master.switch_frame(Screen0), + height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side="bottom", pady=20) + + +#time to print +class Screen13(tk.Frame): + def __init__(self, master): + + tk.Frame.__init__(self, master, bg='#bcfef9') + master.add_home_button(self) + + # Create a container to hold the widgets + container = tk.Frame(self) + container.place(relx=0.5, rely=0.5, anchor='center') + + # instructions + tk.Label(container, text="Wonderful! It is now time to post your item to Scuttlebutt and to print your tag. You can still cancel by hitting Start Over if you like.", wraplength=600, font=GlobalVars.TEXT_FONT).grid(row=0, column=0, columnspan=2) + + # buttons + master.add_home_button(self) + tk.Button(container, text="Print", command=self.printy, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).grid(row=2, column=0, pady=20) + + + # go ahead and print the thing + def printy(self): + global print_type, migration_ticket, MIGRATION_ITEMS_DIR + + # Specify the path to your image file + path_to_image = "/home/trav/Documents/custodiosk/freeze_frame.jpg" + + # Get QR data from the main application + QRX = self.master.QRX + QRY = self.master.QRY + QRscale = self.master.QRscale + + # make ssb post + key = addtoDB.addToSSB(path_to_image, info_text, 1) + + # If we have a migration ticket, append the message ID to the description file + if migration_ticket: + description_file_path = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.txt") + try: + with open(description_file_path, 'a') as f: + f.write(f"\n\n\n{key}") + print(f"Appended message ID to description file: {description_file_path}") + except Exception as e: + print(f"Error appending message ID to description file: {e}") + + # ssb give! (make sure we have a UID to give to first) + if GlobalVars.selected_user and GlobalVars.selected_user.strip() != "": + nothing = addtoDB.addToSSB(GlobalVars.selected_user, key, 2) + + # gonna need to revise this later but for now the textile tags are always full-size QR: + if print_type == "ribbon": + QRscale = 7 + + # Create qr code + #from https://ourcodeworld.com/articles/read/554/how-to-create-a-qr-code-image-or-svg-in-python + qr = qrcode.QRCode( + version = 1, + error_correction = qrcode.constants.ERROR_CORRECT_H, + box_size = QRscale, + border = 0, + ) + # Add data + qr.add_data(key) + qr.make(fit=True) + + # Create an image from the QR Code instance + img = qr.make_image() + whereToSaveQR = 'qr.jpg' + img.save(whereToSaveQR) + + # compose image for tag + drawing = Image.open("drawing.png") # drawing + qr = Image.open("qr.jpg") # qr + + #### merge em + + ## if sticker + if print_type == "sticker": + ## if we didn't custom set X/Y, set to defaults + if QRX is None and QRY is None: + QRX=506 + QRY=217 + merged_image = Image.new('L', (675, 375), "white") + merged_image.paste(drawing, (0, 8)) + merged_image.paste(qr, (QRX, QRY+8)) # we add 8 because this is slightly off from when we drew it + merged_image.save("merged_image.png") + + # if ribbon + if print_type == "ribbon": + merged_image = Image.new('L', (375, 675), "white") + merged_image.paste(drawing, (25, 100)) # set it 25 in because that's the border + merged_image.paste(qr, (42, 279)) # paste without mask + merged_image.save("merged_image.png") + image = Image.open("merged_image.png") + # rotated_image = image.transpose(Image.ROTATE_270) # Transpose and rotate 90 degrees, old version when we weren't doing ribbon vertical + # rotated_image.save("merged_image.png") + + # Get the ZPL code for the image + zpl_code = tozpl.print_to_zpl("merged_image.png") + + #save the zpl + # Open the file in write mode + with open("to_print.zpl", "w") as file: + # Write the string to the file + file.write(zpl_code) + + #print(zpl_code) #only needed for testing + #print (print_type) + + + # print to sticker printer + if print_type == "sticker": + try: + result = subprocess.Popen('lpr -P sticker_printer -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE, ) + # print('no print') + except: + print('traceback.format_exc():\n%s' % traceback.format_exc()) + exit() + # or print to tag printer: + if print_type == "ribbon": + try: + result = subprocess.Popen('lpr -P tag-printer -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE, ) + except: + print('traceback.format_exc():\n%s' % traceback.format_exc()) + exit() + + self.master.switch_frame(Screen10) # Switching to Screen10 after Done + +# lookup item +class Screen14(tk.Frame): + def __init__(self, master): + tk.Frame.__init__(self, master, bg='white') + + # Main container + self.main_container = tk.Frame(self, bg='white') + self.main_container.pack(fill='both', expand=True) + + # Video feed frame (left side) + self.video_frame = tk.Frame(self.main_container, bg='white') + self.video_frame.pack(side='left', fill='both', expand=True, padx=20, pady=20) + + # Instruction frame (right side) + self.instruction_frame = tk.Frame(self.main_container, bg='white') + self.instruction_frame.pack(side='right', fill='both', expand=True, padx=20, pady=20) + + # Setup the video feed + self.video = tk.Label(self.video_frame, bg='white') + self.video.pack(fill='both', expand=True) + + # Setup the instruction + self.instruction = tk.Label(self.instruction_frame, + text="Hold the QR code up to the camera", + font=("Helvetica", 36), + bg='white', fg='black', + wraplength=500, + justify='center') + self.instruction.pack(fill='both', expand=True) + + # Cancel button + self.cancel_button = tk.Button(self.instruction_frame, + text="Cancel", + command=lambda: self.master.switch_frame(Screen0), + height=3, + width=30, + bg='white', fg='black', + font=GlobalVars.BUTTON_FONT) + self.cancel_button.pack(pady=20) + + self.qreader = QReader() + self.cap = cv2.VideoCapture(2) + self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) + self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) + + self.frame_count = 0 + self.update_frame() + + def update_frame(self): + ret, frame = self.cap.read() + self.frame_count += 1 + + if ret: + # Process every 3rd frame for QR detection + if self.frame_count % 3 == 0: + # Convert to grayscale for QR detection + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + decoded_text = self.qreader.detect_and_decode(image=gray) + + if decoded_text: + if isinstance(decoded_text, tuple) and len(decoded_text) > 0: + qr_value = next((item for item in decoded_text if item is not None), None) + if qr_value: + GlobalVars.qr_code_value = qr_value + print(f"QR code scanned: {GlobalVars.qr_code_value}") + self.cap.release() + self.master.switch_frame(Screen12) + return + + # Display the original frame + rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + image = Image.fromarray(rgb_frame) + imgtk = ImageTk.PhotoImage(image=image) + self.video.imgtk = imgtk + self.video.configure(image=imgtk) + + self.after(10, self.update_frame) + + def destroy(self): + if self.cap.isOpened(): + self.cap.release() + super().destroy() + + + + +# New screen for Create Migration Item +class Screen15(tk.Frame): + def __init__(self, master): + + tk.Frame.__init__(self, master, bg='#bcfef9') + + # Create a main container frame + main_frame = tk.Frame(self, bg='#bcfef9') + main_frame.place(relx=0.5, rely=0.5, anchor='center') + + # Add the home button to the main frame (this will be at the top) + master.add_home_button(self) + + # Instructions (now in the centered main frame) + tk.Label(main_frame, text="If you're archiving an Item of Migration, please enter the last 3 digits of your ticket number. Otherwise, you can leave this blank.", font=GlobalVars.TEXT_FONT, bg='#bcfef9', wraplength=500).pack(pady=20) + + # Text box for entering ticket number (now in the centered main frame) + self.ticket_entry = tk.Entry(main_frame, font=GlobalVars.TEXT_FONT) + self.ticket_entry.pack(pady=20) + + # Done button (now in the centered main frame) + tk.Button(main_frame, text="Done", command=self.process_ticket, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20) + + # Set focus to the ticket entry field + self.ticket_entry.focus_set() + + def process_ticket(self): + global migration_ticket + ticket = self.ticket_entry.get().strip() + + if ticket == "": + # If the field is blank, proceed without setting migration_ticket + migration_ticket = "" + self.master.switch_frame(Screen1) # Go to the Scuttlebutt username selection screen + elif re.match(r'^\d{1,4}$', ticket): + if self.is_ticket_available(ticket): + migration_ticket = ticket + self.master.switch_frame(Screen1) # Go to the Scuttlebutt username selection screen + else: + messagebox.showerror("Invalid Input", "This migration number is already in use. Please try a different number.") + else: + messagebox.showerror("Invalid Input", "Please check your number and try again. It should be a 1-4 digit number or left blank.") + + def is_ticket_available(self, ticket): + # Get all files in the migration items directory + files = os.listdir(MIGRATION_ITEMS_DIR) + + # Strip file extensions and check if the ticket number exists + existing_numbers = [os.path.splitext(f)[0] for f in files] + return ticket not in existing_numbers + +if __name__ == "__main__": + app = Kiosk() + app.mainloop() + diff --git a/kiosk6.py b/kiosk6-backup-pre-claude.py similarity index 98% rename from kiosk6.py rename to kiosk6-backup-pre-claude.py index d616451..d8f46b6 100644 --- a/kiosk6.py +++ b/kiosk6-backup-pre-claude.py @@ -533,8 +533,14 @@ class Screen5(tk.Frame): # Saving the text from the entry field to a global variable global info_text info_text = self.info_entry.get("1.0", "end-1c") + # test + # print(info_text) # escape the newlines!: info_text = info_text.replace('\n', '\\n') + # print(info_text) + # escape quotes as well: + # info_text = info_text.replace('"', '\\"') + # print(info_text) self.master.switch_frame(Screen11) # I understand @@ -801,6 +807,10 @@ class Screen13(tk.Frame): # ssb give! (make sure we have a UID to give to first) if GlobalVars.selected_user and GlobalVars.selected_user.strip() != "": nothing = addtoDB.addToSSB(GlobalVars.selected_user,key,2) + + # gonna need to revise this later but for now the textile tags are always full-size QR: + if print_type == "ribbon": + QRscale = 7 # Create qr code #from https://ourcodeworld.com/articles/read/554/how-to-create-a-qr-code-image-or-svg-in-python @@ -840,7 +850,7 @@ class Screen13(tk.Frame): if print_type == "ribbon": merged_image = Image.new('L', (375, 675), "white") merged_image.paste(drawing, (25, 100)) # set it 25 in because that's the border - merged_image.paste(qr, (25, 279)) # paste without mask + merged_image.paste(qr, (42, 279)) # paste without mask merged_image.save("merged_image.png") image = Image.open("merged_image.png") # rotated_image = image.transpose(Image.ROTATE_270) # Transpose and rotate 90 degrees, old version when we weren't doing ribbon vertical @@ -863,6 +873,7 @@ class Screen13(tk.Frame): if print_type == "sticker": try: result = subprocess.Popen('lpr -P sticker_printer -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE, ) + # print('no print') except: print('traceback.format_exc():\n%s' % traceback.format_exc()) exit() diff --git a/ssb-custodisco-plugin.js b/ssb-custodisco-plugin.js new file mode 100644 index 0000000..1bdd83f --- /dev/null +++ b/ssb-custodisco-plugin.js @@ -0,0 +1,26 @@ +const ssbClient = require('ssb-client') + +module.exports = { + name: 'custodisco', + version: '1.0.0', + manifest: { + getItem: 'async' + }, + init: (server, config) => { + return { + getItem: async (id) => { + return new Promise((resolve, reject) => { + ssbClient((err, sbot) => { + if (err) return reject(err) + + sbot.get(id, (err, value) => { + sbot.close() + if (err) return reject(err) + resolve(value) + }) + }) + }) + } + } + } +} diff --git a/ssb-post.sh b/ssb-post.sh index 76c131f..5a3175a 100755 --- a/ssb-post.sh +++ b/ssb-post.sh @@ -65,3 +65,82 @@ BLAAB fi +## get_message_content +if [ "$1" == "get_message_content" ] +then + messageID=$2 + + echo "Attempting to retrieve message: $messageID" >&2 + + # Get message content + message_content=$(ssb-server get "$messageID" 2>&1) + + # Check if ssb-server get was successful + if [ $? -ne 0 ]; then + echo "Error: ssb-server get failed" >&2 + echo "$message_content" >&2 + exit 1 + fi + + echo "Successfully retrieved message content" >&2 + + # Extract blob information + blobs=$(echo "$message_content" | jq -r '.content.mentions[] | select(.link | startswith("&")) | .link') + +# Process blobs +if [ -n "$blobs" ]; then + echo "Processing blobs..." >&2 + echo "$message_content" | jq -c '.content.mentions[]' | while read -r mention; do + blob=$(echo "$mention" | jq -r '.link') + if [[ "$blob" == \&* ]]; then + echo "Found a blob: $blob" >&2 + # First, try to get the mime type from the message content + mime_type=$(echo "$mention" | jq -r '.type') + + # If mime type is not in the message content, try to get it from blobs.meta + if [[ "$mime_type" == "null" || -z "$mime_type" ]]; then + if ssb-server blobs.has "$blob"; then + mime_type=$(ssb-server blobs.meta "$blob" | jq -r '.type') + else + echo "Blob not available locally: $blob" >&2 + continue + fi + fi + + echo "Blob mime type: $mime_type" >&2 + + if [[ "$mime_type" == "image/jpeg" || "$mime_type" == "image/png" ]]; then + # Create tmp directory if it doesn't exist + mkdir -p tmp + + # Get blob and save it to a file + file_extension="${mime_type#image/}" + short_name=$(echo "$blob" | md5sum | cut -d' ' -f1) + file_name="${short_name}.$file_extension" + full_path="tmp/$file_name" + + echo "Attempting to save blob to: $full_path" >&2 + echo "Executing command: ssb-server blobs.get $blob > $full_path" >&2 + + if ssb-server blobs.get "$blob" > "$full_path" 2>/dev/null; then + echo "Blob saved: $full_path" >&2 + echo "IMAGE_PATH:$full_path" + else + echo "Failed to retrieve blob: $blob" >&2 + echo "ssb-server blobs.get exit code: $?" >&2 + # If the file wasn't created, let's try to output the blob data directly + echo "Attempting to output blob data directly:" >&2 + ssb-server blobs.get "$blob" | head -c 100 | xxd >&2 + fi + else + echo "Skipping non-image blob: $blob (type: $mime_type)" >&2 + fi + fi + done +else + echo "No blobs found in the message." >&2 +fi + + # Output message content + echo "$message_content" +fi