#!/usr/bin/env python3 """ Desktop GUI wrapper for chatter.py using Tkinter. - Runs the Flask chatter backend locally (threaded) - Simple chat interface: login/register + message view/send - Polls messages every 1.5s - Popup + camera notifications (using plyer + OpenCV) """ import threading import tkinter as tk from tkinter import messagebox, filedialog, scrolledtext import requests, os, queue, time, cv2 from plyer import notification SERVER = "https://chatter.wholeworldcoding.com" SESSION = requests.Session() MSG_QUEUE = queue.Queue() LAST_MSG_IDS = set() class ChatApp: def __init__(self, root): self.root = root root.title("chatter") root.geometry("800x600") root.minsize(500, 400) # --- Default settings --- self.settings = { "popup_notification": True, # Show system tray notifications "camera_notification": False, # Blink webcam LED } # Make window scalable root.rowconfigure(0, weight=1) root.columnconfigure(0, weight=1) self.username = None self.build_login_screen() # ---------- Screens ---------- def build_login_screen(self): for w in self.root.winfo_children(): w.destroy() frame = tk.Frame(self.root) frame.grid(row=0, column=0, sticky="nsew") frame.rowconfigure(0, weight=1) frame.columnconfigure(0, weight=1) inner = tk.Frame(frame) inner.place(relx=0.5, rely=0.5, anchor="center") tk.Label(inner, text="chatter", font=("Courier", 22, "bold")).pack(pady=10) tk.Label(inner, text="Sign up or login to continue.", font=("Courier", 11, "bold")).pack(pady=5) tk.Label(inner, text="Username").pack() self.entry_user = tk.Entry(inner, width=30) self.entry_user.pack(pady=3) tk.Label(inner, text="Password").pack() self.entry_pass = tk.Entry(inner, show="*", width=30) self.entry_pass.pack(pady=3) tk.Button(inner, text="Login", command=self.login, width=15).pack(pady=6) tk.Button(inner, text="Register", command=self.register, width=15).pack(pady=2) def build_chat_screen(self): for w in self.root.winfo_children(): w.destroy() self.root.rowconfigure(1, weight=1) self.root.columnconfigure(0, weight=1) header = tk.Frame(self.root) header.pack(fill=tk.X, pady=3) tk.Label(header, text=f"chatter - {self.username}", font=("Courier", 12)).pack(side=tk.LEFT, padx=10) tk.Button(header, text="Settings ⚙", command=self.open_settings).pack(side=tk.RIGHT, padx=10) self.chat_box = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, state='disabled') self.chat_box.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) frame = tk.Frame(self.root) frame.pack(fill=tk.X, pady=5) frame.columnconfigure(0, weight=1) self.msg_entry = tk.Entry(frame) self.msg_entry.grid(row=0, column=0, sticky="ew", padx=5) tk.Button(frame, text="Send", command=self.send_message).grid(row=0, column=1, padx=5) tk.Button(frame, text="File", command=self.send_file).grid(row=0, column=2, padx=5) tk.Button(self.root, text="Logout", command=self.logout).pack(pady=3) self.poll_messages() # ---------- Settings ---------- def open_settings(self): win = tk.Toplevel(self.root) win.title("Settings") win.geometry("300x200") win.resizable(False, False) popup_var = tk.BooleanVar(value=self.settings["popup_notification"]) cam_var = tk.BooleanVar(value=self.settings["camera_notification"]) tk.Label(win, text="Notification Options", font=("Courier", 12, "bold")).pack(pady=10) tk.Checkbutton(win, text="Popup Notification", variable=popup_var).pack(anchor="w", padx=20, pady=5) tk.Checkbutton(win, text="Camera Notification", variable=cam_var).pack(anchor="w", padx=20, pady=5) def save_settings(): self.settings["popup_notification"] = popup_var.get() self.settings["camera_notification"] = cam_var.get() messagebox.showinfo("Saved", "Settings updated.") win.destroy() tk.Button(win, text="Save", command=save_settings).pack(pady=10) # ---------- Auth ---------- def login(self): data = {"username": self.entry_user.get(), "password": self.entry_pass.get()} r = SESSION.post(SERVER + "/login", data=data, allow_redirects=False) if r.status_code == 302: self.username = data["username"] self.build_chat_screen() else: messagebox.showerror("Error", "Login failed") def register(self): data = {"username": self.entry_user.get(), "password": self.entry_pass.get()} r = SESSION.post(SERVER + "/register", data=data, allow_redirects=False) if r.status_code == 302: messagebox.showinfo("Success", "Account created! You can log in now.") else: messagebox.showerror("Error", "Registration failed") def logout(self): SESSION.get(SERVER + "/logout") self.username = None self.build_login_screen() # ---------- Messaging ---------- def send_message(self): text = self.msg_entry.get().strip() if not text: return SESSION.post(SERVER + "/api/messages", json={"text": text}) self.msg_entry.delete(0, tk.END) self.poll_messages(force=True) def send_file(self): path = filedialog.askopenfilename() if not path: return with open(path, "rb") as f: files = {"file": (os.path.basename(path), f)} SESSION.post(SERVER + "/api/messages", files=files) self.poll_messages(force=True) def poll_messages(self, force=False): def _poll(): try: r = SESSION.get(SERVER + "/api/messages", timeout=5) if r.status_code == 200: msgs = r.json() new_ids = {m["id"] for m in msgs} new = [m for m in msgs if m["id"] not in LAST_MSG_IDS] if new or force: self.chat_box.config(state='normal') self.chat_box.delete("1.0", tk.END) for m in msgs: line = f"[{m['username']}] {m['text']}\n" self.chat_box.insert(tk.END, line) self.chat_box.config(state='disabled') self.chat_box.yview(tk.END) for m in new: if m["username"] != self.username: self.notify_user(m["username"], m["text"]) LAST_MSG_IDS.clear() LAST_MSG_IDS.update(new_ids) except Exception: pass self.root.after(1500, self.poll_messages) threading.Thread(target=_poll, daemon=True).start() # ---------- Notifications ---------- def notify_user(self, sender, message): """Triggers all enabled notifications.""" if self.settings.get("popup_notification"): threading.Thread( target=lambda: self.show_system_notification(sender, message), daemon=True ).start() if self.settings.get("camera_notification"): threading.Thread( target=self.blink_camera_light, daemon=True ).start() def show_system_notification(self, title, message): """Show a real system notification using plyer.""" try: notification.notify( title=f"New message from {title}", app_name="chatter", message=message[:200], timeout=5 ) except Exception as e: print(f"[Notification failed] {e}") def blink_camera_light(self, blink_time=3): """Turn camera on briefly to activate LED.""" try: cap = cv2.VideoCapture(0) if cap.isOpened(): ret, frame = cap.read() # read one frame so LED actually activates if ret: time.sleep(blink_time) cap.release() else: print("[⚠] Could not access camera.") except Exception as e: print(f"[Camera blink failed] {e}") # --- Run GUI --- if __name__ == "__main__": root = tk.Tk() app = ChatApp(root) root.mainloop()