after hours of work, attachments work!

This commit is contained in:
2025-10-10 23:15:43 +00:00
parent d4197679cb
commit b49955a974

View File

@@ -1,228 +1,278 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Desktop GUI wrapper for chatter.py using Tkinter. Desktop GUI wrapper for chatter.py using Tkinter.
- Runs the Flask chatter backend locally (threaded) Supports text messages and attachments (files) with clickable filenames.
- Simple chat interface: login/register + message view/send Polls server every 1.5s for updates.
- Polls messages every 1.5s """
- Popup + camera notifications (using plyer + OpenCV)
""" import threading
import tkinter as tk
import threading from tkinter import filedialog, scrolledtext, messagebox
import tkinter as tk import requests, os, time, webbrowser
from tkinter import messagebox, filedialog, scrolledtext from datetime import datetime
import requests, os, queue, time, cv2 from plyer import notification
from plyer import notification import cv2
from PIL import Image, ImageTk
SERVER = "https://chatter.wholeworldcoding.com" from io import BytesIO
SESSION = requests.Session()
MSG_QUEUE = queue.Queue() SERVER = "https://chatter.wholeworldcoding.com"
LAST_MSG_IDS = set() SESSION = requests.Session()
LAST_MSG_IDS = set()
class ChatApp:
def __init__(self, root): class ChatApp:
self.root = root def __init__(self, root):
root.title("chatter") self.root = root
root.geometry("800x600") root.title("chatter")
root.minsize(500, 400) root.geometry("800x600")
root.minsize(500, 400)
# --- Default settings ---
self.settings = { self.settings = {
"popup_notification": True, # Show system tray notifications "popup_notification": True,
"camera_notification": False, # Blink webcam LED "camera_notification": False,
} }
# Make window scalable self.username = None
root.rowconfigure(0, weight=1) self.build_login_screen()
root.columnconfigure(0, weight=1)
# ---------- Screens ----------
self.username = None def build_login_screen(self):
self.build_login_screen() for w in self.root.winfo_children():
w.destroy()
# ---------- Screens ----------
def build_login_screen(self): frame = tk.Frame(self.root)
for w in self.root.winfo_children(): frame.pack(expand=True)
w.destroy()
tk.Label(frame, text="chatter", font=("Courier", 22, "bold")).pack(pady=10)
frame = tk.Frame(self.root) tk.Label(frame, text="Sign up or login to continue.", font=("Courier", 11)).pack(pady=5)
frame.grid(row=0, column=0, sticky="nsew")
frame.rowconfigure(0, weight=1) tk.Label(frame, text="Username").pack()
frame.columnconfigure(0, weight=1) self.entry_user = tk.Entry(frame, width=30)
self.entry_user.pack(pady=3)
inner = tk.Frame(frame)
inner.place(relx=0.5, rely=0.5, anchor="center") tk.Label(frame, text="Password").pack()
self.entry_pass = tk.Entry(frame, show="*", width=30)
tk.Label(inner, text="chatter", font=("Courier", 22, "bold")).pack(pady=10) self.entry_pass.pack(pady=3)
tk.Label(inner, text="Sign up or login to continue.", font=("Courier", 11, "bold")).pack(pady=5)
tk.Button(frame, text="Login", command=self.login, width=15).pack(pady=6)
tk.Label(inner, text="Username").pack() tk.Button(frame, text="Register", command=self.register, width=15).pack(pady=2)
self.entry_user = tk.Entry(inner, width=30)
self.entry_user.pack(pady=3) def build_chat_screen(self):
tk.Label(inner, text="Password").pack() for w in self.root.winfo_children():
self.entry_pass = tk.Entry(inner, show="*", width=30) w.destroy()
self.entry_pass.pack(pady=3)
tk.Button(inner, text="Login", command=self.login, width=15).pack(pady=6) self.root.rowconfigure(1, weight=1)
tk.Button(inner, text="Register", command=self.register, width=15).pack(pady=2) self.root.columnconfigure(0, weight=1)
def build_chat_screen(self): header = tk.Frame(self.root)
for w in self.root.winfo_children(): header.pack(fill=tk.X, pady=3)
w.destroy() 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)
self.root.rowconfigure(1, weight=1) tk.Button(header, text="Logout", command=self.logout).pack(side=tk.RIGHT, padx=5)
self.root.columnconfigure(0, weight=1)
self.chat_box = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, state='disabled')
header = tk.Frame(self.root) self.chat_box.pack(padx=10, pady=5, fill=tk.BOTH, expand=True)
header.pack(fill=tk.X, pady=3)
tk.Label(header, text=f"chatter - {self.username}", font=("Courier", 12)).pack(side=tk.LEFT, padx=10) frame = tk.Frame(self.root)
tk.Button(header, text="Settings ⚙", command=self.open_settings).pack(side=tk.RIGHT, padx=10) frame.pack(fill=tk.X, pady=5)
frame.columnconfigure(0, weight=1)
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) self.msg_entry = tk.Entry(frame)
self.msg_entry.grid(row=0, column=0, sticky="ew", padx=5)
frame = tk.Frame(self.root) tk.Button(frame, text="Send", command=self.send_message).grid(row=0, column=1, padx=5)
frame.pack(fill=tk.X, pady=5) tk.Button(frame, text="📎 File", command=self.send_file).grid(row=0, column=2, padx=5)
frame.columnconfigure(0, weight=1)
self.poll_messages()
self.msg_entry = tk.Entry(frame)
self.msg_entry.grid(row=0, column=0, sticky="ew", padx=5) # ---------- Settings ----------
tk.Button(frame, text="Send", command=self.send_message).grid(row=0, column=1, padx=5) def open_settings(self):
tk.Button(frame, text="File", command=self.send_file).grid(row=0, column=2, padx=5) win = tk.Toplevel(self.root)
tk.Button(self.root, text="Logout", command=self.logout).pack(pady=3) win.title("Settings")
win.geometry("300x200")
self.poll_messages() win.resizable(False, False)
# ---------- Settings ---------- popup_var = tk.BooleanVar(value=self.settings["popup_notification"])
def open_settings(self): cam_var = tk.BooleanVar(value=self.settings["camera_notification"])
win = tk.Toplevel(self.root)
win.title("Settings") tk.Label(win, text="Notification Options", font=("Courier", 12, "bold")).pack(pady=10)
win.geometry("300x200") tk.Checkbutton(win, text="Popup Notification", variable=popup_var).pack(anchor="w", padx=20, pady=5)
win.resizable(False, False) tk.Checkbutton(win, text="Camera Notification", variable=cam_var).pack(anchor="w", padx=20, pady=5)
popup_var = tk.BooleanVar(value=self.settings["popup_notification"]) def save_settings():
cam_var = tk.BooleanVar(value=self.settings["camera_notification"]) self.settings["popup_notification"] = popup_var.get()
self.settings["camera_notification"] = cam_var.get()
tk.Label(win, text="Notification Options", font=("Courier", 12, "bold")).pack(pady=10) messagebox.showinfo("Saved", "Settings updated.")
tk.Checkbutton(win, text="Popup Notification", variable=popup_var).pack(anchor="w", padx=20, pady=5) win.destroy()
tk.Checkbutton(win, text="Camera Notification", variable=cam_var).pack(anchor="w", padx=20, pady=5)
tk.Button(win, text="Save", command=save_settings).pack(pady=10)
def save_settings():
self.settings["popup_notification"] = popup_var.get() # ---------- Auth ----------
self.settings["camera_notification"] = cam_var.get() def login(self):
messagebox.showinfo("Saved", "Settings updated.") data = {"username": self.entry_user.get(), "password": self.entry_pass.get()}
win.destroy() r = SESSION.post(SERVER + "/login", data=data)
if r.url.endswith("/chat"):
tk.Button(win, text="Save", command=save_settings).pack(pady=10) self.username = data["username"]
self.build_chat_screen()
# ---------- Auth ---------- else:
def login(self): messagebox.showerror("Error", "Login failed")
data = {"username": self.entry_user.get(), "password": self.entry_pass.get()}
r = SESSION.post(SERVER + "/login", data=data, allow_redirects=False) def register(self):
if r.status_code == 302: data = {"username": self.entry_user.get(), "password": self.entry_pass.get()}
self.username = data["username"] r = SESSION.post(SERVER + "/register", data=data)
self.build_chat_screen() if r.url.endswith("/login"):
else: messagebox.showinfo("Success", "Account created! You can log in now.")
messagebox.showerror("Error", "Login failed") else:
messagebox.showerror("Error", "Registration failed")
def register(self):
data = {"username": self.entry_user.get(), "password": self.entry_pass.get()} def logout(self):
r = SESSION.post(SERVER + "/register", data=data, allow_redirects=False) SESSION.get(SERVER + "/logout")
if r.status_code == 302: self.username = None
messagebox.showinfo("Success", "Account created! You can log in now.") self.build_login_screen()
else:
messagebox.showerror("Error", "Registration failed") # ---------- Messaging ----------
def send_message(self):
def logout(self): text = self.msg_entry.get().strip()
SESSION.get(SERVER + "/logout") if not text:
self.username = None return
self.build_login_screen() try:
r = SESSION.post(SERVER + "/api/messages", json={"text": text})
# ---------- Messaging ---------- if r.status_code == 201:
def send_message(self): self.msg_entry.delete(0, tk.END)
text = self.msg_entry.get().strip() self.poll_messages(force=True)
if not text: except Exception as e:
return print(f"Send error: {e}")
SESSION.post(SERVER + "/api/messages", json={"text": text})
self.msg_entry.delete(0, tk.END) def send_file(self):
self.poll_messages(force=True) path = filedialog.askopenfilename()
if not path:
def send_file(self): return
path = filedialog.askopenfilename() try:
if not path: with open(path, "rb") as f:
return files = {"file": (os.path.basename(path), f)}
with open(path, "rb") as f: SESSION.post(SERVER + "/api/messages", files=files)
files = {"file": (os.path.basename(path), f)} self.poll_messages(force=True)
SESSION.post(SERVER + "/api/messages", files=files) except Exception as e:
self.poll_messages(force=True) print(f"File send error: {e}")
def poll_messages(self, force=False): def show_attachment_popup(self, file_name, username):
def _poll(): popup = tk.Toplevel(self.root)
try: popup.title(f"{username} sent a file")
r = SESSION.get(SERVER + "/api/messages", timeout=5) popup.geometry("400x400")
if r.status_code == 200:
msgs = r.json() file_url = f"{SERVER}/uploads/{file_name}"
new_ids = {m["id"] for m in msgs} preview_url = file_url.replace("/uploads/", "/preview/")
new = [m for m in msgs if m["id"] not in LAST_MSG_IDS]
if new or force: tk.Label(popup, wrap="400", text=f"{username} sent: {file_name}", font=("Courier", 12, "bold")).pack(pady=10)
self.chat_box.config(state='normal')
self.chat_box.delete("1.0", tk.END) # Attempt to preview image inline
for m in msgs: try:
line = f"[{m['username']}] {m['text']}\n" resp = requests.get(file_url)
self.chat_box.insert(tk.END, line) pil_img = Image.open(BytesIO(resp.content))
self.chat_box.config(state='disabled') pil_img.thumbnail((350, 350))
self.chat_box.yview(tk.END) tk_img = ImageTk.PhotoImage(pil_img)
for m in new: label_img = tk.Label(popup, image=tk_img)
if m["username"] != self.username: label_img.image = tk_img
self.notify_user(m["username"], m["text"]) label_img.pack(pady=5)
LAST_MSG_IDS.clear() except Exception:
LAST_MSG_IDS.update(new_ids) tk.Label(popup, wrap="400", text="You can preview the file, or download it.").pack(pady=5)
except Exception:
pass btn_frame = tk.Frame(popup)
self.root.after(1500, self.poll_messages) btn_frame.pack(pady=10)
threading.Thread(target=_poll, daemon=True).start()
def download_file():
# ---------- Notifications ---------- save_path = filedialog.asksaveasfilename(initialfile=file_name)
def notify_user(self, sender, message): if save_path:
"""Triggers all enabled notifications.""" with open(save_path, "wb") as f:
if self.settings.get("popup_notification"): f.write(requests.get(file_url).content)
threading.Thread( messagebox.showinfo("Downloaded", f"File saved to {save_path}")
target=lambda: self.show_system_notification(sender, message),
daemon=True tk.Button(btn_frame, text="Download", command=download_file).pack(side=tk.LEFT, padx=5)
).start() tk.Button(btn_frame, text="Preview", command=lambda: webbrowser.open(preview_url)).pack(side=tk.LEFT, padx=5)
if self.settings.get("camera_notification"):
threading.Thread( def poll_messages(self, force=False):
target=self.blink_camera_light, try:
daemon=True r = SESSION.get(SERVER + "/api/messages")
).start() if r.status_code != 200:
return
def show_system_notification(self, title, message): msgs = r.json()
"""Show a real system notification using plyer.""" new_ids = {m["id"] for m in msgs}
try: new_msgs = [m for m in msgs if m["id"] not in LAST_MSG_IDS]
notification.notify(
title=f"New message from {title}", if new_msgs or force:
app_name="chatter", self.chat_box.config(state='normal')
message=message[:200], self.chat_box.delete("1.0", tk.END)
timeout=5 for m in msgs:
) ts = m.get("created_at") or time.time()
except Exception as e: try:
print(f"[Notification failed] {e}") ts_str = datetime.fromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M:%S")
except:
def blink_camera_light(self, blink_time=3): ts_str = str(ts)
"""Turn camera on briefly to activate LED."""
try: if m.get("attachment"):
cap = cv2.VideoCapture(0) attach_name = m["attachment"]
if cap.isOpened(): tag_name = f"attach{m['id']}"
ret, frame = cap.read() # read one frame so LED actually activates
if ret: # Grey italic text for "sent a file"
time.sleep(blink_time) self.chat_box.insert(tk.END, f"[{ts_str}] {m['username']} sent a file: ", f"grey{m['id']}")
cap.release() self.chat_box.tag_config(f"grey{m['id']}", foreground="grey", font=("Courier", 10, "italic"))
else:
print("[⚠] Could not access camera.") # Blue clickable filename
except Exception as e: self.chat_box.insert(tk.END, f"{attach_name}\n", tag_name)
print(f"[Camera blink failed] {e}") 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))
# --- Run GUI --- else:
if __name__ == "__main__": text = m.get("text", "")
root = tk.Tk() self.chat_box.insert(tk.END, f"[{ts_str}] {m['username']}: {text}\n")
app = ChatApp(root)
root.mainloop() 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()