#!/usr/bin/env python3 """ Desktop GUI wrapper for chatter.py using Tkinter. Supports text messages and attachments with clickable filenames. Polls server every 1.5s for updates. Includes 24-hour toggle and timezone selection. """ import threading import tkinter as tk from tkinter import filedialog, scrolledtext, messagebox import requests, os, time, webbrowser from datetime import datetime from dateutil import parser, tz 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() # ---------------- ChatApp ---------------- class ChatApp: def __init__(self, root): self.root = root root.title("chatter") root.geometry("800x600") root.minsize(500, 400) # Settings self.settings = { "popup_notification": True, "camera_notification": False, "time_24h": False, "timezone": time.tzname[0] # Default local timezone } 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("350x250") win.resizable(False, False) popup_var = tk.BooleanVar(value=self.settings["popup_notification"]) cam_var = tk.BooleanVar(value=self.settings["camera_notification"]) time24_var = tk.BooleanVar(value=self.settings.get("time_24h", False)) tz_var = tk.StringVar(value=self.settings.get("timezone", time.tzname[0])) tk.Label(win, text="Notification Options", font=("Courier", 12, "bold")).pack(pady=5) tk.Checkbutton(win, text="Popup Notification", variable=popup_var).pack(anchor="w", padx=20) tk.Checkbutton(win, text="Camera Notification", variable=cam_var).pack(anchor="w", padx=20) tk.Label(win, text="Time Display", font=("Courier", 12, "bold")).pack(pady=5) tk.Checkbutton(win, text="24-Hour Clock", variable=time24_var).pack(anchor="w", padx=20) tk.Label(win, text="Timezone", font=("Courier", 12, "bold")).pack(pady=5) # Common timezones for dropdown tz_options = ["UTC", "US/Eastern", "US/Central", "US/Mountain", "US/Pacific", "Europe/London", "Europe/Berlin"] tk.OptionMenu(win, tz_var, *tz_options).pack(anchor="w", padx=20) def save_settings(): self.settings["popup_notification"] = popup_var.get() self.settings["camera_notification"] = cam_var.get() self.settings["time_24h"] = time24_var.get() self.settings["timezone"] = tz_var.get() self.update_chat_timestamps() 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) 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) # ---------- Timestamp helper ---------- def format_timestamp(self, ts): try: dt = parser.parse(ts) dt = dt.astimezone(tz.gettz(self.settings.get("timezone", time.tzname[0]))) if self.settings.get("time_24h"): return dt.strftime("%Y-%m-%d %H:%M:%S") else: return dt.strftime("%Y-%m-%d %I:%M:%S %p") except Exception: return str(ts) def update_chat_timestamps(self): """Re-render chat box to apply new timezone or 24h/12h setting.""" try: # Re-poll messages and re-render r = SESSION.get(SERVER + "/api/messages") if r.status_code != 200: return msgs = r.json() 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() ts_str = self.format_timestamp(ts) if m.get("attachment"): attach_name = m["attachment"] tag_name = f"attach{m['id']}" 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")) 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) except Exception as e: print(f"Update timestamp error: {e}") # ---------- Poll messages ---------- 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.update_chat_timestamps() # 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()