diff --git a/desktop.py b/desktop.py new file mode 100644 index 0000000..65b1f2e --- /dev/null +++ b/desktop.py @@ -0,0 +1,228 @@ +#!/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()