it is now split into files and there are many things
This commit is contained in:
24
agents.md
24
agents.md
@ -3,6 +3,24 @@ kiosk.py is the main program here. It's the front-end to a physical kiosk with 2
|
||||
the whole thing is graphical so it's hard for you to test so you'll need to ask the user to test.
|
||||
|
||||
you'll run into errors if you try `python -m py_compile kiosk.py && echo "Syntax OK"`
|
||||
you gotta do `python3 -m py_compile
|
||||
/home/trav/Documents/custodisco-kiosk/kiosk.py && echo "Syntax
|
||||
OK"`
|
||||
you gotta do `python3 -m py_compile /home/trav/Documents/custodisco-kiosk/kiosk.py && echo "Syntax OK"`
|
||||
|
||||
## QR Code Sizing
|
||||
|
||||
SSB message IDs are always 52 characters (format: `%<43-char-base64>=.sha256`).
|
||||
Due to special characters (`%` and `.`), QR uses **byte mode encoding**.
|
||||
|
||||
With ERROR_CORRECT_H for 52 bytes:
|
||||
- **Version 6** is required
|
||||
- Module count = 4 × 6 + 17 = **41 modules**
|
||||
- QR pixel size = **41 × scale**
|
||||
|
||||
| Scale | QR Size (px) | Notes |
|
||||
|-------|--------------|-------|
|
||||
| 3 | 123 | Small - fits in 650×360 canvas |
|
||||
| 6 | 246 | Medium - fits in canvas |
|
||||
| 8 | 328 | Large - max that fits in 360px height with margin |
|
||||
| 9 | 369 | Too tall for 360px canvas |
|
||||
|
||||
Canvas dimensions: 650×360
|
||||
Merged image: 675×375 (drawing pasted at y=8)
|
||||
|
||||
24
desktop-mode.sh
Executable file
24
desktop-mode.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Switch to GNOME desktop mode
|
||||
# Run with: sudo ./desktop-mode.sh
|
||||
|
||||
ACCOUNTS_FILE="/var/lib/AccountsService/users/trav"
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run with sudo: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop the kiosk script service first
|
||||
sudo -u trav XDG_RUNTIME_DIR=/run/user/$(id -u trav) systemctl --user stop org.gnome.Kiosk.Script.service 2>/dev/null
|
||||
sudo -u trav XDG_RUNTIME_DIR=/run/user/$(id -u trav) systemctl --user mask org.gnome.Kiosk.Script.service 2>/dev/null
|
||||
|
||||
# Update AccountsService to use GNOME desktop
|
||||
cat > "$ACCOUNTS_FILE" << EOF
|
||||
[User]
|
||||
Session=gnome-wayland
|
||||
SystemAccount=false
|
||||
EOF
|
||||
|
||||
echo "Switched to desktop mode."
|
||||
echo "Run 'sudo systemctl restart gdm3' to apply, or log out and back in."
|
||||
101
kiosk-commands.md
Normal file
101
kiosk-commands.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Custodisco Kiosk - Systemd Services & Commands
|
||||
|
||||
## Services Overview
|
||||
|
||||
### 1. ssb-server.service (User Service)
|
||||
- **Purpose**: Runs the Scuttlebutt server for decentralized social features
|
||||
- **Location**: `~/.config/systemd/user/ssb-server.service`
|
||||
- **Auto-starts**: Yes, via `WantedBy=default.target`
|
||||
|
||||
### 2. org.gnome.Kiosk.Script.service (GNOME User Service)
|
||||
- **Purpose**: Runs the kiosk application via ~/.local/bin/gnome-kiosk-script
|
||||
- **Location**: `/usr/lib/systemd/user/org.gnome.Kiosk.Script.service`
|
||||
- **Auto-starts**: Only when session type is `gnome-kiosk-script-wayland`
|
||||
|
||||
### 3. gdm.service (System Service)
|
||||
- **Purpose**: GNOME Display Manager - handles login and session management
|
||||
- **Controls**: Which session type to start (kiosk vs desktop)
|
||||
|
||||
## SSH Commands
|
||||
|
||||
### Check Service Status
|
||||
```bash
|
||||
# SSB server status
|
||||
systemctl --user status ssb-server.service
|
||||
|
||||
# Kiosk script status (only relevant in kiosk mode)
|
||||
systemctl --user status org.gnome.Kiosk.Script.service
|
||||
|
||||
# GDM status
|
||||
systemctl status gdm3
|
||||
```
|
||||
|
||||
### Restart Services
|
||||
```bash
|
||||
# Restart SSB server
|
||||
systemctl --user restart ssb-server.service
|
||||
|
||||
# Restart GDM (will log out user and restart display)
|
||||
sudo systemctl restart gdm3
|
||||
```
|
||||
|
||||
### Switch to Kiosk Mode
|
||||
```bash
|
||||
cd /home/trav/Documents/custodisco-kiosk
|
||||
sudo ./kiosk-mode.sh
|
||||
sudo systemctl restart gdm3
|
||||
```
|
||||
|
||||
### Switch to Desktop Mode
|
||||
```bash
|
||||
cd /home/trav/Documents/custodisco-kiosk
|
||||
sudo ./desktop-mode.sh
|
||||
sudo systemctl restart gdm3
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# SSB server logs
|
||||
journalctl --user -u ssb-server.service -f
|
||||
|
||||
# Kiosk script logs
|
||||
journalctl --user -u org.gnome.Kiosk.Script.service -f
|
||||
```
|
||||
|
||||
## How Boot Works
|
||||
|
||||
1. System boots, starts `gdm.service`
|
||||
2. GDM reads `/var/lib/AccountsService/users/trav` to get session type
|
||||
3. If `Session=gnome-kiosk-script-wayland`, GDM starts kiosk session
|
||||
4. Kiosk session starts `org.gnome.Kiosk.Script.service`
|
||||
5. Service runs `/usr/bin/gnome-kiosk-script` which calls `~/.local/bin/gnome-kiosk-script`
|
||||
6. That script activates venv and runs `python kiosk.py`
|
||||
|
||||
## File Locations
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `~/.local/bin/gnome-kiosk-script` | Kiosk startup script |
|
||||
| `~/Documents/custodisco-kiosk/kiosk-mode.sh` | Switch to kiosk mode |
|
||||
| `~/Documents/custodisco-kiosk/desktop-mode.sh` | Switch to desktop mode |
|
||||
| `~/.config/systemd/user/ssb-server.service` | SSB server service file |
|
||||
| `/var/lib/AccountsService/users/trav` | AccountsService config (root owned) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Kiosk not starting at boot
|
||||
1. Check session type: `cat /var/lib/AccountsService/users/trav`
|
||||
2. Should show `Session=gnome-kiosk-script-wayland`
|
||||
3. If not, run: `sudo ./kiosk-mode.sh && sudo systemctl restart gdm3`
|
||||
|
||||
### SSB server not running
|
||||
```bash
|
||||
systemctl --user status ssb-server.service
|
||||
systemctl --user restart ssb-server.service
|
||||
journalctl --user -u ssb-server.service --no-pager -n 50
|
||||
```
|
||||
|
||||
### Check if kiosk.py is running
|
||||
```bash
|
||||
ps aux | grep kiosk.py
|
||||
```
|
||||
23
kiosk-mode.sh
Executable file
23
kiosk-mode.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# Switch to GNOME kiosk mode
|
||||
# Run with: sudo ./kiosk-mode.sh
|
||||
|
||||
ACCOUNTS_FILE="/var/lib/AccountsService/users/trav"
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run with sudo: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Unmask the kiosk script service
|
||||
sudo -u trav XDG_RUNTIME_DIR=/run/user/$(id -u trav) systemctl --user unmask org.gnome.Kiosk.Script.service 2>/dev/null
|
||||
|
||||
# Update AccountsService to use kiosk session
|
||||
cat > "$ACCOUNTS_FILE" << EOF
|
||||
[User]
|
||||
Session=gnome-kiosk-script-wayland
|
||||
SystemAccount=false
|
||||
EOF
|
||||
|
||||
echo "Switched to kiosk mode."
|
||||
echo "Run 'sudo systemctl restart gdm3' to apply, or log out and back in."
|
||||
6
kiosk/__init__.py
Normal file
6
kiosk/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Custodisco Kiosk - A touchscreen kiosk for item tagging."""
|
||||
from kiosk.app import Kiosk
|
||||
from kiosk.config import CONFIG, BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
|
||||
__all__ = ['Kiosk', 'CONFIG', 'BG_COLOR', 'GlobalVars']
|
||||
BIN
kiosk/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
kiosk/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/__pycache__/app.cpython-313.pyc
Normal file
BIN
kiosk/__pycache__/app.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/__pycache__/config.cpython-313.pyc
Normal file
BIN
kiosk/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/__pycache__/state.cpython-313.pyc
Normal file
BIN
kiosk/__pycache__/state.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/__pycache__/utils.cpython-313.pyc
Normal file
BIN
kiosk/__pycache__/utils.cpython-313.pyc
Normal file
Binary file not shown.
92
kiosk/app.py
Normal file
92
kiosk/app.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""Main Kiosk application controller."""
|
||||
import subprocess
|
||||
import tkinter as tk
|
||||
from tkinter import font as tkfont, messagebox
|
||||
|
||||
from kiosk.config import CONFIG
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.screens.home import Screen0
|
||||
|
||||
|
||||
class Kiosk(tk.Tk):
|
||||
"""Main kiosk application controller."""
|
||||
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 = 3
|
||||
|
||||
# Initialize fonts
|
||||
GlobalVars.BUTTON_FONT = tkfont.Font(size=24, family='Helvetica')
|
||||
GlobalVars.TEXT_FONT = tkfont.Font(size=30, family='Georgia')
|
||||
|
||||
# Key blocking for kiosk mode (unless developer_mode is enabled)
|
||||
self.keys_blocked = False
|
||||
if not CONFIG.get("developer_mode", False):
|
||||
self._check_network_and_block_keys()
|
||||
|
||||
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 _has_network(self):
|
||||
"""Check if we have network connectivity."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["ping", "-c", "1", "-W", "2", "8.8.8.8"],
|
||||
capture_output=True, check=True
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
def _block_keys(self):
|
||||
"""Block Alt/Super keys to prevent exiting kiosk."""
|
||||
if self.keys_blocked:
|
||||
return
|
||||
self.bind_all("<Alt_L>", lambda e: "break")
|
||||
self.bind_all("<Alt_R>", lambda e: "break")
|
||||
self.bind_all("<Super_L>", lambda e: "break")
|
||||
self.bind_all("<Super_R>", lambda e: "break")
|
||||
self.bind_all("<Alt-Tab>", lambda e: "break")
|
||||
self.bind_all("<Alt-F4>", lambda e: "break")
|
||||
self.keys_blocked = True
|
||||
|
||||
def _check_network_and_block_keys(self):
|
||||
"""Check network; block keys if connected, otherwise retry."""
|
||||
if self._has_network():
|
||||
self._block_keys()
|
||||
else:
|
||||
self.after(5000, self._check_network_and_block_keys)
|
||||
|
||||
def start_over(self):
|
||||
# Reset global variables
|
||||
GlobalVars.qr_code_value = None
|
||||
GlobalVars.print_type = "neither"
|
||||
GlobalVars.selected_user = None
|
||||
GlobalVars.ribbon_size = None
|
||||
|
||||
# Switch to the home screen
|
||||
self.switch_frame(Screen0)
|
||||
23
kiosk/config.py
Normal file
23
kiosk/config.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""Configuration loading and global settings."""
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
def load_config():
|
||||
"""Load configuration from config.json."""
|
||||
config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config.json')
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
print("Warning: config.json not found. Copy config.example.json to config.json and customize it.")
|
||||
return {
|
||||
"printers": {"sticker": "sticker_printer", "ribbon": "tag-printer"},
|
||||
"camera": {"preferred_name": "NexiGo"},
|
||||
"ribbon": {"width": 450, "margin": 50},
|
||||
"background_color": "#bcfef9"
|
||||
}
|
||||
|
||||
|
||||
CONFIG = load_config()
|
||||
BG_COLOR = CONFIG.get("background_color", "#bcfef9")
|
||||
28
kiosk/screens/__init__.py
Normal file
28
kiosk/screens/__init__.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Screen components for the kiosk application."""
|
||||
from .home import Screen0
|
||||
from .intro import HaveYouGeneratedTagScreen, InfoPage1, InfoPage2, InfoPage3
|
||||
from .ssb_selection import Screen1, ScreenWhyScuttlebutt, Screen2
|
||||
from .camera import Screen3, Screen14
|
||||
from .drawing import Screen4, Screen8
|
||||
from .description import Screen5
|
||||
from .confirmation import Screen6, Screen11, ScreenRibbonSize
|
||||
from .print_flow import Screen13, PrintFailedScreen
|
||||
from .completion import Screen10
|
||||
from .lookup import Screen12
|
||||
from .placeholder import Screen7, Screen9
|
||||
from .no_qr_flow import NoQRRibbonSizeScreen, NoQRDrawingScreen, NoQRImportImageScreen, NoQRPrintScreen
|
||||
|
||||
__all__ = [
|
||||
'Screen0',
|
||||
'HaveYouGeneratedTagScreen', 'InfoPage1', 'InfoPage2', 'InfoPage3',
|
||||
'Screen1', 'ScreenWhyScuttlebutt', 'Screen2',
|
||||
'Screen3', 'Screen14',
|
||||
'Screen4', 'Screen8',
|
||||
'Screen5',
|
||||
'Screen6', 'Screen11', 'ScreenRibbonSize',
|
||||
'Screen13', 'PrintFailedScreen',
|
||||
'Screen10',
|
||||
'Screen12',
|
||||
'Screen7', 'Screen9',
|
||||
'NoQRRibbonSizeScreen', 'NoQRDrawingScreen', 'NoQRImportImageScreen', 'NoQRPrintScreen',
|
||||
]
|
||||
BIN
kiosk/screens/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/camera.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/camera.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/completion.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/completion.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/confirmation.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/confirmation.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/description.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/description.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/drawing.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/drawing.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/home.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/home.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/intro.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/intro.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/lookup.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/lookup.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/no_qr_flow.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/no_qr_flow.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/placeholder.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/placeholder.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/print_flow.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/print_flow.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/screens/__pycache__/ssb_selection.cpython-313.pyc
Normal file
BIN
kiosk/screens/__pycache__/ssb_selection.cpython-313.pyc
Normal file
Binary file not shown.
223
kiosk/screens/camera.py
Normal file
223
kiosk/screens/camera.py
Normal file
@ -0,0 +1,223 @@
|
||||
"""Camera screens (Screen3 for photo, Screen14 for QR lookup)."""
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
from PIL import Image, ImageTk
|
||||
import cv2
|
||||
import threading
|
||||
import time
|
||||
|
||||
from qreader import QReader
|
||||
|
||||
from kiosk.config import CONFIG, BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.utils import get_preferred_camera_index
|
||||
from kiosk.widgets import RoundedLabel
|
||||
|
||||
|
||||
class Screen3(tk.Frame):
|
||||
"""Take photo of item."""
|
||||
def __init__(self, master=None):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# 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
|
||||
|
||||
# Prefer the external NexiGo USB webcam when available
|
||||
self.vid = cv2.VideoCapture(get_preferred_camera_index(CONFIG["camera"]["preferred_name"]))
|
||||
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=BG_COLOR)
|
||||
self.text_frame.pack(side="right", fill="both", expand=True)
|
||||
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)
|
||||
|
||||
# Instructional text for photo
|
||||
instruction_text = ("Now take a photo of your object. If you don't have it with you "
|
||||
"you can draw a detailed drawing of it and take a picture of that. "
|
||||
"Please be mindful of who/what you include in the photo: "
|
||||
"THIS PHOTO IS POSTED ONLINE AND CANNOT BE DELETED")
|
||||
self.instruction_label = RoundedLabel(self.text_frame, text=instruction_text, bg='white',
|
||||
font=('Georgia', 18), wraplength=500, justify="center")
|
||||
self.instruction_label.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("<Destroy>", 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()
|
||||
from kiosk.screens.home import Screen0
|
||||
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=("Georgia", 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):
|
||||
self.release_resources()
|
||||
from kiosk.screens.description import Screen5
|
||||
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()
|
||||
|
||||
|
||||
class Screen14(tk.Frame):
|
||||
"""Lookup item - QR scanner."""
|
||||
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=("Georgia", 36),
|
||||
bg='white', fg='black',
|
||||
wraplength=500,
|
||||
justify='center')
|
||||
self.instruction.pack(fill='both', expand=True)
|
||||
|
||||
# Deferred import for navigation
|
||||
def go_home():
|
||||
from kiosk.screens.home import Screen0
|
||||
self.master.switch_frame(Screen0)
|
||||
|
||||
# Cancel button
|
||||
self.cancel_button = tk.Button(self.instruction_frame,
|
||||
text="Cancel",
|
||||
command=go_home,
|
||||
height=3,
|
||||
width=30,
|
||||
bg='white', fg='black',
|
||||
font=GlobalVars.BUTTON_FONT)
|
||||
self.cancel_button.pack(pady=20)
|
||||
|
||||
self.qreader = QReader()
|
||||
# Prefer the external NexiGo USB webcam when available
|
||||
self.cap = cv2.VideoCapture(get_preferred_camera_index(CONFIG["camera"]["preferred_name"]))
|
||||
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()
|
||||
from kiosk.screens.lookup import Screen12
|
||||
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()
|
||||
126
kiosk/screens/completion.py
Normal file
126
kiosk/screens/completion.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""Completion screen (Screen10 - Thank you)."""
|
||||
import tkinter as tk
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from kiosk.config import CONFIG, BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel
|
||||
|
||||
|
||||
class Screen10(tk.Frame):
|
||||
"""Thank you / completion screen."""
|
||||
def __init__(self, master):
|
||||
GlobalVars.selected_user = None # Reset the selected user
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# Center content vertically and horizontally
|
||||
container = tk.Frame(self, bg=BG_COLOR)
|
||||
container.pack(expand=True)
|
||||
|
||||
RoundedLabel(container, text="Thank you!", bg='white', font=('Georgia', 48)).pack(pady=(0, 20))
|
||||
|
||||
# Video player frame
|
||||
self.video_frame = tk.Frame(container, width=640, height=400, bg='black')
|
||||
self.video_frame.pack(pady=20)
|
||||
self.video_frame.pack_propagate(False)
|
||||
|
||||
# Video label for displaying frames
|
||||
self.video_label = tk.Label(self.video_frame, bg='black')
|
||||
self.video_label.pack(fill='both', expand=True)
|
||||
|
||||
# Determine which video to play
|
||||
self.video_file = self._get_video_file()
|
||||
self.cap = None
|
||||
self.running = False
|
||||
self.photo = None # Keep reference to prevent garbage collection
|
||||
|
||||
if self.video_file and os.path.exists(self.video_file):
|
||||
self.cap = cv2.VideoCapture(self.video_file)
|
||||
self.running = True
|
||||
self._update_frame()
|
||||
|
||||
# Button container for side-by-side layout
|
||||
button_frame = tk.Frame(container, bg=BG_COLOR)
|
||||
button_frame.pack(pady=10)
|
||||
|
||||
tk.Button(button_frame, text="Done", command=self._cleanup_and_done,
|
||||
height=2, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=10)
|
||||
tk.Button(button_frame, text="printer problems?", command=self._cleanup_and_failed,
|
||||
height=2, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=10)
|
||||
|
||||
def _get_video_file(self):
|
||||
"""Determine which video to play based on print type and config."""
|
||||
base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
if GlobalVars.print_type == "sticker":
|
||||
return os.path.join(base_path, "sticker.mp4")
|
||||
elif GlobalVars.print_type == "ribbon":
|
||||
has_cutter = CONFIG.get("ribbon", {}).get("has_cutter", False)
|
||||
if has_cutter:
|
||||
return os.path.join(base_path, "ribbon-no-cut.mp4")
|
||||
else:
|
||||
return os.path.join(base_path, "ribbon-cut.mp4")
|
||||
return None
|
||||
|
||||
def _update_frame(self):
|
||||
"""Update video frame using OpenCV."""
|
||||
if not self.running or self.cap is None:
|
||||
return
|
||||
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
# Loop: reset to beginning
|
||||
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
return
|
||||
|
||||
# Convert BGR to RGB
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
|
||||
# Get original dimensions
|
||||
h, w = frame.shape[:2]
|
||||
target_w, target_h = 640, 400 # Larger video area
|
||||
|
||||
# Calculate scale to fit while preserving aspect ratio
|
||||
scale = min(target_w / w, target_h / h)
|
||||
new_w, new_h = int(w * scale), int(h * scale)
|
||||
|
||||
# Resize preserving aspect ratio
|
||||
frame = cv2.resize(frame, (new_w, new_h))
|
||||
|
||||
# Create black background and center the frame (letterboxing)
|
||||
canvas = np.zeros((target_h, target_w, 3), dtype=np.uint8)
|
||||
x_offset = (target_w - new_w) // 2
|
||||
y_offset = (target_h - new_h) // 2
|
||||
canvas[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = frame
|
||||
frame = canvas
|
||||
|
||||
# Convert to PIL Image then to PhotoImage
|
||||
img = Image.fromarray(frame)
|
||||
self.photo = ImageTk.PhotoImage(image=img)
|
||||
self.video_label.config(image=self.photo)
|
||||
|
||||
# Schedule next frame update (~30fps = 33ms)
|
||||
if self.running:
|
||||
self.after(33, self._update_frame)
|
||||
|
||||
def _cleanup_and_done(self):
|
||||
"""Stop video and go to home screen."""
|
||||
self.running = False
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
from kiosk.screens.home import Screen0
|
||||
self.master.switch_frame(Screen0)
|
||||
|
||||
def _cleanup_and_failed(self):
|
||||
"""Stop video and go to print failed screen."""
|
||||
self.running = False
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
from kiosk.screens.print_flow import PrintFailedScreen
|
||||
self.master.switch_frame(PrintFailedScreen)
|
||||
145
kiosk/screens/confirmation.py
Normal file
145
kiosk/screens/confirmation.py
Normal file
@ -0,0 +1,145 @@
|
||||
"""Confirmation screens (Screen6, Screen11, ScreenRibbonSize)."""
|
||||
import tkinter as tk
|
||||
|
||||
from kiosk.config import CONFIG, BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel
|
||||
|
||||
|
||||
class Screen6(tk.Frame):
|
||||
"""I understand screen."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
master.add_home_button(self)
|
||||
|
||||
def go_to_screen3():
|
||||
from kiosk.screens.camera import Screen3
|
||||
master.switch_frame(Screen3)
|
||||
|
||||
tk.Button(self, text="I Understand", command=go_to_screen3, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10)
|
||||
|
||||
|
||||
class Screen11(tk.Frame):
|
||||
"""Sticker or tag selection screen."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
master.add_home_button(self)
|
||||
|
||||
# Instructions
|
||||
self.label = RoundedLabel(self, text="Which type of tag would you like to design?",
|
||||
wraplength=400,
|
||||
font=GlobalVars.TEXT_FONT,
|
||||
bg='white')
|
||||
self.label.pack(pady=50)
|
||||
|
||||
# Button functions
|
||||
def select_ribbon():
|
||||
GlobalVars.print_type = 'ribbon'
|
||||
master.switch_frame(ScreenRibbonSize)
|
||||
|
||||
def select_sticker():
|
||||
GlobalVars.print_type = 'sticker'
|
||||
from kiosk.screens.drawing import Screen4
|
||||
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)
|
||||
|
||||
|
||||
class ScreenRibbonSize(tk.Frame):
|
||||
"""Select ribbon size screen."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
master.add_home_button(self)
|
||||
|
||||
# Get ribbon config
|
||||
self.ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
self.margin = CONFIG.get("ribbon", {}).get("margin", 50)
|
||||
self.printable_width = self.ribbon_width - 50 # 25px margin each side
|
||||
|
||||
# Title
|
||||
title_label = RoundedLabel(self, text="Select your ribbon size:",
|
||||
font=GlobalVars.TEXT_FONT, bg='white', padx=40, wraplength=0)
|
||||
title_label.pack(pady=30)
|
||||
|
||||
# Container for the three size options
|
||||
options_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
options_frame.pack(pady=20)
|
||||
|
||||
# Size options with multipliers
|
||||
sizes = [
|
||||
('Small', 0.5, 'Half-height drawing'),
|
||||
('Medium', 1.0, 'Square drawing'),
|
||||
('Large', 1.5, 'Tall drawing')
|
||||
]
|
||||
|
||||
for size_name, multiplier, description in sizes:
|
||||
self.create_size_option(options_frame, size_name, multiplier, description, master)
|
||||
|
||||
def create_size_option(self, parent, size_name, multiplier, description, master):
|
||||
# Frame for each option
|
||||
option_frame = tk.Frame(parent, bg=BG_COLOR, padx=20)
|
||||
option_frame.pack(side='left', padx=20)
|
||||
|
||||
# Calculate dimensions for this size (based on printable width)
|
||||
drawing_height = int(self.printable_width * multiplier)
|
||||
qr_scale = self.printable_width // 21
|
||||
qr_size = 21 * qr_scale
|
||||
# Total height includes top/bottom margins
|
||||
total_height = self.margin + drawing_height + qr_size + self.margin
|
||||
|
||||
# Create preview canvas (scaled down for display)
|
||||
preview_scale = 0.3
|
||||
preview_width = int(self.ribbon_width * preview_scale)
|
||||
preview_height = int(total_height * preview_scale)
|
||||
margin_preview = int(self.margin * preview_scale)
|
||||
drawing_preview_height = int(drawing_height * preview_scale)
|
||||
qr_preview_size = int(qr_size * preview_scale)
|
||||
side_margin_preview = int(25 * preview_scale) # 25px side margins
|
||||
|
||||
canvas = tk.Canvas(option_frame, width=preview_width, height=preview_height,
|
||||
bg='white', highlightthickness=2, highlightbackground='gray')
|
||||
canvas.pack(pady=10)
|
||||
|
||||
# Draw top margin area
|
||||
canvas.create_rectangle(0, 0, preview_width, margin_preview,
|
||||
fill='#f5f5f5', outline='')
|
||||
|
||||
# Draw drawing area (with side margins indicated)
|
||||
canvas.create_rectangle(side_margin_preview, margin_preview,
|
||||
preview_width - side_margin_preview, margin_preview + drawing_preview_height,
|
||||
fill='#e0e0e0', outline='black')
|
||||
canvas.create_text(preview_width // 2, margin_preview + drawing_preview_height // 2,
|
||||
text="Drawing", font=('Georgia', 10))
|
||||
|
||||
# Draw QR indicator (centered)
|
||||
qr_x = (preview_width - qr_preview_size) // 2
|
||||
qr_y = margin_preview + drawing_preview_height
|
||||
canvas.create_rectangle(qr_x, qr_y,
|
||||
qr_x + qr_preview_size, qr_y + qr_preview_size,
|
||||
fill='#808080', outline='black')
|
||||
canvas.create_text(preview_width // 2, qr_y + qr_preview_size // 2,
|
||||
text="QR", font=('Georgia', 8), fill='white')
|
||||
|
||||
# Draw bottom margin area
|
||||
canvas.create_rectangle(0, qr_y + qr_preview_size, preview_width, preview_height,
|
||||
fill='#f5f5f5', outline='')
|
||||
|
||||
# Size name and dimensions button (show printable dimensions)
|
||||
button_text = f"{size_name}\n{self.printable_width}×{drawing_height}px"
|
||||
btn = tk.Button(option_frame, text=button_text,
|
||||
command=lambda s=size_name.lower(): self.select_size(s, master),
|
||||
height=3, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT)
|
||||
btn.pack(pady=10)
|
||||
|
||||
# Description label (hidden)
|
||||
# desc_label = tk.Label(option_frame, text=description,
|
||||
# font=('Georgia', 12), bg='white')
|
||||
# desc_label.pack()
|
||||
|
||||
def select_size(self, size, master):
|
||||
GlobalVars.ribbon_size = size
|
||||
from kiosk.screens.drawing import Screen8
|
||||
master.switch_frame(Screen8)
|
||||
36
kiosk/screens/description.py
Normal file
36
kiosk/screens/description.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""Description entry screen (Screen5)."""
|
||||
import tkinter as tk
|
||||
|
||||
from kiosk.config import BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.utils import sanitize_for_ssb
|
||||
from kiosk.widgets import RoundedLabel
|
||||
|
||||
|
||||
class Screen5(tk.Frame):
|
||||
"""Typed description entry screen."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
master.add_home_button(self)
|
||||
|
||||
# Adding the information label
|
||||
self.info_label = RoundedLabel(self, text="Please enter a description of your item (maybe some stories of its history or why it's important to you or identifying features). You may can use Markdown formatting. Once posted this text is on the internet FOREVER.", font=GlobalVars.TEXT_FONT, wraplength=900, bg='white')
|
||||
self.info_label.pack(pady=40)
|
||||
|
||||
# Adding the text entry field
|
||||
self.info_entry = tk.Text(self, height=10, width=50, font=("Georgia", 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):
|
||||
GlobalVars.info_text = self.info_entry.get("1.0", "end-1c")
|
||||
GlobalVars.info_text = sanitize_for_ssb(GlobalVars.info_text)
|
||||
|
||||
from kiosk.screens.confirmation import Screen11
|
||||
self.master.switch_frame(Screen11)
|
||||
271
kiosk/screens/drawing.py
Normal file
271
kiosk/screens/drawing.py
Normal file
@ -0,0 +1,271 @@
|
||||
"""Drawing screens (Screen4 for sticker, Screen8 for ribbon)."""
|
||||
import tkinter as tk
|
||||
from tkinter import Canvas, filedialog
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from kiosk.config import CONFIG, BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel, DrawingMixin
|
||||
|
||||
|
||||
class Screen4(tk.Frame, DrawingMixin):
|
||||
"""Draw a sticker."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# Configure column minsizes
|
||||
self.grid_columnconfigure(0, minsize=622)
|
||||
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=BG_COLOR)
|
||||
self.left_frame.grid(row=0, column=0, padx=2, pady=(10, 30))
|
||||
|
||||
# Frame for the tools
|
||||
self.right_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
self.right_frame.grid(row=0, column=2, padx=70)
|
||||
|
||||
# Add instructions
|
||||
self.label = RoundedLabel(self.left_frame, text="You may now draw your sticker! Anything you draw inside the QR box will get covered by a QR code", wraplength=597, font=GlobalVars.TEXT_FONT, bg='white')
|
||||
self.label.pack(pady=(20, 10))
|
||||
|
||||
# QR Size selection buttons
|
||||
self.qr_size_frame = tk.Frame(self.left_frame, bg=BG_COLOR)
|
||||
self.qr_size_frame.pack(pady=(5, 15))
|
||||
|
||||
tk.Label(self.qr_size_frame, text="QR Size:", font=GlobalVars.BUTTON_FONT, bg=BG_COLOR).pack(side='left', padx=5)
|
||||
|
||||
for name, label in [('small', 'Small'), ('medium', 'Medium'), ('large', 'Large')]:
|
||||
tk.Button(self.qr_size_frame, text=label,
|
||||
command=lambda n=name: self.set_qr_size(n),
|
||||
height=1, width=8, bg='peach puff',
|
||||
font=GlobalVars.BUTTON_FONT).pack(side='left', padx=5)
|
||||
|
||||
# Creating the Canvas for drawing
|
||||
self.canvas = Canvas(self.left_frame, width=597, height=360, bg='white')
|
||||
self.canvas.bind("<B1-Motion>", self.draw_line)
|
||||
self.image_on_canvas = None
|
||||
self.canvas.pack(pady=(20, 40))
|
||||
self.canvas.bind("<ButtonRelease-1>", self.reset_last_draw)
|
||||
|
||||
# Initialize drawing via mixin (grid_size=1 for stickers)
|
||||
self.init_drawing(597, 360, grid_size=1)
|
||||
self.draw_size = 3 # Override default size for stickers
|
||||
self.imported_img = None
|
||||
|
||||
# QR size positions and defaults
|
||||
# SSB message IDs = 52 bytes → Version 6 QR → 41 modules
|
||||
# QR pixel size = 41 * scale. Positions calculated with ~20px margins.
|
||||
# X positions shifted left by 53 to account for narrower canvas (scan-tag on right edge at merge)
|
||||
self.qr_positions = {
|
||||
'small': (454, 217, 3), # 123px QR
|
||||
'medium': (331, 94, 6), # 246px QR
|
||||
'large': (249, 12, 8) # 328px QR
|
||||
}
|
||||
self.qr_x, self.qr_y, self.qr_scale = self.qr_positions['small']
|
||||
self.master.QRX = self.qr_x
|
||||
self.master.QRY = self.qr_y
|
||||
self.master.QRscale = self.qr_scale
|
||||
self.add_qr_box()
|
||||
|
||||
# Create frames for pen size and color tools
|
||||
self.pen_size_frame = tk.Frame(self.right_frame, bg=BG_COLOR)
|
||||
self.pen_size_frame.pack(pady=(0, 20))
|
||||
self.pen_color_frame = tk.Frame(self.right_frame, bg=BG_COLOR)
|
||||
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=BG_COLOR).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=BG_COLOR).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)
|
||||
|
||||
# Frame for Done and Import Image buttons (horizontal row)
|
||||
self.action_buttons_frame = tk.Frame(self.right_frame, bg=BG_COLOR)
|
||||
self.action_buttons_frame.pack(pady=10)
|
||||
|
||||
# Done button (left)
|
||||
tk.Button(self.action_buttons_frame, text="Done", command=self.next,
|
||||
height=2, width=8, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=5)
|
||||
|
||||
# Import Image Button (right)
|
||||
tk.Button(self.action_buttons_frame, text="Import", command=self.import_image,
|
||||
height=2, width=8, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=5)
|
||||
|
||||
# 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=BG_COLOR, font=GlobalVars.TEXT_FONT)
|
||||
self.info_label.pack(pady=5)
|
||||
|
||||
def clear_drawing(self):
|
||||
# Override to add QR box after clearing
|
||||
DrawingMixin.clear_drawing(self)
|
||||
self.imported_img = None
|
||||
self.add_qr_box()
|
||||
|
||||
def add_qr_box(self):
|
||||
"""Draw QR placeholder box using current size settings."""
|
||||
qr_size = 41 * self.qr_scale # 41 modules for Version 6 QR (52-byte SSB message ID)
|
||||
|
||||
# Draw QR placeholder
|
||||
self.canvas.create_rectangle(self.qr_x, self.qr_y,
|
||||
self.qr_x + qr_size, self.qr_y + qr_size,
|
||||
outline='black', fill='white', tags='qr_box')
|
||||
self.canvas.create_text(self.qr_x + qr_size/2, self.qr_y + qr_size/2,
|
||||
text="QR", fill="black", tags='qr_box')
|
||||
|
||||
def set_qr_size(self, size_name):
|
||||
"""Update QR size and redraw preview."""
|
||||
self.qr_x, self.qr_y, self.qr_scale = self.qr_positions[size_name]
|
||||
self.master.QRX = self.qr_x
|
||||
self.master.QRY = self.qr_y
|
||||
self.master.QRscale = self.qr_scale
|
||||
# Just delete and redraw the QR box, not the whole canvas
|
||||
self.canvas.delete('qr_box')
|
||||
self.add_qr_box()
|
||||
|
||||
def next(self):
|
||||
self.drawing.save("drawing.png")
|
||||
from kiosk.screens.print_flow import Screen13
|
||||
self.master.switch_frame(Screen13)
|
||||
|
||||
def import_image(self):
|
||||
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.jpeg *.png")])
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
self.canvas.delete("all")
|
||||
img = Image.open(file_path)
|
||||
img = img.convert('1')
|
||||
if img.size[0] > 597 or img.size[1] > 360:
|
||||
img = img.crop((0, 0, 597, 360))
|
||||
|
||||
self.drawing.paste(img)
|
||||
self.imported_img = ImageTk.PhotoImage(img)
|
||||
self.canvas.create_image(0, 0, image=self.imported_img, anchor='nw')
|
||||
self.add_qr_box()
|
||||
self.info_label.config(text="") # Clear any old info
|
||||
|
||||
|
||||
class Screen8(tk.Frame, DrawingMixin):
|
||||
"""Draw a ribbon tag."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# Get ribbon width from config and calculate dimensions based on selected size
|
||||
self.ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
self.printable_width = self.ribbon_width - 50 # 25px margin each side
|
||||
multiplier = {'small': 0.5, 'medium': 1.0, 'large': 1.5}.get(GlobalVars.ribbon_size, 1.0)
|
||||
width = self.printable_width
|
||||
height = int(self.printable_width * multiplier)
|
||||
|
||||
# Adaptive display scaling based on size
|
||||
# Target max display height ~700px to fit comfortably on screen
|
||||
max_display_height = 700
|
||||
ideal_scale = max_display_height / height
|
||||
scale_factor = min(2, max(1, int(ideal_scale))) # Clamp between 1x and 2x
|
||||
|
||||
# Display dimensions
|
||||
display_width = width * scale_factor
|
||||
display_height = height * scale_factor
|
||||
|
||||
# Main container to hold all columns
|
||||
main_container = tk.Frame(self, bg=BG_COLOR)
|
||||
main_container.place(relx=0.5, rely=0.5, anchor='center')
|
||||
|
||||
# Column 1: Start Over button and instruction text
|
||||
col1_frame = tk.Frame(main_container, bg=BG_COLOR)
|
||||
col1_frame.pack(side='left', padx=(15, 25), anchor='n', pady=20)
|
||||
|
||||
# Start Over button (replaces home button) - Screen8
|
||||
tk.Button(col1_frame, text="Start Over", command=master.show_warning_dialog,
|
||||
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
|
||||
|
||||
# Instruction label
|
||||
self.label = RoundedLabel(col1_frame, text="Draw your\nribbon :)",
|
||||
wraplength=150, font=GlobalVars.TEXT_FONT, bg='white')
|
||||
self.label.pack(pady=5)
|
||||
|
||||
# Column 2: Drawing canvas
|
||||
col2_frame = tk.Frame(main_container, bg=BG_COLOR)
|
||||
col2_frame.pack(side='left', padx=(10, 30))
|
||||
|
||||
# Canvas for drawing
|
||||
self.canvas = Canvas(col2_frame, width=display_width, height=display_height, bg='white')
|
||||
self.canvas.bind("<B1-Motion>", self.draw_line)
|
||||
self.canvas.bind("<ButtonRelease-1>", self.reset_last_draw)
|
||||
self.canvas.pack()
|
||||
|
||||
# Initialize drawing via mixin
|
||||
self.init_drawing(width, height, grid_size=scale_factor)
|
||||
|
||||
# Right panel: Pen tools (grid layout) + action buttons
|
||||
right_panel = tk.Frame(main_container, bg=BG_COLOR)
|
||||
right_panel.pack(side='left', padx=15)
|
||||
|
||||
# Pen tools grid (labels align with buttons)
|
||||
pen_grid = tk.Frame(right_panel, bg=BG_COLOR)
|
||||
pen_grid.pack()
|
||||
|
||||
# Pen Size label - spans vertically alongside pen size buttons
|
||||
tk.Label(pen_grid, text="Pen\nSize", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).grid(
|
||||
row=0, column=0, rowspan=5, padx=(0, 10), sticky='n', pady=(5, 0))
|
||||
|
||||
# Pen size buttons (5 buttons in grid column 1)
|
||||
pen_sizes = [(".", 1), ("*", 2), ("⚬", 3), ("⬤", 4), ("⬛", 5)]
|
||||
for i, (text, size) in enumerate(pen_sizes):
|
||||
tk.Button(pen_grid, text=text, command=lambda s=size: self.set_draw_size(s),
|
||||
height=2, width=5, bg='peach puff').grid(row=i, column=1, pady=2)
|
||||
|
||||
# Pen Color label - spans vertically alongside color buttons
|
||||
tk.Label(pen_grid, text="Pen\nColor", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).grid(
|
||||
row=6, column=0, rowspan=3, padx=(0, 10), sticky='n', pady=(15, 0))
|
||||
|
||||
# Pen color buttons (3 buttons in grid column 1)
|
||||
colors = ['black', 'gray', 'white']
|
||||
for i, color in enumerate(colors):
|
||||
tk.Button(pen_grid, height=2, width=5, bg=color,
|
||||
command=lambda c=color: self.set_draw_color(c)).grid(row=6+i, column=1, pady=2)
|
||||
|
||||
# Action buttons below the grid
|
||||
action_frame = tk.Frame(right_panel, bg=BG_COLOR)
|
||||
action_frame.pack(pady=(20, 0))
|
||||
|
||||
# Import Image Button
|
||||
tk.Button(action_frame, text="Import Image", command=self.import_image,
|
||||
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
|
||||
|
||||
# Clear Drawing Button
|
||||
tk.Button(action_frame, text="Clear Drawing", command=self.clear_drawing,
|
||||
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
|
||||
|
||||
# Done button
|
||||
tk.Button(action_frame, text="Done", command=self.next,
|
||||
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
|
||||
|
||||
def import_image(self):
|
||||
self.import_image_base()
|
||||
|
||||
def next(self):
|
||||
self.drawing.save("drawing.png")
|
||||
from kiosk.screens.print_flow import Screen13
|
||||
self.master.switch_frame(Screen13)
|
||||
67
kiosk/screens/home.py
Normal file
67
kiosk/screens/home.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Home screen (Screen0)."""
|
||||
import tkinter as tk
|
||||
from tkinter import font as tkfont
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
|
||||
from kiosk.config import CONFIG, BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel
|
||||
|
||||
|
||||
class Screen0(tk.Frame):
|
||||
"""Home screen with main navigation options."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
title_font = tkfont.Font(size=42, family='Helvetica') # 30% bigger
|
||||
|
||||
# Split the screen into two frames
|
||||
left_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
right_frame = tk.Frame(self, bg=BG_COLOR, 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 - using custo.png image (scaled to 1/4)
|
||||
logo_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'custo.png')
|
||||
logo_image = Image.open(logo_path)
|
||||
new_size = (logo_image.width // 4, logo_image.height // 4)
|
||||
logo_image = logo_image.resize(new_size, Image.LANCZOS)
|
||||
self.logo_photo = ImageTk.PhotoImage(logo_image)
|
||||
title_label = tk.Label(left_frame, image=self.logo_photo, bg=BG_COLOR)
|
||||
title_label.pack(side='top', pady=50)
|
||||
|
||||
# Info text below logo
|
||||
info_label = RoundedLabel(left_frame, text="this is a touchscreen kiosk\n\n more information on the project at\n\nwww.cust.ooo",
|
||||
bg='white', font=GlobalVars.TEXT_FONT, wraplength=650, justify='center')
|
||||
info_label.pack(side='top', pady=20)
|
||||
|
||||
# Deferred imports to avoid circular dependencies
|
||||
def go_to_generate_tag():
|
||||
from kiosk.screens.intro import HaveYouGeneratedTagScreen
|
||||
master.switch_frame(HaveYouGeneratedTagScreen)
|
||||
|
||||
def go_to_lookup():
|
||||
from kiosk.screens.camera import Screen14
|
||||
master.switch_frame(Screen14)
|
||||
|
||||
def go_to_no_qr():
|
||||
from kiosk.screens.no_qr_flow import NoQRRibbonSizeScreen
|
||||
master.switch_frame(NoQRRibbonSizeScreen)
|
||||
|
||||
tk.Button(right_frame, text="Generate Tag", command=go_to_generate_tag, height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30)
|
||||
|
||||
tk.Button(right_frame, text="Lookup Item", command=go_to_lookup, height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30)
|
||||
|
||||
# No QR Ribbon button (only show if enabled in config)
|
||||
if CONFIG.get("show_no_qr_ribbon", 0) == 1:
|
||||
tk.Button(right_frame, text="No QR Ribbon", command=go_to_no_qr, height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30)
|
||||
|
||||
# Create the quit button (only show in developer mode)
|
||||
if CONFIG.get("developer_mode", False):
|
||||
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()
|
||||
172
kiosk/screens/intro.py
Normal file
172
kiosk/screens/intro.py
Normal file
@ -0,0 +1,172 @@
|
||||
"""Introduction screens (HaveYouGeneratedTagScreen, InfoPage1-3)."""
|
||||
import tkinter as tk
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
|
||||
from kiosk.config import BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel
|
||||
|
||||
|
||||
class HaveYouGeneratedTagScreen(tk.Frame):
|
||||
"""Have you generated a tag before?"""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# Center container
|
||||
container = tk.Frame(self, bg=BG_COLOR)
|
||||
container.place(relx=0.5, rely=0.5, anchor='center')
|
||||
|
||||
# Question label
|
||||
label = RoundedLabel(container, text="have you made a tag on this kiosk before?",
|
||||
font=GlobalVars.TEXT_FONT, bg='white', padx=20, pady=10, wraplength=0)
|
||||
label.pack(pady=50)
|
||||
|
||||
# Deferred imports
|
||||
def go_to_screen1():
|
||||
from kiosk.screens.ssb_selection import Screen1
|
||||
master.switch_frame(Screen1)
|
||||
|
||||
def go_to_info1():
|
||||
master.switch_frame(InfoPage1)
|
||||
|
||||
# Buttons
|
||||
tk.Button(container, text="yea", command=go_to_screen1,
|
||||
height=3, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
|
||||
tk.Button(container, text="not yet!", command=go_to_info1,
|
||||
height=3, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
|
||||
|
||||
|
||||
class InfoPage1(tk.Frame):
|
||||
"""Info Page 1 - Custo introduction with tags.jpg"""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# Split the screen into two frames
|
||||
left_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
right_frame = tk.Frame(self, bg=BG_COLOR, padx=40)
|
||||
left_frame.grid(row=0, column=0, sticky='nsew')
|
||||
right_frame.grid(row=0, column=1, sticky='nsew')
|
||||
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(1, weight=1)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# Left side: tags.jpg image
|
||||
image_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'tags.jpg')
|
||||
img = Image.open(image_path)
|
||||
# Scale to fit nicely (max 300px height)
|
||||
max_height = 400
|
||||
if img.height > max_height:
|
||||
ratio = max_height / img.height
|
||||
new_size = (int(img.width * ratio), max_height)
|
||||
img = img.resize(new_size, Image.LANCZOS)
|
||||
self.photo = ImageTk.PhotoImage(img)
|
||||
img_label = tk.Label(left_frame, image=self.photo, bg=BG_COLOR)
|
||||
img_label.pack(expand=True)
|
||||
|
||||
# Right side: Custo introduction text
|
||||
intro_text = """This kiosk is called Custo (pronounced KOOSTO)
|
||||
|
||||
It is here to foster and document the relationships between people and things
|
||||
|
||||
It can print stickers and ribbons to affix to an item you care about."""
|
||||
|
||||
text_label = RoundedLabel(right_frame, text=intro_text,
|
||||
font=GlobalVars.TEXT_FONT, bg='white',
|
||||
wraplength=500, justify='left', padx=20, pady=20)
|
||||
text_label.pack(expand=True)
|
||||
|
||||
# Bottom: Forward button only
|
||||
nav_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
nav_frame.grid(row=1, column=0, columnspan=2, sticky='ew', pady=20)
|
||||
|
||||
tk.Button(nav_frame, text="Forward →", command=lambda: master.switch_frame(InfoPage2),
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='right', padx=40)
|
||||
|
||||
|
||||
class InfoPage2(tk.Frame):
|
||||
"""Info Page 2 - Take care of each other with hard.jpg"""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# Split the screen into two frames
|
||||
left_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
right_frame = tk.Frame(self, bg=BG_COLOR, padx=40)
|
||||
left_frame.grid(row=0, column=0, sticky='nsew')
|
||||
right_frame.grid(row=0, column=1, sticky='nsew')
|
||||
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(1, weight=1)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# Left side: hard.jpg image
|
||||
image_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'hard.jpg')
|
||||
img = Image.open(image_path)
|
||||
# Scale to fit nicely (max 300px height)
|
||||
max_height = 500
|
||||
if img.height > max_height:
|
||||
ratio = max_height / img.height
|
||||
new_size = (int(img.width * ratio), max_height)
|
||||
img = img.resize(new_size, Image.LANCZOS)
|
||||
self.photo = ImageTk.PhotoImage(img)
|
||||
img_label = tk.Label(left_frame, image=self.photo, bg=BG_COLOR)
|
||||
img_label.pack(expand=True)
|
||||
|
||||
# Right side: Take care of each other text
|
||||
care_text = """Items can live long lives! Let's take care of each other while we're alive.
|
||||
|
||||
by tagging an item you are saying to it, 'you matter, I will take care of you'"""
|
||||
|
||||
text_label = RoundedLabel(right_frame, text=care_text,
|
||||
font=GlobalVars.TEXT_FONT, bg='white',
|
||||
wraplength=500, justify='left', padx=20, pady=20)
|
||||
text_label.pack(expand=True)
|
||||
|
||||
# Bottom: Back and Forward buttons
|
||||
nav_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
nav_frame.grid(row=1, column=0, columnspan=2, sticky='ew', pady=20)
|
||||
|
||||
tk.Button(nav_frame, text="← Back", command=lambda: master.switch_frame(InfoPage1),
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=40)
|
||||
tk.Button(nav_frame, text="Forward →", command=lambda: master.switch_frame(InfoPage3),
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='right', padx=40)
|
||||
|
||||
|
||||
class InfoPage3(tk.Frame):
|
||||
"""Info Page 3 - Mindful use, permanent storage, no shitposting"""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# Center container for text
|
||||
container = tk.Frame(self, bg=BG_COLOR)
|
||||
container.place(relx=0.5, rely=0.4, anchor='center')
|
||||
|
||||
# Mindful use text
|
||||
mindful_text = """Please be mindful while operating the kiosk.
|
||||
|
||||
All generated items are stored permanently and replicated across the Scuttlebutt network. Once published, it cannot be deleted.
|
||||
|
||||
Please do not create imaginary items or shitpost.
|
||||
|
||||
The QR codes contain a Scuttlebutt messageID, scanning it works best at www.cust.ooo or any Scuttlebutt client.
|
||||
"""
|
||||
|
||||
text_label = RoundedLabel(container, text=mindful_text,
|
||||
font=GlobalVars.TEXT_FONT, bg='white',
|
||||
wraplength=700, justify='center', padx=30, pady=30)
|
||||
text_label.pack()
|
||||
|
||||
# Deferred import
|
||||
def go_to_screen1():
|
||||
from kiosk.screens.ssb_selection import Screen1
|
||||
master.switch_frame(Screen1)
|
||||
|
||||
# Bottom: Back and Forward buttons
|
||||
nav_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
nav_frame.place(relx=0.5, rely=0.9, anchor='center')
|
||||
|
||||
tk.Button(nav_frame, text="← Back", command=lambda: master.switch_frame(InfoPage2),
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=40)
|
||||
tk.Button(nav_frame, text="Forward →", command=go_to_screen1,
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='right', padx=40)
|
||||
139
kiosk/screens/lookup.py
Normal file
139
kiosk/screens/lookup.py
Normal file
@ -0,0 +1,139 @@
|
||||
"""Lookup screen (Screen12 - after QR scanned)."""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
import addtoDB
|
||||
|
||||
from kiosk.config import BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel
|
||||
|
||||
|
||||
class Screen12(tk.Frame):
|
||||
"""Display content after QR scanned for lookup."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
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, replies = addtoDB.get_message_content(qr_code_str)
|
||||
print(f"Received message content: {message_content}")
|
||||
print(f"Received image path: {image_path}")
|
||||
print(f"Received replies: {replies}")
|
||||
|
||||
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, replies)
|
||||
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=BG_COLOR)
|
||||
error_frame.pack(expand=True, fill='both', padx=20, pady=20)
|
||||
|
||||
RoundedLabel(error_frame, text=message, bg='white', font=GlobalVars.TEXT_FONT, wraplength=500).pack(pady=50)
|
||||
|
||||
# Add "Go Back" button for error cases
|
||||
def go_home():
|
||||
from kiosk.screens.home import Screen0
|
||||
self.master.switch_frame(Screen0)
|
||||
|
||||
tk.Button(error_frame, text="Go Back", command=go_home,
|
||||
height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
|
||||
|
||||
def get_alias(self, user_id):
|
||||
try:
|
||||
with open('users.json') as f:
|
||||
users = json.load(f)
|
||||
for user in users:
|
||||
if user['id'] == user_id:
|
||||
return user.get('alias', '')
|
||||
except FileNotFoundError:
|
||||
return ''
|
||||
return ''
|
||||
|
||||
def display_content(self, message_content, image_path, replies):
|
||||
# Main content frame
|
||||
content_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
content_frame.pack(expand=True, fill='both', padx=20, pady=10)
|
||||
|
||||
# Left column: Image
|
||||
left_column = tk.Frame(content_frame, bg=BG_COLOR)
|
||||
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=BG_COLOR)
|
||||
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=BG_COLOR)
|
||||
right_column.pack(side='right', fill='both', padx=10, expand=True)
|
||||
|
||||
canvas = tk.Canvas(right_column, bg=BG_COLOR)
|
||||
scrollbar = ttk.Scrollbar(right_column, orient="vertical", command=canvas.yview)
|
||||
scrollable_frame = tk.Frame(canvas, bg=BG_COLOR)
|
||||
|
||||
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)
|
||||
|
||||
# Display original message
|
||||
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='white', font=("Georgia", 28)).pack(pady=5, padx=(5, 0))
|
||||
|
||||
# Display replies
|
||||
if replies:
|
||||
tk.Label(scrollable_frame, text="Replies:", wraplength=650, justify='left', bg='white', font=("Georgia", 24, "bold")).pack(pady=(20, 5), padx=(5, 0))
|
||||
for reply in replies:
|
||||
author_id = reply.get('value', {}).get('author', 'Unknown')
|
||||
author_alias = self.get_alias(author_id)
|
||||
author_display = f"{author_alias} ({author_id})" if author_alias else author_id
|
||||
reply_text = reply.get('value', {}).get('content', {}).get('text', '')
|
||||
tk.Label(scrollable_frame, text=f"{author_display}:", wraplength=650, justify='left', bg='white', font=("Georgia", 20, "bold")).pack(pady=(10, 0), padx=(5, 0))
|
||||
tk.Label(scrollable_frame, text=reply_text, wraplength=650, justify='left', bg='white', font=("Georgia", 18)).pack(pady=(0, 10), 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
|
||||
def go_home():
|
||||
from kiosk.screens.home import Screen0
|
||||
self.master.switch_frame(Screen0)
|
||||
|
||||
tk.Button(self, text="Done", command=go_home,
|
||||
height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side="bottom", pady=20)
|
||||
426
kiosk/screens/no_qr_flow.py
Normal file
426
kiosk/screens/no_qr_flow.py
Normal file
@ -0,0 +1,426 @@
|
||||
"""No QR ribbon flow screens."""
|
||||
import tkinter as tk
|
||||
from tkinter import Canvas, filedialog
|
||||
import subprocess
|
||||
import traceback
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
import tozpl
|
||||
|
||||
from kiosk.config import CONFIG, BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel, DrawingMixin
|
||||
|
||||
|
||||
class NoQRRibbonSizeScreen(tk.Frame):
|
||||
"""No QR Ribbon - Size Selection."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
master.add_home_button(self)
|
||||
|
||||
# Get ribbon config
|
||||
self.ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
self.margin = CONFIG.get("ribbon", {}).get("margin", 50)
|
||||
self.printable_width = self.ribbon_width - 50 # 25px margin each side
|
||||
|
||||
# Title
|
||||
title_label = RoundedLabel(self, text="Select your ribbon size:",
|
||||
font=GlobalVars.TEXT_FONT, bg='white')
|
||||
title_label.pack(pady=30)
|
||||
|
||||
# Container for size options
|
||||
options_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
options_frame.pack(pady=20)
|
||||
|
||||
# Size options with multipliers
|
||||
sizes = [
|
||||
('Small', 0.5, 'Half-height drawing'),
|
||||
('Medium', 1.0, 'Square drawing'),
|
||||
('Large', 1.5, 'Tall drawing')
|
||||
]
|
||||
|
||||
for size_name, multiplier, description in sizes:
|
||||
self.create_size_option(options_frame, size_name, multiplier, description, master)
|
||||
|
||||
# Add Import Image option
|
||||
self.create_import_option(options_frame, master)
|
||||
|
||||
def create_import_option(self, parent, master):
|
||||
"""Create the Import Image option column"""
|
||||
option_frame = tk.Frame(parent, bg=BG_COLOR, padx=20)
|
||||
option_frame.pack(side='left', padx=20)
|
||||
|
||||
# Create preview canvas (show a simplified "variable height" preview)
|
||||
preview_scale = 0.3
|
||||
preview_width = int(self.ribbon_width * preview_scale)
|
||||
# Use medium size for base preview height
|
||||
base_height = int((self.margin + self.printable_width + self.margin) * preview_scale)
|
||||
margin_preview = int(self.margin * preview_scale)
|
||||
side_margin_preview = int(25 * preview_scale)
|
||||
|
||||
canvas = tk.Canvas(option_frame, width=preview_width, height=base_height,
|
||||
bg='white', highlightthickness=2, highlightbackground='gray')
|
||||
canvas.pack(pady=10)
|
||||
|
||||
# Draw top margin
|
||||
canvas.create_rectangle(0, 0, preview_width, margin_preview,
|
||||
fill='#f5f5f5', outline='')
|
||||
|
||||
# Draw image area (dashed to indicate variable)
|
||||
drawing_height = base_height - 2 * margin_preview
|
||||
canvas.create_rectangle(side_margin_preview, margin_preview,
|
||||
preview_width - side_margin_preview, margin_preview + drawing_height,
|
||||
fill='#d0e8d0', outline='black', dash=(4, 2))
|
||||
canvas.create_text(preview_width // 2, margin_preview + drawing_height // 2,
|
||||
text="Your\nImage", font=('Georgia', 10))
|
||||
|
||||
# Draw bottom margin
|
||||
canvas.create_rectangle(0, margin_preview + drawing_height, preview_width, base_height,
|
||||
fill='#f5f5f5', outline='')
|
||||
|
||||
# Import button
|
||||
button_text = f"Import Image\n{self.printable_width}px wide"
|
||||
btn = tk.Button(option_frame, text=button_text,
|
||||
command=lambda: master.switch_frame(NoQRImportImageScreen),
|
||||
height=3, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT)
|
||||
btn.pack(pady=10)
|
||||
|
||||
# Description
|
||||
desc_label = tk.Label(option_frame, text="Variable height",
|
||||
font=('Georgia', 12), bg='white')
|
||||
desc_label.pack()
|
||||
|
||||
def create_size_option(self, parent, size_name, multiplier, description, master):
|
||||
# Frame for each option
|
||||
option_frame = tk.Frame(parent, bg=BG_COLOR, padx=20)
|
||||
option_frame.pack(side='left', padx=20)
|
||||
|
||||
# Calculate dimensions for this size (based on printable width)
|
||||
drawing_height = int(self.printable_width * multiplier)
|
||||
|
||||
# Total height includes top/bottom margins (no QR)
|
||||
total_height = self.margin + drawing_height + self.margin
|
||||
|
||||
# Create preview canvas (scaled down for display)
|
||||
preview_scale = 0.3
|
||||
preview_width = int(self.ribbon_width * preview_scale)
|
||||
preview_height = int(total_height * preview_scale)
|
||||
margin_preview = int(self.margin * preview_scale)
|
||||
drawing_preview_height = int(drawing_height * preview_scale)
|
||||
side_margin_preview = int(25 * preview_scale) # 25px side margins
|
||||
|
||||
canvas = tk.Canvas(option_frame, width=preview_width, height=preview_height,
|
||||
bg='white', highlightthickness=2, highlightbackground='gray')
|
||||
canvas.pack(pady=10)
|
||||
|
||||
# Draw top margin area
|
||||
canvas.create_rectangle(0, 0, preview_width, margin_preview,
|
||||
fill='#f5f5f5', outline='')
|
||||
|
||||
# Draw drawing area (with side margins indicated)
|
||||
canvas.create_rectangle(side_margin_preview, margin_preview,
|
||||
preview_width - side_margin_preview, margin_preview + drawing_preview_height,
|
||||
fill='#e0e0e0', outline='black')
|
||||
canvas.create_text(preview_width // 2, margin_preview + drawing_preview_height // 2,
|
||||
text="Drawing", font=('Georgia', 10))
|
||||
|
||||
# Draw bottom margin area
|
||||
canvas.create_rectangle(0, margin_preview + drawing_preview_height, preview_width, preview_height,
|
||||
fill='#f5f5f5', outline='')
|
||||
|
||||
# Size name and dimensions button (show printable dimensions)
|
||||
button_text = f"{size_name}\n{self.printable_width}×{drawing_height}px"
|
||||
btn = tk.Button(option_frame, text=button_text,
|
||||
command=lambda s=size_name.lower(): self.select_size(s, master),
|
||||
height=3, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT)
|
||||
btn.pack(pady=10)
|
||||
|
||||
# Description label
|
||||
desc_label = tk.Label(option_frame, text=description,
|
||||
font=('Georgia', 12), bg='white')
|
||||
desc_label.pack()
|
||||
|
||||
def select_size(self, size, master):
|
||||
GlobalVars.ribbon_size = size
|
||||
master.switch_frame(NoQRDrawingScreen)
|
||||
|
||||
|
||||
class NoQRDrawingScreen(tk.Frame, DrawingMixin):
|
||||
"""No QR Ribbon - Drawing Screen."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# Get ribbon width from config and calculate dimensions based on selected size
|
||||
ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
printable_width = ribbon_width - 50 # 25px margin each side
|
||||
multiplier = {'small': 0.5, 'medium': 1.0, 'large': 1.5}.get(GlobalVars.ribbon_size, 1.0)
|
||||
width = printable_width
|
||||
height = int(printable_width * multiplier)
|
||||
|
||||
# Adaptive display scaling based on size
|
||||
max_display_height = 700
|
||||
ideal_scale = max_display_height / height
|
||||
scale_factor = min(2, max(1, int(ideal_scale)))
|
||||
|
||||
# Display dimensions
|
||||
display_width = width * scale_factor
|
||||
display_height = height * scale_factor
|
||||
|
||||
# Main container to hold all columns
|
||||
main_container = tk.Frame(self, bg=BG_COLOR)
|
||||
main_container.place(relx=0.5, rely=0.5, anchor='center')
|
||||
|
||||
# Column 1: Start Over button and instruction text
|
||||
col1_frame = tk.Frame(main_container, bg=BG_COLOR)
|
||||
col1_frame.pack(side='left', padx=(15, 25), anchor='n', pady=20)
|
||||
|
||||
# Start Over button - NoQRDrawingScreen
|
||||
tk.Button(col1_frame, text="Start Over", command=master.show_warning_dialog,
|
||||
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
|
||||
|
||||
# Instruction label
|
||||
self.label = RoundedLabel(col1_frame, text="Draw your\nribbon :)",
|
||||
wraplength=150, font=GlobalVars.TEXT_FONT, bg='white')
|
||||
self.label.pack(pady=5)
|
||||
|
||||
# Column 2: Drawing canvas
|
||||
col2_frame = tk.Frame(main_container, bg=BG_COLOR)
|
||||
col2_frame.pack(side='left', padx=(10, 30))
|
||||
|
||||
# Canvas for drawing
|
||||
self.canvas = Canvas(col2_frame, width=display_width, height=display_height, bg='white')
|
||||
self.canvas.bind("<B1-Motion>", self.draw_line)
|
||||
self.canvas.bind("<ButtonRelease-1>", self.reset_last_draw)
|
||||
self.canvas.pack()
|
||||
|
||||
# Initialize drawing via mixin
|
||||
self.init_drawing(width, height, grid_size=scale_factor)
|
||||
|
||||
# Right panel: Pen tools + action buttons
|
||||
right_panel = tk.Frame(main_container, bg=BG_COLOR)
|
||||
right_panel.pack(side='left', padx=15)
|
||||
|
||||
# Pen tools grid
|
||||
pen_grid = tk.Frame(right_panel, bg=BG_COLOR)
|
||||
pen_grid.pack()
|
||||
|
||||
# Pen Size label
|
||||
tk.Label(pen_grid, text="Pen\nSize", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).grid(
|
||||
row=0, column=0, rowspan=5, padx=(0, 10), sticky='n', pady=(5, 0))
|
||||
|
||||
# Pen size buttons
|
||||
pen_sizes = [(".", 1), ("*", 2), ("⚬", 3), ("⬤", 4), ("⬛", 5)]
|
||||
for i, (text, size) in enumerate(pen_sizes):
|
||||
tk.Button(pen_grid, text=text, command=lambda s=size: self.set_draw_size(s),
|
||||
height=2, width=5, bg='peach puff').grid(row=i, column=1, pady=2)
|
||||
|
||||
# Pen Color label
|
||||
tk.Label(pen_grid, text="Pen\nColor", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).grid(
|
||||
row=6, column=0, rowspan=3, padx=(0, 10), sticky='n', pady=(15, 0))
|
||||
|
||||
# Pen color buttons
|
||||
colors = ['black', 'gray', 'white']
|
||||
for i, color in enumerate(colors):
|
||||
tk.Button(pen_grid, height=2, width=5, bg=color,
|
||||
command=lambda c=color: self.set_draw_color(c)).grid(row=6+i, column=1, pady=2)
|
||||
|
||||
# Action buttons
|
||||
action_frame = tk.Frame(right_panel, bg=BG_COLOR)
|
||||
action_frame.pack(pady=(20, 0))
|
||||
|
||||
# Import Image Button
|
||||
tk.Button(action_frame, text="Import Image", command=self.import_image,
|
||||
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
|
||||
|
||||
# Clear Drawing Button
|
||||
tk.Button(action_frame, text="Clear Drawing", command=self.clear_drawing,
|
||||
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
|
||||
|
||||
# Print button (goes directly to print)
|
||||
tk.Button(action_frame, text="Print", command=self.go_to_print,
|
||||
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
|
||||
|
||||
def import_image(self):
|
||||
self.import_image_base()
|
||||
|
||||
def go_to_print(self):
|
||||
self.drawing.save("drawing.png")
|
||||
self.master.switch_frame(NoQRPrintScreen)
|
||||
|
||||
|
||||
class NoQRImportImageScreen(tk.Frame):
|
||||
"""No QR Ribbon - Import Image Screen."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
master.add_home_button(self)
|
||||
|
||||
# Get ribbon config
|
||||
self.ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
self.margin = CONFIG.get("ribbon", {}).get("margin", 50)
|
||||
self.printable_width = self.ribbon_width - 50 # 25px margin each side
|
||||
|
||||
# Main container
|
||||
self.container = tk.Frame(self, bg=BG_COLOR)
|
||||
self.container.place(relx=0.5, rely=0.5, anchor='center')
|
||||
|
||||
# Title
|
||||
self.title_label = tk.Label(self.container, text="Import an image for your ribbon",
|
||||
font=GlobalVars.TEXT_FONT, bg=BG_COLOR)
|
||||
self.title_label.pack(pady=20)
|
||||
|
||||
# Preview area (will show scaled image)
|
||||
self.preview_frame = tk.Frame(self.container, bg=BG_COLOR)
|
||||
self.preview_frame.pack(pady=20)
|
||||
|
||||
self.preview_label = tk.Label(self.preview_frame, text="No image selected",
|
||||
font=('Georgia', 14), bg='white', width=40, height=15)
|
||||
self.preview_label.pack()
|
||||
|
||||
# Info label for dimensions
|
||||
self.info_label = tk.Label(self.container, text="",
|
||||
font=('Georgia', 12), bg=BG_COLOR)
|
||||
self.info_label.pack(pady=10)
|
||||
|
||||
# Buttons frame
|
||||
buttons_frame = tk.Frame(self.container, bg=BG_COLOR)
|
||||
buttons_frame.pack(pady=20)
|
||||
|
||||
# Select Image button
|
||||
self.select_btn = tk.Button(buttons_frame, text="Select Image",
|
||||
command=self.select_image,
|
||||
height=2, width=15, bg='peach puff',
|
||||
font=GlobalVars.BUTTON_FONT)
|
||||
self.select_btn.pack(side='left', padx=10)
|
||||
|
||||
# Print button (disabled until image selected)
|
||||
self.print_btn = tk.Button(buttons_frame, text="Print",
|
||||
command=self.go_to_print,
|
||||
height=2, width=15, bg='peach puff',
|
||||
font=GlobalVars.BUTTON_FONT, state='disabled')
|
||||
self.print_btn.pack(side='left', padx=10)
|
||||
|
||||
# Cancel button
|
||||
self.cancel_btn = tk.Button(buttons_frame, text="Cancel",
|
||||
command=lambda: master.switch_frame(NoQRRibbonSizeScreen),
|
||||
height=2, width=15, bg='peach puff',
|
||||
font=GlobalVars.BUTTON_FONT)
|
||||
self.cancel_btn.pack(side='left', padx=10)
|
||||
|
||||
# Store the processed image
|
||||
self.processed_image = None
|
||||
self.preview_photo = None
|
||||
|
||||
# Auto-open file dialog
|
||||
self.after(100, self.select_image)
|
||||
|
||||
def select_image(self):
|
||||
file_path = filedialog.askopenfilename(
|
||||
filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp *.gif")]
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
# Open and process the image
|
||||
img = Image.open(file_path)
|
||||
|
||||
# Calculate new dimensions maintaining aspect ratio
|
||||
original_width, original_height = img.size
|
||||
aspect_ratio = original_height / original_width
|
||||
new_width = self.printable_width
|
||||
new_height = int(self.printable_width * aspect_ratio)
|
||||
|
||||
# Resize maintaining aspect ratio
|
||||
img = img.resize((new_width, new_height), Image.LANCZOS)
|
||||
|
||||
# Convert to 1-bit black/white
|
||||
img = img.convert('1')
|
||||
|
||||
# Store the processed image
|
||||
self.processed_image = img
|
||||
|
||||
# Update info label
|
||||
self.info_label.config(text=f"Size: {new_width} x {new_height} pixels")
|
||||
|
||||
# Create preview (scaled for display)
|
||||
max_preview_height = 400
|
||||
max_preview_width = 500
|
||||
preview_scale = min(max_preview_width / new_width, max_preview_height / new_height, 1.0)
|
||||
preview_width = int(new_width * preview_scale)
|
||||
preview_height = int(new_height * preview_scale)
|
||||
|
||||
preview_img = img.resize((preview_width, preview_height), Image.NEAREST)
|
||||
self.preview_photo = ImageTk.PhotoImage(preview_img)
|
||||
|
||||
# Update preview label to show image
|
||||
self.preview_label.config(image=self.preview_photo, text="", width=preview_width, height=preview_height)
|
||||
|
||||
# Enable print button
|
||||
self.print_btn.config(state='normal')
|
||||
|
||||
def go_to_print(self):
|
||||
if self.processed_image:
|
||||
self.processed_image.save("drawing.png")
|
||||
# Set ribbon_size to 'import' to signal variable height
|
||||
GlobalVars.ribbon_size = 'import'
|
||||
self.master.switch_frame(NoQRPrintScreen)
|
||||
|
||||
|
||||
class NoQRPrintScreen(tk.Frame):
|
||||
"""No QR Ribbon - Print Screen (no QR, no SSB, no photo, no description)."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
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
|
||||
RoundedLabel(container, text="Ready to print your ribbon!", wraplength=600, font=GlobalVars.TEXT_FONT, bg='white').grid(row=0, column=0, columnspan=2)
|
||||
|
||||
# Print button
|
||||
tk.Button(container, text="Print", command=self.print_ribbon, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).grid(row=2, column=0, pady=20)
|
||||
|
||||
def print_ribbon(self):
|
||||
# Get config values
|
||||
ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
margin = CONFIG.get("ribbon", {}).get("margin", 50)
|
||||
printable_width = ribbon_width - 50 # 25px side margins
|
||||
|
||||
# Load the drawing
|
||||
drawing = Image.open("drawing.png")
|
||||
drawing_width, drawing_height = drawing.size
|
||||
|
||||
# For fixed sizes, use multiplier; for imports, use actual image dimensions
|
||||
if GlobalVars.ribbon_size != 'import':
|
||||
multiplier = {'small': 0.5, 'medium': 1.0, 'large': 1.5}.get(GlobalVars.ribbon_size, 1.0)
|
||||
drawing_height = int(printable_width * multiplier)
|
||||
|
||||
# Total label height: just margins + drawing (no QR, no scan-tag)
|
||||
total_height = margin + drawing_height + margin
|
||||
|
||||
# Create full-width image with margins
|
||||
merged_image = Image.new('L', (ribbon_width, total_height), "white")
|
||||
# Paste drawing 25px in from left, margin down from top
|
||||
merged_image.paste(drawing, (25, margin))
|
||||
merged_image.save("merged_image.png")
|
||||
|
||||
# Get the ZPL code for the image
|
||||
zpl_code = tozpl.print_to_zpl("merged_image.png",
|
||||
print_width=ribbon_width,
|
||||
label_length=total_height)
|
||||
|
||||
# Save the ZPL
|
||||
with open("to_print.zpl", "w") as file:
|
||||
file.write(zpl_code)
|
||||
|
||||
# Print to ribbon printer
|
||||
GlobalVars.last_print_printer = "ribbon"
|
||||
try:
|
||||
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["ribbon"]} -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE)
|
||||
except:
|
||||
print('traceback.format_exc():\n%s' % traceback.format_exc())
|
||||
|
||||
from kiosk.screens.completion import Screen10
|
||||
self.master.switch_frame(Screen10)
|
||||
29
kiosk/screens/placeholder.py
Normal file
29
kiosk/screens/placeholder.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Placeholder screens (Screen7, Screen9) - not implemented."""
|
||||
import tkinter as tk
|
||||
|
||||
from kiosk.config import BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
|
||||
|
||||
class Screen7(tk.Frame):
|
||||
"""Create user - not implemented."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
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=("Georgia", 16), wraplength=500, bg='white')
|
||||
self.info_label.pack()
|
||||
|
||||
|
||||
class Screen9(tk.Frame):
|
||||
"""Text update - placeholder."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
master.add_home_button(self)
|
||||
|
||||
def go_to_screen10():
|
||||
from kiosk.screens.completion import Screen10
|
||||
master.switch_frame(Screen10)
|
||||
|
||||
tk.Button(self, text="Done", command=go_to_screen10, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10)
|
||||
243
kiosk/screens/print_flow.py
Normal file
243
kiosk/screens/print_flow.py
Normal file
@ -0,0 +1,243 @@
|
||||
"""Print flow screens (Screen13, PrintFailedScreen)."""
|
||||
import tkinter as tk
|
||||
import subprocess
|
||||
import traceback
|
||||
import os
|
||||
|
||||
from PIL import Image
|
||||
import qrcode
|
||||
import tozpl
|
||||
import addtoDB
|
||||
|
||||
from kiosk.config import CONFIG, BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel
|
||||
|
||||
|
||||
class Screen13(tk.Frame):
|
||||
"""Time to print screen."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
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
|
||||
RoundedLabel(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, bg='white').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)
|
||||
|
||||
def printy(self):
|
||||
"""Go ahead and print the thing."""
|
||||
# Specify the path to your image file
|
||||
path_to_image = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), '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, GlobalVars.info_text, 1)
|
||||
|
||||
# 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)
|
||||
|
||||
# Calculate QR scale based on print type
|
||||
if GlobalVars.print_type == "ribbon":
|
||||
ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
printable_width = ribbon_width - 50 # 25px margin each side
|
||||
|
||||
# First pass: determine actual QR version needed for this data
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_H,
|
||||
box_size=1, # Minimal size just to determine version
|
||||
border=0,
|
||||
)
|
||||
qr.add_data(key)
|
||||
qr.make(fit=True)
|
||||
|
||||
# Calculate module count: 4 * version + 17
|
||||
module_count = 4 * qr.version + 17
|
||||
|
||||
# Calculate pixel-perfect box_size to fit printable width
|
||||
QRscale = printable_width // module_count
|
||||
|
||||
# Second pass: generate QR at correct size
|
||||
qr = qrcode.QRCode(
|
||||
version=qr.version, # Use determined version
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_H,
|
||||
box_size=QRscale,
|
||||
border=0,
|
||||
)
|
||||
qr.add_data(key)
|
||||
qr.make(fit=False) # Don't allow further upgrades
|
||||
|
||||
img = qr.make_image()
|
||||
else:
|
||||
# Sticker path - use existing QRscale from master
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_H,
|
||||
box_size=QRscale,
|
||||
border=0,
|
||||
)
|
||||
qr.add_data(key)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image()
|
||||
|
||||
whereToSaveQR = 'qr.png'
|
||||
img.convert('1').save(whereToSaveQR)
|
||||
|
||||
# compose image for tag
|
||||
drawing = Image.open("drawing.png") # drawing
|
||||
qr_img = Image.open("qr.png").convert("L") # qr
|
||||
|
||||
#### merge em
|
||||
|
||||
## if sticker
|
||||
if GlobalVars.print_type == "sticker":
|
||||
## if we didn't custom set X/Y, set to defaults
|
||||
if QRX is None and QRY is None:
|
||||
QRX = 454
|
||||
QRY = 217
|
||||
|
||||
# Load and rotate scan-tag 90 degrees clockwise
|
||||
scan_tag = Image.open("scan-tag.png").convert("L")
|
||||
scan_tag_rotated = scan_tag.rotate(-90, expand=True) # -90 = clockwise
|
||||
|
||||
merged_image = Image.new('L', (675, 375), "white")
|
||||
merged_image.paste(drawing, (0, 8))
|
||||
merged_image.paste(qr_img, (QRX, QRY + 8))
|
||||
|
||||
# Paste rotated scan-tag right after drawing, vertically centered
|
||||
scan_tag_x = 597 # Right edge of drawing
|
||||
scan_tag_y = (375 - scan_tag_rotated.height) // 2 # Center vertically
|
||||
merged_image.paste(scan_tag_rotated, (scan_tag_x, scan_tag_y))
|
||||
|
||||
merged_image.save("merged_image.png")
|
||||
|
||||
# if ribbon
|
||||
if GlobalVars.print_type == "ribbon":
|
||||
# Get config values
|
||||
ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
margin = CONFIG.get("ribbon", {}).get("margin", 50)
|
||||
printable_width = ribbon_width - 50 # 25px side margins
|
||||
|
||||
multiplier = {'small': 0.5, 'medium': 1.0, 'large': 1.5}.get(GlobalVars.ribbon_size, 1.0)
|
||||
drawing_height = int(printable_width * multiplier)
|
||||
|
||||
# Load scan-tag image
|
||||
scan_tag = Image.open("scan-tag.png").convert("L")
|
||||
scan_tag_height = scan_tag.height
|
||||
|
||||
# Use actual QR dimensions (already correctly sized from generation)
|
||||
qr_size = qr_img.width
|
||||
|
||||
# Total label height includes top/bottom margins and scan-tag
|
||||
total_height = margin + drawing_height + scan_tag_height + qr_size + margin
|
||||
|
||||
# Create full-width image with margins
|
||||
merged_image = Image.new('L', (ribbon_width, total_height), "white")
|
||||
# Paste drawing 25px in from left, margin down from top
|
||||
merged_image.paste(drawing, (25, margin))
|
||||
# Paste scan-tag centered below drawing
|
||||
scan_tag_x = (ribbon_width - scan_tag.width) // 2
|
||||
merged_image.paste(scan_tag, (scan_tag_x, margin + drawing_height))
|
||||
# Center QR horizontally within the full ribbon width, below scan-tag
|
||||
qr_x = (ribbon_width - qr_size) // 2
|
||||
merged_image.paste(qr_img, (qr_x, margin + drawing_height + scan_tag_height))
|
||||
merged_image.save("merged_image.png")
|
||||
|
||||
# Get the ZPL code for the image
|
||||
if GlobalVars.print_type == "ribbon":
|
||||
zpl_code = tozpl.print_to_zpl("merged_image.png",
|
||||
print_width=ribbon_width,
|
||||
label_length=total_height)
|
||||
else:
|
||||
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 to sticker printer
|
||||
if GlobalVars.print_type == "sticker":
|
||||
GlobalVars.last_print_printer = "sticker"
|
||||
try:
|
||||
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["sticker"]} -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE, )
|
||||
except:
|
||||
print('traceback.format_exc():\n%s' % traceback.format_exc())
|
||||
exit()
|
||||
# or print to tag printer:
|
||||
if GlobalVars.print_type == "ribbon":
|
||||
GlobalVars.last_print_printer = "ribbon"
|
||||
try:
|
||||
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["ribbon"]} -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE, )
|
||||
except:
|
||||
print('traceback.format_exc():\n%s' % traceback.format_exc())
|
||||
exit()
|
||||
|
||||
from kiosk.screens.completion import Screen10
|
||||
self.master.switch_frame(Screen10) # Switching to Screen10 after Done
|
||||
|
||||
|
||||
class PrintFailedScreen(tk.Frame):
|
||||
"""Print failed screen with re-print option."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
|
||||
# Content positioned higher
|
||||
container = tk.Frame(self, bg=BG_COLOR)
|
||||
container.pack(pady=(50, 0))
|
||||
|
||||
# Troubleshooting instructions
|
||||
troubleshoot_text = """if print failed:
|
||||
|
||||
1. un-hook the red nylon cord and carefully pull out printer.
|
||||
2. there are teal latches on either side of the printer: pull them towards you and lift the lid all the way open.
|
||||
3. make sure there is enough resin and printable nylon/stickers and they are lined up straight
|
||||
4. make sure the resin is not torn. If it is it may need to be re-attached to the uptake spool
|
||||
5. close printer and press the button on top of printer to stop the green light from flashing
|
||||
6. put printer back, re-hook the nylon cord and try printing again.
|
||||
|
||||
if you're still having trouble you could try searching the internet for Zebra GX430T troubleshooting
|
||||
AND OR
|
||||
please email maintenance@cust.ooo
|
||||
|
||||
thanks!"""
|
||||
RoundedLabel(container, text=troubleshoot_text, bg='white', font=('Georgia', 22),
|
||||
wraplength=900, justify='left').pack(pady=10)
|
||||
|
||||
# Re-print button
|
||||
tk.Button(container, text="Re-print", command=self._reprint,
|
||||
height=2, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
|
||||
|
||||
# Done button
|
||||
def go_home():
|
||||
from kiosk.screens.home import Screen0
|
||||
master.switch_frame(Screen0)
|
||||
|
||||
tk.Button(container, text="Done", command=go_home,
|
||||
height=2, width=30, bg='light gray', font=GlobalVars.BUTTON_FONT).pack(pady=5)
|
||||
|
||||
def _reprint(self):
|
||||
"""Re-send the last print job."""
|
||||
if GlobalVars.last_print_printer and os.path.exists("to_print.zpl"):
|
||||
try:
|
||||
printer_name = CONFIG["printers"][GlobalVars.last_print_printer]
|
||||
subprocess.Popen(f'lp -d {printer_name} -o raw to_print.zpl',
|
||||
shell=True, stdout=subprocess.PIPE)
|
||||
except:
|
||||
print('traceback.format_exc():\n%s' % traceback.format_exc())
|
||||
# Go back to thank you screen
|
||||
from kiosk.screens.completion import Screen10
|
||||
self.master.switch_frame(Screen10)
|
||||
266
kiosk/screens/ssb_selection.py
Normal file
266
kiosk/screens/ssb_selection.py
Normal file
@ -0,0 +1,266 @@
|
||||
"""Scuttlebutt selection screens (Screen1, ScreenWhyScuttlebutt, Screen2)."""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import subprocess
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from kiosk.config import BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel
|
||||
|
||||
|
||||
class Screen1(tk.Frame):
|
||||
"""Do you have SSB?"""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
master.add_home_button(self)
|
||||
|
||||
# Create the label widget with the text
|
||||
label = RoundedLabel(self, text="Would you like to associate your item with a Scuttlebutt account?", font=GlobalVars.TEXT_FONT, bg='white', wraplength=600)
|
||||
label.pack(pady=90)
|
||||
|
||||
# Deferred imports
|
||||
def go_to_screen2():
|
||||
master.switch_frame(Screen2)
|
||||
|
||||
def go_to_screen3():
|
||||
from kiosk.screens.camera import Screen3
|
||||
master.switch_frame(Screen3)
|
||||
|
||||
def go_to_why():
|
||||
master.switch_frame(ScreenWhyScuttlebutt)
|
||||
|
||||
tk.Button(self, text="Yes, and I already have an account", command=go_to_screen2, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
|
||||
tk.Button(self, text="No thanks", command=go_to_screen3, height=3, width=50, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
|
||||
tk.Button(self, text="Why would I want to do that?", command=go_to_why, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
|
||||
|
||||
|
||||
class ScreenWhyScuttlebutt(tk.Frame):
|
||||
"""Why Scuttlebutt explanation screen."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
master.add_home_button(self)
|
||||
|
||||
label = RoundedLabel(self, text="Items associated with a Scuttlebutt account can have their ownership cryptographically verified. This effectively makes the item an NFT. For more info on how to make a Scuttlebutt account and have it visible on the kiosk visit www.cust.ooo/ssb", font=GlobalVars.TEXT_FONT, bg='white', wraplength=600)
|
||||
label.pack(pady=90)
|
||||
|
||||
tk.Button(self, text="OK", command=lambda: master.switch_frame(Screen1), height=3, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
|
||||
|
||||
|
||||
class Screen2(tk.Frame):
|
||||
"""Find yourself in list of SSB users."""
|
||||
def __init__(self, master):
|
||||
tk.Frame.__init__(self, master, bg=BG_COLOR)
|
||||
self.selected_label = None
|
||||
self.filtered_users = []
|
||||
|
||||
# Create a new frame at the top for the label and text box
|
||||
self.top_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
self.top_frame.pack(side="top", fill="x", pady=(60, 10))
|
||||
|
||||
# Add a label with text wrapping
|
||||
self.label = RoundedLabel(self.top_frame,
|
||||
text="slowly type your public key or alias one letter at a time to find yourself in the list then click on your key to select it.",
|
||||
font=GlobalVars.TEXT_FONT,
|
||||
wraplength=800,
|
||||
bg='white')
|
||||
self.label.pack(side="top", pady=(0, 10))
|
||||
|
||||
# Add text box to the top frame
|
||||
self.entry = tk.Entry(self.top_frame, font=GlobalVars.TEXT_FONT)
|
||||
self.entry.bind('<KeyRelease>', 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 = tk.Frame(self, bg=BG_COLOR)
|
||||
self.container.pack(fill='both', expand=True, padx=20, pady=10)
|
||||
|
||||
# Create a canvas for the user list
|
||||
self.canvas = tk.Canvas(self.container, bg=BG_COLOR)
|
||||
self.scrollbar = ttk.Scrollbar(self.container, orient="vertical", command=self.canvas.yview)
|
||||
self.scrollable_frame = tk.Frame(self.canvas, bg=BG_COLOR)
|
||||
|
||||
self.scrollable_frame.bind(
|
||||
"<Configure>",
|
||||
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
||||
)
|
||||
|
||||
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
||||
self.canvas.configure(yscrollcommand=self.scrollbar.set)
|
||||
|
||||
self.canvas.pack(side="left", fill="both", expand=True)
|
||||
self.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)
|
||||
|
||||
# Create a frame for action buttons at the bottom
|
||||
self.button_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
self.button_frame.pack(side="bottom", fill="x", pady=10)
|
||||
|
||||
# The 'Refresh List' button
|
||||
self.refresh_button = tk.Button(self.button_frame, text="Refresh List", command=self.refresh_users,
|
||||
height=2, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT)
|
||||
self.refresh_button.pack(side="left", padx=(20, 10))
|
||||
|
||||
# Deferred import for navigation
|
||||
def go_to_screen3():
|
||||
from kiosk.screens.camera import Screen3
|
||||
master.switch_frame(Screen3)
|
||||
|
||||
# The 'Done' button to navigate to next screen
|
||||
self.done_button = tk.Button(self.button_frame, text="Done", command=go_to_screen3,
|
||||
height=2, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT)
|
||||
self.done_button.pack(side="right", padx=(10, 20))
|
||||
|
||||
# Initialize users list from users.json
|
||||
self.users = self.get_users_from_file()
|
||||
self.update_users_list()
|
||||
|
||||
master.add_home_button(self)
|
||||
|
||||
def update_users_list(self):
|
||||
search_text = self.entry.get().lower()
|
||||
self.filtered_users = [user for user in self.users if search_text in user['id'].lower() or search_text in self.unescape_unicode(user.get('alias', '')).lower()]
|
||||
self.display_users()
|
||||
|
||||
# Preserve selection after filtering
|
||||
if GlobalVars.selected_user:
|
||||
for widget in self.scrollable_frame.winfo_children():
|
||||
if isinstance(widget, tk.Frame):
|
||||
id_label = widget.winfo_children()[-1]
|
||||
if id_label['text'] == GlobalVars.selected_user:
|
||||
self.highlight_frame(widget)
|
||||
self.selected_label = widget
|
||||
break
|
||||
|
||||
def display_users(self):
|
||||
for widget in self.scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
for index, user in enumerate(self.filtered_users):
|
||||
try:
|
||||
frame = tk.Frame(self.scrollable_frame, bg=BG_COLOR)
|
||||
frame.pack(fill='x', expand=True, pady=2)
|
||||
|
||||
alias = self.unescape_unicode(user.get('alias', ''))
|
||||
id = user.get('id', '')
|
||||
|
||||
alias_label = tk.Label(frame, text=alias, font=('Georgia', 14, 'bold'), width=20, anchor='w', bg='white')
|
||||
alias_label.pack(side='left', padx=(0, 10))
|
||||
|
||||
id_label = tk.Label(frame, text=id, font=('Georgia', 14), anchor='w', bg='white')
|
||||
id_label.pack(side='left', expand=True, fill='x')
|
||||
|
||||
frame.bind('<Button-1>', lambda e, u=user, f=frame: self.on_user_clicked(f, u))
|
||||
alias_label.bind('<Button-1>', lambda e, u=user, f=frame: self.on_user_clicked(f, u))
|
||||
id_label.bind('<Button-1>', lambda e, u=user, f=frame: self.on_user_clicked(f, u))
|
||||
|
||||
if GlobalVars.selected_user == id:
|
||||
self.highlight_frame(frame)
|
||||
self.selected_label = frame
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error displaying user {index + 1}: {str(e)}")
|
||||
print(f"User data: {user}")
|
||||
print(traceback.format_exc())
|
||||
|
||||
self.scrollable_frame.update_idletasks()
|
||||
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
||||
|
||||
def on_user_clicked(self, frame, user):
|
||||
# Unhighlight the previously selected user
|
||||
if self.selected_label is not None and self.widget_exists(self.selected_label):
|
||||
self.unhighlight_frame(self.selected_label)
|
||||
|
||||
# Update the selected user
|
||||
GlobalVars.selected_user = user['id']
|
||||
self.selected_label = frame
|
||||
|
||||
# Highlight the newly selected user
|
||||
self.highlight_frame(frame)
|
||||
self.update_done_button_state()
|
||||
|
||||
# Print debug information
|
||||
print(f"User clicked: {user['id']}")
|
||||
print(f"Selected label: {self.selected_label}")
|
||||
print(f"Global selected user: {GlobalVars.selected_user}")
|
||||
|
||||
def highlight_frame(self, frame):
|
||||
frame.configure(bg="light blue")
|
||||
for child in frame.winfo_children():
|
||||
child.configure(bg="light blue")
|
||||
print(f"Highlighted frame: {frame}")
|
||||
|
||||
def unhighlight_frame(self, frame):
|
||||
frame.configure(bg=BG_COLOR)
|
||||
for child in frame.winfo_children():
|
||||
child.configure(bg=BG_COLOR)
|
||||
print(f"Unhighlighted frame: {frame}")
|
||||
|
||||
def scroll_to_widget(self, widget):
|
||||
self.canvas.update_idletasks()
|
||||
self.canvas.yview_moveto(widget.winfo_y() / self.scrollable_frame.winfo_height())
|
||||
|
||||
def update_done_button_state(self):
|
||||
if GlobalVars.selected_user:
|
||||
self.done_button.configure(state="normal")
|
||||
else:
|
||||
self.done_button.configure(state="disabled")
|
||||
|
||||
def widget_exists(self, widget):
|
||||
try:
|
||||
widget.winfo_exists()
|
||||
return True
|
||||
except tk.TclError:
|
||||
return False
|
||||
|
||||
def refresh_users(self):
|
||||
try:
|
||||
self.users = self.get_scuttlebutt_users()
|
||||
self.update_users_list()
|
||||
self.save_users_to_file(self.users)
|
||||
messagebox.showinfo("Success", "User list has been refreshed and updated with aliases.")
|
||||
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):
|
||||
try:
|
||||
result = subprocess.run(['node', 'scuttlebot.js'], capture_output=True, text=True, timeout=60)
|
||||
if result.returncode == 0:
|
||||
users = json.loads(result.stdout)
|
||||
return users
|
||||
else:
|
||||
print(f"Command failed with error: {result.stderr}")
|
||||
return []
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Command timed out after 60 seconds")
|
||||
return []
|
||||
except json.JSONDecodeError:
|
||||
print("Failed to parse JSON output")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_users_from_file(self):
|
||||
try:
|
||||
with open('users.json') as f:
|
||||
users = json.load(f)
|
||||
return users
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
def save_users_to_file(self, users):
|
||||
with open('users.json', 'w') as f:
|
||||
json.dump(users, f)
|
||||
|
||||
@staticmethod
|
||||
def unescape_unicode(s):
|
||||
return s.encode('utf-8').decode('unicode_escape')
|
||||
13
kiosk/state.py
Normal file
13
kiosk/state.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Global state container for the kiosk application."""
|
||||
|
||||
|
||||
class GlobalVars:
|
||||
"""Shared state container for kiosk data."""
|
||||
qr_code_value = None
|
||||
print_type = "neither"
|
||||
selected_user = None
|
||||
ribbon_size = None # 'small', 'medium', or 'large'
|
||||
info_text = None
|
||||
BUTTON_FONT = None
|
||||
TEXT_FONT = None
|
||||
last_print_printer = None # "sticker" or "ribbon" - for re-print functionality
|
||||
94
kiosk/utils.py
Normal file
94
kiosk/utils.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""Utility functions for the kiosk application."""
|
||||
import os
|
||||
import re
|
||||
import glob
|
||||
import cv2
|
||||
|
||||
|
||||
def get_preferred_camera_index(preferred_name_fragment=None):
|
||||
"""
|
||||
Try to find a stable camera index for an external webcam.
|
||||
|
||||
Strategy (Linux):
|
||||
- Look in /dev/v4l/by-id, which has stable, human-readable symlinks.
|
||||
- If `preferred_name_fragment` is provided, pick the first device whose
|
||||
name contains that fragment.
|
||||
- Otherwise, prefer devices whose name looks like an external USB webcam.
|
||||
- Fall back to the lowest working index (usually the built-in camera).
|
||||
|
||||
You can customize behaviour by passing a string like "Logitech" or by
|
||||
changing the heuristics below.
|
||||
"""
|
||||
by_id_dir = "/dev/v4l/by-id"
|
||||
|
||||
try:
|
||||
if os.path.isdir(by_id_dir):
|
||||
paths = sorted(glob.glob(os.path.join(by_id_dir, "*")))
|
||||
if paths:
|
||||
# Optionally filter by a user-specified name fragment
|
||||
if preferred_name_fragment:
|
||||
name_fragment = preferred_name_fragment.lower()
|
||||
candidate_paths = [
|
||||
p for p in paths if name_fragment in os.path.basename(p).lower()
|
||||
]
|
||||
if not candidate_paths:
|
||||
candidate_paths = paths
|
||||
else:
|
||||
candidate_paths = paths
|
||||
|
||||
# Heuristic: prefer names that look like external USB webcams
|
||||
# (often contain "usb-" but are not clearly an integrated camera)
|
||||
usb_like = [
|
||||
p
|
||||
for p in candidate_paths
|
||||
if "usb-" in os.path.basename(p).lower()
|
||||
]
|
||||
# If that filter gives us something, use it, otherwise use all
|
||||
candidate_paths = usb_like or candidate_paths
|
||||
|
||||
for path in candidate_paths:
|
||||
target = os.path.realpath(path) # e.g. /dev/video2
|
||||
m = re.search(r"video(\d+)", target)
|
||||
if not m:
|
||||
continue
|
||||
idx = int(m.group(1))
|
||||
cap = cv2.VideoCapture(idx)
|
||||
if cap.isOpened():
|
||||
cap.release()
|
||||
print(f"Selected camera index {idx} from {path}")
|
||||
return idx
|
||||
cap.release()
|
||||
except Exception as e:
|
||||
print(f"Error while trying to select preferred camera: {e}")
|
||||
|
||||
# Fallback: probe a small range of indices and pick the first that works
|
||||
for idx in range(0, 6):
|
||||
cap = cv2.VideoCapture(idx)
|
||||
if cap.isOpened():
|
||||
cap.release()
|
||||
print(f"Falling back to first working camera index: {idx}")
|
||||
return idx
|
||||
cap.release()
|
||||
|
||||
# Absolute fallback if nothing opens
|
||||
print("No working camera found; defaulting to index 0")
|
||||
return 0
|
||||
|
||||
|
||||
def sanitize_for_ssb(text):
|
||||
"""Sanitize text for safe passage through shell and JSON."""
|
||||
# Order matters: escape backslashes first
|
||||
text = text.replace('\\', '\\\\')
|
||||
# Escape double quotes for JSON and shell
|
||||
text = text.replace('"', '\\"')
|
||||
# Escape dollar signs to prevent shell variable expansion
|
||||
text = text.replace('$', '\\$')
|
||||
# Escape backticks to prevent shell command substitution
|
||||
text = text.replace('`', '\\`')
|
||||
# Escape newlines for JSON
|
||||
text = text.replace('\n', '\\n')
|
||||
# Escape carriage returns
|
||||
text = text.replace('\r', '')
|
||||
# Escape tabs
|
||||
text = text.replace('\t', ' ')
|
||||
return text
|
||||
6
kiosk/widgets/__init__.py
Normal file
6
kiosk/widgets/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Widget components for the kiosk application."""
|
||||
from .rounded_label import RoundedLabel
|
||||
from .scrolled_frame import VirtualScrolledFrame
|
||||
from .drawing_mixin import DrawingMixin
|
||||
|
||||
__all__ = ['RoundedLabel', 'VirtualScrolledFrame', 'DrawingMixin']
|
||||
BIN
kiosk/widgets/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
kiosk/widgets/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/widgets/__pycache__/drawing_mixin.cpython-313.pyc
Normal file
BIN
kiosk/widgets/__pycache__/drawing_mixin.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/widgets/__pycache__/rounded_label.cpython-313.pyc
Normal file
BIN
kiosk/widgets/__pycache__/rounded_label.cpython-313.pyc
Normal file
Binary file not shown.
BIN
kiosk/widgets/__pycache__/scrolled_frame.cpython-313.pyc
Normal file
BIN
kiosk/widgets/__pycache__/scrolled_frame.cpython-313.pyc
Normal file
Binary file not shown.
124
kiosk/widgets/drawing_mixin.py
Normal file
124
kiosk/widgets/drawing_mixin.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""Drawing functionality mixin for kiosk screens."""
|
||||
from PIL import Image, ImageTk, ImageDraw
|
||||
from tkinter import filedialog
|
||||
|
||||
|
||||
class DrawingMixin:
|
||||
"""Mixin providing drawing canvas functionality for kiosk screens."""
|
||||
|
||||
def init_drawing(self, width, height, grid_size=1):
|
||||
"""Initialize drawing state. Call this in __init__ after creating self.canvas."""
|
||||
self.original_width = width
|
||||
self.original_height = height
|
||||
self.grid_size = grid_size
|
||||
|
||||
self.drawing = Image.new('1', (width, height), 1)
|
||||
self.draw = ImageDraw.Draw(self.drawing)
|
||||
self.last_draw = None
|
||||
self.draw_color = 'black'
|
||||
self.draw_size = 1
|
||||
|
||||
def draw_line(self, event):
|
||||
"""Handle mouse drag to draw lines."""
|
||||
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):
|
||||
"""Bresenham's line algorithm for smooth drawing."""
|
||||
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):
|
||||
"""Draw a solid point at coordinates."""
|
||||
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)*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=''
|
||||
)
|
||||
self.draw.point((x+dx, y+dy), fill=color)
|
||||
|
||||
def _draw_dithered_point(self, x, y):
|
||||
"""Draw a dithered (gray) point using checkerboard pattern."""
|
||||
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)*self.grid_size, (y+dy)*self.grid_size,
|
||||
(x+dx+1)*self.grid_size, (y+dy+1)*self.grid_size,
|
||||
fill='black', outline=''
|
||||
)
|
||||
self.draw.point((x+dx, y+dy), fill=0)
|
||||
else:
|
||||
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=''
|
||||
)
|
||||
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_base(self):
|
||||
"""Base import image functionality. Returns file path or None."""
|
||||
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.jpeg *.png")])
|
||||
if not file_path:
|
||||
return None
|
||||
|
||||
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 scaled size
|
||||
display_width = self.original_width * self.grid_size
|
||||
display_height = self.original_height * self.grid_size
|
||||
display_img = img.resize((display_width, 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')
|
||||
|
||||
return file_path
|
||||
60
kiosk/widgets/rounded_label.py
Normal file
60
kiosk/widgets/rounded_label.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Rounded label widget using Canvas."""
|
||||
import tkinter as tk
|
||||
from kiosk.config import BG_COLOR
|
||||
|
||||
|
||||
class RoundedLabel(tk.Canvas):
|
||||
"""A label with rounded corners using Canvas."""
|
||||
def __init__(self, parent, text="", font=None, bg='white', fg='black',
|
||||
wraplength=400, justify='center', padx=15, pady=15, radius=20, **kwargs):
|
||||
# Use a temporary label to calculate text dimensions
|
||||
temp_label = tk.Label(parent, text=text, font=font, wraplength=wraplength, justify=justify)
|
||||
temp_label.update_idletasks()
|
||||
text_width = temp_label.winfo_reqwidth()
|
||||
text_height = temp_label.winfo_reqheight()
|
||||
temp_label.destroy()
|
||||
|
||||
# Calculate canvas dimensions
|
||||
width = text_width + 2 * padx
|
||||
height = text_height + 2 * pady
|
||||
|
||||
# Initialize canvas with background matching the app background
|
||||
super().__init__(parent, width=width, height=height,
|
||||
highlightthickness=0, bg=BG_COLOR, **kwargs)
|
||||
|
||||
# Draw rounded rectangle
|
||||
self._draw_rounded_rect(0, 0, width, height, radius, bg)
|
||||
|
||||
# Add text
|
||||
self.create_text(width // 2, height // 2, text=text, font=font, fill=fg,
|
||||
width=wraplength, justify=justify)
|
||||
|
||||
def _draw_rounded_rect(self, x1, y1, x2, y2, radius, fill):
|
||||
"""Draw a rounded rectangle on the canvas."""
|
||||
# Ensure radius isn't larger than half the smallest dimension
|
||||
radius = min(radius, (x2 - x1) // 2, (y2 - y1) // 2)
|
||||
|
||||
# Draw the rounded rectangle using arcs and rectangles
|
||||
# Top-left corner
|
||||
self.create_arc(x1, y1, x1 + 2*radius, y1 + 2*radius,
|
||||
start=90, extent=90, fill=fill, outline=fill)
|
||||
# Top-right corner
|
||||
self.create_arc(x2 - 2*radius, y1, x2, y1 + 2*radius,
|
||||
start=0, extent=90, fill=fill, outline=fill)
|
||||
# Bottom-left corner
|
||||
self.create_arc(x1, y2 - 2*radius, x1 + 2*radius, y2,
|
||||
start=180, extent=90, fill=fill, outline=fill)
|
||||
# Bottom-right corner
|
||||
self.create_arc(x2 - 2*radius, y2 - 2*radius, x2, y2,
|
||||
start=270, extent=90, fill=fill, outline=fill)
|
||||
|
||||
# Fill the center rectangles
|
||||
# Center horizontal
|
||||
self.create_rectangle(x1 + radius, y1, x2 - radius, y2,
|
||||
fill=fill, outline=fill)
|
||||
# Left vertical
|
||||
self.create_rectangle(x1, y1 + radius, x1 + radius, y2 - radius,
|
||||
fill=fill, outline=fill)
|
||||
# Right vertical
|
||||
self.create_rectangle(x2 - radius, y1 + radius, x2, y2 - radius,
|
||||
fill=fill, outline=fill)
|
||||
44
kiosk/widgets/scrolled_frame.py
Normal file
44
kiosk/widgets/scrolled_frame.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Virtual scrolled frame widget."""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from kiosk.config import BG_COLOR
|
||||
|
||||
|
||||
class VirtualScrolledFrame(ttk.Frame):
|
||||
"""A scrollable frame widget using canvas."""
|
||||
def __init__(self, parent, *args, **kw):
|
||||
ttk.Frame.__init__(self, parent, *args, **kw)
|
||||
|
||||
# Create a canvas object and a vertical scrollbar for scrolling it
|
||||
self.vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
|
||||
self.canvas = tk.Canvas(self, yscrollcommand=self.vscrollbar.set, bg=BG_COLOR)
|
||||
self.vscrollbar.config(command=self.canvas.yview)
|
||||
|
||||
# Reset the view
|
||||
self.canvas.xview_moveto(0)
|
||||
self.canvas.yview_moveto(0)
|
||||
|
||||
# Create a frame inside the canvas which will be scrolled with it
|
||||
self.interior = tk.Frame(self.canvas, bg=BG_COLOR)
|
||||
self.interior_id = self.canvas.create_window(0, 0, window=self.interior, anchor=tk.NW)
|
||||
|
||||
# Pack the widgets
|
||||
self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# Bind events to the Canvas
|
||||
self.canvas.bind('<Configure>', self._configure_canvas)
|
||||
self.interior.bind('<Configure>', self._configure_interior)
|
||||
|
||||
def _configure_interior(self, event):
|
||||
# Update the scrollbars to match the size of the inner frame
|
||||
size = (self.interior.winfo_reqwidth(), self.interior.winfo_reqheight())
|
||||
self.canvas.config(scrollregion="0 0 %s %s" % size)
|
||||
if self.interior.winfo_reqwidth() != self.canvas.winfo_width():
|
||||
# Update the canvas's width to fit the inner frame
|
||||
self.canvas.config(width=self.interior.winfo_reqwidth())
|
||||
|
||||
def _configure_canvas(self, event):
|
||||
if self.interior.winfo_reqwidth() != self.canvas.winfo_width():
|
||||
# Update the inner frame's width to fill the canvas
|
||||
self.canvas.itemconfigure(self.interior_id, width=self.canvas.winfo_width())
|
||||
64
ssb-post.sh
64
ssb-post.sh
@ -26,22 +26,30 @@ then
|
||||
#add blob
|
||||
blobID=$(cat $itemImagePath | ssb-server blobs.add) || exit 1
|
||||
|
||||
# publish the item
|
||||
ssb-server publish . <<BLAAB
|
||||
{
|
||||
"type":"post",
|
||||
"text":"\\n\\n$itemDescription\\n\\n a #custodisco item ",
|
||||
"custodisco":"true",
|
||||
"nft": "mint",
|
||||
"mentions": [
|
||||
{
|
||||
"name": "photo.jpg",
|
||||
"type": "image/jpeg",
|
||||
"link": "$blobID"
|
||||
}
|
||||
]
|
||||
}
|
||||
BLAAB
|
||||
# Build the full text and escape for JSON using jq
|
||||
fullText="
|
||||
|
||||
$itemDescription
|
||||
|
||||
a #custodisco item "
|
||||
|
||||
# publish the item using jq to safely build JSON
|
||||
jq -n \
|
||||
--arg text "$fullText" \
|
||||
--arg blobID "$blobID" \
|
||||
'{
|
||||
type: "post",
|
||||
text: $text,
|
||||
custodisco: "true",
|
||||
nft: "mint",
|
||||
mentions: [
|
||||
{
|
||||
name: "photo.jpg",
|
||||
type: "image/jpeg",
|
||||
link: $blobID
|
||||
}
|
||||
]
|
||||
}' | ssb-server publish .
|
||||
fi
|
||||
|
||||
|
||||
@ -51,17 +59,19 @@ then
|
||||
account=$2
|
||||
messageID=$3
|
||||
|
||||
ssb-server publish . <<BLAAB
|
||||
{
|
||||
"type":"post",
|
||||
"text":"This item is stewarded by $account",
|
||||
"custodisco":"true",
|
||||
"nft": "give",
|
||||
"target": "$account",
|
||||
"root": "$messageID",
|
||||
"branch": "$messageID"
|
||||
}
|
||||
BLAAB
|
||||
# Use jq to safely build JSON with proper escaping
|
||||
jq -n \
|
||||
--arg account "$account" \
|
||||
--arg messageID "$messageID" \
|
||||
'{
|
||||
type: "post",
|
||||
text: ("This item is stewarded by " + $account),
|
||||
custodisco: "true",
|
||||
nft: "give",
|
||||
target: $account,
|
||||
root: $messageID,
|
||||
branch: $messageID
|
||||
}' | ssb-server publish .
|
||||
fi
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user