From 20201ddedf5bda3c1369a6eb8d9288548d64bf2b Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 14:08:04 -0700 Subject: [PATCH 01/28] merge bash scripts and move to root, re add sh extension --- src/run => run_dev.sh | 4 ++-- src/run_verbose | 13 ------------- 2 files changed, 2 insertions(+), 15 deletions(-) rename src/run => run_dev.sh (61%) delete mode 100755 src/run_verbose diff --git a/src/run b/run_dev.sh similarity index 61% rename from src/run rename to run_dev.sh index ab07bed..7d28756 100755 --- a/src/run +++ b/run_dev.sh @@ -1,7 +1,7 @@ #!/bin/bash set -a -source "$(dirname "$0")/../.env" +source "$(dirname "$0")/.env" set +a output_file="log.txt" @@ -10,4 +10,4 @@ echo "" >> "$output_file" date "+%Y-%m-%d %H:%M:%S" >> "$output_file" -python main.py 2>&1 | tee -a log.txt \ No newline at end of file +python src/main.py "$@" 2>&1 | tee -a log.txt diff --git a/src/run_verbose b/src/run_verbose deleted file mode 100755 index e94a274..0000000 --- a/src/run_verbose +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -set -a -source "$(dirname "$0")/../.env" -set +a - -output_file="log.txt" - -echo "" >> "$output_file" - -date "+%Y-%m-%d %H:%M:%S" >> "$output_file" - -python main.py -v 2>&1 | tee -a log.txt \ No newline at end of file From a15efd0b371ccb926a7af8b11e746a9ff4f1b36d Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 14:15:39 -0700 Subject: [PATCH 02/28] refactor file and class names --- src/core/handle_check_in.py | 12 ++++---- src/gui.py | 30 +++++++++---------- src/main.py | 18 +++++------ .../{AccNoWaiver.py => acc_no_waiver.py} | 0 ...oWaiverSwipe.py => acc_no_waiver_swipe.py} | 2 +- .../{CheckInNoId.py => check_in_no_id.py} | 18 +++++------ src/screens/{MainPage.py => main_page.py} | 6 ++-- src/screens/{ManualFill.py => manual_fill.py} | 4 +-- ...CheckInOnly.py => no_acc_check_in_only.py} | 0 .../{NoAccNoWaiver.py => no_acc_no_waiver.py} | 0 ...iverSwipe.py => no_acc_no_waiver_swipe.py} | 2 +- src/screens/{QRCodes.py => qr_codes.py} | 2 +- src/screens/{UserThank.py => user_thank.py} | 4 +-- .../{UserWelcome.py => user_welcome.py} | 4 +-- .../{WaiverNoAcc.py => waiver_no_acc.py} | 0 ...erNoAccSwipe.py => waiver_no_acc_swipe.py} | 2 +- src/swipe.py | 16 +++++----- src/utils.py | 8 ++--- 18 files changed, 64 insertions(+), 64 deletions(-) rename src/screens/{AccNoWaiver.py => acc_no_waiver.py} (100%) rename src/screens/{AccNoWaiverSwipe.py => acc_no_waiver_swipe.py} (97%) rename src/screens/{CheckInNoId.py => check_in_no_id.py} (87%) rename src/screens/{MainPage.py => main_page.py} (92%) rename src/screens/{ManualFill.py => manual_fill.py} (98%) rename src/screens/{NoAccCheckInOnly.py => no_acc_check_in_only.py} (100%) rename src/screens/{NoAccNoWaiver.py => no_acc_no_waiver.py} (100%) rename src/screens/{NoAccNoWaiverSwipe.py => no_acc_no_waiver_swipe.py} (97%) rename src/screens/{QRCodes.py => qr_codes.py} (96%) rename src/screens/{UserThank.py => user_thank.py} (93%) rename src/screens/{UserWelcome.py => user_welcome.py} (94%) rename src/screens/{WaiverNoAcc.py => waiver_no_acc.py} (100%) rename src/screens/{WaiverNoAccSwipe.py => waiver_no_acc_swipe.py} (97%) diff --git a/src/core/handle_check_in.py b/src/core/handle_check_in.py index 9d686cb..8d5f964 100644 --- a/src/core/handle_check_in.py +++ b/src/core/handle_check_in.py @@ -9,12 +9,12 @@ def handle_check_in(tag): status = result.get("status") def update_ui(): - from screens.MainPage import MainPage - from screens.NoAccNoWaiver import NoAccNoWaiver - from screens.NoAccNoWaiverSwipe import NoAccNoWaiverSwipe - from screens.AccNoWaiver import AccNoWaiver - from screens.AccNoWaiverSwipe import AccNoWaiverSwipe - from screens.UserWelcome import UserWelcome + from screens.main_page import MainPage + from screens.no_acc_no_waiver import NoAccNoWaiver + from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe + from screens.acc_no_waiver import AccNoWaiver + from screens.acc_no_waiver_swipe import AccNoWaiverSwipe + from screens.user_welcome import UserWelcome if status == "api_error": logging.error("API error during check-in") diff --git a/src/gui.py b/src/gui.py index 6b4f3e4..99f0ce7 100644 --- a/src/gui.py +++ b/src/gui.py @@ -9,7 +9,7 @@ ################################################# # Acts as the controller and the user interface # ################################################# -class gui(tk.Tk): +class Gui(tk.Tk): def __init__(self, *args, **kwargs): tk.Tk.__init__(self, *args, **kwargs) @@ -42,19 +42,19 @@ def __init__(self, *args, **kwargs): self.curr_frame = None self.frame_uuid = uuid.uuid4().hex - from screens.MainPage import MainPage - from screens.AccNoWaiver import AccNoWaiver - from screens.AccNoWaiverSwipe import AccNoWaiverSwipe - from screens.ManualFill import ManualFill - from screens.CheckInNoId import CheckInNoId - from screens.NoAccCheckInOnly import NoAccCheckInOnly - from screens.NoAccNoWaiver import NoAccNoWaiver - from screens.NoAccNoWaiverSwipe import NoAccNoWaiverSwipe - from screens.QRCodes import QRCodes - from screens.UserThank import UserThank - from screens.UserWelcome import UserWelcome - from screens.WaiverNoAcc import WaiverNoAcc - from screens.WaiverNoAccSwipe import WaiverNoAccSwipe + from screens.main_page import MainPage + from screens.acc_no_waiver import AccNoWaiver + from screens.acc_no_waiver_swipe import AccNoWaiverSwipe + from screens.manual_fill import ManualFill + from screens.check_in_no_id import CheckInNoId + from screens.no_acc_check_in_only import NoAccCheckInOnly + from screens.no_acc_no_waiver import NoAccNoWaiver + from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe + from screens.qr_codes import QRCodes + from screens.user_thank import UserThank + from screens.user_welcome import UserWelcome + from screens.waiver_no_acc import WaiverNoAcc + from screens.waiver_no_acc_swipe import WaiverNoAccSwipe self._timeouts = { AccNoWaiverSwipe: 30000, @@ -86,7 +86,7 @@ def _on_map(self, event): self.attributes("-fullscreen", True) def timeout_fn(self, curr_uuid): - from screens.MainPage import MainPage + from screens.main_page import MainPage if curr_uuid == self.frame_uuid: self.show_frame(MainPage) global_.traffic_light.set_off() diff --git a/src/main.py b/src/main.py index c8e6c85..766f4b1 100644 --- a/src/main.py +++ b/src/main.py @@ -1,13 +1,13 @@ from tkinter import Label -from gui import gui +from gui import Gui from reader import * -from swipe import swipe +from swipe import Swipe from sheets import * from threading import Thread -from screens.MainPage import MainPage -from screens.ManualFill import ManualFill -from screens.CheckInNoId import CheckInNoId -from utils import utils +from screens.main_page import MainPage +from screens.manual_fill import ManualFill +from screens.check_in_no_id import CheckInNoId +from utils import Utils from core.handle_check_in import handle_check_in from core.render_ports import get_usb_ids import global_ @@ -149,13 +149,13 @@ def clearAndReturn(): reader_usb_id, traffic_usb_id = get_usb_ids() check_api_health() global_.init(traffic_usb_id) - app = gui() + app = Gui() global_.setApp(app) global_.traffic_light.set_off() - sw = swipe() + sw = Swipe() reader = Reader(reader_usb_id) - util = utils() + util = Utils() thread = Thread(target=myLoop, args=(app, reader)) logging.info("Starting thread") thread.start() diff --git a/src/screens/AccNoWaiver.py b/src/screens/acc_no_waiver.py similarity index 100% rename from src/screens/AccNoWaiver.py rename to src/screens/acc_no_waiver.py diff --git a/src/screens/AccNoWaiverSwipe.py b/src/screens/acc_no_waiver_swipe.py similarity index 97% rename from src/screens/AccNoWaiverSwipe.py rename to src/screens/acc_no_waiver_swipe.py index ea877c3..33ff24c 100644 --- a/src/screens/AccNoWaiverSwipe.py +++ b/src/screens/acc_no_waiver_swipe.py @@ -49,6 +49,6 @@ def _build(self, controller): self._window(875.0, 581.0, btn, width=344, height=71) def _back_to_main(self): - from .MainPage import MainPage + from .main_page import MainPage global_.traffic_light.set_off() global_.app.show_frame(MainPage) diff --git a/src/screens/CheckInNoId.py b/src/screens/check_in_no_id.py similarity index 87% rename from src/screens/CheckInNoId.py rename to src/screens/check_in_no_id.py index 1cd5d26..daa9312 100644 --- a/src/screens/CheckInNoId.py +++ b/src/screens/check_in_no_id.py @@ -13,10 +13,10 @@ class CheckInNoId(Screen): def _build(self, controller): - from .NoAccCheckInOnly import NoAccCheckInOnly - from .NoAccNoWaiverSwipe import NoAccNoWaiverSwipe - from .UserWelcome import UserWelcome - from .AccNoWaiver import AccNoWaiver + from .no_acc_check_in_only import NoAccCheckInOnly + from .no_acc_no_waiver_swipe import NoAccNoWaiverSwipe + from .user_welcome import UserWelcome + from .acc_no_waiver import AccNoWaiver self.loading_text_id = None self.pid = StringVar() @@ -63,11 +63,11 @@ def updateEntries(self, pid): self.pid_entry.insert(0, pid) def _call_check_in(self, controller): - from .NoAccCheckInOnly import NoAccCheckInOnly - from .NoAccNoWaiverSwipe import NoAccNoWaiverSwipe - from .UserWelcome import UserWelcome - from .AccNoWaiver import AccNoWaiver - from .MainPage import MainPage + from .no_acc_check_in_only import NoAccCheckInOnly + from .no_acc_no_waiver_swipe import NoAccNoWaiverSwipe + from .user_welcome import UserWelcome + from .acc_no_waiver import AccNoWaiver + from .main_page import MainPage pid = self.pid_entry.get() if not pid: diff --git a/src/screens/MainPage.py b/src/screens/main_page.py similarity index 92% rename from src/screens/MainPage.py rename to src/screens/main_page.py index c05e873..b22740e 100644 --- a/src/screens/MainPage.py +++ b/src/screens/main_page.py @@ -8,8 +8,8 @@ class MainPage(Screen): def _build(self, controller): - from .QRCodes import QRCodes - from .CheckInNoId import CheckInNoId + from .qr_codes import QRCodes + from .check_in_no_id import CheckInNoId logo = self._photo(ASSETS_PATH / "image_3.png") self._image(88.0, 90.0, image=logo) @@ -48,7 +48,7 @@ def _build(self, controller): self._window(1130.0, 40.0, btn2) def _go_to_no_id(self, controller): - from .CheckInNoId import CheckInNoId + from .check_in_no_id import CheckInNoId no_id = global_.app.get_frame(CheckInNoId) no_id.clearEntries() controller.show_frame(CheckInNoId) diff --git a/src/screens/ManualFill.py b/src/screens/manual_fill.py similarity index 98% rename from src/screens/ManualFill.py rename to src/screens/manual_fill.py index 44e9f54..791265c 100644 --- a/src/screens/ManualFill.py +++ b/src/screens/manual_fill.py @@ -1,7 +1,7 @@ from pathlib import Path from tkinter import Button, Entry, StringVar, END from .screen import Screen -from utils import utils +from utils import Utils import logging import timeit @@ -109,7 +109,7 @@ def updateEntries(self, fname, lname, email, pid): self.pid_entry.insert(0, pid) def _call_account_creation(self): - util = utils() + util = Utils() data = self.getEntries() self.clearEntries() try: diff --git a/src/screens/NoAccCheckInOnly.py b/src/screens/no_acc_check_in_only.py similarity index 100% rename from src/screens/NoAccCheckInOnly.py rename to src/screens/no_acc_check_in_only.py diff --git a/src/screens/NoAccNoWaiver.py b/src/screens/no_acc_no_waiver.py similarity index 100% rename from src/screens/NoAccNoWaiver.py rename to src/screens/no_acc_no_waiver.py diff --git a/src/screens/NoAccNoWaiverSwipe.py b/src/screens/no_acc_no_waiver_swipe.py similarity index 97% rename from src/screens/NoAccNoWaiverSwipe.py rename to src/screens/no_acc_no_waiver_swipe.py index 57c86c2..3295962 100644 --- a/src/screens/NoAccNoWaiverSwipe.py +++ b/src/screens/no_acc_no_waiver_swipe.py @@ -43,6 +43,6 @@ def _build(self, controller): self._window(465.0, 554.0, btn, width=349, height=71) def _go_to_manual_fill(self, controller): - from .ManualFill import ManualFill + from .manual_fill import ManualFill global_.app.get_frame(ManualFill).clearEntries() controller.show_frame(ManualFill) diff --git a/src/screens/QRCodes.py b/src/screens/qr_codes.py similarity index 96% rename from src/screens/QRCodes.py rename to src/screens/qr_codes.py index e05205d..e9cd16c 100644 --- a/src/screens/QRCodes.py +++ b/src/screens/qr_codes.py @@ -7,7 +7,7 @@ class QRCodes(Screen): def _build(self, controller): - from .MainPage import MainPage + from .main_page import MainPage img3 = self._photo(ASSETS_PATH / "image_3.png") self._image(88.0, 90.0, image=img3) diff --git a/src/screens/UserThank.py b/src/screens/user_thank.py similarity index 93% rename from src/screens/UserThank.py rename to src/screens/user_thank.py index e5ff541..db5afa2 100644 --- a/src/screens/UserThank.py +++ b/src/screens/user_thank.py @@ -20,7 +20,7 @@ def hide(self): self.canvas.delete("thank") def displayName(self, name, nextPage): - from .MainPage import MainPage + from .main_page import MainPage global_.app.show_frame(UserThank) if nextPage == MainPage: @@ -40,7 +40,7 @@ def displayName(self, name, nextPage): global_.app.after(4000, lambda: self._go_to_next(nextPage)) def _go_to_next(self, nextPage): - from .MainPage import MainPage + from .main_page import MainPage global_.app.show_frame(nextPage) if nextPage == MainPage: global_.traffic_light.set_off() diff --git a/src/screens/UserWelcome.py b/src/screens/user_welcome.py similarity index 94% rename from src/screens/UserWelcome.py rename to src/screens/user_welcome.py index 387463f..0302d74 100644 --- a/src/screens/UserWelcome.py +++ b/src/screens/user_welcome.py @@ -26,7 +26,7 @@ def displayName(self, name): self.last_name = name - from .MainPage import MainPage + from .main_page import MainPage global_.app.show_frame(UserWelcome) text_id = self.canvas.create_text( @@ -43,7 +43,7 @@ def displayName(self, name): self.canvas.after(3000, lambda: self._remove_name(text_id)) def _remove_name(self, text_id): - from .MainPage import MainPage + from .main_page import MainPage self.canvas.delete(text_id) self.offset -= 73 diff --git a/src/screens/WaiverNoAcc.py b/src/screens/waiver_no_acc.py similarity index 100% rename from src/screens/WaiverNoAcc.py rename to src/screens/waiver_no_acc.py diff --git a/src/screens/WaiverNoAccSwipe.py b/src/screens/waiver_no_acc_swipe.py similarity index 97% rename from src/screens/WaiverNoAccSwipe.py rename to src/screens/waiver_no_acc_swipe.py index 8c841c3..d988575 100644 --- a/src/screens/WaiverNoAccSwipe.py +++ b/src/screens/waiver_no_acc_swipe.py @@ -43,6 +43,6 @@ def _build(self, controller): self._window(465.0, 554.0, btn, width=349, height=71) def _go_to_manual_fill(self, controller): - from .ManualFill import ManualFill + from .manual_fill import ManualFill global_.app.get_frame(ManualFill).clearEntries() controller.show_frame(ManualFill) diff --git a/src/swipe.py b/src/swipe.py index a212fc0..1326a05 100644 --- a/src/swipe.py +++ b/src/swipe.py @@ -1,11 +1,11 @@ import tkinter -from screens.ManualFill import ManualFill -from screens.NoAccNoWaiverSwipe import NoAccNoWaiverSwipe -from screens.WaiverNoAccSwipe import WaiverNoAccSwipe -from screens.AccNoWaiverSwipe import AccNoWaiverSwipe -from screens.CheckInNoId import CheckInNoId +from screens.manual_fill import ManualFill +from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe +from screens.waiver_no_acc_swipe import WaiverNoAccSwipe +from screens.acc_no_waiver_swipe import AccNoWaiverSwipe +from screens.check_in_no_id import CheckInNoId from get_info_from_pid import contact_client -from utils import utils +from utils import Utils import global_ import logging @@ -16,13 +16,13 @@ swipe_error_shown = False -class swipe: +class Swipe: def __init__(self): global id_string id_string = "" def keyboardPress(self, key): - util = utils() + util = Utils() global id_string, swipe_error_shown curr_frame = global_.app.get_curr_frame() diff --git a/src/utils.py b/src/utils.py index 4f2fa0a..a8201ff 100644 --- a/src/utils.py +++ b/src/utils.py @@ -2,9 +2,9 @@ import time import global_ import tkinter -from screens.MainPage import MainPage -from screens.AccNoWaiverSwipe import AccNoWaiverSwipe -from screens.UserThank import UserThank +from screens.main_page import MainPage +from screens.acc_no_waiver_swipe import AccNoWaiverSwipe +from screens.user_thank import UserThank import logging ###################################################### @@ -12,7 +12,7 @@ ###################################################### -class utils: +class Utils: def __init__(self) -> None: pass From 3d750edf2aa3672603289c56089989cda0efc843 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 14:21:56 -0700 Subject: [PATCH 03/28] remove unnecessary wrapper calls for globals --- src/global_.py | 17 ++++++----------- src/main.py | 4 ++-- src/main_checkin_only.py | 4 ++-- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/global_.py b/src/global_.py index 1f9e8c3..88982e6 100644 --- a/src/global_.py +++ b/src/global_.py @@ -2,6 +2,11 @@ from sheets import SheetManager from traffic import TrafficLight +rfid = "" +sheets = None +app = None +traffic_light = None + class _TrafficProxy: def __init__(self, light, sheets_mgr): @@ -33,16 +38,6 @@ def set_off(self): def init(traffic_usb_id=None): - global rfid, sheets, app, traffic_light + global sheets, traffic_light sheets = SheetManager() traffic_light = _TrafficProxy(TrafficLight(traffic_usb_id), sheets) - - -def setRFID(new_rfid): - global rfid - rfid = new_rfid - - -def setApp(new_app): - global app - app = new_app diff --git a/src/main.py b/src/main.py index 766f4b1..269c722 100644 --- a/src/main.py +++ b/src/main.py @@ -90,7 +90,7 @@ def myLoop(app, reader): else: logging.debug("RFID Check Succeeded") - global_.setRFID(tag) + global_.rfid = tag handle_check_in(tag) last_tag = tag @@ -150,7 +150,7 @@ def clearAndReturn(): check_api_health() global_.init(traffic_usb_id) app = Gui() - global_.setApp(app) + global_.app = app global_.traffic_light.set_off() sw = Swipe() diff --git a/src/main_checkin_only.py b/src/main_checkin_only.py index 3456ce7..2bfe6c3 100644 --- a/src/main_checkin_only.py +++ b/src/main_checkin_only.py @@ -82,7 +82,7 @@ def myLoop(app, reader): else: logging.debug("RFID Check Succeeded") - global_.setRFID(tag) + global_.rfid = tag handle_check_in(tag, contact, util) last_tag = tag @@ -127,7 +127,7 @@ def clearAndReturn(): check_api_health() global_.init(traffic_usb_id) app = gui() - global_.setApp(app) + global_.app = app reader = Reader() util = utils() From 21dc827c905a3b5f635aba6c8f43f1fb8114d618 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 14:39:33 -0700 Subject: [PATCH 04/28] remove legacy file --- src/main_checkin_only.py | 139 --------------------------------------- 1 file changed, 139 deletions(-) delete mode 100644 src/main_checkin_only.py diff --git a/src/main_checkin_only.py b/src/main_checkin_only.py deleted file mode 100644 index 2bfe6c3..0000000 --- a/src/main_checkin_only.py +++ /dev/null @@ -1,139 +0,0 @@ -from datetime import datetime as dt -from core.handle_check_in import handle_check_in -from core.render_ports import get_usb_ids -from gui import * -from reader import * -import json -from sheets import * -from threading import Thread -from UserWelcome import * -from ManualFill import * -from CheckInNoId import * -from swipe import * -from tkinter import * -import global_ -import socket -import logging -import argparse -from sheets import check_api_health - - -def is_connected(host="8.8.8.8", port=53, timeout=3): - """ - Host: 8.8.8.8 (google-public-dns-a.google.com) - OpenPort: 53/tcp - Service: domain (DNS/TCP) - """ - try: - socket.setdefaulttimeout(timeout) - socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) - return True - except socket.error as ex: - return False - - -############################################################## -# This acts as the main loop of the program, ran in a thread # -############################################################## - -no_wifi_shown = False - - -def myLoop(app, reader): - global no_wifi_shown, no_wifi - logging.info("Now reading ID cards") - last_tag = 0 - last_time = 0 - while True: - time.sleep(0.1) - in_waiting = reader.getSerInWaiting() - tag = 0 - - if in_waiting >= 14: - if not is_connected(): - logging.info("ERROR wifi is not connected") - if not no_wifi_shown: - no_wifi_shown = True - no_wifi = Label( - app.get_frame(MainPage), - text="ERROR! Connection cannot be established, please let staff know.", - font=("Arial", 25), - ) - no_wifi.pack(pady=40) - no_wifi.after(4000, lambda: destroyNoWifiError(no_wifi)) - continue - - tag_read_start = perf_counter() - tag = reader.grabRFID() - tag_read_end = perf_counter() - - if " " in tag: - continue - - if tag == last_tag and not reader.canScanAgain(last_time): - logging.debug("Suppressing repeat scan") - continue - - s_reason = reader.checkRFID(tag) - - if s_reason != "good": - logging.debug(s_reason) - continue - else: - logging.debug("RFID Check Succeeded") - - global_.rfid = tag - handle_check_in(tag, contact, util) - - last_tag = tag - last_time = time.time() - - reader.readSerial() - -def destroyNoWifiError(no_wifi): - global no_wifi_shown - no_wifi.destroy() - no_wifi_shown = False - - -def clearAndReturn(): - global_.app.show_frame(MainPage) - global_.app.get_frame(ManualFill).clearEntries() - global_.app.get_frame(CheckInNoId).clearEntries() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Makerspace Check-in System", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - parser.add_argument( - "-v", - "--verbose", - action="store_true", - help="Increase verbosity (print debug info)", - ) - - args = parser.parse_args() - config = vars(args) - - if config["verbose"]: - logging.basicConfig(level=logging.DEBUG) - else: - logging.basicConfig(level=logging.INFO) - - reader_usb_id, traffic_usb_id = get_usb_ids() - check_api_health() - global_.init(traffic_usb_id) - app = gui() - global_.app = app - - reader = Reader() - util = utils() - thread = Thread(target=myLoop, args=(app, reader)) - logging.info("Starting thread") - thread.start() - app.bind("", lambda i: clearAndReturn()) - logging.info("Made it to app start") - app.start() From b232762557135bcb6967aad12c69e0b7bccdb5a9 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 14:45:32 -0700 Subject: [PATCH 05/28] switch from global vars to a shared app context --- src/app_context.py | 59 +++++++++++++++++++++++++++ src/core/handle_check_in.py | 27 ++++++------ src/global_.py | 43 ------------------- src/gui.py | 7 ++-- src/main.py | 51 ++++++++++++----------- src/screens/acc_no_waiver_swipe.py | 6 +-- src/screens/check_in_no_id.py | 7 ++-- src/screens/main_page.py | 4 +- src/screens/manual_fill.py | 2 +- src/screens/no_acc_no_waiver_swipe.py | 4 +- src/screens/user_thank.py | 13 +++--- src/screens/user_welcome.py | 7 ++-- src/screens/waiver_no_acc_swipe.py | 4 +- src/swipe.py | 36 +++++----------- src/utils.py | 17 ++++---- 15 files changed, 138 insertions(+), 149 deletions(-) create mode 100644 src/app_context.py delete mode 100644 src/global_.py diff --git a/src/app_context.py b/src/app_context.py new file mode 100644 index 0000000..8ad57fa --- /dev/null +++ b/src/app_context.py @@ -0,0 +1,59 @@ +import threading + +from sheets import SheetManager +from traffic import TrafficLight + + +class _TrafficProxy: + def __init__(self, light: TrafficLight, sheets: SheetManager): + self._light = light + self._sheets = sheets + + @property + def connected(self) -> bool: + return self._light.ser is not None + + def _post(self, color: str) -> None: + threading.Thread( + target=self._sheets.set_traffic_light, + args=(color,), + daemon=True, + ).start() + + def set_red(self) -> None: + self._post("red") + + def set_green(self) -> None: + self._post("green") + + def set_yellow(self) -> None: + self._post("yellow") + + def set_off(self) -> None: + self._post("off") + + +class AppContext: + def __init__(self, sheets: SheetManager, traffic_light: _TrafficProxy): + self.sheets = sheets + self.traffic_light = traffic_light + self.app = None # set after Gui is created + self._rfid_lock = threading.Lock() + self._rfid: str = "" + + @property + def rfid(self) -> str: + with self._rfid_lock: + return self._rfid + + @rfid.setter + def rfid(self, value: str) -> None: + with self._rfid_lock: + self._rfid = value + + @classmethod + def create(cls, traffic_usb_id=None) -> "AppContext": + sheets = SheetManager() + light = TrafficLight(traffic_usb_id) + proxy = _TrafficProxy(light, sheets) + return cls(sheets, proxy) diff --git a/src/core/handle_check_in.py b/src/core/handle_check_in.py index 8d5f964..8fc70a7 100644 --- a/src/core/handle_check_in.py +++ b/src/core/handle_check_in.py @@ -1,11 +1,10 @@ -import global_ import logging from tkinter import Label from core.write_checkin import write_checkin -def handle_check_in(tag): - result = global_.sheets.checkin_by_uuid(tag) +def handle_check_in(ctx, tag): + result = ctx.sheets.checkin_by_uuid(tag) status = result.get("status") def update_ui(): @@ -18,9 +17,9 @@ def update_ui(): if status == "api_error": logging.error("API error during check-in") - global_.traffic_light.set_red() + ctx.traffic_light.set_red() error_label = Label( - global_.app.canvas, + ctx.app.canvas, text="System error, please let staff know.", bg="#153246", fg="white", font=("Arial", 25), ) @@ -30,24 +29,24 @@ def update_ui(): if status == "no_account": logging.info(f"User {tag} not found.") - global_.traffic_light.set_red() - global_.app.show_frame(NoAccNoWaiver) - global_.app.after(3000, lambda: global_.app.show_frame(NoAccNoWaiverSwipe)) + ctx.traffic_light.set_red() + ctx.app.show_frame(NoAccNoWaiver) + ctx.app.after(3000, lambda: ctx.app.show_frame(NoAccNoWaiverSwipe)) return if status == "no_waiver": logging.info(f"User {tag} does not have waiver.") - global_.traffic_light.set_yellow() - global_.app.show_frame(AccNoWaiver) - global_.app.after(3000, lambda: global_.app.show_frame(AccNoWaiverSwipe)) + ctx.traffic_light.set_yellow() + ctx.app.show_frame(AccNoWaiver) + ctx.app.after(3000, lambda: ctx.app.show_frame(AccNoWaiverSwipe)) return logging.info(f"User found: {result['name']}") - global_.traffic_light.set_green() - global_.app.get_frame(UserWelcome).displayName(result["name"]) + ctx.traffic_light.set_green() + ctx.app.get_frame(UserWelcome).displayName(result["name"]) write_checkin({ "Name": result["name"], "Student ID": result["student_id"], }, tag) - global_.app.after(0, update_ui) + ctx.app.after(0, update_ui) diff --git a/src/global_.py b/src/global_.py deleted file mode 100644 index 88982e6..0000000 --- a/src/global_.py +++ /dev/null @@ -1,43 +0,0 @@ -import threading -from sheets import SheetManager -from traffic import TrafficLight - -rfid = "" -sheets = None -app = None -traffic_light = None - - -class _TrafficProxy: - def __init__(self, light, sheets_mgr): - self._light = light - self._sheets = sheets_mgr - - @property - def connected(self): - return self._light.ser is not None - - def _post(self, color): - threading.Thread( - target=self._sheets.set_traffic_light, - args=(color,), - daemon=True, - ).start() - - def set_red(self): - self._post("red") - - def set_green(self): - self._post("green") - - def set_yellow(self): - self._post("yellow") - - def set_off(self): - self._post("off") - - -def init(traffic_usb_id=None): - global sheets, traffic_light - sheets = SheetManager() - traffic_light = _TrafficProxy(TrafficLight(traffic_usb_id), sheets) diff --git a/src/gui.py b/src/gui.py index 99f0ce7..522e0bd 100644 --- a/src/gui.py +++ b/src/gui.py @@ -1,7 +1,7 @@ import uuid import tkinter as tk from pathlib import Path -import global_ +from app_context import AppContext ASSETS_PATH = Path(__file__).parent / "assets" / "shared" @@ -10,8 +10,9 @@ # Acts as the controller and the user interface # ################################################# class Gui(tk.Tk): - def __init__(self, *args, **kwargs): + def __init__(self, ctx: AppContext, *args, **kwargs): tk.Tk.__init__(self, *args, **kwargs) + self.ctx = ctx self.title("Check-In") self.geometry("1280x720") @@ -89,7 +90,7 @@ def timeout_fn(self, curr_uuid): from screens.main_page import MainPage if curr_uuid == self.frame_uuid: self.show_frame(MainPage) - global_.traffic_light.set_off() + self.ctx.traffic_light.set_off() def show_frame(self, cont): if self.curr_frame is not None: diff --git a/src/main.py b/src/main.py index 269c722..5faa0c1 100644 --- a/src/main.py +++ b/src/main.py @@ -2,7 +2,6 @@ from gui import Gui from reader import * from swipe import Swipe -from sheets import * from threading import Thread from screens.main_page import MainPage from screens.manual_fill import ManualFill @@ -10,12 +9,12 @@ from utils import Utils from core.handle_check_in import handle_check_in from core.render_ports import get_usb_ids -import global_ +from app_context import AppContext +from sheets import check_api_health import socket import logging import argparse from sys import stdout -from sheets import check_api_health def is_connected(host="8.8.8.8", port=53, timeout=3): """ @@ -36,7 +35,7 @@ def is_connected(host="8.8.8.8", port=53, timeout=3): no_wifi_shown = False -def myLoop(app, reader): +def myLoop(ctx: AppContext, reader): global no_wifi_shown, no_wifi logging.info("Now reading ID cards") last_tag = 0 @@ -64,7 +63,7 @@ def myLoop(app, reader): if not no_wifi_shown: no_wifi_shown = True no_wifi = Label( - app.canvas, + ctx.app.canvas, text="ERROR! Connection cannot be established, please let staff know.", bg="#153246", fg="white", font=("Arial", 25), ) @@ -72,7 +71,7 @@ def myLoop(app, reader): no_wifi.after(4000, lambda: destroyNoWifiError(no_wifi)) continue - app.get_frame(ManualFill).clearEntries() + ctx.app.get_frame(ManualFill).clearEntries() tag = reader.grabRFID() if " " in tag: @@ -90,18 +89,18 @@ def myLoop(app, reader): else: logging.debug("RFID Check Succeeded") - global_.rfid = tag - handle_check_in(tag) + ctx.rfid = tag + handle_check_in(ctx, tag) last_tag = tag last_time = time.time() -def trafficLightPoller(): +def trafficLightPoller(ctx: AppContext): last_color = None - light = global_.traffic_light._light + light = ctx.traffic_light._light while True: time.sleep(0.1) - color = global_.sheets.get_traffic_light() + color = ctx.sheets.get_traffic_light() if color != last_color: last_color = color if color == "red": @@ -119,11 +118,11 @@ def destroyNoWifiError(no_wifi): no_wifi.destroy() no_wifi_shown = False -def clearAndReturn(): - global_.traffic_light.set_off() - global_.app.show_frame(MainPage) - global_.app.get_frame(ManualFill).clearEntries() - global_.app.get_frame(CheckInNoId).clearEntries() +def clearAndReturn(ctx: AppContext): + ctx.traffic_light.set_off() + ctx.app.show_frame(MainPage) + ctx.app.get_frame(ManualFill).clearEntries() + ctx.app.get_frame(CheckInNoId).clearEntries() if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -148,21 +147,21 @@ def clearAndReturn(): reader_usb_id, traffic_usb_id = get_usb_ids() check_api_health() - global_.init(traffic_usb_id) - app = Gui() - global_.app = app - global_.traffic_light.set_off() + ctx = AppContext.create(traffic_usb_id) + app = Gui(ctx) + ctx.app = app + ctx.traffic_light.set_off() - sw = Swipe() + sw = Swipe(ctx) reader = Reader(reader_usb_id) util = Utils() - thread = Thread(target=myLoop, args=(app, reader)) + thread = Thread(target=myLoop, args=(ctx, reader)) logging.info("Starting thread") thread.start() - if global_.traffic_light.connected: - poller = Thread(target=trafficLightPoller, daemon=True) + if ctx.traffic_light.connected: + poller = Thread(target=trafficLightPoller, args=(ctx,), daemon=True) poller.start() app.bind("", lambda i: sw.keyboardPress(i)) - app.bind("", lambda i: clearAndReturn()) + app.bind("", lambda i: clearAndReturn(ctx)) logging.info("Made it to app start") - app.start() \ No newline at end of file + app.start() diff --git a/src/screens/acc_no_waiver_swipe.py b/src/screens/acc_no_waiver_swipe.py index 33ff24c..fe85e28 100644 --- a/src/screens/acc_no_waiver_swipe.py +++ b/src/screens/acc_no_waiver_swipe.py @@ -1,8 +1,6 @@ from pathlib import Path from tkinter import Button from .screen import Screen -import global_ - ASSETS_PATH = Path(__file__).parent.parent / "assets" / "acc_no_waiver_swipe_assets" @@ -50,5 +48,5 @@ def _build(self, controller): def _back_to_main(self): from .main_page import MainPage - global_.traffic_light.set_off() - global_.app.show_frame(MainPage) + self.controller.ctx.traffic_light.set_off() + self.controller.show_frame(MainPage) diff --git a/src/screens/check_in_no_id.py b/src/screens/check_in_no_id.py index daa9312..5d53898 100644 --- a/src/screens/check_in_no_id.py +++ b/src/screens/check_in_no_id.py @@ -1,7 +1,6 @@ from pathlib import Path from tkinter import Button, Entry, StringVar, END from .screen import Screen -import global_ import logging ASSETS_PATH = Path(__file__).parent.parent / "assets" / "check_in_no_id_assets" @@ -77,7 +76,7 @@ def _call_check_in(self, controller): self.canvas.update_idletasks() self.clearEntries() - result = global_.sheets.checkin_by_pid(pid) + result = self.controller.ctx.sheets.checkin_by_pid(pid) status = result.get("status") if self.loading_text_id is not None: @@ -97,5 +96,5 @@ def _call_check_in(self, controller): return logging.info(f"Manual check-in for {result['name']}") - global_.traffic_light.set_green() - global_.app.get_frame(UserWelcome).displayName(result["name"]) + self.controller.ctx.traffic_light.set_green() + self.controller.get_frame(UserWelcome).displayName(result["name"]) diff --git a/src/screens/main_page.py b/src/screens/main_page.py index b22740e..b14d1a9 100644 --- a/src/screens/main_page.py +++ b/src/screens/main_page.py @@ -1,8 +1,6 @@ from pathlib import Path from tkinter import Button from .screen import Screen -import global_ - ASSETS_PATH = Path(__file__).parent.parent / "assets" / "main_page_assets" @@ -49,6 +47,6 @@ def _build(self, controller): def _go_to_no_id(self, controller): from .check_in_no_id import CheckInNoId - no_id = global_.app.get_frame(CheckInNoId) + no_id = controller.get_frame(CheckInNoId) no_id.clearEntries() controller.show_frame(CheckInNoId) diff --git a/src/screens/manual_fill.py b/src/screens/manual_fill.py index 791265c..2c2fa17 100644 --- a/src/screens/manual_fill.py +++ b/src/screens/manual_fill.py @@ -114,7 +114,7 @@ def _call_account_creation(self): self.clearEntries() try: delay = timeit.timeit( - lambda: util.createAccount(data[0], data[1], data[2], data[3], ManualFill), + lambda: util.createAccount(self.controller.ctx, data[0], data[1], data[2], data[3], ManualFill), number=1, ) logging.debug(f"Time to create account: {delay}") diff --git a/src/screens/no_acc_no_waiver_swipe.py b/src/screens/no_acc_no_waiver_swipe.py index 3295962..8ddeb3d 100644 --- a/src/screens/no_acc_no_waiver_swipe.py +++ b/src/screens/no_acc_no_waiver_swipe.py @@ -1,8 +1,6 @@ from pathlib import Path from tkinter import Button from .screen import Screen -import global_ - ASSETS_PATH = Path(__file__).parent.parent / "assets" / "no_acc_no_waiver_swipe_assets" @@ -44,5 +42,5 @@ def _build(self, controller): def _go_to_manual_fill(self, controller): from .manual_fill import ManualFill - global_.app.get_frame(ManualFill).clearEntries() + controller.get_frame(ManualFill).clearEntries() controller.show_frame(ManualFill) diff --git a/src/screens/user_thank.py b/src/screens/user_thank.py index db5afa2..65b1c20 100644 --- a/src/screens/user_thank.py +++ b/src/screens/user_thank.py @@ -1,5 +1,4 @@ from .screen import Screen -import global_ class UserThank(Screen): @@ -21,12 +20,12 @@ def hide(self): def displayName(self, name, nextPage): from .main_page import MainPage - global_.app.show_frame(UserThank) + self.controller.show_frame(UserThank) if nextPage == MainPage: - global_.traffic_light.set_green() + self.controller.ctx.traffic_light.set_green() else: - global_.traffic_light.set_yellow() + self.controller.ctx.traffic_light.set_yellow() self.canvas.create_text( 99.0, 323.0, anchor="nw", @@ -37,10 +36,10 @@ def displayName(self, name, nextPage): ) self.canvas.after(4500, lambda: self.canvas.delete("thank")) - global_.app.after(4000, lambda: self._go_to_next(nextPage)) + self.controller.after(4000, lambda: self._go_to_next(nextPage)) def _go_to_next(self, nextPage): from .main_page import MainPage - global_.app.show_frame(nextPage) + self.controller.show_frame(nextPage) if nextPage == MainPage: - global_.traffic_light.set_off() + self.controller.ctx.traffic_light.set_off() diff --git a/src/screens/user_welcome.py b/src/screens/user_welcome.py index 0302d74..82d9da7 100644 --- a/src/screens/user_welcome.py +++ b/src/screens/user_welcome.py @@ -1,5 +1,4 @@ from .screen import Screen -import global_ class UserWelcome(Screen): @@ -27,7 +26,7 @@ def displayName(self, name): self.last_name = name from .main_page import MainPage - global_.app.show_frame(UserWelcome) + self.controller.show_frame(UserWelcome) text_id = self.canvas.create_text( 99.0, @@ -54,5 +53,5 @@ def _remove_name(self, text_id): if not self.canvas.find_withtag("welcome"): self.last_name = None - global_.traffic_light.set_off() - global_.app.show_frame(MainPage) + self.controller.ctx.traffic_light.set_off() + self.controller.show_frame(MainPage) diff --git a/src/screens/waiver_no_acc_swipe.py b/src/screens/waiver_no_acc_swipe.py index d988575..5f8e0c2 100644 --- a/src/screens/waiver_no_acc_swipe.py +++ b/src/screens/waiver_no_acc_swipe.py @@ -1,8 +1,6 @@ from pathlib import Path from tkinter import Button from .screen import Screen -import global_ - ASSETS_PATH = Path(__file__).parent.parent / "assets" / "waiver_no_acc_swipe_assets" @@ -44,5 +42,5 @@ def _build(self, controller): def _go_to_manual_fill(self, controller): from .manual_fill import ManualFill - global_.app.get_frame(ManualFill).clearEntries() + controller.get_frame(ManualFill).clearEntries() controller.show_frame(ManualFill) diff --git a/src/swipe.py b/src/swipe.py index 1326a05..70f1777 100644 --- a/src/swipe.py +++ b/src/swipe.py @@ -6,7 +6,6 @@ from screens.check_in_no_id import CheckInNoId from get_info_from_pid import contact_client from utils import Utils -import global_ import logging ############################################ @@ -17,14 +16,15 @@ class Swipe: - def __init__(self): + def __init__(self, ctx): global id_string id_string = "" + self.ctx = ctx def keyboardPress(self, key): util = Utils() global id_string, swipe_error_shown - curr_frame = global_.app.get_curr_frame() + curr_frame = self.ctx.app.get_curr_frame() if curr_frame not in (NoAccNoWaiverSwipe, WaiverNoAccSwipe, CheckInNoId): return @@ -37,7 +37,7 @@ def keyboardPress(self, key): id_string = "" if not swipe_error_shown: swipe_error_shown = True - canvas = global_.app.canvas + canvas = self.ctx.app.canvas id_error = tkinter.Label( canvas, text="Error, please scan again", bg="#153246", fg="white", font=("Arial", 20), @@ -75,35 +75,21 @@ def pullUser(self, barcode, u_type): if not u_info: logging.info("Student search returned False, returning...") return - + logging.info(f"Info pull succeeded:\n {u_info[0]}, {u_info[1]}, {u_info[3]}") return u_info def swipeCard(self, id_string): - # Grabs the input from the global swipe entry - # Deletes text from the entry box - # Checks if any of the ID is a letter - # If so return - # Calls magswipe() on the entered string - user_card_number = id_string.strip() - # u_info = self.magSwipe(id_string) - - # u_type = u_info[0] - # u_id = u_info[1] - # u_id = u_id.replace("+E?", "")[:9] - - # u_data is a list containing the user type and their ID u_data = self.pullUser(user_card_number, "Student") if not u_data: logging.info("Student search returned False, returning...") return - # if u_type == "Student": - # u_id = "A" + u_id - if global_.app.get_curr_frame() == CheckInNoId: - global_.app.get_frame(CheckInNoId).clearEntries() - global_.app.get_frame(CheckInNoId).updateEntries(u_data[3]) + + if self.ctx.app.get_curr_frame() == CheckInNoId: + self.ctx.app.get_frame(CheckInNoId).clearEntries() + self.ctx.app.get_frame(CheckInNoId).updateEntries(u_data[3]) return email_to_use = "" if len(u_data[2]) == 0 else u_data[2][0] @@ -111,14 +97,14 @@ def swipeCard(self, id_string): if email.endswith("@ucsd.edu"): email_to_use = email - manfill = global_.app.get_frame(ManualFill) + manfill = self.ctx.app.get_frame(ManualFill) manfill.clearEntries() logging.info( f"Filling data with {u_data[0]} {u_data[1]} {email_to_use} {u_data[3]}" ) manfill.updateEntries(u_data[0], u_data[1], email_to_use, u_data[3]) - global_.app.show_frame(ManualFill) + self.ctx.app.show_frame(ManualFill) def magSwipe(self, ID): # Makes a new empty string diff --git a/src/utils.py b/src/utils.py index a8201ff..123c2f8 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,6 +1,5 @@ from datetime import datetime import time -import global_ import tkinter from screens.main_page import MainPage from screens.acc_no_waiver_swipe import AccNoWaiverSwipe @@ -51,13 +50,13 @@ def IDVet(self, id_check): def getDatetime(self): return datetime.now().strftime("%m/%d/%Y %H:%M:%S") - def createAccount(self, fname, lname, email, pid, ManualFill): + def createAccount(self, ctx, fname, lname, email, pid, ManualFill): start = time.perf_counter() idValid = self.IDCheck(pid) emailValid = self.emailCheck(email) nameValid = self.nameCheck(fname, lname) - canvas = global_.app.canvas + canvas = ctx.app.canvas for validation in (idValid, emailValid, nameValid): if validation != "good": @@ -77,7 +76,7 @@ def createAccount(self, fname, lname, email, pid, ManualFill): bg="#153246", fg="white", font=("Arial", 25), ) inProgress.place(relx=0.5, rely=0.87, anchor="center") - global_.app.update() + ctx.app.update() full_name = fname + " " + lname logging.info(f"Creating user account for {full_name}") @@ -94,7 +93,7 @@ def createAccount(self, fname, lname, email, pid, ManualFill): retries = 1 while retries < 6: try: - result = global_.sheets.create_account(fname, lname, email, pid, global_.rfid) + result = ctx.sheets.create_account(fname, lname, email, pid, ctx.rfid) end3 = time.perf_counter() logging.debug(f"Time to create account: {end3 - end2}") @@ -106,25 +105,25 @@ def createAccount(self, fname, lname, email, pid, ManualFill): logging.warning("Exception occurred while in account creation") logging.exception("Exception occurred while in account creation") no_wifi.place(relx=0.5, rely=0.91, anchor="center") - global_.app.update() + ctx.app.update() time.sleep(retries) retries += 1 no_wifi.destroy() if retries == 6: - global_.app.show_frame(MainPage) + ctx.app.show_frame(MainPage) inProgress.destroy() return end4 = time.perf_counter() logging.debug(f"Total time to send data: {end4 - end2}") - checkin_result = global_.sheets.checkin_by_uuid(global_.rfid) + checkin_result = ctx.sheets.checkin_by_uuid(ctx.rfid) toGoTo = AccNoWaiverSwipe if checkin_result.get("status") == "no_waiver" else MainPage end5 = time.perf_counter() logging.debug(f"Time to check waiver via check-in: {end5 - end4}") - global_.app.get_frame(UserThank).displayName(full_name, toGoTo) + ctx.app.get_frame(UserThank).displayName(full_name, toGoTo) inProgress.destroy() From 351489a2311e5b9877488851d16aaef5cd4ddf40 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 15:02:39 -0700 Subject: [PATCH 06/28] separate controller logic --- src/app_context.py | 3 +- src/controllers/__init__.py | 0 src/controllers/navigation_controller.py | 78 ++++++++++++++ src/controllers/swipe_controller.py | 88 ++++++++++++++++ src/core/handle_check_in.py | 14 +-- src/gui.py | 113 -------------------- src/main.py | 57 +++++----- src/swipe.py | 129 ----------------------- src/utils.py | 10 +- src/window.py | 38 +++++++ 10 files changed, 242 insertions(+), 288 deletions(-) create mode 100644 src/controllers/__init__.py create mode 100644 src/controllers/navigation_controller.py create mode 100644 src/controllers/swipe_controller.py delete mode 100644 src/gui.py delete mode 100644 src/swipe.py create mode 100644 src/window.py diff --git a/src/app_context.py b/src/app_context.py index 8ad57fa..154c59d 100644 --- a/src/app_context.py +++ b/src/app_context.py @@ -37,7 +37,8 @@ class AppContext: def __init__(self, sheets: SheetManager, traffic_light: _TrafficProxy): self.sheets = sheets self.traffic_light = traffic_light - self.app = None # set after Gui is created + self.window = None + self.nav = None self._rfid_lock = threading.Lock() self._rfid: str = "" diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/navigation_controller.py b/src/controllers/navigation_controller.py new file mode 100644 index 0000000..fa8a37f --- /dev/null +++ b/src/controllers/navigation_controller.py @@ -0,0 +1,78 @@ +import uuid + + +class NavigationController: + def __init__(self, window, ctx): + self.ctx = ctx + self._window = window + self._frames = {} + self._curr = None + self._frame_uuid = uuid.uuid4().hex + + from screens.main_page import MainPage + from screens.acc_no_waiver import AccNoWaiver + from screens.acc_no_waiver_swipe import AccNoWaiverSwipe + from screens.manual_fill import ManualFill + from screens.check_in_no_id import CheckInNoId + from screens.no_acc_check_in_only import NoAccCheckInOnly + from screens.no_acc_no_waiver import NoAccNoWaiver + from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe + from screens.qr_codes import QRCodes + from screens.user_thank import UserThank + from screens.user_welcome import UserWelcome + from screens.waiver_no_acc import WaiverNoAcc + from screens.waiver_no_acc_swipe import WaiverNoAccSwipe + + self._timeouts = { + AccNoWaiverSwipe: 30000, + QRCodes: 30000, + NoAccNoWaiverSwipe: 30000, + } + + for F in ( + MainPage, + AccNoWaiver, + AccNoWaiverSwipe, + ManualFill, + CheckInNoId, + NoAccCheckInOnly, + NoAccNoWaiver, + NoAccNoWaiverSwipe, + QRCodes, + UserThank, + UserWelcome, + WaiverNoAcc, + WaiverNoAccSwipe, + ): + self._frames[F] = F(window.canvas, self) + + self.show_frame(MainPage) + + def show_frame(self, screen_class): + if self._curr is not None: + self._frames[self._curr].hide() + self._curr = screen_class + self._frame_uuid = uuid.uuid4().hex + self._frames[screen_class].show() + + if screen_class in self._timeouts: + uid = self._frame_uuid + self._window.after( + self._timeouts[screen_class], + lambda: self._on_timeout(uid), + ) + + def get_frame(self, screen_class): + return self._frames[screen_class] + + def get_curr_frame(self): + return self._curr + + def after(self, ms, fn): + self._window.after(ms, fn) + + def _on_timeout(self, uid): + from screens.main_page import MainPage + if uid == self._frame_uuid: + self.show_frame(MainPage) + self.ctx.traffic_light.set_off() diff --git a/src/controllers/swipe_controller.py b/src/controllers/swipe_controller.py new file mode 100644 index 0000000..11193e3 --- /dev/null +++ b/src/controllers/swipe_controller.py @@ -0,0 +1,88 @@ +import tkinter +import logging +from screens.manual_fill import ManualFill +from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe +from screens.waiver_no_acc_swipe import WaiverNoAccSwipe +from screens.check_in_no_id import CheckInNoId +from get_info_from_pid import contact_client +from utils import Utils + + +class SwipeController: + def __init__(self, ctx): + self.ctx = ctx + self._id_string = "" + self._swipe_error_shown = False + + def keyboardPress(self, key): + util = Utils() + curr_frame = self.ctx.nav.get_curr_frame() + + if curr_frame not in (NoAccNoWaiverSwipe, WaiverNoAccSwipe, CheckInNoId): + return + + self._id_string += key.char + logging.debug("The array is now: " + repr(str(self._id_string))) + + if self._id_string.endswith("\r"): + if util.IDVet(self._id_string) == "bad": + self._id_string = "" + if not self._swipe_error_shown: + self._swipe_error_shown = True + id_error = tkinter.Label( + self.ctx.window.canvas, text="Error, please scan again", + bg="#153246", fg="white", font=("Arial", 20), + ) + id_error.place(relx=0.5, rely=0.85, anchor="center") + id_error.after(1500, lambda: self._destroySwipeError(id_error)) + return + + self._swipeCard(self._id_string) + self._id_string = "" + + def _pullUser(self, barcode, u_type): + logging.info(f"Card barcode read is: {barcode}. Trying to pull user...") + + contact = contact_client() + try: + if u_type == "Staff": + u_info = contact.get_staff_info(barcode) + elif u_type == "Student": + u_info = contact.get_student_info(barcode) + except Exception: + logging.warning("An exception has ocurred with pulling user information", exc_info=True) + return None + + if not u_info: + logging.info("Student search returned False, returning...") + return None + + logging.info(f"Info pull succeeded:\n {u_info[0]}, {u_info[1]}, {u_info[3]}") + return u_info + + def _swipeCard(self, id_string): + u_data = self._pullUser(id_string.strip(), "Student") + if not u_data: + logging.info("Student search returned False, returning...") + return + + if self.ctx.nav.get_curr_frame() == CheckInNoId: + self.ctx.nav.get_frame(CheckInNoId).clearEntries() + self.ctx.nav.get_frame(CheckInNoId).updateEntries(u_data[3]) + return + + email_to_use = "" if len(u_data[2]) == 0 else u_data[2][0] + for email in u_data[2]: + if email.endswith("@ucsd.edu"): + email_to_use = email + + manfill = self.ctx.nav.get_frame(ManualFill) + manfill.clearEntries() + logging.info(f"Filling data with {u_data[0]} {u_data[1]} {email_to_use} {u_data[3]}") + manfill.updateEntries(u_data[0], u_data[1], email_to_use, u_data[3]) + + self.ctx.nav.show_frame(ManualFill) + + def _destroySwipeError(self, id_error): + id_error.destroy() + self._swipe_error_shown = False diff --git a/src/core/handle_check_in.py b/src/core/handle_check_in.py index 8fc70a7..f2e4ea2 100644 --- a/src/core/handle_check_in.py +++ b/src/core/handle_check_in.py @@ -19,7 +19,7 @@ def update_ui(): logging.error("API error during check-in") ctx.traffic_light.set_red() error_label = Label( - ctx.app.canvas, + ctx.window.canvas, text="System error, please let staff know.", bg="#153246", fg="white", font=("Arial", 25), ) @@ -30,23 +30,23 @@ def update_ui(): if status == "no_account": logging.info(f"User {tag} not found.") ctx.traffic_light.set_red() - ctx.app.show_frame(NoAccNoWaiver) - ctx.app.after(3000, lambda: ctx.app.show_frame(NoAccNoWaiverSwipe)) + ctx.nav.show_frame(NoAccNoWaiver) + ctx.window.after(3000, lambda: ctx.nav.show_frame(NoAccNoWaiverSwipe)) return if status == "no_waiver": logging.info(f"User {tag} does not have waiver.") ctx.traffic_light.set_yellow() - ctx.app.show_frame(AccNoWaiver) - ctx.app.after(3000, lambda: ctx.app.show_frame(AccNoWaiverSwipe)) + ctx.nav.show_frame(AccNoWaiver) + ctx.window.after(3000, lambda: ctx.nav.show_frame(AccNoWaiverSwipe)) return logging.info(f"User found: {result['name']}") ctx.traffic_light.set_green() - ctx.app.get_frame(UserWelcome).displayName(result["name"]) + ctx.nav.get_frame(UserWelcome).displayName(result["name"]) write_checkin({ "Name": result["name"], "Student ID": result["student_id"], }, tag) - ctx.app.after(0, update_ui) + ctx.window.after(0, update_ui) diff --git a/src/gui.py b/src/gui.py deleted file mode 100644 index 522e0bd..0000000 --- a/src/gui.py +++ /dev/null @@ -1,113 +0,0 @@ -import uuid -import tkinter as tk -from pathlib import Path -from app_context import AppContext - -ASSETS_PATH = Path(__file__).parent / "assets" / "shared" - - -################################################# -# Acts as the controller and the user interface # -################################################# -class Gui(tk.Tk): - def __init__(self, ctx: AppContext, *args, **kwargs): - tk.Tk.__init__(self, *args, **kwargs) - self.ctx = ctx - - self.title("Check-In") - self.geometry("1280x720") - self.bind("", self._on_map) - - # Single shared canvas — background is always painted here, never redrawn - self.canvas = tk.Canvas( - self, - bg="#153246", - height=720, - width=1280, - bd=0, - highlightthickness=0, - ) - self.canvas.pack(fill="both", expand=True) - - # Load background images once and draw them permanently - self._bg_photos = [] - bg1 = tk.PhotoImage(file=str(ASSETS_PATH / "image_1.png")) - self._bg_photos.append(bg1) - self.canvas.create_image(640.0, 360.0, image=bg1) - - bg2 = tk.PhotoImage(file=str(ASSETS_PATH / "image_2.png")) - self._bg_photos.append(bg2) - self.canvas.create_image(639.333984375, 359.333984375, image=bg2) - - self.frames = {} - self.curr_frame = None - self.frame_uuid = uuid.uuid4().hex - - from screens.main_page import MainPage - from screens.acc_no_waiver import AccNoWaiver - from screens.acc_no_waiver_swipe import AccNoWaiverSwipe - from screens.manual_fill import ManualFill - from screens.check_in_no_id import CheckInNoId - from screens.no_acc_check_in_only import NoAccCheckInOnly - from screens.no_acc_no_waiver import NoAccNoWaiver - from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe - from screens.qr_codes import QRCodes - from screens.user_thank import UserThank - from screens.user_welcome import UserWelcome - from screens.waiver_no_acc import WaiverNoAcc - from screens.waiver_no_acc_swipe import WaiverNoAccSwipe - - self._timeouts = { - AccNoWaiverSwipe: 30000, - QRCodes: 30000, - NoAccNoWaiverSwipe: 30000, - } - - for F in ( - MainPage, - AccNoWaiver, - AccNoWaiverSwipe, - ManualFill, - CheckInNoId, - NoAccCheckInOnly, - NoAccNoWaiver, - NoAccNoWaiverSwipe, - QRCodes, - UserThank, - UserWelcome, - WaiverNoAcc, - WaiverNoAccSwipe, - ): - self.frames[F] = F(self.canvas, self) - - self.show_frame(MainPage) - - def _on_map(self, event): - self.unbind("") - self.attributes("-fullscreen", True) - - def timeout_fn(self, curr_uuid): - from screens.main_page import MainPage - if curr_uuid == self.frame_uuid: - self.show_frame(MainPage) - self.ctx.traffic_light.set_off() - - def show_frame(self, cont): - if self.curr_frame is not None: - self.frames[self.curr_frame].hide() - self.curr_frame = cont - self.frame_uuid = uuid.uuid4().hex - self.frames[cont].show() - - if cont in self._timeouts: - curr_uuid = self.frame_uuid - self.after(self._timeouts[cont], lambda: self.timeout_fn(curr_uuid)) - - def get_frame(self, cont): - return self.frames[cont] - - def get_curr_frame(self): - return self.curr_frame - - def start(self): - self.mainloop() diff --git a/src/main.py b/src/main.py index 5faa0c1..06a0009 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,8 @@ from tkinter import Label -from gui import Gui +from window import CheckInWindow +from controllers.navigation_controller import NavigationController from reader import * -from swipe import Swipe +from controllers.swipe_controller import SwipeController from threading import Thread from screens.main_page import MainPage from screens.manual_fill import ManualFill @@ -16,25 +17,19 @@ import argparse from sys import stdout + def is_connected(host="8.8.8.8", port=53, timeout=3): - """ - Host: 8.8.8.8 (google-public-dns-a.google.com) - OpenPort: 53/tcp - Service: domain (DNS/TCP) - """ try: socket.setdefaulttimeout(timeout) socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) return True - except socket.error as ex: + except socket.error: return False -############################################################## -# This acts as the main loop of the program, ran in a thread # -############################################################## no_wifi_shown = False + def myLoop(ctx: AppContext, reader): global no_wifi_shown, no_wifi logging.info("Now reading ID cards") @@ -63,7 +58,7 @@ def myLoop(ctx: AppContext, reader): if not no_wifi_shown: no_wifi_shown = True no_wifi = Label( - ctx.app.canvas, + ctx.window.canvas, text="ERROR! Connection cannot be established, please let staff know.", bg="#153246", fg="white", font=("Arial", 25), ) @@ -71,7 +66,7 @@ def myLoop(ctx: AppContext, reader): no_wifi.after(4000, lambda: destroyNoWifiError(no_wifi)) continue - ctx.app.get_frame(ManualFill).clearEntries() + ctx.nav.get_frame(ManualFill).clearEntries() tag = reader.grabRFID() if " " in tag: @@ -95,6 +90,7 @@ def myLoop(ctx: AppContext, reader): last_tag = tag last_time = time.time() + def trafficLightPoller(ctx: AppContext): last_color = None light = ctx.traffic_light._light @@ -118,29 +114,23 @@ def destroyNoWifiError(no_wifi): no_wifi.destroy() no_wifi_shown = False + def clearAndReturn(ctx: AppContext): ctx.traffic_light.set_off() - ctx.app.show_frame(MainPage) - ctx.app.get_frame(ManualFill).clearEntries() - ctx.app.get_frame(CheckInNoId).clearEntries() + ctx.nav.show_frame(MainPage) + ctx.nav.get_frame(ManualFill).clearEntries() + ctx.nav.get_frame(CheckInNoId).clearEntries() + if __name__ == "__main__": parser = argparse.ArgumentParser( description="Makerspace Check-in System", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - - parser.add_argument( - "-v", - "--verbose", - action="store_true", - help="Increase verbosity (print debug info)", - ) - + parser.add_argument("-v", "--verbose", action="store_true", help="Increase verbosity (print debug info)") args = parser.parse_args() - config = vars(args) - if config["verbose"]: + if args.verbose: logging.basicConfig(level=logging.DEBUG, stream=stdout) else: logging.basicConfig(level=logging.INFO) @@ -148,20 +138,21 @@ def clearAndReturn(ctx: AppContext): reader_usb_id, traffic_usb_id = get_usb_ids() check_api_health() ctx = AppContext.create(traffic_usb_id) - app = Gui(ctx) - ctx.app = app + window = CheckInWindow() + nav = NavigationController(window, ctx) + ctx.window = window + ctx.nav = nav ctx.traffic_light.set_off() - sw = Swipe(ctx) + sw = SwipeController(ctx) reader = Reader(reader_usb_id) - util = Utils() thread = Thread(target=myLoop, args=(ctx, reader)) logging.info("Starting thread") thread.start() if ctx.traffic_light.connected: poller = Thread(target=trafficLightPoller, args=(ctx,), daemon=True) poller.start() - app.bind("", lambda i: sw.keyboardPress(i)) - app.bind("", lambda i: clearAndReturn(ctx)) + window.bind("", lambda i: sw.keyboardPress(i)) + window.bind("", lambda i: clearAndReturn(ctx)) logging.info("Made it to app start") - app.start() + window.start() diff --git a/src/swipe.py b/src/swipe.py deleted file mode 100644 index 70f1777..0000000 --- a/src/swipe.py +++ /dev/null @@ -1,129 +0,0 @@ -import tkinter -from screens.manual_fill import ManualFill -from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe -from screens.waiver_no_acc_swipe import WaiverNoAccSwipe -from screens.acc_no_waiver_swipe import AccNoWaiverSwipe -from screens.check_in_no_id import CheckInNoId -from get_info_from_pid import contact_client -from utils import Utils -import logging - -############################################ -# This class helps handle reading magswipe # -############################################ - -swipe_error_shown = False - - -class Swipe: - def __init__(self, ctx): - global id_string - id_string = "" - self.ctx = ctx - - def keyboardPress(self, key): - util = Utils() - global id_string, swipe_error_shown - curr_frame = self.ctx.app.get_curr_frame() - - if curr_frame not in (NoAccNoWaiverSwipe, WaiverNoAccSwipe, CheckInNoId): - return - - id_string += key.char - logging.debug("The array is now: " + repr(str(id_string))) - - if id_string.endswith("\r"): - if util.IDVet(id_string) == "bad": - id_string = "" - if not swipe_error_shown: - swipe_error_shown = True - canvas = self.ctx.app.canvas - id_error = tkinter.Label( - canvas, text="Error, please scan again", - bg="#153246", fg="white", font=("Arial", 20), - ) - id_error.place(relx=0.5, rely=0.85, anchor="center") - id_error_2 = id_error # single label serves both swipe screens - id_error.after(1500, lambda: self.destroySwipeError(id_error)) - id_error_2.after(1500, lambda: self.destroySwipeError(id_error_2)) - return - - self.swipeCard(id_string) - id_string = "" - - def pullUser(self, barcode, u_type): - # This function takes in the User's ID and - # if they are a Student or Staff - # and runs David's query funciton accordingly - # It returns a list containing: - # [fname, lname, [emails]] - u_info = [] - - logging.info(f"Card barcode read is: {barcode}. Trying to pull user...") - - contact = contact_client() - try: - if u_type == "Staff": - u_info = contact.get_staff_info(barcode) - elif u_type == "Student": - u_info = contact.get_student_info(barcode) - except Exception as e: - logging.warning( - "An exception has ocurred with pulling user information", exc_info=True - ) - return None - if not u_info: - logging.info("Student search returned False, returning...") - return - - logging.info(f"Info pull succeeded:\n {u_info[0]}, {u_info[1]}, {u_info[3]}") - return u_info - - def swipeCard(self, id_string): - user_card_number = id_string.strip() - - u_data = self.pullUser(user_card_number, "Student") - if not u_data: - logging.info("Student search returned False, returning...") - return - - if self.ctx.app.get_curr_frame() == CheckInNoId: - self.ctx.app.get_frame(CheckInNoId).clearEntries() - self.ctx.app.get_frame(CheckInNoId).updateEntries(u_data[3]) - return - - email_to_use = "" if len(u_data[2]) == 0 else u_data[2][0] - for email in u_data[2]: - if email.endswith("@ucsd.edu"): - email_to_use = email - - manfill = self.ctx.app.get_frame(ManualFill) - manfill.clearEntries() - logging.info( - f"Filling data with {u_data[0]} {u_data[1]} {email_to_use} {u_data[3]}" - ) - manfill.updateEntries(u_data[0], u_data[1], email_to_use, u_data[3]) - - self.ctx.app.show_frame(ManualFill) - - def magSwipe(self, ID): - # Makes a new empty string - # Takes only chars 3-11 from the card swipe text - # Returns student or staff ID - - u_type = "" - - if ID[2] == "9": - u_type = "Student" - elif ID[2] == "0": - u_type = "Staff" - - s = "" - for c in range(3, 11): - s += ID[c] - return [u_type, s] - - def destroySwipeError(self, id_error): - global swipe_error_shown - id_error.destroy() - swipe_error_shown = False diff --git a/src/utils.py b/src/utils.py index 123c2f8..8a00f42 100644 --- a/src/utils.py +++ b/src/utils.py @@ -56,7 +56,7 @@ def createAccount(self, ctx, fname, lname, email, pid, ManualFill): emailValid = self.emailCheck(email) nameValid = self.nameCheck(fname, lname) - canvas = ctx.app.canvas + canvas = ctx.window.canvas for validation in (idValid, emailValid, nameValid): if validation != "good": @@ -76,7 +76,7 @@ def createAccount(self, ctx, fname, lname, email, pid, ManualFill): bg="#153246", fg="white", font=("Arial", 25), ) inProgress.place(relx=0.5, rely=0.87, anchor="center") - ctx.app.update() + ctx.window.update() full_name = fname + " " + lname logging.info(f"Creating user account for {full_name}") @@ -105,14 +105,14 @@ def createAccount(self, ctx, fname, lname, email, pid, ManualFill): logging.warning("Exception occurred while in account creation") logging.exception("Exception occurred while in account creation") no_wifi.place(relx=0.5, rely=0.91, anchor="center") - ctx.app.update() + ctx.window.update() time.sleep(retries) retries += 1 no_wifi.destroy() if retries == 6: - ctx.app.show_frame(MainPage) + ctx.nav.show_frame(MainPage) inProgress.destroy() return @@ -125,5 +125,5 @@ def createAccount(self, ctx, fname, lname, email, pid, ManualFill): end5 = time.perf_counter() logging.debug(f"Time to check waiver via check-in: {end5 - end4}") - ctx.app.get_frame(UserThank).displayName(full_name, toGoTo) + ctx.nav.get_frame(UserThank).displayName(full_name, toGoTo) inProgress.destroy() diff --git a/src/window.py b/src/window.py new file mode 100644 index 0000000..e8f4738 --- /dev/null +++ b/src/window.py @@ -0,0 +1,38 @@ +import tkinter as tk +from pathlib import Path + +ASSETS_PATH = Path(__file__).parent / "assets" / "shared" + + +class CheckInWindow(tk.Tk): + def __init__(self): + super().__init__() + self.title("Check-In") + self.geometry("1280x720") + self.bind("", self._on_map) + + self.canvas = tk.Canvas( + self, + bg="#153246", + height=720, + width=1280, + bd=0, + highlightthickness=0, + ) + self.canvas.pack(fill="both", expand=True) + + self._bg_photos = [] + bg1 = tk.PhotoImage(file=str(ASSETS_PATH / "image_1.png")) + self._bg_photos.append(bg1) + self.canvas.create_image(640.0, 360.0, image=bg1) + + bg2 = tk.PhotoImage(file=str(ASSETS_PATH / "image_2.png")) + self._bg_photos.append(bg2) + self.canvas.create_image(639.333984375, 359.333984375, image=bg2) + + def _on_map(self, event): + self.unbind("") + self.attributes("-fullscreen", True) + + def start(self): + self.mainloop() From 8d2b9239506a7571304031e99e058f5862faec08 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 16:31:05 -0700 Subject: [PATCH 07/28] separate controller logic 2 --- src/api/__init__.py | 0 src/{ => api}/get_info_from_pid.py | 0 src/{ => api}/sheets.py | 0 src/app_context.py | 6 +- .../account_controller.py} | 78 +++++------ src/controllers/card_reader_controller.py | 107 +++++++++++++++ src/controllers/check_in_controller.py | 77 +++++++++++ src/controllers/navigation_controller.py | 19 ++- src/controllers/swipe_controller.py | 13 +- src/core/__init__.py | 2 - src/core/handle_check_in.py | 52 -------- src/core/write_checkin.py | 27 ---- src/hardware/__init__.py | 0 src/{ => hardware}/reader.py | 0 src/{core => hardware}/render_ports.py | 0 src/{ => hardware}/traffic.py | 0 src/main.py | 126 ++---------------- src/screens/acc_no_waiver_swipe.py | 4 +- src/screens/check_in_no_id.py | 25 +--- src/screens/main_page.py | 5 +- src/screens/manual_fill.py | 4 +- src/screens/no_acc_no_waiver_swipe.py | 4 +- src/screens/user_thank.py | 12 +- src/screens/user_welcome.py | 5 +- src/screens/waiver_no_acc_swipe.py | 4 +- 25 files changed, 267 insertions(+), 303 deletions(-) create mode 100644 src/api/__init__.py rename src/{ => api}/get_info_from_pid.py (100%) rename src/{ => api}/sheets.py (100%) rename src/{utils.py => controllers/account_controller.py} (65%) create mode 100644 src/controllers/card_reader_controller.py create mode 100644 src/controllers/check_in_controller.py delete mode 100644 src/core/__init__.py delete mode 100644 src/core/handle_check_in.py delete mode 100644 src/core/write_checkin.py create mode 100644 src/hardware/__init__.py rename src/{ => hardware}/reader.py (100%) rename src/{core => hardware}/render_ports.py (100%) rename src/{ => hardware}/traffic.py (100%) diff --git a/src/api/__init__.py b/src/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/get_info_from_pid.py b/src/api/get_info_from_pid.py similarity index 100% rename from src/get_info_from_pid.py rename to src/api/get_info_from_pid.py diff --git a/src/sheets.py b/src/api/sheets.py similarity index 100% rename from src/sheets.py rename to src/api/sheets.py diff --git a/src/app_context.py b/src/app_context.py index 154c59d..19782c1 100644 --- a/src/app_context.py +++ b/src/app_context.py @@ -1,7 +1,7 @@ import threading -from sheets import SheetManager -from traffic import TrafficLight +from api.sheets import SheetManager +from hardware.traffic import TrafficLight class _TrafficProxy: @@ -39,6 +39,8 @@ def __init__(self, sheets: SheetManager, traffic_light: _TrafficProxy): self.traffic_light = traffic_light self.window = None self.nav = None + self.check_in = None + self.account = None self._rfid_lock = threading.Lock() self._rfid: str = "" diff --git a/src/utils.py b/src/controllers/account_controller.py similarity index 65% rename from src/utils.py rename to src/controllers/account_controller.py index 8a00f42..7a09d59 100644 --- a/src/utils.py +++ b/src/controllers/account_controller.py @@ -1,62 +1,37 @@ -from datetime import datetime import time import tkinter +import logging from screens.main_page import MainPage from screens.acc_no_waiver_swipe import AccNoWaiverSwipe from screens.user_thank import UserThank -import logging -###################################################### -# Utilities that I couldn't get to fit anywhere else # -###################################################### +class AccountController: + def __init__(self, ctx): + self.ctx = ctx -class Utils: - def __init__(self) -> None: - pass - - def emailCheck(self, email): - validations = ( - (lambda s: "@" in s, "Email is invalid"), - (lambda s: "." in s, "Email is invalid"), - ) - - for valid, message in validations: - if not valid(email): - return message - + def _email_check(self, email): + if "@" not in email or "." not in email: + return "Email is invalid" return "good" - def nameCheck(self, fname, lname): + def _name_check(self, fname, lname): if len(fname) == 0 or len(lname) == 0: return "Name was not entered" - return "good" - def IDCheck(self, user_id): + def _id_check(self, user_id): if len(user_id) <= 2 or len(user_id) > 12: return "PID was not entered correctly" return "good" - def IDVet(self, id_check): - if any(i.isalpha() for i in id_check): - return "bad" - - if len(id_check) >= 16: - return "bad" - - return "good" - - def getDatetime(self): - return datetime.now().strftime("%m/%d/%Y %H:%M:%S") - - def createAccount(self, ctx, fname, lname, email, pid, ManualFill): + def create_account(self, fname, lname, email, pid): start = time.perf_counter() - idValid = self.IDCheck(pid) - emailValid = self.emailCheck(email) - nameValid = self.nameCheck(fname, lname) + idValid = self._id_check(pid) + emailValid = self._email_check(email) + nameValid = self._name_check(fname, lname) - canvas = ctx.window.canvas + canvas = self.ctx.window.canvas for validation in (idValid, emailValid, nameValid): if validation != "good": @@ -76,7 +51,7 @@ def createAccount(self, ctx, fname, lname, email, pid, ManualFill): bg="#153246", fg="white", font=("Arial", 25), ) inProgress.place(relx=0.5, rely=0.87, anchor="center") - ctx.window.update() + self.ctx.window.update() full_name = fname + " " + lname logging.info(f"Creating user account for {full_name}") @@ -93,7 +68,7 @@ def createAccount(self, ctx, fname, lname, email, pid, ManualFill): retries = 1 while retries < 6: try: - result = ctx.sheets.create_account(fname, lname, email, pid, ctx.rfid) + result = self.ctx.sheets.create_account(fname, lname, email, pid, self.ctx.rfid) end3 = time.perf_counter() logging.debug(f"Time to create account: {end3 - end2}") @@ -105,25 +80,38 @@ def createAccount(self, ctx, fname, lname, email, pid, ManualFill): logging.warning("Exception occurred while in account creation") logging.exception("Exception occurred while in account creation") no_wifi.place(relx=0.5, rely=0.91, anchor="center") - ctx.window.update() + self.ctx.window.update() time.sleep(retries) retries += 1 no_wifi.destroy() if retries == 6: - ctx.nav.show_frame(MainPage) + self.ctx.nav.show_frame(MainPage) inProgress.destroy() return end4 = time.perf_counter() logging.debug(f"Total time to send data: {end4 - end2}") - checkin_result = ctx.sheets.checkin_by_uuid(ctx.rfid) + checkin_result = self.ctx.sheets.checkin_by_uuid(self.ctx.rfid) toGoTo = AccNoWaiverSwipe if checkin_result.get("status") == "no_waiver" else MainPage end5 = time.perf_counter() logging.debug(f"Time to check waiver via check-in: {end5 - end4}") - ctx.nav.get_frame(UserThank).displayName(full_name, toGoTo) + self.ctx.nav.get_frame(UserThank).displayName(full_name, toGoTo) inProgress.destroy() + + def on_thank_start(self, next_page): + from screens.main_page import MainPage + if next_page == MainPage: + self.ctx.traffic_light.set_green() + else: + self.ctx.traffic_light.set_yellow() + + def on_thank_done(self, next_page): + from screens.main_page import MainPage + self.ctx.nav.show_frame(next_page) + if next_page == MainPage: + self.ctx.traffic_light.set_off() diff --git a/src/controllers/card_reader_controller.py b/src/controllers/card_reader_controller.py new file mode 100644 index 0000000..67e03ea --- /dev/null +++ b/src/controllers/card_reader_controller.py @@ -0,0 +1,107 @@ +import time +import socket +import logging +from tkinter import Label +from threading import Thread +from screens.manual_fill import ManualFill + + +class CardReaderController: + def __init__(self, ctx): + self.ctx = ctx + self._no_wifi_shown = False + + def start(self, reader): + thread = Thread(target=self._run, args=(reader,)) + thread.start() + if self.ctx.traffic_light.connected: + poller = Thread(target=self._poll_traffic_light, daemon=True) + poller.start() + + def _run(self, reader): + logging.info("Now reading ID cards") + last_tag = 0 + last_time = 0 + scanner_error = False + while True: + if scanner_error: + time.sleep(0.1) + if reader.reconnect(): + logging.info("Card reader reconnected") + scanner_error = False + continue + + try: + in_waiting = reader.getSerInWaiting() + except OSError as e: + if not scanner_error: + logging.error("Card reader disconnected, disabling until reconnection: %s", e) + scanner_error = True + continue + + if in_waiting >= 14: + if not self._is_connected(): + logging.info("ERROR wifi is not connected") + if not self._no_wifi_shown: + self._no_wifi_shown = True + no_wifi = Label( + self.ctx.window.canvas, + text="ERROR! Connection cannot be established, please let staff know.", + bg="#153246", fg="white", font=("Arial", 25), + ) + no_wifi.place(relx=0.5, rely=0.1, anchor="center") + no_wifi.after(4000, lambda: self._destroy_wifi_error(no_wifi)) + continue + + self.ctx.nav.get_frame(ManualFill).clearEntries() + tag = reader.grabRFID() + + if " " in tag: + continue + + if tag == last_tag and not reader.canScanAgain(last_time): + logging.debug("Suppressing repeat scan") + continue + + s_reason = reader.checkRFID(tag) + + if s_reason != "good": + logging.debug(s_reason) + continue + else: + logging.debug("RFID Check Succeeded") + + self.ctx.rfid = tag + self.ctx.check_in.handle_by_uuid(tag) + + last_tag = tag + last_time = time.time() + + def _poll_traffic_light(self): + last_color = None + light = self.ctx.traffic_light._light + while True: + time.sleep(0.1) + color = self.ctx.sheets.get_traffic_light() + if color != last_color: + last_color = color + if color == "red": + light.set_red() + elif color == "green": + light.set_green() + elif color == "yellow": + light.set_yellow() + else: + light.set_off() + + def _is_connected(self, host="8.8.8.8", port=53, timeout=3): + try: + socket.setdefaulttimeout(timeout) + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) + return True + except socket.error: + return False + + def _destroy_wifi_error(self, label): + label.destroy() + self._no_wifi_shown = False diff --git a/src/controllers/check_in_controller.py b/src/controllers/check_in_controller.py new file mode 100644 index 0000000..dd58140 --- /dev/null +++ b/src/controllers/check_in_controller.py @@ -0,0 +1,77 @@ +import logging +from tkinter import Label + + +class CheckInController: + def __init__(self, ctx): + self.ctx = ctx + + def handle_by_uuid(self, tag): + result = self.ctx.sheets.checkin_by_uuid(tag) + status = result.get("status") + + def update_ui(): + from screens.main_page import MainPage + from screens.no_acc_no_waiver import NoAccNoWaiver + from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe + from screens.acc_no_waiver import AccNoWaiver + from screens.acc_no_waiver_swipe import AccNoWaiverSwipe + from screens.user_welcome import UserWelcome + + if status == "api_error": + logging.error("API error during check-in") + self.ctx.traffic_light.set_red() + error_label = Label( + self.ctx.window.canvas, + text="System error, please let staff know.", + bg="#153246", fg="white", font=("Arial", 25), + ) + error_label.place(relx=0.5, rely=0.1, anchor="center") + error_label.after(4000, error_label.destroy) + return + + if status == "no_account": + logging.info(f"User {tag} not found.") + self.ctx.traffic_light.set_red() + self.ctx.nav.show_frame(NoAccNoWaiver) + self.ctx.window.after(3000, lambda: self.ctx.nav.show_frame(NoAccNoWaiverSwipe)) + return + + if status == "no_waiver": + logging.info(f"User {tag} does not have waiver.") + self.ctx.traffic_light.set_yellow() + self.ctx.nav.show_frame(AccNoWaiver) + self.ctx.window.after(3000, lambda: self.ctx.nav.show_frame(AccNoWaiverSwipe)) + return + + logging.info(f"User found: {result['name']}") + self.ctx.traffic_light.set_green() + self.ctx.nav.get_frame(UserWelcome).displayName(result["name"]) + + self.ctx.window.after(0, update_ui) + + def handle_by_pid(self, pid): + from screens.no_acc_check_in_only import NoAccCheckInOnly + from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe + from screens.user_welcome import UserWelcome + from screens.acc_no_waiver import AccNoWaiver + from screens.main_page import MainPage + + result = self.ctx.sheets.checkin_by_pid(pid) + status = result.get("status") + + if status == "no_account": + logging.info("Manual check-in: user account not found") + self.ctx.nav.show_frame(NoAccCheckInOnly) + self.ctx.nav.after(5000, lambda: self.ctx.nav.show_frame(MainPage)) + return + + if status == "no_waiver": + logging.info(f"Manual check-in: no waiver for {result.get('name', pid)}") + self.ctx.nav.show_frame(AccNoWaiver) + self.ctx.nav.after(3000, lambda: self.ctx.nav.show_frame(NoAccNoWaiverSwipe)) + return + + logging.info(f"Manual check-in for {result['name']}") + self.ctx.traffic_light.set_green() + self.ctx.nav.get_frame(UserWelcome).displayName(result["name"]) diff --git a/src/controllers/navigation_controller.py b/src/controllers/navigation_controller.py index fa8a37f..bbd6ec3 100644 --- a/src/controllers/navigation_controller.py +++ b/src/controllers/navigation_controller.py @@ -71,8 +71,21 @@ def get_curr_frame(self): def after(self, ms, fn): self._window.after(ms, fn) - def _on_timeout(self, uid): + def back_to_main(self): from screens.main_page import MainPage + self.ctx.traffic_light.set_off() + self.show_frame(MainPage) + + def go_to_no_id(self): + from screens.check_in_no_id import CheckInNoId + self.get_frame(CheckInNoId).clearEntries() + self.show_frame(CheckInNoId) + + def go_to_manual_fill(self): + from screens.manual_fill import ManualFill + self.get_frame(ManualFill).clearEntries() + self.show_frame(ManualFill) + + def _on_timeout(self, uid): if uid == self._frame_uuid: - self.show_frame(MainPage) - self.ctx.traffic_light.set_off() + self.back_to_main() diff --git a/src/controllers/swipe_controller.py b/src/controllers/swipe_controller.py index 11193e3..308d043 100644 --- a/src/controllers/swipe_controller.py +++ b/src/controllers/swipe_controller.py @@ -4,8 +4,7 @@ from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe from screens.waiver_no_acc_swipe import WaiverNoAccSwipe from screens.check_in_no_id import CheckInNoId -from get_info_from_pid import contact_client -from utils import Utils +from api.get_info_from_pid import contact_client class SwipeController: @@ -14,8 +13,14 @@ def __init__(self, ctx): self._id_string = "" self._swipe_error_shown = False + def _id_vet(self, id_check): + if any(i.isalpha() for i in id_check): + return "bad" + if len(id_check) >= 16: + return "bad" + return "good" + def keyboardPress(self, key): - util = Utils() curr_frame = self.ctx.nav.get_curr_frame() if curr_frame not in (NoAccNoWaiverSwipe, WaiverNoAccSwipe, CheckInNoId): @@ -25,7 +30,7 @@ def keyboardPress(self, key): logging.debug("The array is now: " + repr(str(self._id_string))) if self._id_string.endswith("\r"): - if util.IDVet(self._id_string) == "bad": + if self._id_vet(self._id_string) == "bad": self._id_string = "" if not self._swipe_error_shown: self._swipe_error_shown = True diff --git a/src/core/__init__.py b/src/core/__init__.py deleted file mode 100644 index 32f632f..0000000 --- a/src/core/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# This is empty to ensure the directory is recognized as a package. -# You can add any necessary imports or initializations here if needed. \ No newline at end of file diff --git a/src/core/handle_check_in.py b/src/core/handle_check_in.py deleted file mode 100644 index f2e4ea2..0000000 --- a/src/core/handle_check_in.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -from tkinter import Label -from core.write_checkin import write_checkin - - -def handle_check_in(ctx, tag): - result = ctx.sheets.checkin_by_uuid(tag) - status = result.get("status") - - def update_ui(): - from screens.main_page import MainPage - from screens.no_acc_no_waiver import NoAccNoWaiver - from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe - from screens.acc_no_waiver import AccNoWaiver - from screens.acc_no_waiver_swipe import AccNoWaiverSwipe - from screens.user_welcome import UserWelcome - - if status == "api_error": - logging.error("API error during check-in") - ctx.traffic_light.set_red() - error_label = Label( - ctx.window.canvas, - text="System error, please let staff know.", - bg="#153246", fg="white", font=("Arial", 25), - ) - error_label.place(relx=0.5, rely=0.1, anchor="center") - error_label.after(4000, error_label.destroy) - return - - if status == "no_account": - logging.info(f"User {tag} not found.") - ctx.traffic_light.set_red() - ctx.nav.show_frame(NoAccNoWaiver) - ctx.window.after(3000, lambda: ctx.nav.show_frame(NoAccNoWaiverSwipe)) - return - - if status == "no_waiver": - logging.info(f"User {tag} does not have waiver.") - ctx.traffic_light.set_yellow() - ctx.nav.show_frame(AccNoWaiver) - ctx.window.after(3000, lambda: ctx.nav.show_frame(AccNoWaiverSwipe)) - return - - logging.info(f"User found: {result['name']}") - ctx.traffic_light.set_green() - ctx.nav.get_frame(UserWelcome).displayName(result["name"]) - write_checkin({ - "Name": result["name"], - "Student ID": result["student_id"], - }, tag) - - ctx.window.after(0, update_ui) diff --git a/src/core/write_checkin.py b/src/core/write_checkin.py deleted file mode 100644 index 720dc98..0000000 --- a/src/core/write_checkin.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import json -from datetime import datetime as dt - -LOG_BASE_PATH = "assets/logs" - -def write_checkin(curr_user, tag): - now = dt.now() - year = now.strftime("%Y") - month = now.strftime("%m") - day = now.strftime("%d") - timestamp = now.isoformat(timespec="seconds") - - log_dir = os.path.join(LOG_BASE_PATH, year, month) - os.makedirs(log_dir, exist_ok=True) - - log_path = os.path.join(log_dir, f"{day}.log") - log_entry = { - "tag": tag, - "name": curr_user.get("Name", ""), - "pid": curr_user.get("Student ID", ""), - "timestamp": timestamp, - } - - with open(log_path, "a", encoding="utf-8") as f: - json.dump(log_entry, f) - f.write("\n") \ No newline at end of file diff --git a/src/hardware/__init__.py b/src/hardware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/reader.py b/src/hardware/reader.py similarity index 100% rename from src/reader.py rename to src/hardware/reader.py diff --git a/src/core/render_ports.py b/src/hardware/render_ports.py similarity index 100% rename from src/core/render_ports.py rename to src/hardware/render_ports.py diff --git a/src/traffic.py b/src/hardware/traffic.py similarity index 100% rename from src/traffic.py rename to src/hardware/traffic.py diff --git a/src/main.py b/src/main.py index 06a0009..56ada31 100644 --- a/src/main.py +++ b/src/main.py @@ -1,123 +1,22 @@ -from tkinter import Label from window import CheckInWindow from controllers.navigation_controller import NavigationController -from reader import * from controllers.swipe_controller import SwipeController -from threading import Thread -from screens.main_page import MainPage +from controllers.check_in_controller import CheckInController +from controllers.account_controller import AccountController +from controllers.card_reader_controller import CardReaderController +from hardware.reader import Reader from screens.manual_fill import ManualFill from screens.check_in_no_id import CheckInNoId -from utils import Utils -from core.handle_check_in import handle_check_in -from core.render_ports import get_usb_ids +from hardware.render_ports import get_usb_ids from app_context import AppContext -from sheets import check_api_health -import socket +from api.sheets import check_api_health import logging import argparse from sys import stdout -def is_connected(host="8.8.8.8", port=53, timeout=3): - try: - socket.setdefaulttimeout(timeout) - socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) - return True - except socket.error: - return False - - -no_wifi_shown = False - - -def myLoop(ctx: AppContext, reader): - global no_wifi_shown, no_wifi - logging.info("Now reading ID cards") - last_tag = 0 - last_time = 0 - scanner_error = False - while True: - if scanner_error: - time.sleep(0.1) - if reader.reconnect(): - logging.info("Card reader reconnected") - scanner_error = False - continue - - try: - in_waiting = reader.getSerInWaiting() - except OSError as e: - if not scanner_error: - logging.error("Card reader disconnected, disabling until reconnection: %s", e) - scanner_error = True - continue - - if in_waiting >= 14: - if not is_connected(): - logging.info("ERROR wifi is not connected") - if not no_wifi_shown: - no_wifi_shown = True - no_wifi = Label( - ctx.window.canvas, - text="ERROR! Connection cannot be established, please let staff know.", - bg="#153246", fg="white", font=("Arial", 25), - ) - no_wifi.place(relx=0.5, rely=0.1, anchor="center") - no_wifi.after(4000, lambda: destroyNoWifiError(no_wifi)) - continue - - ctx.nav.get_frame(ManualFill).clearEntries() - tag = reader.grabRFID() - - if " " in tag: - continue - - if tag == last_tag and not reader.canScanAgain(last_time): - logging.debug("Suppressing repeat scan") - continue - - s_reason = reader.checkRFID(tag) - - if s_reason != "good": - logging.debug(s_reason) - continue - else: - logging.debug("RFID Check Succeeded") - - ctx.rfid = tag - handle_check_in(ctx, tag) - - last_tag = tag - last_time = time.time() - - -def trafficLightPoller(ctx: AppContext): - last_color = None - light = ctx.traffic_light._light - while True: - time.sleep(0.1) - color = ctx.sheets.get_traffic_light() - if color != last_color: - last_color = color - if color == "red": - light.set_red() - elif color == "green": - light.set_green() - elif color == "yellow": - light.set_yellow() - else: - light.set_off() - - -def destroyNoWifiError(no_wifi): - global no_wifi_shown - no_wifi.destroy() - no_wifi_shown = False - - def clearAndReturn(ctx: AppContext): - ctx.traffic_light.set_off() - ctx.nav.show_frame(MainPage) + ctx.nav.back_to_main() ctx.nav.get_frame(ManualFill).clearEntries() ctx.nav.get_frame(CheckInNoId).clearEntries() @@ -142,16 +41,15 @@ def clearAndReturn(ctx: AppContext): nav = NavigationController(window, ctx) ctx.window = window ctx.nav = nav + ctx.check_in = CheckInController(ctx) + ctx.account = AccountController(ctx) ctx.traffic_light.set_off() sw = SwipeController(ctx) reader = Reader(reader_usb_id) - thread = Thread(target=myLoop, args=(ctx, reader)) - logging.info("Starting thread") - thread.start() - if ctx.traffic_light.connected: - poller = Thread(target=trafficLightPoller, args=(ctx,), daemon=True) - poller.start() + card_reader = CardReaderController(ctx) + card_reader.start(reader) + window.bind("", lambda i: sw.keyboardPress(i)) window.bind("", lambda i: clearAndReturn(ctx)) logging.info("Made it to app start") diff --git a/src/screens/acc_no_waiver_swipe.py b/src/screens/acc_no_waiver_swipe.py index fe85e28..70ed8d0 100644 --- a/src/screens/acc_no_waiver_swipe.py +++ b/src/screens/acc_no_waiver_swipe.py @@ -47,6 +47,4 @@ def _build(self, controller): self._window(875.0, 581.0, btn, width=344, height=71) def _back_to_main(self): - from .main_page import MainPage - self.controller.ctx.traffic_light.set_off() - self.controller.show_frame(MainPage) + self.controller.back_to_main() diff --git a/src/screens/check_in_no_id.py b/src/screens/check_in_no_id.py index 5d53898..0e95262 100644 --- a/src/screens/check_in_no_id.py +++ b/src/screens/check_in_no_id.py @@ -62,12 +62,6 @@ def updateEntries(self, pid): self.pid_entry.insert(0, pid) def _call_check_in(self, controller): - from .no_acc_check_in_only import NoAccCheckInOnly - from .no_acc_no_waiver_swipe import NoAccNoWaiverSwipe - from .user_welcome import UserWelcome - from .acc_no_waiver import AccNoWaiver - from .main_page import MainPage - pid = self.pid_entry.get() if not pid: return @@ -76,25 +70,8 @@ def _call_check_in(self, controller): self.canvas.update_idletasks() self.clearEntries() - result = self.controller.ctx.sheets.checkin_by_pid(pid) - status = result.get("status") + self.controller.ctx.check_in.handle_by_pid(pid) if self.loading_text_id is not None: self.canvas.delete(self.loading_text_id) self.loading_text_id = None - - if status == "no_account": - logging.info("Manual check-in: user account not found") - controller.show_frame(NoAccCheckInOnly) - controller.after(5000, lambda: controller.show_frame(MainPage)) - return - - if status == "no_waiver": - logging.info(f"Manual check-in: no waiver for {result.get('name', pid)}") - controller.show_frame(AccNoWaiver) - controller.after(3000, lambda: controller.show_frame(NoAccNoWaiverSwipe)) - return - - logging.info(f"Manual check-in for {result['name']}") - self.controller.ctx.traffic_light.set_green() - self.controller.get_frame(UserWelcome).displayName(result["name"]) diff --git a/src/screens/main_page.py b/src/screens/main_page.py index b14d1a9..ecedd04 100644 --- a/src/screens/main_page.py +++ b/src/screens/main_page.py @@ -46,7 +46,4 @@ def _build(self, controller): self._window(1130.0, 40.0, btn2) def _go_to_no_id(self, controller): - from .check_in_no_id import CheckInNoId - no_id = controller.get_frame(CheckInNoId) - no_id.clearEntries() - controller.show_frame(CheckInNoId) + controller.go_to_no_id() diff --git a/src/screens/manual_fill.py b/src/screens/manual_fill.py index 2c2fa17..51d6442 100644 --- a/src/screens/manual_fill.py +++ b/src/screens/manual_fill.py @@ -1,7 +1,6 @@ from pathlib import Path from tkinter import Button, Entry, StringVar, END from .screen import Screen -from utils import Utils import logging import timeit @@ -109,12 +108,11 @@ def updateEntries(self, fname, lname, email, pid): self.pid_entry.insert(0, pid) def _call_account_creation(self): - util = Utils() data = self.getEntries() self.clearEntries() try: delay = timeit.timeit( - lambda: util.createAccount(self.controller.ctx, data[0], data[1], data[2], data[3], ManualFill), + lambda: self.controller.ctx.account.create_account(data[0], data[1], data[2], data[3]), number=1, ) logging.debug(f"Time to create account: {delay}") diff --git a/src/screens/no_acc_no_waiver_swipe.py b/src/screens/no_acc_no_waiver_swipe.py index 8ddeb3d..0deadc8 100644 --- a/src/screens/no_acc_no_waiver_swipe.py +++ b/src/screens/no_acc_no_waiver_swipe.py @@ -41,6 +41,4 @@ def _build(self, controller): self._window(465.0, 554.0, btn, width=349, height=71) def _go_to_manual_fill(self, controller): - from .manual_fill import ManualFill - controller.get_frame(ManualFill).clearEntries() - controller.show_frame(ManualFill) + controller.go_to_manual_fill() diff --git a/src/screens/user_thank.py b/src/screens/user_thank.py index 65b1c20..2946c87 100644 --- a/src/screens/user_thank.py +++ b/src/screens/user_thank.py @@ -19,13 +19,8 @@ def hide(self): self.canvas.delete("thank") def displayName(self, name, nextPage): - from .main_page import MainPage self.controller.show_frame(UserThank) - - if nextPage == MainPage: - self.controller.ctx.traffic_light.set_green() - else: - self.controller.ctx.traffic_light.set_yellow() + self.controller.ctx.account.on_thank_start(nextPage) self.canvas.create_text( 99.0, 323.0, anchor="nw", @@ -39,7 +34,4 @@ def displayName(self, name, nextPage): self.controller.after(4000, lambda: self._go_to_next(nextPage)) def _go_to_next(self, nextPage): - from .main_page import MainPage - self.controller.show_frame(nextPage) - if nextPage == MainPage: - self.controller.ctx.traffic_light.set_off() + self.controller.ctx.account.on_thank_done(nextPage) diff --git a/src/screens/user_welcome.py b/src/screens/user_welcome.py index 82d9da7..689f91e 100644 --- a/src/screens/user_welcome.py +++ b/src/screens/user_welcome.py @@ -25,7 +25,6 @@ def displayName(self, name): self.last_name = name - from .main_page import MainPage self.controller.show_frame(UserWelcome) text_id = self.canvas.create_text( @@ -42,7 +41,6 @@ def displayName(self, name): self.canvas.after(3000, lambda: self._remove_name(text_id)) def _remove_name(self, text_id): - from .main_page import MainPage self.canvas.delete(text_id) self.offset -= 73 @@ -53,5 +51,4 @@ def _remove_name(self, text_id): if not self.canvas.find_withtag("welcome"): self.last_name = None - self.controller.ctx.traffic_light.set_off() - self.controller.show_frame(MainPage) + self.controller.back_to_main() diff --git a/src/screens/waiver_no_acc_swipe.py b/src/screens/waiver_no_acc_swipe.py index 5f8e0c2..b449a28 100644 --- a/src/screens/waiver_no_acc_swipe.py +++ b/src/screens/waiver_no_acc_swipe.py @@ -41,6 +41,4 @@ def _build(self, controller): self._window(465.0, 554.0, btn, width=349, height=71) def _go_to_manual_fill(self, controller): - from .manual_fill import ManualFill - controller.get_frame(ManualFill).clearEntries() - controller.show_frame(ManualFill) + controller.go_to_manual_fill() From 8971b7d93ee0e65fedf6760c949e1359035a0697 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 17:15:27 -0700 Subject: [PATCH 08/28] traffic light api refactoring, remove dynamic imports --- src/api/traffic_light_api.py | 33 ++++++++++++++++++++++ src/app_context.py | 36 +++--------------------- src/controllers/account_controller.py | 2 -- src/controllers/check_in_controller.py | 21 ++++++-------- src/controllers/navigation_controller.py | 31 +++++++++----------- src/screens/check_in_no_id.py | 5 ---- src/screens/main_page.py | 4 +-- src/screens/qr_codes.py | 4 +-- 8 files changed, 62 insertions(+), 74 deletions(-) create mode 100644 src/api/traffic_light_api.py diff --git a/src/api/traffic_light_api.py b/src/api/traffic_light_api.py new file mode 100644 index 0000000..e2896f1 --- /dev/null +++ b/src/api/traffic_light_api.py @@ -0,0 +1,33 @@ +import threading + +from api.sheets import SheetManager +from hardware.traffic import TrafficLight + + +class TrafficLightApi: + def __init__(self, light: TrafficLight, sheets: SheetManager): + self._light = light + self._sheets = sheets + + @property + def connected(self) -> bool: + return self._light.ser is not None + + def _post(self, color: str) -> None: + threading.Thread( + target=self._sheets.set_traffic_light, + args=(color,), + daemon=True, + ).start() + + def set_red(self) -> None: + self._post("red") + + def set_green(self) -> None: + self._post("green") + + def set_yellow(self) -> None: + self._post("yellow") + + def set_off(self) -> None: + self._post("off") diff --git a/src/app_context.py b/src/app_context.py index 19782c1..c8e6cfd 100644 --- a/src/app_context.py +++ b/src/app_context.py @@ -1,40 +1,12 @@ import threading from api.sheets import SheetManager +from api.traffic_light_api import TrafficLightApi from hardware.traffic import TrafficLight -class _TrafficProxy: - def __init__(self, light: TrafficLight, sheets: SheetManager): - self._light = light - self._sheets = sheets - - @property - def connected(self) -> bool: - return self._light.ser is not None - - def _post(self, color: str) -> None: - threading.Thread( - target=self._sheets.set_traffic_light, - args=(color,), - daemon=True, - ).start() - - def set_red(self) -> None: - self._post("red") - - def set_green(self) -> None: - self._post("green") - - def set_yellow(self) -> None: - self._post("yellow") - - def set_off(self) -> None: - self._post("off") - - class AppContext: - def __init__(self, sheets: SheetManager, traffic_light: _TrafficProxy): + def __init__(self, sheets: SheetManager, traffic_light: TrafficLightApi): self.sheets = sheets self.traffic_light = traffic_light self.window = None @@ -58,5 +30,5 @@ def rfid(self, value: str) -> None: def create(cls, traffic_usb_id=None) -> "AppContext": sheets = SheetManager() light = TrafficLight(traffic_usb_id) - proxy = _TrafficProxy(light, sheets) - return cls(sheets, proxy) + traffic = TrafficLightApi(light, sheets) + return cls(sheets, traffic) diff --git a/src/controllers/account_controller.py b/src/controllers/account_controller.py index 7a09d59..3f10be7 100644 --- a/src/controllers/account_controller.py +++ b/src/controllers/account_controller.py @@ -104,14 +104,12 @@ def create_account(self, fname, lname, email, pid): inProgress.destroy() def on_thank_start(self, next_page): - from screens.main_page import MainPage if next_page == MainPage: self.ctx.traffic_light.set_green() else: self.ctx.traffic_light.set_yellow() def on_thank_done(self, next_page): - from screens.main_page import MainPage self.ctx.nav.show_frame(next_page) if next_page == MainPage: self.ctx.traffic_light.set_off() diff --git a/src/controllers/check_in_controller.py b/src/controllers/check_in_controller.py index dd58140..1a9856f 100644 --- a/src/controllers/check_in_controller.py +++ b/src/controllers/check_in_controller.py @@ -1,6 +1,14 @@ import logging from tkinter import Label +from screens.main_page import MainPage +from screens.no_acc_no_waiver import NoAccNoWaiver +from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe +from screens.no_acc_check_in_only import NoAccCheckInOnly +from screens.acc_no_waiver import AccNoWaiver +from screens.acc_no_waiver_swipe import AccNoWaiverSwipe +from screens.user_welcome import UserWelcome + class CheckInController: def __init__(self, ctx): @@ -11,13 +19,6 @@ def handle_by_uuid(self, tag): status = result.get("status") def update_ui(): - from screens.main_page import MainPage - from screens.no_acc_no_waiver import NoAccNoWaiver - from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe - from screens.acc_no_waiver import AccNoWaiver - from screens.acc_no_waiver_swipe import AccNoWaiverSwipe - from screens.user_welcome import UserWelcome - if status == "api_error": logging.error("API error during check-in") self.ctx.traffic_light.set_red() @@ -51,12 +52,6 @@ def update_ui(): self.ctx.window.after(0, update_ui) def handle_by_pid(self, pid): - from screens.no_acc_check_in_only import NoAccCheckInOnly - from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe - from screens.user_welcome import UserWelcome - from screens.acc_no_waiver import AccNoWaiver - from screens.main_page import MainPage - result = self.ctx.sheets.checkin_by_pid(pid) status = result.get("status") diff --git a/src/controllers/navigation_controller.py b/src/controllers/navigation_controller.py index bbd6ec3..ea4dc7e 100644 --- a/src/controllers/navigation_controller.py +++ b/src/controllers/navigation_controller.py @@ -1,5 +1,19 @@ import uuid +from screens.main_page import MainPage +from screens.acc_no_waiver import AccNoWaiver +from screens.acc_no_waiver_swipe import AccNoWaiverSwipe +from screens.manual_fill import ManualFill +from screens.check_in_no_id import CheckInNoId +from screens.no_acc_check_in_only import NoAccCheckInOnly +from screens.no_acc_no_waiver import NoAccNoWaiver +from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe +from screens.qr_codes import QRCodes +from screens.user_thank import UserThank +from screens.user_welcome import UserWelcome +from screens.waiver_no_acc import WaiverNoAcc +from screens.waiver_no_acc_swipe import WaiverNoAccSwipe + class NavigationController: def __init__(self, window, ctx): @@ -9,20 +23,6 @@ def __init__(self, window, ctx): self._curr = None self._frame_uuid = uuid.uuid4().hex - from screens.main_page import MainPage - from screens.acc_no_waiver import AccNoWaiver - from screens.acc_no_waiver_swipe import AccNoWaiverSwipe - from screens.manual_fill import ManualFill - from screens.check_in_no_id import CheckInNoId - from screens.no_acc_check_in_only import NoAccCheckInOnly - from screens.no_acc_no_waiver import NoAccNoWaiver - from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe - from screens.qr_codes import QRCodes - from screens.user_thank import UserThank - from screens.user_welcome import UserWelcome - from screens.waiver_no_acc import WaiverNoAcc - from screens.waiver_no_acc_swipe import WaiverNoAccSwipe - self._timeouts = { AccNoWaiverSwipe: 30000, QRCodes: 30000, @@ -72,17 +72,14 @@ def after(self, ms, fn): self._window.after(ms, fn) def back_to_main(self): - from screens.main_page import MainPage self.ctx.traffic_light.set_off() self.show_frame(MainPage) def go_to_no_id(self): - from screens.check_in_no_id import CheckInNoId self.get_frame(CheckInNoId).clearEntries() self.show_frame(CheckInNoId) def go_to_manual_fill(self): - from screens.manual_fill import ManualFill self.get_frame(ManualFill).clearEntries() self.show_frame(ManualFill) diff --git a/src/screens/check_in_no_id.py b/src/screens/check_in_no_id.py index 0e95262..4a62fdb 100644 --- a/src/screens/check_in_no_id.py +++ b/src/screens/check_in_no_id.py @@ -12,11 +12,6 @@ class CheckInNoId(Screen): def _build(self, controller): - from .no_acc_check_in_only import NoAccCheckInOnly - from .no_acc_no_waiver_swipe import NoAccNoWaiverSwipe - from .user_welcome import UserWelcome - from .acc_no_waiver import AccNoWaiver - self.loading_text_id = None self.pid = StringVar() diff --git a/src/screens/main_page.py b/src/screens/main_page.py index ecedd04..85c79d2 100644 --- a/src/screens/main_page.py +++ b/src/screens/main_page.py @@ -1,13 +1,13 @@ from pathlib import Path from tkinter import Button from .screen import Screen +from .qr_codes import QRCodes + ASSETS_PATH = Path(__file__).parent.parent / "assets" / "main_page_assets" class MainPage(Screen): def _build(self, controller): - from .qr_codes import QRCodes - from .check_in_no_id import CheckInNoId logo = self._photo(ASSETS_PATH / "image_3.png") self._image(88.0, 90.0, image=logo) diff --git a/src/screens/qr_codes.py b/src/screens/qr_codes.py index e9cd16c..a25d1e4 100644 --- a/src/screens/qr_codes.py +++ b/src/screens/qr_codes.py @@ -7,8 +7,6 @@ class QRCodes(Screen): def _build(self, controller): - from .main_page import MainPage - img3 = self._photo(ASSETS_PATH / "image_3.png") self._image(88.0, 90.0, image=img3) @@ -30,7 +28,7 @@ def _build(self, controller): btn_img = self._photo(ASSETS_PATH / "image_6.png") btn = Button( self.canvas, image=btn_img, bg="#153246", - command=lambda: controller.show_frame(MainPage), + command=lambda: controller.back_to_main(), relief="flat", ) self._window(53.0, 55.0, btn) From 430f8b06b1bf55a35246b8750c08b441bf91f5ce Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 17:17:34 -0700 Subject: [PATCH 09/28] refactor method names --- src/controllers/card_reader_controller.py | 8 ++++---- src/hardware/reader.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/card_reader_controller.py b/src/controllers/card_reader_controller.py index 67e03ea..5908334 100644 --- a/src/controllers/card_reader_controller.py +++ b/src/controllers/card_reader_controller.py @@ -32,7 +32,7 @@ def _run(self, reader): continue try: - in_waiting = reader.getSerInWaiting() + in_waiting = reader.get_ser_in_waiting() except OSError as e: if not scanner_error: logging.error("Card reader disconnected, disabling until reconnection: %s", e) @@ -54,16 +54,16 @@ def _run(self, reader): continue self.ctx.nav.get_frame(ManualFill).clearEntries() - tag = reader.grabRFID() + tag = reader.grab_rfid() if " " in tag: continue - if tag == last_tag and not reader.canScanAgain(last_time): + if tag == last_tag and not reader.can_scan_again(last_time): logging.debug("Suppressing repeat scan") continue - s_reason = reader.checkRFID(tag) + s_reason = reader.check_rfid(tag) if s_reason != "good": logging.debug(s_reason) diff --git a/src/hardware/reader.py b/src/hardware/reader.py index 7b83ea5..06bfa6b 100644 --- a/src/hardware/reader.py +++ b/src/hardware/reader.py @@ -40,7 +40,7 @@ def reconnect(self): self._pn532 = None return False - def getSerInWaiting(self): + def get_ser_in_waiting(self): try: uid = self._pn532.read_passive_target(timeout=0.1) except Exception as e: @@ -53,16 +53,16 @@ def getSerInWaiting(self): self._pending_tag = None return 0 - def grabRFID(self): + def grab_rfid(self): tag = self._pending_tag self._pending_tag = None logging.info("Parsed tag: " + str(tag)) return str(tag) - def checkRFID(self, tag): + def check_rfid(self, tag): if not tag or len(tag) != expected_characters: return "Tag was not the expected number of chars" return "good" - def canScanAgain(self, lastTime): + def can_scan_again(self, lastTime): return time.time() - lastTime > 3 From 738e436d4f098854f299f737ab85b74c0846edc8 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 17:46:57 -0700 Subject: [PATCH 10/28] refactoring --- src/api/_client.py | 12 +++++++ src/api/get_info_from_pid.py | 25 +++++++------- src/api/sheets.py | 12 ++----- src/api/traffic_light_api.py | 21 +++++++++--- src/controllers/account_controller.py | 8 ++--- src/controllers/card_reader_controller.py | 12 ++----- src/controllers/check_in_controller.py | 14 ++++---- src/controllers/navigation_controller.py | 6 ++-- src/controllers/swipe_controller.py | 34 +++++++++--------- src/hardware/render_ports.py | 20 +++++++---- src/hardware/traffic.py | 4 +++ src/main.py | 18 +++++----- src/screens/check_in_no_id.py | 10 +++--- src/screens/manual_fill.py | 42 +++++++++-------------- src/screens/user_thank.py | 2 +- src/screens/user_welcome.py | 2 +- 16 files changed, 126 insertions(+), 116 deletions(-) create mode 100644 src/api/_client.py diff --git a/src/api/_client.py b/src/api/_client.py new file mode 100644 index 0000000..a1f848e --- /dev/null +++ b/src/api/_client.py @@ -0,0 +1,12 @@ +import logging +import time + +import requests + + +def _req(method, url, **kwargs): + start = time.time() + resp = requests.request(method, url, **kwargs) + ms = (time.time() - start) * 1000 + logging.info(f"[CLIENT] {method.upper()} {url} -> {resp.status_code} ({ms:.0f}ms)") + return resp diff --git a/src/api/get_info_from_pid.py b/src/api/get_info_from_pid.py index 76b4924..af28c96 100644 --- a/src/api/get_info_from_pid.py +++ b/src/api/get_info_from_pid.py @@ -1,27 +1,28 @@ import logging -import time - -import requests +from dataclasses import dataclass from config import API_BASE_URL +from api._client import _req -def _req(method, url, **kwargs): - start = time.time() - resp = requests.request(method, url, **kwargs) - ms = (time.time() - start) * 1000 - logging.info(f"[CLIENT] {method.upper()} {url} -> {resp.status_code} ({ms:.0f}ms)") - return resp +@dataclass +class StudentInfo: + first_name: str + last_name: str + emails: list + pid: str + first_enr_term: str + last_enr_term: str -class contact_client: +class ContactClient: def get_student_info(self, barcode): try: resp = _req("GET", f"{API_BASE_URL}/students/barcode/{barcode}", timeout=5) if not resp.ok: return False d = resp.json() - return [d["first_name"], d["last_name"], d["emails"], d["pid"], d["first_enr_term"], d["last_enr_term"]] + return StudentInfo(d["first_name"], d["last_name"], d["emails"], d["pid"], d["first_enr_term"], d["last_enr_term"]) except Exception as e: logging.error(f"Error fetching student by barcode: {e}") return False @@ -32,7 +33,7 @@ def get_student_info_pid(self, pid): if not resp.ok: return False d = resp.json() - return [d["first_name"], d["last_name"], d["emails"], d["pid"], d["first_enr_term"], d["last_enr_term"]] + return StudentInfo(d["first_name"], d["last_name"], d["emails"], d["pid"], d["first_enr_term"], d["last_enr_term"]) except Exception as e: logging.error(f"Error fetching student by pid: {e}") return False diff --git a/src/api/sheets.py b/src/api/sheets.py index 307f2af..7bc3b67 100644 --- a/src/api/sheets.py +++ b/src/api/sheets.py @@ -2,18 +2,10 @@ import sys import time -import requests - from config import API_BASE_URL +from api._client import _req -def _req(method, url, **kwargs): - start = time.time() - resp = requests.request(method, url, **kwargs) - ms = (time.time() - start) * 1000 - logging.info(f"[CLIENT] {method.upper()} {url} -> {resp.status_code} ({ms:.0f}ms)") - return resp - def check_api_health(retries=3, delay=3): logging.info(API_BASE_URL) @@ -58,7 +50,7 @@ def set_traffic_light(self, color): def get_traffic_light(self): try: - resp = requests.get(f"{API_BASE_URL}/traffic-light", timeout=5) + resp = _req("GET", f"{API_BASE_URL}/traffic-light", timeout=5) return resp.json().get("color", "off") except Exception as e: logging.error(f"Error getting traffic light: {e}") diff --git a/src/api/traffic_light_api.py b/src/api/traffic_light_api.py index e2896f1..da94ce9 100644 --- a/src/api/traffic_light_api.py +++ b/src/api/traffic_light_api.py @@ -11,7 +11,18 @@ def __init__(self, light: TrafficLight, sheets: SheetManager): @property def connected(self) -> bool: - return self._light.ser is not None + return self._light.connected + + def drive(self, color: str) -> None: + """Directly set the physical traffic light without posting to the API.""" + if color == "red": + self._light.set_red() + elif color == "green": + self._light.set_green() + elif color == "yellow": + self._light.set_yellow() + else: + self._light.set_off() def _post(self, color: str) -> None: threading.Thread( @@ -20,14 +31,14 @@ def _post(self, color: str) -> None: daemon=True, ).start() - def set_red(self) -> None: + def request_red(self) -> None: self._post("red") - def set_green(self) -> None: + def request_green(self) -> None: self._post("green") - def set_yellow(self) -> None: + def request_yellow(self) -> None: self._post("yellow") - def set_off(self) -> None: + def request_off(self) -> None: self._post("off") diff --git a/src/controllers/account_controller.py b/src/controllers/account_controller.py index 3f10be7..ed851c3 100644 --- a/src/controllers/account_controller.py +++ b/src/controllers/account_controller.py @@ -100,16 +100,16 @@ def create_account(self, fname, lname, email, pid): end5 = time.perf_counter() logging.debug(f"Time to check waiver via check-in: {end5 - end4}") - self.ctx.nav.get_frame(UserThank).displayName(full_name, toGoTo) + self.ctx.nav.get_frame(UserThank).display_name(full_name, toGoTo) inProgress.destroy() def on_thank_start(self, next_page): if next_page == MainPage: - self.ctx.traffic_light.set_green() + self.ctx.traffic_light.request_green() else: - self.ctx.traffic_light.set_yellow() + self.ctx.traffic_light.request_yellow() def on_thank_done(self, next_page): self.ctx.nav.show_frame(next_page) if next_page == MainPage: - self.ctx.traffic_light.set_off() + self.ctx.traffic_light.request_off() diff --git a/src/controllers/card_reader_controller.py b/src/controllers/card_reader_controller.py index 5908334..4a1472f 100644 --- a/src/controllers/card_reader_controller.py +++ b/src/controllers/card_reader_controller.py @@ -53,7 +53,7 @@ def _run(self, reader): no_wifi.after(4000, lambda: self._destroy_wifi_error(no_wifi)) continue - self.ctx.nav.get_frame(ManualFill).clearEntries() + self.ctx.nav.get_frame(ManualFill).clear_entries() tag = reader.grab_rfid() if " " in tag: @@ -79,20 +79,12 @@ def _run(self, reader): def _poll_traffic_light(self): last_color = None - light = self.ctx.traffic_light._light while True: time.sleep(0.1) color = self.ctx.sheets.get_traffic_light() if color != last_color: last_color = color - if color == "red": - light.set_red() - elif color == "green": - light.set_green() - elif color == "yellow": - light.set_yellow() - else: - light.set_off() + self.ctx.traffic_light.drive(color) def _is_connected(self, host="8.8.8.8", port=53, timeout=3): try: diff --git a/src/controllers/check_in_controller.py b/src/controllers/check_in_controller.py index 1a9856f..c4cd557 100644 --- a/src/controllers/check_in_controller.py +++ b/src/controllers/check_in_controller.py @@ -21,7 +21,7 @@ def handle_by_uuid(self, tag): def update_ui(): if status == "api_error": logging.error("API error during check-in") - self.ctx.traffic_light.set_red() + self.ctx.traffic_light.request_red() error_label = Label( self.ctx.window.canvas, text="System error, please let staff know.", @@ -33,21 +33,21 @@ def update_ui(): if status == "no_account": logging.info(f"User {tag} not found.") - self.ctx.traffic_light.set_red() + self.ctx.traffic_light.request_red() self.ctx.nav.show_frame(NoAccNoWaiver) self.ctx.window.after(3000, lambda: self.ctx.nav.show_frame(NoAccNoWaiverSwipe)) return if status == "no_waiver": logging.info(f"User {tag} does not have waiver.") - self.ctx.traffic_light.set_yellow() + self.ctx.traffic_light.request_yellow() self.ctx.nav.show_frame(AccNoWaiver) self.ctx.window.after(3000, lambda: self.ctx.nav.show_frame(AccNoWaiverSwipe)) return logging.info(f"User found: {result['name']}") - self.ctx.traffic_light.set_green() - self.ctx.nav.get_frame(UserWelcome).displayName(result["name"]) + self.ctx.traffic_light.request_green() + self.ctx.nav.get_frame(UserWelcome).display_name(result["name"]) self.ctx.window.after(0, update_ui) @@ -68,5 +68,5 @@ def handle_by_pid(self, pid): return logging.info(f"Manual check-in for {result['name']}") - self.ctx.traffic_light.set_green() - self.ctx.nav.get_frame(UserWelcome).displayName(result["name"]) + self.ctx.traffic_light.request_green() + self.ctx.nav.get_frame(UserWelcome).display_name(result["name"]) diff --git a/src/controllers/navigation_controller.py b/src/controllers/navigation_controller.py index ea4dc7e..8389678 100644 --- a/src/controllers/navigation_controller.py +++ b/src/controllers/navigation_controller.py @@ -72,15 +72,15 @@ def after(self, ms, fn): self._window.after(ms, fn) def back_to_main(self): - self.ctx.traffic_light.set_off() + self.ctx.traffic_light.request_off() self.show_frame(MainPage) def go_to_no_id(self): - self.get_frame(CheckInNoId).clearEntries() + self.get_frame(CheckInNoId).clear_entries() self.show_frame(CheckInNoId) def go_to_manual_fill(self): - self.get_frame(ManualFill).clearEntries() + self.get_frame(ManualFill).clear_entries() self.show_frame(ManualFill) def _on_timeout(self, uid): diff --git a/src/controllers/swipe_controller.py b/src/controllers/swipe_controller.py index 308d043..c4b00eb 100644 --- a/src/controllers/swipe_controller.py +++ b/src/controllers/swipe_controller.py @@ -4,7 +4,7 @@ from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe from screens.waiver_no_acc_swipe import WaiverNoAccSwipe from screens.check_in_no_id import CheckInNoId -from api.get_info_from_pid import contact_client +from api.get_info_from_pid import ContactClient class SwipeController: @@ -20,7 +20,7 @@ def _id_vet(self, id_check): return "bad" return "good" - def keyboardPress(self, key): + def keyboard_press(self, key): curr_frame = self.ctx.nav.get_curr_frame() if curr_frame not in (NoAccNoWaiverSwipe, WaiverNoAccSwipe, CheckInNoId): @@ -39,16 +39,16 @@ def keyboardPress(self, key): bg="#153246", fg="white", font=("Arial", 20), ) id_error.place(relx=0.5, rely=0.85, anchor="center") - id_error.after(1500, lambda: self._destroySwipeError(id_error)) + id_error.after(1500, lambda: self._destroy_swipe_error(id_error)) return - self._swipeCard(self._id_string) + self._swipe_card(self._id_string) self._id_string = "" - def _pullUser(self, barcode, u_type): + def _pull_user(self, barcode, u_type): logging.info(f"Card barcode read is: {barcode}. Trying to pull user...") - contact = contact_client() + contact = ContactClient() try: if u_type == "Staff": u_info = contact.get_staff_info(barcode) @@ -62,32 +62,32 @@ def _pullUser(self, barcode, u_type): logging.info("Student search returned False, returning...") return None - logging.info(f"Info pull succeeded:\n {u_info[0]}, {u_info[1]}, {u_info[3]}") + logging.info(f"Info pull succeeded:\n {u_info.first_name}, {u_info.last_name}, {u_info.pid}") return u_info - def _swipeCard(self, id_string): - u_data = self._pullUser(id_string.strip(), "Student") + def _swipe_card(self, id_string): + u_data = self._pull_user(id_string.strip(), "Student") if not u_data: logging.info("Student search returned False, returning...") return if self.ctx.nav.get_curr_frame() == CheckInNoId: - self.ctx.nav.get_frame(CheckInNoId).clearEntries() - self.ctx.nav.get_frame(CheckInNoId).updateEntries(u_data[3]) + self.ctx.nav.get_frame(CheckInNoId).clear_entries() + self.ctx.nav.get_frame(CheckInNoId).update_entries(u_data.pid) return - email_to_use = "" if len(u_data[2]) == 0 else u_data[2][0] - for email in u_data[2]: + email_to_use = "" if len(u_data.emails) == 0 else u_data.emails[0] + for email in u_data.emails: if email.endswith("@ucsd.edu"): email_to_use = email manfill = self.ctx.nav.get_frame(ManualFill) - manfill.clearEntries() - logging.info(f"Filling data with {u_data[0]} {u_data[1]} {email_to_use} {u_data[3]}") - manfill.updateEntries(u_data[0], u_data[1], email_to_use, u_data[3]) + manfill.clear_entries() + logging.info(f"Filling data with {u_data.first_name} {u_data.last_name} {email_to_use} {u_data.pid}") + manfill.update_entries(u_data.first_name, u_data.last_name, email_to_use, u_data.pid) self.ctx.nav.show_frame(ManualFill) - def _destroySwipeError(self, id_error): + def _destroy_swipe_error(self, id_error): id_error.destroy() self._swipe_error_shown = False diff --git a/src/hardware/render_ports.py b/src/hardware/render_ports.py index 6609188..2ad7cca 100644 --- a/src/hardware/render_ports.py +++ b/src/hardware/render_ports.py @@ -1,16 +1,24 @@ +from dataclasses import dataclass + import serial.tools.list_ports SHARED_VID = 0x1A86 TRAFFIC_LOCATION = "1-1.1.2" -def get_usb_ids(): - reader_usb_id = None - traffic_usb_id = None +@dataclass +class UsbIds: + reader: str | None + traffic_light: str | None + + +def get_usb_ids() -> UsbIds: + reader = None + traffic_light = None for p in serial.tools.list_ports.comports(): if p.vid == SHARED_VID: if p.location == TRAFFIC_LOCATION: - traffic_usb_id = p.device + traffic_light = p.device else: - reader_usb_id = p.device - return reader_usb_id, traffic_usb_id + reader = p.device + return UsbIds(reader, traffic_light) diff --git a/src/hardware/traffic.py b/src/hardware/traffic.py index ace092a..6f3ce61 100644 --- a/src/hardware/traffic.py +++ b/src/hardware/traffic.py @@ -11,6 +11,10 @@ def __init__(self, addr=None, baud=115200): self.ser = serial.Serial(addr, baud) self.ser.reset_input_buffer() + @property + def connected(self) -> bool: + return self.ser is not None + def set_off(self): if self.ser: self.ser.write(b"off\n") diff --git a/src/main.py b/src/main.py index 56ada31..d7bb907 100644 --- a/src/main.py +++ b/src/main.py @@ -15,10 +15,10 @@ from sys import stdout -def clearAndReturn(ctx: AppContext): +def clear_and_return(ctx: AppContext): ctx.nav.back_to_main() - ctx.nav.get_frame(ManualFill).clearEntries() - ctx.nav.get_frame(CheckInNoId).clearEntries() + ctx.nav.get_frame(ManualFill).clear_entries() + ctx.nav.get_frame(CheckInNoId).clear_entries() if __name__ == "__main__": @@ -34,23 +34,23 @@ def clearAndReturn(ctx: AppContext): else: logging.basicConfig(level=logging.INFO) - reader_usb_id, traffic_usb_id = get_usb_ids() + usb = get_usb_ids() check_api_health() - ctx = AppContext.create(traffic_usb_id) + ctx = AppContext.create(usb.traffic_light) window = CheckInWindow() nav = NavigationController(window, ctx) ctx.window = window ctx.nav = nav ctx.check_in = CheckInController(ctx) ctx.account = AccountController(ctx) - ctx.traffic_light.set_off() + ctx.traffic_light.request_off() sw = SwipeController(ctx) - reader = Reader(reader_usb_id) + reader = Reader(usb.reader) card_reader = CardReaderController(ctx) card_reader.start(reader) - window.bind("", lambda i: sw.keyboardPress(i)) - window.bind("", lambda i: clearAndReturn(ctx)) + window.bind("", lambda i: sw.keyboard_press(i)) + window.bind("", lambda i: clear_and_return(ctx)) logging.info("Made it to app start") window.start() diff --git a/src/screens/check_in_no_id.py b/src/screens/check_in_no_id.py index 4a62fdb..135186d 100644 --- a/src/screens/check_in_no_id.py +++ b/src/screens/check_in_no_id.py @@ -42,7 +42,7 @@ def _build(self, controller): self.pid_entry = Entry(self.canvas, textvariable=self.pid, width=40, font=52) self._window(420.0, 412.0, self.pid_entry) - def displayLoading(self): + def display_loading(self): if self.loading_text_id is None: self.loading_text_id = self.canvas.create_text( 420.0, 545.0, anchor="nw", @@ -50,10 +50,10 @@ def displayLoading(self): fill="#FF0000", font=("Montserrat", 36 * -1, "bold"), justify="center", ) - def clearEntries(self): + def clear_entries(self): self.pid_entry.delete(0, END) - def updateEntries(self, pid): + def update_entries(self, pid): self.pid_entry.insert(0, pid) def _call_check_in(self, controller): @@ -61,9 +61,9 @@ def _call_check_in(self, controller): if not pid: return - self.displayLoading() + self.display_loading() self.canvas.update_idletasks() - self.clearEntries() + self.clear_entries() self.controller.ctx.check_in.handle_by_pid(pid) diff --git a/src/screens/manual_fill.py b/src/screens/manual_fill.py index 51d6442..a1145c9 100644 --- a/src/screens/manual_fill.py +++ b/src/screens/manual_fill.py @@ -2,7 +2,6 @@ from tkinter import Button, Entry, StringVar, END from .screen import Screen import logging -import timeit ASSETS_PATH = Path(__file__).parent.parent / "assets" / "manual_fill_assets" @@ -13,10 +12,10 @@ class ManualFill(Screen): def _build(self, controller): - self.first_name = StringVar() - self.last_name = StringVar() - self.email = StringVar() - self.pid = StringVar() + self.first_name_var = StringVar() + self.last_name_var = StringVar() + self.email_var = StringVar() + self.pid_var = StringVar() img2 = self._photo(ASSETS_PATH / "image_2.png") self._image(640.0, 76.0, image=img2) @@ -75,46 +74,37 @@ def _build(self, controller): ) self._window(465.0, 598.0, btn, width=349, height=71) - self.first_name_entry = Entry(self.canvas, textvariable=self.first_name, width=40, font=52) + self.first_name_entry = Entry(self.canvas, textvariable=self.first_name_var, width=40, font=52) self._window(420.0, 227.0, self.first_name_entry) - self.last_name_entry = Entry(self.canvas, textvariable=self.last_name, width=40, font=52) + self.last_name_entry = Entry(self.canvas, textvariable=self.last_name_var, width=40, font=52) self._window(420.0, 327.0, self.last_name_entry) - self.email_entry = Entry(self.canvas, textvariable=self.email, width=40, font=52) + self.email_entry = Entry(self.canvas, textvariable=self.email_var, width=40, font=52) self._window(420.0, 428.0, self.email_entry) - self.pid_entry = Entry(self.canvas, textvariable=self.pid, width=40, font=52) + self.pid_entry = Entry(self.canvas, textvariable=self.pid_var, width=40, font=52) self._window(420.0, 530.0, self.pid_entry) - def getEntries(self): - return [ - self.first_name.get(), - self.last_name.get(), - self.email.get(), - self.pid.get(), - ] - - def clearEntries(self): + def clear_entries(self): self.first_name_entry.delete(0, END) self.last_name_entry.delete(0, END) self.email_entry.delete(0, END) self.pid_entry.delete(0, END) - def updateEntries(self, fname, lname, email, pid): + def update_entries(self, fname, lname, email, pid): self.first_name_entry.insert(0, fname) self.last_name_entry.insert(0, lname) self.email_entry.insert(0, email) self.pid_entry.insert(0, pid) def _call_account_creation(self): - data = self.getEntries() - self.clearEntries() + first_name, last_name, email, pid = ( + self.first_name_var.get(), self.last_name_var.get(), + self.email_var.get(), self.pid_var.get(), + ) + self.clear_entries() try: - delay = timeit.timeit( - lambda: self.controller.ctx.account.create_account(data[0], data[1], data[2], data[3]), - number=1, - ) - logging.debug(f"Time to create account: {delay}") + self.controller.ctx.account.create_account(first_name, last_name, email, pid) except Exception: logging.warning("Error occurred trying to create a user account", exc_info=True) diff --git a/src/screens/user_thank.py b/src/screens/user_thank.py index 2946c87..00828ce 100644 --- a/src/screens/user_thank.py +++ b/src/screens/user_thank.py @@ -18,7 +18,7 @@ def hide(self): super().hide() self.canvas.delete("thank") - def displayName(self, name, nextPage): + def display_name(self, name, nextPage): self.controller.show_frame(UserThank) self.controller.ctx.account.on_thank_start(nextPage) diff --git a/src/screens/user_welcome.py b/src/screens/user_welcome.py index 689f91e..d8d31c2 100644 --- a/src/screens/user_welcome.py +++ b/src/screens/user_welcome.py @@ -19,7 +19,7 @@ def hide(self): self.last_name = None self.offset = 0 - def displayName(self, name): + def display_name(self, name): if name == self.last_name: return From c83a8f7dbd5f60080e36564304cfb2ebc3b58401 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 21:05:20 -0700 Subject: [PATCH 11/28] reorganize the ui --- run_dev.sh | 7 ++ .../button_check_in.png} | Bin src/assets/check_in_no_id_assets/image_2.png | Bin 6666 -> 0 bytes .../icon_check_in.png} | Bin .../button_fill_manually.png} | Bin .../outline_1.png} | Bin .../outline_2.png} | Bin .../outline_1.png} | Bin .../outline_2.png} | Bin .../register.png} | Bin src/assets/manual_fill_assets/image_4.png | Bin 1031 -> 0 bytes src/assets/manual_fill_assets/image_5.png | Bin 1031 -> 0 bytes src/assets/manual_fill_assets/image_6.png | Bin 2070 -> 0 bytes src/assets/manual_fill_assets/image_7.png | Bin 2070 -> 0 bytes src/assets/manual_fill_assets/image_8.png | Bin 2070 -> 0 bytes src/assets/manual_fill_assets/image_9.png | Bin 2070 -> 0 bytes .../no_acc_no_waiver_swipe_assets/image_4.png | Bin 1031 -> 0 bytes .../no_acc_no_waiver_swipe_assets/image_5.png | Bin 1031 -> 0 bytes .../image_5.png => qr_codes/qr_waiver.png} | Bin .../image_4.png => qr_codes/qr_website.png} | Bin src/assets/qr_codes_assets/image_3.png | Bin 1285 -> 0 bytes .../{image_1.png => background_main.png} | Bin .../image_3.png => shared/button_generic.png} | Bin .../image_3.png => shared/field.png} | Bin .../icon_checked_box.png} | Bin .../image_6.png => shared/icon_home.png} | Bin .../icon_unchecked_box.png} | Bin .../shared/{image_2.png => outline_full.png} | Bin .../button_done_scanning.png} | Bin .../image_2.png => sign_waiver/outline_1.png} | Bin .../image_3.png => sign_waiver/outline_2.png} | Bin .../image_4.png => sign_waiver/outline_3.png} | Bin .../image_7.png => sign_waiver/qr_waiver.png} | Bin .../waiver_no_acc_swipe_assets/button_1.png | Bin 7548 -> 0 bytes .../waiver_no_acc_swipe_assets/image_2.png | Bin 2522 -> 0 bytes .../waiver_no_acc_swipe_assets/image_3.png | Bin 5651 -> 0 bytes .../waiver_no_acc_swipe_assets/image_4.png | Bin 1031 -> 0 bytes .../waiver_no_acc_swipe_assets/image_5.png | Bin 1738 -> 0 bytes src/controllers/account_controller.py | 26 +---- src/controllers/card_reader_controller.py | 4 +- src/controllers/check_in_controller.py | 82 +++++-------- src/controllers/navigation_controller.py | 104 ++++++++++++----- src/controllers/swipe_controller.py | 19 ++- src/main.py | 14 ++- src/screens/acc_no_waiver.py | 10 -- src/screens/acc_no_waiver_swipe.py | 50 -------- src/screens/{screen.py => base.py} | 15 +-- .../{check_in_no_id.py => check_in_manual.py} | 47 +++++--- .../{main_page.py => check_in_rfid.py} | 18 ++- src/screens/components/__init__.py | 0 src/screens/components/canvas_entry.py | 73 ++++++++++++ src/screens/create_account_barcode.py | 54 +++++++++ src/screens/create_account_manual.py | 102 ++++++++++++++++ src/screens/dev_overlay.py | 107 +++++++++++++++++ src/screens/manual_fill.py | 110 ------------------ src/screens/no_acc_check_in_only.py | 10 -- src/screens/no_acc_no_waiver.py | 10 -- src/screens/no_acc_no_waiver_swipe.py | 44 ------- src/screens/qr_codes.py | 23 ++-- src/screens/sign_waiver.py | 49 ++++++++ src/screens/transition_screen.py | 15 +++ src/screens/user_thank.py | 37 ------ src/screens/user_welcome.py | 10 +- src/screens/waiver_no_acc.py | 10 -- src/screens/waiver_no_acc_swipe.py | 44 ------- src/window.py | 4 +- 66 files changed, 596 insertions(+), 502 deletions(-) rename src/assets/{check_in_no_id_assets/button_1.png => check_in_manual/button_check_in.png} (100%) delete mode 100644 src/assets/check_in_no_id_assets/image_2.png rename src/assets/{main_page_assets/image_4.png => check_in_rfid/icon_check_in.png} (100%) rename src/assets/{no_acc_no_waiver_swipe_assets/button_1.png => create_account_barcode/button_fill_manually.png} (100%) rename src/assets/{manual_fill_assets/image_2.png => create_account_barcode/outline_1.png} (100%) rename src/assets/{manual_fill_assets/image_3.png => create_account_barcode/outline_2.png} (100%) rename src/assets/{no_acc_no_waiver_swipe_assets/image_2.png => create_account_manual/outline_1.png} (100%) rename src/assets/{no_acc_no_waiver_swipe_assets/image_3.png => create_account_manual/outline_2.png} (100%) rename src/assets/{manual_fill_assets/button_1.png => create_account_manual/register.png} (100%) delete mode 100644 src/assets/manual_fill_assets/image_4.png delete mode 100644 src/assets/manual_fill_assets/image_5.png delete mode 100644 src/assets/manual_fill_assets/image_6.png delete mode 100644 src/assets/manual_fill_assets/image_7.png delete mode 100644 src/assets/manual_fill_assets/image_8.png delete mode 100644 src/assets/manual_fill_assets/image_9.png delete mode 100644 src/assets/no_acc_no_waiver_swipe_assets/image_4.png delete mode 100644 src/assets/no_acc_no_waiver_swipe_assets/image_5.png rename src/assets/{qr_codes_assets/image_5.png => qr_codes/qr_waiver.png} (100%) rename src/assets/{qr_codes_assets/image_4.png => qr_codes/qr_website.png} (100%) delete mode 100644 src/assets/qr_codes_assets/image_3.png rename src/assets/shared/{image_1.png => background_main.png} (100%) rename src/assets/{main_page_assets/image_3.png => shared/button_generic.png} (100%) rename src/assets/{check_in_no_id_assets/image_3.png => shared/field.png} (100%) rename src/assets/{acc_no_waiver_swipe_assets/image_5.png => shared/icon_checked_box.png} (100%) rename src/assets/{qr_codes_assets/image_6.png => shared/icon_home.png} (100%) rename src/assets/{acc_no_waiver_swipe_assets/image_6.png => shared/icon_unchecked_box.png} (100%) rename src/assets/shared/{image_2.png => outline_full.png} (100%) rename src/assets/{acc_no_waiver_swipe_assets/button_1.png => sign_waiver/button_done_scanning.png} (100%) rename src/assets/{acc_no_waiver_swipe_assets/image_2.png => sign_waiver/outline_1.png} (100%) rename src/assets/{acc_no_waiver_swipe_assets/image_3.png => sign_waiver/outline_2.png} (100%) rename src/assets/{acc_no_waiver_swipe_assets/image_4.png => sign_waiver/outline_3.png} (100%) rename src/assets/{acc_no_waiver_swipe_assets/image_7.png => sign_waiver/qr_waiver.png} (100%) delete mode 100644 src/assets/waiver_no_acc_swipe_assets/button_1.png delete mode 100644 src/assets/waiver_no_acc_swipe_assets/image_2.png delete mode 100644 src/assets/waiver_no_acc_swipe_assets/image_3.png delete mode 100644 src/assets/waiver_no_acc_swipe_assets/image_4.png delete mode 100644 src/assets/waiver_no_acc_swipe_assets/image_5.png delete mode 100644 src/screens/acc_no_waiver.py delete mode 100644 src/screens/acc_no_waiver_swipe.py rename src/screens/{screen.py => base.py} (79%) rename src/screens/{check_in_no_id.py => check_in_manual.py} (60%) rename src/screens/{main_page.py => check_in_rfid.py} (73%) create mode 100644 src/screens/components/__init__.py create mode 100644 src/screens/components/canvas_entry.py create mode 100644 src/screens/create_account_barcode.py create mode 100644 src/screens/create_account_manual.py create mode 100644 src/screens/dev_overlay.py delete mode 100644 src/screens/manual_fill.py delete mode 100644 src/screens/no_acc_check_in_only.py delete mode 100644 src/screens/no_acc_no_waiver.py delete mode 100644 src/screens/no_acc_no_waiver_swipe.py create mode 100644 src/screens/sign_waiver.py create mode 100644 src/screens/transition_screen.py delete mode 100644 src/screens/user_thank.py delete mode 100644 src/screens/waiver_no_acc.py delete mode 100644 src/screens/waiver_no_acc_swipe.py diff --git a/run_dev.sh b/run_dev.sh index 7d28756..e190a06 100755 --- a/run_dev.sh +++ b/run_dev.sh @@ -4,6 +4,13 @@ set -a source "$(dirname "$0")/.env" set +a +for arg in "$@"; do + if [ "$arg" = "--dev" ] || [ "$arg" = "-d" ]; then + export DEV_MODE=1 + break + fi +done + output_file="log.txt" echo "" >> "$output_file" diff --git a/src/assets/check_in_no_id_assets/button_1.png b/src/assets/check_in_manual/button_check_in.png similarity index 100% rename from src/assets/check_in_no_id_assets/button_1.png rename to src/assets/check_in_manual/button_check_in.png diff --git a/src/assets/check_in_no_id_assets/image_2.png b/src/assets/check_in_no_id_assets/image_2.png deleted file mode 100644 index eb574c4269c9f5971ad39fc0214315fcc73e0c57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6666 zcmeI1`%_bA9>%l1;nE^@-9f5AICBw-zR!Hm zbKdtopLxDFzln`eKp@=Cd>x5HAl_RnY1mcqyXCnU`ceQ49 zHl^vc?pxW0uAqoERz)+ckVU^Od%r_D_!;)eASUxxL%eHX!w2=Ho<|$z%exNr{;n5) z;J*ak)}svZx#}ODe)D@bo~Q}9Q~Lbaxt8qDD$<&TCx2e6!!XjuYBbQsMcRKpJ%j() zbY*V4`|y3uN1I~iCbN#O{RbV20CEF(9Icv$`O?Rm`ItkEg0zFU;c7?Ki1okaWdSLA zruipgtxp1mxwe6ee&db_duf4?w~>!zDE7md0Sa@$Ty8Q`Du_@ucS?1CKS6sLJf=AH zd8tF@A&>E#gXl_V3hH&;oq9VN5kFR}Pmg6r(#uy~)^5ZdC1woE~pBx2%WZtdqbLS518JJP}x3fDL^X(NG2`)Cf;bz*7#t%Z1Xu70}J@+2HG z@e|2chvj2wZF5UYO-ODw02=tPaHMp4d3PWk(+i5XNtPbQcR=pdOuT>-oDj8ZYE_%Y5{ru{>FHSjsJ5Kq%BSb3CSyxwks=E`1}c>dsQ*N} zI3{$#7iq1QlMA8IL{^#hN+%Kf=DMTeU$Rcb;*rsQM|1+s&P$!dXhL?I%nVklXL2`1 z0Gw|gCv^!cl4G$7|Jm#e;O~m|!ug+D(VDYlQA9P>p|CEchOosHuBT+@wN!HX6q^-6 zLP>+Mct3G3pII&6TUa)i;fLC|n1a!P;MDz&S?~P&wBxexfPKd!5TE|(zGK)mg!7@( z1m{X{ZU^U?;5-BuuPsyaD4`eS=iZ;ukELuRIt!=}bVQVUB; zgG0%t>pkvyFdf)7#$_RulhG+YMfJyFNv?i5V+YrqyCj~p$K_)9)ur>YoG`PlQL=HU zcxx%8*dX+kns-io#Jly~T5Owhll2{<@9#C!HONv*v)Q9z4{8=X0(-;2P0Qj?qxn0tdukj3_4u8)*J&7f@a?~0J>W|D!HLk;YOY! zUCuOj4uH65KFAhh!fMcrwHMYhxm5q_*qU!*Hp@4*&TDbAfP}5w0}LqA?$?-Z)gDya zn;*idSyRbys0FjJe3UR`GX}GE*ZU*Hwq1Mg!GN;rym`0b302XLa1q{58+T#P+RpB+ xUoQe$KA$6-Olv!P=@Qp!!6lyt={r-!@7~ywFF%_k>{Wc`ud$JWzg+m&{{RVnLP-Ds diff --git a/src/assets/main_page_assets/image_4.png b/src/assets/check_in_rfid/icon_check_in.png similarity index 100% rename from src/assets/main_page_assets/image_4.png rename to src/assets/check_in_rfid/icon_check_in.png diff --git a/src/assets/no_acc_no_waiver_swipe_assets/button_1.png b/src/assets/create_account_barcode/button_fill_manually.png similarity index 100% rename from src/assets/no_acc_no_waiver_swipe_assets/button_1.png rename to src/assets/create_account_barcode/button_fill_manually.png diff --git a/src/assets/manual_fill_assets/image_2.png b/src/assets/create_account_barcode/outline_1.png similarity index 100% rename from src/assets/manual_fill_assets/image_2.png rename to src/assets/create_account_barcode/outline_1.png diff --git a/src/assets/manual_fill_assets/image_3.png b/src/assets/create_account_barcode/outline_2.png similarity index 100% rename from src/assets/manual_fill_assets/image_3.png rename to src/assets/create_account_barcode/outline_2.png diff --git a/src/assets/no_acc_no_waiver_swipe_assets/image_2.png b/src/assets/create_account_manual/outline_1.png similarity index 100% rename from src/assets/no_acc_no_waiver_swipe_assets/image_2.png rename to src/assets/create_account_manual/outline_1.png diff --git a/src/assets/no_acc_no_waiver_swipe_assets/image_3.png b/src/assets/create_account_manual/outline_2.png similarity index 100% rename from src/assets/no_acc_no_waiver_swipe_assets/image_3.png rename to src/assets/create_account_manual/outline_2.png diff --git a/src/assets/manual_fill_assets/button_1.png b/src/assets/create_account_manual/register.png similarity index 100% rename from src/assets/manual_fill_assets/button_1.png rename to src/assets/create_account_manual/register.png diff --git a/src/assets/manual_fill_assets/image_4.png b/src/assets/manual_fill_assets/image_4.png deleted file mode 100644 index 37f2074c2bc6d7dbce23662733a569449c28b791..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmV+i1o-=jP)6`yzpu zs9A?k-Byk;gH$>4mTF7yy(TQZQI55KnIwJ5ZgGk>PMEb@c(l-W?zA=dOjX|Vu3?hAF5a}C( zNZ%Ml`o$>P5xb)W05=cjx`JCFW^i|7Ox!#yk~y>t_%;!cs;Z(O zhfKKzsK;B5*i1SB-JN2rhzJh9%y4`q5)fj5nQf)i=h$sQie_s1bVv~*0yxey$WOOz ztMggyDW*q@`ZJ7fCzm2X5dhSWApuCZ8vj~~KNVAYXkO2~>#_>-*Qz-Oqo}7A;i;yj=Zfxbfm?UCm}kdgRlTNWy0X3&rn`IRVCBN~3de26cyBza z4m3@xA|k5#xvHHarf=9v4TR-L1b54}NH7SDi=fJ+Ylnu$)8jPuNzh~-A{><_kfD4Q4%jE4XK002ovPDHLkV1kXt B=XC%8 diff --git a/src/assets/manual_fill_assets/image_5.png b/src/assets/manual_fill_assets/image_5.png deleted file mode 100644 index 37f2074c2bc6d7dbce23662733a569449c28b791..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmV+i1o-=jP)6`yzpu zs9A?k-Byk;gH$>4mTF7yy(TQZQI55KnIwJ5ZgGk>PMEb@c(l-W?zA=dOjX|Vu3?hAF5a}C( zNZ%Ml`o$>P5xb)W05=cjx`JCFW^i|7Ox!#yk~y>t_%;!cs;Z(O zhfKKzsK;B5*i1SB-JN2rhzJh9%y4`q5)fj5nQf)i=h$sQie_s1bVv~*0yxey$WOOz ztMggyDW*q@`ZJ7fCzm2X5dhSWApuCZ8vj~~KNVAYXkO2~>#_>-*Qz-Oqo}7A;i;yj=Zfxbfm?UCm}kdgRlTNWy0X3&rn`IRVCBN~3de26cyBza z4m3@xA|k5#xvHHarf=9v4TR-L1b54}NH7SDi=fJ+Ylnu$)8jPuNzh~-A{><_kfD4Q4%jE4XK002ovPDHLkV1kXt B=XC%8 diff --git a/src/assets/manual_fill_assets/image_6.png b/src/assets/manual_fill_assets/image_6.png deleted file mode 100644 index 581d067a64c5c5a465a64b1aca1052fee1de2d62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2070 zcmds&`#;m|AICp(XgMU^5#4`N@iu(JSlyU&%J>Ku@{eHe)*Za!4atWazdrB4n z00pEg+zS9CHo(0TL6tGf&pkh5PI?6zzWx+B>1Q1k5b1<^4k_9#IA>C;;uPqnG?891qGFg< zp#L(gpqVNpEU&K-%^zv&2SN1NF+Y-gw@b-S@$8@^m?FnMgQSJ!A+f*>E0aWHvhaAD z*qhDQWnR;bfRa1PbaZsq9M5EV)t1e+GMiD?*rw8tr{hq$9Y+8x&Mx8HwAu26HMVyt zsWI@*_RH2*?o_!9xxrEW=oNX^{Bc7)%1sj;omf_0#kOXAkR&_QYP{vpS`XGiykB-! zYrmBW16*R?_O&`$L(!rTtM-@)vY5*bG4qSOrM;j|JbH|d*G=eRUWCKvcxKQ-uMn)# z8E1iyy?O|&m=o)Ec>|T}(7fKpC9HpYP%=7<`QD~QM3iR~_e&X8-jJDI*`}{+4)uQl zi~%FvXRJ6SrTpTIu_z-ant!y}sm6-5okTPcrsX`WvDG+6vm535*`?Yp`(YN`O0X_= z;&-zzt|r{snZ%2RyMl|OqI7}UnvcX9+4YMIw6XXIZ{*hMegO!IiY zf%5%>UzuH<5%JdXI<^$si=FKZ@)e?7cZ_bbR5?BLjv&gYQJQj!rrCY$1ek4K z3zC;I-<_xzM(mUpm)F$fau%`!eeTIv23C;2b@ff@0v;w$#ls=j=b3yG)YnJ(VG&D@ z7d@9^-PWv336Bke!^7(Hn2ek!YQ*?dvD%{V*H~)CVZ{gIq`e{Lw?@PyX0QG?fDjBV zQ3Y1oa5?kgD}RhC<3aLC?~SkHjnWhBdp10Le02G={>Rj!dUP{sE6p8dZ=Y!2W@hF;r?G6| zM+468~g-(|U#6r<(+@W>N2l6L#w`NfkD%qHs|DEsQvL z-&Y=sO*rBr;sk;$9CgCe6*8TVV`UH{%e@P=i&f^J6NIPhlbcSx7I?zp71AdTY^QSN z6Iflq1*elrO5qaHAjbcP1H@X3Czc$me=sG2cR9X~(u}YQNQzoWOq_3Yhth%Qedky` zH^;V&ZILG!z4;^qDUO#cB#yB&i`ABB+W!j#0pC&?bi>T&7S!U|kg1>Elj{oCT&kBT z)Smqq*9N*V3^DVdnFJ;qzZ%vLJKtrn+f@c-jJ>dRiQ#@09AAV0Fx<1TB<@`t;5Yt# zNCmt#!hcm+rm~R3D|}-OL);e)7k#~J%blR5&vaoNOxu_<<+q#nFz^W}wXLmgP!}oS zf(*y(L9>wDHkH+rh<1jAT2awQ25^)2y86(>xQ^P4vo!xK^MKzw%h`KKj)m_RFltLx z&4OjKK5< z3Y)ke{o^DkjTzti=VjQfLX4vzH#u<52G2%S$YuUVjt7#AQiek1UZklfb(MV=$gzcp ze+v$u%&y8a78OZ`wj}U31T#yQsnl|+5`cWnpC^_s)z>8@wKV4g4K=BXWu33W(LXaf z3R9oha}(ARj^w$LMcD_o)dgI!WN3>=5x&@duGCR--{P94bedI%)ekK#?FJl?*^nVW2|zkpu>O=GQi zds8zeE-hZzrR~n94D!<9N432-V=~7_ig3d6v(r+?Bvg@qmz=kT7FYs)9#drc-1d3V z-43cpzE^I_y|8(D*ROB0l|jjidj|5{%_({SWD2#p2+;UpvSUAS|9kl9#LiSyYXbKu zp?cc1O*qm=ZetQ3&U3ai$|e6L?KyO2L4N3EDA;6_T7kUwvmri^OSzfMnH!|2=@bcnMzT9FqD!Vj0n3 diff --git a/src/assets/manual_fill_assets/image_7.png b/src/assets/manual_fill_assets/image_7.png deleted file mode 100644 index 581d067a64c5c5a465a64b1aca1052fee1de2d62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2070 zcmds&`#;m|AICp(XgMU^5#4`N@iu(JSlyU&%J>Ku@{eHe)*Za!4atWazdrB4n z00pEg+zS9CHo(0TL6tGf&pkh5PI?6zzWx+B>1Q1k5b1<^4k_9#IA>C;;uPqnG?891qGFg< zp#L(gpqVNpEU&K-%^zv&2SN1NF+Y-gw@b-S@$8@^m?FnMgQSJ!A+f*>E0aWHvhaAD z*qhDQWnR;bfRa1PbaZsq9M5EV)t1e+GMiD?*rw8tr{hq$9Y+8x&Mx8HwAu26HMVyt zsWI@*_RH2*?o_!9xxrEW=oNX^{Bc7)%1sj;omf_0#kOXAkR&_QYP{vpS`XGiykB-! zYrmBW16*R?_O&`$L(!rTtM-@)vY5*bG4qSOrM;j|JbH|d*G=eRUWCKvcxKQ-uMn)# z8E1iyy?O|&m=o)Ec>|T}(7fKpC9HpYP%=7<`QD~QM3iR~_e&X8-jJDI*`}{+4)uQl zi~%FvXRJ6SrTpTIu_z-ant!y}sm6-5okTPcrsX`WvDG+6vm535*`?Yp`(YN`O0X_= z;&-zzt|r{snZ%2RyMl|OqI7}UnvcX9+4YMIw6XXIZ{*hMegO!IiY zf%5%>UzuH<5%JdXI<^$si=FKZ@)e?7cZ_bbR5?BLjv&gYQJQj!rrCY$1ek4K z3zC;I-<_xzM(mUpm)F$fau%`!eeTIv23C;2b@ff@0v;w$#ls=j=b3yG)YnJ(VG&D@ z7d@9^-PWv336Bke!^7(Hn2ek!YQ*?dvD%{V*H~)CVZ{gIq`e{Lw?@PyX0QG?fDjBV zQ3Y1oa5?kgD}RhC<3aLC?~SkHjnWhBdp10Le02G={>Rj!dUP{sE6p8dZ=Y!2W@hF;r?G6| zM+468~g-(|U#6r<(+@W>N2l6L#w`NfkD%qHs|DEsQvL z-&Y=sO*rBr;sk;$9CgCe6*8TVV`UH{%e@P=i&f^J6NIPhlbcSx7I?zp71AdTY^QSN z6Iflq1*elrO5qaHAjbcP1H@X3Czc$me=sG2cR9X~(u}YQNQzoWOq_3Yhth%Qedky` zH^;V&ZILG!z4;^qDUO#cB#yB&i`ABB+W!j#0pC&?bi>T&7S!U|kg1>Elj{oCT&kBT z)Smqq*9N*V3^DVdnFJ;qzZ%vLJKtrn+f@c-jJ>dRiQ#@09AAV0Fx<1TB<@`t;5Yt# zNCmt#!hcm+rm~R3D|}-OL);e)7k#~J%blR5&vaoNOxu_<<+q#nFz^W}wXLmgP!}oS zf(*y(L9>wDHkH+rh<1jAT2awQ25^)2y86(>xQ^P4vo!xK^MKzw%h`KKj)m_RFltLx z&4OjKK5< z3Y)ke{o^DkjTzti=VjQfLX4vzH#u<52G2%S$YuUVjt7#AQiek1UZklfb(MV=$gzcp ze+v$u%&y8a78OZ`wj}U31T#yQsnl|+5`cWnpC^_s)z>8@wKV4g4K=BXWu33W(LXaf z3R9oha}(ARj^w$LMcD_o)dgI!WN3>=5x&@duGCR--{P94bedI%)ekK#?FJl?*^nVW2|zkpu>O=GQi zds8zeE-hZzrR~n94D!<9N432-V=~7_ig3d6v(r+?Bvg@qmz=kT7FYs)9#drc-1d3V z-43cpzE^I_y|8(D*ROB0l|jjidj|5{%_({SWD2#p2+;UpvSUAS|9kl9#LiSyYXbKu zp?cc1O*qm=ZetQ3&U3ai$|e6L?KyO2L4N3EDA;6_T7kUwvmri^OSzfMnH!|2=@bcnMzT9FqD!Vj0n3 diff --git a/src/assets/manual_fill_assets/image_8.png b/src/assets/manual_fill_assets/image_8.png deleted file mode 100644 index 581d067a64c5c5a465a64b1aca1052fee1de2d62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2070 zcmds&`#;m|AICp(XgMU^5#4`N@iu(JSlyU&%J>Ku@{eHe)*Za!4atWazdrB4n z00pEg+zS9CHo(0TL6tGf&pkh5PI?6zzWx+B>1Q1k5b1<^4k_9#IA>C;;uPqnG?891qGFg< zp#L(gpqVNpEU&K-%^zv&2SN1NF+Y-gw@b-S@$8@^m?FnMgQSJ!A+f*>E0aWHvhaAD z*qhDQWnR;bfRa1PbaZsq9M5EV)t1e+GMiD?*rw8tr{hq$9Y+8x&Mx8HwAu26HMVyt zsWI@*_RH2*?o_!9xxrEW=oNX^{Bc7)%1sj;omf_0#kOXAkR&_QYP{vpS`XGiykB-! zYrmBW16*R?_O&`$L(!rTtM-@)vY5*bG4qSOrM;j|JbH|d*G=eRUWCKvcxKQ-uMn)# z8E1iyy?O|&m=o)Ec>|T}(7fKpC9HpYP%=7<`QD~QM3iR~_e&X8-jJDI*`}{+4)uQl zi~%FvXRJ6SrTpTIu_z-ant!y}sm6-5okTPcrsX`WvDG+6vm535*`?Yp`(YN`O0X_= z;&-zzt|r{snZ%2RyMl|OqI7}UnvcX9+4YMIw6XXIZ{*hMegO!IiY zf%5%>UzuH<5%JdXI<^$si=FKZ@)e?7cZ_bbR5?BLjv&gYQJQj!rrCY$1ek4K z3zC;I-<_xzM(mUpm)F$fau%`!eeTIv23C;2b@ff@0v;w$#ls=j=b3yG)YnJ(VG&D@ z7d@9^-PWv336Bke!^7(Hn2ek!YQ*?dvD%{V*H~)CVZ{gIq`e{Lw?@PyX0QG?fDjBV zQ3Y1oa5?kgD}RhC<3aLC?~SkHjnWhBdp10Le02G={>Rj!dUP{sE6p8dZ=Y!2W@hF;r?G6| zM+468~g-(|U#6r<(+@W>N2l6L#w`NfkD%qHs|DEsQvL z-&Y=sO*rBr;sk;$9CgCe6*8TVV`UH{%e@P=i&f^J6NIPhlbcSx7I?zp71AdTY^QSN z6Iflq1*elrO5qaHAjbcP1H@X3Czc$me=sG2cR9X~(u}YQNQzoWOq_3Yhth%Qedky` zH^;V&ZILG!z4;^qDUO#cB#yB&i`ABB+W!j#0pC&?bi>T&7S!U|kg1>Elj{oCT&kBT z)Smqq*9N*V3^DVdnFJ;qzZ%vLJKtrn+f@c-jJ>dRiQ#@09AAV0Fx<1TB<@`t;5Yt# zNCmt#!hcm+rm~R3D|}-OL);e)7k#~J%blR5&vaoNOxu_<<+q#nFz^W}wXLmgP!}oS zf(*y(L9>wDHkH+rh<1jAT2awQ25^)2y86(>xQ^P4vo!xK^MKzw%h`KKj)m_RFltLx z&4OjKK5< z3Y)ke{o^DkjTzti=VjQfLX4vzH#u<52G2%S$YuUVjt7#AQiek1UZklfb(MV=$gzcp ze+v$u%&y8a78OZ`wj}U31T#yQsnl|+5`cWnpC^_s)z>8@wKV4g4K=BXWu33W(LXaf z3R9oha}(ARj^w$LMcD_o)dgI!WN3>=5x&@duGCR--{P94bedI%)ekK#?FJl?*^nVW2|zkpu>O=GQi zds8zeE-hZzrR~n94D!<9N432-V=~7_ig3d6v(r+?Bvg@qmz=kT7FYs)9#drc-1d3V z-43cpzE^I_y|8(D*ROB0l|jjidj|5{%_({SWD2#p2+;UpvSUAS|9kl9#LiSyYXbKu zp?cc1O*qm=ZetQ3&U3ai$|e6L?KyO2L4N3EDA;6_T7kUwvmri^OSzfMnH!|2=@bcnMzT9FqD!Vj0n3 diff --git a/src/assets/manual_fill_assets/image_9.png b/src/assets/manual_fill_assets/image_9.png deleted file mode 100644 index 581d067a64c5c5a465a64b1aca1052fee1de2d62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2070 zcmds&`#;m|AICp(XgMU^5#4`N@iu(JSlyU&%J>Ku@{eHe)*Za!4atWazdrB4n z00pEg+zS9CHo(0TL6tGf&pkh5PI?6zzWx+B>1Q1k5b1<^4k_9#IA>C;;uPqnG?891qGFg< zp#L(gpqVNpEU&K-%^zv&2SN1NF+Y-gw@b-S@$8@^m?FnMgQSJ!A+f*>E0aWHvhaAD z*qhDQWnR;bfRa1PbaZsq9M5EV)t1e+GMiD?*rw8tr{hq$9Y+8x&Mx8HwAu26HMVyt zsWI@*_RH2*?o_!9xxrEW=oNX^{Bc7)%1sj;omf_0#kOXAkR&_QYP{vpS`XGiykB-! zYrmBW16*R?_O&`$L(!rTtM-@)vY5*bG4qSOrM;j|JbH|d*G=eRUWCKvcxKQ-uMn)# z8E1iyy?O|&m=o)Ec>|T}(7fKpC9HpYP%=7<`QD~QM3iR~_e&X8-jJDI*`}{+4)uQl zi~%FvXRJ6SrTpTIu_z-ant!y}sm6-5okTPcrsX`WvDG+6vm535*`?Yp`(YN`O0X_= z;&-zzt|r{snZ%2RyMl|OqI7}UnvcX9+4YMIw6XXIZ{*hMegO!IiY zf%5%>UzuH<5%JdXI<^$si=FKZ@)e?7cZ_bbR5?BLjv&gYQJQj!rrCY$1ek4K z3zC;I-<_xzM(mUpm)F$fau%`!eeTIv23C;2b@ff@0v;w$#ls=j=b3yG)YnJ(VG&D@ z7d@9^-PWv336Bke!^7(Hn2ek!YQ*?dvD%{V*H~)CVZ{gIq`e{Lw?@PyX0QG?fDjBV zQ3Y1oa5?kgD}RhC<3aLC?~SkHjnWhBdp10Le02G={>Rj!dUP{sE6p8dZ=Y!2W@hF;r?G6| zM+468~g-(|U#6r<(+@W>N2l6L#w`NfkD%qHs|DEsQvL z-&Y=sO*rBr;sk;$9CgCe6*8TVV`UH{%e@P=i&f^J6NIPhlbcSx7I?zp71AdTY^QSN z6Iflq1*elrO5qaHAjbcP1H@X3Czc$me=sG2cR9X~(u}YQNQzoWOq_3Yhth%Qedky` zH^;V&ZILG!z4;^qDUO#cB#yB&i`ABB+W!j#0pC&?bi>T&7S!U|kg1>Elj{oCT&kBT z)Smqq*9N*V3^DVdnFJ;qzZ%vLJKtrn+f@c-jJ>dRiQ#@09AAV0Fx<1TB<@`t;5Yt# zNCmt#!hcm+rm~R3D|}-OL);e)7k#~J%blR5&vaoNOxu_<<+q#nFz^W}wXLmgP!}oS zf(*y(L9>wDHkH+rh<1jAT2awQ25^)2y86(>xQ^P4vo!xK^MKzw%h`KKj)m_RFltLx z&4OjKK5< z3Y)ke{o^DkjTzti=VjQfLX4vzH#u<52G2%S$YuUVjt7#AQiek1UZklfb(MV=$gzcp ze+v$u%&y8a78OZ`wj}U31T#yQsnl|+5`cWnpC^_s)z>8@wKV4g4K=BXWu33W(LXaf z3R9oha}(ARj^w$LMcD_o)dgI!WN3>=5x&@duGCR--{P94bedI%)ekK#?FJl?*^nVW2|zkpu>O=GQi zds8zeE-hZzrR~n94D!<9N432-V=~7_ig3d6v(r+?Bvg@qmz=kT7FYs)9#drc-1d3V z-43cpzE^I_y|8(D*ROB0l|jjidj|5{%_({SWD2#p2+;UpvSUAS|9kl9#LiSyYXbKu zp?cc1O*qm=ZetQ3&U3ai$|e6L?KyO2L4N3EDA;6_T7kUwvmri^OSzfMnH!|2=@bcnMzT9FqD!Vj0n3 diff --git a/src/assets/no_acc_no_waiver_swipe_assets/image_4.png b/src/assets/no_acc_no_waiver_swipe_assets/image_4.png deleted file mode 100644 index 37f2074c2bc6d7dbce23662733a569449c28b791..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmV+i1o-=jP)6`yzpu zs9A?k-Byk;gH$>4mTF7yy(TQZQI55KnIwJ5ZgGk>PMEb@c(l-W?zA=dOjX|Vu3?hAF5a}C( zNZ%Ml`o$>P5xb)W05=cjx`JCFW^i|7Ox!#yk~y>t_%;!cs;Z(O zhfKKzsK;B5*i1SB-JN2rhzJh9%y4`q5)fj5nQf)i=h$sQie_s1bVv~*0yxey$WOOz ztMggyDW*q@`ZJ7fCzm2X5dhSWApuCZ8vj~~KNVAYXkO2~>#_>-*Qz-Oqo}7A;i;yj=Zfxbfm?UCm}kdgRlTNWy0X3&rn`IRVCBN~3de26cyBza z4m3@xA|k5#xvHHarf=9v4TR-L1b54}NH7SDi=fJ+Ylnu$)8jPuNzh~-A{><_kfD4Q4%jE4XK002ovPDHLkV1kXt B=XC%8 diff --git a/src/assets/no_acc_no_waiver_swipe_assets/image_5.png b/src/assets/no_acc_no_waiver_swipe_assets/image_5.png deleted file mode 100644 index 37f2074c2bc6d7dbce23662733a569449c28b791..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmV+i1o-=jP)6`yzpu zs9A?k-Byk;gH$>4mTF7yy(TQZQI55KnIwJ5ZgGk>PMEb@c(l-W?zA=dOjX|Vu3?hAF5a}C( zNZ%Ml`o$>P5xb)W05=cjx`JCFW^i|7Ox!#yk~y>t_%;!cs;Z(O zhfKKzsK;B5*i1SB-JN2rhzJh9%y4`q5)fj5nQf)i=h$sQie_s1bVv~*0yxey$WOOz ztMggyDW*q@`ZJ7fCzm2X5dhSWApuCZ8vj~~KNVAYXkO2~>#_>-*Qz-Oqo}7A;i;yj=Zfxbfm?UCm}kdgRlTNWy0X3&rn`IRVCBN~3de26cyBza z4m3@xA|k5#xvHHarf=9v4TR-L1b54}NH7SDi=fJ+Ylnu$)8jPuNzh~-A{><_kfD4Q4%jE4XK002ovPDHLkV1kXt B=XC%8 diff --git a/src/assets/qr_codes_assets/image_5.png b/src/assets/qr_codes/qr_waiver.png similarity index 100% rename from src/assets/qr_codes_assets/image_5.png rename to src/assets/qr_codes/qr_waiver.png diff --git a/src/assets/qr_codes_assets/image_4.png b/src/assets/qr_codes/qr_website.png similarity index 100% rename from src/assets/qr_codes_assets/image_4.png rename to src/assets/qr_codes/qr_website.png diff --git a/src/assets/qr_codes_assets/image_3.png b/src/assets/qr_codes_assets/image_3.png deleted file mode 100644 index 7eeb581ac0f7eddef0ae356cf0990d38161bc220..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1285 zcmeAS@N?(olHy`uVBq!ia0vp^DIm4(U{h;~(#T z-uM2D+T@h~r?0hn2`<%G!S1@@)}rf|Uf&a)m}}d4rMW_Yzo}iRB4C%t$49>XTf+~E z?$u~YTDU^wR$lJgyvcV{)K5OCy?@-pOL{>SkRE+ zNp|@BCXp`p!w+VuJpX~(|TdbEUJ z+87k|rRu}I_g_A~_c~{}WT{Zq+&j!ARi%F~&Hnn!*wR?oa5JO9wanPxn_^~LO!(@{o0fH_w3uaEvUg#vQ1)5 z@M5z)=gbfPIQ1lF)5Vy`HGWt71NT+^R1n(klEA0<&sfyZt?b;yi;9x6Oi3vf?{7*N z?|E@k;;@2&_}}I}UD@-S&g5)~YRp^a9Htj+eZHChqO{VB4R_>~8YFXcRkwPjSL#_$RJwx3dil|dFf@R6|&MZ1QJa;45VBnc$IqC1l^iB7CW|FR5&zjgg2)OEMB>- zcjxJorRBFyoaqoOICtr;*y`%!p2p&7zZ4A^_FcB)YdEf(sAL?I^!Cb+ichcg_pkLo ze|A|be|gNBcfG~aW?c`f_P5zpf5+;z(*3%qnU(>&ch)Etp8Qw0|GbNfp_|@R?=PQs z+Mk>MBjMwj`R3QEuSGFE;``fs_mIM|4qIX2mnX}``s}{`ILvNU+1_h-e7uey?k~DYXZzq!qk%;UV)6+TIZ_DN!Nifpd zy6Wuhx93;xEc$rU#CIQab5f6s$qnseC(cC$5Kar}?!7MjTIyzHs(r0>eHd%h@?5;bNk5rm8%QQR<~9rk*$ diff --git a/src/assets/shared/image_1.png b/src/assets/shared/background_main.png similarity index 100% rename from src/assets/shared/image_1.png rename to src/assets/shared/background_main.png diff --git a/src/assets/main_page_assets/image_3.png b/src/assets/shared/button_generic.png similarity index 100% rename from src/assets/main_page_assets/image_3.png rename to src/assets/shared/button_generic.png diff --git a/src/assets/check_in_no_id_assets/image_3.png b/src/assets/shared/field.png similarity index 100% rename from src/assets/check_in_no_id_assets/image_3.png rename to src/assets/shared/field.png diff --git a/src/assets/acc_no_waiver_swipe_assets/image_5.png b/src/assets/shared/icon_checked_box.png similarity index 100% rename from src/assets/acc_no_waiver_swipe_assets/image_5.png rename to src/assets/shared/icon_checked_box.png diff --git a/src/assets/qr_codes_assets/image_6.png b/src/assets/shared/icon_home.png similarity index 100% rename from src/assets/qr_codes_assets/image_6.png rename to src/assets/shared/icon_home.png diff --git a/src/assets/acc_no_waiver_swipe_assets/image_6.png b/src/assets/shared/icon_unchecked_box.png similarity index 100% rename from src/assets/acc_no_waiver_swipe_assets/image_6.png rename to src/assets/shared/icon_unchecked_box.png diff --git a/src/assets/shared/image_2.png b/src/assets/shared/outline_full.png similarity index 100% rename from src/assets/shared/image_2.png rename to src/assets/shared/outline_full.png diff --git a/src/assets/acc_no_waiver_swipe_assets/button_1.png b/src/assets/sign_waiver/button_done_scanning.png similarity index 100% rename from src/assets/acc_no_waiver_swipe_assets/button_1.png rename to src/assets/sign_waiver/button_done_scanning.png diff --git a/src/assets/acc_no_waiver_swipe_assets/image_2.png b/src/assets/sign_waiver/outline_1.png similarity index 100% rename from src/assets/acc_no_waiver_swipe_assets/image_2.png rename to src/assets/sign_waiver/outline_1.png diff --git a/src/assets/acc_no_waiver_swipe_assets/image_3.png b/src/assets/sign_waiver/outline_2.png similarity index 100% rename from src/assets/acc_no_waiver_swipe_assets/image_3.png rename to src/assets/sign_waiver/outline_2.png diff --git a/src/assets/acc_no_waiver_swipe_assets/image_4.png b/src/assets/sign_waiver/outline_3.png similarity index 100% rename from src/assets/acc_no_waiver_swipe_assets/image_4.png rename to src/assets/sign_waiver/outline_3.png diff --git a/src/assets/acc_no_waiver_swipe_assets/image_7.png b/src/assets/sign_waiver/qr_waiver.png similarity index 100% rename from src/assets/acc_no_waiver_swipe_assets/image_7.png rename to src/assets/sign_waiver/qr_waiver.png diff --git a/src/assets/waiver_no_acc_swipe_assets/button_1.png b/src/assets/waiver_no_acc_swipe_assets/button_1.png deleted file mode 100644 index 6771e90ff6f66e8f6be74d71741392b70002759f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7548 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 diff --git a/src/assets/waiver_no_acc_swipe_assets/image_3.png b/src/assets/waiver_no_acc_swipe_assets/image_3.png deleted file mode 100644 index d42add79ab4ea08ebf1c91c6f823d2cb31f85217..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5651 zcmeHL{Z~?X9%g4|$Jz01=gdwuV%g44Tb7B9jf|_p)Ty;_%tA9mldLTr5%Fbw0g~I< zHa)J1r-|BJUsz9zX6%IwFKUX8OCy_viU=ek*s#bTD)|DWye&BMSM1@pd%oX$p7VU4 z`+Pt5^Lc7dpGpjTKjeKF3>J9u#4#)k_Rc&E_SXLQ-u0|pfA_c+2Kz{L^4O8I%I2k6 z{KNkmE{IkPWLoK1W2tpE55nKHJzH ze&`)9Mpi10aD_+&0|UvU^%O34l{Lhm03dJW~r&CW`c_I@%eG6;bmG)!4? z(bH5CWXldlI7HK*1>mzYnX`{GB1GkTM(=8=V_XpO82GT3h8fyXz*2_WIeFHPF62k`6-bGn^j9!h9{rAL>Da z=^`^p*qC6Cb#E~mw*XArW(77A-3P%PB1)XF+i@Pl7C3qi}0 z?_&iZcAWK< zk=bWyM>-t^F>rcAgI&%8AqC2Ux;I^vnb7;@MGc?dL(^R(0sn%65QU?6C#xU$xgF7A zlsT6l>g<^2N#9`Ar4!>`)55fptE?%py-ru+>J&C4ypb}UdYizG>?m$dmZjx`kPX%P zGB!4Jc?tMu_Tc&poylY(;Ul)Cf>5&TFcDwzD8a=7HhEz~PEN4)4X2-j&Wh+L4ke1W zwEnVrW^XkDy}}(2YtTt2 zR7qyfaWM!|o_2oC`_6an;nm7mkB!S#lJCFmS-1}S=~KM*mss!qd%yPFd>Cx^m%dG( z2tFtHu7j^8_zI(^7xanX6Je`m^Ets&5q%=~MDVrEzS+SyK>c3~ik0#xWl|@c7bxBO z#krW>Sg}7SP|uBgy`Ptp;qEF$UclReM=UuHhqZp*sYu!Ec`h>-J$0%xNqHdgmTY~J z3PRZY=@xl*D=rh?f4M8;D@YMT*L4ec(dxD3iUE^w$TAFM0b(1;6BiYSQVX}DtKOIZ z;c2A-PTY_HeU2ow?RGyuWhubE9g*ybKd4NFLZOmMrFVGPh0DTC)y^wjKe8BD0$Ahc z=j4YC=&Qo0uOFl&6uG%okD3MKQUVnf7kRvS%bFxzp8H~iff?^`qBjLL#{`HJ7>Y2q*Lp4%+XYb6b;UkUhM%F%L zva`~@UdFpohj2W%8hK`}z(%QjrPZWeZE`>6^UEn+8FWz5#yC*Sv1w{@@`MwifASK% zt~_)q^HqnR*26OkKSd zbV77bwAlQIQmgbv-S{k-Tw!iYpOJbbsM)xt!O4DRA;qzq*2r;Pr6qab!ANs9<;T_K z;go^TM(?hoU6MAm6`yzpu zs9A?k-Byk;gH$>4mTF7yy(TQZQI55KnIwJ5ZgGk>PMEb@c(l-W?zA=dOjX|Vu3?hAF5a}C( zNZ%Ml`o$>P5xb)W05=cjx`JCFW^i|7Ox!#yk~y>t_%;!cs;Z(O zhfKKzsK;B5*i1SB-JN2rhzJh9%y4`q5)fj5nQf)i=h$sQie_s1bVv~*0yxey$WOOz ztMggyDW*q@`ZJ7fCzm2X5dhSWApuCZ8vj~~KNVAYXkO2~>#_>-*Qz-Oqo}7A;i;yj=Zfxbfm?UCm}kdgRlTNWy0X3&rn`IRVCBN~3de26cyBza z4m3@xA|k5#xvHHarf=9v4TR-L1b54}NH7SDi=fJ+Ylnu$)8jPuNzh~-A{><_kfD4Q4%jE4XK002ovPDHLkV1kXt B=XC%8 diff --git a/src/assets/waiver_no_acc_swipe_assets/image_5.png b/src/assets/waiver_no_acc_swipe_assets/image_5.png deleted file mode 100644 index 19cd880f8b4503a457fb7027f73c5dd724f666ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1738 zcmV;*1~vJKP)n$dEuuR5o5%%AYO$Lfp-oe1PsFv30&%j_|2+te~pP108;?!owR0r zi_gP_VrBk*Gy1mc(m0M>HUt2W#5hfCyDlX&U(99?l(~Pp@6(-@Vh2EXmQ%zVeU?8} z6ve4#eIxF+cOoLo1h`(iwm1N2Ha22j6sKI~p7JUuXlYc^Vq|)}n%5_rrimirmu&NE zN<@6qG_hWppG?rtd9Q}U$rL9*g*r`aA^JOU9su^d#+EcAg!5hq`IZyi zn5L*cb1Ss;K$=Eifj#ZeK}cFz&$QWY*68G7qOQg%Z4mrmJ)7O#cfEvaU5w(=%~O&$%(Jcn#Y^dAORdykQ*1g=PZ) z4(-TJk|6}?n&w}kdS1keWNr&;_c{$9=$po{AOvuTGeVMiS&F4}AxU~68A@beoW@TU zWl_W!A+q?v^-w@aT3J=vY;OH2%iZ@p-7y$magpz>%KDwn*0!nYE!lD?AcEcVkofZ! z{W<{njK>H*h?IFeP?D?&1X6c-LuKtPZQGdC~2KZK2*);Z|&N4VF$ln z3pqPUWLYMmDji}(v)%AU`3wnq@6oq?ug;%$BW2RqJ8^f4;uAyH-LTyv+Ve<|WR_(Z zr|~b_8yjD1+xF*KmdTCkMkKAMYqgYqs1~0~NOOzQ)9m4DHaj*=)4qX7v;Oi;Z$c!g zssdo5@tlxrF`?2RU#aJ_v(4sKx>mp-X(0;|r}%es@?nX#POk$dzAXgzT{BaXf_+M{ z_dFvaJ4Q|xw@h%t1(^x(M7P~su4gm1>o)*^kYxhFcg)gNQ4P}+9hg2H^4y#4R66$q zV$UEVA}P`GBF}^KU!ds-K+JR$rZ4twd$q`OXJjm^QrqqAKZ5vyr#qI$exp!-|7<>c zd)I95i-Ru=vI7I{yPnl-_AVek?vz{VP>B{V#b}>TW=#k|QcB6W@T60;0L^FNc%nTL zW8y&+Y!(_75s`L#JJs{q$5O&M2mR+T^~ZzHemhRXm-4E1eLwzEIQ=3~SDpAxi}I1# ze15X;d+zsa0DE-y>qU~zIXdU2z8^kIpFfK@ye{|oD=`kAQs>r^O~02cSB!RtD!wmH z$`lW#^tABE?9`N^h#(JHcmip-tH|6|Vq7KM)6Vn{s-g7eMj|~@)@2o^xNqV1zjbTp z8&&E1!SXyi5xo0qZ)r}ZQ5~wNmOBmGG4=O_5Z==^?X}Q-AiCwXs;b&HZ7PfME)VzR zonM(SO^mS%0(mS1f8e#{eQ`PwaZS^vYBqa^6Zcq(Xd`8vkaI;@-rX&jzV69}MVyM9O0VF%MODn~z-!C<3Igm_4(nPVg0^XP3Ok2T9^dy=S45<3+e88)aEEnS zaZowCJM2pey$!EL{zoDLHM8+{h7l2%*`$D6@!sJ`T&V9+BM>nyhu3{^zBen?LBl3LQ=ko-xxx+>RacSl_$Q@JviWa(!yj+a$6 zWe)m{W^?n!x~@IR_}bkpS0rOy*S_6qUIg^(BI;GtWznX8q%l1@^!=53b@kfbl8#aU z*tI1AKz(f0_kDjQj>Dsw$KOOZS`T^7QTxPBKAWLYLv zSvpaP-R9Ph;xK(&RsMYKnUnnI!!tLMeH3-40XYMa(*WKqK(GmZeSK=8bZJQF9Z gsisWSz54L%->G=L_;DKN(*OVf07*qoM6N<$g3pLdqW}N^ diff --git a/src/controllers/account_controller.py b/src/controllers/account_controller.py index ed851c3..e65b292 100644 --- a/src/controllers/account_controller.py +++ b/src/controllers/account_controller.py @@ -1,9 +1,6 @@ import time import tkinter import logging -from screens.main_page import MainPage -from screens.acc_no_waiver_swipe import AccNoWaiverSwipe -from screens.user_thank import UserThank class AccountController: @@ -87,29 +84,14 @@ def create_account(self, fname, lname, email, pid): no_wifi.destroy() if retries == 6: - self.ctx.nav.show_frame(MainPage) + self.ctx.nav.back_to_main() inProgress.destroy() return end4 = time.perf_counter() logging.debug(f"Total time to send data: {end4 - end2}") - checkin_result = self.ctx.sheets.checkin_by_uuid(self.ctx.rfid) - toGoTo = AccNoWaiverSwipe if checkin_result.get("status") == "no_waiver" else MainPage - - end5 = time.perf_counter() - logging.debug(f"Time to check waiver via check-in: {end5 - end4}") - - self.ctx.nav.get_frame(UserThank).display_name(full_name, toGoTo) inProgress.destroy() - - def on_thank_start(self, next_page): - if next_page == MainPage: - self.ctx.traffic_light.request_green() - else: - self.ctx.traffic_light.request_yellow() - - def on_thank_done(self, next_page): - self.ctx.nav.show_frame(next_page) - if next_page == MainPage: - self.ctx.traffic_light.request_off() + # pop() runs the on_done continuation from the nav stack, + # which re-runs _run_check_in to handle waiver check and check-in. + self.ctx.nav.pop() diff --git a/src/controllers/card_reader_controller.py b/src/controllers/card_reader_controller.py index 4a1472f..1d2c74a 100644 --- a/src/controllers/card_reader_controller.py +++ b/src/controllers/card_reader_controller.py @@ -3,7 +3,7 @@ import logging from tkinter import Label from threading import Thread -from screens.manual_fill import ManualFill +from screens.create_account_manual import CreateAccountManual class CardReaderController: @@ -53,7 +53,7 @@ def _run(self, reader): no_wifi.after(4000, lambda: self._destroy_wifi_error(no_wifi)) continue - self.ctx.nav.get_frame(ManualFill).clear_entries() + self.ctx.nav.get_frame(CreateAccountManual).clear_entries() tag = reader.grab_rfid() if " " in tag: diff --git a/src/controllers/check_in_controller.py b/src/controllers/check_in_controller.py index c4cd557..c0abd31 100644 --- a/src/controllers/check_in_controller.py +++ b/src/controllers/check_in_controller.py @@ -1,12 +1,6 @@ import logging from tkinter import Label -from screens.main_page import MainPage -from screens.no_acc_no_waiver import NoAccNoWaiver -from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe -from screens.no_acc_check_in_only import NoAccCheckInOnly -from screens.acc_no_waiver import AccNoWaiver -from screens.acc_no_waiver_swipe import AccNoWaiverSwipe from screens.user_welcome import UserWelcome @@ -15,58 +9,46 @@ def __init__(self, ctx): self.ctx = ctx def handle_by_uuid(self, tag): - result = self.ctx.sheets.checkin_by_uuid(tag) - status = result.get("status") - - def update_ui(): - if status == "api_error": - logging.error("API error during check-in") - self.ctx.traffic_light.request_red() - error_label = Label( - self.ctx.window.canvas, - text="System error, please let staff know.", - bg="#153246", fg="white", font=("Arial", 25), - ) - error_label.place(relx=0.5, rely=0.1, anchor="center") - error_label.after(4000, error_label.destroy) - return - - if status == "no_account": - logging.info(f"User {tag} not found.") - self.ctx.traffic_light.request_red() - self.ctx.nav.show_frame(NoAccNoWaiver) - self.ctx.window.after(3000, lambda: self.ctx.nav.show_frame(NoAccNoWaiverSwipe)) - return - - if status == "no_waiver": - logging.info(f"User {tag} does not have waiver.") - self.ctx.traffic_light.request_yellow() - self.ctx.nav.show_frame(AccNoWaiver) - self.ctx.window.after(3000, lambda: self.ctx.nav.show_frame(AccNoWaiverSwipe)) - return - - logging.info(f"User found: {result['name']}") - self.ctx.traffic_light.request_green() - self.ctx.nav.get_frame(UserWelcome).display_name(result["name"]) - - self.ctx.window.after(0, update_ui) + # Called from background thread — defer to main thread. + self.ctx.window.after( + 0, lambda: self._run_check_in(tag, self.ctx.sheets.checkin_by_uuid) + ) def handle_by_pid(self, pid): - result = self.ctx.sheets.checkin_by_pid(pid) + self._run_check_in(pid, self.ctx.sheets.checkin_by_pid) + + def _run_check_in(self, identifier, check_fn, welcome_message="Welcome back"): + result = check_fn(identifier) status = result.get("status") + if status == "api_error": + logging.error("API error during check-in") + self.ctx.traffic_light.request_red() + error_label = Label( + self.ctx.window.canvas, + text="System error, please let staff know.", + bg="#153246", fg="white", font=("Arial", 25), + ) + error_label.place(relx=0.5, rely=0.1, anchor="center") + error_label.after(4000, error_label.destroy) + return + if status == "no_account": - logging.info("Manual check-in: user account not found") - self.ctx.nav.show_frame(NoAccCheckInOnly) - self.ctx.nav.after(5000, lambda: self.ctx.nav.show_frame(MainPage)) + logging.info(f"No account found for {identifier}") + self.ctx.traffic_light.request_red() + self.ctx.nav.go_to_create_account( + on_done=lambda: self._run_check_in( + identifier, check_fn, welcome_message="Thank you for registering" + ) + ) return if status == "no_waiver": - logging.info(f"Manual check-in: no waiver for {result.get('name', pid)}") - self.ctx.nav.show_frame(AccNoWaiver) - self.ctx.nav.after(3000, lambda: self.ctx.nav.show_frame(NoAccNoWaiverSwipe)) + logging.info(f"No waiver for {identifier}") + self.ctx.traffic_light.request_yellow() + self.ctx.nav.go_to_sign_waiver() return - logging.info(f"Manual check-in for {result['name']}") + logging.info(f"Check-in successful: {result['name']}") self.ctx.traffic_light.request_green() - self.ctx.nav.get_frame(UserWelcome).display_name(result["name"]) + self.ctx.nav.get_frame(UserWelcome).display_name(result["name"], welcome_message) diff --git a/src/controllers/navigation_controller.py b/src/controllers/navigation_controller.py index 8389678..1794f53 100644 --- a/src/controllers/navigation_controller.py +++ b/src/controllers/navigation_controller.py @@ -1,52 +1,51 @@ import uuid -from screens.main_page import MainPage -from screens.acc_no_waiver import AccNoWaiver -from screens.acc_no_waiver_swipe import AccNoWaiverSwipe -from screens.manual_fill import ManualFill -from screens.check_in_no_id import CheckInNoId -from screens.no_acc_check_in_only import NoAccCheckInOnly -from screens.no_acc_no_waiver import NoAccNoWaiver -from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe +from screens.check_in_rfid import CheckInRFID +from screens.transition_screen import TransitionScreen +from screens.create_account_barcode import CreateAccountBarcode +from screens.create_account_manual import CreateAccountManual +from screens.sign_waiver import SignWaiver +from screens.check_in_manual import CheckInManual from screens.qr_codes import QRCodes -from screens.user_thank import UserThank from screens.user_welcome import UserWelcome -from screens.waiver_no_acc import WaiverNoAcc -from screens.waiver_no_acc_swipe import WaiverNoAccSwipe class NavigationController: - def __init__(self, window, ctx): + def __init__(self, window, ctx, dev_mode=False): self.ctx = ctx self._window = window self._frames = {} self._curr = None self._frame_uuid = uuid.uuid4().hex + self._on_done_stack = [] + self._dev_overlay = None self._timeouts = { - AccNoWaiverSwipe: 30000, + SignWaiver: 30000, QRCodes: 30000, - NoAccNoWaiverSwipe: 30000, } for F in ( - MainPage, - AccNoWaiver, - AccNoWaiverSwipe, - ManualFill, - CheckInNoId, - NoAccCheckInOnly, - NoAccNoWaiver, - NoAccNoWaiverSwipe, + CheckInRFID, + TransitionScreen, + CreateAccountBarcode, + CreateAccountManual, + SignWaiver, + CheckInManual, QRCodes, - UserThank, UserWelcome, - WaiverNoAcc, - WaiverNoAccSwipe, ): self._frames[F] = F(window.canvas, self) - self.show_frame(MainPage) + if dev_mode: + from screens.dev_overlay import DevOverlay + self._dev_overlay = DevOverlay(window.canvas, self) + + self.show_frame(CheckInRFID) + + # ------------------------------------------------------------------ + # Core frame switching + # ------------------------------------------------------------------ def show_frame(self, screen_class): if self._curr is not None: @@ -55,6 +54,9 @@ def show_frame(self, screen_class): self._frame_uuid = uuid.uuid4().hex self._frames[screen_class].show() + if self._dev_overlay is not None: + self._dev_overlay.update(screen_class) + if screen_class in self._timeouts: uid = self._frame_uuid self._window.after( @@ -71,17 +73,55 @@ def get_curr_frame(self): def after(self, ms, fn): self._window.after(ms, fn) + # ------------------------------------------------------------------ + # Stack-based flow + # ------------------------------------------------------------------ + + def push(self, screen_class, on_done=None): + """Show screen_class and register a continuation to run when pop() is called.""" + self._on_done_stack.append(on_done) + self.show_frame(screen_class) + + def pop(self): + """Signal that the current screen is done; run the stored continuation.""" + cb = self._on_done_stack.pop() if self._on_done_stack else None + if cb: + cb() + else: + self.back_to_main() + + # ------------------------------------------------------------------ + # Named navigations + # ------------------------------------------------------------------ + def back_to_main(self): + self._on_done_stack.clear() self.ctx.traffic_light.request_off() - self.show_frame(MainPage) + self.show_frame(CheckInRFID) def go_to_no_id(self): - self.get_frame(CheckInNoId).clear_entries() - self.show_frame(CheckInNoId) + self.get_frame(CheckInManual).clear_entries() + self.show_frame(CheckInManual) + + def go_to_create_account_manual(self): + self.get_frame(CreateAccountManual).clear_entries() + self.show_frame(CreateAccountManual) + + def go_to_create_account(self, on_done): + self.get_frame(TransitionScreen).display( + "Looks like you don't have an account,\nlet's set one up!" + ) + self._window.after(3000, lambda: self.push(CreateAccountBarcode, on_done=on_done)) + + def go_to_sign_waiver(self): + self.get_frame(TransitionScreen).display( + "Looks like you haven't signed\nthe waiver yet,\nlet's fix that!" + ) + self._window.after(3000, lambda: self.show_frame(SignWaiver)) - def go_to_manual_fill(self): - self.get_frame(ManualFill).clear_entries() - self.show_frame(ManualFill) + # ------------------------------------------------------------------ + # Internal + # ------------------------------------------------------------------ def _on_timeout(self, uid): if uid == self._frame_uuid: diff --git a/src/controllers/swipe_controller.py b/src/controllers/swipe_controller.py index c4b00eb..5d64be0 100644 --- a/src/controllers/swipe_controller.py +++ b/src/controllers/swipe_controller.py @@ -1,9 +1,8 @@ import tkinter import logging -from screens.manual_fill import ManualFill -from screens.no_acc_no_waiver_swipe import NoAccNoWaiverSwipe -from screens.waiver_no_acc_swipe import WaiverNoAccSwipe -from screens.check_in_no_id import CheckInNoId +from screens.create_account_manual import CreateAccountManual +from screens.create_account_barcode import CreateAccountBarcode +from screens.check_in_manual import CheckInManual from api.get_info_from_pid import ContactClient @@ -23,7 +22,7 @@ def _id_vet(self, id_check): def keyboard_press(self, key): curr_frame = self.ctx.nav.get_curr_frame() - if curr_frame not in (NoAccNoWaiverSwipe, WaiverNoAccSwipe, CheckInNoId): + if curr_frame not in (CreateAccountBarcode, CheckInManual): return self._id_string += key.char @@ -71,9 +70,9 @@ def _swipe_card(self, id_string): logging.info("Student search returned False, returning...") return - if self.ctx.nav.get_curr_frame() == CheckInNoId: - self.ctx.nav.get_frame(CheckInNoId).clear_entries() - self.ctx.nav.get_frame(CheckInNoId).update_entries(u_data.pid) + if self.ctx.nav.get_curr_frame() == CheckInManual: + self.ctx.nav.get_frame(CheckInManual).clear_entries() + self.ctx.nav.get_frame(CheckInManual).update_entries(u_data.pid) return email_to_use = "" if len(u_data.emails) == 0 else u_data.emails[0] @@ -81,12 +80,12 @@ def _swipe_card(self, id_string): if email.endswith("@ucsd.edu"): email_to_use = email - manfill = self.ctx.nav.get_frame(ManualFill) + manfill = self.ctx.nav.get_frame(CreateAccountManual) manfill.clear_entries() logging.info(f"Filling data with {u_data.first_name} {u_data.last_name} {email_to_use} {u_data.pid}") manfill.update_entries(u_data.first_name, u_data.last_name, email_to_use, u_data.pid) - self.ctx.nav.show_frame(ManualFill) + self.ctx.nav.show_frame(CreateAccountManual) def _destroy_swipe_error(self, id_error): id_error.destroy() diff --git a/src/main.py b/src/main.py index d7bb907..cf15f5c 100644 --- a/src/main.py +++ b/src/main.py @@ -5,8 +5,8 @@ from controllers.account_controller import AccountController from controllers.card_reader_controller import CardReaderController from hardware.reader import Reader -from screens.manual_fill import ManualFill -from screens.check_in_no_id import CheckInNoId +from screens.create_account_manual import CreateAccountManual +from screens.check_in_manual import CheckInManual from hardware.render_ports import get_usb_ids from app_context import AppContext from api.sheets import check_api_health @@ -17,8 +17,8 @@ def clear_and_return(ctx: AppContext): ctx.nav.back_to_main() - ctx.nav.get_frame(ManualFill).clear_entries() - ctx.nav.get_frame(CheckInNoId).clear_entries() + ctx.nav.get_frame(CreateAccountManual).clear_entries() + ctx.nav.get_frame(CheckInManual).clear_entries() if __name__ == "__main__": @@ -27,6 +27,7 @@ def clear_and_return(ctx: AppContext): formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("-v", "--verbose", action="store_true", help="Increase verbosity (print debug info)") + parser.add_argument("-d", "--dev", action="store_true", help="Enable dev mode with on-screen navigation overlay") args = parser.parse_args() if args.verbose: @@ -34,11 +35,14 @@ def clear_and_return(ctx: AppContext): else: logging.basicConfig(level=logging.INFO) + import os + dev_mode = args.dev or os.environ.get("DEV_MODE") == "1" + usb = get_usb_ids() check_api_health() ctx = AppContext.create(usb.traffic_light) window = CheckInWindow() - nav = NavigationController(window, ctx) + nav = NavigationController(window, ctx, dev_mode=dev_mode) ctx.window = window ctx.nav = nav ctx.check_in = CheckInController(ctx) diff --git a/src/screens/acc_no_waiver.py b/src/screens/acc_no_waiver.py deleted file mode 100644 index 899da8d..0000000 --- a/src/screens/acc_no_waiver.py +++ /dev/null @@ -1,10 +0,0 @@ -from .screen import Screen - - -class AccNoWaiver(Screen): - def _build(self, controller): - self._text( - 169.0, 258.0, anchor="nw", - text="Looks like you haven't signed\n the waiver, let's solve that", - fill="#F5F0E6", font=("Montserrat", 64 * -1), - ) diff --git a/src/screens/acc_no_waiver_swipe.py b/src/screens/acc_no_waiver_swipe.py deleted file mode 100644 index 70ed8d0..0000000 --- a/src/screens/acc_no_waiver_swipe.py +++ /dev/null @@ -1,50 +0,0 @@ -from pathlib import Path -from tkinter import Button -from .screen import Screen -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "acc_no_waiver_swipe_assets" - - -class AccNoWaiverSwipe(Screen): - def _build(self, controller): - img2 = self._photo(ASSETS_PATH / "image_2.png") - self._image(1042.0, 359.0, image=img2) - - img3 = self._photo(ASSETS_PATH / "image_3.png") - self._image(408.0, 76.0, image=img3) - - img4 = self._photo(ASSETS_PATH / "image_4.png") - self._image(408.0, 429.0, image=img4) - - img5 = self._photo(ASSETS_PATH / "image_5.png") - self._image(395.0, 70.0, image=img5) - - img6 = self._photo(ASSETS_PATH / "image_6.png") - self._image(750.0, 70.0, image=img6) - - img7 = self._photo(ASSETS_PATH / "image_7.png") - self._image(1042.0, 328.0, image=img7) - - self._text( - 37.0, 45.0, anchor="nw", - text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 430.0, 45.0, anchor="nw", - text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 45.0, 270.0, anchor="nw", - text="Please scan the QR code\non the right and sign our \n waiver", - fill="#F5F0E6", font=("Montserrat", 48 * -1), - ) - - btn_img = self._photo(ASSETS_PATH / "button_1.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=self._back_to_main, relief="flat", - ) - self._window(875.0, 581.0, btn, width=344, height=71) - - def _back_to_main(self): - self.controller.back_to_main() diff --git a/src/screens/screen.py b/src/screens/base.py similarity index 79% rename from src/screens/screen.py rename to src/screens/base.py index 054629c..73a2b21 100644 --- a/src/screens/screen.py +++ b/src/screens/base.py @@ -11,10 +11,6 @@ def __init__(self, canvas, controller): self._build(controller) self.hide() - # ------------------------------------------------------------------ - # Helpers for subclasses - # ------------------------------------------------------------------ - def _photo(self, path): img = PhotoImage(file=str(path)) self._photos.append(img) @@ -30,8 +26,13 @@ def _text(self, x, y, **kwargs): self._items.append(item) return item + def _canvas_entry(self, x, y, w, h, font, fg="#F5F0E6"): + from .components.canvas_entry import CanvasEntry + entry = CanvasEntry(self.canvas, x, y, w, h, font, fg) + self._items.extend(entry.item_ids) + return entry + def _window(self, x, y, widget, width=None, height=None): - """Embed a tk widget into the canvas. x, y = top-left corner.""" kw = dict(anchor="nw", window=widget) if width is not None: kw["width"] = width @@ -41,10 +42,6 @@ def _window(self, x, y, widget, width=None, height=None): self._windows.append(item) return item - # ------------------------------------------------------------------ - # Visibility - # ------------------------------------------------------------------ - def show(self): for item in self._items: self.canvas.itemconfigure(item, state="normal") diff --git a/src/screens/check_in_no_id.py b/src/screens/check_in_manual.py similarity index 60% rename from src/screens/check_in_no_id.py rename to src/screens/check_in_manual.py index 135186d..e0df8d6 100644 --- a/src/screens/check_in_no_id.py +++ b/src/screens/check_in_manual.py @@ -1,37 +1,41 @@ from pathlib import Path -from tkinter import Button, Entry, StringVar, END -from .screen import Screen -import logging +from tkinter import Button, END +from .base import Screen +from .components.canvas_entry import CanvasEntry -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "check_in_no_id_assets" +ASSETS_PATH = Path(__file__).parent.parent / "assets" / "check_in_manual" +SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" -######################################################## -# This is the frame where users will manually check in # -######################################################## - -class CheckInNoId(Screen): +class CheckInManual(Screen): def _build(self, controller): self.loading_text_id = None - self.pid = StringVar() - img2 = self._photo(ASSETS_PATH / "image_2.png") - self._image(640.0, 360.0, image=img2) + logo = self._photo(SHARED_PATH / "button_generic.png") + self._image(88.0, 90.0, image=logo) + + home_img = self._photo(SHARED_PATH / "icon_home.png") + home_btn = Button( + self.canvas, image=home_img, bg="#153246", + command=lambda: controller.back_to_main(), + relief="flat", highlightthickness=0, bd=0, + ) + self._window(53.0, 55.0, home_btn) - img3 = self._photo(ASSETS_PATH / "image_3.png") - self._image(640.0, 424.0, image=img3) + field_img = self._photo(SHARED_PATH / "field.png") + self._image(640.0, 424.0, image=field_img) self._text( - 212.0, 120.0, anchor="nw", + 640.0, 206.0, anchor="center", text="If you have already made an\naccount, scan your UCSD barcode\nor enter your PID manually", fill="#F5F0E6", font=("Montserrat", 48 * -1), justify="center", ) self._text( - 605.0, 480.0, anchor="nw", + 640.0, 492.0, anchor="center", text="PID", fill="#F5F0E6", font=("Montserrat", 24 * -1), ) - btn_img = self._photo(ASSETS_PATH / "button_1.png") + btn_img = self._photo(ASSETS_PATH / "button_check_in.png") btn = Button( self.canvas, image=btn_img, borderwidth=0, highlightthickness=0, @@ -39,8 +43,13 @@ def _build(self, controller): ) self._window(465.0, 598.0, btn, width=349, height=71) - self.pid_entry = Entry(self.canvas, textvariable=self.pid, width=40, font=52) - self._window(420.0, 412.0, self.pid_entry) + self.pid_entry = self._canvas_entry( + 640.0, 424.0, w=800, h=44, font=("Montserrat", 20), + ) + + def hide(self): + CanvasEntry.blur_all() + super().hide() def display_loading(self): if self.loading_text_id is None: diff --git a/src/screens/main_page.py b/src/screens/check_in_rfid.py similarity index 73% rename from src/screens/main_page.py rename to src/screens/check_in_rfid.py index 85c79d2..cdd72ed 100644 --- a/src/screens/main_page.py +++ b/src/screens/check_in_rfid.py @@ -1,19 +1,20 @@ from pathlib import Path from tkinter import Button -from .screen import Screen +from .base import Screen from .qr_codes import QRCodes -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "main_page_assets" +ASSETS_PATH = Path(__file__).parent.parent / "assets" / "check_in_rfid" +SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" -class MainPage(Screen): +class CheckInRFID(Screen): def _build(self, controller): - logo = self._photo(ASSETS_PATH / "image_3.png") + logo = self._photo(SHARED_PATH / "button_generic.png") self._image(88.0, 90.0, image=logo) self._text( - 336.0, 602.0, anchor="nw", + 640.0, 618.0, anchor="center", text="Please tap ID on the black box to start", fill="#F5F0E6", font=("Montserrat", 32 * -1), ) @@ -28,7 +29,7 @@ def _build(self, controller): fill="#F5F0E6", font=("Montserrat", 73 * -1), ) - btn1_img = self._photo(ASSETS_PATH / "image_4.png") + btn1_img = self._photo(ASSETS_PATH / "icon_check_in.png") btn1 = Button( self.canvas, image=btn1_img, bg="#153246", command=lambda: controller.show_frame(QRCodes), @@ -39,11 +40,8 @@ def _build(self, controller): btn2 = Button( self.canvas, image=logo, text="No\nID", compound="center", bg="#153246", fg="white", - command=lambda: self._go_to_no_id(controller), + command=lambda: controller.go_to_no_id(), relief="flat", highlightthickness=0, bd=0, font=("Montserrat", 36 * -1), ) self._window(1130.0, 40.0, btn2) - - def _go_to_no_id(self, controller): - controller.go_to_no_id() diff --git a/src/screens/components/__init__.py b/src/screens/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/screens/components/canvas_entry.py b/src/screens/components/canvas_entry.py new file mode 100644 index 0000000..e46d0d7 --- /dev/null +++ b/src/screens/components/canvas_entry.py @@ -0,0 +1,73 @@ +import tkinter as tk + +_focused = None + + +class CanvasEntry: + def __init__(self, canvas, x, y, w, h, font, fg="#F5F0E6"): + self.canvas = canvas + self._x = x + self._y = y + + canvas.configure(insertbackground=fg, insertontime=600, insertofftime=400) + + self._hit_id = canvas.create_rectangle( + x - w / 2, y - h / 2, x + w / 2, y + h / 2, + fill="", outline="", state="hidden", + ) + self._text_id = canvas.create_text( + x, y, text="", fill=fg, font=font, + anchor="center", state="hidden", + ) + + canvas.tag_bind(self._hit_id, "", self._on_click) + canvas.tag_bind(self._text_id, "", self._on_click) + + @property + def item_ids(self): + return [self._hit_id, self._text_id] + + def _on_click(self, event=None): + global _focused + if _focused and _focused is not self: + _focused._blur() + _focused = self + self.canvas.focus_set() + self.canvas.focus(self._text_id) + self.canvas.bind("", _dispatch_key) + if event: + idx = self.canvas.index(self._text_id, f"@{event.x},{event.y}") + self.canvas.icursor(self._text_id, idx) + else: + self.canvas.icursor(self._text_id, tk.END) + + def _blur(self): + global _focused + if _focused is self: + _focused = None + self.canvas.focus("") + + @classmethod + def blur_all(cls): + global _focused + if _focused: + _focused._blur() + + def get(self): + return self.canvas.itemcget(self._text_id, "text") + + def delete(self, start, end=None): + self.canvas.dchars(self._text_id, 0, tk.END) + + def insert(self, index, text): + self.canvas.insert(self._text_id, index, text) + + +def _dispatch_key(event): + if _focused: + if event.keysym == "BackSpace": + idx = _focused.canvas.index(_focused._text_id, tk.INSERT) + if idx > 0: + _focused.canvas.dchars(_focused._text_id, idx - 1, idx - 1) + elif event.char and event.char.isprintable(): + _focused.canvas.insert(_focused._text_id, tk.INSERT, event.char) diff --git a/src/screens/create_account_barcode.py b/src/screens/create_account_barcode.py new file mode 100644 index 0000000..0aa7b01 --- /dev/null +++ b/src/screens/create_account_barcode.py @@ -0,0 +1,54 @@ +from pathlib import Path +from tkinter import Button +from .base import Screen + +ASSETS_PATH = Path(__file__).parent.parent / "assets" / "create_account_barcode" +SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" + + +class CreateAccountBarcode(Screen): + def _build(self, controller): + logo = self._photo(SHARED_PATH / "button_generic.png") + self._image(88.0, 90.0, image=logo) + + home_img = self._photo(SHARED_PATH / "icon_home.png") + home_btn = Button( + self.canvas, image=home_img, bg="#153246", + command=lambda: controller.back_to_main(), + relief="flat", highlightthickness=0, bd=0, + ) + self._window(53.0, 55.0, home_btn) + + outline1_img = self._photo(ASSETS_PATH / "outline_1.png") + self._image(640.0, 76.0, image=outline1_img) + + outline2_img = self._photo(ASSETS_PATH / "outline_2.png") + self._image(640.0, 430.0, image=outline2_img) + + icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") + self._image(576.0, 65.0, image=icon_unchecked) + + icon_checked = self._photo(SHARED_PATH / "icon_checked_box.png") + self._image(1030.0, 65.0, image=icon_checked) + + self._text( + 640.0, 374.0, anchor="center", + text="Please scan your ID barcode", + fill="#F5F0E6", font=("Montserrat", 48 * -1), + ) + self._text( + 215.0, 45.0, anchor="nw", + text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + self._text( + 690.0, 45.0, anchor="nw", + text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + + btn_img = self._photo(ASSETS_PATH / "button_fill_manually.png") + btn = Button( + self.canvas, image=btn_img, + borderwidth=0, highlightthickness=0, + command=lambda: controller.go_to_create_account_manual(), relief="flat", + ) + self._window(465.0, 554.0, btn, width=349, height=71) diff --git a/src/screens/create_account_manual.py b/src/screens/create_account_manual.py new file mode 100644 index 0000000..7151cb7 --- /dev/null +++ b/src/screens/create_account_manual.py @@ -0,0 +1,102 @@ +from pathlib import Path +from tkinter import Button, END +from .base import Screen +from .components.canvas_entry import CanvasEntry +import logging + +ASSETS_PATH = Path(__file__).parent.parent / "assets" / "create_account_manual" +SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" + + +class CreateAccountManual(Screen): + def _build(self, controller): + logo = self._photo(SHARED_PATH / "button_generic.png") + self._image(88.0, 90.0, image=logo) + + home_img = self._photo(SHARED_PATH / "icon_home.png") + home_btn = Button( + self.canvas, image=home_img, bg="#153246", + command=lambda: controller.back_to_main(), + relief="flat", highlightthickness=0, bd=0, + ) + self._window(53.0, 55.0, home_btn) + + outline1_img = self._photo(ASSETS_PATH / "outline_1.png") + self._image(640.0, 76.0, image=outline1_img) + + outline2_img = self._photo(ASSETS_PATH / "outline_2.png") + self._image(640.0, 430.0, image=outline2_img) + + icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") + self._image(605.0, 77.0, image=icon_unchecked) + self._image(1010.0, 77.0, image=icon_unchecked) + + field_img = self._photo(SHARED_PATH / "field.png") + self._image(640.0, 542.0, image=field_img) + self._image(640.0, 440.0, image=field_img) + self._image(640.0, 339.0, image=field_img) + self._image(640.0, 239.0, image=field_img) + + self._text( + 250.0, 45.0, anchor="nw", + text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + self._text( + 670.0, 45.0, anchor="nw", + text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + self._text( + 640.0, 189.0, anchor="center", + text="First Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + self._text( + 640.0, 290.0, anchor="center", + text="Last Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + self._text( + 640.0, 391.0, anchor="center", + text="Email", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + self._text( + 640.0, 492.0, anchor="center", + text="PID", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + + btn_img = self._photo(ASSETS_PATH / "register.png") + btn = Button( + self.canvas, image=btn_img, + borderwidth=0, highlightthickness=0, + command=self._call_account_creation, relief="flat", + ) + self._window(465.0, 598.0, btn, width=349, height=71) + + self.first_name_entry = self._canvas_entry(640.0, 239.0, w=800, h=44, font=("Montserrat", 20)) + self.last_name_entry = self._canvas_entry(640.0, 339.0, w=800, h=44, font=("Montserrat", 20)) + self.email_entry = self._canvas_entry(640.0, 440.0, w=800, h=44, font=("Montserrat", 20)) + self.pid_entry = self._canvas_entry(640.0, 542.0, w=800, h=44, font=("Montserrat", 20)) + + def hide(self): + CanvasEntry.blur_all() + super().hide() + + def clear_entries(self): + for entry in (self.first_name_entry, self.last_name_entry, + self.email_entry, self.pid_entry): + entry.delete(0, END) + + def update_entries(self, fname, lname, email, pid): + self.first_name_entry.insert(0, fname) + self.last_name_entry.insert(0, lname) + self.email_entry.insert(0, email) + self.pid_entry.insert(0, pid) + + def _call_account_creation(self): + first_name = self.first_name_entry.get() + last_name = self.last_name_entry.get() + email = self.email_entry.get() + pid = self.pid_entry.get() + self.clear_entries() + try: + self.controller.ctx.account.create_account(first_name, last_name, email, pid) + except Exception: + logging.warning("Error occurred trying to create a user account", exc_info=True) diff --git a/src/screens/dev_overlay.py b/src/screens/dev_overlay.py new file mode 100644 index 0000000..8254126 --- /dev/null +++ b/src/screens/dev_overlay.py @@ -0,0 +1,107 @@ +import tkinter as tk + +from screens.check_in_rfid import CheckInRFID +from screens.create_account_barcode import CreateAccountBarcode +from screens.create_account_manual import CreateAccountManual +from screens.sign_waiver import SignWaiver +from screens.check_in_manual import CheckInManual +from screens.qr_codes import QRCodes +from screens.user_welcome import UserWelcome + +_DEV_NAME = "Dev User" +_DEV_EMAIL = "devuser@ucsd.edu" +_DEV_PID = "A12345678" +_THANK_MSG = "Thank you for registering" + + +def _sim_no_account_success(nav): + def on_done(): + nav.ctx.traffic_light.request_green() + nav.get_frame(UserWelcome).display_name(_DEV_NAME, _THANK_MSG) + nav.go_to_create_account(on_done=on_done) + + +def _sim_no_account_needs_waiver(nav): + nav.go_to_create_account(on_done=nav.go_to_sign_waiver) + + +def _sim_fill_and_go(nav): + frm = nav.get_frame(CreateAccountManual) + frm.clear_entries() + frm.update_entries("Dev", "User", _DEV_EMAIL, _DEV_PID) + nav.go_to_create_account_manual() + + +TRANSITIONS = { + CheckInRFID: [ + ("QR Codes", lambda nav: nav.show_frame(QRCodes)), + ("No ID", lambda nav: nav.go_to_no_id()), + ("card: success", lambda nav: nav.get_frame(UserWelcome).display_name(_DEV_NAME)), + ("card: no account [→ success]", _sim_no_account_success), + ("card: no account [→ waiver]", _sim_no_account_needs_waiver), + ("card: no waiver", lambda nav: nav.go_to_sign_waiver()), + ], + QRCodes: [ + ("← Main", lambda nav: nav.back_to_main()), + ], + CheckInManual: [ + ("← Main", lambda nav: nav.back_to_main()), + ("PID: success", lambda nav: nav.get_frame(UserWelcome).display_name(_DEV_NAME)), + ("PID: no account [→ success]", _sim_no_account_success), + ("PID: no account [→ waiver]", _sim_no_account_needs_waiver), + ("PID: no waiver", lambda nav: nav.go_to_sign_waiver()), + ], + CreateAccountBarcode: [ + ("sim barcode swipe", _sim_fill_and_go), + ("manual fill", lambda nav: nav.go_to_create_account_manual()), + ("← Main", lambda nav: nav.back_to_main()), + ], + CreateAccountManual: [ + ("submit", lambda nav: nav.pop()), + ("← Main", lambda nav: nav.back_to_main()), + ], + SignWaiver: [ + ("← Main", lambda nav: nav.back_to_main()), + ], +} + + +class DevOverlay: + def __init__(self, canvas, nav): + self._canvas = canvas + self._nav = nav + self._buttons = [] + + self._frame = tk.Frame(canvas, bg="#1a1a2e", relief="solid", bd=1) + tk.Label( + self._frame, + text="DEV NAV", + bg="#1a1a2e", fg="#aaaaaa", + font=("Courier", 9, "bold"), + ).pack(pady=(4, 2), padx=6) + + self._canvas_window = canvas.create_window( + 1270, 715, anchor="se", window=self._frame, + ) + + def update(self, screen_class): + for btn in self._buttons: + btn.destroy() + self._buttons.clear() + + for label, action in TRANSITIONS.get(screen_class, []): + btn = tk.Label( + self._frame, + text=label, + bg="#2a2a4e", fg="white", + font=("Courier", 9), + padx=6, pady=3, + cursor="hand2", + ) + btn.pack(fill="x", padx=4, pady=1) + btn.bind("", lambda e, a=action: a(self._nav)) + btn.bind("", lambda e, w=btn: w.configure(bg="#4a4a8e")) + btn.bind("", lambda e, w=btn: w.configure(bg="#2a2a4e")) + self._buttons.append(btn) + + self._canvas.tag_raise(self._canvas_window) diff --git a/src/screens/manual_fill.py b/src/screens/manual_fill.py deleted file mode 100644 index a1145c9..0000000 --- a/src/screens/manual_fill.py +++ /dev/null @@ -1,110 +0,0 @@ -from pathlib import Path -from tkinter import Button, Entry, StringVar, END -from .screen import Screen -import logging - -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "manual_fill_assets" - - -####################################################### -# This is the frame where users will type information # -####################################################### - -class ManualFill(Screen): - def _build(self, controller): - self.first_name_var = StringVar() - self.last_name_var = StringVar() - self.email_var = StringVar() - self.pid_var = StringVar() - - img2 = self._photo(ASSETS_PATH / "image_2.png") - self._image(640.0, 76.0, image=img2) - - img3 = self._photo(ASSETS_PATH / "image_3.png") - self._image(640.0, 430.0, image=img3) - - img4 = self._photo(ASSETS_PATH / "image_4.png") - self._image(605.0, 77.0, image=img4) - - img5 = self._photo(ASSETS_PATH / "image_5.png") - self._image(1010.0, 77.0, image=img5) - - img6 = self._photo(ASSETS_PATH / "image_6.png") - self._image(640.0, 542.0, image=img6) - - img7 = self._photo(ASSETS_PATH / "image_7.png") - self._image(640.0, 440.0, image=img7) - - img8 = self._photo(ASSETS_PATH / "image_8.png") - self._image(640.0, 339.0, image=img8) - - img9 = self._photo(ASSETS_PATH / "image_9.png") - self._image(640.0, 239.0, image=img9) - - self._text( - 250.0, 45.0, anchor="nw", - text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 670.0, 45.0, anchor="nw", - text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 565.0, 177.0, anchor="nw", - text="First Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 565.0, 278.0, anchor="nw", - text="Last Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 595.0, 379.0, anchor="nw", - text="Email", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 605.0, 480.0, anchor="nw", - text="PID", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - - btn_img = self._photo(ASSETS_PATH / "button_1.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=self._call_account_creation, relief="flat", - ) - self._window(465.0, 598.0, btn, width=349, height=71) - - self.first_name_entry = Entry(self.canvas, textvariable=self.first_name_var, width=40, font=52) - self._window(420.0, 227.0, self.first_name_entry) - - self.last_name_entry = Entry(self.canvas, textvariable=self.last_name_var, width=40, font=52) - self._window(420.0, 327.0, self.last_name_entry) - - self.email_entry = Entry(self.canvas, textvariable=self.email_var, width=40, font=52) - self._window(420.0, 428.0, self.email_entry) - - self.pid_entry = Entry(self.canvas, textvariable=self.pid_var, width=40, font=52) - self._window(420.0, 530.0, self.pid_entry) - - def clear_entries(self): - self.first_name_entry.delete(0, END) - self.last_name_entry.delete(0, END) - self.email_entry.delete(0, END) - self.pid_entry.delete(0, END) - - def update_entries(self, fname, lname, email, pid): - self.first_name_entry.insert(0, fname) - self.last_name_entry.insert(0, lname) - self.email_entry.insert(0, email) - self.pid_entry.insert(0, pid) - - def _call_account_creation(self): - first_name, last_name, email, pid = ( - self.first_name_var.get(), self.last_name_var.get(), - self.email_var.get(), self.pid_var.get(), - ) - self.clear_entries() - try: - self.controller.ctx.account.create_account(first_name, last_name, email, pid) - except Exception: - logging.warning("Error occurred trying to create a user account", exc_info=True) diff --git a/src/screens/no_acc_check_in_only.py b/src/screens/no_acc_check_in_only.py deleted file mode 100644 index e9dde38..0000000 --- a/src/screens/no_acc_check_in_only.py +++ /dev/null @@ -1,10 +0,0 @@ -from .screen import Screen - - -class NoAccCheckInOnly(Screen): - def _build(self, controller): - self._text( - 160.0, 180.0, anchor="nw", - text="Looks like you don't have an\n account, please scan your ID\nat the main desk", - fill="#F5F0E6", font=("Montserrat", 64 * -1), - ) diff --git a/src/screens/no_acc_no_waiver.py b/src/screens/no_acc_no_waiver.py deleted file mode 100644 index 5377ebe..0000000 --- a/src/screens/no_acc_no_waiver.py +++ /dev/null @@ -1,10 +0,0 @@ -from .screen import Screen - - -class NoAccNoWaiver(Screen): - def _build(self, controller): - self._text( - 80.0, 180.0, anchor="nw", - text="Looks like your card isn't registered, \n let's set up your account.", - fill="#F5F0E6", font=("Montserrat", 64 * -1), - ) diff --git a/src/screens/no_acc_no_waiver_swipe.py b/src/screens/no_acc_no_waiver_swipe.py deleted file mode 100644 index 0deadc8..0000000 --- a/src/screens/no_acc_no_waiver_swipe.py +++ /dev/null @@ -1,44 +0,0 @@ -from pathlib import Path -from tkinter import Button -from .screen import Screen -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "no_acc_no_waiver_swipe_assets" - - -class NoAccNoWaiverSwipe(Screen): - def _build(self, controller): - img2 = self._photo(ASSETS_PATH / "image_2.png") - self._image(640.0, 76.0, image=img2) - - img3 = self._photo(ASSETS_PATH / "image_3.png") - self._image(640.0, 430.0, image=img3) - - img4 = self._photo(ASSETS_PATH / "image_4.png") - self._image(576.0, 65.0, image=img4) - - img5 = self._photo(ASSETS_PATH / "image_5.png") - self._image(1030.0, 65.0, image=img5) - - self._text( - 303.0, 350.0, anchor="nw", - text="Please scan your ID barcode", - fill="#F5F0E6", font=("Montserrat", 48 * -1), - ) - self._text( - 215.0, 45.0, anchor="nw", - text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 690.0, 45.0, anchor="nw", - text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - - btn_img = self._photo(ASSETS_PATH / "button_1.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=lambda: self._go_to_manual_fill(controller), relief="flat", - ) - self._window(465.0, 554.0, btn, width=349, height=71) - - def _go_to_manual_fill(self, controller): - controller.go_to_manual_fill() diff --git a/src/screens/qr_codes.py b/src/screens/qr_codes.py index a25d1e4..974ded5 100644 --- a/src/screens/qr_codes.py +++ b/src/screens/qr_codes.py @@ -1,31 +1,32 @@ from pathlib import Path from tkinter import Button -from .screen import Screen +from .base import Screen -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "qr_codes_assets" +ASSETS_PATH = Path(__file__).parent.parent / "assets" / "qr_codes" +SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" class QRCodes(Screen): def _build(self, controller): - img3 = self._photo(ASSETS_PATH / "image_3.png") - self._image(88.0, 90.0, image=img3) + logo = self._photo(SHARED_PATH / "button_generic.png") + self._image(88.0, 90.0, image=logo) - img4 = self._photo(ASSETS_PATH / "image_4.png") - self._image(421.0, 360.0, image=img4) + qr_website_img = self._photo(ASSETS_PATH / "qr_website.png") + self._image(421.0, 360.0, image=qr_website_img) - img5 = self._photo(ASSETS_PATH / "image_5.png") - self._image(859.0, 360.0, image=img5) + qr_waiver_img = self._photo(ASSETS_PATH / "qr_waiver.png") + self._image(859.0, 360.0, image=qr_waiver_img) self._text( - 335.0, 551.0, anchor="nw", + 421.0, 571.0, anchor="center", text="Website", fill="#F5F0E6", font=("Montserrat", 40 * -1), ) self._text( - 788.0, 557.0, anchor="nw", + 859.0, 571.0, anchor="center", text="Waiver", fill="#F5F0E6", font=("Montserrat", 40 * -1), ) - btn_img = self._photo(ASSETS_PATH / "image_6.png") + btn_img = self._photo(SHARED_PATH / "icon_home.png") btn = Button( self.canvas, image=btn_img, bg="#153246", command=lambda: controller.back_to_main(), diff --git a/src/screens/sign_waiver.py b/src/screens/sign_waiver.py new file mode 100644 index 0000000..3924b90 --- /dev/null +++ b/src/screens/sign_waiver.py @@ -0,0 +1,49 @@ +from pathlib import Path +from tkinter import Button +from .base import Screen + +ASSETS_PATH = Path(__file__).parent.parent / "assets" / "sign_waiver" +SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" + + +class SignWaiver(Screen): + def _build(self, controller): + outline1_img = self._photo(ASSETS_PATH / "outline_1.png") + self._image(1042.0, 359.0, image=outline1_img) + + outline2_img = self._photo(ASSETS_PATH / "outline_2.png") + self._image(408.0, 76.0, image=outline2_img) + + outline3_img = self._photo(ASSETS_PATH / "outline_3.png") + self._image(408.0, 429.0, image=outline3_img) + + icon_checked = self._photo(SHARED_PATH / "icon_checked_box.png") + self._image(395.0, 70.0, image=icon_checked) + + icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") + self._image(750.0, 70.0, image=icon_unchecked) + + qr_waiver_img = self._photo(ASSETS_PATH / "qr_waiver.png") + self._image(1042.0, 328.0, image=qr_waiver_img) + + self._text( + 37.0, 45.0, anchor="nw", + text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + self._text( + 430.0, 45.0, anchor="nw", + text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + self._text( + 45.0, 270.0, anchor="nw", + text="Please scan the QR code\non the right and sign our \n waiver", + fill="#F5F0E6", font=("Montserrat", 48 * -1), + ) + + btn_img = self._photo(ASSETS_PATH / "button_done_scanning.png") + btn = Button( + self.canvas, image=btn_img, + borderwidth=0, highlightthickness=0, + command=lambda: controller.back_to_main(), relief="flat", + ) + self._window(875.0, 581.0, btn, width=344, height=71) diff --git a/src/screens/transition_screen.py b/src/screens/transition_screen.py new file mode 100644 index 0000000..b6df9ac --- /dev/null +++ b/src/screens/transition_screen.py @@ -0,0 +1,15 @@ +from .base import Screen + + +class TransitionScreen(Screen): + def _build(self, controller): + self._msg_id = self._text( + 640, 340, anchor="center", + text="", + fill="#F5F0E6", font=("Montserrat", 64 * -1), + justify="center", + ) + + def display(self, message): + self.canvas.itemconfigure(self._msg_id, text=message) + self.controller.show_frame(TransitionScreen) diff --git a/src/screens/user_thank.py b/src/screens/user_thank.py deleted file mode 100644 index 00828ce..0000000 --- a/src/screens/user_thank.py +++ /dev/null @@ -1,37 +0,0 @@ -from .screen import Screen - - -class UserThank(Screen): - def _build(self, controller): - self._text( - 99.33203125, 259.33203125, anchor="nw", - text="Thank you for registering", - fill="#F5F0E6", font=("Montserrat", 45 * -1), - ) - self._text( - 429.0, 550.0, anchor="nw", - text="UCSD Makerspace", - fill="#F5F0E6", font=("Montserrat", 45 * -1), - ) - - def hide(self): - super().hide() - self.canvas.delete("thank") - - def display_name(self, name, nextPage): - self.controller.show_frame(UserThank) - self.controller.ctx.account.on_thank_start(nextPage) - - self.canvas.create_text( - 99.0, 323.0, anchor="nw", - text=name, - fill="#F5F0E6", - font=("Montserrat", 73 * -1), - tag="thank", - ) - - self.canvas.after(4500, lambda: self.canvas.delete("thank")) - self.controller.after(4000, lambda: self._go_to_next(nextPage)) - - def _go_to_next(self, nextPage): - self.controller.ctx.account.on_thank_done(nextPage) diff --git a/src/screens/user_welcome.py b/src/screens/user_welcome.py index d8d31c2..3804eb3 100644 --- a/src/screens/user_welcome.py +++ b/src/screens/user_welcome.py @@ -1,4 +1,4 @@ -from .screen import Screen +from .base import Screen class UserWelcome(Screen): @@ -6,7 +6,7 @@ def _build(self, controller): self.last_name = None self.offset = 0 - self._text( + self._msg_item = self._text( 99.33203125, 259.33203125, anchor="nw", text="Welcome back", fill="#F5F0E6", font=("Montserrat", 45 * -1), @@ -14,17 +14,17 @@ def _build(self, controller): def hide(self): super().hide() - # Clean up any dynamic name items when leaving this screen self.canvas.delete("welcome") + self.canvas.itemconfigure(self._msg_item, text="Welcome back") self.last_name = None self.offset = 0 - def display_name(self, name): + def display_name(self, name, message="Welcome back"): if name == self.last_name: return self.last_name = name - + self.canvas.itemconfigure(self._msg_item, text=message) self.controller.show_frame(UserWelcome) text_id = self.canvas.create_text( diff --git a/src/screens/waiver_no_acc.py b/src/screens/waiver_no_acc.py deleted file mode 100644 index a1029a6..0000000 --- a/src/screens/waiver_no_acc.py +++ /dev/null @@ -1,10 +0,0 @@ -from .screen import Screen - - -class WaiverNoAcc(Screen): - def _build(self, controller): - self._text( - 191.0, 258.0, anchor="nw", - text="Looks like you don't have an\n account, let's solve that", - fill="#F5F0E6", font=("Montserrat", 64 * -1), - ) diff --git a/src/screens/waiver_no_acc_swipe.py b/src/screens/waiver_no_acc_swipe.py deleted file mode 100644 index b449a28..0000000 --- a/src/screens/waiver_no_acc_swipe.py +++ /dev/null @@ -1,44 +0,0 @@ -from pathlib import Path -from tkinter import Button -from .screen import Screen -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "waiver_no_acc_swipe_assets" - - -class WaiverNoAccSwipe(Screen): - def _build(self, controller): - img2 = self._photo(ASSETS_PATH / "image_2.png") - self._image(640.0, 76.0, image=img2) - - img3 = self._photo(ASSETS_PATH / "image_3.png") - self._image(640.0, 430.0, image=img3) - - img4 = self._photo(ASSETS_PATH / "image_4.png") - self._image(576.0, 65.0, image=img4) - - img5 = self._photo(ASSETS_PATH / "image_5.png") - self._image(1030.0, 65.0, image=img5) - - self._text( - 420.0, 350.0, anchor="nw", - text="Please scan your ID barcode", - fill="#F5F0E6", font=("Montserrat", 48 * -1), - ) - self._text( - 215.0, 45.0, anchor="nw", - text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 690.0, 45.0, anchor="nw", - text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - - btn_img = self._photo(ASSETS_PATH / "button_1.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=lambda: self._go_to_manual_fill(controller), relief="flat", - ) - self._window(465.0, 554.0, btn, width=349, height=71) - - def _go_to_manual_fill(self, controller): - controller.go_to_manual_fill() diff --git a/src/window.py b/src/window.py index e8f4738..23be69a 100644 --- a/src/window.py +++ b/src/window.py @@ -22,11 +22,11 @@ def __init__(self): self.canvas.pack(fill="both", expand=True) self._bg_photos = [] - bg1 = tk.PhotoImage(file=str(ASSETS_PATH / "image_1.png")) + bg1 = tk.PhotoImage(file=str(ASSETS_PATH / "background_main.png")) self._bg_photos.append(bg1) self.canvas.create_image(640.0, 360.0, image=bg1) - bg2 = tk.PhotoImage(file=str(ASSETS_PATH / "image_2.png")) + bg2 = tk.PhotoImage(file=str(ASSETS_PATH / "outline_full.png")) self._bg_photos.append(bg2) self.canvas.create_image(639.333984375, 359.333984375, image=bg2) From 59aeb8c1db753d968aeee5a2a67a208769efbf1a Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 22:00:24 -0700 Subject: [PATCH 12/28] remove old files --- install.sh | 8 -------- test.py | 11 ----------- 2 files changed, 19 deletions(-) delete mode 100644 install.sh delete mode 100644 test.py diff --git a/install.sh b/install.sh deleted file mode 100644 index 1435e7e..0000000 --- a/install.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -echo "Attempting install..." -pip install tk -pip install pathlib -pip install requests -pip install datetime -pip install OAuth2 -echo "Installation complete" \ No newline at end of file diff --git a/test.py b/test.py deleted file mode 100644 index adadd57..0000000 --- a/test.py +++ /dev/null @@ -1,11 +0,0 @@ -from tkinter import Tk, Label - -root=Tk() - -def key_pressed(event): - print(event.char) - return - -root.bind("",key_pressed) - -root.mainloop() \ No newline at end of file From 3b6971ff198ce80c25bcf5ba2a55e179792103df Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 22:00:40 -0700 Subject: [PATCH 13/28] more refactoring --- src/api/traffic_light_api.py | 2 +- src/app_context.py | 2 +- src/controllers/navigation_controller.py | 2 +- ...ard_reader_controller.py => rfid_reader_controller.py} | 2 +- src/hardware/{reader.py => rfid_reader.py} | 0 src/hardware/{traffic.py => traffic_light.py} | 0 src/hardware/{render_ports.py => usb_ports.py} | 0 src/main.py | 8 ++++---- src/screens/{ => components}/dev_overlay.py | 0 9 files changed, 8 insertions(+), 8 deletions(-) rename src/controllers/{card_reader_controller.py => rfid_reader_controller.py} (99%) rename src/hardware/{reader.py => rfid_reader.py} (100%) rename src/hardware/{traffic.py => traffic_light.py} (100%) rename src/hardware/{render_ports.py => usb_ports.py} (100%) rename src/screens/{ => components}/dev_overlay.py (100%) diff --git a/src/api/traffic_light_api.py b/src/api/traffic_light_api.py index da94ce9..e571dd4 100644 --- a/src/api/traffic_light_api.py +++ b/src/api/traffic_light_api.py @@ -1,7 +1,7 @@ import threading from api.sheets import SheetManager -from hardware.traffic import TrafficLight +from hardware.traffic_light import TrafficLight class TrafficLightApi: diff --git a/src/app_context.py b/src/app_context.py index c8e6cfd..da66cb1 100644 --- a/src/app_context.py +++ b/src/app_context.py @@ -2,7 +2,7 @@ from api.sheets import SheetManager from api.traffic_light_api import TrafficLightApi -from hardware.traffic import TrafficLight +from hardware.traffic_light import TrafficLight class AppContext: diff --git a/src/controllers/navigation_controller.py b/src/controllers/navigation_controller.py index 1794f53..4c3b6f5 100644 --- a/src/controllers/navigation_controller.py +++ b/src/controllers/navigation_controller.py @@ -38,7 +38,7 @@ def __init__(self, window, ctx, dev_mode=False): self._frames[F] = F(window.canvas, self) if dev_mode: - from screens.dev_overlay import DevOverlay + from screens.components.dev_overlay import DevOverlay self._dev_overlay = DevOverlay(window.canvas, self) self.show_frame(CheckInRFID) diff --git a/src/controllers/card_reader_controller.py b/src/controllers/rfid_reader_controller.py similarity index 99% rename from src/controllers/card_reader_controller.py rename to src/controllers/rfid_reader_controller.py index 1d2c74a..4c562d9 100644 --- a/src/controllers/card_reader_controller.py +++ b/src/controllers/rfid_reader_controller.py @@ -6,7 +6,7 @@ from screens.create_account_manual import CreateAccountManual -class CardReaderController: +class RfidReaderController: def __init__(self, ctx): self.ctx = ctx self._no_wifi_shown = False diff --git a/src/hardware/reader.py b/src/hardware/rfid_reader.py similarity index 100% rename from src/hardware/reader.py rename to src/hardware/rfid_reader.py diff --git a/src/hardware/traffic.py b/src/hardware/traffic_light.py similarity index 100% rename from src/hardware/traffic.py rename to src/hardware/traffic_light.py diff --git a/src/hardware/render_ports.py b/src/hardware/usb_ports.py similarity index 100% rename from src/hardware/render_ports.py rename to src/hardware/usb_ports.py diff --git a/src/main.py b/src/main.py index cf15f5c..e91f1fd 100644 --- a/src/main.py +++ b/src/main.py @@ -3,11 +3,11 @@ from controllers.swipe_controller import SwipeController from controllers.check_in_controller import CheckInController from controllers.account_controller import AccountController -from controllers.card_reader_controller import CardReaderController -from hardware.reader import Reader +from controllers.rfid_reader_controller import RfidReaderController +from hardware.rfid_reader import Reader from screens.create_account_manual import CreateAccountManual from screens.check_in_manual import CheckInManual -from hardware.render_ports import get_usb_ids +from hardware.usb_ports import get_usb_ids from app_context import AppContext from api.sheets import check_api_health import logging @@ -51,7 +51,7 @@ def clear_and_return(ctx: AppContext): sw = SwipeController(ctx) reader = Reader(usb.reader) - card_reader = CardReaderController(ctx) + card_reader = RfidReaderController(ctx) card_reader.start(reader) window.bind("", lambda i: sw.keyboard_press(i)) diff --git a/src/screens/dev_overlay.py b/src/screens/components/dev_overlay.py similarity index 100% rename from src/screens/dev_overlay.py rename to src/screens/components/dev_overlay.py From 305faa45d26dd2048365ebfdf2732de00411d6c5 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 21 Mar 2026 23:21:20 -0700 Subject: [PATCH 14/28] refactor the api, re add barcode endpoint and simplify barcode account registration ui --- src/api/{sheets.py => client.py} | 22 +++--- src/api/get_info_from_pid.py | 70 ------------------ src/api/traffic_light_api.py | 4 +- src/app_context.py | 6 +- src/controllers/account_controller.py | 90 +++++------------------- src/controllers/navigation_controller.py | 1 + src/controllers/swipe_controller.py | 52 ++------------ src/main.py | 2 +- src/screens/components/dev_overlay.py | 5 +- src/screens/create_account_manual.py | 43 ++--------- 10 files changed, 46 insertions(+), 249 deletions(-) rename src/api/{sheets.py => client.py} (82%) delete mode 100644 src/api/get_info_from_pid.py diff --git a/src/api/sheets.py b/src/api/client.py similarity index 82% rename from src/api/sheets.py rename to src/api/client.py index 7bc3b67..dc81408 100644 --- a/src/api/sheets.py +++ b/src/api/client.py @@ -23,7 +23,7 @@ def check_api_health(retries=3, delay=3): sys.exit(1) -class SheetManager: +class ApiClient: def checkin_by_uuid(self, uuid): try: resp = _req("GET", f"{API_BASE_URL}/check-in/uuid/{uuid}", timeout=10) @@ -56,20 +56,14 @@ def get_traffic_light(self): logging.error(f"Error getting traffic light: {e}") return "off" - def create_account(self, first_name, last_name, email, pid, rfid): + def create_account(self, rfid, *, barcode=None, pid=None): try: - resp = _req( - "POST", - f"{API_BASE_URL}/accounts", - json={ - "first_name": first_name, - "last_name": last_name, - "email": email, - "pid": pid, - "rfid": rfid, - }, - timeout=30, - ) + payload = {"rfid": rfid} + if barcode: + payload["barcode"] = barcode + if pid: + payload["pid"] = pid + resp = _req("POST", f"{API_BASE_URL}/accounts", json=payload, timeout=30) resp.raise_for_status() return resp.json() except Exception as e: diff --git a/src/api/get_info_from_pid.py b/src/api/get_info_from_pid.py deleted file mode 100644 index af28c96..0000000 --- a/src/api/get_info_from_pid.py +++ /dev/null @@ -1,70 +0,0 @@ -import logging -from dataclasses import dataclass - -from config import API_BASE_URL -from api._client import _req - - -@dataclass -class StudentInfo: - first_name: str - last_name: str - emails: list - pid: str - first_enr_term: str - last_enr_term: str - - -class ContactClient: - def get_student_info(self, barcode): - try: - resp = _req("GET", f"{API_BASE_URL}/students/barcode/{barcode}", timeout=5) - if not resp.ok: - return False - d = resp.json() - return StudentInfo(d["first_name"], d["last_name"], d["emails"], d["pid"], d["first_enr_term"], d["last_enr_term"]) - except Exception as e: - logging.error(f"Error fetching student by barcode: {e}") - return False - - def get_student_info_pid(self, pid): - try: - resp = _req("GET", f"{API_BASE_URL}/students/pid/{pid}", timeout=5) - if not resp.ok: - return False - d = resp.json() - return StudentInfo(d["first_name"], d["last_name"], d["emails"], d["pid"], d["first_enr_term"], d["last_enr_term"]) - except Exception as e: - logging.error(f"Error fetching student by pid: {e}") - return False - - # TODO: I assume this was probably to add in support for employee checkin, when reimplemented - # TODO: it should be started with an implementation on the api side - # # not yet tested, still need to be authorized access to employeeData API. - # def get_staff_info(self, pid): - # if self.token["expires_at"] < time.time() + 60: - # self.token = self.oauth2_client.fetch_token( - # api_url + "token", grant_type="client_credentials" - # ) - # url = api_url + "employee_data/v1/employees/" + str(pid) - # token = self.token["access_token"] - # response = self.safe_get(url, token) - # if not response.ok: - # return False - # email = response.json()["officialEmail"] - # fname = response.json()["firstName"] - # lname = response.json()["lastName"] - # return [fname, lname, [email]] - # - # def safe_get(self, url, token, retries=2): - # for _ in range(retries): - # try: - # response = requests.get( - # url, headers={"Authorization": f"Bearer {token}"}, timeout=4 - # ) - # if response.ok: - # return response - # except (requests.exceptions.Timeout, requests.exceptions.ConnectionError): - # pass - # time.sleep(0.5) # small pause before retry - # return False diff --git a/src/api/traffic_light_api.py b/src/api/traffic_light_api.py index e571dd4..943bcc1 100644 --- a/src/api/traffic_light_api.py +++ b/src/api/traffic_light_api.py @@ -1,11 +1,11 @@ import threading -from api.sheets import SheetManager +from api.client import ApiClient from hardware.traffic_light import TrafficLight class TrafficLightApi: - def __init__(self, light: TrafficLight, sheets: SheetManager): + def __init__(self, light: TrafficLight, sheets: ApiClient): self._light = light self._sheets = sheets diff --git a/src/app_context.py b/src/app_context.py index da66cb1..904de3b 100644 --- a/src/app_context.py +++ b/src/app_context.py @@ -1,12 +1,12 @@ import threading -from api.sheets import SheetManager +from api.client import ApiClient from api.traffic_light_api import TrafficLightApi from hardware.traffic_light import TrafficLight class AppContext: - def __init__(self, sheets: SheetManager, traffic_light: TrafficLightApi): + def __init__(self, sheets: ApiClient, traffic_light: TrafficLightApi): self.sheets = sheets self.traffic_light = traffic_light self.window = None @@ -28,7 +28,7 @@ def rfid(self, value: str) -> None: @classmethod def create(cls, traffic_usb_id=None) -> "AppContext": - sheets = SheetManager() + sheets = ApiClient() light = TrafficLight(traffic_usb_id) traffic = TrafficLightApi(light, sheets) return cls(sheets, traffic) diff --git a/src/controllers/account_controller.py b/src/controllers/account_controller.py index e65b292..7fd9de2 100644 --- a/src/controllers/account_controller.py +++ b/src/controllers/account_controller.py @@ -1,4 +1,3 @@ -import time import tkinter import logging @@ -7,41 +6,14 @@ class AccountController: def __init__(self, ctx): self.ctx = ctx - def _email_check(self, email): - if "@" not in email or "." not in email: - return "Email is invalid" - return "good" + def create_account_from_barcode(self, barcode): + self._create(barcode=barcode) - def _name_check(self, fname, lname): - if len(fname) == 0 or len(lname) == 0: - return "Name was not entered" - return "good" - - def _id_check(self, user_id): - if len(user_id) <= 2 or len(user_id) > 12: - return "PID was not entered correctly" - return "good" - - def create_account(self, fname, lname, email, pid): - start = time.perf_counter() - idValid = self._id_check(pid) - emailValid = self._email_check(email) - nameValid = self._name_check(fname, lname) + def create_account_from_pid(self, pid): + self._create(pid=pid) + def _create(self, *, barcode=None, pid=None): canvas = self.ctx.window.canvas - - for validation in (idValid, emailValid, nameValid): - if validation != "good": - invalidID = tkinter.Label( - canvas, text=validation, bg="#153246", fg="white", font=("Arial", 20) - ) - invalidID.place(relx=0.5, rely=0.83, anchor="center") - invalidID.after(3000, lambda: invalidID.destroy()) - return - - end1 = time.perf_counter() - logging.debug(f"Time to validate info: {end1 - start}") - inProgress = tkinter.Label( canvas, text="Account creation in progress!", @@ -50,48 +22,18 @@ def create_account(self, fname, lname, email, pid): inProgress.place(relx=0.5, rely=0.87, anchor="center") self.ctx.window.update() - full_name = fname + " " + lname - logging.info(f"Creating user account for {full_name}") - - no_wifi = tkinter.Label( - canvas, - text="ERROR! Connection cannot be established, please let staff know.", - bg="#153246", fg="white", font=("Arial", 25), - ) - - end2 = time.perf_counter() - logging.debug(f"Time to structure row entries: {end2 - end1}") - - retries = 1 - while retries < 6: - try: - result = self.ctx.sheets.create_account(fname, lname, email, pid, self.ctx.rfid) - end3 = time.perf_counter() - logging.debug(f"Time to create account: {end3 - end2}") - - if result is None: - raise Exception("Account creation returned no result") - - break - except Exception as e: - logging.warning("Exception occurred while in account creation") - logging.exception("Exception occurred while in account creation") - no_wifi.place(relx=0.5, rely=0.91, anchor="center") - self.ctx.window.update() - time.sleep(retries) - retries += 1 - - no_wifi.destroy() + result = self.ctx.sheets.create_account(self.ctx.rfid, barcode=barcode, pid=pid) + inProgress.destroy() - if retries == 6: - self.ctx.nav.back_to_main() - inProgress.destroy() + if result is None: + error = tkinter.Label( + canvas, + text="ERROR! Could not create account, please try manually.", + bg="#153246", fg="white", font=("Arial", 20), + ) + error.place(relx=0.5, rely=0.87, anchor="center") + error.after(3000, lambda: error.destroy()) return - end4 = time.perf_counter() - logging.debug(f"Total time to send data: {end4 - end2}") - - inProgress.destroy() - # pop() runs the on_done continuation from the nav stack, - # which re-runs _run_check_in to handle waiver check and check-in. + logging.info("Account creation succeeded") self.ctx.nav.pop() diff --git a/src/controllers/navigation_controller.py b/src/controllers/navigation_controller.py index 4c3b6f5..669bd3b 100644 --- a/src/controllers/navigation_controller.py +++ b/src/controllers/navigation_controller.py @@ -96,6 +96,7 @@ def pop(self): def back_to_main(self): self._on_done_stack.clear() + self.ctx.rfid = "" self.ctx.traffic_light.request_off() self.show_frame(CheckInRFID) diff --git a/src/controllers/swipe_controller.py b/src/controllers/swipe_controller.py index 5d64be0..d3c26d6 100644 --- a/src/controllers/swipe_controller.py +++ b/src/controllers/swipe_controller.py @@ -1,9 +1,7 @@ import tkinter import logging -from screens.create_account_manual import CreateAccountManual from screens.create_account_barcode import CreateAccountBarcode -from screens.check_in_manual import CheckInManual -from api.get_info_from_pid import ContactClient +from screens.create_account_manual import CreateAccountManual class SwipeController: @@ -22,7 +20,7 @@ def _id_vet(self, id_check): def keyboard_press(self, key): curr_frame = self.ctx.nav.get_curr_frame() - if curr_frame not in (CreateAccountBarcode, CheckInManual): + if curr_frame not in (CreateAccountBarcode, CreateAccountManual): return self._id_string += key.char @@ -41,52 +39,10 @@ def keyboard_press(self, key): id_error.after(1500, lambda: self._destroy_swipe_error(id_error)) return - self._swipe_card(self._id_string) + logging.info(f"Card barcode read: {self._id_string.strip()!r}") + self.ctx.account.create_account_from_barcode(self._id_string.strip()) self._id_string = "" - def _pull_user(self, barcode, u_type): - logging.info(f"Card barcode read is: {barcode}. Trying to pull user...") - - contact = ContactClient() - try: - if u_type == "Staff": - u_info = contact.get_staff_info(barcode) - elif u_type == "Student": - u_info = contact.get_student_info(barcode) - except Exception: - logging.warning("An exception has ocurred with pulling user information", exc_info=True) - return None - - if not u_info: - logging.info("Student search returned False, returning...") - return None - - logging.info(f"Info pull succeeded:\n {u_info.first_name}, {u_info.last_name}, {u_info.pid}") - return u_info - - def _swipe_card(self, id_string): - u_data = self._pull_user(id_string.strip(), "Student") - if not u_data: - logging.info("Student search returned False, returning...") - return - - if self.ctx.nav.get_curr_frame() == CheckInManual: - self.ctx.nav.get_frame(CheckInManual).clear_entries() - self.ctx.nav.get_frame(CheckInManual).update_entries(u_data.pid) - return - - email_to_use = "" if len(u_data.emails) == 0 else u_data.emails[0] - for email in u_data.emails: - if email.endswith("@ucsd.edu"): - email_to_use = email - - manfill = self.ctx.nav.get_frame(CreateAccountManual) - manfill.clear_entries() - logging.info(f"Filling data with {u_data.first_name} {u_data.last_name} {email_to_use} {u_data.pid}") - manfill.update_entries(u_data.first_name, u_data.last_name, email_to_use, u_data.pid) - - self.ctx.nav.show_frame(CreateAccountManual) - def _destroy_swipe_error(self, id_error): id_error.destroy() self._swipe_error_shown = False diff --git a/src/main.py b/src/main.py index e91f1fd..70f0fc4 100644 --- a/src/main.py +++ b/src/main.py @@ -9,7 +9,7 @@ from screens.check_in_manual import CheckInManual from hardware.usb_ports import get_usb_ids from app_context import AppContext -from api.sheets import check_api_health +from api.client import check_api_health import logging import argparse from sys import stdout diff --git a/src/screens/components/dev_overlay.py b/src/screens/components/dev_overlay.py index 8254126..fbc827c 100644 --- a/src/screens/components/dev_overlay.py +++ b/src/screens/components/dev_overlay.py @@ -11,10 +11,12 @@ _DEV_NAME = "Dev User" _DEV_EMAIL = "devuser@ucsd.edu" _DEV_PID = "A12345678" +_DEV_RFID = "1a2b3c4d5e6f7g" _THANK_MSG = "Thank you for registering" def _sim_no_account_success(nav): + nav.ctx.rfid = _DEV_RFID def on_done(): nav.ctx.traffic_light.request_green() nav.get_frame(UserWelcome).display_name(_DEV_NAME, _THANK_MSG) @@ -22,13 +24,14 @@ def on_done(): def _sim_no_account_needs_waiver(nav): + nav.ctx.rfid = _DEV_RFID nav.go_to_create_account(on_done=nav.go_to_sign_waiver) def _sim_fill_and_go(nav): frm = nav.get_frame(CreateAccountManual) frm.clear_entries() - frm.update_entries("Dev", "User", _DEV_EMAIL, _DEV_PID) + frm.pid_entry.insert(0, _DEV_PID) nav.go_to_create_account_manual() diff --git a/src/screens/create_account_manual.py b/src/screens/create_account_manual.py index 7151cb7..7e5e45a 100644 --- a/src/screens/create_account_manual.py +++ b/src/screens/create_account_manual.py @@ -32,10 +32,7 @@ def _build(self, controller): self._image(1010.0, 77.0, image=icon_unchecked) field_img = self._photo(SHARED_PATH / "field.png") - self._image(640.0, 542.0, image=field_img) - self._image(640.0, 440.0, image=field_img) - self._image(640.0, 339.0, image=field_img) - self._image(640.0, 239.0, image=field_img) + self._image(640.0, 390.0, image=field_img) self._text( 250.0, 45.0, anchor="nw", @@ -46,19 +43,7 @@ def _build(self, controller): text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), ) self._text( - 640.0, 189.0, anchor="center", - text="First Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 640.0, 290.0, anchor="center", - text="Last Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 640.0, 391.0, anchor="center", - text="Email", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 640.0, 492.0, anchor="center", + 640.0, 340.0, anchor="center", text="PID", fill="#F5F0E6", font=("Montserrat", 24 * -1), ) @@ -68,35 +53,21 @@ def _build(self, controller): borderwidth=0, highlightthickness=0, command=self._call_account_creation, relief="flat", ) - self._window(465.0, 598.0, btn, width=349, height=71) + self._window(465.0, 490.0, btn, width=349, height=71) - self.first_name_entry = self._canvas_entry(640.0, 239.0, w=800, h=44, font=("Montserrat", 20)) - self.last_name_entry = self._canvas_entry(640.0, 339.0, w=800, h=44, font=("Montserrat", 20)) - self.email_entry = self._canvas_entry(640.0, 440.0, w=800, h=44, font=("Montserrat", 20)) - self.pid_entry = self._canvas_entry(640.0, 542.0, w=800, h=44, font=("Montserrat", 20)) + self.pid_entry = self._canvas_entry(640.0, 390.0, w=800, h=44, font=("Montserrat", 20)) def hide(self): CanvasEntry.blur_all() super().hide() def clear_entries(self): - for entry in (self.first_name_entry, self.last_name_entry, - self.email_entry, self.pid_entry): - entry.delete(0, END) - - def update_entries(self, fname, lname, email, pid): - self.first_name_entry.insert(0, fname) - self.last_name_entry.insert(0, lname) - self.email_entry.insert(0, email) - self.pid_entry.insert(0, pid) + self.pid_entry.delete(0, END) def _call_account_creation(self): - first_name = self.first_name_entry.get() - last_name = self.last_name_entry.get() - email = self.email_entry.get() - pid = self.pid_entry.get() + pid = self.pid_entry.get() self.clear_entries() try: - self.controller.ctx.account.create_account(first_name, last_name, email, pid) + self.controller.ctx.account.create_account_from_pid(pid) except Exception: logging.warning("Error occurred trying to create a user account", exc_info=True) From a8bea41d9818ec7b10d1f9c66617577bb3896133 Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 17:10:22 -0700 Subject: [PATCH 15/28] barcode scanner --- .github/workflows/build.yml | 18 ++- src/api/client.py | 32 ++++- src/controllers/account_controller.py | 77 +++++++++++- src/controllers/barcode_scanner_controller.py | 56 +++++++++ src/controllers/navigation_controller.py | 19 +++ src/controllers/swipe_controller.py | 48 -------- src/hardware/barcode_scanner.py | 43 +++++++ src/hardware/usb_ports.py | 10 +- src/main.py | 16 ++- src/screens/base.py | 21 +++- src/screens/components/canvas_entry.py | 7 ++ src/screens/components/dev_overlay.py | 11 ++ src/screens/create_account_manual.py | 24 ++-- src/screens/create_account_no_pid.py | 90 ++++++++++++++ src/screens/create_account_review.py | 111 ++++++++++++++++++ 15 files changed, 515 insertions(+), 68 deletions(-) create mode 100644 src/controllers/barcode_scanner_controller.py delete mode 100644 src/controllers/swipe_controller.py create mode 100644 src/hardware/barcode_scanner.py create mode 100644 src/screens/create_account_no_pid.py create mode 100644 src/screens/create_account_review.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d70c9c..442775b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,10 +4,11 @@ on: push: branches: - main + - dev workflow_dispatch: concurrency: - group: production_environment + group: ${{ github.ref_name }}_environment cancel-in-progress: true jobs: @@ -23,6 +24,17 @@ jobs: id: lowercase run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + - name: Set image name + id: image + run: | + if [ "${{ github.ref_name }}" = "dev" ]; then + echo "name=check-in-dev" >> $GITHUB_OUTPUT + echo "extra_tag=ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in-dev:dev" >> $GITHUB_OUTPUT + else + echo "name=check-in" >> $GITHUB_OUTPUT + echo "extra_tag=ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in:latest" >> $GITHUB_OUTPUT + fi + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -39,5 +51,5 @@ jobs: context: . push: true tags: | - ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in:${{ github.sha }} - ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in:latest + ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.image.outputs.name }}:${{ github.sha }} + ${{ steps.image.outputs.extra_tag }} diff --git a/src/api/client.py b/src/api/client.py index dc81408..b411861 100644 --- a/src/api/client.py +++ b/src/api/client.py @@ -56,13 +56,43 @@ def get_traffic_light(self): logging.error(f"Error getting traffic light: {e}") return "off" - def create_account(self, rfid, *, barcode=None, pid=None): + def lookup_by_pid(self, pid): + """Returns dict with first_name/last_name/email/pid, or None if not found.""" + try: + resp = _req("GET", f"{API_BASE_URL}/accounts/lookup/pid/{pid}", timeout=10) + if resp.status_code == 404: + return None + resp.raise_for_status() + return resp.json() + except Exception as e: + logging.error(f"Error looking up student by pid {pid}: {e}") + return None + + def lookup_by_barcode(self, barcode): + """Returns dict with first_name/last_name/email/pid, or None if not found.""" + try: + resp = _req("GET", f"{API_BASE_URL}/accounts/lookup/barcode/{barcode}", timeout=10) + if resp.status_code == 404: + return None + resp.raise_for_status() + return resp.json() + except Exception as e: + logging.error(f"Error looking up student by barcode: {e}") + return None + + def create_account(self, rfid, *, barcode=None, pid=None, first_name=None, last_name=None, email=None): try: payload = {"rfid": rfid} if barcode: payload["barcode"] = barcode if pid: payload["pid"] = pid + if first_name: + payload["first_name"] = first_name + if last_name: + payload["last_name"] = last_name + if email: + payload["email"] = email resp = _req("POST", f"{API_BASE_URL}/accounts", json=payload, timeout=30) resp.raise_for_status() return resp.json() diff --git a/src/controllers/account_controller.py b/src/controllers/account_controller.py index 7fd9de2..8d3fb23 100644 --- a/src/controllers/account_controller.py +++ b/src/controllers/account_controller.py @@ -9,10 +9,76 @@ def __init__(self, ctx): def create_account_from_barcode(self, barcode): self._create(barcode=barcode) + def go_to_review_from_barcode(self, barcode): + canvas = self.ctx.window.canvas + loading = tkinter.Label( + canvas, + text="Looking up student...", + bg="#153246", fg="white", font=("Arial", 25), + ) + loading.place(relx=0.5, rely=0.87, anchor="center") + self.ctx.window.update() + + student = self.ctx.sheets.lookup_by_barcode(barcode) + loading.destroy() + + if student is None: + error = tkinter.Label( + canvas, + text="Student not found. Please enter your details manually.", + bg="#153246", fg="white", font=("Arial", 20), + ) + error.place(relx=0.5, rely=0.87, anchor="center") + error.after(3000, error.destroy) + return + + self.ctx.nav.go_to_create_account_review( + pid=student["pid"], + first_name=student["first_name"], + last_name=student["last_name"], + email=student["email"], + ) + def create_account_from_pid(self, pid): self._create(pid=pid) - def _create(self, *, barcode=None, pid=None): + def go_to_review_from_pid(self, pid): + canvas = self.ctx.window.canvas + loading = tkinter.Label( + canvas, + text="Looking up student...", + bg="#153246", fg="white", font=("Arial", 25), + ) + loading.place(relx=0.5, rely=0.87, anchor="center") + self.ctx.window.update() + + student = self.ctx.sheets.lookup_by_pid(pid) + loading.destroy() + + if student is None: + error = tkinter.Label( + canvas, + text="Student not found. Please check your PID.", + bg="#153246", fg="white", font=("Arial", 20), + ) + error.place(relx=0.5, rely=0.87, anchor="center") + error.after(3000, error.destroy) + return + + self.ctx.nav.go_to_create_account_review( + pid=pid, + first_name=student["first_name"], + last_name=student["last_name"], + email=student["email"], + ) + + def create_account_from_review(self, *, first_name, last_name, email, pid): + if pid: + self._create(pid=pid) + else: + self._create(first_name=first_name, last_name=last_name, email=email) + + def _create(self, *, barcode=None, pid=None, first_name=None, last_name=None, email=None): canvas = self.ctx.window.canvas inProgress = tkinter.Label( canvas, @@ -22,7 +88,14 @@ def _create(self, *, barcode=None, pid=None): inProgress.place(relx=0.5, rely=0.87, anchor="center") self.ctx.window.update() - result = self.ctx.sheets.create_account(self.ctx.rfid, barcode=barcode, pid=pid) + result = self.ctx.sheets.create_account( + self.ctx.rfid, + barcode=barcode, + pid=pid, + first_name=first_name, + last_name=last_name, + email=email, + ) inProgress.destroy() if result is None: diff --git a/src/controllers/barcode_scanner_controller.py b/src/controllers/barcode_scanner_controller.py new file mode 100644 index 0000000..03a770c --- /dev/null +++ b/src/controllers/barcode_scanner_controller.py @@ -0,0 +1,56 @@ +import logging +import time +from threading import Thread + +from screens.check_in_manual import CheckInManual +from screens.create_account_barcode import CreateAccountBarcode +from screens.create_account_manual import CreateAccountManual + + +class BarcodeScannerController: + def __init__(self, ctx): + self.ctx = ctx + + def start(self, scanner): + thread = Thread(target=self._run, args=(scanner,), daemon=True) + thread.start() + + def _run(self, scanner): + logging.info("Now reading barcodes") + scanner_error = False + try: + while True: + if scanner_error: + time.sleep(0.5) + if scanner.reconnect(): + logging.info("Barcode scanner reconnected") + scanner_error = False + continue + + try: + barcode = scanner.read_barcode() + except OSError as e: + logging.error("Barcode scanner disconnected: %s", e) + scanner_error = True + continue + + if barcode is None: + continue + + logging.debug("Raw barcode received: %r", barcode) + + if not scanner.is_valid(barcode): + logging.warning("Invalid barcode rejected: %r", barcode) + continue + + logging.info("Barcode scanned: %r", barcode) + curr_frame = self.ctx.nav.get_curr_frame() + + if curr_frame == CheckInManual: + self.ctx.window.after(0, lambda b=barcode: self.ctx.check_in.handle_by_pid(b)) + elif curr_frame in (CreateAccountBarcode, CreateAccountManual): + self.ctx.window.after(0, lambda b=barcode: self.ctx.account.go_to_review_from_barcode(b)) + else: + logging.debug("Barcode scanned on unhandled screen: %s", curr_frame) + except Exception as e: + logging.exception("Barcode scanner thread crashed: %s", e) diff --git a/src/controllers/navigation_controller.py b/src/controllers/navigation_controller.py index 669bd3b..8376755 100644 --- a/src/controllers/navigation_controller.py +++ b/src/controllers/navigation_controller.py @@ -4,6 +4,8 @@ from screens.transition_screen import TransitionScreen from screens.create_account_barcode import CreateAccountBarcode from screens.create_account_manual import CreateAccountManual +from screens.create_account_no_pid import CreateAccountNoPid +from screens.create_account_review import CreateAccountReview from screens.sign_waiver import SignWaiver from screens.check_in_manual import CheckInManual from screens.qr_codes import QRCodes @@ -30,6 +32,8 @@ def __init__(self, window, ctx, dev_mode=False): TransitionScreen, CreateAccountBarcode, CreateAccountManual, + CreateAccountNoPid, + CreateAccountReview, SignWaiver, CheckInManual, QRCodes, @@ -108,6 +112,21 @@ def go_to_create_account_manual(self): self.get_frame(CreateAccountManual).clear_entries() self.show_frame(CreateAccountManual) + def go_to_create_account_no_pid(self): + self.get_frame(CreateAccountNoPid).clear_entries() + self.show_frame(CreateAccountNoPid) + + def go_to_create_account_review(self, pid="", first_name="", last_name="", email=""): + pid_locked = bool(pid) + self.get_frame(CreateAccountReview).setup( + first_name=first_name, + last_name=last_name, + email=email, + pid=pid, + pid_locked=pid_locked, + ) + self.show_frame(CreateAccountReview) + def go_to_create_account(self, on_done): self.get_frame(TransitionScreen).display( "Looks like you don't have an account,\nlet's set one up!" diff --git a/src/controllers/swipe_controller.py b/src/controllers/swipe_controller.py deleted file mode 100644 index d3c26d6..0000000 --- a/src/controllers/swipe_controller.py +++ /dev/null @@ -1,48 +0,0 @@ -import tkinter -import logging -from screens.create_account_barcode import CreateAccountBarcode -from screens.create_account_manual import CreateAccountManual - - -class SwipeController: - def __init__(self, ctx): - self.ctx = ctx - self._id_string = "" - self._swipe_error_shown = False - - def _id_vet(self, id_check): - if any(i.isalpha() for i in id_check): - return "bad" - if len(id_check) >= 16: - return "bad" - return "good" - - def keyboard_press(self, key): - curr_frame = self.ctx.nav.get_curr_frame() - - if curr_frame not in (CreateAccountBarcode, CreateAccountManual): - return - - self._id_string += key.char - logging.debug("The array is now: " + repr(str(self._id_string))) - - if self._id_string.endswith("\r"): - if self._id_vet(self._id_string) == "bad": - self._id_string = "" - if not self._swipe_error_shown: - self._swipe_error_shown = True - id_error = tkinter.Label( - self.ctx.window.canvas, text="Error, please scan again", - bg="#153246", fg="white", font=("Arial", 20), - ) - id_error.place(relx=0.5, rely=0.85, anchor="center") - id_error.after(1500, lambda: self._destroy_swipe_error(id_error)) - return - - logging.info(f"Card barcode read: {self._id_string.strip()!r}") - self.ctx.account.create_account_from_barcode(self._id_string.strip()) - self._id_string = "" - - def _destroy_swipe_error(self, id_error): - id_error.destroy() - self._swipe_error_shown = False diff --git a/src/hardware/barcode_scanner.py b/src/hardware/barcode_scanner.py new file mode 100644 index 0000000..f376ace --- /dev/null +++ b/src/hardware/barcode_scanner.py @@ -0,0 +1,43 @@ +import serial +import logging +from os.path import exists + + +class BarcodeScanner: + def __init__(self, usb_id): + self._usb_id = usb_id + self._ser = None + self._connect() + + def _connect(self): + self._ser = serial.Serial(self._usb_id, baudrate=9600, timeout=0.1) + self._ser.reset_input_buffer() + logging.info("Barcode scanner connected at %s", self._usb_id) + + def reconnect(self): + if not exists(self._usb_id): + return False + try: + self._connect() + return True + except Exception: + self._ser = None + return False + + def read_barcode(self): + """Read one barcode from the scanner. Returns stripped string or None.""" + # Use read_until(\r) to handle scanners that terminate with CR only + line = self._ser.read_until(b"\r") + if not line: + return None + barcode = line.decode("ascii", errors="ignore").strip() + # Strip Codabar start/stop characters (A, B, C, D) from both ends + barcode = barcode.strip("ABCDabcd") + return barcode if barcode else None + + def is_valid(self, barcode): + if not barcode: + return False + if len(barcode) > 32: + return False + return True diff --git a/src/hardware/usb_ports.py b/src/hardware/usb_ports.py index 2ad7cca..ce624af 100644 --- a/src/hardware/usb_ports.py +++ b/src/hardware/usb_ports.py @@ -1,24 +1,32 @@ +import logging from dataclasses import dataclass import serial.tools.list_ports SHARED_VID = 0x1A86 TRAFFIC_LOCATION = "1-1.1.2" +BARCODE_VID = 0x9901 @dataclass class UsbIds: reader: str | None traffic_light: str | None + barcode: str | None def get_usb_ids() -> UsbIds: reader = None traffic_light = None + barcode = None for p in serial.tools.list_ports.comports(): + logging.debug("USB port: %s vid=%s desc=%s", p.device, hex(p.vid) if p.vid else None, p.description) if p.vid == SHARED_VID: if p.location == TRAFFIC_LOCATION: traffic_light = p.device else: reader = p.device - return UsbIds(reader, traffic_light) + elif p.vid == BARCODE_VID: + barcode = p.device + logging.info("USB detected — reader: %s, traffic_light: %s, barcode: %s", reader, traffic_light, barcode) + return UsbIds(reader, traffic_light, barcode) diff --git a/src/main.py b/src/main.py index 70f0fc4..9532df2 100644 --- a/src/main.py +++ b/src/main.py @@ -1,11 +1,14 @@ from window import CheckInWindow from controllers.navigation_controller import NavigationController -from controllers.swipe_controller import SwipeController +from controllers.barcode_scanner_controller import BarcodeScannerController +from hardware.barcode_scanner import BarcodeScanner from controllers.check_in_controller import CheckInController from controllers.account_controller import AccountController from controllers.rfid_reader_controller import RfidReaderController from hardware.rfid_reader import Reader from screens.create_account_manual import CreateAccountManual +from screens.create_account_no_pid import CreateAccountNoPid +from screens.create_account_review import CreateAccountReview from screens.check_in_manual import CheckInManual from hardware.usb_ports import get_usb_ids from app_context import AppContext @@ -18,6 +21,8 @@ def clear_and_return(ctx: AppContext): ctx.nav.back_to_main() ctx.nav.get_frame(CreateAccountManual).clear_entries() + ctx.nav.get_frame(CreateAccountNoPid).clear_entries() + ctx.nav.get_frame(CreateAccountReview).clear_entries() ctx.nav.get_frame(CheckInManual).clear_entries() @@ -49,12 +54,17 @@ def clear_and_return(ctx: AppContext): ctx.account = AccountController(ctx) ctx.traffic_light.request_off() - sw = SwipeController(ctx) reader = Reader(usb.reader) card_reader = RfidReaderController(ctx) card_reader.start(reader) - window.bind("", lambda i: sw.keyboard_press(i)) + if usb.barcode: + barcode_scanner = BarcodeScanner(usb.barcode) + barcode_controller = BarcodeScannerController(ctx) + barcode_controller.start(barcode_scanner) + else: + logging.warning("No barcode scanner found, barcode scanning disabled") + window.bind("", lambda i: clear_and_return(ctx)) logging.info("Made it to app start") window.start() diff --git a/src/screens/base.py b/src/screens/base.py index 73a2b21..4d2215b 100644 --- a/src/screens/base.py +++ b/src/screens/base.py @@ -1,4 +1,4 @@ -from tkinter import PhotoImage +from tkinter import PhotoImage, Canvas class Screen: @@ -26,6 +26,25 @@ def _text(self, x, y, **kwargs): self._items.append(item) return item + def _rounded_button(self, text, w, h, r, bg, fg, font, command, parent_bg="#153246"): + """A Canvas-based button with rounded corners.""" + c = Canvas(self.canvas, width=w, height=h, bg=parent_bg, highlightthickness=0) + c.create_polygon( + r, 0, w - r, 0, + w, 0, w, r, + w, h - r, w, h, + w - r, h, r, h, + 0, h, 0, h - r, + 0, r, 0, 0, + smooth=True, fill=bg, outline=bg, + ) + c.create_text(w // 2, h // 2, text=text, fill=fg, font=font) + c.bind("", lambda e: command()) + for item in c.find_all(): + c.tag_bind(item, "", lambda e: command()) + c.configure(cursor="hand2") + return c + def _canvas_entry(self, x, y, w, h, font, fg="#F5F0E6"): from .components.canvas_entry import CanvasEntry entry = CanvasEntry(self.canvas, x, y, w, h, font, fg) diff --git a/src/screens/components/canvas_entry.py b/src/screens/components/canvas_entry.py index e46d0d7..b295008 100644 --- a/src/screens/components/canvas_entry.py +++ b/src/screens/components/canvas_entry.py @@ -28,6 +28,8 @@ def item_ids(self): return [self._hit_id, self._text_id] def _on_click(self, event=None): + if getattr(self, '_readonly', False): + return global _focused if _focused and _focused is not self: _focused._blur() @@ -62,6 +64,11 @@ def delete(self, start, end=None): def insert(self, index, text): self.canvas.insert(self._text_id, index, text) + def set_readonly(self, readonly: bool): + self._readonly = readonly + color = "#C8C0B0" if readonly else "#F5F0E6" + self.canvas.itemconfigure(self._text_id, fill=color) + def _dispatch_key(event): if _focused: diff --git a/src/screens/components/dev_overlay.py b/src/screens/components/dev_overlay.py index fbc827c..ee81074 100644 --- a/src/screens/components/dev_overlay.py +++ b/src/screens/components/dev_overlay.py @@ -3,6 +3,8 @@ from screens.check_in_rfid import CheckInRFID from screens.create_account_barcode import CreateAccountBarcode from screens.create_account_manual import CreateAccountManual +from screens.create_account_no_pid import CreateAccountNoPid +from screens.create_account_review import CreateAccountReview from screens.sign_waiver import SignWaiver from screens.check_in_manual import CheckInManual from screens.qr_codes import QRCodes @@ -60,6 +62,15 @@ def _sim_fill_and_go(nav): ("← Main", lambda nav: nav.back_to_main()), ], CreateAccountManual: [ + ("→ review (pid lookup)", lambda nav: nav.ctx.account.go_to_review_from_pid(_DEV_PID)), + ("→ no-pid screen", lambda nav: nav.go_to_create_account_no_pid()), + ("← Main", lambda nav: nav.back_to_main()), + ], + CreateAccountNoPid: [ + ("submit", lambda nav: nav.pop()), + ("← Main", lambda nav: nav.back_to_main()), + ], + CreateAccountReview: [ ("submit", lambda nav: nav.pop()), ("← Main", lambda nav: nav.back_to_main()), ], diff --git a/src/screens/create_account_manual.py b/src/screens/create_account_manual.py index 7e5e45a..5e57e15 100644 --- a/src/screens/create_account_manual.py +++ b/src/screens/create_account_manual.py @@ -1,5 +1,5 @@ from pathlib import Path -from tkinter import Button, END +from tkinter import Button, Canvas, END from .base import Screen from .components.canvas_entry import CanvasEntry import logging @@ -51,9 +51,18 @@ def _build(self, controller): btn = Button( self.canvas, image=btn_img, borderwidth=0, highlightthickness=0, - command=self._call_account_creation, relief="flat", + command=self._go_to_review, relief="flat", ) - self._window(465.0, 490.0, btn, width=349, height=71) + self._window(465.0, 460.0, btn, width=349, height=71) + + no_pid_btn = self._rounded_button( + text="I don't have a PID →", + w=349, h=80, r=20, + bg="#F5F0E6", fg="#4EBEEE", + font=("Montserrat", 20), + command=lambda: controller.go_to_create_account_no_pid(), + ) + self._window(465.0, 545.0, no_pid_btn, width=349, height=80) self.pid_entry = self._canvas_entry(640.0, 390.0, w=800, h=44, font=("Montserrat", 20)) @@ -64,10 +73,7 @@ def hide(self): def clear_entries(self): self.pid_entry.delete(0, END) - def _call_account_creation(self): - pid = self.pid_entry.get() + def _go_to_review(self): + pid = self.pid_entry.get().strip() self.clear_entries() - try: - self.controller.ctx.account.create_account_from_pid(pid) - except Exception: - logging.warning("Error occurred trying to create a user account", exc_info=True) + self.controller.ctx.account.go_to_review_from_pid(pid) diff --git a/src/screens/create_account_no_pid.py b/src/screens/create_account_no_pid.py new file mode 100644 index 0000000..1e7160e --- /dev/null +++ b/src/screens/create_account_no_pid.py @@ -0,0 +1,90 @@ +from pathlib import Path +from tkinter import Button, END +from .base import Screen +from .components.canvas_entry import CanvasEntry +import logging + +ASSETS_PATH = Path(__file__).parent.parent / "assets" / "create_account_manual" +SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" + + +class CreateAccountNoPid(Screen): + def _build(self, controller): + logo = self._photo(SHARED_PATH / "button_generic.png") + self._image(88.0, 90.0, image=logo) + + home_img = self._photo(SHARED_PATH / "icon_home.png") + home_btn = Button( + self.canvas, image=home_img, bg="#153246", + command=lambda: controller.back_to_main(), + relief="flat", highlightthickness=0, bd=0, + ) + self._window(53.0, 55.0, home_btn) + + outline1_img = self._photo(ASSETS_PATH / "outline_1.png") + self._image(640.0, 76.0, image=outline1_img) + + outline2_img = self._photo(ASSETS_PATH / "outline_2.png") + self._image(640.0, 430.0, image=outline2_img) + + icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") + self._image(605.0, 77.0, image=icon_unchecked) + self._image(1010.0, 77.0, image=icon_unchecked) + + field_img = self._photo(SHARED_PATH / "field.png") + self._image(640.0, 289.0, image=field_img) + self._image(640.0, 390.0, image=field_img) + self._image(640.0, 490.0, image=field_img) + + self._text( + 250.0, 45.0, anchor="nw", + text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + self._text( + 670.0, 45.0, anchor="nw", + text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + self._text( + 640.0, 240.0, anchor="center", + text="First Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + self._text( + 640.0, 341.0, anchor="center", + text="Last Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + self._text( + 640.0, 441.0, anchor="center", + text="Email", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + + btn_img = self._photo(ASSETS_PATH / "register.png") + btn = Button( + self.canvas, image=btn_img, + borderwidth=0, highlightthickness=0, + command=self._submit, relief="flat", + ) + self._window(465.0, 545.0, btn, width=349, height=71) + + self.first_name_entry = self._canvas_entry(640.0, 289.0, w=800, h=44, font=("Montserrat", 20)) + self.last_name_entry = self._canvas_entry(640.0, 390.0, w=800, h=44, font=("Montserrat", 20)) + self.email_entry = self._canvas_entry(640.0, 490.0, w=800, h=44, font=("Montserrat", 20)) + + def hide(self): + CanvasEntry.blur_all() + super().hide() + + def clear_entries(self): + for entry in (self.first_name_entry, self.last_name_entry, self.email_entry): + entry.delete(0, END) + + def _submit(self): + first = self.first_name_entry.get().strip() + last = self.last_name_entry.get().strip() + email = self.email_entry.get().strip() + self.clear_entries() + try: + self.controller.ctx.account.create_account_from_review( + first_name=first, last_name=last, email=email, pid="" + ) + except Exception: + logging.warning("Error occurred trying to create a user account", exc_info=True) diff --git a/src/screens/create_account_review.py b/src/screens/create_account_review.py new file mode 100644 index 0000000..05849ff --- /dev/null +++ b/src/screens/create_account_review.py @@ -0,0 +1,111 @@ +from pathlib import Path +from tkinter import Button, END +from .base import Screen +from .components.canvas_entry import CanvasEntry +import logging + +ASSETS_PATH = Path(__file__).parent.parent / "assets" / "create_account_manual" +SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" + + +class CreateAccountReview(Screen): + def _build(self, controller): + logo = self._photo(SHARED_PATH / "button_generic.png") + self._image(88.0, 90.0, image=logo) + + home_img = self._photo(SHARED_PATH / "icon_home.png") + home_btn = Button( + self.canvas, image=home_img, bg="#153246", + command=lambda: controller.back_to_main(), + relief="flat", highlightthickness=0, bd=0, + ) + self._window(53.0, 55.0, home_btn) + + outline1_img = self._photo(ASSETS_PATH / "outline_1.png") + self._image(640.0, 76.0, image=outline1_img) + + outline2_img = self._photo(ASSETS_PATH / "outline_2.png") + self._image(640.0, 430.0, image=outline2_img) + + icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") + self._image(605.0, 77.0, image=icon_unchecked) + self._image(1010.0, 77.0, image=icon_unchecked) + + field_img = self._photo(SHARED_PATH / "field.png") + self._image(640.0, 239.0, image=field_img) + self._image(640.0, 339.0, image=field_img) + self._image(640.0, 440.0, image=field_img) + self._image(640.0, 542.0, image=field_img) + + self._text( + 250.0, 45.0, anchor="nw", + text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + self._text( + 670.0, 45.0, anchor="nw", + text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + ) + self._text( + 640.0, 189.0, anchor="center", + text="First Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + self._text( + 640.0, 290.0, anchor="center", + text="Last Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + self._text( + 640.0, 391.0, anchor="center", + text="Email", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + self._text( + 640.0, 492.0, anchor="center", + text="PID", fill="#F5F0E6", font=("Montserrat", 24 * -1), + ) + + btn_img = self._photo(ASSETS_PATH / "register.png") + btn = Button( + self.canvas, image=btn_img, + borderwidth=0, highlightthickness=0, + command=self._submit, relief="flat", + ) + self._window(465.0, 598.0, btn, width=349, height=71) + + self.first_name_entry = self._canvas_entry(640.0, 239.0, w=800, h=44, font=("Montserrat", 20)) + self.last_name_entry = self._canvas_entry(640.0, 339.0, w=800, h=44, font=("Montserrat", 20)) + self.email_entry = self._canvas_entry(640.0, 440.0, w=800, h=44, font=("Montserrat", 20)) + self.pid_entry = self._canvas_entry(640.0, 542.0, w=800, h=44, font=("Montserrat", 20)) + + def setup(self, first_name="", last_name="", email="", pid="", pid_locked=False): + self.clear_entries() + if first_name: + self.first_name_entry.insert(0, first_name) + if last_name: + self.last_name_entry.insert(0, last_name) + if email: + self.email_entry.insert(0, email) + if pid: + self.pid_entry.insert(0, pid) + self.pid_entry.set_readonly(pid_locked) + + def hide(self): + CanvasEntry.blur_all() + super().hide() + + def clear_entries(self): + for entry in (self.first_name_entry, self.last_name_entry, + self.email_entry, self.pid_entry): + entry.delete(0, END) + self.pid_entry.set_readonly(False) + + def _submit(self): + first = self.first_name_entry.get().strip() + last = self.last_name_entry.get().strip() + email = self.email_entry.get().strip() + pid = self.pid_entry.get().strip() + self.clear_entries() + try: + self.controller.ctx.account.create_account_from_review( + first_name=first, last_name=last, email=email, pid=pid + ) + except Exception: + logging.warning("Error occurred trying to create a user account", exc_info=True) From adcc50abff8256d2975ee89f54f70d672696b5fb Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 20:07:01 -0700 Subject: [PATCH 16/28] replace tkinter with pyqt6 --- fonts/Montserrat-VariableFont_wght.ttf | Bin 0 -> 688600 bytes requirements.txt | 2 + src/app_context.py | 1 + src/controllers/account_controller.py | 73 +++------ src/controllers/barcode_scanner_controller.py | 8 +- src/controllers/check_in_controller.py | 19 +-- src/controllers/navigation_controller.py | 51 +++++-- src/controllers/rfid_reader_controller.py | 30 ++-- src/dispatcher.py | 9 ++ src/main.py | 26 +++- src/screens/base.py | 82 ++--------- src/screens/check_in_manual.py | 119 +++++++-------- src/screens/check_in_rfid.py | 90 +++++++----- src/screens/components/dev_overlay.py | 102 ++++++++----- src/screens/components/outline_frame.py | 30 ++++ src/screens/components/styled_button.py | 82 +++++++++++ src/screens/components/styled_entry.py | 30 ++++ src/screens/components/theme.py | 7 + src/screens/create_account_barcode.py | 77 +++++----- src/screens/create_account_manual.py | 114 +++++++-------- src/screens/create_account_no_pid.py | 120 +++++++-------- src/screens/create_account_review.py | 138 ++++++++---------- src/screens/qr_codes.py | 78 ++++++---- src/screens/sign_waiver.py | 87 +++++++---- src/screens/transition_screen.py | 32 +++- src/screens/user_welcome.py | 96 +++++++----- src/window.py | 87 +++++++---- 27 files changed, 926 insertions(+), 664 deletions(-) create mode 100644 fonts/Montserrat-VariableFont_wght.ttf create mode 100644 src/dispatcher.py create mode 100644 src/screens/components/outline_frame.py create mode 100644 src/screens/components/styled_button.py create mode 100644 src/screens/components/styled_entry.py create mode 100644 src/screens/components/theme.py diff --git a/fonts/Montserrat-VariableFont_wght.ttf b/fonts/Montserrat-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..451e69288c17eb9823f8e96f1a2b9621094d2668 GIT binary patch literal 688600 zcmeFZbwE_l_b_~B?(W{*rMqF5r5h9lMFA-hm6ot52?J2EMN!PPyF0L5Ozg()kKGuc zVqox5QF+eY1>^Je`+J_x`#$e~@1DKqPMn!JbLLFmnE?U-#)x=O6c!v3x+|ag3ZVW0 zK%+%iRAlV1q68Zd%s33pTjQ|U_%?|~n>GQ=$N*?9Y1=6IMA z&b9y){UJ0m*4=C9>~79z$1so*8y6V&>D~1dOiYOOvebd8{dP=!h>4R=X!lC*lbWxs zF4&E6c>rkkPDlSi5o-<5e>VDCqz^2RXlw9u02p<&56{fc>QiB|G#S8QF@VE~KjBjC9?;)89xBc>fv`44v$yrE0(q}|GFg}xkNotziBQ-;I z{RXB-U68(6HaZA9YWGIsztC=z-KSvilS^t>0kWn8IR4c;FFkeNC~wTa5Av&D-6wT$ zzJ|Z{Xr$kP^pf1vK0U5X*U1DBWaA^x&+A{1k{*=~aB&Q>KP10jk9@a-E0G!AVGOel z2$1@E(84!bK@+r=LjVZ)3_~b&=d(Qa^Jh2o;{3$}pa3b8Km-(X=ne~!YeuUW%MoKi z37o3uW15!Vw`7=zYL2^cML0Ld~Ba0>K+ zrk4Yd5BzaqL;yEz3thTv`F#X6RlIj=VH4;5xY?{e-{s$1VtV({DnQOuKUG>nw43)Y z=$FSi4$jUhz?YI&P*=umju=Cj)dnb^0L&0la1RmbYFXl&NAn>AFke(o6VV=_dXsM8 zMVg`g$8R~%`T(#Q*60x5K2XACvyQ6d!iy&-BtYhI{*+Jm<4XbzeTL8ehQ}HhfckHZ zPkrn9Z=9dX4*Cds2tTpP<)7{vNTbz2t@9(4Cg?VVG^QSy6Fyn5lz=womY|Oa#DlW3 z3ox(1Jq&7#QdDXxJ5eVZXgY;%nnVk+3qIOJ2Qlg;(It9_Kj5QJI9*0b5kq2xCd?-> zCMFo81Wl&IOcgi5+KcHD;7SM;8N`TffO0%=MVC{M+M8G?Ig@}3_&{5TgB0iugJBH1 z;*Y2)c~zM;IL;c}wK(25sR1QA9Cu1ttLXGN9vkQkl;x>%H~NLfgyXb<#!M*(25ppZ z+Msj~sP}8yGTzrd!^+3L-m1A>y;U>YdMj_+FIG)$a9Gj>#aPb^;YiE z5-T@Jy_Ks~y_Jh)y_K_NhLw{=y_KUyoRx!ly_M9Y-b!LxZ)JsVOT&6A3!{1~b9|fm znbdbS_A{*SY|5F8IFo@%fMI>IUWsnJPKkEBZjyGAMu~bnzl0aBp2SPi;#u+hdEI#< zd53rpc&t{5X1toEIG(i>#FG+;*G%%zoUTd5NrEJafIJisNYa8lmOJM2)Wo7iVQ`&c;|;cH{zT0^wl`0t%jb zgPDLO^SZ%IL6dpdU}nIJ+0tO<{X)YBQ)YI9y8zt>HkgHA&BQgBMZcKUFdnWhbLp!8 zV%8wmsO&0tO=w2WG?=wPpX_Qd>wu6n@?i4Q9o1BwtW?nuG3cURE8`(*Kk143Pu{S$ z>XYFOG@9T6&U_*Ef?s}MmPYq^$u!idmW&`X2${Wmv zUVTm#Fw_7H`@8v1xs5^UETrY~-0#0gq&>!# zg(Xk`KTUs5`)B@tQvS&;5YsDANegxKedP*M7*mD#re(BcTm+5{N}pCdM^Lne!g#mxEy>$Qh>_jWJ}P z%K(fm16}%|xd3_NXk$^6+T%O2A^l%t=JNhC1{b92qzv63-MKQ5fG2XsJ(6EI`#s%8 zZci{d);D%2g%RL72uMUgr3B)UT%te}k4I%ziey$BU&ixOW_+rc+O89ANiLtiF z{I&d}F8qw^f3L-jlFo%7jYG=Nw5{$Re~dJhP>fMxzw+S8;fxEGNt|H z5BoQzUG~F1RcUYc!|uh|$xpkT(%$EXzd2`TTz}XNl;J=Au$w6TML+C4l;L$6?Z`F9 z-vP8!n1wuA;v<2kh?`-#5o}-65NDvK5v)gp5f8<@6CBr#M?4Wq5pRcch%dlr#C1d; z2#zML5KDDg3@b+T(qXW(40$g{zd!IA&7_5 zv53c0Op8vWlMqj#C5YG4t%ytMA;d@N8N}!4W5iFHHb9tQrU&9|<{;w3yeRD1qj_;a zdGWlVXdcePICEk?YIw;b_G9>&Q#!aIfd4DTl5+q??IPxy&I_{sck zh*SBHA>xsIjEyhn??Zfm{}JWWh$noK{f5ZozIe?-T4t zd{BUcIKffDF~sG9+lcQ9?je34c!;<{@Dy>Spc?UO!5hRif*Qo{1@94m6#R|&v*0V@ z?}G0@SYUBx#|l^t#9A!Q5m_Bp2eBS&hS-92K$oihB$-GM4Zj$Anwf;As)>ZBOc3+Lp*`Sa$%>ia}Y0J7a(55EQ9EXsl1!yZ9=j4eleian3`3X9TXZ?Y(P_6~ao@qP9Y z;>YZB#IM=6h~KgGK!ij{5etNRhz*1Wh>e6sh%JSdh^0a)Vq2j-Vn?A9VizIiUFadi zT#I-jl$A&z5+K$Tp{ztw5z>nsMUIGlMZSphM0tn{MTNkK#)xJjo-Mk8_?B1`7_pAn z46#g%BQmj**a@+-*awKXg&5^2ZY^$&xUCpVU)){X9dVjC197GpB`NML?t{3WxIfkx zf}!LPDpLbYZ8KG0^kvvnP!K5j z1d&2?0Ar?NHPAv1b-+YyBen%oC5PrnC;Ach)`qy9MdhM$a8ahxpl(F9Q`N)I>CC_f$cSCNBa7Lz4arG!(@O}dN9czX1l|9 zFOYk|J#>}wCJB4I-$?1+Rt z(XciKHpIfgSUAxMb|k>=1UQlaXA|ILS2&dl6=_76L&W(^49Y+$hk5gkA3uKc!p;jf zFNa)?yi#{{#r0J;PTf3y^XAR!TZ-EYZZE&R?e_M28}IGBclDn5{`UKO?|*o-w_8aR<(Y$*Q+hBuD!ne`o`;9ukXIO`KIpe z-M3HP{{5D|HGSK;W>w9-n)@{sHBV|PYiervHKH1EjarR*jmf(;@2+$~4`-=BZ-amc+?Y-c``45jjJo)hS!?O>SAF4jod=PwKKOXv6`SHca_aC)t6}87} zFVx!+`EUyZ+A{r2eF`){AVeg5|4Tiv&> z-@bn{{ATj~`uD2uU%uCU|N5PL7k}6K9$x>nzOMdj{kQt>_4W0%UQo~0i|Z{&HF-lm zk#D4)5JCw<1VoK!5N-OBA}f@Kt3d)Prdrg5N~j$TVcs(DnRCN?r}GmiL)g$NSFX^H`n`8z3=Hji=7j+tpY27DvF3Ez@0<;(bXd<@IYH_&GdSQFNgm9RE!xR57gg(9I? zs3Fu6>Iij(dP04nq0m%lj_s(G&_h%$`X=IIODPs3gpQmhU&)Zrs`(u=IR#emg*99YjqoS zTMe;>hK81gwuXs@sfL+`g@&bum4=Omorbd}(G+UxYMNnlepWO?yoT zO=nG4&1RZDn%%W(v}(1)T3TBAS_WE1TIO1oT2d`*Et!^$mV=g~maA4vEq|?k+G1@3 zZ6j?fZK<|Q+eX_~+fLhF+dOFdgXJ3V_n2R#=(S3PgN zw)z$NmHM^%e0{OLzP^dRrM`{6t-gc4qrR)Yo4$v>r@ohdbNy)j7z0ZKD+8&4wSmmQ z#=ybA$-v#f!@$$f$WUr%V`y*aVCZS+W!S{9siC)FGeaMv8Y5jJBO_xY2O~!#cOws@ zrbgaI&5W8G`4|Nl3yh78O^jv6&c-grp2l9r&5eDGeT`d~d@|vi=$lxYNK9lV4kpef zt|ne4J|?~Sx;8G{7{_G{UsK zX^iP`Gea{oGg~tsGe5IdX06Tq&4SF@n?;*@n0uMGFmG+{ZysPCXdYr7ZeecWXAx)- zWD#u9*0RE~+OpPC-_p|3)3Sx7pQXQLkY%uCh-Ii{m}R(SJIjui@s!v{M_6~T?r0rv-AQIClgS)pUb5!0P+7PvQWhnPk@c}@W)ox+ zX4B3l#wOM#*(TRE%r@FK)~=&nq+OI%dMwdp4(veWcRM_neM&a^V|n{bn{5@$oA;tk?WD?nc^S;eT`1JHC@EPno zxMfbu!2vx31_k$x3eCvCew*vD$?*#}D=MpNYU{q%6CPh66p1yo^i51H9bCQq8ff6= zy^6}`)iv)ve60QZOWoIR^@Q^HtXNkc$&|f4n7zdnd3*veQv7kfAUCSf=T?N2Z=NQ% z#+-~67Vv8fXy7`6FN}aqq?k-5v&jmwmaHdx$VY5bWwZ%xj&p%%8b_087n(vdX>Xc` zGlBth1TCV)bTXYvXVN)zK3zf;^azgq@6l@dntsJLScB1H3>Y&;!Z{k}?3C=49FQE6>PZcy)>1pvOE+n-G)~%0I!3y{npz92wXAim z4XjPAEv=oc+ghhs_psSzbIEpv?RMMUwg+sF*q*dKV|&j2xl5)?o=g7{(-QlVz>+B? zvr0CWY$@4Oa;oHf$=!{ybrio0aX$@^;M}?euJLUlPU+WM;{_lFcQhB_~VHmfS|! z;#W7}cHtTv1#^1+%kTT%Uo2aHnAlFxa;|!ZKK9Rit2C-6 zp=oB7@|%~6uBvC1s7j1OJuiUDn#%8$mX)TQPh}T;BJjblnw374E|sEZFEPcuC!HSS zw@=mY@rK9i9(e*DngiQkN~@>}x*_-%i9{Er4c=7gVy z&t(2l{&9Xe|D0f#U_Z{yfAB9TS4AK|ZW@1ry__rig|iFS3AYHh39)>B{)Af^&BAq@ zNw`+H38@sq9gXhjwyDvKzDOl3`}wGRgxeb4T*z%d!(vQ(g$IR)h2_Fi!ZX4v!oP&K zg?Afc)x!MD2wKMA}#q zwzwM4eZA|2Ydu3@94x|bOC_)y4&Vw;EqsOVL`;luMJI$rkXX`*qzfMkj|(4(;)FNF zt~8kDU@JWW*Jw)Vc3hXaNS|Wc@e$h%b!?GCmkE0L=xP-G_36FZCa1q$J7QLM;7?1SUYDB%L(Lb02ut4LRPNbD<|FDMnh z5%w463k!q?@jC>?uMQH>!5U_btBOguR@@QSJ7?kw)nu3oVQ>cS;|kRksDWFew(y2D zA-2SkxR3<+!enAU6GA)C4D4~Z-ew?Oiml)*x`^JUm+4>hI-QK8c^AeUdli4iUKGx> zV#YCpnc>VxW&;z>Yywl-71sbm!HjkTH`*VX(816YSFk+j0BA`^LTfr2y3iHSj!uIF zx){3C)%b0CI`pGukdJk`fbNC(ATyh^WTp~- zW+v&tEF+1`Mv_D_m@<;Z>?1vxJxn&u2S;$Bec?4Uqr-6BFc5<2L|miJf)wb3Yb^sv zD9pnzO{?iU`icGx`=OjMg3Iubaf8oHTarQ~On+icyqO6kfV3rD83pNy>v!4ElPUz; z1v>=W1Us=$3&l0Sb+DZwa1c&1h9IKtVG_Lxml!MJzzidv%oyUz6cKl3H1S}HaaV~h zjR)@Au?@|J!SpcnrMqDTeGS!23wXt}gdDn+R^nJBgvue0?tl}FK3LFHkkKr7%d`e< z8Vkm>GsM${(3viWDfAl5rgz~cV+WOtH=#@n5ikkFnHfR+m}x|ZNh9V=E)1bZppc$` z({P>%0zKLZ+S3_un{j{-xJG%0afG{!6Y0)u11o4w3tnH{tTj)nR#eslsIOH1fx`hxDGJDEs&0mnZN@k6ga!_dz-_cWk^j0Ka- zbYccF1DGMqEM_6ISg=R1R~#e`5c`W;;p$X~I9MDgZX@;(HxY-4J;h$)rs7bMy4YLn zB5sCjSk1-mBB4l4G)XiS*P#}RR*2?{CgR%BTG2w$Own9XiQuH*l;Ex4u%HT8C|?L( z3aSN91zjuJCvau+wBU^3tl*sBqTmkBSnmt&aVwmHCxXYg z#`&CE+ho(&bX;ZZ!S=%S#XL5j?aTIK2e6}XopB6X%&jxBa&{uFG)`k@vvb%!Y%V*O z&19$Is^fHa20N3T#7<^su>;xuY!N$NP{Ym>ykqAJ-m?pEfnuTH0$YGP~8!`iZESv&Te;3%%i+Oy|b2lfK%$X;Zf*h{Q4dzp2?Ra95@ zD(lAnCHT&+!c~B+tS5T|SA1^@orI1|I~pXqF1jYVAv!Di3)lP#!IGwd9qkGBxB~Bl z>+fE45ctz#2%ux24X(cX(IRL?M?p8b3cAvj5Jl%eCfx`f=uGHMx4|G>i62Ez!)RQK zFQTVl1TBZ*^f)Y}6)=t7faUZRtfFsW4Sf#^`W;GXJ?vm8Y^HTk#;~x55yD%5* z%u>>kSxzFE6(ovTN#dDxB#tQ|$;@Vw$?PTB%zl!?9H8f!d}a-^opxt%>B4L!otZ798&gWsn4Q9F!mBt^xGt;~z7kdm zUkG0cpNlL+mN<@>C)h3W6nTr>MINH2B7ac}T-j+NauT%``H7l|nu}WED!G=>iw+lM ziBd)Bq8_3gQBP5}C{2_h$`r+ml0=W7g40BqiBF=h-jc_sA!OAm}szQIB;7@ zcH;LT5lv_Ezy$Vzl*BNrg*HN&&|2sK#q=z!r5{8hkp}6-9D=dnP78=3lZD@!(qN;w zEs?n*DjrtlM)j;#mB`)M@4q*&_1GFc-yd02P@FiPjB={ z!YD8lhQ%l%Vr3E0$%&E>d5SU=y2SlvQ~CRFF)IBUObQwlm#7GJMt7x;${wb)BcaOL z_8;Eijovbe0;1&dj8b51F`59gQld2QgC-?mPLgDbG-sJqme>QMDiwmdG%h6wP3ny% zA_>D3BxTx=hEIBetc*06k`on@l+2{INDb6hq5Q{|fwye1%ABH*q)Q|UfvqepDp4*~ zkQA9&gFPk@B}P)s74f-XPNk9>xbfugt=g5&rI15A&ESYUosH8V53jjrt(wC=nP}-+S}Ou#6FJ%xGt_x$Qc|$13#VO@L|G3rSyHLKzC1sqR9ibJLLP*Qj=H8) zu+mh4U5c|jQNgJkr)VZ`Ym}QnT_KkBXroY*wfW)W5B@420q4WY+9-&jO6no95Q%Y)JYANC z8W<3jm}Qokl!|dH0%WNQo~(^oDG%CU?J_2qrI1qS=!|)bKpl*9j_Qn+loN(TE)SNJ z2Jq}s(^ENnuoUaPyunKr9L!bP#t1vGLh<6H(5I_Ib^mS$2Gp??PMVtNWnd+843oqP%6p5jewlW zK$NoG_^(75#SbD0mZ>swxpqrVZ#WzJD{W}Ozg&^^yEXfVH59#38F;pCDqT_V>^RL! zlq!0fDS9Vy`l2F9RTQO3)!u0VUg>65+QsuE27PtyzreLuL$V}}alVanJZb?eHDvJ+OZ-*L^r~os$Tqai# zWYIPhi7|RRg}^SHd*MIdS(e%Z3z17tlG;NVHWb-aN{8dmEJP+vLK14r$(<7%wiam| zN|&QHD!O8A$LYIks9|vBBlprFTlfZb70g36&^|Iu$jU6GAv=OTvg$gd(i5 z%J3Kk$4wvS(ypxSPZxBwRpdFVh=s~nk+YaYMHH7P_h*%VH1u^=P$M7oiZ=jN&x{K#QBds4l#PZed^i`>tih!r7`iCiU9Q)}enrKCnu=dlq5>{{ zg}U-DvQ-Fev7i+^19t_k6^ zl7z}26m_Ve$fB z(^E239W_cB@GBHIR6==RLE|6Gb|BX@#U=91cwEiG8l@PdDpIUgC>M_4hyNgK2{~!0 zBq`+lm$7~g+}k_-RO5<^u`mZn0aWQ1#t?1UU+|2-*2RY2{y z1h8?Xs%*65vcg9}W1W`E(^GMT)m4+Lq3U)z=&p--^2MzAHslRuGaU0B#ifosV1|{} zxI`}dYN!NC$6opvy za=B3B&pyI`(rYLKaFriRS>wl>i>0ADh=UYrLEQKS3s52A+95YA-@+4|x}jDmhZDba zhe{`|D*oC@8gsHoL1PeYRP36u z9pc)ZBx4*)#c-ozoB_zRCBzT>RMR4vO54v?yz^;Lc%fVP@xm-oZjmg!iJR%l_z|GjiK4xO% z>BszQzVrX&jh$0a9JW_wkS!Z5>xDX^$NF-gbQN*sk3{S!($>rFk%4OWV zsuMTSLCksL-cS#7ZhXLvWq#0GnB!dSR|j>B0P|K>ztNnVIQ>j_)Q@xrVY(a#^2UVA zAnT9Gawv#_6K83@o|i8~EYun&wjy*B>^OgbRpTKfb1kk$a!*$JS8>M z2)|MW&|Y*9e(BvqPteP_rzVa`VU{sRnU6ez-xb4nF}ze>AKnn&UOw<`>V)WI>GacCrgKYIiaSz#bwhMJ=qBmr=nm1HsJlRSz3xuk zBf6J$@9DnM{jA5=)6q-U%hMaCH(qa+-cr2{db{+F>Rr*R!2Ku_^k?gz(!Yv(R$l3U zGte-wFo-r7WKe7{&ETxTTSE)O-i8MZpBsKOB1Y=C_rwPGodg)QH%c^`WOUkCY;0(3 zhr3PM7VlvxgxydGzOD1in1PwPnX#FTSyQuMvpBO1v;Jly%_f=6Gb=HB zW^QHfY~IW~)I7#K+kCD0HuDGOFD!_K)WX%m*CNCs+M=sPj>Q~{-4@3zE?T^>sI{b) znwF-Pc9ve2iIy3b`Id_<4_cnaJt?m(zgP*Zbge9{oN$*(g4IN;IaVvIHe2nnI&O8z z>Y-J&)dz_oe)S(L86%l4SuRmX4ofac9!jbupK$xBvD888gF8>+q$$#T>2T=;>3r#G zsY1G6dP@42^r5s``pMeCx`}mX>xtHLtXEiXwmx8e+WMBv9QUF$l?BKmWXZBj*+AK7 z*-Y6=**4ix*#+5c*)!QY*>@YUjiHUTjh9Uuo8>muHh;?7``%wEB`)>9 zA={zAVYS04hrb-v9E}`(9D^OB9J@GXJN9=R={U)8o@0sQcE^K`ubn)cVw{FLjdPme zw8Uwh(-o)tPA{DGoh8mu&i$NcJ1=)W?R?Gok@G9(&n|oy9T%yKt4k}F4lZ3?a$E+u zjB=UcvczSx%K?}3E_Yq3Tt2vhtG27TtAlG3*C5wO*HNxhTo<^maV>Q{;Ck9k?B?gz z)-BF0#jUs7V7D=D$K5Wu-E}u`m$(PHN4j@*&vNhYKGJ=X`#kqm?pxgVy5IHC@Cfio z^cd(d)nl*6QIGGQB2Rr!Yfn#4f6oq{ojo&g|IA3wsh*2HH+YtL9`n55dDpYj^Su}G zQui|Qvi5TF^7ab!YVVccmFAV}wcYEWS6!2gCizWT&&_$wwVRtaw{PC0d13Rb%^&zU`Q-Wx z^^yCW@+H3NzDB;*zOKG4eM5cYd{cb$d<%Ui`7ZEX?Yq@?uWz~UW#7BLmA>ypVZIQZ?Ipn-yy#vHsot=lNgoza0Pp>H)?9HUS<1egSO* z;sR0v@&kqkOb(bIusT2yus`5zz>`2hpl+abpm$(KV1D4Fz0D6vyi}$_92NO=^?oxLqo=f>FD=;6?_ zp|?V-LTkf#VY*=!VGd!0Q7QQb{u~FTkU`9lsLH61 z(M+^Xv_-T-bdzZR=#J6dqWeS-kDe4gH+n_%rs%!VrU-Eb?%hYX;7!Jou+qM)M-PfJ)KTClx16O zFLb`$`9&AqF48VRU5dI)>hh+mT345@v0b}$&FMOzYf;w?U3Yao+Vw&=(#@e;c(+Wvd)4S((AJTnH_i5c1bzj@PvirO4-%^Ar!6}g`V^T^| zwxt|MIhArXF3jLraw-9 zlcASknb9&MGoxR|h>R^6dooUB)b{Z0F|^0H9*25VWa?y^XU1f9&CJdmm^mtQa^|@# zjVyz#4q5qG!?MO@&CFVowJvK%*2`?iZ13!p?BeWc*~_vwWFO1EkbNurN%ouUZ#lvo z!yM}z&m8}p4mn+NvU2+66y{9KnVqvNXG6};oZ~rHa~|}x>FLrlr01ZX3wo~ZspwhO z^KCEfUS_@8^-Ag0tJjKNn|f9Cdf)4NZ&7dk-d4TSd(Y~9y!VIR^?h3ODeN=5&$d1X z`<(7mpDWHa$d%-}j%|DTUIsaB)+E=r0v%VpHqx+^}W&eao;z6>-(AXv+d{EFQs3vegpf>>9@S!#(ul|J?(GaziEH}{_XmA z>Yv)bcmKiti~E=MKhXa{|Cjx13#FpGYA@^+}kP~)LC zLlcJ18hUx?-Jvgs)(+zj(;j9v%x;+HuvWv`51Tpc#;|Y0HHX^{_ZZ%Cc-Zim;k||r z96oCJl;K;4?;C!1`0GNgLes)Fg&hhL3)2hp3WpYsEu3Aryl`vb!NT)}w+f#WzA3C5 zAs%5m!f}Mph_DecBf5^r9x-S{*@#yoK8<8X>W;J=={2(T$etqyjFgXDKXT{DBO}j` zyfyOa$eJSaB8Q@|qL`wtMFWb8iY67!Em~2uxu~q@Zc$~?yP|KS%tzUe3LKR@s>i7O zQNu=!8#Qy((oqLTm5;hG>ei^Iqu!4CI$Ai|VRXmQNux7H_a8la^o-HVMsFFtXY}#W zS4UTjt|^9Mtzy$++v29h{>2f+$;Ca2^NWWSk1L*8yrg(t@%G|F#jlG$kCBdv9MgGB z)|mcdipQK7b7{<-G0(=-jHw?h9&0?-W^9wOfny`bb{m^BwqWeYv6IHm9lK)erm?%n zmXEzQ&THJDaYx3T8+UWO_IR)Ht;P==KWhBs@$<&79KU&d+4$NC+7pZ?$R@Z=@SPAm zA#y^u2|Xtam{2rf(uBDaR!mqwVaJ4<6FyFaiL!~#6Pr)WpE!2njEPGouA6vz;YRZi%&!*H&`8t)IYB<$;s_RsrsbN!Nr;eXGYwEJ88>jA`dTi>&Y1-4w zr*)i`G%aIV-n609#!Z_s?ees{(<-OEo9;Zl+4St`lc&#{zH<8J>1ET8Pro$%?ewoR zgfsMJSk3UC5iuiSM*57r85?F?pYdrX%v7JLKht|=%*_5XXU*I<^VG~sGizqq%<`TU zI4fpW*I5H+mCU+2>&~poS?_1N&i0-iF*{*)`s@X>_s>2xyJn8|9OF5XIo@+x%?X*4 zJZHq5iE}Q@c`;XguKnELxlwbo=8m5`YwpguN9KN-2lHCYE0|X_Z{xh}^X|^Ko*y-T z%=~rppUkgbAX;FsK(fGPL8k?y7OY*cZNb3>9~Q#G4hxeO_E=c7aK*yS3$HA^zldGr zxX63a=tWZ(EnKvA(Y?hwi(4%2wphM+=i(!a&o92V_|X!pCC*ElF9}*QV9BT@Q*XVs&s)B7 z`IhB-mLFOEYWc?%WQE2GgB8*hX)AJ9%vy19Mfr*gD{ikeUDcFbgt1hp)yXwiRx2wLc7OvJ`Em`fd+GlnC>cOiwuimrz$QsQx{%fMvbY9bY z&7d{Y)*M>%V$HiX-_{D(wq84C?ZUNd)+*LMD`87?OInq5F3B$$j5~7Ylq|tLqkBq@ zuj8##U+1)qBjTi<*AiuIe|aVH}2ecWaIgbw>D{RO5BvOY1F1goAzxwx#{_)kDDzv+is5E zynOSf&1IX9Z~n5yV@v#&fm@buIkV;3mWNxaw!Gi!vbFivHd{MvE!;X`>#VIyw_aDc zDtr_{ijImTMTR0zu}pDA@pozK(m|yoOP7|O+NQqEW1HW$$=l9vyS80zd&ljawx@3I zy?x5|<=fY7S8OlaerJ2-4(lCmJG$+dx#QxFCp+Hk__C9~v*XTAJG<@7+}U^M@SQ7m zZrb^FXWcH@uB2V5yK;6F>{`9+&Tie^>AUCbK34{1YGsCHmSv7*DP_IN29}K~n_M=p zY-QQzvZs5@_c-hc+!MYhde6i?+xFbv%kI_MYqU3EZ~oqOdyns}*!z8-|GvTd7VX=x z@78{`{jK(A>>s;-!T#<0%MZ{4;sbsM+8-EpVB>-E1Aia%KiKJD$-y^=q=!5Ybw4!i z(1t^I4n00pcbGY>cDU8yxWi)(?>Kz%2y-OzNY;_Eqr9Wdj&?XY?C8d$jPgF^1ItI2Pbi;JzOZ~%`KI!n|;St5e~p;!pKBHTu-@Q@2kWoOV7Pb~^9$ zjMK|cZ#{kfjPQ*18RIkdXPTX9e62tn94&*_LO+&c>bXayH{^{@KB2 zi_T6syYlS*v+vJ2oojin{kgbvUCw2k>wT`^-0*V?&aF9DdhWov+vom1&z#pfZ*|`N ze9QA~&&QolIiGz#|NNBm^Utq7uQ-4H{EPFo7w84u3)UChF0{N5bRqLX?u8{6HeI-K z;r@kJ7d~H9yQqJ$)5VLIY%T>{D!DAWoPK%AeO&j%;)&yvW>4BZiO1d5lb@`AQu^fllj~0& zJgIt8^Hk%h*V9f1o!%4d~tD!<|$aKkEDl}nXxRhz2FsxDPIRRgO=R!ykdTD8CGbk+5$2UXQopPvh! zYdtr6Zu8vrx%czd&%>TaJx_X``h48;sn6#SeR=ie-Iq^Z*1Y^)tx;`S-KsjI zx)(nrIOp&!S6Jn->utwwFA zYVW!MRjzp=QvQtb?yOd1@4o}k{Dpp((!7}CJ;9pM2q`3<6Q zPfQb7PmY5O_gTw+1Ak%&^XY{e5+ zTfqh2TgjPU0ne{`aDe{50PfF28%!fUh|m2F)E3Kn4VKYf)D9h`6zuU{wVQaS%kSm> z?`=DAXW&jwhdF&EG5?Ps7xi>Hg3)jK*#!3yHi0nG3w5y9{|0*FEfpNhMLi0~oTY-- zuRt1Y33!U?pAb&%$T4cCr0?=;sDEk34S2r*{~w_ZmG;Sqe=i#uN~{;|+O?+5FrU6u z8%AJ#8$xvd9bmctM+n?~$$?U*3GQ%%H)uYElnw}KO3axYsE{n>-hdO6U!W^oLE9Ci zUrAcyX=l*Du@-M;iCq0~2njL<+5D7fTQyD+oqUC75LU6I0#&P%u7=bNos^SuJgIY@7*Ush2Y6QC*AVTL zHh;W)(7&;LqBgi|`y9cuGVl>T5-EpOK=tuF8E0-7;WUhM#8iqSotAjcY$Be`8uo8X z49~kx#S;haAXkQ51*@PMs!3m1i;xZ3WFLncpcme8)C}gJ?H=wo%EDXtvVMapWD2@k z{LuR!B0jg=uj{u&PNWb#y%8Ff&W*8*^c^KhQF;qVXm z#F>aaQkeY_qKPJG<4NP6a7tySz3{!C9L95nxx|RW{}m44X~RbNNhNBeC(Frl7(|v+ z3+nkBM58U31C;Qu(BU^*6Fm9%E1V{!)CKE=6G|hOT*bS?{$B!~D&$}io^}uTmoQaD zk2-qV$n2)IoCD^TMG+XCwM3-~se)A5aF zj)pW+{ucg6T!(RgMW2zrFzu9rgywRrz(8PP*N#=UF*X$yq_vYoc4 znxp`C)Z{`sf($OJ#?8txViMxPc%m_xvyuKVo*YHJas?e6-{Bw0Orws7SCJ6BMIZ^zkR+N(B5`bifMW@JYERmroX!F_S4|^o zG?J<#L{TG>h;0RqDc}YfgEtL?kwR=UZ1FVlY|;+<GVn}p3Av0XdKTer1PLS( z<7-YWX&=0M*bn3T8}BXPV$?w}A{$8w#<`Ff&{L>_c8 zPwT)G&pDfd8P0f8XehaeXVfF{KA>rMBi=N~SM~W^2r?S+COk21s_b#Q;%Utg(t`w( z#qd(u6ZQN_tLmF+4=5!g;3dxWx8f{b4@W@r@LaGE+ckY0w_SxL#Eoo#FYp)yV1ehB zU&CBz4MVAb;2mLj2jW85N&IofWQwC3M6W;U9DGpR@6X zwTJ(EY5Xas>sSL?p@t4qrgk08Ef9{XfJc>m@FRFcj?w^}9ekv&)D_zYrJXb5jTaIn z%??n|4CwfSXU?Rw;rzv&I#Xw5jK7+X(jgph?s&qzADKaB;0WKL(TuJ`Ilw(^=TXyB zNoUfX^G7cB;1dsVZa}Va{!G27IcW!Kphg7H9Aj#$)QS|cge*bVZOC^B zZHf0yayRjEk3QhI8e>?C5<3HrjBmZ;(hhJQLelcT6a*%8z^T>kV zUH^aE|MwE0Qql!apti-`m#BZr^lsv4trxBbtwGyNY6j1+O)kb+Q!%z7!>~nOfo=Z) z)R!&Tc5J~mWfy7!u7{DqxT2v;+u?Xfsd+dqe}Qu;U1&n0Nir-Tr*LGDhN=bpRKTO1 zWDU+jxElevTYo45I^l@b7Ad>Ia_pnjC3DLGB6TykJ}tSk?rJnu5G3Ol}Huu;2Cud2^WF99A~xt`QvX12cV~$QMTY z!bD$~>I(~eVN44c+k(4WaCQq=hz7h{uqDiF35#1okso)@puF*h!P%`~V(W&x1*f%! zxvgPiYnbE@)BRzAKj8I(a|2+R@_NC!fv`3Z6oG)349;%@?)!XTI#46}k^ zw(`Qki@`7{1QvwAyiiyj29w&tytc5oEv&-&jaYfr;HY-6pdB1+2U8=sYX?_HfFc5p zM8JU#aIyn;=imXnc`ypfqTpc^D5AM52(QM%p*T1l2l9A08V_gV;a)tHC&0yI*xni5 zbcTwqa5n`ur^16&s7QzEEO^|Dhzp46Fk&%++(Mx%3*^AF6Q6Eey7A<8mwTp<^eYZm z7(PAzRR7uWDvjs*uWr5adTsgk&bzyK_1`nR@{fJ5_FnhH|6=bwz^f{{z42+gos*o< zI~aNs$T{hNKpMSCFDir(Y9N@%4+Fs>&}>ltM&FtNyUj=#_o^dyYx z3HApwn8kvu9zP26ATX4t4C2L!iZF&(3+97bi!7LViGxw5rlt+kc9qQLFk>f;*j2(2 z`kGcE4Ayz(>Z;l`>{`k&U3HkGYv{Cs0lLaCJy$bOE07py6lB`WT%BMOn3?MpS0S!C zjLX%8c{yoZu0dRST&H*kCgtjAgK`bxo5#0~A0Da`s;5lJHG(0zW})_>P6@SPG_Eep z#+{Sk8IEgQp@uRN*8=9@I>I1aQz+cFtdv-(X{BbBT31S{lw7HM<+#cXVDPPZ<(8FO zRcQ$|T4~fRqe@$tbdv_%T2yW68gr|fRyDn9$EuyHHLupHTI+HK-P%@bU%htqrqx?i z4=Yn{<7(800k_sQl53>YNQ3#d_BHZqbgMC;W?ap4YbMo9i<)igR5Q0`Ud?Vb`_#Os zRx%8>rNdZTR;>$RoUN@k&z1xA*}U4FYmcqds7`vFOc-eE1oLd!b#m%-uX91&#&w&) zWK(wCoO-S5Wxz07wld1r1M09X&q+Tg^PINI09)6@)`=YwJ0@o1&{&s5*92S7#PbvT zCk{v)k~k`HV*L*FJJrvw-=ls{7+~vHzduxB2i6}{e@y*x4SFggY<(K^Yj9!1?1p*D z_}cj}zc#qxutvS4>9xk~8V_hZu<@YAgPU|~GO)?uCc~TNG#%P>c+(M0Cp61y)}z@V zm|MH3S$?x=&3iQO(|l0#!Oe%j?AnOt`OOPkv~4k=WxJNcV03L%%Ly$fwJd=uYR_}e zSH{=Iv>MfFOslc2#+vweHW3P^Q(MnzGo;N0ZAP>i)uyn` z_%;*UOiLQCjImvuRHDqWO;QHgE`b@g!pMY3Nu(6U*ybh=OP-KilsqkYX3CJ15h>$R zrlw4XVYXSRV^YV$G~3M7S*f$3Dmo3u*+!;MPM-x8(YYC=8PhW^%^a0Ex9z3v=Cq%B z-t_Zkb(+{|PS=aON)xddx&A9pdf){723S7%4+6gqR%`x6{`@=Qc)$*m1%BYuVQP$l}jl{C`NRq(rr(HQ9g zh^qqYIt%`zFbI}0R)RVkL;`d}+1Ie>5d9ucLS2TD1`3A5TFo@PlQ667AU9FBvq1pA z8;*M`Zrc`X3MYhOOM_dUz-0v;iY__Jv1k4Xnw58v4Z8#e$vj{*b%h6`oe z;WyMU4ZZ{$Ab%&*B?5Y5e|IVE47&{VNNw7dG^QUBp&?HC>`8p%>=4g#$pTRyX>zf* z>Vdu(Qg%YdodGO15cgl?MBi0Kn&x~n>g0!A*8){Bx_ag97=f zc4FjKf#j7P}25q!y#CnlIa_vkN*{%kFxpFz{=D-4VLfOi~H~Q`;oxu z-(h!>0BF&G^m#?tU6Nsd$RMrG3Ij3!(o4Go`TE&#T?4?{Mv%FqibP1M$UuqeGu?H`{U34eA@gcI%0m?>4NMt@dxG` z_-6xg&S~%DT;v-&XMM&#O1i{QQuzb&6+<)8`&ysm(g>5KXRWkdmkm!47 zmK~GnU>h#|Y#?!8;+X6!Syx!i3y3+Gmt=qT121z7+VEP~ZR-MydG~{!=m4DjPr_Xo zzyCV_N&HQ$F&bdusWb{p@Vtslm*ZW-)mniCTT*Z?+EK%J>dT*fx(&~BlYo(RC1#1(3!qPzbreX;1v2Wom7{=)}UMx z91}EAHJoT6UZsg>j#6%6?R7?nB&Xxa$LRDsV&6d!=k;+0XD>Xul$!3*315 zE((*tO)UVF0{Q|f0peUJdjkHsfQkT_22ge!ehYw17Xq}Wl`)TpNKH3fc8vZ8PXBMn ztE_CjA83QJ9W7OhVj(5AR+LB*B* z48H)9VEJB_=@|ItY4|3>WxvxbQUjVe4e+kIK$z%b5LTJ#5`5!KHKOHYemtixSEn_o$q zvRlX`1#({Vp8u2nb)@m>-=QaoDFOSIvEDQ9OybT8OWb$#C&V?JrbH@PLS(RPgrFfO@`5#ki`Jl6QEH>12)4lW-@5@ z|0MiMGR~TR60Aq+Z}SxJTQsl|l+6!s!2+?Qd1c%@%zIPd_e8!DW|W91w) zfV(f#v{CP$#BcoY3M?4wFs}~Y1VDu!MgHJ%h)ra+*nr>2bZy}Oa~S`3e}j1?9gu+Z zZRyCe?^u%Wd)J{35^93)0-*1cR8|@9C0vbo06l_s)X%5;^h;6+(1KZIzo>UBk$zX% zVTA*GP*;fyF0}MLyK(n|wylPCk?#k9uCd8%4TIs=1XOmRY!m!sKn=hI7s~#C9|p*D zRRKAW`jyZISPN9R>?Clo1ajYJLsLOIYQ18m%#G3^j#?S zJzeJp%2ep`t4mF@aB0lE=Wu?}a@w}C5Lz>hle-PB1>(Bo>XeIpud<2|6 zkCqF|IaIHu<^8gs%ZH&$b;fj9gZ*=#obSAOtSf1z=CN{fmp6}sm^O-t@sU4Os4XWfIEvZkKGIz))45eKxRQ-CJUe) zJ_7SwAMz;HK=`nvZPRgO-;&4h{3^!Z99`HRR zQOai^4_Xw3JiLF<1rC$$d@J5%0HiD+3-QkZc0#UmF7mnocK`tR<=~wZg0Hb1{MuBq zNaO+r;x~~hq!#q8(T;hKx^8BEJFPnf_G6H*18$^(2H=RLqFaE zbX)?d#QQ`3fp4=n@cs|CyzCRCc^>c*X~uE4IPZq<;Vwh54d3Yp*nt})yfPMn9+I$} z)aOs5%C*1)fXdP|uR@?Q*Y$LP>ETOl?9;^YI1HYJ@&jYB(5RxaVKyI)f@BzLNDSMs^ z&}0WdyFW{6@Y%R80(~X>Ow|u{R6OtqZk6!yoTI$YHkuzA!Ws^pXwdYbDF1N|@_LEP zKv`z+;lvTE&_B1KFTl&?4Z%;9XlE};C9kWjY#m!sb_l$dX{bXtJfFh)XR(>U{kiydRZ(eFA7zx~T!DJ5M%q`9 zCjg2-(^Y#kK-^UDK<|OS7LZNSu-BNT+81psd8(@YB>!?C-sv`!yu@f5%C_Y!~ZaM{JU2!;ymVFYzAjsOmUNrJXWfjOQ(XUi_}u{) z07e4_19||40lai0@qE^F(1(*coWr!=9DXA}5+DW83eXtP8t}K$k-N(V@snhjhGyDt z?(>uIEc5+a`Td`s4)Y9=fHo#X-i&oAU^GDf$#?jptJgkKSUZ*o3)8uaugt@!d|zt)#yhyltspfcc4skk=gKC3RiefnJOtJOQu*aWb!dq@IcI@^caQ8elhM zL-PGjG6-}=J23{&a-M?xUG?`HWVGyWmV~kG!hGMe7k9d!hcps;;QIyeBm4%)2}YN_ zFXuklPM`%ANZCHh+XLS#MIA;!&KnVzLgqgN`-LGmvoHiVx(vasTtjeE*ATJ+0W!a-fI9$N0FP;3rb9VYIc0gg zHublsth<-rnf%ksAj4k!%Kq`%_w@S8@CN|d|GG_8dp0;r8$!1_+I})$`TLmw*Sk_b4#VfbhPV%=>aaVqSlZF@JKc_)H4|nrWZIh315O4+JBwh^!9&f>U ztxE9M0LlRG0agK4>#%&5@74kK0%W{|Hvn=x$@EX-i#g;Ok`3B5OU|3L7;{xIX(Q(< zyvOfoOJF&)u`%aKy#eUdNSOfkEerIV(?HHYQa&f=4Mp2X{T<2Q0goGNLI&u$DoWo# z?$u=)%%?OU=YIHcWR98>cJ4WVw=S4fQpD;MHeIo0f}^7 zPCvN&dt?oR+pAB{L^`u~)}Tb%wRg{4(6+t%_evyR_3P6M@dIS~{R8^sCXuHH^y`7= zLEQ!s8s#;E#RS*Mpu~4Gwqy{t$IT9B7sJL?7g$3ul5hq zSN(_SEC0jvmHuJ+ivKWu!aqzO_Yc!MXHHLh5+l1yzeI8{yMNb2@>ovSfr;euUOjRX zNx%O6vJ=rqXX~LVXU_4g=|lf8ef&R6AN+^u17}I!mQacNa?L&qZdN$UFJ*enV;GU; zasSMi-!MPFXexc8AW~XDA1=r*Eu`xT3MNgVD@GTMA4`{wo?JYME*?{oUqI(0BA?DK znmlb1omx~}P(&vycbsxZDfa^94pMGk<#tzYu5vplH$%A*<({kDCdy4zZY^YALaP+Z zuY`(AMo*^Z6!}bG!E`b?Svr357;?OHQo$5*q_i|COb(S!8--J*rPHRAlJ}<~C3#EL zJw>^g)6ou1)J)b6kCFnS*at|rDO~wS)%wk_;)PWxRk_WTE9Z5Jxk9?NmD^akptulI zQ@J&i>&`aS)pH}|HdJmsg#2Ot=(CRfL9A zW!;|{>MhnP^sEESXrHz}ZE0HHv|4E-^&MEuUXePr#je_+n%iqGt+Btxuo|^$467ch z_G`8FRS#6%RdsXKRaHk;jo@gEsB)yr#wxR`6jiyX@{!8BDsQN~vhu>pQ!0!s)fLqe9hV_j_uyb+^PI6xh8xq&iWjNz`1J0W-r#ImS z^_%G}^j6rNxt*@W3C%m{U9duM53GRP3u{$tU<2hox(=tK*W-lpgRt85Fnxq>fK7G-_OuZuz~O#PDj2#x8TNum*~rME8T`u{X6JR`U>5}I>2hgt2jlv z2Pcj9(br*z=1uw*EYG|{-=+I;Kf?R;1NtHTh#sIH(@*H9^dS9=evVrcpjk)1qF>W* z=(qGR{f>T5f1pR`kMt+{Gd)U=(c?IJ^(+02{;m#S{XtLBG91yPj4{pxGnk3PST=K5 zfCX_*I-Z4KA*2G%TvuY1Srt~5Rl}_eHDEES7OTzb;DmBLb`CDxs}Fla4Ot`Bm^Hz< z?Pjbwtg^I(4Ukr>HERRgQem8OO=c-9m8IcScLvT2wq@;Dd;Thajql-m`9A(Se}lh? z+rHoC@9=l|e*PYRpMStV!|F5)PbI5miMsQC-v!HPzv>I-;(qC(aRxqP}P#8saYd#-fR6Dw>JrqJ?NF&K0e2 zK&_2P5@8V$$s$FhiZqcfGDN0mi~A|si}OSW(NS~~St48Hh+L5;I^))guA-ahE_#Tb zqL=6``iQ=wzZf6}86O!3jE{{^j8Bb&#%IRo#uvsR<4fZ!<7?v^<6Gmf@tyI#@q=;1 z_|f>u_}MsW95ap^zZkz7zZt(9CybNEAI2%8%!K-s$xLnv(=bib!m&BW446SP4#($0 zW`bG4tcYWDmCY(ee?~@MrLEPiP_X_W;Qol zm@Un7%~ob>vyGW#hRukXY^Ip0W}2Cf+ch%Hwq`rCy?LJ5!R%;uGPBHVGY5BWSA@Zx>?<=9#&7Q7j6>iWA(MpxB6NAtpV0RYmhbA8e$E_{UXDy z3#<#Ri>wjWNGsp^$@tIP(Q)n+!gg>BfTZP~W% z;69R|9fu>2Av?jY;5+yj#@!^A)5vM+Gz$z448fc`9dn+X?Q4-n@H4?XXb2ubXVRE- z#lE{K$;W?3@ZiRhPGlmP2EN-2><{~ptH@&Tqn5zNz#v$^xD%>Bbl?u}nfzQx_v_fgL7XAfZTKExiP z7H*S&3{7&3a~j|VH*iaNF3+X4_&7eD*2Ycpuug@$h|RyYk(9H|;LY7yW1t zF;EPoy~G8gl=c?W#C$qNtP=Oq60t^Xrc=cW;sv@yyeM9#*NSao8(k(|5xeR2Vvl%( zE*I~L{q$DxrTB*4E)I(y=w0GRah%>Oeigsb`^5=yl0G2HL>Ya^z!XX!#&OjI`iN1{ zs6ih$>KJwDCL_^Eq|X@*jTZEI1J_~D?M7=OiS97+jeNSxm}jh`yN&h6L-d&OxUq@; zX1rj$#hCG~@gA#Td|-UYYMT$553#zqO?xvt$J}P_WQn**dk<@5zHYwGnwjsI@37|P ze)9v?0(WU2Vy$rx_hFW7{%HQlQY@G(VW~JYo50fKp;?xWo2Bcq^Kg%JW7ZKjM>l2J zxGTC9%fao~ZCD=ehmNq$xQlxf>w=rOC$p}&hkGjPY0a=^u)fwDYYsafcjU}x{csca z5;oAf*}9nx!!6fOvf;SZ`UQ3Y?ycU+M&Xv~-E1`OrGAZ##ZA<2uyMG1`)xMf+HdV= z6Kq_K!X`SxF<6n4=p?eqPGhGDE5?1k-Ppx}{(=6i1iK-*cau=dUBkiiF-1SZ*fDWF z+`WjU>``C3CcynVpf{LIX+A`l5obP#wf{?`6p+O+$OJ)c((2+B_?j{R9X1yBP$tpI zcCwvfr`l3rjS9cYKTjb*r_ntf@XXLqnW+MVnyJKN5&bL~94Gj28NXZN=U*aPiB z_F#L6J=7j%Z^j)*FW~m)m+h_gZu<>&JM@S4NA>~xWBU{PQ~P`S2m6TqqfwHDZal zRxB0QiDlw?af7%~EEhM472;-bi?~(XCT_Po7H@=%1fv4xv`RRNU&W|uR5Pj@HH?}@Eu*%a#!zFJ zG2FPoxX`!=w||Yqd^*Y~Fh(1N#u#I)G0qroOu(IBMaCp!vQcbIF)lVrj8bE&G0m86 z%rIsevy9oeF>H=;sWBI`@O z9x@&_9x*l;j~b6*hJONglWjDfGM+Y`F`hLx8JmsgjOQ`uZ^4aaFBvZzTa9hTc4LRJ z(|E<$W$ZRyHC{9J7<-L<#_Pr##+$}l#@liQGTsrtqtD(o_8adR@1t!#FgD0_!mekZ zW4~&@X7920+WYKZ?ceO*?GyG%`w#n+UFN|4g2No{7*4}L!$6}z<3N)@(?GL83(UC# zaFf{}%$-$OM;?#uUIO1u_NoxB`GNBxD~`^|ybitQ-yGY2HL}k>uH~qf-O)P~<5!N}RKv8n4k&3Tp1ZVSLuaILQL_lB1-;Pl6VI3>ME! zSo^YRB4xAzok(kg;x2*hF;Lueyrj741iQLjgHBYVcaj>tlhx=gQKNUR8ol$>=$)@d z?`3NAUam&(6>9V@P^0%MHF~dBqj#Yiy^GZ7T^t>~6kIGxgE7!f?Fq5F*nJW9#r4EIYNI0);Z$y}4f8u?KL5d>G z4d$Z~5Gy4Tud-ZT8zqpOsYWOjN8IOtO8SdQcxL2Pq>*oS{pk&5xrp;h+ySQp-MpIU zyei>sYn@kXRqEmPXoO{Mb$q$G?+We7DlN5(eYu)t4qEb(W@?mbSdmC*i>RLXBO_vMB* zrPHytqD`?+^(1d@Z>?9UcLP_wTGT^ntEllK*yq^6e1Q~#EJW<>G6(2Oz7F3_%^)j^ z!^odPFUIIErBmfznD5{_`72!FavXTWBe1(aOumE7xQp0oc7V6#?V$DGiIq!7Jxc;`h8{vDLJKRdD53gQty?+tf(857<@#tpuz^OUXI15#U$VY6Ne% zdOCzScU;S~%y};mf{I8x>~qubrUF1pOBlFQ>Hov~Y`63&(bCION=b zVs5tbSqHnCT@xc%)>5YPRvekG5?W^og#Am=@(y;;H;_2s{&EtJ7QBUou#>)nRKz}d zHK{6h(4@9pr%7G8k0$l7lYWFGV$I%2>dSpJX)1Tpq&ZgZ7f4IFpC;#GNBt&g9cY7{ zbon-`0e#PXd<%aO_bR{44)Lw#CR?t74%^G#V+YtL>>&GseQiEvH&uNVAV9j zFY(16@Wl^{#hXw0;jw1Jad|ZUYf8l z;qrtz3DXm%Buq#cop2HEcFB{YEXxhe^v1UrF+PKJ($j)cAm zeI7ax+8=r&^lE5(XiMnX&=aADL+e7TLMuZnLd!ynLsy38hGvFJLPeo5p^>3sp#h=Z zp{}897_&C2#NSp@xR9Z6n{AWQ2ZzH@5jFtzbAfY{7dnh<2S}{ zh+iMSI{wc1TjFnsUlM;+{QUUY@l)d`$B&C289yw3Kz#4`uJPIN?c>wplj2*%H;S(p zUn9O!d|bR4PvU-$I~wW?V^JQQVlgk#WP~2E_G_>l&9G*FG*SE-9`>T%)*pFxgirE-ucDBf;N;M}yx7 zzX~16laAR;oaD8xf@Xp{Z!5e~0f>#CS2WJPT1}6u{1xE!h2o4JN z4R#Oa20H{Zf|204!6v~(+}m9x7z)}!7C0F=9yk*CCU7wDVc?y>zQC@)*1+?Drvr}# z9t^Av+#R?ruspCdurP3WU`}9qU`k*@V07T3z>q*c(8Rfc4uOn7BycWv)QK=USS1hw zpN@ftcO1OAZ@}w2;OqxC>s8>&7Ux+|*AF}Ez!h7GF}@5Ov@4yt&U9yrGr<|{T;vRK z`Z+zF&Q2$%t&`%kcA9~gSjVXb{#yXbo2Rg^|H(cKUgIa=J-%h{iF*s&$d|yy+h}jF z*W0V@JM9(rGJ7%ZFQ04Av`g$FdyG92e7*s8Z@Vjaf$hQNOtM?pjj$i7VON4&!L$iv z5J#==L5m#(o%W8k&)Q{ewVt=0wjQ${wANa8Ten%uaXb1#>vHgcr-Lq=02=Kg$Ts>x z?$H@Ck+z`p8iC6!%%kS#=3C}g^9e{&mP7Kg5K@^rkl0LtG-osvBCkRU0-B1$wkziBe*3= zN=Mc!>BwVBIH$WA34c|}P_b}8w|K_wmeMoCA$Q__(ilyt;W(h*xpM}kT^ z5(l2|b2J3L?`t#xGtFLF0Wh69S)q4qCuiH)7CU1o#w z#27|CcCq~m;r4Dw5q2Q7&)yDqkG&0UXY8FB`1S4J=GtxH=Gd8Vv+WGHS#~t+a5b;2I<96xNU7n1R>2B3^&6De-F})fpF980dQ08{%})lP{inu z^ND3&ZZE+MGT9z&_r-qOvM;l*vG2Dh*#&kVI}hRcm{rzcJ{g6XBp2a%_9FW}d!n6h z_p);ko@+0(*V+>>+w`=v5x&&E+FoOiw@27L>@0-m*jHhG8VCK1?sg}HFR>Tc_u6Ca z3+--pM}%i%&RT`p>H^GD9T1*nUt!;47uv(YM}P#!`qoCjSl`&7_^@~U1MWfV1l&)o z-{F32p^vb`L~CQW`7_)M3$2FzXIHo>Ru0@`3vFsXBv60zL4o=k8$@5Y`$ZqP@8YaF zGu{-v;JzVx!hK!zfV)q0hr3sFgS$r{H{&(Y1@5b&Gu+)G5AH6U4P?eEIIYKwogy3V z4v__SyXXXW8?0t9V=L?dGUH`|x)?7B^s(`xKzWQU0)1?}AW&LklRzIE&kFRh@r*zp z8&3=LvGJ5Z9~&D5`q+39=Zl!}gb2fZTqI!^v)kAJcbBmaF-t@nxYvl*a2JbKa2LTY z5Hl8vmT<2YE#O`yn!{Znn!&wNG=+PGXaY{kWaDzt7@^BVBe?T%HjWwdL<6{UVb_uI zQO0yU4-<)ae#JN+?oLCtUmaLFV@54e4{l9S7j6wv2X1vy8*VjG3vN|W6K)kz18!xU z%4J3+Q4MZIQ59|loDXD1f~X8Pg!8M+h!+*%#)%4$;+=y1XvTMls#H zWdz)(1c$p(Ft|^`<~f7Z2G}MZ=VfpogUtgb9>r-*CN|)l4ik@X;GcMy1OLQBIOWR3 zgB(~W9^k-2v7V#6;(mS%?mB)H?tL71Dc16z;I836!o8OtfxC+T0CzslU@$R{e+PFi zKMZ#c$1Ec*;orcW&A)~_i+=@oCYNn6gC9bjwu$Nd3xuZe&*7Hx&)}BugK#hApTeEO zKY?4!KZZLQCxDrl#6N;t#6N^Pk$(Vp0)HRwc>W&TaeP1AvHV@QWB5C83;Ek{NAtJf z7VtOWj^b~?&F8Pf9m)5>9f7l?Ok9MMrc7MOUnAVyATHpqq6a@@64O6m64T$qsVFc(nZs&OT(=U^_t*(Xlx z7?~)mY&GUnV5>2Y9zly#RI@-n1 zWY7)S3HM#P1MZu2JKQ(uHn^|Tt#J3@yeSiV=}U0;K+m6vS79Z9LA&GyNg2@R@w|&Z z2lo}a8SXZ^3GU1E8MrUer{TUxpMv`WeHOKu0&SIz2yMb$4Vbs+6L6oQkHdYMJ_h$G z`Y7CubOYQc=_7ETpbx`+9FjH6Tl7J=kJ1O=ZlLSoK0@z@`!HPx_aU50!@Nb;!hL|Q zfx8~(z%Xyod*QC5tKi;8?}59P-VJvRy$kNW^iH^|=pE{LrFy;{m@q>u!1H{18{By~ zuZEe7-U9bhdNbTPbOqc?=uL2E;}j|8GkPOj+ zYvE3zOW+pMYv4}C`BThfbP?Plx)AO}dNte$^eVXHsVv1fdL`Vk^a{9R=;d$=>1A+7 z)A?`<=sdWia0d%!GI}Z8kvRW{nT%cn_aZtQ?uB%gJCo6wiU*)Ld1i0k2-*afiOZGN zz+X!i|14Ept>pe&w51zn71|hHi~jq17+%|M)vbFbEsDR^ zp*ZxH`V*&XPn=zMqW0g`jrgxcC2^7YSkP6EDDC@*?avM`BMp3j5a4N*`t~IAmwm*7NDeDBTzfytam}K5bNg z&(q%f7@B*o-rixQvFGaRx!QVvXiYt-r)NfW^qL0Z|D>VU0@91Yp2m&R*nJvjG~_9b z#!AlHf@I(%SUX7ht3b-13#nZf{MRKT@eiy1I0JePv~nlloQAwVuMzl83-HTp@E;*J z;~y91KrWR+?uYd2Jn|$?f_KF!$!Bn%-6pabJp1kV?@e~%zYlp0ca!zSy<#W8q5cC} z1eZdKAOwz81zL?PrS)ljaue-DaS0CYe(OSRrrl_F+y&I14kRn-NIH_-g*z=LlDi>q zEWw%1jx2}V%PwFS;4Eh$tZUrQ#9aiVe` zdz@`1Z?fmvtK=iLhwUMU**^9W`Hp?e4v`b=D|VD}4i$b{0oTOD(~8hcss;VuIy{p$ z=Iwb7&Ea``1nt4|`B*xJ&*0b7N&H5BD_zKM=PT)Td@Wx`ujkM6t#mo0p}U~TD>Z`e zQX0W`D~;f%l}7MZr4hVcX#^j@p7CY+vC;@Ws5F8{rx5j;R?1W!;J!IP9maIw+|z8G@%msyFq9eTl~N-uc2(g>an zjo|m$B}yZBuF?p;LTLnFX&yI^vjs{o_$s9re6`XGUa0hfmnyyBWlAsjMx__LTdNwnJ*9v7Jhr_!aPoCbL~qzlFU8{gzGaZTmUV z?FXlAH+v8It#7gS?GNow*+mHfTZO8x;lulejp zr@$#-KRJa?A^X`G>x^Ybp#i&)9Ru%vG5ZbruS?kPQUjKqkPW~ZbUEGvT+ z>>Zp+O<2yr^Iy#c^kDDfmXzReTT1YFKx)JCAhcnhyV)O)d>B&F}`00)h};ye`}mL z!>=2VkpA_^=M;Q|8H09}IgSmC z3ycp;3`~yZT|JgJD`5BIRMI8lPH3N=z?m5NjTTNz=Um8sBKm9C(#yZHm6+wv@E4ed z$VRvWoKa2%HK+;If1B)}4(?G7(l{EAd3_XQ6BTGhT8TVFE7K~pDy@c_cnfF^S`*e8 z9;UT%gKQmGmuO7u;hwid%>NB&L)r*B34hSWun#brHla;vGu$iL0;lbp(sOAm+)&wu zCebkN4o{{jxK}lerqc|XNxsHfR*1V9+tK#)JhBrvzjwq+l7*W>b8t#z7uJ)`WH;?1 zZvmy(iho_yRq zUqD*Y(a>`nL&wr_&~(FEM3pW-Ialhz;MO~6!r%tdQnCkUPkyJ<$X-ZTX82D6!ZONS zc@_{Bd@l2!1|)6%>O3Gh2n#?fyfcBYW_SCUPX&@O*jY!g$KL0j4aEJd55`Uh;^e_& z?)gCSCQb-GrOpRFd*%~@xb1d3dBi;>NH)OM(k^*U5Sl!%!4A&Cj{ zrr4gW7jBV&N=D0c;>{0v*hTu%T=i8xD%)Lg+({U?W*R8->w08q~`eHkOS8 z{W5_~WJPQeo6L&Y6m~HyVWn&;o5rTI8Eht-#b&ch*c^5#o6F|0`Rp=wIlF>g$riAy z*wt(yTcmW4mOyg1lwHS`vFq6l>_)bn-NaU~o7pYwR(2b^ovmbdushjZ>~3}sTgC2$ zW)^5A$obYm%D0|90BY$W&`OW64N}qvdg%%FB-_ZIVo$SY*t2XC+YH&?^Xvt-g}umL zVlT6;Y#ZCocCel76}F4*X0JjjxCb)9ee8Ai278me#olJ`uy@&hXnDNPK42fRk02}j zn0>-Ng~rEc>~m;+9D>~NEA}<}hJDKpv+vmV><4y){m6b|KeMCIlRD0RVZXB9*zfEF zJBiEuPO&nM<%2U&tmC<)*bLBX7Pq-WCh`Ce@;Dw34XgxG#4GTMyb`ZWCh#h}DzCK5xJq@rVkkS;ulhk1ljp3GBtDo^9-JcDOS87Fj7x{@iN6wl)wct=vgJMk=@&2u36%;TL& zDeuC&l8bpa-kta0J$WzQoA=>;`T4vb@6QMDfqW1j%!lxyd>H8lYl7YR1^hzt8^4H; zfYdafkKzS<@^eMCF#W%@T>UMd?8=N7n9!n8oq>I%a`)&_%ePy z^!56HzFp35;w$*g{1$#IWVN7lA*;QU-^K6d_u#(ZzI+wGm#^k)_*&RsypOMg40k<$ zfIr9|;t%sj_y+zce~irJkCT367Jq_2$v2Wq_*498{tSPXZ{nND9R3_=<`?)D*g$+y z>IdAmwW;F#>v>Z zUf72`0@=vmv!CsS^^Cdh`Cc+#p714?`Oo;`u9|;&%9jkqC6nr$FBx|FNnhC8YYNHO z1>`WKWY3=I#4ouJ(zq}G?Nh(dLR2Sz2Z}*H4%q+C&ohxNAM}RaFyDlp*xTS(ybFHCd*=JlBKpw$$UIc&?Ni9JY;@per0}beq(+MUdDIk_vR1g5%4p9GJl4y*)i}meldSFe=~nK zPnajoKg?5R88nfo#Vl?K%dkw#vTVz-0#?w9!zIEYb&YUETqIl>+>feOHLJQ+16K;y zvT9p(Bqzi=$4a#7TMfVuX=F9Fnt&tHjNC!)w3=Hjtd^29VzmZmB*_YcGvc0ba8E;I zo@OT?3zhA z8du=NSeHRt?+Sfh;%ew$ErRwH_%YVC(B!+$T4r5u-C*4a-pozb3h-ubv2KNy-|gVf z++p2m-36V$d#qK~z1C`L4Y)P;S?jF(t@YLe&m|w6v9?*;tsT}*@OE}tyRBEj-PvR9g^uCt;PAX@y#)@> zJJ!44@Vxi;&WXrVC)N+v5$ng(>?r&OEyff7mK}v^lCxyj1ZSzXT?d>c$xluMZ>fRZ z5PFi0?Iw0pyP4e_T9Ym9bM01kYr73}D8qIHnp1y1H-4s**Il5~)D3!1J)j%a3;I)i zpksBW)7WR?VHZLtYAp1o#zVJiBJ{5&K}W0Do?>4N?aoqrsy)q~ZqI>qu zOQDH1587InL8I#m=u|C$-qqF6%~}Ngt!tp;b*;VBz79H~*V{Lc+wB`+G3O@ejNWYD zV&7`tX5VhFwC{ix>0S2S(7sv)4XxGC;#vz$uXW1GPgR`ns16;=nm8L%o791Z*n0Z` z`$795`(gVL=%7AoKW0B}KVd%!e%w>`)Alp=v*5{X21m|4XZ|9zRpnW8|4I7YicjyJ zTYX)hL4VtR$9~t|FVEP6bA1%&>_3Dx!sEV^>Hc%~U)f*V-`L+mGxj_0#Jc3{dFS!Z zdKwmX2$T&C+i{>@8+78Fcqil}NKT+riEyW~x>&rLQ{AcI)Wr4TwVgURWs(4nd_D32 zIAZt0?$;{vGPxR-kW6TbcXaAH^_+9SL#(f539xS9z&?l59Oo4tBageh9g^*|0)M9s zPJzVXPOFiy6Whs2a>C&FBtz>rm28ErjVBcU{weVC*W;wf3y=#0oHQpLJfTeR*4jDk zo%5UyPDf}8XF1tUj+5);k+skq?&5THx;fpQ9?&E1<@9#?IDMf7>yU-eZ@q>rCQHZ_ z&iPJ1oCo^K=}#^vFF6C8fzBXjFm#WHI>Vgd&IQhe&_o_Vu5?C1-_-&abQJjoT+q?r zfsO$ObeuCD+RDnp0a*YW2gS}5vckF8DRD}jso*;PK#oA_kVvkBoZ=jE6HW}=2&qIq zSxvH>X*lt+68g?FV6E|1vc{PSOsQJ+KZZC)O+b0}ny}`VnVCIV%KD zDLVwuDoX^ICWr>IKe$CVKrqgK{Umo8zo^OK7RL z3dr*yNr5nQ){_G%fz&`+ARQX)nSr*DPPGr52YvRAflh%evJ@via>%vhW3oSx9mpY{ z26ABy^ds^fd7m60p9Jy(odaD0T_ry{&;$JFUV+}=NB0Fcx?fbrH5gj*Lj%JC!vhxt zE(E7^L||kfUvYa!gHJj}b4MqDKPow=E|2tL#Rr}mmGXpuJi>Kxn6ckRLDw88S?`P?5jgzz8Y&xs#(PAlPZb81Rh-&Dt zu5dVl=#t`ml?w`|Dgj@$XHq!B3%l(W&d79g&G2$pg+uPCW}fOsGcWoPw6gAnu!OC;{KFZY-QQ=s%^J?r4dIm|tizeQnLDSLT~rGMyTm%n z%_0&BbSYPxq;RI&X^|9-0})-yJlz1{9MN@Deu?}&@JLtL{UpK}!LH@MfjC`_h;GS< z$|x({jYqXr9$D!bxvX3F3f&Z%j_BT6T0w-pQ?pxiD56WjV{!+$O(2YUuxj4i$#$X{i$TH8Qf2VlXb0<-E`qh zFP*MkicX)CZuX3gyhyT|Bv9#Gvu6}5!+FUXx0AC)PxOadtmr3H))>WJmfojStr4#q zHd(iZ`{{6Aii*d34YFBG=o zy&E5iB=Nk-W94r#$RcJRU$^CIM9fs(7tYI76@jlSlB+9_t3R3Relns*e8j^tx4V-f zZeO6g^}xu6N{jMK$3;6CO`^Y;;x>FFqQj}WeyKVyccnlFc|$JC z?B{k&zi7uKg>~wPH$A56R!i0OOm#~V340~hUv*1?+*8c{zQU#HLZqby`=#KX!oUEDO?aNhFhVW+#yqM==6 ziKL{M1N}Ip+9;ftu5kmCsYJDKu9^f9uWFC5UJY{fWSXnd*`2=7`)S6&7_Q{$ignf) z>Gm#C=^o3=iy!!BTnVRH1O2#?o@EX6PczwJW1w7-&4Ip2CS89jT^Br4_d~iKo$iE# zisL%@$jAx~ zjb);Ls=gb}!|E_J`qQMayJjO=x3f3DW$Cbs(|Dg@4)bAEW>R2SxrHR0=Mf2RMUk`W z3C#1^acYvtOl>Xg?8*QVr$qxv;m%1akf6yMcPYpkl%QA7Xjsqf(Xc{@ysXp0Inl80 zBrhDqeC0m(?^QwlHanui(wugJipU772w2lLvsz0#yTL3~Y5lD5Q$4%2$jb8tveW#5 zB!3{{52Qo`m{fCBph8xxx1w)BQO1%=^3wmt|E;VH{~Nd5Zj&dur3httSQJet$lC0`6Ax6Er=@dffqPpcy_8MP} zr)Vn7ZJkvB)U6w-soZD^N$UCnox{FBPOLS7a{gpFsVbT2W*T%UXB8E6DN6mUX1`dD zpe>_;a9-H$p9*zd z*qeu%&GdUUn{=~Wf*yMi9 z?4}Y}-MTA3=n}2^IaZDe-OkYW7#wa7gu0dMEw{Q6x4H@4qLZnwC^>ojAP^f3N#SzScv|c? zcoPdqsz~K#4pd%nU~Cmqg#xvtU&OG_Ep4ug%M}Km0Tm@R6)hyFKR1uiz(3)oB(tIk zC9M?=NP^2Bh&4Cf`1?zboZ!IIaW~H`LO@Yv(X2p%#kvvHMy#3f#^0EEvBtz3x5+A% zquHW)qK3M)QxTyk z1?S5wBi0R)meaYEZz)ODsTbx@_x;cq4pi__cj{$m=%4x!8OIPxl6^Xt@?VhzpnBns zvd*!BVcX^F_DD*(%-kZI!`wVw!cafQreZhCOt&i&V&tOldzrCMfF^W{SmiV_P$jS8 ztvHHJkTuASJdGf9BfT#5r>uH9xu=ry0VBp$J)Hre-l)AsL~mho)7`yAM3eH740rdC zo1SOptMTVj*jakJ>+)&9C(;}hkHmHfV$6h^63^0Gl`M~usJAvQKL~V~{%n%oPUmWF zNW@F0Qm!nZbhC1QjTVu70v5v$Az|;S9Bh!Zy|2 zLq(Ev%+b*;DCj6%@vxVxW>F-$EQ?60+fcbKV+^I%4e0I1-R%-q5V!Q2Au_-5l2OIl4P?bZK&QX>xR1CZz@n%heBDgX=-*(5b(Y~& zt1#Ac=oX#cV*$9VfuxAryjYm^@3M7K!*2R;uGb*$W;x<+)zGbOgM_{Egmt*HE_zzB zIaakqaBQr%)Yu3|k^^JQ)ha2X`Bf3U0gPnoTwJ~s2o2Rh_^OT%=ZbML&Z4fGTLdJ| z42~=RO~mQyMRW^CR7T(|s(2JY?~XI|-Zd@N93LH@Sf4bfB1yM?q_Z>L-T(FL;f`RB z$CaboDJk0;ulr@ZzaP@{`?L&myl=}N$z|gwR2Z+u&4>x^Rq#GqAY!>-OaHd9u zOqZV(Ny{;dqDYP`-C~B5b3_rk)oqUG_f*!H9dvoz=E0A2pQR}u6N~nf^$77;%37Qd zNq2uAN$YG*Rz(X=&Yx0TI<=&D%D6&vvadKPy0jikJj$f>TG4Hhh{pwTIfG#I>Zvs) zlNWl-$@EmS*pCGs^HCKBOiA5txf-cF29nEY0(q@FFrq6P(VgUW4mM`$J^ao_ag5RA zvZFwuc)99S?i`NzaD4Hf8BLg=iv9C|I}c*OsIr18)YZ?^&6B6gmYNbQ{u5JDe=$Xu zH=@&egC|wt3G&u?>eVPWU8hUQHgWrr7BOLLrI;nr4g!U)Xx-%4V)wyVH zSHzp5qbTmZ&oxVqBdm+z&Wpf}bYof!lk#-Sxf zaR84~ukX>Ym%pZ7UAAOicJ#gc9wx0=*zHv>toWLd3^%>xYnu6rpbh4$7SPWMY1Ok! zqi6X!FxmdCpWcVM$;xdNvSYhFNz$s^D&)ty(4T!QDX3bP<*7gH|5jFPXDNx_Xa<;r z-Q92~-$Sw3yKru-?4Z@k<>&rNehE$H27>wJ_dobhZ2yA~>42u2-OqSk<}te@1+Ct= zWVCurh*8whvVq`?l|a(g>P;{|#*F}x?WWemcJ!wxudi4jPIc*YLPj7M|Ws+2!y-ce5s%eZLR;5g@Ef;f*n`13^f~cJq>I_GOUmN6)bTqp5Du z%rU;yVYi{oA{7^O39jgS&?CMK!WwH7K7#(zKPlf$k`nX%KG{v>NSZ7fjWqreC&&6n zl4#Ln7?rU%l30tqiKMxKV3DF4)C;TFMW}#UL{*gOe$$Z)s)`7@t7LOmx7_&79$u&|R3+a|Pe(q#{DmCCXQt z41Z}dH2S$KRy37dwxR)4K(jw)skq>**wk9Vw}z>gp;_gS-mRwQd%J&HeiU$*U`)vc@)4{{Ah&u< z0>>u$m;pYNmjJ6(fxyC8;CRa-p7f&Vrpc2_qJESMqW1_4_a4y+-XmH;J-Q2FL8#m^ zs1s#*iQFa7i;d+imp?ByRs*?&6vX)!KK&uN?75F>(er-jmCs%9P%Ax;dhfkLyGxzU zAXWV;y5R~*T+@?ycZ!lM`s$8p(L`VH1r zufOo9iZfa%!bRUFd5C~k!Q7|{hWdLo1v5UDK~fAOQZe1SX~P-v+p<2A%@uu=G(>?} zICUI6Av?v1_KD*W5s35JM*Ut95V~n3`A`rlM?B~^l63GA#SmJO4LVj54c?jpdLUZ8=3Q z=zJAEx$Pn82A!Ft8oYd?Ie_gJEsi7}bPkf|?LJ~!k3OpUh%q`0bdawEWK}w;Tqbk| za?LGp#(MLW(v}uz zY5U`orX)}xG!z0fSflrQ&b`kzlI`?;-#^}bBG2=jyPbRPx#ym9?z#7YyUu(h&%1%0 z)ei0AF_c_)Muz5vRtDyRG~yu-Ir6OBCJ9=&8*)04{yFTkIrXgk79km1mEUNH9DFJ- z`S{FYh#@hsCf#~C4U5R|Ol7FivsW$|{A5szhYqel0DU8N$W>sFI-H8U;{|HC#H? zn1-c%*lTFS3RlvHjYng_#+cGf^GbK5N=JPr@^DH5>XAAymqw}$Jp%`+{i^ECAwXWU zQPz7^4UCG{P=JTi4p}W5w8DP0K^r_8!U|f20R`$jO%Zx9?3FG}ezMC=lr-CKrj2XW zKJ9KBk49%8qm9m>6=V8At1v|oJe*cB%Ih69*d+rVPXN*m6ZEistwUYZm{+<%@uVgJ zHQ+luWfH)>V04h&qbffh4GQ4VLY{VtWrc8+;xX%R%G`lfZW5rS#h@*;SgR2Vg93Uf zlZdj;V4tdL0qq(!!S7|m#$Xj>FzQDJsz2A^WmWZHjp1y{*J{>oF60n$26E(dHHu;5 z#(Ff`#Bxm1(<>dYc=Fj?v7BHGkQ5lYQ4ow_APW*1P&`t{(#aYdPCJ5sWU;tKadY`c z%m6`W94Bv~krNJFDtT1B!zLi4(?~jdr9;vuixUxvL&Q4~q@VB@$2z z>x99V*mbu?#x8bjc^ShoRPL3|U$YsO#Ybrv!2w<8g*Edxz7GXD3GSGNhfrKi(70`Al{yfGLX_4X5wKH1j!OpaL=x z$7G#IouhD411(_F|5H{Y+*k)a(bn!+bUyW#NsjSq?KeF4r}Om zViAcjGDM8DQ&g+ra2WPE+zrE7HfMwCEsv+9u^d3d&A{o>$)K{bU6Xd&#Hh`p4xGDa z`wX>hcpip*4!^^2md)#6v~6^W-057_K4#L}pt6#$fgh!(!NABMm-I3ic`Yb;7>3i} zUl{fp-UU24r!t!>*>=BzQM7uY_cMw52D`dLmrcm$Zq_>r4hXub&XNA$7JcsMTR>0~Z;isG! zobEF{8D@4Y`J{96JG>djV~*w6u(Cq^G}D=v*b29$vnkH+Ub7~jX^nG&&os%^t1+pp zYNT;)zQcYbfx+(qr|08gymEoB5R`&+mS_InR(h zhfQx5qSzRtQlDwv+mlmS(_&@I*tg~9^}_ycO$|d#*!qBxrFGEEFEHOp~t$6k77}N727_39940jI%iRo=o@Gr#-orn8v?=+OFOduO|u-+iX>Y)>}1fIHm6 zjxd>S+0yELssMmpW)7pzw5K_HCR@_{=p}QSp8`(nS=LLlRT|4OH`%lN@9oB%yebt+TPi|HQ!O*?4ZvyZ#j9U zv~l^_d(=SLODa3VvXtJ=FFsRw=T|v%*dQ?95eV#%Y0!@y@6^MGPX4BRE7vyRC1c~2NAK2!TQ8$v;gMy4HLB{%nzF~IKur$u1qLEkOe z$;{5S0jK-z27+0FLRpMj<-oG3U{5!>UQ)Zi&z_$`UF`2r`b|dhnKp!39gzR*Eg1U2En$ zCJ@;x{O%>Ag`a$;<)U7v5q@mLaB`l*kYSMQ8E-Aw-_4#gGzg|$STvz7NF;N6G; zjd6NQF9+4OL}hFpepSqv3RP_BZKEzzx)B1!<#c(vIjYM5Lj1~hbQvfZ)m0F?t}ac` z)%jFig)ZwV$XHkBf^~#S6TZ5-3PV>x^E%G%GF*mKqa6;$!>MXjhLa23le+1HHY@n_#UC>p?w63mA&{dGLE^j}G>N4CCc*$eGy1WemaD}gmUDoC8 z2!Jd6WYjszNqXw?OgYjy77xD$o<={6cY_WxWL{TcpSrr*Lsyacy1Y^W^&9mW_^I~f zRUuJb9`nYps$WIi>gp_nu41iq8P6BhReZ7DE{`?qG9ovs_sHODUEOY?%d_x+%khaD zbdk5F0Iu4T$Hwul@RR%&)zy6sy1K$dSLgM0c_{|cW%;;4S9z%h;Hnh>31-u4pJ<*fzy zRpYf#m6s>g0axYK{R6tX{6m)~*rWP@nl}Tgyt=kQA5i56)Hn>te#h0ki>vt-SNt9~ z_*C`zmSQ_>OS9gVLheDcHrl%|Uu>~Um}zzl z)Hza#4q0JlnJAVM^959}z4X~%pp;nI92nSfGY)l1nFg)PEMq&4h*@E;+8%VL(+;Ou zrXM>!){7lCQpl_ndpqR%HB;=Ts==!E6nnRA$M%RbZQ1OHG{JjT3K-CO(aFnaGzymE z7b(BheJr{<6QQen3UnF#8`ag3AzfX9qpRRxU0rXZtHUUIOwE&+l9Tej50sbdnl3M4 ziR$XSh8|P$IHu%)3T)Ta?L)e}atYPbalgvt`1h{@>&ektK_S?FGiPVB%-=J zB!geMuIh0mr{uLPfGd3E5g)uOIiap&(dC&D;9=CS@KGlzbamZ}E(7(EPss;)3;^$n zF1nI$>Wq}G0{wM$)JRuHkaTs|iLQ>QI|5k0(GLsd{=;J?io#T^*LwWz0UxD>|s_MRavHm99>#=}JG* z<K+qa9of>=p+8+7^22(d=&KG<=?fJdx*bamEE zS4X_`0mat?s@#AY{{h+WxYEz!N-v8meJF0|K{9?ns;f(?;!1Ccw+u*mk3*y^?~U~W zv5U_4l=ULknv$MKc~|R)Szo&|9b1yl`qlbk=1*C#;D~YNA8@`S2*!~PSBCRF1z((v zcc!P{r_1X}%=Ho{-ktduI^SVRaJspEs``v~#K}`S)=Nom>|xXUie>$Z{_;kXwKvII zSoD~EU5MEwIR$VSvR5YuxAkIQPh<8)!WMfB7O*dEv^8v2V2?f4b$iw7rU1E~Qiz3F zCT?F?Ks76cyiaB}`;7q*Znl)TePOZ1DPwLr7Cn3C)aiMNj_o`<<}2I)W~25q@R+f- z6nogdPTmc(3~g$fFZPQ`FeU9Nwk}}O*-~f=(5L{@i(hzcjJ4~ybL`Nr-9zra{UZ|8 z(~i@B1d^}4eeU7?BZ$T!5G=k7yy1ruoBQPw29FVomY#$(jquAN4Zkd6kj^3kzbrDa zH_S^0(s?Pt@qaFlUwOi=UmcX~R~ISvtBb7q)fut=-dd)5iSC4l**Any#%A2I z=h)$#DCLcyl$~>$Y#5Et^n?V)hhw44P7vU6C?eV=Hu9beNQlwLzNS9UOLDq;2T;cJ(URfU@#5K|b z*NKNrRV(2n>*Xh|neuVnluy-8kE(vkC|jT>Ze#c8;X~X8KTsR=pxU5E*@k?gHaH)( z!TDqx{3zStjNAr4aT}v24jtn*_<`D>2h|2W$~NQ^wZZwQ4bCUq;78d8XXG~MiC7Q8 z2+T70)xDq?=i8sfl#RMkA zfR!b0-w8y^?>R*0B2}h<2};QZM&F0L_G}EYvSFwWAICj+$hQl`m$U)|fm*5WTn{4q zxCGK=B?xL_fJ)8g7b7XxL`)_7O@Jedh3D8XXR%?)BPlTgc0LPVLe{(Y;9(r6#nVjV z2}`xlcpf$vv}lV|v=PuE74H`1&}mVQnigpgAYB@0Ez!I5LEivY+S z!!KJ%xW1}2)9nZHzs54CrCWNE0XxmKni|45Ax!LyIXM1MuLEeOd zCGf0!Ic{zE@S$A_8YQUp9LJc^wFR~u&9*w|*~pD&<<^KrdsPj6O+yEHWRb5$!Eh|t z%xB=&M|G-xwW@rrwtPJb)`NV_UJe1aVH5+jEnq?i%|6*+slA{w21Se>V4K9GDm@!i zrDubxc#gMa?>~I_=Am7OPcmH;-u(*4ejDD>?2jooaZH*2G373fDK|h&ZMVdfJ0R8) z%|a~6u0012pQ3Kc29!@frhGOr<%^FgA4^R6mSf6i5L0^ z?=Pl&c`@b7iz(k-O!@9&%6Au2J}Pq%m9H+Qe0eeDGmEMFO=9Y5+kSP=Nq?WgfEZwt ztn_SoS`ZLx0tjY0_6QzXO5`f;7At&ANf>;zaT@L<_q>P7#FP>pKMUOzb$I_w?)~>Ey~;9qIQW}lo!86N@g4tVdWEN zf|P4amg`B#-D3PIxZEwqJL|akWu2=hA$NoE%dRy1_7PoCAH04l(4FpfH6G#dQ;&S` ziKYp)%^ErJm?|hW^v%al92?!UJAE&)O7_V;$BtvAfb|5;Et6rKEzgm_B;yznd^@D6 z_okk{Wee9eog6yWbMSbyxs(Qr63Fj3B{jTedN`Ldw)LizQjIN~lAsz}IDMs5jH9ws zaV$0x5W$n0R&Gn1whLS<41p;nL*yZt0-fbBdZr{3D(Xb`l(g{f!#G-gYzkbG1Dgt| zxJzwHohDmn@#o4yrATbjaCiYeN1+I3LG*{akA>09F( zzUA%Gse;g?t`-)e0Y}j^Xp+ zF_B1AguP;6P%I25U&&0qvMy6Rn@MG~&rPiR$3S3mQkHWMYc>2$c#ro1SA*-K2|r}Q zF9H4u6aIpNM=}d2zsr*u!td`WeR5EH%59X3c*k80SAT-9Sly_6&?ry+aDQz|{l!Rs z&20bAr=%C+%j0Qgy49bHgx@sq3l8|F&HC~Q&i(nc>)*}(81-5Gxl#EbzPH8Yxj$c; zQa`@3pJ>I7scrR1sJl?Yo_+_)cJk_5f(8p`mXO+YFNczyY z;d|BwAMm)D`K|f_)7EFe=Zdd2zRd=TLGQM(G<*IYJ z(kq*^xIRydwan5&L7x`Ef%dpwQ&Ux$kr4{!=coG<5r51tKDGSpP$IGGp5-g<8ES1E zx@YCkrOlf!4efk%^X5myvpG4zwD{oND_7jLL)QoITE61$o$=s?hlhtgx_$r%-`f`@&5eeEw*W#{F!_4)vsDOZB}h(cz@S`VQ|U@ z!IGg$csvym?WyFS2a|su7TuYmdtGL7g%l1iPOQ^D2l3#-H})V`+@FD5$#7-j3w!yD zU#KcUq9x?d>Na#rV>&PQb)8J+yOX^frMWsIC^jn-FOCA_`w|{1zYk%!HkTmL`yzBR8?2B z5O-8n#Epk@B6RT1w%tAcH8;23epm9BqVULx6GxJ-i_G1J5ARO?Nqcegrj8Xg*>hHR zZW;`%TEBi}VD-wCtBHo(mYZfsHRCq!TEV!T!wdlqb~q%PaGjf+$G9PPcnW3~MkZ?h z6x@C;N}RZT%YDbR$X)#?xMeoRBMwUV@P)q?mn%?HgGmw#(Y(kH2d1RfiMxcWNq%{E7_&{aR}*u^`w zP*qjf7&((pDP$VFCFk>>&&j*)Kz=C@_?R`8G&ZXB5VuUsYB36P&N=a4$!bw}_~h}U zV2^+tvgI2Y2mqp_b0c3GG41rV6`U$_E)Cj(iYLvmmBg6XA>u@ku3y zRxG@&F)Mm(!Q$Jt#bVoTTfE>{G^_D8cYNnv%a`A^Gv3hXYpRKDJ+o-hnXR!%ldrK6 ze8;W0hg~(UMmZ;32buSC(E7h|>)7D%CZhQDb#a*ZH?j0Oc!+I0KT~nJNVqVL z^+XBG0m3oE`}|RwF`bZ*51N?Qc%MJWbJ{Ee>Y1q??*#{Znc3fQ+26@4lbzcA=x-s@ zJ0qi}CgP5`Yicq)k=E9HFnKtTpWm8D7l}Yjc!YL;USqVu|HHIz7sR7^K7RR@`=1Nq z&Hjl)VpiJKg5)34lW#u~3Tv~<#eE*JuOXD5pUtny$J3Kz4WV$TRs3_B_+)bBjAR$2 z&8o=>cdNSrUk7c%@K@0~gu%wb<2F~N>8P)LI#?R*oaL#WAG!V*XnXtSw3lf*DaNcr-t`_s7>|fdN`A`mWn5&6`O`nM{|QIa1DM zR7Jq(aviQZC%lKRY}&jI?`7cf`t&^lk6<0pHUPsc8m)@TnmD2hA?o7NSg0h0NzA?T zj?=e@SxH^Imi(azC;#*0;UfpOY}_Xzfr5y-%}$Q zk#HU{H>XcSYqf4~L~YV4v8fB^94dlR3{?BtMjrXa#4)f(I!%t)$aaN%Zdawv8d_{u2I-BR^?< z5`NhM|Bix3GK&b#bng@yX8TjqgD#a09vHFV{S|QvbP%rjSW%a2FS(j&J*}bX(i#i% z0~s6;?M<4pGn8h931qDBy&J|lx<-1_dbUMtde_hGJQ>X_S+pY3Hr%~;(?>5X*xi2e z^xAuBa%#`JqZ`}0*VJXtS=!v#*O*^fnHwt2sa%v8SQ(hTw0+f%xpmo%G;v)E@lV*8 zBNa4j@#n^$7EU%ni(VsM$w{6O_Xf0!f#k;nz&{iChfVw=iG0G2t)r^A@dSO4)=OzW zS*mh|2yQ%In^SYo+S4c7hkGu3bkpAMUF|h12TC)er#j}Y@2!b$>q+Y!>FOABw=`zg z&E2u8ed+AL%7Mh9%AC?rZe?XYdegjgPIldz?zW9lmzey|$qDf-tk-2&pDJqr?%>`?YAzjU$dlgX8H1sb9q_bc?auXnE@|pf6xL4v_)sO#Byhs=9E?k;)Cy7 zw&bo|3BtA#<6Hp#&G3}4jOBNdjr2~kck;KG55hHoeC^;MjDX3{v9O!b4UlPIR0Gm4 zoFU2M?)`FJAfuyQ`+Z=dv}Jxtx;BF>QqqVfw2SXe zA5KK#G4bTev%6Ybcb#3i=yvGjW4%jm-yVx?zkNyXiKeW^+dmr!Os@{tHsv%nZqbtH z54Xr`(kS=m0oPuZXA(}nE(yN`_$Q{xf64LlSSm%bl5(Uszn%_YK#jk_X@O_X1Hr? z6C^DZ5=FDy=C{r69qEV-uPw?c**MY~-MnZH13wfWXsK(Q-!y+&WkD>GJttliYFdJ= z^X9r}Z}W#Nn$SuiX>!>{6QAOVIX9w71gz${F=qrs{PcC1<39p{aWM%A__N99#qYJh zaLol*N9KXtH8mbjBmp6Trw9xOB0-oOS$K@Yh9QquzI*iR_bnT^ZOt}qW8aa!*((;* zY#fYb#rJHjS-c@K|K`3Gw=G`v!Izf>Giv5-Xl)%R$()&&<*gmiXJa!a%{!~uKWo;E zG*4p1oWznj-WlG?Rh=E%duv%%VkF30LOx@7gDc=Dqd%Ij+QY$#e+gnPvHJ*~U~dv>9et|8^6IeLSvh32H@W2XFgu@Sanr?%|T3a&Ox7;P|s&zWdzy2R}}87JYO*!khw`9`m0I zQZ~rg(~z;t0MDh`ZKHxb;9){tEb2NiC#!aM+u^%{J;#&(8FW{dvLnUEATg^-opE9FN(+U&$JDQe;mbspguWj@|n-x zFJ<)PFw1E0zX-6#FX5K}CmEd%c%d!-MU_7lenG+~{}Q#nZp;6)$`5~}>rc!*vi`Fc zTzK8zg`^^QBr}iTq`OWI3lG{q0qE1Va^tGpiOE6N)2y?~`T%F{nt}W;+S)&_@`DRp z|KKKFTjswqxyROT(zPZ08I`{r`IGEnWq4+Vg0GyccfDe^n~|CO-^3rcL$@0SA3y~Q zH8M1xuRt?uIXdTqhPEA6ehxjA`x;30U+L^bmk?_9|#{7CSA zk;c5ZS?UVK$mMpEMkev;0zM?^t0y;!Y?QX(hgc^|14cWTGc9csS_HJ_V8)l@+e>_< zYnvZVzAK70xZ_P(4bkVs8-ZlTu3@T^%Q6ig0~KJWO+JY+5w0xM`7G*`8mzPjq)QcZ ziF!+V3b&B)L``2^<*J@{-sv1Dt?sCpvoc0)b!>DO#cRsiX9vonMdc0ovzK>^Uj>q) zZ$0F(#_j%yamH*ib!4MXl59@Q_ls61^5m5x9T=5i%a3(1$m>KL?jVmFLsB6U;-lgx z#mT?SdL<}+lYBNuZ2n5I_+|1NGm{T7wa;WHZvh3k2ByFhOo0bn1Eyv6qz!)2fKPg; zMT`#Rhh{QBbR-FALCGE*U{49v{$1^t@EWm(2mIi$BhQ(Kg&iY#wtPWHIJtmu>$Qrj4MuLHgP)T?5f<$?!pw867JG8&Y z*hw5e;(Ca2CvOFMY{D59lnXJm8HEmM_#Q z`8)u}T)WH^a$~J`@ybGW1bA5W8#E#vpe%^`4`YeCX`Naya zSndzbNPaCV`Bg6}{<<~*zIzhQ9k&P5!nbWXdMixnH{{H~R8%so|SG~qm3Qd>RGdmw@>`)1Tm84)GO{|7ZX4dm=e~|S&n9hdX{3ZA1jSr3|w}>Z6-%G~Hh-Sg)T$CeVzGfp7urHL>i%i0@&KbOM8s;%%DQ%TiQNu^|1D$#AAZ? z#$|tLwWqe>f5A=>d<)p|p|SN~&xc3XgON?q5`7vC^$ZNO*sGTL6-$Hp^$Td+!!+ox z@_N5{tM>JY{^q64sEU0BZZB{SxFV!?rp6SIg+{VKZsJ+HcgFUKv3;k0O^evrI|;OG zJ+pZ6nXN6xFIitb(04DPH#rzRyN6Njp&*h0W})&g<_AdPTRJDe@B1+9qZSg=?&gHcKp6z?=5b9sJC~guJ7#a9nu3gZ{0F7cT-pQ z=BW6<+zlPW!>e}eST#J{v0*OpB>4-x%<=!1k|!rwo=7-(FC_eu>!jhmaOA%TIPLu{ zL_Gm`DKb3eEOAroy`b_*_@`C9e_S*iMz2%*dR~A0BLY!1yaXVg9%LiRXdFl^& z#R+e#{$kg^uvc8RPhN3ndLi&2uXrlnPdo4?`6}sg)&Zvo7+GJQ>Q9mDBuCW9`mA#3 zkBz=oxxkcipaXk-WP419Y4AJmsE;&#ng5FGH0vC){S}aO9d;fky%mf`_B4e^kY#Oz z(p-z}dj^}^TC-;4WoGsCM$rXY_uAH2X^B;0hl58VM$!8pJ@-9jE zWe1%083<1M5C%t|2OO~XMJHqTS*y`dv>(UjO`HmmJ_`@ zUR|%>ajs|0{p;Iz&3o>-dAr)zpTDW++#Pz`op;agJ+Vda-qVf$rF5)k=~(ZYPnfac zlkm_IZWomBSuwJtCt;b7w4|`l$0B-uS0XQCW`-d)>38|<%jYa^oKxnn$eNj3otwf# zKWvDEn)L#|5QZqF-)$e>mNTO^QBhu=H8aPT{e~e`jbAyYz?t>SUtuh0XOzZ5xEdjK z?`996OQ@JJ=6RyAfiV1Vt*xTiKy%s{4eezEqp3j}vi+~4eNFK~MyAheK}oA14yhq1 zYi@Q)K-@{GPVOWRf#fQZPf|{vWDkMls;^E-hy1anTk^#y&vFUt5Vkhc0^qt3)*;y9 z*cYB+*}}-R?a7MIjb7Ql{mSTLkBxk6L9YAeM_9f1f z(u*FD#dtqc&riNF=(a``z?obF9*c(eo0#4&T0c9Lj^b|-XD4Y%<3D-U9bw(4(vm#Z zUR4;AoR$QfG7D!^n{(tEs?a0FdZN9&sIsr8Y(uASUh-=x6MBkjbS9@PT9IBID_rDT ze!sC27{82NyZft3GfNn9(ZViO%A`{qP#i&@adJTACv3Xf-8)-bcix@4N@+W0^>@{{ z-NDusv!@_8MQ{67ZhFc57O`U4u3t|Sw0C$6Z_9@;GFk9k!b`;?L+TlrIaCpeg{!TB zIrrB49%9}W|L29m7m}IQ(40BM9G*-%dHh4n<4MKiy(W*_;TK&B4j&BgfE=Lwurzu} z;?PX7=YHDA&2Z92WbNYy9Co=?3;Aq%r3FH8mZy}Tv@yV0uG+DpwNlpij9K5PqrS^1 zXLx2Ts==dnJ-QKpUyFA43xpe{k}t_#8#ip*R_$XLn=nSE2HYdL!!~t@Nm>&aBKt6j zvC`#>=bDo!($!lmN*x3CL}6Zj54IfDj8A?q+8)RWKWGjgd5k)+(v*R75dmk}D3EeZ z=^RL4+w%_2N@smOy8PZMU&ZZ9mJiL1uHG@YI@-MNrnW;hGwb%wU9z(&x_WTO>S)UW zcUygSq_u8QeM>i-JP}-^v%00KG`DP4{i22#K!p(;IA7h2v1bg3gG8p{pgiUv3IDQ! zM-Y<_I5h8ot3+til)FgjCj2S0T-Yp^#~O17_G-{$m5x}NJmSDqEH0R}c^tW)v*iX~ zDxAm7+=I5<#Fu%@mmT2CGLrb5pZF4R@FkbSD8T;!58%tn!XhnK31K*PA^{Ia9k8v; zR&#zBa#TL?hdIm-ZoL z|IGTGc)3;%ZD7jg!Z9&^Na%yOI^V)JK#8-CR>4}E=L!m%$E3_kWVyM z_=J(o-c4~?J1t)E(TE=EY|r%uy~bqPzOJt?-!N{aF`J(wgmZ?S886OnnHx~nEM_X= z_S_Uh1xrPQs>o9C!TCtGc59*vQ;Lo@kwQ-PYE2~WUi?2pJIMh<2J6nD{}Gi34$@%) zQoRoz&|VC-{n-$mCLn`#=+D}Mfy5X07|zH&Urf;HK1tV4n{;{<^dLX1gd3yb90iMq z3l;T3CJ%3N@aXTrqZB6xEkwJ3e$g5OIZY=I#lqyPh*r-Ov2gNLm^wI61apUJ(4}pX zG=N;e5X{pO$nSHjum$*IBY2;Mcep+pLbZi~($Y*33@3l$Eh@~NQ&>=k*nMyECt(rH z1fVeT?O>$1u%kPXudSGPB-pICgeqrORnbFG+duJaexj?rptLrKUT0#C0Z&k4Axpb) zR~4@X*yYw9(Y`}kzEjr09m75Y3>CcX;=CGulG<4$za26)z@j^+$PW!N`Kt>Th_9J4 zw7bnMg3bsRaL~&*jU_gV&T#U3naS^kMJJhtbV3tkLT;!QY1rMM-J}Y4EKn8TKX;Wo z<`$)I=l^2n+rhUpe-U~+_*U9miqcGN?aIU!!hy^4%wMKt{M#&PQ{fj}b~s5+ng8rH z;Nu2-a**&d^8?P3E4BXflwb8{T>DS5oUQ)M)8t06qg%hq{6Tgk+7kBGuFe1F;D6e> zH&)XaNXzmEXZ1|n!~I0PWGdC6@;C-U-cNxDcK)6=0F!{%f;8V9D5lLqT zoxyyY{!WhgQL;z8oUF@v$KZ-hJR%#^h@^8X#4UFC1?*Z<>k>{QBJ-cU27KIrgU^AL znICW-k<|LnQ-0NpA8S8@M(z^4Tb~GFM~`|FXl>2cemt{sRaNq8dQf#LS=`mFeLpvx z*Ph&*^N#A=rJbAGA$p?;Zkr`bz5WA>s_!KW?Mn8)HGOxm)DRD@czbTKihg(vu+ zvL}!;F$hNj>OqvI8dt=&@S0o5G)aYD1fLPDQsEaIaN+}5&$t0M$J>BIu2DTWMI!k? z!Y@PKk#5TM8?>?afizP`Kaz2A$m)xXToA4uqTY23<=ybOVQ_W?MZNfN@-S%Aik|ks zDx(-soSSMw5GeU{aB9npsh^b=5ohuDi8Ac;Bg0_ho3=;^E=5Jw0cKlOy7h z4~&n0Ai3{n9OuDxv1~l8=8BB9mheO1eF-;WttFf^K7!MI1QaDr_Yv&n`v^#{ev;z< zWIFr@d)be0qw+zcJoSz4BS@*g80oKpFJ$|;k02$z(6yKEBS`JfMZ(W4M?M_cA0rlA z<|mC!_9vh6bANuqyQi{0Mmh3}$o|}@e9)!JbAPaVno@tU>$|*rD%;2IX-Yc!!@i)@ z{ygpI56LFkpR*1)**&s9d8$7}uD#OkVSKD|=#QiijT`%%WPRLElWtahfobbA=;QI8 zH|pbdXkC)^U2*NVjRRR268?-MKdo01e%XM-f_TA(*H^@=JfEH8Mz;a%m6|rUi`xKH zJ&zf1+y-FpAM*2U0C&Rs@g%$-G?&=xJpF8KNBZ{OGYr4NKAObh z5lmu=ff&O-|D#qI^ny<57=IM!h4_(Hd9*6zhq_O<1>BvyL%ekLKJlew@A*5PfBxj1 z*kek5JCOWZ;5UK5Z^TFMe=P9e+2qaO0Ir)Q#3dymUgMFJa2hcQzwDCmbI|HP2wc<| zRIS;H1WNy)o6cLek>rqYNN(F1)UL zVPo^n^bV7`W5$@OtyR&F!)4tgJ#@G%acK3tvD(ar{T)Y+?%nk1PvKFc^+)Xxq&uVwmr_W ztp|QWt1UW5Ih%biJ&*-fUr6T7W*L5?dV?;bUTT+Qp0nLz*L^JWBuz->In(L9FGY-` z((-9Xe@J#wIl6-u2i&R+H1ZrYl@t$q@vQY(0>tDE>fPgzS5qlZXPROIh64!;MdT_K_eq$DHIajTPjunpc|~9P&GG2zyvC zoBw}v;?uPi*wDF11h>OqQWFtj!5ba$TTOo|a`A0DinUuj6Axv~d35NSry^T6cL}dQ zdDXLe5F5qQpNdr4CfOJ{Sgo+Th?T?=s(ei7&eOMdjnrmGhT4uFN)w00>$`3(Ezd10 zKD0AgK}Qrp{X4Pf_3#XZiv=@x9BY6)q1d4vLEUsFi6SpK^wQY^Ma}YIMGl;`HAO+Q z-xh~Ik(2MsYp&HA%vLATU&7PKfo(i~|wwzt>xK?DM7z7Fv3=Hz*_~C%8<0lZJ20oMly2I{Ni+;hJpA4%MN(AuoHaj%vk$yc z0qd&%J&hchJvSV^Z;1E}2k@mVs#u(NFJJeiq|6vC!IpVF9A+EMQZ*F zDnI;C;*_nPrycdsx+&{9>wuGAn&5Pn3hk2KF!>bk*~oURa^pt1$w8u*Ss&oM-lpPp z-pJ2*(RwS}zcTr>t$(!MO87I5{KP2|ewpz~bIqNsQ{z-9W3PCmlnO{|r}TAmG@Utq zGs1O6q5AU7`ufHae|h%I+{o8$Bl=pfrX(C|3Hgc_msfIlobN|!kkOulFLKECw>ZPW z9m#(YX?6}t;>ILvWN9^`o?tb4ilLLs9Q1n}^m`5ZK-%OZ@9_-QK6wU9TN0S~q;Uod zWqIuXm3hcIgY}5`B6z!uPe@74!qkstaHRbVR%Bhz&wkdqtkQl4YhQ88?8**0gH=*i zPiL^S)q#nRnP;#b5&z3GSWe`{H`O_mN8I0JtKccZU;L|vvp}E;tc#Yk&FGT%HQaF}a;B<&8=P$OQK{d1Q~ ze-+it3!@B%AtP>U->%<=J57$mJSk^vn)-q_z zP3!a?p24ZDo;UM%x?iMILR=4>Z2S<#ii%)OQ*6(Y-pdKH`6BAN;?M#}wxh zh??L@=s7Z&BR<{H2kZ^KW1Ky-AA;B*&m=r2KIJ?Mk=%mGK&#*xbvmJ#=CGv;$OvE$ zR{v7md}xHPUL!PbV=(hZ!FXry^S<(m_r8aX_&ehl+Ef}SUyeR{hKIW;nY4lQ}ONgfk?u;%mpsCR%23$s$09O@V;+C-uw9Db1((A<>VX`u%?;LBQ zeLjT%CcebI5?^QRiGcyQmt#*f;O>I=TKP+5%qpp{@;;G#+~SA1TU40UadgviP`-Bk zHhuTeT7T)^2E{Y8mMrbA0sRYRsy*&|VXbsK`X;)1O5dT1%HRD{WrClgzN~;?V zU@bqh=f3{F^F5jK?qvpAZx+_>pIE7VndTYoMn4Rh3aN#8W*!=&2R>OQK5*-U!MjhM zyqgXe-Q#|6TwFjB)1Gw|DrfzMzs9iq!UG~01j!vBhPdn)U0w=x*d<{ z%G>ezJ$qlce`ftoylBm^r(^BdPAgg}KR)E>CFzo~H_tfWq)STpWe1%0H@Fq}Zsh1< zl-YMf5iNXQfGYPD@dB@OvVHQk$@(5eIV1ApVU9>3xD#7ztP52u77uN5@apd{mgJW} zD`^NB0y)h=G%#;`kAq`aG~7r#?qUP6y8&*>S61^WW+|H!l7xx%%!~ zZn+!YU*mVx_&MCZIVdTa)=A^6mCBhdnhm~iz%Mx9kj5(i*=xYZP52=7!ORc1!KaS; z&r^QY=U1^?4jYg><*IY|aR@HeeQbYnU_(Y)r_tXXb}HnR)i#AaRb8{S;pAszm!~1e z#>!ZEj(cXF-v?K z_}UU5xc`c_8JB|m`YjTuKW2-M_?O#Ys#hy{x{axx3cuj8!-?uL|JiH6$4&U4c7>@f z^N$PMwm40@=PAGJv%4JoqilH_fwj~0*(5kc>|CSsCef!S!uu?0&rNP0zK7%OWxpYv zRR6BHcG&2Fd8^=;8DDMkoykYsIGvA~f&Q7=W9u{+(+tTVD^1*bjgd;$xU;T3S{iOC zvPKN@h%v#;-Uc-T=VV;kcHl9Sy=As|1yNN+H}57i7l$n*T?ku9zUg`8`<}1bcKqpf zAdCvv!rXuptt?|@q(Um(h+s&C8xaf=PFy1EF(Mce@bCjm8(c;(B%syU;l^G+?W#Y? z+Rkah2fOOOV5!#wxXr^3_-7cdEOJOgwr?~2^-@OH*`*qC|2t}vd+~GJ_1D{rpKI;K zf0y^-zi92nFCe-@hfO2B!$Y1#Bl4Q=RFX=UWfQ^_tFx+eL?Zxfk4s+Uqt%EpMX^-^4FXB;ft`Bppu<$zy0>z!PEa{ zFDKk!GvOKm;bx;OqN$((3u=ST_>bg@Pk4j=Q7!(E+{R`>?p#*~WVMGHq0j|}ex1FE^*@Tzk!YclgC=G&KD z2XnI|uuCl&R3!=XqLf;Tm-HCexeHR#3g4jHH&QWw+Q1PMd|s8v@Se4xh}tnQ&tuGs z-WW6Mu}X}q5;PJsR0*;OF%m{@TTOu}H5sFq;XQA)t^L8o=!y-BJ6{yXJj2jd@N)xN z&2S?|$!JacgqiEIDqAo9!-m^eT)(k$yR(eknG3&f)?q=tk6P`!2aLSl#}qW*(d$Ri z>%YMK?U{TQ@SAvEAzJi3&n!L+a)a6R=g`fP|8;{owp4p!V#w@`_KJ!3VSG`Y7`;pGnX}_}w(_TxrklMaUtaZ=sX^<1Yv!5qE ztw7*H_s7uk3igKbmZeR)V3&=yER_Tb^NK<)qaYm-@gQiKjFGJLXwk9W-ebv^f|;2=!(Fzxub2OI_nlh4;zVEHi51IF^|`e-le-57 zUdo*}x@hsC?(Rd2fi&@gY8e<|J%W@R?~~9$!bd^L6WPZe*D-wkVFRL>u}+4~$Ty1X zm+V0S{)V0IG+85OnF5KS{>EAGS-feTe^{2Wz_RK%NwQKnB zt_@oce@lC5m;al zkf}D?g?+WDRFmPD#-8`IBs}UMVYsHbu%f-tm)~04)OTHS?x|=l&nYbO6^7d5$v37@ z7TD3QpO^h!5m$_`vtKhn-*|C)I&n7&I3K&a2m`jA_mv-dNj>koI5Rglb7oG?jnDcD zoionN%$4Ih>KNC{7}wXK#{@yiQIr4brlaNb97u}%n1h5VT$ub1QF&ud)V4z<0v^tJ z@ZRxoBP99yv*F30IQ<^*c!lslWG`gluOX3OgWI@DF%r)`BZx^~GS=HWwxn&S!Z&M+ z-glt8`#_(*WtOjE$lZ5x#fp=?b#*y&BA9QB4?#%e)YW0m$=XRe9Fv;PRg%By7=sFK zQ~DPS2{2u&MD~5xY&p4?Xg+Q0B7=R-sTf?mO5D*pSx>z-@p|3{HLykW6R?GLrqYN` zMjCV+8AK&t{7X7=?mo5VM(uLdO*4F0K)J1Z>nEt6?Lv4;PCsG^9x$J%} z*f7U8@%kI??-u+h{hvPR4~oKKq0bPdRk{AWY=1x$6er(IPyS_PUY@@J+pc0o^3$H= z<3wNdSvv{Yl||>FoZq=HrR{XI{&ldVDN*bx@2dX#H7PEh`*K5jsmRXD%}O*+JUl(^ zLE#wq>X)pWbYbKPr)NRGo8-Rf^*N2zfNOKxzpw^yjZv2?KV%Zopn>Ok*>vm7lLeU*tn9bugRICJz?hdcwcsX#q|vZx7RYBE^8Td)H04*=o=SA zg_pEnV6`oiJ5lEN^ZgmqQUQUf(zOKx_Il)))zVSot(ZUO22?3+>?m>P`13MbqZ1F) zTGl$*p?yW+1hQu3iC44a>`?slCGl;x?Tod>FY(Voryic5d5A3hWZw~^Gv@Mu>Y6iGp(4JtOkl@=) zcm(jvJVP#O2k`55K~iXGNDsTJ=%j;rvkf-Ua)?)y52E*2XWo?@M|jXWBcYe3gqc3trOe?6lBv1tpvfdXbe5oTsVzysZwh#AVG_Y*4bq zCG;5^l&l>Iy=+0A2HNd@7<>brdE_ufwUHyqS7Su65?Qwsd)O`phh8QccUf-rUUS{Dx44yv+;vF2h>? z@0dFr>%ds5&#st(xO7=Q+pUJRn;*tCE|BD+q;+TKuC4Co)zQ|CvBu4{-s-iHxocV) zmNahMux4oP+SZo6)!DTN+_U@Ub_~p%)fTDioK;Zh4~K;)iq%v$7Y4fGv%9K=HWR4ZWgXMW_Hj%vvuf{_}&Lw(Qw>LyIL9Dyab7A%xUq1WMW3g?)jF* zT(vtvbUf(p9b2Yp!kp94cO!FP6hjwUezFhW4R~|1M1-J!VP`&UT8RiSaYVERGcvt- z-n1FHo_M^c%bS;(?#Z2@xAy#0)AEGYk9H)lZc|#YXhBBVJc;dQ{{rB#(;Z^v4yaFLTVS$4x20QdP^)KF?!eNLh}MB~H1{ zSdixf3{rEgv1r>dP;>3Uhz#s($7tZgU~f)#h;NYB*Yhn}h15Ib2)#oxUVNIkqy;fS zUT{ez%{e~0AB%Y?zNI1&P@h`iXte~~Eux@dZBNaa+rrx0;;G~z?d^uKLtTTdjhmN- zvh!E$cr`z|Oy4-_5uNnCl~;3it)9PXcDSx2dsbrsG~qCs+YavFJ2;$i&ZQa$I^WvU zRg%c_l<6V9T>aP)%9X>&h00E)N^df(xVF&99_EPSWlqyK>x!1ot^s^ z_8;hEc}G~0^nRA9{s6WRge!ZpQ(T3o-{c3}8UUNfaccmM2&-EIw9>i7ImOkXpXL81 zx4hWjTv-{-4wvQqclgb`%Ho`ordNySmgd*Z?Wh(XO>Qb_ZmiF*p4%7+M(Sea;+f>l z<%yQ2g0iNhf4f#w7z{7>^o?~qmX64$1pvO z{aIM*F|==d{l)q5mwzZ}Y;G*kx3qWftML`}t!`*o)m6o?|7u#$6sc>C=nJdr>V2iP zq5Ov4hB+M#k;cyY{~``phJTW>{?wDj21C-x;>sXbSJkx&b@vzfYW8)vZ_#BF^_`88 zhK@O?I6qWd>Z`A-TBzf~tq7V%<&*Gsi!We4luy?KzN`bEulXmgDzPQjc_QK~S-Ngc z`|fTAr8g~z)-`q3we(ih*ZF4E2CJ90HO`OL&+V#HmKX61t^9JtFPkHNAJ0MZa^m## zbh8k`r8;%YQ-|C+&g(^G@^kNTHr^EoY`ErxB+84)Zt)4tLtnuPN615G|GEvkq2&>8 zlEUiiHW?Xi?f94Xv<}W+to7>~^y04OrUP9w>sKx+Yg<^>vR?1n)qdd5UkQYk?w!}z zk>?Ni138WTO=WFW)pa>#vvX^EW;ZXI>+|M>dt3TOR}mItSlDpt8{S~r8=P(zbu4A` z^v&e`Q@2eaR}p2V>0DFMHR~dQe%e{zX;BG}@zg4{L+n2qo9uF&O234&_OD?T`7Pn? zeiY+Qs|dw1u7UK`VCmPZ{xn__3y(GX`3_C>Ih>S^2Bv-yXIez3d>zNc?u?1u1z`6% z$UQ$}J!HbqD)?{;d|biDuzznqlW*LtG6so8qc$5u$a@yp?WS#Aa}%6zz%dsdt(f<{ z5#}ei!NWDtB62>5)mt0<-1V^b*BFVvC;rLI0mW5MUH765x8xr11oq#Ne>D=* zc>U!3_%a3Z1D=OcKeynSWeT1Jxb#=b`e$7OUMk^}&msTcTlILKR{0?vTz|{k z!NEzcKe%;;Kb_qm{ORn*8rM;?f3y-xe2Jq7-(h?Q$Le=P^_#SpHDrs?dgJ=64UhAx z95i0nOS~^C>${@hvj1d{N&G4m+=u+XV!15g6$*~;Q@Q@kq`T2RzKio3Q-AVZ;pfN$ zSAfn)D`(S~>5@UD!Wo;>#SI^?%k>T-(6h-j@uCxF{-vPXf!D*LVVcR ztN=|UuB1~-T+1b{h&5U!(x>3+@jeb(K?Z@Ao#G-z#dA5MFNlt(J1k(q=d32}V>RZg z8|Rwo+bw(-i(HG~*5|;s4E_oizR(n-uQX-NYRmIgCR*ab`9BCI@O7qt$KR{aKFeW| z3`{)VKJY4aK^x>S5{%bpCp%q_GmdYfP2%GX_!SJlACyGjqzz=YmuEHMs2JSArGCq! zFKe4cwM_#rRq(%>>~Q^-T6ce2wSFAG+T@pj)$h4=@JCAfG&}_s4vT;3z(V{r?Tffo z4S&s4L!{3%b=Zg4QJ*o^Q|h$_oxEMN^3`Ij)E86eOE(_*)gN~vaF;k<`tP})4~l#L zJNN3N1!C>rW+i{9y$NX&%}Rb7V~?TLYTYE^e?>iKc>oE|>rySROA^lO67rAXd25SoW928Uvy{{6>1Lq_d`@ zHYe2nPu@4e#U62!M|(5Bxqjk)@dwY5?%Mj?aQH`sNw22o-A-TLf^F(bGDaRt(!3wi zg;8GytoeQ<*Cg`r7uonL6vjmp)^~^S4NJMjuypX7ZF>UW0sBQIlDWAe9?^98U5UrA~}(@?U>137QO zpHuP|wFugmQTb>5jq8uLb-~JStOxJltY5l&xvD=R*9q|ZWz_dMj7=-*%TqLw@ntZf zaY71zkAgD)CTRnSi_Z;7&LNp;wC|La+T^>Fqr#gM;gP2V?SIE&U;P z->NmM#iGs5fJ9R}lN^9HVZ*yK7{do#Z}He+OaaGQui)bb9A5+hj&`_;t>|RjLb<%_!9Ni7m%bmW8sl% zzkxPFX|}#8K)TVk!cNlzHCJ zS)8`ie5GcHV^c61huc_210YI}_bSN`;dC;kIvko8AMNcux_I%io}OciZ5!a%-a;i_ zBEN0}^BvZE;=6Rxa3oZ=j1J$ZZ9Z0it@b}JesXOTek)~rDi)WNhsKH9#c81zEGX?; zxv3x62}3`y6L!1Y38OuBnp>2Hm56R%L|?C0GN?)d1KRO&)LJ}Z?07ZMj+Y~?Fx}Yk zvRcHBmxUusTS@D)7L+1QRXur(c~LsWtV+7jER`jUm;u7dDiLs$puQ3xQwh=wSMFsUmxLCaKo~;N8bcAY{^q`T1gHUoSL#fXXE9i86mbp{=ETPnA zgX3S}J}>`&*z4)liT6&6bJtKi)bx2TG}5+C(N0oF{vq4Q!}?Yu|BMX^pQwUfwxG1* z$x!TuGQWk=jor|ODQUrU+6`SXIjJSFCNmfD&W!CVOXjI4-oS{&Qope@qiIo7L349K zMN_zDSzF)A;OgGCr8VKkih|1eP{G`Rxh+c~Gi#TKNZ-chMIY{}*|sa%xp&@{{C1agd+f?BO84U5TX>d+SD!Uu|tSLTg96BXbbsCj-Fb z8t^RITw{*{pbO^kcl#SnhOaa-J~a>G6qD`|jcmV!2N)UvaWe?xnx1{S^Z1 zlK1=rG2Eef^C*_PTUv3{GI7;HDZ%+HAL(%;yfRAYc&~+}jMYk6iPYFT&~pRjR5yhq z;C7Lkm=u5hB=$!g-erU(TIdJtXp02(1mU|@G_@1EYtazF-2bm==>AM{XetrYTvebJ zMQzAP{3#Iy85!3YKf35q+BPngPV$OpMkUC<`dzcOKQ&HEC zR&%uN7=(Wiw%Li#4g z$h7H0m6arw{4oDQBddg4+rD2*|B$Vo>PE6YzE4QY<;-+0?qLQlW>T!4oW@qc%Pfx z#NGz%?g377U-tNE&Mj$2Im%hRuM*%`mo78)j-rLN zT81l^d@Is@X6!8akCwL8n!y?3-kSDu6`GN}12_+&FE!YiU7#;Q zy2aFwXW>H_{Ozn?%pWbyD%m}6+v=M6%a_lusadlix}!9!Y+b|p6@V<8UsJcsehPq1>v?eq3 zBH40*zdUH9F%QWS7i}f{W{I#Y@gmt`R>vCE zfu-(EQE-T9?Bse z$SVt^GJ!vTro{yv%b)Q6jTe}(2NhJg#to9O(EIIyRb#fes)@%qstR?4!N=Qis}${) zQEv>VA59O69{6MTf?%tzSNif26C&asXczA$XC^wzeStjj4zFncH*N0$7*}=d5AV5G z+SRJ>?v+->vbt8iODjuOvDK?B$-T+F$Q@T~Awgm7o)HYSwRy_X&Pnz0eH4aH;VM0o6e3ZQ+ zDYwagc-~AeO^Z!%Mk(=Clhnk3k=nqn(#AUYNck9`kN7i88aGm&Jy6r2Iur5OHM;E3 zx~n;ibwXK)pYU94n}iYroq{CkK?o0UxW>NDbv2hi#E|rA`?NiC;2h`;4UGzNR=NDC zf0Dx5CaGrjk-(uw5DxCema?&9CV%EQzqP2mG=)6>%je^MVe+4^ll}7(gwq=134b^+ zk!IZ?G%L1_l75cf8%_3h6ox*!ELtEdv3<&7V=L-TX@Ag`w6j`)t=*hLQXL3A5fpk{ zgEmsnHG0W=ox*28Adb5XI7lOl-W~EmDYJn?bHeTDGkkz4w8vaOw%8Be;Wr%`Uu z-?0J{boB}C9q@1*KE?1G?OBNey{)7a^4hIOY}VM95wu0w>VbV3^-;}zaH20~z0mEb zDI@S(toP!s9-XJ#z2k}+N(85UIggQ1%f7etUokrk6a;&FRC}9FpOH6^DAC_GThAJP zr9Hp@ucy4f8s+Vq-d<#Lt4j3koB>Kc4go#wgHpW&jiX6?^z13*Wm*rvjoTm{KV`H} zWTxz~OVTjr3#ve9xxdhWnNZSN(z@8dQb;jVyK9|IbuVnqNv$uI&;7cyr(w9cWw@cc z>$wNy;`&rq>%tIM`%q2U{N7~QVb9&)*}9>rX+vx0{#?65PVQY$S~JwnBQSgWQzhInYBFWR1p+^W{kspn}F4Ae3Q z-GY_rU}0WzkzMc!BH^L*Bw|f+E&hpUzt%6hJ&Rv4^}?@}$HOi-E`&WE_iOtF%LUO- z(O34q8VxBBZ<9{@HgEmd4w?~x(BlRuX$bM`p`g$a0Y$VERB6xBY}vu*Yar%P46EqF z?NT+5CR=bi!otZ%x3!DF8l-ahcB>snL4QL1u<8A}Cn_T+TPn7NnX_HE83JAMcJZl< zJiFc#E>(i&le|5QBKy6@GY9UTc4oROkdZ-rp6~+uRpKLIk0ktt{XXeep}%6WmP_xw z2bQSsGSe8>7vgQ=!&%O=Lr)XW0-+;MV|5t&J61uGI?6T@_8_-T8zl#>gOl9oVSmP? zxR|)&)SSu5Iide@s&6}rDn>qV{_CRC85l{(fodXdHlmDU1UW|8crHBB{zB`Sv{-VBD&kJ$Yiou#y4E0JhloK8 z=wN+PPU!P-6_p9$6B;L68sE*QiVH)cVxto$dtTJL>usGhsOZ621FfX*1oX5I%12v3 zPl8ezXB_I-7upzLzYH7HN2)#w6mFZPj|35F7>Re-{{|lnGI6~W(hL@@o+AFx43_d? zfAf)N-sZwpcLBcNCPx|H<QH4u6t zDD=1iN-`ucj0h++B`DFLq$ywEGrSmCBS!dX!0A3NALXQ}475`ok{|u*IhNz=1K+lz z=ZjdrX^V6oVujI6d{6-xv@){*AOHovNA`bx1k)gaq zJs%NcDzXjrH?=H7#OFnhU8r`NNkotJV_bStU$j&-u{bHMrRt{Rt&N4bc@2t^RrD)d zX`EJ_=9rY5SyJCEm3p4dugkW9c2x5+)Q!PSTw7>-s!`+1cRCNyy#&q2tzEB<%YnqN zzti6_#3@nfYlQ17!X@G#$&dh-fJ1MHNYr}#YOt*E>uEg2S0HH!^BaC2x-U=YWawkm z`jdvNw!D1D9~4@Uo{!CO>OyjHjGjZ-5ZpAP2XBz68 z_ysfR-1#GX75ByGek)|&Cip|t&`c1}n|)AjBM}tUpO7dA3)i-YkAfcMrr%u9h&Jwb zxYC=7_9n%XGqtvKtjV@6j+K7P4)`t6Mm41ZDL>(((LnieFSLZm=LO~QpqwhAj^GR; zVG2&W@7=<2?<(GRi9(9e`!JislehWD=#9T6+Wcem^c!OIUOEf8_eMYu`Jg=FF7Vhn z-;-?qF?#XFi1vmahbJhlPZ$lIX9i4;ahQB#KI&_mTjc`(Sp$^(l>&Os0Hu{iKu>GX z0;z#}zyvKnWj)$lBhHZ(%byN!^>`)E2eqE`VV2wUXRWtt(0H0N8kFwXvZ0O?otvWA z;Q;JBs^JhMLJ=5L4HdJA-|upz(gliomGR7@C>#`QV)arXDV^WkCKntz1%8WaPIq9g zcL*N^Iophi5jHYEM#Ft@qM8#@LPK5I7VZu+R&t{BvM(#-v}Z>1ipe2rsfGR48d9~O zv1N5#(}uRHxoOeu+a|5<{gO&fjcXd`+vi1B4^8&fahj=>aiZ~}%2apo`SuofYAexUr*oWqpWRZe~y7M#e>rQ&-hCtZS>H8dfW`@TIYLtsNU1d66n@+~Rq!{=;YI zv5$4xG(v9<3O%7g3!wc;HU)+U3{aY51jQNqk7@1)$8+3=oogTdMtD5E5gxZeKV$D} zgZy?MbFT&IGasGM#enmSqbp!kG>jf8o@+B>YsvF;Z5Gcnj*P!0eyq#6uSIAxUrSt@ z(Hp3e7Zf_8L5XIaUt}YJ<{%?6YQRC3uE2cO041#?pyv!wl0*SL?St~n5rQJCMDX=v zJ*y;s+;7Uq`WbtYL*z$@2pKu)Xc|(m47I8JCaO;&ythXp6f#~ z7N@ka==K}?QzXaZ=R8v2L*Yk(?WhOdjkzZ*t$F3-Q1*;jbwkZ9YHVIfv%PXrYINrE z8qR0AakznWS=HjnD8GT(ZajUDXcyGXOzc7~cfPtJZi^ud5dJem7(hQ>4UTgx@>x&%HtzeVQ-p%tZ-s5;F@5_=}$@1=!<|4XbL7T;2W3UN->w{ERczG%w)|>}JN9J= zZAhjRP()|=c(lY2rQm&`sN&~_{=7-?2}6%qA2x!#k{4?Pjs&_h?6Ri~sNkFC3g^g! zoIHpyv=!tG!gu`|CmoIm7{=jqli(*#BRonqmL}sg!gR`&*SLIq>?GHu$X=#V)8NQn zKIy}uLoGOq@G@WXa4N>|G>!d9(qY&k0ZwNTICj#4{RU-xgbIH_+U1#&<#?ONBGs@$ApZhoR zbrG?kxJ(OhIO!Zg@WjKw!TZ}~(hF^dP6eKBP@fq$e$=vYMCeol&z}sTDhy=tHT!F} zV7WS%vmkh>5m6%Z@_A8Jy*pc*wsf_vX$(;_EbK*VNX?Qdt;3U1X41U`<$G6uX=;>F zNBU}Gl9i+DAlml_)cr6jAA{=g+)85#k1(62N01D2Qbdp{;{NRmAbtI37}0>>BRDt# z7?xfkrd1I~(MOLF&t-y-G(!YEh(3nEUW4T2Jtm+9M5PY3KJXwx=VI<5o~S{|ig{3r zy;%I7z8@|X*))LYBccrWmxw;Xzu^v06Et525OF-ri4_sZqxW-@yi4yok3jOkD=3H{ z?opJHQ%ljuZXSJH7yuv9;D~b=DC0UF^I43-%&!)G6hjgu`gqf$G(49LKBC`CF@Kj| zail2v=r`h*4L%ahbzfC-ZoY8Y$Ro|Y;E~5nW90GcKMN9g6sKp-z*C2W5VEOs-RJ)g zGP3xs5lyStDvOpZ9uj}nt{z-v3$d+|x9nso>OVHF8Q#JY)YrERuh~f7cW&vMGpA2T zq!gu!mB) z7eN8FM5BR}I_wONX|XDQs2-aoD2;@8R*$y&Lr1(AZswyv&thh>0c{lgF(dZL$DmHS z7h{0jBbcy!5JW~yus(Z7LCRjcE_l!NabQ1DcVDwzt8x>_gr9iFf=eaQg?)_Hx3k_} zWK))z2Gt{!c$SZv-~*z5&^h5a3v?7?CX+=^_mDEL&puc$Xl|LqrTywfzb^RV?Q6-2 ztkztHcor~%@Z@VWBHAd{FR1_;kTPn#e*8G!c_`nZLdr2>T~jutVKZL0d~tvTNv{{> z@ftz5$Sr+zun^X$IHvRk`v@W=QN3XH1o!Sc{C(su7b39Eint1+pZ{sg4JR$%^m)p^ zKvp7IN&*YXVY`sSXGl5&p(lbuj~k#QodUy%fMSIJbsCgbh-bJQRzP5C3~dZLH*5pthW3-$A{eC{4V!5Z{YrB&l}`#mSVWS z87;!jWY1u?BdS2Tq-U9%W=wdD>3ZHW{r;HucZ>89{f_sx0RAh2^ZgCIha6b`-hO;i z2F@)~J$_F`Ek>cuxi`zVDZx&~TQ{BJ8B*Bf`sVujW+iKCaq-kFeIukY=2`{oFQdP^ zyWjIGsdjQ-c6Q%n&p))Sk%X(^s7k>+M4PXYKH<9&C&e1Dg6Nuy3p-T?mrEz1;#93;tyVV2 zDwTMilhQmd<0@HT6>pL5S^zB2;cwGUDiC_J4@!5H@n_-Ve>MD(W(xEw)n`6Oc0&^|J2G&wozgHlxh4SEik zWK#_!bFWm+_i+Ips-XKgEef_^@KA5>;eo+xdwQ;&(YLF;eOF)a?)LWGY(ZqCB6l7b z?BCbXv9EvdK&PW+XI~GM{O{@8*%JF~SNDH=p40m!;l4ACn$sE=3M8uHB%l`SPgxhA zKU{L;YW?Z()<1bA-v_mx^z~ETq(5uDRl^)lGtP_oB=}1=U|7a}gGxcG^?7fcixix& zFbmDcvGB1eR<_8hUSM@`>P3o>O6i`LzGt_;B;Daz?CsCn-d0b6yOJjH+B|0rcj+KV zX|bfcqex{15dV=f zx3ZOSanb5`mWF*1$~LNJ*edln!(nBI+4DD`x(omEeBxksp{IzCpK~b|6|aw(KHwFv z1)N4hz)uq#^lN$z(t(`&0J>NKvXSW$$3v0Gtp8)zD|1#YpBA-r*~-;_`}^EQ zi{`%i`{{G$On*))X>V(5i|lM~>v1$pZJz9Ctgq)cJqY|X7Y=ZZHWEkUx(joGw5fn% zE|}t=o#@uJHUl-U{o6pzDe0*or7mb>=%_wVl#e;utmyL)!Dw(jWZ z-r4$}Xlr;>RJfHguXf?qFXITD#t2X~ZE#*{=j)b}a`4=Ae_3XxR~&YQdFCCL1%18to% z9o>x$9qN0K9MW#t?oOC5_*b(cJ2GcMCD&=al?!qrvsZA=?d!_Q ziq1-H-O=5(vo#g}%m%jvcGAK}#k%am+!<5RW(V@dQnX#R9+%gEH;Dn3jQ4O*v z(Q7YV5Aa+@0oAERL<|l^*b>?s#3%hXti=W>%~H|75#GPwk1YZ2>aTovLU~MfZg$KM zLcZ-Na79PQ)PBz+zB8YGdqyd^hfUuY-@tEChr!Bw7UDQv!Q*^y@d4yjHr zNt7aNN^zCvPc&D8jyug$1w1f^4q)7gqF;l1tG>#J^El7tW$k0uqNwLDBXQNV`eC3&SV!n>%KN9k$UYi}Q4tua zhcfM0jIAXP(~gDngzs2s`uMgJ9R9o0P& z7-{DKorsm+jflcOu=2wPjg_CL3^@4$p_L)k5`5qwF(d@>*e8ovv$kwTm0a2BO090q zDPEFmb=Eed7WI`4wC=jPclqL>qTc$rsI;r(SuLeg(<9RBTv^pw_M{21w)n_occHu9 z-qhUCWlN2Z%A)(%aHl15PqA8WO+nrR`W#57`H25y{Kp`^wpGR1e zqCSzvXX$F%X-uNUXi~7bN_!zOx~IhGqDH21Zk*EQ2H7sNi7z1+`G{%XP1%~Q2Gw_k zI{;AQy?xrQHYwI~$$}!vZChGew{?}zbA@LwaZlgdncuaHR#B_N3VW?>N2lDiqjh3t zOlC&czW&-(eT9$^(vPoT9N_h#QLwP8Y$rYuePiquwTay=tN&r4q3Qzmp!z;@I>K0D z^nWal*v})>mq>$%_eq2N3iMY>jmT6n2x zZtV0Po02@%N~<=pn-w%L~d#5ltK6WiU`*qxgZol!)xLo@#k z&`cF6u};u-kU~&gdxkQuI{(QeuWt&^lE3sxPTvTZKH_o)=EX#!|4KG7^5o(FVvA-R z#)f}JE$m{~f~vkf9UXi6supEOCN#7kbp$a~vSSm{VmqGUzFE;0ap);d!Ldx@kY#Kc z?*q(FeMVlsqWp*U^tjlBgs{Ik-?YRl(HRaUJ;H7ed(-*1u=u#Bxb$z^(&CkpvLYLM zL3PHu@^fuTDHD=oT?MXK_HVVwR#aN7B&6{i#z1Or&J?yX@xw>siJ;Kq8q`9(^Y-U{ zezsZQC2L3Yas<5;R_e{tDE0stxNLATT0+d}a(e;UYsJP%F%Iz76`bvs1Xo$2xv-`6 znPiaC%YSM#9B*EPZ<$NEHMuJy|e>tV7vM8l$MsfX?v>0dm%tBXZZAMCQ zMRCfO{7Jd#S!G#eb;*S#QRxM7_Uzg`S5;0%PNj<+)D%@?zZ3YcR2|-;aC8~9ovy-} z?ar9gEe*vps(7j;^LUJg1GpimZDOEO)2KpY{dA{ znk2^WG{#T($Bt<8#NR(QZgU}K3LQSX@@zS&USYm$#-wFqZ;jog!{`h}AXJO8|4s?e}j5Z?yIowt?^Y=#s00xajU1V*ru!*8d}2%?UP$Nwyyui z*H^CDuz8;*U#4T6WH>~1l}Xt2{Q2;Smw~4U_}C+O{trGpsHiURQ~>@vKFR{m1Pu=EnpAlaMxWqRQ610o z=*k?^t)Ocd(A#|P!Iv-a4WptuMS;Y74|&_gNhAEsOjp4|DaHt5w$ceXZ?*w!l0PSxPE;x~ za?``JG9%|}Y_CJzFs+|#IPFbH+$An?;ycZHe8=u?;t`U!KxoPlvSY&0 z>SDAita7ok^SuQ575ScBBf)%aYE8z3rmBk>H3;?je+3J)6;M@!7QF_#i_wLs zq+^(yjWV_`$rE{p!oH?{6vLc1CgZf98mYpa1EDC0R(}od2@K$As7ySy9*fmAX*J2d z20bC5s9}rNywLmkO~IrweNftu#_Ib~4mWcgg4O|PCOkmsA12zN1&uAAM$1&ioYuT%FIQ~v zi2Lz{VTf)H;Em_yvGMF-PtsWN6_@>u;}h#z9G0%>AmpF?v?S@lzj0ngf`ghK#0ZlP zzJ#~t5V4mG3r6KTD_U=jd|iI8s+OY?tk^} zCBPsAmS|41>1qw8IvS^W4UhJlXy;k}BxIehyOjH)+p_U-#=7O}==I$h`AX~rhmwYLy9gRsYqL#EjcMA4<9Mc)Ex92 zb}jn_twy1n`Meu>iRK-x-#%JUt9)9_n?-GwO8_se0L_cVTuhM7uIo`g2jtD5`GOR^VMNpc{tH5`P@eN6L zgBMuixJ5)Jkslo>aFD}3h;UYwP6Q>9phV%6McQB`?TfKTL>J~rjp}6fOV2wRjdvDR z*`t7aE5_vza8s2oQTqg2F&DPr_z02(Xp;}2;>u7<&vILcV|nL-b;{zIvlgQY`%I{N z&vSdEDU&-pSXbNdDz-$uWAV`PwHO`kQ{$!2dS>9Gjra*Zo(WfM>ilpZc|&r7gHF#I z8uw~B_n=jXdj^}5^rL{n0;Xrj%7nelq2y;QzVtkYTMTf_iDGR|2y0ct5nz{UIJjLJ zgpN_z+-&T1M0<*`55j0!Q2U#PMN2Xh=ODti2)thp0zX7M(BfrS!3-!?wa!I-$pCk(_u*)vz*0If-)Z}Yd znn<>#qBV9ady`{*LMqYPQ8;cblBxBQ+ppeU!bZ_}a*FRPc8x71{=qLD&u1dy{}nc2 zkzgg~GGe9yv|#vHW=4ZEI=`{5R%xT5OrUYREj~Wl)z%>udR~BKfw@h)N_d&@I&iet+I{HEW-|v~ow_XFyu$Rvd>LqFF z65&I?!n=z1U5o;G(fu$dn{G0ZADz=kessTI6LCu_`OyP=aF?$=uGIw35BZ>!t)pSG z^ByDt6J`DgOr(j$J0n5gaRj_Wy@s!eze9Qwz4qerjrphn7kuzq3ul2#m%%53)%V76 zC9;z#t$=hPFPanNnn_JAGbdFft>D$YCsgG|7uuuUgT?;&Kbn+;n)|fs-suJLA##YL zZ+P;RB^RM8A-TM7)aNJA3xWpgvv@kN&jN}*d-+52TR=}4pgyjB(*VVOO5@r&1C%TS zFMouC{8hIGFrl86he{$e^oI4C!x5P$?@LNKuHLdA#Bz2bQC9=osH=>b7Vmg-+ zj0Cu;Mc-*fa_CW8eb?uAUhzMd4fIRpAYuA`EvI8=SN9G>PRFm&<|=*k!*lr#gd+St z9h5^*lG+@9i4*c-kT@B>0!>~Lz4Ahy0wFIrr{?#o9rEJ))u&CwyIh;VGDS5|# zOeWuQ5dALBB8o!U2GLvX^YeSFs`~OBGdkJ_2HHDjF!x~Fv}p}pT@BNwwGGnQpc?#{1sdW+&O^Gs8}ROb zd05R5KVmE9UDwld-Ml?}XQ00CH8b{_eF0y;!~};Sq$DyOlF?2SRbhgKtdDo5JFd!` zhUVUNp>0`p^|H1Ht=5N@u3o+LA*9t64%F5T7KZ=kH`dwHrp>Z~=VYZO^L1pt&^a=< zs5q2#vw)7sw1y@DN*NZ^F5*#_Z5fF~zC^LV{Jd>>b@lSLpL)qS=jUh>Bw2sWNtzvY zk-Tko_dp(IqoA!soU}sRh2Oux6YMk{jZ`b!kq4=B5B^ki_JXp(L0xkQw7Av8^BgG0 zijVuX1mzR1L^*Bs#_??@b~FK;`zh^E$sc|SH!o1W=tL7TM`dWSHcsHBhFih9f4mS)Q?CCi;Xvmjio}`+xiL$KZH>Ah~y{qdd zuk6Y1MC#0eZb#eBp6=}}$gt?y*>*>i?5xOT=Ba}hJa-lqn`6Ku8bxG(bI&gJk6dc_ z3JY;fM?tS_qsXq_!6lYmb3Ea=y4EMSOJ;Mqed;NH$<5@FyG%%KBEKtk`$Advv5LrYHEQKr#13&;tP|OrDsQ%W@ME_WacbREK15KKn-bU!Nl~c zK=^#XHOxTKyZa;Q*^Qi+v|+)907;kKF#@u7Fa*{2UR|@vj&OFCIpz`pm`} z=LIPyBeuG~XsH|#>4=DIZ*FZ34UK&&YC^)=mFfemcX(%Hq7w6qFiS{QTSZU5*kQ0= z^3e~V#!y+>Z~R>-EzL~zC@r3}qJF+l1`6k<{PncVP^Ei!jAG> zl7m!tVRn-y2j#h?$qr2peqjA8RhEYB#a={|m{+#LeAd!@c;oO;kkkBVT2*sq{xoD* zbi4AqOV+Pp`$}8#mBe_Pl91X~r(Vl5C&1wl;5rWuTWGFkVt<@z`~CCmq37x3kLRCI zL53%Ij8r^BbC*AND=w_&k7{udfsZ5w;V+EiiTg?Kg&;2tt$3ji+)sKh1es($D4o&1 zpL;z+P#?huhldBQ63~IHokx(hqvg{HIw;2!bUgUneQc^-eN2$T^K}S6?;FXMpyiOD zB>+02Q8;91ljQS$?yH04&HfJlz%Zz=IU#OzW;O>L2dCV&FTxhoPYOwiigSdf))lfz zFNR53VaR)rNUU)eCOe=wL_at9;XV!A+9buqcFDO4TyCYIquJQ6Js3ZrvuRoV+)6n)%uc7rMz0hm16}i?CDQ<-=dmiv_CT9RQ!IW z9gy+YB>YPLl&x6TjV8+f?#j^C>f==uHW0JyKJC{JQqgXsJ-j`wa`nK+4@Zb zlq?a^w{r$4?VAPkv=2%r1345P3_gl>@RWQMTj39&86nnjejf?8{_qV2c*xEE^_5n- zD&y)VDKXA)pG-HjPW9fArQM4aBS`@=-n9jlCkyP_)}Q7i?TP;g3NrB}F4OCqqA-&i zvE0B?&(a@%HN6ujUa-mU?fKPPgLB$<_hKtCYo=796yMN=%zZc*dPfTtd%@9uZ1Ayz zm_AxyC*8wkpU#hI{5k34kEHY8kM9OKO$?CzZa+yp1vz*beQ`oL=b%QT^Uf#OQrN3T*9ju*@T_86QX6 z`ROaSb3K(-=5~fBPMPf5F7Mbu+G^wa^&4Nx%#V(+#VC=svb=xn*g@@ax~PxT9{Ttp zSrlP_CJAc2b)A#PoS#N~8c)>nlc@L^Pa=9G&&EGvp!vOpkVReR4===@kGP=xJp6pz z1?BP3$Fy$pu1blX*U-9nn{?(+cyEuHZsBt-5PsYbPTEL3f9Nvs5gm^D&Y;fwJm7p* z2I3#3=e6EEDh(1>e7(^sK!|G3)T!{FE02bsi%84OcUaS!ZTG}I9OCU9dw*hfjEtM( z6U)_5DarGy-b*fBg6fl6Ylv(ib4KvafvkQiBQ00)m~G-K&nNuS{7dJvFDLx3`F~kn zEQ?n?Ojh3xS6^gKm7+xAZDezXp64TXh({I!p_Jpv3~^y(%sI7|+;OTmApArO+lx@} zYV}~u6TmIv$TTjGbKJ-NLaR(5^tb^^V|DzV3eOrSB%LUrr+rX9Rswnwlt|dWo1_qo6pyPLIY=gmkBjQ(>{BBr zDXC3wpX(8U^OW8@EmjZh=8M!DqV)bTdK$-4{CStt;a8iul-CeX(Zp|BUy^bwh?|BaI)fj zl#`|isH+V6C3UGrzdFhae0|^v2cTH1t1Ovi@DSgzCQ$Yf@^n7s->s+*oKN(KQ^eu< zyb|#)R4q1Snuq6`&hV!jP@x!kMp|ByIXwRj!dU|xh6oFueu6)}l~*RFzS8Px0sH}c z3Z1?i+5$ZiI^C-&NVR_$`ciaSMtXF3Ts3NCmv~-f?@i2%3Js5qj;T=bzGT{pKb6=k z@oGxC$H9Ah2=*wHCBD_YqNQbpTUkFewBF3lvX<##2&zfJS|jn1g#@`?a~^q=ri>ZJvz5WUbg9vu|)$7}T8!J~th zqu=*#<+yhhllCt0i$@2In3GL6@aUlE0YwLmzfunem;LI&gWNRXG^n%`%?9bvgqnZ;o_kALTB~;=gjxi>)2#Py*PgAJZQJSt5pjY zqepg-pzsv9h#9JRR`?91Iz@r@uM@jTsnR$_)p^J@p)H{>Zy%;f%Gxxa`FdDOUD`T= zKG4y9{y%)u6^*#bTrWp#W6!Wn>YdE3KFL;l%Gg5nTbozibkm}hh=zP(SI6v+IUJ9% z=1qGXn^&uklJ*j~X{1TJAScXpjN4KIPU9!wr!_cy)5ddr=G%jx;{*I>9N!5^*hr`@ z%-(|5D&pQI!#OIg?xp74337}|SuH?P0_@ZsKNLrV&K z8kDHC&GM|KlBTrCjE4N&skw=`T*BE^UE5@@pHkO_lN~Xcxztxd1Nv%~`K&tjK1o?1 z^yZ+@6B@LDdf~)54O(kGk}WGcMR&j1cnp28G(ZhUQ! z=jMIxxtW5s)7J_#Ea3bMppKEfH4dYqVFd3+3}*m+ILPMGFrW27NyBK+a|S3RRD+)OK`Fk@ zq1c^*vsAa{eI8#Iy$$?Tp~2>@j$6*{ z$^UZst*(vFO|_**M#N_SuGp0}P#I>6y|O@;cDbEt8Ich&(NSbmXlDb-UgCMa6hNaB z+}_$6r0Mv6ucBOB`AJ1MD8}_#+f`{%arM1LLyMR9UcIYzpmbW%#Ku~uHE(foPHT0l ztFuzBno+h^&drL7PqlS5G-FobaAI;~ye)P@k{w4DU3ECTIJLB87L+)6_-l;!D$H|u zQsKF}Jn#IbI&qBrjV$)v3Bbs?V7oJoGUG`k4_KPEa`}2|2C`X=G1N7 zty^0C&kuGlm2*2wU$8xGZR!bjevsR9I6p`cQMergYlxCBcGkYvu3Y5`%UyefIppk)H7!WP;Uz5Ec61MHZ_F*Q7v#yk@-Ni4 zg)OX|f-~b}4e7v4(y-)_;b-GCXu#Px4H|GZPJ;%Vjnkkw8|TGK8bGubc;YGxmeE)0 zH|64fN#Akk>WaOmr5d-7V;rVnr52}X{HlO?D}Wj6e-Y2LcW|y3$JDg%&0I6$JK5yl zpuUQyXpP~}@YJBt6oQU@z^U*)YrV_xEXjz#Fu?~UzZ8eUC;JVdFTcTkvTm$7=#Ta* zY42}v?Trj1zj^Bvt1KLCqm^yd0WB zJ7ewLG|)_^K<@d{Qy{Z5BGdYd=B|vRT5O}n%2)#*MsiTn#r}G_je&UB;=@f>*I&;h)FIx%0!uZ4P z;`!EQ$}`9J+59`&(z4z8nI^ObiD#c;&hzLkDmv1}P|M?i8-&O5oPI^d5l0L0`dqg! z?_Cowtr==soEeq5W})@ZR;iqC8&_@Kux90sog22awadFVtScytDJ&h0-5G-x$$E5O3c38CAGSB;@}pjhlaIl!u^-BjnC;panCrAWefS9t_zk)7@^>*Z(8b8; z(2}2>nS3cRRK@Phiht$7UxHp*cO0;}v9|eY{(PI3XUj4{j$=BreHbb`c6Ji8` zfh4?F=%#$qO#-hEdO|>vV~*CeXDMP-;LS1j!4ZMvwz(!IVH8$Qy`P`Iobez0JSnYWwcCs+I ziKJzuS?Z;V6zOt#Pcx`o-XbOZa;Z*h)>}`VvaY`dF$xi>z+cQ55vwgn78+)Z15SUR z4S>j|Ey`N8(Ot8t%38jmvU1&jDeU#w)a2yU*p#Fs&lgg*=g&m95eLrajmJWkN+2c# zu*V?_jAfhWaY-`9vPotH)ELXwp@Fe%9WG+opcEG9qp(1!w?Qc-I}?;{a=SNIg_mut zs9bkK`4OAqP#+sd?6_#RMprU0ULN8710!E&D1yC7I%PbHDM0j~P zTS0tWM9IRA28=~UTUT!X{PgIAI~6vNFnMzMM2v`%5Ro&vIYs?09~DS&2>hn!`5YsD z(WxUn`aQ%?>qk-sqMpMeqThbQ&%Tz}Sc^D#N2)sF$B}Ct5svaa3W*mLAtb)QD-mSl zhhQ&{90DOv*M;J$9~8t|#q4j6r$djl~O$ z_|3*(~a;;qX? z&;4+Yi=O-aIxc#ywP=i8^9VDCV`uM+it?Sk7%N|_dLPBXH5~VvKJ!M;_wjXxo~E0H z0Vi8n!!;!N-qL@ib_I#nU+;VYOo(yZYMh%{?Z1C89GCmcE9mx(lME$ESEv>@MxIgOnZ zUz3cJ#*oqiZwN2x-`cpS(z$H*oTc&0=FDCiw{d9Nnpin{wOq2Klg(AXziMb`6`SMz z{N9GO-F=vomI60@QU$NGk54h|_pgEJP#BxU3{ zEE7so*lX&$>(_Pm^>xCHOZ-^8Snz{BV<+S#N62CJsPICv7jZdn79eQ^_NCBTMTE~5F!IvOl1h$$>sx8X2U=GOI(JxVI{ zhw3_bnyGEk265#^j0WacD%o>^(BlRujf8miP*CWI1|5=6byItmW~T7^1!5jer7_qz zb`NsG2Lp0HtHi=$=&6_e{G=f3Zd;uh&ATke59?iyNlJI|A*fn5P+AyWSh#-UwNiVV z-u0Cea+4#8_Ynoz>QAPv@o|A`1ktmzhMv(5RX|VspnS9h^dx#9jd?Iul;? z?uRW8^T=$p=S_yWbb8?y+G)RpwQz6C`qKiFe0FOjG05p{(6mT z0B?+0gP#Wc|3-|N;=G2)6XLvxe4r%DUNqUE6DZywN&cLD2vHSNq4!j z!m|sh)m*f?0j<*6CI8#V5NyNlNt1IW{;86#{QRyG@wZ`MprL+nko_pPy{Nb?H@B_0 zs6E%-Ti?*r(@>8sAcO7Lfb+Yrk|#El%IWbkmsHU}#M@YgdWnV5e~S1}FI`~k9gjQM zEcFNYP;UaK4v#gMZov&?W-DxO@txllmP6mMrYC9H1VgCXa+7pkGKW0^I5SOSP3)+g zL3t;XZz}9`!!3S@iovUXy*JuiA>S_XWw;KVdV z^cv#<-wmqSok!hA7EN43u9Nh2e2=8w^Rd>;QHw(knVBQ(?`p+oyY?O0|IYi{cD(hz zbRGp2mn~kl(lLMDeQ1F&+y)FW*l|#GF~l=->zhvUL9OI*x&@VP+X%Y@ zCw@SU)ze3k#-EUvK_61a5XEwHOegd=d6PC*0bb zX6pesTKTQ!sHT4&Jq!mIY0aXU;ISI@@|Sr2k-d^`adIn<86A_b;z4nYl@z+N;FW?3vZJqBbJ8EVeQ~ zx^Y8S>&7miluTiu^v`I)Mwx7GSA2tnYcHs*m?c`axRE%H3XJth1@1I_Wj|8XofbJ} zV&x(xJ+F9XF>AQoa?P2AZ+QBo+y5}<&I3L7JoM0+Gdl0)bILnHyx-+z;2^$(m!qae ze(r;KJYVki!VeJ~oK1s%yod8j+yJ8G8&GwdbcW+l5MT>9PZe+I41vo9x&D^=g`GuS zx{5{G)h|ByB=NoYN5hn`M?>uIC9)M^>P>8Yn0hN)$cEIr!olY=(s@q{n~&V{FgBZL zMCBmKDLp2c*~46e5N-E?1}pI%(Htti!A2A*G)93V=e%cFI^k)-f5V!r+$S%`h(E<; zWuFNu#0w`G67W&e2J{ayJ~m%=$jdQnV!3C`?L;oEh3brnX5ff*RfupB>}Tm>RrK2J zlRZDy8fVhvK`BmZtE_Beacst!d#sL!s5@SYqz~#V3o7yd_3$)NKW;Se(!{yw!SFGw%YeYzVZmlfIj*!kfuV>ZZ;*YIX^@ZVYEu;#dpjWN~Mrd8| z-LKlnh>i7HS&vN#Q-2nz;y~$n&%;t1-oM@ROKCE{ahiI2FWxqrc5%sT#ZJW`J&y0c z#k3Ri{vrI%M^&TJHiGPAtLHVs$7%72F#Mh+Fr}lfn9!Ka zEYhleAWcv|u%b^6u1ffgq1a)QPK?DY!vt=W;50LvGu-$XvX%@LX2>kvss40AXYU2| zk4`or65gfOs!(a7=ew1~qf({k6_O_0JSy$uQuZf|f!VrmK3IjjJKw^&s%J3*JW8Cc z`#@_Ewbt^r^CfqAd6{0YFhh@4;@SoqJHP$zMT_p<&X~k@-ZM0G&rbX!gt5(M=FYuy zGs8d5&YO33l~Ue*-J+o*?d?Z~@Ylhn?wx}|*Hf>VH+S#Uxzay9{QL&AwbX)sMquO^ zrYI&QO6AMxEcE?^S6@r`fSD6M|BS80diexvQtws2#pbJLpoJ#my?VUIW455*yGd~) z*_{(vy!n8#eO2AzHfieN$Ca@SOWt{B&hm`3xYYD98Z)jd`{h{Ru%OBmpRZJH&sjzD zDsDtU{ktXBy%vnqJ6&x#S% z{_J-aQ#v%$&DI+{vgWJWl#-8lh zy|AKU;qH#E14WUgJ6oE!%_=LKwXL~jXK7^70d*5QC08xp-_x^yaaCbaba76_{HxpB zubyAwDvpM)b{e*aSh>;W)xI{h^OQ85L|G_qT#BWb{PrxT^B`}lWh=F{wS~8Juv4St zHZ(cDDV+$m7)=U2W%@R_42z36jCF_PoS!5&eSsB|tkPuho6#<dwAltX>~{@E`U>*rv5BHi2)}F}l%*)|XTx=b-h4`ZW{xNG&7TB}!-CS##yG z=_P%K&&-zQ-F>vbYW6yviD8Z?OF@7Aw7C-|=EW5xXDl6To?B`~AMEJEi`ZM~_Yo2Q zjJhK;lM*ri=u;nWz4Icj-g{MjMpGaczY9T=$@)Ov>Ur3I6ZmXKS=$O)H=NE)7xG#% zv5r%Yj)lSSqC|T(amK{H#qMGE(6p2p@ze5l_ZBDxeY^Ae*fy5uxtlrE=cS|Sb3d#f zEXW_Mzvs?b^VM@~=KNWAvej&g`Y3CL>GdciPFt5@#->55;M_d=^WV~cQ}SSkml)Wu#P>8C247wO^I!u=Xo8N(qKzLGeQ$TNqWk9tqcPn z@Nn3+WY6Vrd(Irbkl(yXw)u1XX<^4)i@LrLN3MzQ09@pn@Ri7DC(*h<)dZ#M)Yt4R z&GUv5_U>k7uAPPWTTAEITJ>8$B#RO_bJ3^o`QzkvKUuNBCoETndIgJESbzy8VJqPI zIk#JB(t7Pw*mI=YKjv7iB4#A$Bpt6~l>Vxrd-ZcalkP5u4;45>Rw!98IbJV9cp1wWysKmC_O>HadBJ;`;%Ccjptm|yu+>IIvoVFsO!TdWu zst2Sk_++N8u}|cU&?D8l4&hbXMedcDVZyqZC}9aGMg?ykpQZUGvquN6UsgT5D16@X zxwo&Heba`?_3PT&HZ%?#`{{D?jBg)VQPaGmw`bebyxui9{JG@tw9WTb+RMJTcEzo; z>(|~ez5kky&I2=+-nKF=S#d@;t+`=v&e7?;*Uf6)y|OzG6t_aM$Ue+5k$q?hU&vq& z0Zw}?a2b1mKZ>51{>*y<`xYl_xTOFM1er%YI_8dJg`w)-oJ-oJ9nlV zv?QqMp~8+5#;slTSYgVbp5GQSsf3xk#A{Ze$CJ^*k9iA}#|^n+pW+jiDw-iXPsS>M zWrlRVE10!ctEBnad$XuS=3V2AK9WdaJPM3rzG~hweR(3%rS02R*B&9Waq{K^o-sw3 zjcI8>Mq2jW<5{Ik8iklix|O(eEEinvutiI^a4W#i z@{kJJ_AaTZTC}&b>zaj?p7P2m$q|V*XGFrJBIyF|;N0HYx_xeW)q?F)8}`>mH(V!| zEjTc}_u%3xTE0TULJC@@Wm(f(CuhlG>Dt}Vy?b^^QAtc$2~nJZc?-&<918@EOBymW zKry&6-e$n_X<+_6eBU56}e^1ITJ6Y*put3@|LaNR2kE8^2SN^?H6^$Wo1x@ z5^flnFTIS`%*GurcTELu8KrNE{q7By**fwK?{|XlAIZ{iOW#L3TE>F6ZY8gWD7u50 z@|(z%sdYI@Nqdj_T_sdvcf`a;+skrg^QDi9Yn0fZapLDFmb?ZOt+PB$@H97I3+*M;ZzD}oMuy-6 z@^a`i^6Xix`%DM9J`-^A*$DU{+?_%-Jn)eGLrz5oui#H}b3T7x_7L_^m$bY_Tf$uvfFjz&yZXVJL$%5Sm)BuJOfr(ac%8aFQ}*-vA z%O_PYIlwJIOdUtylqoOPCl_esZPTeM3+!6->5yqNrya7783MRwLmV=l=Wy;5A~?4p z@ch`b+y@BVASIx$u<=5@-c?dRnN<$1IjN+J*nG3}nIq{@Y5Qhu+_LSO?9U{%pZ(%9 zDR%C*Ni`efnt~`-_2TK%7ss$N92ivp$5J+@wsuY#*Gu5veLOZMd`G9rcZ5C=U+?3z z3iu%n&MhkLOCwJQ^=&`*HR&D@2j*R#&U0OtMI$>3PQUbta6lBxnHk{r-M!Z?sjgmf zZLj{>zH33H)s?Wg6KJW0%zG!zur`5VU!yI!_s7MNd29Th+>!e2D78aD|BeBRrhJ1=w zrutMFn%m@~urJur#PUS0c}u$I=0_FI?<+}lvtc%%J`SV$T!eb(#h7SK|6b*euHCk2 zWkFtD8U)2uj6eNoQJk7JcxA{EE%x)wT78Z&{ z=@RAw{Go-uue=X8k&pfi4e++qiti z2ADqb*kf2Z@6uX8uAfEYGY_)oki3vnXR3GMd{kngO2E1)+2H8G>dSp{wBmmNd`?>N zgL{WH%QC2w7{e!B4-+%gJ4j;R08cSS?hf*LlZ4Yg?|;#Xg=}MfV=(M!3#di#BCeMu zC@jILz89{(H&6O+#j}WLqaixOV-*-*tOU|``1{_s6Ih+X>YVD+;p)@+^BBpNu|Gl| z{}cMfQXTdXa}dsq(0ux?cC1b-znn2@Ez;GC}t9^0|HKKvVbEukHh!j{r{o}F!S*D z`FFB<={w%vXSy4ovS}aW#RI#<4EpnY_7XNCkZx~s7Ef)2XU!OfxtE(z>XW;_p&Z=4 z{UCBFldhdJJ{0qvZ;Y_B>imO;N58>t^tAj)o-;@K*c*@`juNH3XM(a(({ijl68XhA z|M~7H7-zA^k2^edXMg{Wsmirmwj3H>GM=AM*vtf9H1o;7pf3w;Kx?$nrd_VbO z-wP+n*fn;Gbb>feNvQgF;y8Wh9RD8Ycnfw?HC$r8`n-8;K+Kx~{S(#*<_)(>=<}=0 zn{eq~Pd~V^1N;n{>^Y_!+`9GPq~Y#q`^#dht{?2*(r`>Zu!%jR zmap2rWqHd`MeU@>`i7b{o$L3~YyfWVf!FU9z5;gIdt9akmZV&+5qL78Owp1REs?k8 zE3^bhokdCt+856jmVDW^q`SG55(dxzTD0V(y|1pf8)QWFEELiU!S?qG-7L0Hz%MGd zGA-7^BG?@f>UL{{WU*(~$>yLW@kn4<|I!tOtlEJ%VA1fZS(TS#Iqh}?b@BR{!YW+W zs7G3a4^%I8nwd$CA$fDp=0-JFQP>}q4>s>@*woXuYSNCiZ%tYwS1zo-N4-u&A=GDA z&23m%=_sDz9$a`&&5ROAbja8(3@Zk73i)g#lO2-u7S{5wHU@F*m7pE=+|Qn%;r>(& z8z)mzJBEPf8nhJ6wH%sjm`evtBKlGS_$8Wy*g?{n3VpXXUJaA7knRz?zKecJL+ts2 zst&Zw^*z^{r0+>@-XruW7;uK~g#`SNX(iXY@XN`caUR`l(A%VUNk1b>$>TWl!MTsP zc7KFF#lLfG_>NW%nb!HVIPH2gt$mtwBT>b5CTV@4GY@HSu7l`#P3N384e{p%9M5a; zLw<0qDjNKo8l2B#qQ}yu(L=id=ww*zv?ivJ9Y?+dveNaHjwk-{jhp?{z1y*=scCa}*S2Oy!@AbiHFb4sT3gpO z(8|6IvG6*Yfg(b7jA8=TcM_?w5i1_rtw zl0v6WX==1KPnp{0sHv~7aZIYInM8X5w7@Q+1v9OyJm9|#w~U+*lR_O~s+qVYo_HE^ zv;y2h4)90%d*6{Z8FPlVu_TB3niRTruNqD7ja^5Wz3=S@b0rgE6YP1k$1OfL>{;Z| zQJO&o8${k5wfwrn@w(PJp5HcB1-}H*hqX?l(CNz<^1A1@A^!f`ur;C6u5K+GT)w)v zV&j^b}&ki zAo2ZaQ;vl1;Nd9qJ@tSWdI+IW3y&MU$2sJmDMYK4!-*uD3LMa^h~sg6e4^56h4hCf z?az7&p0z*u;1dZCE4Nsbuv?kq;RI$0!`-qmVd`f{dk9gjxH%jrY`#$KcG;}bD5hg3 zJP0~rr-@ZDUR(kx>H_dP-jT3isO)OiM9=(c#c*y^_R8|=!ICBYvt}1{6_odvxCU~p zd2@;?=2VqVFO%mlts9avT~XPHvt>@ss+#ObM2)$MB*vUhTVle*x_np7#E7=ql4d9( zE{V7SM!RuVPaE2%u;a0cieXe!*grg9uz#p2LMHJn$>e4(lY4h+lIjN?h0%sNG4EFd|ydVtLOn?`XEYO}q)kIFU>w zddG=8iz2fPNlD0U%L$9MhM1#6GxD7gvX#w1LeVe7tRYcJrLrv9nd~0g(Rdg5_jy>@ zcbczRs?U979*}my%<|?X$?yZ%w%Hi4Fq^}CDZ&F~pw49H@b>HKV)%y=w9(4IyT}Tm zI5$;4rQIpU>oE4qyQR=iKJk2U=@NQG`3{vd7vyB@$;CLk@Fpe#Ih%P_m2DgQF(_V% zmIRHe8EY*=ddn0u!kzbI=~NJR+N{PmvOgeW@!dIV&&x7 zhg%eB^8QIj8*lv1oi{Od(|7MYE@84Uw%}lE>%j#`J(_=@rS-s|BD15rcOPX(ckMaq zs9oRFv#z#w9sPYl{{E#o)I01walgtzk>nV|1g~2c9pE91F9<;Nc@A z+j+}0wp#X($RXN>C|;u(f11LOx~GPQg|RQhU(Xll1Lm5%g6sUlkY0;*Kdgx>6$QAcARV`cPiR`-+Pa}BB_k>$<6}Fe za=@$NL8SqUXiHmNe6z=IQ6b0kyVOX|0=nHfw_s8l?C;9WQVilz4XV3_Y zy^25O&;UG^#U)=0wf`ULzB{n3>g@ZTdu>aWZP~J9%X`a`B}*QXE$_YJ5j(MyII$Dk ziL>|KA%TP>5H?LIBaDWSGV>Bv%WNpClmaQMrKP2%6k6b=B|r=D_4j+uy^>|e3GMg3 z-ydJ#%F?-KpXWU5JkN7{njjwFJ3VF-Jnz@^)bzVe^LrcSdycY)q1!wpHFOpXHp#ED zg27?$f;wVyS*}d&kE5`i&j!o9iVp_)FtRRMo2HmX(#2D_fdZaqe*nHuLWQ zQKTuy@+($B+}&cFO_1BoUeZ20ck!D37vvS!n6F_=wLk8ifAX21X{Sw_Ic?d7AYQnMEx=CxP@X#xH?Y4cwSuD*`!GM} zvlN29Rk0NErklwaL6ck_Q_^JXwl$W-l*f9Ld%S4~7e6D}%R=9fXFv50`^`HSxvJ9A zs+|{YuCDv%KkKVDv#`H>{5ON)Z-7!i9n?GzXk!?O0BauQ6a%KEnZ_!S41{1I^wpot z@9M6Lu{blNWJ`+aFpy&?yQJ6~5o!w66}sgus4Wqgmbj&C&1!<}(2K>ZILxPm>jp;$ z&S(wNLv($^ga?QRn#%DqaTUnTPT+5_upWJgM=gOiLIE6$#z;0o;4g)D41bX4m^6>i zVNos7FUE7mQe|AWF`fL?PU0u29zirALHkQJ{(B^Z^~A~ zqUw577|d~P?d1I<%OK)|IscoZ)c4d=6Zk6NUgH^s+f`=)%p2xxE1!kk>=iy|gn8$U zrE37}8hd>y;n@nn_lGY z$**2@#nhm>ZA&|Ii{@6A_j%lNwn`1#I=lB}M_P7FZCzL0zGqrlpQm_U<&^bpv8HH) z&NX#ORo$ZU()mC`-7XHt`#Beu&ABj*u1|Q3W(7B$?pL|7D4G=?Kc-qNt87`lc3`=W zHRH@Q&8GpJ7sGt=+XaI8UclwQSX&l+#{6gPpUr3VrU)?_o8aq_&mP8gAb_vHGT>hW zOL4oQv63&aZGnXm;gEI&_75Nxu)w)3+7Z|bI5EN~U~8dpOdw-K1&DuFM?nDmOJZ3n zwX+wF^sxJVdH(FQ%&~D~$&waOGP%LT z-q+3;Qi)dxv=E$_iP)0wx!;nW;mTCKyslIRsM9^o{=Bg5E%UoktvWgHjrRRqz-SZL)j zYoBuoOTnDG_$dlxH&8YZ(#dRxlsl*BWRf)AepzmRL7^@`C$F?@xF)=ANkwXj{E{lz z!FC8&ZM(N@ut(-@*;Q;QOM@oFFL;Z$f6w#br(H^J#OWF=K>1Cf5vN1)}k&?#SKs`gRk%ClUMd$_cT_9;=3ZM@gm z8NOZz2!CR!(p9@E1oDmn;VPmaL1-~>#4D&lkH{( zxJVHx$1%k$yJ2ND4!gbDvf=8Q1*Ompdgi9;Gl#ux8|tf8NOkMmM3MP7N6X48+?Zu| z)Nkr&-&Bv@DlMY7ny2^}_I|_h9*2JL#GG$H=S&qQ%db=0@1OL(``ZMKUiL z1ihqLIlFvzA+-v+3w_OPXjReT+-@05rM@VsdwHtDJz+Fli-3Md5Ip&Ksz}%^W%_1T z;c3q3|S+}&JVrd=!ypO#%j;S^VveRD>#~7@(_b~WrYA;VH|M%Jq z^k;y52eIH>fy*w?ms?a1hX>TZ7wj9`<#T=T{iT^yBruvu@P>fmy{{ov)qdRO*M8jo z%2+DRuc8y#8<0AtDY=$XRm}v|OY{x(zCo#qv`wi8mDE03q43di2H_NQ7w^~Lsh63~S_upL=Hrh? zzI`q&Dk7@Pl`(zW-0-&L9c2j?Zz{C{Ns@htQE*)hJT=iwrq>NlnQUgrjwJV2?D_b| zwU5WW$lgkLKKS`8R>`6?XFvaZR0x;<8`@rhA+QzjKpUju_kjoU{E*{@Y#bj}cXmcWys@Gvt0EJQ50(fkwhQSM%eh_=3fKVJplB2o zLnO?iLXSWuHu=beU$;0>>*ymKVmVhcE-2+xsf6nuK|}y0Rp9j$ZD1SM25JB#&=_H6 z1MbF9Zu+(+K2R<^ z#Cj&x@UW|}LP375qhix)_pcM}S#F&Kol%U&4A(HP`JB_Bwasg4!O%3XtE*clKV{PC z*wgn!-t!kAm`X4^glwexP3;|<>J>EowD`hPO-=G8m;Ns7ceRxyzE@R|_+AO@3oV71 zjXX=HI&tpxFX|>ku>dqW%Yk6kLKzY=Z zMa8!Io}8kgs`U$Bts1)Z$vG>FXXX{mEtlfm?QYiN?rxtd&+oz{rlo(j)YPlC}t zqk3k~z17`zw!OQvw9R7gsUg^`#7?18I_P4R9a0sHhRGLV>^wprjQxQg@%=GUIxF8= z-QHep?e0pg@S4Lj=6lM!a+IXOPG@sjY(&;7DYe33%TF>y#XBwDa(!G>lsO}CUO>^CWpP ztJ=$lpR;7@(=uIMh52oE!>opaHoGG_#+=vfwbi@Af}#fMytCbyfdzn(M8HmnwD~KV z5#=}01Nbphv|h4{4qV`m|Ob3neuTyGB?;Wk1;0$dyQndvhU z35oe5T!20i7lD%qNZpXK)ZYcPYS6l=zonn@-y!4#D3k)YUc;E8$ScGfpjR)ZXgp-% z_BCfd-E&GpjLWx}9-7+m$Rl;l36<`W!a^K3fa~Nlv0q$AyNb^Q$#{xZ2Qwr+fOM;h zH&C4dp?>=k9JZ@5>YU+BO&e=5;oz^+CYx>NC(&BD5^suE3l)nwAY5XmqxrR_VC9oYySdVtdO6B}I zR1EA~9V+`OP={JMigKjL*IXya8AhCx z`?vv4h0Y;8?kQ`kUDH=ovWr$0*jb_c%QS2I&gQn}+BL29ODZas)Yp?Kf>^t@rE0h? zAv~zO*OqKEvZUD5n%Ydu`qtLE`Ed)71t06Uoub5H^wO3X?yH?wNKqbFOxO#`7C@TT$GQ|Xd8 z;6D?dq<4fJ70bvRU?naGPq2*SM2_MzyJPlLk&R%LsiZeQzqe%35G!h$mzO`cNq(6X z)y!}>X4J?p1N=mx4}8^B)E7-YuV%juE75Th`VHO*#|wQmt6Qd!AT(vxPP6s{6+#x5 zl`THSeg^)eVH4d!Jv4*Kmz}O!)zrMQx@u)Johx8Oq%ZYjRQz5EIthIj;%@`D`hNoo z=r>Ub2$c#h8u@o|QU1IYCiA?;+BvSgzS_!ZIsN^0^UMn_fQux;b^NrCPWdqh-+}G; z!JmThYHMdZU4-#$eH`PlJ*M+Egu{K(I*^`l1bz=hMgE=E7-FHY&6DC+`K_Pp&)XdP zBNi9190IB`fY!{obGcaI2te&s)?7md072^ck&4X0Gfcqs!O^;4lP29}*5P2kCR~0; z8!Bod6LZCO|MYg#q|dFd@57{%2_uuLhc|eBWYpe~J-5FjF97!&#}NhUA*4Tv=pmho zLS#~BUlDWZh>KiK&_&|lh>sNSnbl`{aK^k9Bdc+mDPJmPO&gps?Vf2;cRO49UVClR zEU4msA1!|p)mB|rv!xzKS-5izb8Z2C95?5NF_PnBR8@Kgv5fTw3~3CGaHHA^%sH;q z$)1F;u}?AS671ymi+hM~P(2BKnkBz7mCVBfwyId8AdS2UTUOCBooC}>wT-37`G737QNKQRbo8(CWYqKaVl z%MjTc8dNl|8dRccMU!V9X_`vfHr7|Kma3Ok^+nIo7Y$Zat!#i<+qrtCSUP3Z!WIPm z-zyUiP;fHob`+d^d+b;$=?Ik6y*yRHAIPiXi`$dOun6+1se1!b$8gKFJXP8I)CZ>Y zf$J-sz)M!lgJS78ru!UQHb0((G{r3)@AHXN3c@uhYgsKf4*67~n|x^3zoPp}o1Wtk zV+4GrPQ(f4g-~z8(5!v-n|bfn($l`&zT6(&mUVo8O9;LotH&ppFi#Ogz!XK#m+VRxc_Pa=nk! z2-a83C~h7$i>JgF0{HD43YxOY%VOk5fg5O|U*s#zqevrKH+Rd2H96YQoyi~{*5UmN@CbcCF2zH4x974d zHU+T*qe%D7Dn$>GD8)qJyzw84)><3P;YqEw+J>7e8_K3!d1ZyW%v0E4&_+&SHJxF* z7ss3Qaq*Sq?}{FW#cQyqWjP!+y)_T*Rf;=VhbSxlfq)(|lm#{P6w|G0&*9<+T=o!k zt^Bzt^KZBcVkMlSMWIt{XHF?1upE3OshnA3y;v=GJmU2TDS`^JsIag|G3o&>qo|!E!owU)aqnyYEe>geT@rhkh*4XdTV@GQX8v^%T394h2~}_WyeHC z84Tf!Wv8bh9!+{i9%Hew5%J(dnP5Q=wqSLzHfj%w6+PE^PxO9U)SZ{xkzdg6lz+m? zwbq7QM}yVTnwM1*6A{oZ zqsW7p@l4n}ioM3$PSV>9jA{->g_A%s?^KN3eR``v>UWo;Ayyw-o#AS>+nZe()vzbP6zG*3LB$f7?y9x!qkZ zSC`w_oRib+bW&r=M0GeEn68f8XltX>*_fS8pFpz`BA8aOJETywk$9d~6lEBogHAO* z@XpB3pW!W`&yu#ebKCGNU@KS^d{vtkmy|BTXKD1h`SaJV8yZ^w3*ssfzW{B#j2M^- z4#WIo8o9CpUz8~OWCZrd#x7H1WnH_;C`Pu*pRk{?s+LAIs;Kp$2b)5c%aCa zhF@jK2Y$mC5MMZr2^w5y96c(8GJuVqm1v^%@{1ZARw5!Bpzd9iA{Qvm?%3TXT^Y$^gS#g|x(lg;Sl*sRjNe&T09%Mq~0XbGzH!;TX5fQJdX`8|4Rx zmOH^YT+Au%XN2Wpr#Av0=cg42Q#1kRu(=BBiQ@s*)Va03erspfmiqcFU9GFDs#dp7 zSyNTDMw)f&^5wVAQa+8-4=-B$z3J1xw|LRv=``D*>H@_L3ISCoYYn&Isn9215dTYy zkUI1}2_K{A1O6h4Gmk_whPk;4Lc^@-F%FjJbVUshv@FZkTi3VO4S9_DF8Md%?4?NN zb~cnJx6ZOV^tmoaS4nzJi%Wh>3)qA86EstQMeB-QO5LGcItYw6Kze8K&K#Y-H)*s$6-DD)PA4MJb(_7p@Xy(~`N6d{(KFufMZekW%J8DD$N_^7-Hy-Gu` zDAv`)UO{Lv(s6A-5CnY(9eHkPU$NA^WyxL(TOc3TWQJ^8aZ!n9vC^+aQ%k2iSRJg8 zPwNWiRF(93)RlE$R@29HJbY!%%AIS(KxxPUSJCB;Ni_T*h_&}^t!%q=w!J-7m(Y^4 z>jq7@R;*rDgS}n4uol|gHS=qRiVGJ=GjCn7>E=$o(O}RYI~L!W9a%R}SlC}x)$fKg zSV_-9X!3GrK4r|%3%D%h zaG3~h5EV=Un|{0m-JkzEsG5(ganNDjY3nL0*G+>nV&#TuYj^HkyJp9ZH4E3QpFgy2 z^>FLr;?gBe%}dI>OC;CK^3v|y-0srynJ#00XGed3M`u4<+}+gD)6?A46P42rS9`m? zv)I#~gZZ_fhcUkSMXO_wW%Y7SZ(KK|F`0-a_-}k-r`XhqU0SP5ET8mJn%ViSnB=t* zCN?Va*s+AEnAmx4WnyO@S-J6+PFA5zEuUrIPF^7P{hK?vbJ~vP<{i_N&$bPNq%i_Q4M2)7Y}Rr)PJI@=0r`?Bw2wJGlcp`8d6Yo*PizccGmejNOsR3UZy} zcCsPQCI32zJsrVBOQ}1dyqoW2drMxDx6=BrP&&^{hCksMvG<%EGd^wz-^UT>-N%d1 zjT++L$D=nxi`RSMeZ1n_ef$HA_}pIohx=IR*SY%`7@IIO_A#P!la34f2>sNdMXZH+ z@_jTswvkX?-AO24&iQ-`Jda~wjT)k@5N-w>ccK0je5WbelyP8tj_!9}*S3a+ZC%P| z>)Pt-wXOW~!W7^L0JIi+9}E@bI*mdEtnQd2?}moH{%+_C#!&Gtz|d4IG)CBk+^y&l zcq$(~AQBYFO>u%S#LDyBro6$n#+5k{j`gkGOABL*aDoxR-UCH&HI}93Hao1jkxqL- zS6)UNFAz=bDyRRxYWnnuh>5_0_{=?nTn{Th`3l zH`@i7z@Tsc-s;${ENyLHzNfFU0=~leJ;mPcJRAh(%qpW*pM_alh4BNf5vSTmA6N+M z4j&mo99gscz!+SELcqR3T+8A1e@Y?d_HL`G-rhTVdrj^3-tGJLZQp*$CEHhT*}h`M z)=jIYuP&)r(=l~TW$79zZ)QnpkIU6lS~4@wIIq2Z9zAUF%!bC9^cZqG5QQZdXQA$n zT+H+w^sqNzrURDrToR!dCy945l_e{TeEloeG$m=P#&n(OD_7?Y$+^X#Of0t6s>)h7ZxUEz9 zY$Z>D)+uY@MtWgNq3rnH3+?!N?07`LqqQHz+RyrKy0Mx;YKdI5xN{{qz>Pi43yY%i z^5o}&*snl0vPwOHbfcuoqR@?Lz;~6CVCes7w_kv6v;rTmo0M(@?Dli$Mn8Hr1*QHM zcKhPLV7kwbJ^tUp^kN03 z53l;SVA?h+vrF0SkkrheivbcoD43r&e0%E9&{TRFXU%HFBd!}9ShsFqa9#A`*|Qfd znmv0lU=iop2oH7ZIgmT&%FxjL*M#ngrm)u0^0H7!dyL<5x(4k+-{&s!D&{MGQUzv2 zqXEC;c`-MorrFUP7m?6j(AAoj_jDM`*Ppbx6D%Hce0XAFRc>mD{T`AZnUI5#EJgdE zPUE;rz^j+XM}tim*ZG9J?44!OnL3M_8ycI_ZWQIeFvpEShN)dyO=4Pnq&cQO#= zv<78Bk0XJ5myX1%qs{Di_&R_1|pm>Vo7DHH!G_o)llxrD=*J87eKMe$m)mf$LK(J+z3FY z$R*NE?}W;aYopnmx7hczp?$-8c{ON`1^qpU{*tc5O7s(bp2#ox`-p>3|953<%DTN( z(v_3bRZ`kzw|A9xba!{OPoLi2*w)@q-`3jDIelhZ`>bv;*`1zIn1qQ+cPB>|IUGgw zyk3xP%geJ`3nEKhuCg+hs}ytSLZ7w;%weD$!p~%V9VFIR*$qU@LFVs`cbl{(r@4DE zP2cP)7t}%xxwGeb=B8o(stYlZqE3hDn?H}H&s&A*gP9_q)~736arx~C@{wl^ zPXUeK^>~o7o7Xd>rZ`awfk@ESd^N>_JO_E61!{^5Dcg9;Fr`4@FcW`U!EZ={0KGE1 zo4pq+XV75<^&Tpkr|{0I^d5g#Geq(bG}^i1RL(`u8I`3+C(SWW%>j&r98aMvA8$Y% zz8uubJCoKipw^M&&%tLXpblRS>c`uY*0I1>hd;-}I(#{>ztuYU90#=G%i;9r;4?f) z4%=u2yHxnS0s*5{G>4;AY?<&o1p;`A4Jm7Q9?S?&$w8^5JP-QDQ?iifGM)km;3@gY zvxcXjcRa;|l-;}@Kmbp1qSSt#2mRwI7UVg|^8f;PiVG>*cnTnZr~Lof=Nm!iDnXC? zgq!&+-wbM1j^F+Kw{7%h;A(n5m;cTgy%E%e-p}K|Aw2^B)9(TPJ0I`Y@ZW>{w+Fv> z^YTOdw-fpI^Y`=lZwuZZjivxcV-lv0WZDZ6<+gb66ciBkJ{9)eR! zu^`Vuo`*02rMQr?ji(SMpp^gb`V6X7j=R17ijzy6N+41POv8VDn@+Je;K-3H zgPbzN+^Z74Q1Td!Ql?Rw!SldT6&WZa^5C2wRyGBs;SQuxeLi0Rzm-4oR*i1pd2tYB z_QnM8Sm1K1Qp2Mn!@~6lLy&&xdeGdkNJD6_P8-6bl<-z?)@ao}f{wi{gwS8iJg;$F z`lCD@`Ptvlt8B^#Z1aflIR0w1kFZVnD+&Y3jb*^yI0eI%T#|BrrqI`QL9sy^wf|3u zf-F4k9|VPwKH;j#x4X*iwLOZmtF6#kU0~0vum|>c_UHoG zxoMR)6&)zrb(?s=AQ)PZxqjdfSbp7L5pTv6*!sFzYNNNpoV>S7HJ;v?tsY?)(3J-xo?D z=uBkSvNk{`_;q6u4nDmxFmoqgO#X)_+#PyV=v7yRB3gv@%FrwEzm{G5!=UL=({*@) zei-l-yjdmy<5EyUpGPoR@=A1K1>a(c+$oRDG|jw9Y6rr^ilbMPW8H1+hNJV&fJUC(2;pu;zi)vHWq1+u>Vt0$c)M zQE=9B*s7Vq7uVii;Daq+v)FwDtX8;=ZNzSTTG!2gdxS^XA^d(u*Zn`54TkBe2Y%kn zPtF1$C_#lFR__O){{#p5c7C2R%NN@D$Ju^)>plKfeCt!Flq4(fz-<$_^Qj<{14`1q zzXLZ;CeYb6_C0P!7_X1iOXK$a`%{rxU4p9@80FMIu9Q;;W#P8F1hu)5U%Ff^$O2+V(j7qsS zHWRdcDd6|Cj_wi^@cRt-iV*Fu1ob!SrQdHr+sK6NppPTKKsdKEb1}|~3*$)r`PZVd zvLa(?QBmoszRxbd{O`SkA76g?$Ai>%8Ol7Nmf_OA6?e^%oL@%Sja2ZIlDqfsmtX!F zi9?{VRm_MoIBq8zOU}|b1ch`6qcGLrf@0ulir@}IHvI0GZ#+fj*v!mWbCD-L%?976 zh=?d{wk_>cp$A((-WZp}@{0St=;7bg)_koLdt(r5 z&ew3P+6qa5XgD)IEytoYgohinmYlTsv8GR<0hVuyj|>V5*2kMzJ{l+}l3z=TGsY*$ zuXzg58txcn8nlM9ZrD3i|h8NKR2m4_-Vh}5Wi~G#ixwY#YQC_ z(Z10=Oaq&i5bbYu01h?yyqH63m6$7h4Ns?R?Wa8b zQ%WZYK>jVr56+1!7GT)v#Z^f~T#eJGFip|KK#4hq&=9RI_|ogc2v1YrQR7C`NF^`C zbBVd*m@dp>na~Q(9CveQKj(e<95ESbH#w2MlBbj9yhi(XN=Hk`FMNP>aFi5@Lt}pB zy6eo>UfcR({G<0MC%mn`_rfhv>Y;8HN_;Nd#`mcg>6>}_-+B7q)pX<+K0|s4l0M|l!-}Oc60tTGAtxi8?sK}Lk|dN_&Wb{1eT@Re_q7#4!abqi5rWx zXZeWE`c@f|XlU@d-TqZZjq6b3WUE{g z&~tWhT-PxNWx`q*-#jH`!vqt3!KZJP*Rlbg4t$Ap=3nKt!dpo9ukzFT_L=wYU3=`V zyN)TVyjFNgeXp$Y-o561`;_;5mFHrWF(;wGzNlC7uku{1GSTI+Ri1nLG;BQYgMZmv z@nbPW2Ym)YKxMNNmM@hg6;LuRQX%qH_UH6_WM}{gJ6j?z$5!H zKJuS^UU*FH0l$;*v6Hu=12-7i48Av?W#z(K3QbOraWeh0=r_1P?4(R~71GHT3DlbS zYv)&AeZ_VdjqfT{^N!JK_Q>dcSW7_8**}1j1`vsNNzbB87}s-h+g{RN1L1R!pKeM+8MHT_-s1 zA|dO!9VLCN{~%Lj!^gJO)o(vOyx^|w^;y-n9Jfwq%1O;APOaH^>)_z+8*8dJ+&Y-M zBuAUOB9HsoBAy8O+^VkpOK{wY;J|kdFT8VeP4%W94)<)VEO0DrYIO00#QH(r8?!A@ zBWQA6E-aVDVfkRDRc~o%EjB&)lnzCEqFysA;3 zoxwKf*o{_ienL(~20XYk%d72k5 zV|aw7vdEgB9BoQ1a^zO0eT(14*8C)+DW%Bns!ET5>u!8}MU_&bDA&`hKLE6crEcZlkMu3g^&mF}S;N&bF!L zbBl`RmODEWBjZ|fin{ajyNhyK;vy3}CFE{x-91!XGg_MuesxnT@yRMi`~{zS=n;< zM-|1z6$C{=;01AN0O$j@Wd?qu2nB=V(L@pD6A}R~n?7!MBlHb}G5D3RS2glycd~hR z;*j80`Cir|53!5oM{(`LE_MaN)UKyGE7)~x3D)x8va>y%(`d6b=GYr-wg$VUJTtT0 z!avz{_^qVkGdimx%Tk_|Rc^r-+BN6lgV4H;>A1=(ee(^VhiAwFod`fV^jau@2fHDO)es5TcOan~+vh)QlK?8(tP`Y)coyvUK9U8gVX zD|bg$Gn`)K7g=V_Us^nGsV6_oU2>*&_XejkyS9l&p8`4IuUIwGGjjUH$*AF*-~QJ0 z+uxoN`;9D8{=0GHXkcCtS0yi*j4Js_!ax~b3guX&k(DV$<>!qu7+P-8idf_nb-Zu_ zG~@0$3K)fOek4Rg)??{~h=;@7Jwgx_2rt>Q)2@V##fmGZO}lD&<;XrL*K#gyYQMx; z>NzSu!2S{v=GxZOv^_5&5od20fz*3-)+wJeBQidB{)op|0YRyr6LH zlOdp|`{1O$5Iv#@e5quCkFogu8AvlV390g*9PTrS;bA7j$eA$lMwW6_UaQ^Snx}kL zO`l#>+1(9-+9%!#qI%~@AB!>?H`|(At|puE8QoD?)!trJ*#X`H{Sa~S8N6+7<&r@8 z=q~9MNfbjuqXd$ZoTL9_qu`-DM1lO+iqNq_fF{tNK=OmYAD_#wI+9XKvWuz{YfDON z5@t13%&pc(MK&y`@pL)sdy6X*3iArRi4Nw{vL;t)T6#fZab8lsJ3YO)Fxgv|Kfg7o zb4g)Fbxz)NcdE;gknJqYNh)&6Pm+&6&0!zrkSxe;)I)*?5D9`^?0&A0i7|ZVWa8S9 zy#qV1HeIrL^CjZd=bO(H((GARAC%{?JLw`4dG+~c6+J5-{Q>SHAv!=f)I9;_a96EF zT+>7h3jGsB8DOvWUcaKEV#W2n>fetw%&5Jl=Md~{7a!_T{~kOYd)oZ6?iY(IR$SNH zdmR-}{<3+%U=Ii6?cNA_sI7Y_K%*y~{)VYic?%#%8u z@w>1?JVqnugTx*!Nym;sC>x~DD^6M;q&Bl#u zr>`<>X%hkF01#Jp(G!TjSm9T*sfNMXU2&_!8ie-(~$2NrTpH18~=M3p6?ngA8-h` zh!F96;zo}7$ajdZyk(rEhLA}(&OszrGs816@-U^bd1VU;Mi-WqFPfU0J9UwKvwllt zH^Y%nq|J%9}Fi%I_MO*OgzmY`~}(1KXP&g_W6Rcyf6W?OgxpdIs_5Jg?! zal5~`;Jz98P>db;#WU5m2C$2M*yeP%sZ@?GSrbo^~-? z@;5T|!5xveL`VM!3Jhf@99DPwclMiB)dEi{cIVW+^IGr3vScLQ~PF2 z;jPD?ABAYxE#1os<4mbh@%p@>_SPNg`piq^%fzW84~kPSk1|KN23qqL*~79|mP}t< z5NkwaHkYNy7@lfNa5USCtr3nAbsj^*Q?2nZdrwJ?O3p0oa%B}!q_GsBC)hS&wn(P~ zx!nkD4oL~*8cyHuVSP#Ro#m#B+2c<6&Lq}{d(|N{$hh1Or=v3vCixyNM5uLf5gFpcsRO;8O*V!$m8*E0)?KtV1OmRvr@bM_yx(%2U{*I`&T8f|BI4sMN%j zgR(-5KgWJZiaq;RDRw-A&PPey_~=M5 z?ta3V;#bh{Ss~NDjK3PGFQ^lL`Ia3NCTPvxxV88V3r8z(Bzq#^| z`1#1qzFRBtW9;Heb|5E5qlabEg{lN5`!1*3HRTcU zkoWEF_SUKxVo)=K{0ObiIzVTlljgks2+nv;k0TZ1ZJXE#d`yUHr-gER51`r%))31YZ<7F z?*e!qu(Ik<`3n^NBJ{WHsNBzvsyDw+J#6}oX(}ES`vtY27J@5iHqN;(75(5C*ZxiH znXqq6-{{W7d}I1L=xg~8LH2Lo3bF&r#@(Mfwx3|xN>La1&WB8m{$a;+eXfCO

i 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", lambda i: clear_and_return(ctx)) logging.info("Made it to app start") window.start() + sys.exit(0) diff --git a/src/screens/base.py b/src/screens/base.py index 4d2215b..25934bc 100644 --- a/src/screens/base.py +++ b/src/screens/base.py @@ -1,78 +1,20 @@ -from tkinter import PhotoImage, Canvas +from PyQt6.QtWidgets import QWidget -class Screen: - def __init__(self, canvas, controller): - self.canvas = canvas +class Screen(QWidget): + def __init__(self, controller): + super().__init__() self.controller = controller - self._items = [] - self._windows = [] - self._photos = [] self._build(controller) - self.hide() - def _photo(self, path): - img = PhotoImage(file=str(path)) - self._photos.append(img) - return img - - def _image(self, x, y, **kwargs): - item = self.canvas.create_image(x, y, **kwargs) - self._items.append(item) - return item - - def _text(self, x, y, **kwargs): - item = self.canvas.create_text(x, y, **kwargs) - self._items.append(item) - return item - - def _rounded_button(self, text, w, h, r, bg, fg, font, command, parent_bg="#153246"): - """A Canvas-based button with rounded corners.""" - c = Canvas(self.canvas, width=w, height=h, bg=parent_bg, highlightthickness=0) - c.create_polygon( - r, 0, w - r, 0, - w, 0, w, r, - w, h - r, w, h, - w - r, h, r, h, - 0, h, 0, h - r, - 0, r, 0, 0, - smooth=True, fill=bg, outline=bg, - ) - c.create_text(w // 2, h // 2, text=text, fill=fg, font=font) - c.bind("", lambda e: command()) - for item in c.find_all(): - c.tag_bind(item, "", lambda e: command()) - c.configure(cursor="hand2") - return c - - def _canvas_entry(self, x, y, w, h, font, fg="#F5F0E6"): - from .components.canvas_entry import CanvasEntry - entry = CanvasEntry(self.canvas, x, y, w, h, font, fg) - self._items.extend(entry.item_ids) - return entry - - def _window(self, x, y, widget, width=None, height=None): - kw = dict(anchor="nw", window=widget) - if width is not None: - kw["width"] = width - if height is not None: - kw["height"] = height - item = self.canvas.create_window(x, y, **kw) - self._windows.append(item) - return item - - def show(self): - for item in self._items: - self.canvas.itemconfigure(item, state="normal") - for win in self._windows: - self.canvas.itemconfigure(win, state="normal") + def _build(self, controller): + """Subclasses build their UI here instead of in __init__.""" + pass - def hide(self): - for item in self._items: - self.canvas.itemconfigure(item, state="hidden") - for win in self._windows: - self.canvas.itemconfigure(win, state="hidden") + def on_show(self): + """Called by NavigationController when this screen becomes visible.""" + pass - # Subclasses implement this instead of __init__ - def _build(self, controller): + def on_hide(self): + """Called by NavigationController just before this screen is hidden.""" pass diff --git a/src/screens/check_in_manual.py b/src/screens/check_in_manual.py index e0df8d6..d520f65 100644 --- a/src/screens/check_in_manual.py +++ b/src/screens/check_in_manual.py @@ -1,81 +1,86 @@ -from pathlib import Path -from tkinter import Button, END +from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel +from PyQt6.QtCore import Qt from .base import Screen -from .components.canvas_entry import CanvasEntry - -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "check_in_manual" -SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" +from .components.outline_frame import OutlineFrame +from .components.styled_button import StyledButton, home_button, INNER_MARGIN, OUTER_MARGIN +from .components.styled_entry import StyledEntry class CheckInManual(Screen): def _build(self, controller): - self.loading_text_id = None + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) - logo = self._photo(SHARED_PATH / "button_generic.png") - self._image(88.0, 90.0, image=logo) + outline = OutlineFrame() + outer.addWidget(outline) - home_img = self._photo(SHARED_PATH / "icon_home.png") - home_btn = Button( - self.canvas, image=home_img, bg="#153246", - command=lambda: controller.back_to_main(), - relief="flat", highlightthickness=0, bd=0, - ) - self._window(53.0, 55.0, home_btn) + inner = QVBoxLayout(outline) + inner.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + inner.setSpacing(0) - field_img = self._photo(SHARED_PATH / "field.png") - self._image(640.0, 424.0, image=field_img) + top_row = QHBoxLayout() + top_row.addWidget(home_button(lambda: controller.back_to_main())) + top_row.addStretch() + inner.addLayout(top_row) - self._text( - 640.0, 206.0, anchor="center", - text="If you have already made an\naccount, scan your UCSD barcode\nor enter your PID manually", - fill="#F5F0E6", font=("Montserrat", 48 * -1), justify="center", - ) - self._text( - 640.0, 492.0, anchor="center", - text="PID", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) + inner.addStretch(2) - btn_img = self._photo(ASSETS_PATH / "button_check_in.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=lambda: self._call_check_in(controller), relief="flat", + instruction = QLabel( + "Enter your UCSD PID below\n" + "to check in" ) - self._window(465.0, 598.0, btn, width=349, height=71) - - self.pid_entry = self._canvas_entry( - 640.0, 424.0, w=800, h=44, font=("Montserrat", 20), + instruction.setStyleSheet( + "color: #F5F0E6; font: 36pt Montserrat;" + "background: transparent; border: none;" ) + instruction.setAlignment(Qt.AlignmentFlag.AlignHCenter) + instruction.setWordWrap(True) + inner.addWidget(instruction) - def hide(self): - CanvasEntry.blur_all() - super().hide() + inner.addStretch(1) - def display_loading(self): - if self.loading_text_id is None: - self.loading_text_id = self.canvas.create_text( - 420.0, 545.0, anchor="nw", - text="PLEASE WAIT: LOADING...", - fill="#FF0000", font=("Montserrat", 36 * -1, "bold"), justify="center", - ) + pid_label = QLabel("PID") + pid_label.setStyleSheet( + "color: #F5F0E6; font: 18pt Montserrat;" + "background: transparent; border: none;" + ) + pid_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(pid_label) + + entry_row = QHBoxLayout() + self.pid_entry = StyledEntry() + self.pid_entry.setMaximumWidth(800) + entry_row.addStretch() + entry_row.addWidget(self.pid_entry) + entry_row.addStretch() + inner.addLayout(entry_row) + + inner.addStretch(2) + + btn_row = QHBoxLayout() + check_in_btn = StyledButton("Check In") + check_in_btn.setFixedWidth(349) + check_in_btn.clicked.connect(lambda: self._call_check_in(controller)) + btn_row.addStretch() + btn_row.addWidget(check_in_btn) + btn_row.addStretch() + inner.addLayout(btn_row) + + def on_hide(self): + self.pid_entry.clearFocus() def clear_entries(self): - self.pid_entry.delete(0, END) + self.pid_entry.clear() def update_entries(self, pid): - self.pid_entry.insert(0, pid) + self.pid_entry.setText(pid) def _call_check_in(self, controller): - pid = self.pid_entry.get() + pid = self.pid_entry.text().strip() if not pid: return - - self.display_loading() - self.canvas.update_idletasks() + controller.show_status("PLEASE WAIT: LOADING...") self.clear_entries() - self.controller.ctx.check_in.handle_by_pid(pid) - - if self.loading_text_id is not None: - self.canvas.delete(self.loading_text_id) - self.loading_text_id = None + controller.hide_status() diff --git a/src/screens/check_in_rfid.py b/src/screens/check_in_rfid.py index cdd72ed..49c4157 100644 --- a/src/screens/check_in_rfid.py +++ b/src/screens/check_in_rfid.py @@ -1,47 +1,67 @@ -from pathlib import Path -from tkinter import Button +from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel +from PyQt6.QtCore import Qt +import qtawesome as qta from .base import Screen +from .components.outline_frame import OutlineFrame +from .components.styled_button import StyledButton, NAV_BTN_SIZE, NAV_ICON_SIZE, INNER_MARGIN, OUTER_MARGIN from .qr_codes import QRCodes -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "check_in_rfid" -SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" - class CheckInRFID(Screen): def _build(self, controller): + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) - logo = self._photo(SHARED_PATH / "button_generic.png") - self._image(88.0, 90.0, image=logo) + outline = OutlineFrame() + outer.addWidget(outline) - self._text( - 640.0, 618.0, anchor="center", - text="Please tap ID on the black box to start", - fill="#F5F0E6", font=("Montserrat", 32 * -1), - ) - self._text( - 67.0, 270.0, anchor="nw", - text="UCSD Makerspace", - fill="#F5F0E6", font=("Montserrat", 113 * -1, "bold"), - ) - self._text( - 77.0, 377.66796875, anchor="nw", - text="Welcome Desk", - fill="#F5F0E6", font=("Montserrat", 73 * -1), + inner = QVBoxLayout(outline) + inner.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + inner.setSpacing(0) + + top_row = QHBoxLayout() + top_row.setContentsMargins(0, 0, 0, 0) + + qr_btn = StyledButton(ghost=True) + qr_btn.setIcon(qta.icon('mdi.qrcode-scan', color='#F5F0E6')) + qr_btn.setIconSize(NAV_ICON_SIZE) + qr_btn.setFixedSize(NAV_BTN_SIZE, NAV_BTN_SIZE) + qr_btn.clicked.connect(lambda: controller.show_frame(QRCodes)) + + no_id_btn = StyledButton("No ID", font_size=20, ghost=True) + no_id_btn.setFixedSize(NAV_BTN_SIZE, NAV_BTN_SIZE) + no_id_btn.clicked.connect(lambda: controller.go_to_no_id()) + + top_row.addWidget(qr_btn) + top_row.addStretch() + top_row.addWidget(no_id_btn) + inner.addLayout(top_row) + + inner.addStretch(2) + + title = QLabel("UCSD Makerspace") + title.setStyleSheet( + "color: #F5F0E6; font: bold 80pt Montserrat;" + "background: transparent; border: none;" ) + title.setAlignment(Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(title) - btn1_img = self._photo(ASSETS_PATH / "icon_check_in.png") - btn1 = Button( - self.canvas, image=btn1_img, bg="#153246", - command=lambda: controller.show_frame(QRCodes), - relief="flat", highlightthickness=0, bd=0, + subtitle = QLabel("Welcome Desk") + subtitle.setStyleSheet( + "color: #F5F0E6; font: 55pt Montserrat;" + "background: transparent; border: none;" ) - self._window(53.0, 55.0, btn1) - - btn2 = Button( - self.canvas, image=logo, text="No\nID", compound="center", - bg="#153246", fg="white", - command=lambda: controller.go_to_no_id(), - relief="flat", highlightthickness=0, bd=0, - font=("Montserrat", 36 * -1), + subtitle.setAlignment(Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(subtitle) + + inner.addStretch(3) + + instruction = QLabel("Please tap ID on the blue box to start") + instruction.setStyleSheet( + "color: #F5F0E6; font: 24pt Montserrat;" + "background: transparent; border: none;" ) - self._window(1130.0, 40.0, btn2) + instruction.setAlignment(Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(instruction) diff --git a/src/screens/components/dev_overlay.py b/src/screens/components/dev_overlay.py index ee81074..c31b44a 100644 --- a/src/screens/components/dev_overlay.py +++ b/src/screens/components/dev_overlay.py @@ -1,4 +1,5 @@ -import tkinter as tk +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton +from PyQt6.QtCore import Qt, QTimer from screens.check_in_rfid import CheckInRFID from screens.create_account_barcode import CreateAccountBarcode @@ -30,11 +31,13 @@ def _sim_no_account_needs_waiver(nav): nav.go_to_create_account(on_done=nav.go_to_sign_waiver) -def _sim_fill_and_go(nav): - frm = nav.get_frame(CreateAccountManual) - frm.clear_entries() - frm.pid_entry.insert(0, _DEV_PID) - nav.go_to_create_account_manual() +def _sim_barcode_swipe(nav): + nav.go_to_create_account_review( + pid=_DEV_PID, + first_name=_DEV_NAME.split()[0], + last_name=_DEV_NAME.split()[1], + email=_DEV_EMAIL, + ) TRANSITIONS = { @@ -57,7 +60,7 @@ def _sim_fill_and_go(nav): ("PID: no waiver", lambda nav: nav.go_to_sign_waiver()), ], CreateAccountBarcode: [ - ("sim barcode swipe", _sim_fill_and_go), + ("sim barcode swipe", _sim_barcode_swipe), ("manual fill", lambda nav: nav.go_to_create_account_manual()), ("← Main", lambda nav: nav.back_to_main()), ], @@ -80,42 +83,67 @@ def _sim_fill_and_go(nav): } -class DevOverlay: - def __init__(self, canvas, nav): - self._canvas = canvas +class DevOverlay(QWidget): + + def __init__(self, window, nav): + super().__init__(window.central) self._nav = nav - self._buttons = [] - - self._frame = tk.Frame(canvas, bg="#1a1a2e", relief="solid", bd=1) - tk.Label( - self._frame, - text="DEV NAV", - bg="#1a1a2e", fg="#aaaaaa", - font=("Courier", 9, "bold"), - ).pack(pady=(4, 2), padx=6) - - self._canvas_window = canvas.create_window( - 1270, 715, anchor="se", window=self._frame, + self._stacked = window.stacked + self._buttons: list[QPushButton] = [] + + self.setStyleSheet("QWidget { background-color: #1a1a2e; }") + + layout = QVBoxLayout(self) + layout.setContentsMargins(6, 6, 6, 6) + layout.setSpacing(2) + + header = QLabel("DEV NAV") + header.setStyleSheet( + "color: #aaaaaa; font: bold 9pt Courier;" + "background: transparent; border: none;" ) + header.setAlignment(Qt.AlignmentFlag.AlignHCenter) + layout.addWidget(header) + + self._layout = layout def update(self, screen_class): - for btn in self._buttons: - btn.destroy() + while self._layout.count() > 1: + item = self._layout.takeAt(1) + w = item.widget() + if w: + w.setParent(None) self._buttons.clear() for label, action in TRANSITIONS.get(screen_class, []): - btn = tk.Label( - self._frame, - text=label, - bg="#2a2a4e", fg="white", - font=("Courier", 9), - padx=6, pady=3, - cursor="hand2", - ) - btn.pack(fill="x", padx=4, pady=1) - btn.bind("", lambda e, a=action: a(self._nav)) - btn.bind("", lambda e, w=btn: w.configure(bg="#4a4a8e")) - btn.bind("", lambda e, w=btn: w.configure(bg="#2a2a4e")) + btn = QPushButton(label) + btn.setStyleSheet(""" + QPushButton { + background-color: #2a2a4e; + color: white; + font: 9pt Courier; + padding: 3px 6px; + border: none; + text-align: left; + } + QPushButton:hover { background-color: #4a4a8e; } + """) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + btn.clicked.connect(lambda checked, a=action: a(self._nav)) + self._layout.addWidget(btn) self._buttons.append(btn) - self._canvas.tag_raise(self._canvas_window) + QTimer.singleShot(0, self._refresh) + + def _refresh(self): + self.adjustSize() + self._reposition() + self.raise_() + self.show() + + def _reposition(self): + s = self._stacked + self.move( + s.x() + s.width() - self.width() - 10, + s.y() + s.height() - self.height() - 10, + ) diff --git a/src/screens/components/outline_frame.py b/src/screens/components/outline_frame.py new file mode 100644 index 0000000..53b2eb9 --- /dev/null +++ b/src/screens/components/outline_frame.py @@ -0,0 +1,30 @@ +from PyQt6.QtWidgets import QFrame +from PyQt6.QtGui import QPainter, QPen, QColor, QPainterPath +from PyQt6.QtCore import Qt, QRectF + + +class OutlineFrame(QFrame): + + def __init__(self, parent=None, radius=20): + super().__init__(parent) + self._radius = radius + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + half_pen = 2 + rect = QRectF(self.rect()).adjusted(half_pen, half_pen, -half_pen, -half_pen) + + path = QPainterPath() + path.addRoundedRect(rect, self._radius, self._radius) + + painter.fillPath(path, QColor(0, 0, 0, 35)) + + pen = QPen(QColor(240, 240, 240, 200)) + pen.setWidth(4) + pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin) + painter.setPen(pen) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawPath(path) diff --git a/src/screens/components/styled_button.py b/src/screens/components/styled_button.py new file mode 100644 index 0000000..8d41563 --- /dev/null +++ b/src/screens/components/styled_button.py @@ -0,0 +1,82 @@ +from PyQt6.QtWidgets import QPushButton +from PyQt6.QtGui import QPainter, QPainterPath, QColor, QPen, QFont +from PyQt6.QtCore import Qt, QRectF, QRect +import qtawesome as qta + +from .theme import OUTER_MARGIN, INNER_MARGIN, NAV_BTN_SIZE, NAV_ICON_SIZE + +__all__ = ["StyledButton", "home_button", "OUTER_MARGIN", "INNER_MARGIN", "NAV_BTN_SIZE", "NAV_ICON_SIZE"] + + +class StyledButton(QPushButton): + + def __init__(self, text="", parent=None, font_size=30, ghost=False, radius=20): + super().__init__(text, parent) + self._font_size = font_size + self._ghost = ghost + self._radius = radius + self._hovered = False + self.setCursor(Qt.CursorShape.PointingHandCursor) + self.setMinimumHeight(65) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self.setStyleSheet("background: transparent; border: none;") + + def enterEvent(self, event): + self._hovered = True + self.update() + super().enterEvent(event) + + def leaveEvent(self, event): + self._hovered = False + self.update() + super().leaveEvent(event) + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + if self._ghost: + half_pen = 2 + rect = QRectF(self.rect()).adjusted(half_pen, half_pen, -half_pen, -half_pen) + path = QPainterPath() + path.addRoundedRect(rect, self._radius, self._radius) + + painter.fillPath(path, QColor(255, 255, 255, 30) if self._hovered else QColor(0, 0, 0, 0)) + + pen = QPen(QColor(240, 240, 240, 200)) + pen.setWidth(4) + pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin) + painter.setPen(pen) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawPath(path) + + ico = self.icon() + if not ico.isNull(): + sz = self.iconSize() + ico.paint(painter, QRect( + (self.width() - sz.width()) // 2, + (self.height() - sz.height()) // 2, + sz.width(), sz.height(), + )) + else: + painter.setFont(QFont("Montserrat", self._font_size)) + painter.setPen(QColor("#F5F0E6")) + painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, self.text()) + else: + rect = QRectF(self.rect()).adjusted(1, 1, -1, -1) + path = QPainterPath() + path.addRoundedRect(rect, self._radius, self._radius) + painter.fillPath(path, QColor("#E8E4DA") if self._hovered else QColor("#F5F0E6")) + + painter.setFont(QFont("Montserrat", self._font_size)) + painter.setPen(QColor("#4EBEEE")) + painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, self.text()) + + +def home_button(on_click): + btn = StyledButton(ghost=True) + btn.setIcon(qta.icon('fa5s.home', color='#F5F0E6')) + btn.setIconSize(NAV_ICON_SIZE) + btn.setFixedSize(NAV_BTN_SIZE, NAV_BTN_SIZE) + btn.clicked.connect(on_click) + return btn diff --git a/src/screens/components/styled_entry.py b/src/screens/components/styled_entry.py new file mode 100644 index 0000000..4eb44c3 --- /dev/null +++ b/src/screens/components/styled_entry.py @@ -0,0 +1,30 @@ +from PyQt6.QtWidgets import QLineEdit +from PyQt6.QtCore import Qt + + +class StyledEntry(QLineEdit): + + def __init__(self, parent=None, font_size=20): + super().__init__(parent) + self._font_size = font_size + self.setMinimumHeight(54) + self._apply_style(readonly=False) + + def _apply_style(self, readonly: bool): + text_color = "#C8C0B0" if readonly else "#F5F0E6" + self.setStyleSheet(f""" + QLineEdit {{ + background-color: rgba(0, 0, 0, 80); + border: 2px solid #F5F0E6; + border-radius: 12px; + color: {text_color}; + font: {self._font_size}pt Montserrat; + padding: 6px 14px; + selection-background-color: #4EBEEE; + selection-color: #153246; + }} + """) + + def set_readonly(self, readonly: bool): + self.setReadOnly(readonly) + self._apply_style(readonly) diff --git a/src/screens/components/theme.py b/src/screens/components/theme.py new file mode 100644 index 0000000..7f736c9 --- /dev/null +++ b/src/screens/components/theme.py @@ -0,0 +1,7 @@ +from PyQt6.QtCore import QSize + +OUTER_MARGIN = 14 +INNER_MARGIN = 24 + +NAV_BTN_SIZE = 100 +NAV_ICON_SIZE = QSize(52, 52) diff --git a/src/screens/create_account_barcode.py b/src/screens/create_account_barcode.py index 0aa7b01..f7ae479 100644 --- a/src/screens/create_account_barcode.py +++ b/src/screens/create_account_barcode.py @@ -1,54 +1,45 @@ -from pathlib import Path -from tkinter import Button +from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel +from PyQt6.QtCore import Qt from .base import Screen - -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "create_account_barcode" -SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" +from .components.outline_frame import OutlineFrame +from .components.styled_button import StyledButton, home_button, INNER_MARGIN, OUTER_MARGIN class CreateAccountBarcode(Screen): def _build(self, controller): - logo = self._photo(SHARED_PATH / "button_generic.png") - self._image(88.0, 90.0, image=logo) - - home_img = self._photo(SHARED_PATH / "icon_home.png") - home_btn = Button( - self.canvas, image=home_img, bg="#153246", - command=lambda: controller.back_to_main(), - relief="flat", highlightthickness=0, bd=0, - ) - self._window(53.0, 55.0, home_btn) - - outline1_img = self._photo(ASSETS_PATH / "outline_1.png") - self._image(640.0, 76.0, image=outline1_img) + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) - outline2_img = self._photo(ASSETS_PATH / "outline_2.png") - self._image(640.0, 430.0, image=outline2_img) + outline = OutlineFrame() + outer.addWidget(outline) - icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") - self._image(576.0, 65.0, image=icon_unchecked) + inner = QVBoxLayout(outline) + inner.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + inner.setSpacing(0) - icon_checked = self._photo(SHARED_PATH / "icon_checked_box.png") - self._image(1030.0, 65.0, image=icon_checked) + top_row = QHBoxLayout() + top_row.addWidget(home_button(lambda: controller.back_to_main())) + top_row.addStretch() + inner.addLayout(top_row) - self._text( - 640.0, 374.0, anchor="center", - text="Please scan your ID barcode", - fill="#F5F0E6", font=("Montserrat", 48 * -1), - ) - self._text( - 215.0, 45.0, anchor="nw", - text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 690.0, 45.0, anchor="nw", - text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) + inner.addStretch(3) - btn_img = self._photo(ASSETS_PATH / "button_fill_manually.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=lambda: controller.go_to_create_account_manual(), relief="flat", + instruction = QLabel("Please scan your ID barcode") + instruction.setStyleSheet( + "color: #F5F0E6; font: 36pt Montserrat;" + "background: transparent; border: none;" ) - self._window(465.0, 554.0, btn, width=349, height=71) + instruction.setAlignment(Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(instruction) + + inner.addStretch(3) + + btn_row = QHBoxLayout() + fill_btn = StyledButton("Fill Manually") + fill_btn.setFixedWidth(349) + fill_btn.clicked.connect(lambda: controller.go_to_create_account_manual()) + btn_row.addStretch() + btn_row.addWidget(fill_btn) + btn_row.addStretch() + inner.addLayout(btn_row) diff --git a/src/screens/create_account_manual.py b/src/screens/create_account_manual.py index 5e57e15..c9d6d1b 100644 --- a/src/screens/create_account_manual.py +++ b/src/screens/create_account_manual.py @@ -1,79 +1,77 @@ -from pathlib import Path -from tkinter import Button, Canvas, END +from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel +from PyQt6.QtCore import Qt from .base import Screen -from .components.canvas_entry import CanvasEntry -import logging - -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "create_account_manual" -SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" +from .components.outline_frame import OutlineFrame +from .components.styled_button import StyledButton, home_button, INNER_MARGIN, OUTER_MARGIN +from .components.styled_entry import StyledEntry class CreateAccountManual(Screen): def _build(self, controller): - logo = self._photo(SHARED_PATH / "button_generic.png") - self._image(88.0, 90.0, image=logo) - - home_img = self._photo(SHARED_PATH / "icon_home.png") - home_btn = Button( - self.canvas, image=home_img, bg="#153246", - command=lambda: controller.back_to_main(), - relief="flat", highlightthickness=0, bd=0, - ) - self._window(53.0, 55.0, home_btn) + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) - outline1_img = self._photo(ASSETS_PATH / "outline_1.png") - self._image(640.0, 76.0, image=outline1_img) + outline = OutlineFrame() + outer.addWidget(outline) - outline2_img = self._photo(ASSETS_PATH / "outline_2.png") - self._image(640.0, 430.0, image=outline2_img) + inner = QVBoxLayout(outline) + inner.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + inner.setSpacing(0) - icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") - self._image(605.0, 77.0, image=icon_unchecked) - self._image(1010.0, 77.0, image=icon_unchecked) + top_row = QHBoxLayout() + top_row.addWidget(home_button(lambda: controller.back_to_main())) + top_row.addStretch() + inner.addLayout(top_row) - field_img = self._photo(SHARED_PATH / "field.png") - self._image(640.0, 390.0, image=field_img) + inner.addStretch(3) - self._text( - 250.0, 45.0, anchor="nw", - text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 670.0, 45.0, anchor="nw", - text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 640.0, 340.0, anchor="center", - text="PID", fill="#F5F0E6", font=("Montserrat", 24 * -1), + pid_label = QLabel("PID") + pid_label.setStyleSheet( + "color: #F5F0E6; font: 18pt Montserrat;" + "background: transparent; border: none;" ) + pid_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(pid_label) - btn_img = self._photo(ASSETS_PATH / "register.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=self._go_to_review, relief="flat", - ) - self._window(465.0, 460.0, btn, width=349, height=71) + entry_row = QHBoxLayout() + self.pid_entry = StyledEntry() + self.pid_entry.setMaximumWidth(800) + entry_row.addStretch() + entry_row.addWidget(self.pid_entry) + entry_row.addStretch() + inner.addLayout(entry_row) - no_pid_btn = self._rounded_button( - text="I don't have a PID →", - w=349, h=80, r=20, - bg="#F5F0E6", fg="#4EBEEE", - font=("Montserrat", 20), - command=lambda: controller.go_to_create_account_no_pid(), - ) - self._window(465.0, 545.0, no_pid_btn, width=349, height=80) + inner.addStretch(2) + + btn_row = QHBoxLayout() + register_btn = StyledButton("Register") + register_btn.setFixedWidth(349) + register_btn.clicked.connect(self._go_to_review) + btn_row.addStretch() + btn_row.addWidget(register_btn) + btn_row.addStretch() + inner.addLayout(btn_row) + + inner.addSpacing(12) - self.pid_entry = self._canvas_entry(640.0, 390.0, w=800, h=44, font=("Montserrat", 20)) + no_pid_row = QHBoxLayout() + no_pid_btn = StyledButton("I don't have a PID →") + no_pid_btn.setFixedWidth(349) + no_pid_btn.setMinimumHeight(80) + no_pid_btn.clicked.connect(lambda: controller.go_to_create_account_no_pid()) + no_pid_row.addStretch() + no_pid_row.addWidget(no_pid_btn) + no_pid_row.addStretch() + inner.addLayout(no_pid_row) - def hide(self): - CanvasEntry.blur_all() - super().hide() + def on_hide(self): + self.pid_entry.clearFocus() def clear_entries(self): - self.pid_entry.delete(0, END) + self.pid_entry.clear() def _go_to_review(self): - pid = self.pid_entry.get().strip() + pid = self.pid_entry.text().strip() self.clear_entries() self.controller.ctx.account.go_to_review_from_pid(pid) diff --git a/src/screens/create_account_no_pid.py b/src/screens/create_account_no_pid.py index 1e7160e..f45f349 100644 --- a/src/screens/create_account_no_pid.py +++ b/src/screens/create_account_no_pid.py @@ -1,86 +1,78 @@ -from pathlib import Path -from tkinter import Button, END -from .base import Screen -from .components.canvas_entry import CanvasEntry +from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel +from PyQt6.QtCore import Qt import logging - -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "create_account_manual" -SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" +from .base import Screen +from .components.outline_frame import OutlineFrame +from .components.styled_button import StyledButton, home_button, INNER_MARGIN, OUTER_MARGIN +from .components.styled_entry import StyledEntry class CreateAccountNoPid(Screen): def _build(self, controller): - logo = self._photo(SHARED_PATH / "button_generic.png") - self._image(88.0, 90.0, image=logo) + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) - home_img = self._photo(SHARED_PATH / "icon_home.png") - home_btn = Button( - self.canvas, image=home_img, bg="#153246", - command=lambda: controller.back_to_main(), - relief="flat", highlightthickness=0, bd=0, - ) - self._window(53.0, 55.0, home_btn) + outline = OutlineFrame() + outer.addWidget(outline) - outline1_img = self._photo(ASSETS_PATH / "outline_1.png") - self._image(640.0, 76.0, image=outline1_img) + inner = QVBoxLayout(outline) + inner.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + inner.setSpacing(0) - outline2_img = self._photo(ASSETS_PATH / "outline_2.png") - self._image(640.0, 430.0, image=outline2_img) + top_row = QHBoxLayout() + top_row.addWidget(home_button(lambda: controller.back_to_main())) + top_row.addStretch() + inner.addLayout(top_row) - icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") - self._image(605.0, 77.0, image=icon_unchecked) - self._image(1010.0, 77.0, image=icon_unchecked) + inner.addStretch(1) - field_img = self._photo(SHARED_PATH / "field.png") - self._image(640.0, 289.0, image=field_img) - self._image(640.0, 390.0, image=field_img) - self._image(640.0, 490.0, image=field_img) + def _field_row(label_text): + lbl = QLabel(label_text) + lbl.setStyleSheet( + "color: #F5F0E6; font: 18pt Montserrat;" + "background: transparent; border: none;" + ) + lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(lbl) + + row = QHBoxLayout() + entry = StyledEntry() + entry.setMaximumWidth(800) + row.addStretch() + row.addWidget(entry) + row.addStretch() + inner.addLayout(row) + inner.addSpacing(10) + return entry - self._text( - 250.0, 45.0, anchor="nw", - text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 670.0, 45.0, anchor="nw", - text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 640.0, 240.0, anchor="center", - text="First Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 640.0, 341.0, anchor="center", - text="Last Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 640.0, 441.0, anchor="center", - text="Email", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) + self.first_name_entry = _field_row("First Name") + self.last_name_entry = _field_row("Last Name") + self.email_entry = _field_row("Email") - btn_img = self._photo(ASSETS_PATH / "register.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=self._submit, relief="flat", - ) - self._window(465.0, 545.0, btn, width=349, height=71) + inner.addStretch(1) - self.first_name_entry = self._canvas_entry(640.0, 289.0, w=800, h=44, font=("Montserrat", 20)) - self.last_name_entry = self._canvas_entry(640.0, 390.0, w=800, h=44, font=("Montserrat", 20)) - self.email_entry = self._canvas_entry(640.0, 490.0, w=800, h=44, font=("Montserrat", 20)) + btn_row = QHBoxLayout() + register_btn = StyledButton("Register") + register_btn.setFixedWidth(349) + register_btn.clicked.connect(self._submit) + btn_row.addStretch() + btn_row.addWidget(register_btn) + btn_row.addStretch() + inner.addLayout(btn_row) - def hide(self): - CanvasEntry.blur_all() - super().hide() + def on_hide(self): + for entry in (self.first_name_entry, self.last_name_entry, self.email_entry): + entry.clearFocus() def clear_entries(self): for entry in (self.first_name_entry, self.last_name_entry, self.email_entry): - entry.delete(0, END) + entry.clear() def _submit(self): - first = self.first_name_entry.get().strip() - last = self.last_name_entry.get().strip() - email = self.email_entry.get().strip() + first = self.first_name_entry.text().strip() + last = self.last_name_entry.text().strip() + email = self.email_entry.text().strip() self.clear_entries() try: self.controller.ctx.account.create_account_from_review( diff --git a/src/screens/create_account_review.py b/src/screens/create_account_review.py index 05849ff..7beb70e 100644 --- a/src/screens/create_account_review.py +++ b/src/screens/create_account_review.py @@ -1,107 +1,95 @@ -from pathlib import Path -from tkinter import Button, END -from .base import Screen -from .components.canvas_entry import CanvasEntry +from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel +from PyQt6.QtCore import Qt import logging - -ASSETS_PATH = Path(__file__).parent.parent / "assets" / "create_account_manual" -SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" +from .base import Screen +from .components.outline_frame import OutlineFrame +from .components.styled_button import StyledButton, home_button, INNER_MARGIN, OUTER_MARGIN +from .components.styled_entry import StyledEntry class CreateAccountReview(Screen): def _build(self, controller): - logo = self._photo(SHARED_PATH / "button_generic.png") - self._image(88.0, 90.0, image=logo) + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) - home_img = self._photo(SHARED_PATH / "icon_home.png") - home_btn = Button( - self.canvas, image=home_img, bg="#153246", - command=lambda: controller.back_to_main(), - relief="flat", highlightthickness=0, bd=0, - ) - self._window(53.0, 55.0, home_btn) + outline = OutlineFrame() + outer.addWidget(outline) - outline1_img = self._photo(ASSETS_PATH / "outline_1.png") - self._image(640.0, 76.0, image=outline1_img) + inner = QVBoxLayout(outline) + inner.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + inner.setSpacing(0) - outline2_img = self._photo(ASSETS_PATH / "outline_2.png") - self._image(640.0, 430.0, image=outline2_img) + top_row = QHBoxLayout() + top_row.addWidget(home_button(lambda: controller.back_to_main())) + top_row.addStretch() + inner.addLayout(top_row) - icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") - self._image(605.0, 77.0, image=icon_unchecked) - self._image(1010.0, 77.0, image=icon_unchecked) + inner.addSpacing(8) - field_img = self._photo(SHARED_PATH / "field.png") - self._image(640.0, 239.0, image=field_img) - self._image(640.0, 339.0, image=field_img) - self._image(640.0, 440.0, image=field_img) - self._image(640.0, 542.0, image=field_img) + def _field_row(label_text): + lbl = QLabel(label_text) + lbl.setStyleSheet( + "color: #F5F0E6; font: 18pt Montserrat;" + "background: transparent; border: none;" + ) + lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(lbl) - self._text( - 250.0, 45.0, anchor="nw", - text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 670.0, 45.0, anchor="nw", - text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 640.0, 189.0, anchor="center", - text="First Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 640.0, 290.0, anchor="center", - text="Last Name", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 640.0, 391.0, anchor="center", - text="Email", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) - self._text( - 640.0, 492.0, anchor="center", - text="PID", fill="#F5F0E6", font=("Montserrat", 24 * -1), - ) + row = QHBoxLayout() + entry = StyledEntry() + entry.setMaximumWidth(800) + row.addStretch() + row.addWidget(entry) + row.addStretch() + inner.addLayout(row) + inner.addSpacing(8) + return entry - btn_img = self._photo(ASSETS_PATH / "register.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=self._submit, relief="flat", - ) - self._window(465.0, 598.0, btn, width=349, height=71) + self.first_name_entry = _field_row("First Name") + self.last_name_entry = _field_row("Last Name") + self.email_entry = _field_row("Email") + self.pid_entry = _field_row("PID") - self.first_name_entry = self._canvas_entry(640.0, 239.0, w=800, h=44, font=("Montserrat", 20)) - self.last_name_entry = self._canvas_entry(640.0, 339.0, w=800, h=44, font=("Montserrat", 20)) - self.email_entry = self._canvas_entry(640.0, 440.0, w=800, h=44, font=("Montserrat", 20)) - self.pid_entry = self._canvas_entry(640.0, 542.0, w=800, h=44, font=("Montserrat", 20)) + inner.addStretch(1) + + btn_row = QHBoxLayout() + register_btn = StyledButton("Register") + register_btn.setFixedWidth(349) + register_btn.clicked.connect(self._submit) + btn_row.addStretch() + btn_row.addWidget(register_btn) + btn_row.addStretch() + inner.addLayout(btn_row) def setup(self, first_name="", last_name="", email="", pid="", pid_locked=False): self.clear_entries() if first_name: - self.first_name_entry.insert(0, first_name) + self.first_name_entry.setText(first_name) if last_name: - self.last_name_entry.insert(0, last_name) + self.last_name_entry.setText(last_name) if email: - self.email_entry.insert(0, email) + self.email_entry.setText(email) if pid: - self.pid_entry.insert(0, pid) + self.pid_entry.setText(pid) self.pid_entry.set_readonly(pid_locked) - def hide(self): - CanvasEntry.blur_all() - super().hide() + def on_hide(self): + for entry in (self.first_name_entry, self.last_name_entry, + self.email_entry, self.pid_entry): + entry.clearFocus() def clear_entries(self): for entry in (self.first_name_entry, self.last_name_entry, self.email_entry, self.pid_entry): - entry.delete(0, END) + entry.clear() self.pid_entry.set_readonly(False) def _submit(self): - first = self.first_name_entry.get().strip() - last = self.last_name_entry.get().strip() - email = self.email_entry.get().strip() - pid = self.pid_entry.get().strip() + first = self.first_name_entry.text().strip() + last = self.last_name_entry.text().strip() + email = self.email_entry.text().strip() + pid = self.pid_entry.text().strip() self.clear_entries() try: self.controller.ctx.account.create_account_from_review( diff --git a/src/screens/qr_codes.py b/src/screens/qr_codes.py index 974ded5..823e77b 100644 --- a/src/screens/qr_codes.py +++ b/src/screens/qr_codes.py @@ -1,35 +1,59 @@ from pathlib import Path -from tkinter import Button +from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel +from PyQt6.QtGui import QPixmap +from PyQt6.QtCore import Qt from .base import Screen +from .components.outline_frame import OutlineFrame +from .components.styled_button import home_button, INNER_MARGIN, OUTER_MARGIN ASSETS_PATH = Path(__file__).parent.parent / "assets" / "qr_codes" -SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" class QRCodes(Screen): def _build(self, controller): - logo = self._photo(SHARED_PATH / "button_generic.png") - self._image(88.0, 90.0, image=logo) - - qr_website_img = self._photo(ASSETS_PATH / "qr_website.png") - self._image(421.0, 360.0, image=qr_website_img) - - qr_waiver_img = self._photo(ASSETS_PATH / "qr_waiver.png") - self._image(859.0, 360.0, image=qr_waiver_img) - - self._text( - 421.0, 571.0, anchor="center", - text="Website", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 859.0, 571.0, anchor="center", - text="Waiver", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - - btn_img = self._photo(SHARED_PATH / "icon_home.png") - btn = Button( - self.canvas, image=btn_img, bg="#153246", - command=lambda: controller.back_to_main(), - relief="flat", - ) - self._window(53.0, 55.0, btn) + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) + + outline = OutlineFrame() + outer.addWidget(outline) + + inner = QVBoxLayout(outline) + inner.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + inner.setSpacing(0) + + top_row = QHBoxLayout() + top_row.addWidget(home_button(lambda: controller.back_to_main())) + top_row.addStretch() + inner.addLayout(top_row) + + inner.addStretch(1) + + qr_row = QHBoxLayout() + qr_row.setSpacing(80) + + def _qr_col(image_path, caption): + col = QVBoxLayout() + col.setSpacing(12) + img = QLabel() + px = QPixmap(str(image_path)) + img.setPixmap(px) + img.setAlignment(Qt.AlignmentFlag.AlignHCenter) + img.setStyleSheet("background: transparent; border: none;") + lbl = QLabel(caption) + lbl.setStyleSheet( + "color: #F5F0E6; font: 30pt Montserrat;" + "background: transparent; border: none;" + ) + lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) + col.addWidget(img) + col.addWidget(lbl) + return col + + qr_row.addStretch() + qr_row.addLayout(_qr_col(ASSETS_PATH / "qr_website.png", "Website")) + qr_row.addLayout(_qr_col(ASSETS_PATH / "qr_waiver.png", "Waiver")) + qr_row.addStretch() + inner.addLayout(qr_row) + + inner.addStretch(1) diff --git a/src/screens/sign_waiver.py b/src/screens/sign_waiver.py index 3924b90..f012d20 100644 --- a/src/screens/sign_waiver.py +++ b/src/screens/sign_waiver.py @@ -1,49 +1,72 @@ from pathlib import Path -from tkinter import Button +from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel +from PyQt6.QtGui import QPixmap +from PyQt6.QtCore import Qt from .base import Screen +from .components.outline_frame import OutlineFrame +from .components.styled_button import StyledButton, OUTER_MARGIN, INNER_MARGIN ASSETS_PATH = Path(__file__).parent.parent / "assets" / "sign_waiver" -SHARED_PATH = Path(__file__).parent.parent / "assets" / "shared" class SignWaiver(Screen): def _build(self, controller): - outline1_img = self._photo(ASSETS_PATH / "outline_1.png") - self._image(1042.0, 359.0, image=outline1_img) + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) - outline2_img = self._photo(ASSETS_PATH / "outline_2.png") - self._image(408.0, 76.0, image=outline2_img) + outline = OutlineFrame() + outer.addWidget(outline) - outline3_img = self._photo(ASSETS_PATH / "outline_3.png") - self._image(408.0, 429.0, image=outline3_img) + root = QVBoxLayout(outline) + root.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + root.setSpacing(0) - icon_checked = self._photo(SHARED_PATH / "icon_checked_box.png") - self._image(395.0, 70.0, image=icon_checked) + content = QHBoxLayout() + content.setContentsMargins(50, 0, 50, 0) + content.setSpacing(20) - icon_unchecked = self._photo(SHARED_PATH / "icon_unchecked_box.png") - self._image(750.0, 70.0, image=icon_unchecked) + left = QVBoxLayout() + left.setSpacing(0) - qr_waiver_img = self._photo(ASSETS_PATH / "qr_waiver.png") - self._image(1042.0, 328.0, image=qr_waiver_img) + left.addStretch(1) - self._text( - 37.0, 45.0, anchor="nw", - text="Account Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), + instruction = QLabel( + "Please scan the QR code\non the right and sign the waiver" ) - self._text( - 430.0, 45.0, anchor="nw", - text="Waiver Status:", fill="#F5F0E6", font=("Montserrat", 40 * -1), - ) - self._text( - 45.0, 270.0, anchor="nw", - text="Please scan the QR code\non the right and sign our \n waiver", - fill="#F5F0E6", font=("Montserrat", 48 * -1), + instruction.setStyleSheet( + "color: #F5F0E6; font: 36pt Montserrat;" + "background: transparent; border: none;" ) + instruction.setAlignment(Qt.AlignmentFlag.AlignHCenter) + instruction.setWordWrap(True) + left.addWidget(instruction) - btn_img = self._photo(ASSETS_PATH / "button_done_scanning.png") - btn = Button( - self.canvas, image=btn_img, - borderwidth=0, highlightthickness=0, - command=lambda: controller.back_to_main(), relief="flat", - ) - self._window(875.0, 581.0, btn, width=344, height=71) + left.addStretch(2) + + content.addLayout(left, stretch=1) + + right = QVBoxLayout() + right.setSpacing(0) + right.addStretch() + + qr_px = QPixmap(str(ASSETS_PATH / "qr_waiver.png")) + qr_px = qr_px.scaled(320, 320, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) + qr_label = QLabel() + qr_label.setPixmap(qr_px) + qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + qr_label.setStyleSheet("background: transparent; border: none;") + right.addWidget(qr_label) + + right.addSpacing(24) + + done_btn = StyledButton("Done Scanning") + done_btn.setFixedWidth(280) + done_btn.clicked.connect(lambda: controller.back_to_main()) + right.addWidget(done_btn, alignment=Qt.AlignmentFlag.AlignHCenter) + + right.addStretch() + + content.addLayout(right, stretch=1) + + root.addLayout(content) diff --git a/src/screens/transition_screen.py b/src/screens/transition_screen.py index b6df9ac..4130371 100644 --- a/src/screens/transition_screen.py +++ b/src/screens/transition_screen.py @@ -1,15 +1,35 @@ +from PyQt6.QtWidgets import QVBoxLayout, QLabel +from PyQt6.QtCore import Qt from .base import Screen +from .components.outline_frame import OutlineFrame +from .components.theme import INNER_MARGIN, OUTER_MARGIN class TransitionScreen(Screen): def _build(self, controller): - self._msg_id = self._text( - 640, 340, anchor="center", - text="", - fill="#F5F0E6", font=("Montserrat", 64 * -1), - justify="center", + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) + + outline = OutlineFrame() + outer.addWidget(outline) + + inner = QVBoxLayout(outline) + inner.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + + inner.addStretch() + + self._msg_label = QLabel("") + self._msg_label.setStyleSheet( + "color: #F5F0E6; font: 48pt Montserrat;" + "background: transparent; border: none;" ) + self._msg_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) + self._msg_label.setWordWrap(True) + inner.addWidget(self._msg_label) + + inner.addStretch() def display(self, message): - self.canvas.itemconfigure(self._msg_id, text=message) + self._msg_label.setText(message) self.controller.show_frame(TransitionScreen) diff --git a/src/screens/user_welcome.py b/src/screens/user_welcome.py index 3804eb3..701bcd8 100644 --- a/src/screens/user_welcome.py +++ b/src/screens/user_welcome.py @@ -1,54 +1,80 @@ +from PyQt6.QtWidgets import QVBoxLayout, QLabel +from PyQt6.QtCore import Qt, QTimer from .base import Screen +from .components.outline_frame import OutlineFrame +from .components.theme import INNER_MARGIN, OUTER_MARGIN class UserWelcome(Screen): def _build(self, controller): - self.last_name = None - self.offset = 0 + self._last_name = None + self._active_labels: set = set() - self._msg_item = self._text( - 99.33203125, 259.33203125, anchor="nw", - text="Welcome back", - fill="#F5F0E6", font=("Montserrat", 45 * -1), + outer = QVBoxLayout(self) + outer.setContentsMargins(OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN, OUTER_MARGIN) + outer.setSpacing(0) + + outline = OutlineFrame() + outer.addWidget(outline) + + inner = QVBoxLayout(outline) + inner.setContentsMargins(INNER_MARGIN, INNER_MARGIN, INNER_MARGIN, INNER_MARGIN) + inner.setSpacing(0) + + inner.addStretch() + + self._msg_label = QLabel("Welcome back") + self._msg_label.setStyleSheet( + "color: #F5F0E6; font: 42pt Montserrat;" + "background: transparent; border: none;" ) + self._msg_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(self._msg_label) + + inner.addSpacing(8) - def hide(self): - super().hide() - self.canvas.delete("welcome") - self.canvas.itemconfigure(self._msg_item, text="Welcome back") - self.last_name = None - self.offset = 0 + self._names_layout = QVBoxLayout() + self._names_layout.setContentsMargins(0, 0, 0, 0) + self._names_layout.setSpacing(0) + inner.addLayout(self._names_layout) + + inner.addStretch() + + def on_hide(self): + self._active_labels.clear() + while self._names_layout.count(): + item = self._names_layout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + self._msg_label.setText("Welcome back") + self._last_name = None def display_name(self, name, message="Welcome back"): - if name == self.last_name: + if name == self._last_name: return - self.last_name = name - self.canvas.itemconfigure(self._msg_item, text=message) + self._last_name = name + self._msg_label.setText(message) self.controller.show_frame(UserWelcome) - text_id = self.canvas.create_text( - 99.0, - 323.0 + self.offset, - anchor="nw", - text=name, - fill="#F5F0E6", - font=("Montserrat", 73 * -1), - tag="welcome", + label = QLabel(name) + label.setStyleSheet( + "color: #F5F0E6; font: bold 88pt Montserrat;" + "background: transparent; border: none;" ) + label.setAlignment(Qt.AlignmentFlag.AlignHCenter) + self._names_layout.addWidget(label) + self._active_labels.add(label) - self.offset += 73 - self.canvas.after(3000, lambda: self._remove_name(text_id)) + QTimer.singleShot(3000, lambda: self._remove_name(label)) - def _remove_name(self, text_id): - self.canvas.delete(text_id) - self.offset -= 73 - - for text in self.canvas.find_withtag("welcome"): - coords = self.canvas.coords(text) - if coords[1] > 323.0: - self.canvas.move(text, 0, -73) + def _remove_name(self, label): + if label not in self._active_labels: + return + self._active_labels.discard(label) + self._names_layout.removeWidget(label) + label.deleteLater() - if not self.canvas.find_withtag("welcome"): - self.last_name = None + if not self._active_labels: + self._last_name = None self.controller.back_to_main() diff --git a/src/window.py b/src/window.py index 23be69a..5d81643 100644 --- a/src/window.py +++ b/src/window.py @@ -1,38 +1,67 @@ -import tkinter as tk from pathlib import Path +from PyQt6.QtWidgets import QMainWindow, QWidget, QStackedWidget +from PyQt6.QtGui import QFontDatabase, QPainter, QPixmap, QColor +from PyQt6.QtCore import QTimer, Qt ASSETS_PATH = Path(__file__).parent / "assets" / "shared" -class CheckInWindow(tk.Tk): +class _RootWidget(QWidget): + """Central widget that paints background_main.png centered on the dark base color.""" + + def __init__(self, parent=None): + super().__init__(parent) + bg_path = ASSETS_PATH / "background_main.png" + self._bg = QPixmap(str(bg_path)) if bg_path.exists() else QPixmap() + + def paintEvent(self, event): + painter = QPainter(self) + # Dark base fill + painter.fillRect(self.rect(), QColor("#153246")) + # Background image centered + if not self._bg.isNull(): + x = (self.width() - self._bg.width()) // 2 + y = (self.height() - self._bg.height()) // 2 + painter.drawPixmap(x, y, self._bg) + + +class CheckInWindow(QMainWindow): def __init__(self): super().__init__() - self.title("Check-In") - self.geometry("1280x720") - self.bind("", self._on_map) - - self.canvas = tk.Canvas( - self, - bg="#153246", - height=720, - width=1280, - bd=0, - highlightthickness=0, - ) - self.canvas.pack(fill="both", expand=True) - - self._bg_photos = [] - bg1 = tk.PhotoImage(file=str(ASSETS_PATH / "background_main.png")) - self._bg_photos.append(bg1) - self.canvas.create_image(640.0, 360.0, image=bg1) - - bg2 = tk.PhotoImage(file=str(ASSETS_PATH / "outline_full.png")) - self._bg_photos.append(bg2) - self.canvas.create_image(639.333984375, 359.333984375, image=bg2) - - def _on_map(self, event): - self.unbind("") - self.attributes("-fullscreen", True) + self.setWindowTitle("Check-In") + self.setFixedSize(1280, 720) + + # Load Montserrat if bundled; falls back to system font + fonts_dir = Path(__file__).parent.parent / "fonts" + if fonts_dir.exists(): + for font_file in fonts_dir.glob("*.ttf"): + QFontDatabase.addApplicationFont(str(font_file)) + + self.central = _RootWidget() + self.setCentralWidget(self.central) + + # Stacked widget fills the central widget; transparent so bg shows through + self.stacked = QStackedWidget(self.central) + self.stacked.setGeometry(0, 0, 1280, 720) + self.stacked.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self.stacked.setStyleSheet("background: transparent;") + + self._escape_handler = None + + def set_escape_handler(self, fn): + self._escape_handler = fn + + def keyPressEvent(self, event): + if event.key() == Qt.Key.Key_Escape and self._escape_handler: + self._escape_handler() + else: + super().keyPressEvent(event) + + def after(self, ms, fn): + """Drop-in replacement for tkinter's window.after().""" + QTimer.singleShot(ms, fn) def start(self): - self.mainloop() + from PyQt6.QtWidgets import QApplication + self.showFullScreen() + QApplication.instance().exec() From c3e7da804d63e821bcd51f68612a682cf5d6ae51 Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 20:19:50 -0700 Subject: [PATCH 17/28] fix dev image tagging --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 442775b..89f359c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: run: | if [ "${{ github.ref_name }}" = "dev" ]; then echo "name=check-in-dev" >> $GITHUB_OUTPUT - echo "extra_tag=ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in-dev:dev" >> $GITHUB_OUTPUT + echo "extra_tag=ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in-dev:latest" >> $GITHUB_OUTPUT else echo "name=check-in" >> $GITHUB_OUTPUT echo "extra_tag=ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in:latest" >> $GITHUB_OUTPUT From 1adeb0f6d35a2cc5f5ee9bc1d7ab19e7267ab73a Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 20:41:19 -0700 Subject: [PATCH 18/28] switch to python full image --- Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 811fa35..2263ebf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,7 @@ -FROM python:3.13-slim +FROM python:3.13 RUN apt-get update && apt-get install -y \ - python3-tk \ - gcc \ - python3-dev \ + libegl1 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app From 1d0099e8fa1549f05cc44c16d80f5437bd94eacb Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 21:01:08 -0700 Subject: [PATCH 19/28] make check in image always pull updates --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 6e4fd8d..2f696c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ services: check-in: image: ghcr.io/ucsd-makerspace/check-in:latest + pull_policy: always privileged: true network_mode: host environment: From 521a61f48f649d6274a1932042b5158cdc3d012d Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 21:28:07 -0700 Subject: [PATCH 20/28] re add dependency --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 2263ebf..bb947a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM python:3.13 RUN apt-get update && apt-get install -y \ libegl1 \ + libxkbcommon0 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app From 616a1e61522fe8e6dc5e345ec8a0caf447a060f2 Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 21:32:00 -0700 Subject: [PATCH 21/28] add dependencies 2 --- Dockerfile | 1 + requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bb947a0..6ef28d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM python:3.13 RUN apt-get update && apt-get install -y \ libegl1 \ libxkbcommon0 \ + libgl1 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/requirements.txt b/requirements.txt index 06b768d..e6c0f76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ requests==2.32.5 adafruit-circuitpython-pn532 RPi.GPIO; sys_platform == 'linux' pyqt6==6.11.0 -qtawesome \ No newline at end of file +qtawesome +pyqt6-sip \ No newline at end of file From 426d8bce06aa1cbb00161ab299bfde579389b043 Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 21:40:07 -0700 Subject: [PATCH 22/28] add dependencies 3 --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 6ef28d5..13fcabc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ RUN apt-get update && apt-get install -y \ libegl1 \ libxkbcommon0 \ libgl1 \ + libdbus-1-3 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app From 4e1f6a9b56e26a79490d27a7b9a41445e21c652f Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 21:52:05 -0700 Subject: [PATCH 23/28] soft depend barcode reader instead of hard depend --- src/app_context.py | 3 ++- src/controllers/check_in_controller.py | 7 +++++++ src/main.py | 1 + src/screens/components/dev_overlay.py | 13 +++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/app_context.py b/src/app_context.py index 6c2bb6c..3ba5551 100644 --- a/src/app_context.py +++ b/src/app_context.py @@ -13,7 +13,8 @@ def __init__(self, sheets: ApiClient, traffic_light: TrafficLightApi): self.nav = None self.check_in = None self.account = None - self.dispatcher = None # set by main.py after QApplication is created + self.dispatcher = None + self.has_barcode_scanner = False self._rfid_lock = threading.Lock() self._rfid: str = "" diff --git a/src/controllers/check_in_controller.py b/src/controllers/check_in_controller.py index 010e4f7..9df6fd3 100644 --- a/src/controllers/check_in_controller.py +++ b/src/controllers/check_in_controller.py @@ -3,6 +3,7 @@ from PyQt6.QtCore import QTimer from screens.user_welcome import UserWelcome +from screens.transition_screen import TransitionScreen class CheckInController: @@ -33,6 +34,12 @@ def _run_check_in(self, identifier, check_fn, welcome_message="Welcome back"): if status == "no_account": logging.info(f"No account found for {identifier}") self.ctx.traffic_light.request_red() + if not self.ctx.has_barcode_scanner: + self.ctx.nav.get_frame(TransitionScreen).display( + "Looks like you don't have an account.\nUse the other kiosk to set one up!" + ) + QTimer.singleShot(6000, self.ctx.nav.back_to_main) + return self.ctx.nav.go_to_create_account( on_done=lambda: self._run_check_in( identifier, check_fn, welcome_message="Thank you for registering" diff --git a/src/main.py b/src/main.py index 735d6b9..fb8a6b5 100644 --- a/src/main.py +++ b/src/main.py @@ -75,6 +75,7 @@ def clear_and_return(ctx: AppContext): card_reader.start(reader) if usb.barcode: + ctx.has_barcode_scanner = True barcode_scanner = BarcodeScanner(usb.barcode) barcode_controller = BarcodeScannerController(ctx) barcode_controller.start(barcode_scanner) diff --git a/src/screens/components/dev_overlay.py b/src/screens/components/dev_overlay.py index c31b44a..2fcbcf6 100644 --- a/src/screens/components/dev_overlay.py +++ b/src/screens/components/dev_overlay.py @@ -10,6 +10,7 @@ from screens.check_in_manual import CheckInManual from screens.qr_codes import QRCodes from screens.user_welcome import UserWelcome +from screens.transition_screen import TransitionScreen _DEV_NAME = "Dev User" _DEV_EMAIL = "devuser@ucsd.edu" @@ -20,6 +21,12 @@ def _sim_no_account_success(nav): nav.ctx.rfid = _DEV_RFID + if not nav.ctx.has_barcode_scanner: + nav.get_frame(TransitionScreen).display( + "Looks like you don't have an account.\nUse the other kiosk to set one up!" + ) + QTimer.singleShot(6000, nav.back_to_main) + return def on_done(): nav.ctx.traffic_light.request_green() nav.get_frame(UserWelcome).display_name(_DEV_NAME, _THANK_MSG) @@ -28,6 +35,12 @@ def on_done(): def _sim_no_account_needs_waiver(nav): nav.ctx.rfid = _DEV_RFID + if not nav.ctx.has_barcode_scanner: + nav.get_frame(TransitionScreen).display( + "Looks like you don't have an account.\nUse the other kiosk to set one up!" + ) + QTimer.singleShot(6000, nav.back_to_main) + return nav.go_to_create_account(on_done=nav.go_to_sign_waiver) From 62e2abb4619004f3572276c19994b9fe04ee8cf3 Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 22:03:07 -0700 Subject: [PATCH 24/28] add dependencies 4 --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 13fcabc..ddea6bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ RUN apt-get update && apt-get install -y \ libxkbcommon0 \ libgl1 \ libdbus-1-3 \ + libxcb-cursor0 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app From 817ad496c2f231142eb7ec995cd4d5c858302e46 Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 22:06:41 -0700 Subject: [PATCH 25/28] add dependencies 5 --- Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dockerfile b/Dockerfile index ddea6bb..f5d2615 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,18 @@ FROM python:3.13 RUN apt-get update && apt-get install -y \ libegl1 \ libxkbcommon0 \ + libxkbcommon-x11-0 \ libgl1 \ libdbus-1-3 \ libxcb-cursor0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-shape0 \ + libxcb-xinerama0 \ + libxcb-xkb1 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app From 7a4bc3bc82c8de12bcbb71f296e206e42a92103c Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 22:07:27 -0700 Subject: [PATCH 26/28] add dependencies 6 --- Dockerfile | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index f5d2615..94ff99e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,7 @@ FROM python:3.13 RUN apt-get update && apt-get install -y \ - libegl1 \ - libxkbcommon0 \ - libxkbcommon-x11-0 \ - libgl1 \ - libdbus-1-3 \ - libxcb-cursor0 \ - libxcb-icccm4 \ - libxcb-image0 \ - libxcb-keysyms1 \ - libxcb-randr0 \ - libxcb-render-util0 \ - libxcb-shape0 \ - libxcb-xinerama0 \ - libxcb-xkb1 \ + libqt6widgets6 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app From c36463e907d62a59c9ad3153df030074072aef5a Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 30 Mar 2026 22:17:54 -0700 Subject: [PATCH 27/28] shrink name welcome text slightly --- src/screens/user_welcome.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/screens/user_welcome.py b/src/screens/user_welcome.py index 701bcd8..bdd977b 100644 --- a/src/screens/user_welcome.py +++ b/src/screens/user_welcome.py @@ -25,7 +25,7 @@ def _build(self, controller): self._msg_label = QLabel("Welcome back") self._msg_label.setStyleSheet( - "color: #F5F0E6; font: 42pt Montserrat;" + "color: #F5F0E6; font: 38pt Montserrat;" "background: transparent; border: none;" ) self._msg_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) @@ -59,7 +59,7 @@ def display_name(self, name, message="Welcome back"): label = QLabel(name) label.setStyleSheet( - "color: #F5F0E6; font: bold 88pt Montserrat;" + "color: #F5F0E6; font: bold 70pt Montserrat;" "background: transparent; border: none;" ) label.setAlignment(Qt.AlignmentFlag.AlignHCenter) From 194ea57e2ab8e3d1554eef6be284351fbc391c73 Mon Sep 17 00:00:00 2001 From: Timothy Date: Tue, 31 Mar 2026 22:11:16 -0700 Subject: [PATCH 28/28] add backoff and better error handling --- src/controllers/rfid_reader_controller.py | 2 +- src/hardware/rfid_reader.py | 29 +++++++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/controllers/rfid_reader_controller.py b/src/controllers/rfid_reader_controller.py index 5106ac3..5557645 100644 --- a/src/controllers/rfid_reader_controller.py +++ b/src/controllers/rfid_reader_controller.py @@ -27,7 +27,7 @@ def _run(self, reader): scanner_error = False while True: if scanner_error: - time.sleep(0.1) + time.sleep(1.0) if reader.reconnect(): logging.info("Card reader reconnected") scanner_error = False diff --git a/src/hardware/rfid_reader.py b/src/hardware/rfid_reader.py index 06bfa6b..d29d404 100644 --- a/src/hardware/rfid_reader.py +++ b/src/hardware/rfid_reader.py @@ -11,24 +11,32 @@ class Reader(Thread): - def __init__(self, usb_id="/dev/ttyUSB0"): + def __init__(self, usb_id): super().__init__() self._usb_id = usb_id self._pn532 = None self._pending_tag = None - if usb_id is None: - logging.error("No card reader USB ID configured, exiting") - sys.exit(1) if not exists(usb_id): logging.error("Card reader not found at %s, exiting", usb_id) sys.exit(1) - self._init_pn532() - logging.info("Card reader init finished") + for attempt in range(1, 6): + try: + self._init_pn532() + logging.info("Card reader init finished") + break + except Exception as e: + logging.warning("Card reader init attempt %d/5 failed: %s", attempt, e) + if attempt == 5: + logging.error("Card reader failed to initialize after 5 attempts, exiting") + sys.exit(1) + time.sleep(attempt * 0.5) def _init_pn532(self): uart = serial.Serial(self._usb_id, baudrate=115200, timeout=0.1) + uart.reset_input_buffer() + uart.reset_output_buffer() + time.sleep(0.1) self._pn532 = PN532_UART(uart, debug=False) - self._pn532.SAM_configuration() def reconnect(self): if not exists(self._usb_id): @@ -36,7 +44,8 @@ def reconnect(self): try: self._init_pn532() return True - except Exception: + except Exception as e: + logging.warning("Card reader reconnect attempt failed: %s", e) self._pn532 = None return False @@ -47,8 +56,8 @@ def get_ser_in_waiting(self): raise OSError(f"PN532 error: {e}") if uid: self._pending_tag = "".join(f"{b:02X}" for b in uid) - time.sleep(0.01) # let any remaining in-flight bytes arrive - self._pn532._uart.reset_input_buffer() # and flush them + time.sleep(0.01) + self._pn532._uart.reset_input_buffer() return expected_characters self._pending_tag = None return 0