Files
chatter/desktop.py

279 lines
10 KiB
Python

#!/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, "<Button-1>",
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()