Files
chatter/desktop.py
2025-10-10 19:10:25 +00:00

229 lines
8.7 KiB
Python

#!/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()