From b49955a974beb81a1d792d9b3f899d7e3e57f311 Mon Sep 17 00:00:00 2001 From: octolinkyt Date: Fri, 10 Oct 2025 23:15:43 +0000 Subject: [PATCH] after hours of work, attachments work! --- desktop.py | 506 +++++++++++++++++++++++++++++------------------------ 1 file changed, 278 insertions(+), 228 deletions(-) diff --git a/desktop.py b/desktop.py index 65b1f2e..cd8b2b5 100644 --- a/desktop.py +++ b/desktop.py @@ -1,228 +1,278 @@ -#!/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() +#!/usr/bin/env python3 +""" +Desktop GUI wrapper for chatter.py using Tkinter. +Supports text messages and attachments (files) with clickable filenames. +Polls server every 1.5s for updates. +""" + +import threading +import tkinter as tk +from tkinter import filedialog, scrolledtext, messagebox +import requests, os, time, webbrowser +from datetime import datetime +from plyer import notification +import cv2 +from PIL import Image, ImageTk +from io import BytesIO + +SERVER = "https://chatter.wholeworldcoding.com" +SESSION = requests.Session() +LAST_MSG_IDS = set() + + +class ChatApp: + def __init__(self, root): + self.root = root + root.title("chatter") + root.geometry("800x600") + root.minsize(500, 400) + + self.settings = { + "popup_notification": True, + "camera_notification": False, + } + + 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.pack(expand=True) + + tk.Label(frame, text="chatter", font=("Courier", 22, "bold")).pack(pady=10) + tk.Label(frame, text="Sign up or login to continue.", font=("Courier", 11)).pack(pady=5) + + tk.Label(frame, text="Username").pack() + self.entry_user = tk.Entry(frame, width=30) + self.entry_user.pack(pady=3) + + tk.Label(frame, text="Password").pack() + self.entry_pass = tk.Entry(frame, show="*", width=30) + self.entry_pass.pack(pady=3) + + tk.Button(frame, text="Login", command=self.login, width=15).pack(pady=6) + tk.Button(frame, 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=5) + tk.Button(header, text="Logout", command=self.logout).pack(side=tk.RIGHT, padx=5) + + 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) + + 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) + if r.url.endswith("/chat"): + 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) + if r.url.endswith("/login"): + 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 + try: + r = SESSION.post(SERVER + "/api/messages", json={"text": text}) + if r.status_code == 201: + self.msg_entry.delete(0, tk.END) + self.poll_messages(force=True) + except Exception as e: + print(f"Send error: {e}") + + def send_file(self): + path = filedialog.askopenfilename() + if not path: + return + try: + 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) + except Exception as e: + print(f"File send error: {e}") + + def show_attachment_popup(self, file_name, username): + popup = tk.Toplevel(self.root) + popup.title(f"{username} sent a file") + popup.geometry("400x400") + + file_url = f"{SERVER}/uploads/{file_name}" + preview_url = file_url.replace("/uploads/", "/preview/") + + tk.Label(popup, wrap="400", text=f"{username} sent: {file_name}", font=("Courier", 12, "bold")).pack(pady=10) + + # Attempt to preview image inline + try: + resp = requests.get(file_url) + pil_img = Image.open(BytesIO(resp.content)) + pil_img.thumbnail((350, 350)) + tk_img = ImageTk.PhotoImage(pil_img) + label_img = tk.Label(popup, image=tk_img) + label_img.image = tk_img + label_img.pack(pady=5) + except Exception: + tk.Label(popup, wrap="400", text="You can preview the file, or download it.").pack(pady=5) + + btn_frame = tk.Frame(popup) + btn_frame.pack(pady=10) + + def download_file(): + save_path = filedialog.asksaveasfilename(initialfile=file_name) + if save_path: + with open(save_path, "wb") as f: + f.write(requests.get(file_url).content) + messagebox.showinfo("Downloaded", f"File saved to {save_path}") + + tk.Button(btn_frame, text="Download", command=download_file).pack(side=tk.LEFT, padx=5) + tk.Button(btn_frame, text="Preview", command=lambda: webbrowser.open(preview_url)).pack(side=tk.LEFT, padx=5) + + def poll_messages(self, force=False): + try: + r = SESSION.get(SERVER + "/api/messages") + if r.status_code != 200: + return + msgs = r.json() + new_ids = {m["id"] for m in msgs} + new_msgs = [m for m in msgs if m["id"] not in LAST_MSG_IDS] + + if new_msgs or force: + self.chat_box.config(state='normal') + self.chat_box.delete("1.0", tk.END) + for m in msgs: + ts = m.get("created_at") or time.time() + try: + ts_str = datetime.fromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M:%S") + except: + ts_str = str(ts) + + if m.get("attachment"): + attach_name = m["attachment"] + tag_name = f"attach{m['id']}" + + # Grey italic text for "sent a file" + self.chat_box.insert(tk.END, f"[{ts_str}] {m['username']} sent a file: ", f"grey{m['id']}") + self.chat_box.tag_config(f"grey{m['id']}", foreground="grey", font=("Courier", 10, "italic")) + + # Blue clickable filename + self.chat_box.insert(tk.END, f"{attach_name}\n", tag_name) + self.chat_box.tag_config(tag_name, foreground="blue", underline=True) + self.chat_box.tag_bind(tag_name, "", + lambda e, fn=attach_name, un=m["username"]: self.show_attachment_popup(fn, un)) + else: + text = m.get("text", "") + self.chat_box.insert(tk.END, f"[{ts_str}] {m['username']}: {text}\n") + + self.chat_box.config(state='disabled') + self.chat_box.yview(tk.END) + + # Notify user about new messages + for m in new_msgs: + if m["username"] != self.username: + msg_preview = m.get("text") or m.get("attachment", "Attachment") + self.notify_user(m["username"], msg_preview) + + LAST_MSG_IDS.clear() + LAST_MSG_IDS.update(new_ids) + except Exception as e: + print(f"Poll error: {e}") + + self.root.after(1500, self.poll_messages) + + # ---------- Notifications ---------- + def notify_user(self, sender, message): + 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): + try: + notification.notify( + title=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): + try: + cap = cv2.VideoCapture(0) + if cap.isOpened(): + ret, frame = cap.read() + if ret: + time.sleep(blink_time) + cap.release() + except Exception as e: + print(f"Camera blink failed: {e}") + + +# --- Run GUI --- +if __name__ == "__main__": + root = tk.Tk() + app = ChatApp(root) + root.mainloop()