250 lines
10 KiB
Python
250 lines
10 KiB
Python
import customtkinter as ctk
|
|
|
|
# GUI Configuration
|
|
ctk.set_appearance_mode("Dark")
|
|
ctk.set_default_color_theme("blue")
|
|
|
|
class AppGUI(ctk.CTk):
|
|
def __init__(self, controller):
|
|
super().__init__()
|
|
self.controller = controller
|
|
|
|
self.title("Secure SMS - Raspberry Pi")
|
|
self.geometry("800x480") # Typical Pi touchscreen resolution
|
|
|
|
# Current state
|
|
self.current_contact = None
|
|
|
|
self.setup_ui()
|
|
self.load_contacts()
|
|
|
|
def setup_ui(self):
|
|
# Grid Layout (1x2) - Sidebar | Main Content
|
|
self.grid_rowconfigure(0, weight=1)
|
|
self.grid_columnconfigure(1, weight=1)
|
|
|
|
# --- Sidebar ---
|
|
self.sidebar_frame = ctk.CTkFrame(self, width=200, corner_radius=0)
|
|
self.sidebar_frame.grid(row=0, column=0, sticky="nsew")
|
|
self.sidebar_frame.grid_rowconfigure(4, weight=1)
|
|
|
|
self.logo_label = ctk.CTkLabel(self.sidebar_frame, text="Secure SMS", font=ctk.CTkFont(size=20, weight="bold"))
|
|
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
|
|
|
|
self.contacts_scrollable_frame = ctk.CTkScrollableFrame(self.sidebar_frame, label_text="Contacts")
|
|
self.contacts_scrollable_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew", rowspan=4)
|
|
|
|
# Settings / Add Contact Button
|
|
self.settings_btn = ctk.CTkButton(self.sidebar_frame, text="Settings / Add Contact", command=self.open_settings)
|
|
self.settings_btn.grid(row=5, column=0, padx=10, pady=20)
|
|
|
|
# --- Main Chat Area ---
|
|
self.main_frame = ctk.CTkFrame(self, corner_radius=0)
|
|
self.main_frame.grid(row=0, column=1, sticky="nsew")
|
|
self.main_frame.grid_rowconfigure(1, weight=1)
|
|
self.main_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Chat Header
|
|
self.chat_header_var = ctk.StringVar(value="Select a contact")
|
|
self.chat_header = ctk.CTkLabel(self.main_frame, textvariable=self.chat_header_var, font=ctk.CTkFont(size=18, weight="bold"))
|
|
self.chat_header.grid(row=0, column=0, padx=20, pady=10, sticky="w")
|
|
|
|
# Secured Status Label Indicator
|
|
self.header_secure_status = ctk.CTkLabel(self.main_frame, text="Current Mode: Normal", text_color="yellow")
|
|
self.header_secure_status.grid(row=0, column=0, padx=20, pady=10, sticky="e")
|
|
|
|
# Messages Display
|
|
self.msg_display = ctk.CTkTextbox(self.main_frame, state="disabled", wrap="word")
|
|
self.msg_display.grid(row=1, column=0, padx=20, pady=(0, 10), sticky="nsew")
|
|
self.msg_display.tag_config('sent_normal', foreground='lightgray', justify='right')
|
|
self.msg_display.tag_config('sent_secure', foreground='lightgreen', justify='right')
|
|
self.msg_display.tag_config('recv_normal', foreground='white', justify='left')
|
|
self.msg_display.tag_config('recv_secure', foreground='mediumspringgreen', justify='left')
|
|
self.msg_display.tag_config('system', foreground='red', justify='center')
|
|
|
|
# Bottom Input Area
|
|
self.input_frame = ctk.CTkFrame(self.main_frame, height=50)
|
|
self.input_frame.grid(row=2, column=0, padx=20, pady=10, sticky="nsew")
|
|
self.input_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
self.msg_entry = ctk.CTkEntry(self.input_frame, placeholder_text="Type a message...")
|
|
self.msg_entry.grid(row=0, column=0, padx=(10, 5), pady=10, sticky="nsew")
|
|
self.msg_entry.bind("<Return>", lambda e: self.send_message())
|
|
|
|
# Toggle Secure SMS
|
|
self.secure_mode_var = ctk.StringVar(value="off")
|
|
self.secure_switch = ctk.CTkSwitch(
|
|
self.input_frame,
|
|
text="Secure",
|
|
command=self.toggle_mode,
|
|
variable=self.secure_mode_var,
|
|
onvalue="on",
|
|
offvalue="off"
|
|
)
|
|
self.secure_switch.grid(row=0, column=1, padx=5, pady=10)
|
|
|
|
self.send_btn = ctk.CTkButton(self.input_frame, text="Send", width=80, command=self.send_message)
|
|
self.send_btn.grid(row=0, column=2, padx=(5, 10), pady=10)
|
|
|
|
def toggle_mode(self):
|
|
mode = "Secure" if self.secure_mode_var.get() == "on" else "Normal"
|
|
color = "mediumspringgreen" if mode == "Secure" else "yellow"
|
|
self.header_secure_status.configure(text=f"Current Mode: {mode}", text_color=color)
|
|
|
|
def load_contacts(self):
|
|
# Clear sidebar list
|
|
for widget in self.contacts_scrollable_frame.winfo_children():
|
|
widget.destroy()
|
|
|
|
contacts = self.controller.get_contacts()
|
|
for idx, contact in enumerate(contacts):
|
|
# contact = (name, phone, pubkey)
|
|
name, phone, _ = contact
|
|
btn = ctk.CTkButton(
|
|
self.contacts_scrollable_frame,
|
|
text=f"{name}\n{phone}",
|
|
command=lambda p=phone: self.select_contact(p),
|
|
fg_color="transparent",
|
|
border_width=2,
|
|
text_color=("gray10", "#DCE4EE")
|
|
)
|
|
btn.grid(row=idx, column=0, padx=5, pady=5, sticky="ew")
|
|
|
|
def select_contact(self, phone):
|
|
self.current_contact = self.controller.get_contact_info(phone)
|
|
name = self.current_contact[0]
|
|
self.chat_header_var.set(f"Chat with {name} ({phone})")
|
|
|
|
# Determine if we can do secure
|
|
has_key = self.current_contact[2] is not None
|
|
if not has_key:
|
|
self.secure_switch.deselect()
|
|
self.secure_switch.configure(state="disabled")
|
|
self.toggle_mode()
|
|
self.append_system_msg("This contact has no public key. Secure mode disabled.")
|
|
else:
|
|
self.secure_switch.configure(state="normal")
|
|
|
|
self.refresh_messages()
|
|
|
|
def refresh_messages(self):
|
|
self.msg_display.configure(state="normal")
|
|
self.msg_display.delete("1.0", "end")
|
|
|
|
if self.current_contact:
|
|
messages = self.controller.get_messages(self.current_contact[1])
|
|
for msg in messages:
|
|
# msg = (id, phone, text, date, is_secure, status)
|
|
text = msg[2]
|
|
date = msg[3]
|
|
is_secure = msg[4]
|
|
status = msg[5] # sent / recv
|
|
|
|
tag = f"{status}_{'secure' if is_secure else 'normal'}"
|
|
prefix = "🔒 " if is_secure else ""
|
|
|
|
display_text = f"[{date}]\n{prefix}{text}\n\n"
|
|
self.msg_display.insert("end", display_text, tag)
|
|
|
|
self.msg_display.configure(state="disabled")
|
|
self.msg_display.yview("end")
|
|
|
|
def append_system_msg(self, text):
|
|
self.msg_display.configure(state="normal")
|
|
self.msg_display.insert("end", f"--- {text} ---\n\n", "system")
|
|
self.msg_display.configure(state="disabled")
|
|
self.msg_display.yview("end")
|
|
|
|
def send_message(self):
|
|
if not self.current_contact:
|
|
return
|
|
|
|
text = self.msg_entry.get().strip()
|
|
if not text:
|
|
return
|
|
|
|
is_secure = self.secure_mode_var.get() == "on"
|
|
phone = self.current_contact[1]
|
|
|
|
# Ask controller to send
|
|
self.msg_entry.delete(0, 'end')
|
|
self.append_system_msg("Sending...")
|
|
self.update() # force UI refresh safely
|
|
|
|
success = self.controller.send_message(phone, text, is_secure)
|
|
if success:
|
|
self.refresh_messages()
|
|
else:
|
|
self.append_system_msg("Failed to send SMS!")
|
|
|
|
def open_settings(self):
|
|
SettingsWindow(self, self.controller)
|
|
|
|
class SettingsWindow(ctk.CTkToplevel):
|
|
def __init__(self, master, controller):
|
|
super().__init__(master)
|
|
self.controller = controller
|
|
self.title("Settings")
|
|
self.geometry("500x500")
|
|
|
|
# Bring to front
|
|
self.attributes('-topmost', 1)
|
|
|
|
self.grid_columnconfigure(0, weight=1)
|
|
|
|
# My Public Key
|
|
lbl1 = ctk.CTkLabel(self, text="My Public Key (Share this to chat securely):")
|
|
lbl1.grid(row=0, column=0, padx=20, pady=(20, 5), sticky="w")
|
|
|
|
self.my_key_box = ctk.CTkTextbox(self, height=100)
|
|
self.my_key_box.grid(row=1, column=0, padx=20, pady=5, sticky="ew")
|
|
self.my_key_box.insert("1.0", self.controller.get_my_public_key())
|
|
self.my_key_box.configure(state="disabled")
|
|
|
|
# Add / Update Contact
|
|
lbl_add = ctk.CTkLabel(self, text="Add/Update Contact:", font=ctk.CTkFont(weight="bold"))
|
|
lbl_add.grid(row=2, column=0, padx=20, pady=(30, 5), sticky="w")
|
|
|
|
self.name_entry = ctk.CTkEntry(self, placeholder_text="Contact Name")
|
|
self.name_entry.grid(row=3, column=0, padx=20, pady=5, sticky="ew")
|
|
|
|
self.phone_entry = ctk.CTkEntry(self, placeholder_text="Phone Number (+989...)")
|
|
self.phone_entry.grid(row=4, column=0, padx=20, pady=5, sticky="ew")
|
|
|
|
lbl2 = ctk.CTkLabel(self, text="Contact's Public Key (Optional but required for secure SMS):")
|
|
lbl2.grid(row=5, column=0, padx=20, pady=5, sticky="w")
|
|
|
|
self.contact_key_box = ctk.CTkTextbox(self, height=100)
|
|
self.contact_key_box.grid(row=6, column=0, padx=20, pady=5, sticky="ew")
|
|
|
|
self.save_btn = ctk.CTkButton(self, text="Save Contact", command=self.save_contact)
|
|
self.save_btn.grid(row=7, column=0, padx=20, pady=20)
|
|
|
|
def save_contact(self):
|
|
name = self.name_entry.get().strip()
|
|
phone = self.phone_entry.get().strip()
|
|
pubkey = self.contact_key_box.get("1.0", 'end-1c').strip()
|
|
|
|
if not name or not phone:
|
|
return
|
|
|
|
if not pubkey:
|
|
pubkey = None
|
|
|
|
self.controller.save_contact(name, phone, pubkey)
|
|
self.master.load_contacts()
|
|
self.destroy()
|
|
|
|
if __name__ == "__main__":
|
|
# Mock Controller for standalone GUI test
|
|
class MockController:
|
|
def get_contacts(self): return [("Alice", "+1234", "key"), ("Bob", "+5678", None)]
|
|
def get_contact_info(self, phone): return ("Alice", phone, "key") if phone == "+1234" else ("Bob", phone, None)
|
|
def get_messages(self, phone): return [(1, phone, "Hi", "2023-01-01", 0, "recv"), (2, phone, "Secret", "2023-01-01", 1, "sent")]
|
|
def get_my_public_key(self): return "MY_PEM_MOCK_DATA"
|
|
def send_message(self, *args): return True
|
|
def save_contact(self, *args): pass
|
|
|
|
app = AppGUI(MockController())
|
|
app.mainloop()
|