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,21 +1,22 @@
#!/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 threading
import tkinter as tk import tkinter as tk
from tkinter import messagebox, filedialog, scrolledtext from tkinter import filedialog, scrolledtext, messagebox
import requests, os, queue, time, cv2 import requests, os, time, webbrowser
from datetime import datetime
from plyer import notification from plyer import notification
import cv2
from PIL import Image, ImageTk
from io import BytesIO
SERVER = "https://chatter.wholeworldcoding.com" SERVER = "https://chatter.wholeworldcoding.com"
SESSION = requests.Session() SESSION = requests.Session()
MSG_QUEUE = queue.Queue()
LAST_MSG_IDS = set() LAST_MSG_IDS = set()
@@ -26,16 +27,11 @@ class ChatApp:
root.geometry("800x600") root.geometry("800x600")
root.minsize(500, 400) 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
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
self.username = None self.username = None
self.build_login_screen() self.build_login_screen()
@@ -45,24 +41,21 @@ class ChatApp:
w.destroy() w.destroy()
frame = tk.Frame(self.root) frame = tk.Frame(self.root)
frame.grid(row=0, column=0, sticky="nsew") frame.pack(expand=True)
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
inner = tk.Frame(frame) tk.Label(frame, text="chatter", font=("Courier", 22, "bold")).pack(pady=10)
inner.place(relx=0.5, rely=0.5, anchor="center") tk.Label(frame, text="Sign up or login to continue.", font=("Courier", 11)).pack(pady=5)
tk.Label(inner, text="chatter", font=("Courier", 22, "bold")).pack(pady=10) tk.Label(frame, text="Username").pack()
tk.Label(inner, text="Sign up or login to continue.", font=("Courier", 11, "bold")).pack(pady=5) self.entry_user = tk.Entry(frame, width=30)
tk.Label(inner, text="Username").pack()
self.entry_user = tk.Entry(inner, width=30)
self.entry_user.pack(pady=3) self.entry_user.pack(pady=3)
tk.Label(inner, text="Password").pack()
self.entry_pass = tk.Entry(inner, show="*", width=30) tk.Label(frame, text="Password").pack()
self.entry_pass = tk.Entry(frame, show="*", width=30)
self.entry_pass.pack(pady=3) 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) 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): def build_chat_screen(self):
for w in self.root.winfo_children(): for w in self.root.winfo_children():
@@ -74,7 +67,8 @@ class ChatApp:
header = tk.Frame(self.root) header = tk.Frame(self.root)
header.pack(fill=tk.X, pady=3) 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.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) 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 = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, state='disabled')
self.chat_box.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) self.chat_box.pack(padx=10, pady=5, fill=tk.BOTH, expand=True)
@@ -86,8 +80,7 @@ class ChatApp:
self.msg_entry = tk.Entry(frame) self.msg_entry = tk.Entry(frame)
self.msg_entry.grid(row=0, column=0, sticky="ew", padx=5) 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="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(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() self.poll_messages()
@@ -116,8 +109,8 @@ class ChatApp:
# ---------- Auth ---------- # ---------- Auth ----------
def login(self): def login(self):
data = {"username": self.entry_user.get(), "password": self.entry_pass.get()} data = {"username": self.entry_user.get(), "password": self.entry_pass.get()}
r = SESSION.post(SERVER + "/login", data=data, allow_redirects=False) r = SESSION.post(SERVER + "/login", data=data)
if r.status_code == 302: if r.url.endswith("/chat"):
self.username = data["username"] self.username = data["username"]
self.build_chat_screen() self.build_chat_screen()
else: else:
@@ -125,8 +118,8 @@ class ChatApp:
def register(self): def register(self):
data = {"username": self.entry_user.get(), "password": self.entry_pass.get()} data = {"username": self.entry_user.get(), "password": self.entry_pass.get()}
r = SESSION.post(SERVER + "/register", data=data, allow_redirects=False) r = SESSION.post(SERVER + "/register", data=data)
if r.status_code == 302: if r.url.endswith("/login"):
messagebox.showinfo("Success", "Account created! You can log in now.") messagebox.showinfo("Success", "Account created! You can log in now.")
else: else:
messagebox.showerror("Error", "Registration failed") messagebox.showerror("Error", "Registration failed")
@@ -141,84 +134,141 @@ class ChatApp:
text = self.msg_entry.get().strip() text = self.msg_entry.get().strip()
if not text: if not text:
return return
SESSION.post(SERVER + "/api/messages", json={"text": text}) try:
self.msg_entry.delete(0, tk.END) r = SESSION.post(SERVER + "/api/messages", json={"text": text})
self.poll_messages(force=True) 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): def send_file(self):
path = filedialog.askopenfilename() path = filedialog.askopenfilename()
if not path: if not path:
return return
with open(path, "rb") as f: try:
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)
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): def poll_messages(self, force=False):
def _poll(): try:
try: r = SESSION.get(SERVER + "/api/messages")
r = SESSION.get(SERVER + "/api/messages", timeout=5) if r.status_code != 200:
if r.status_code == 200: return
msgs = r.json() msgs = r.json()
new_ids = {m["id"] for m in msgs} new_ids = {m["id"] for m in msgs}
new = [m for m in msgs if m["id"] not in LAST_MSG_IDS] new_msgs = [m for m in msgs if m["id"] not in LAST_MSG_IDS]
if new or force:
self.chat_box.config(state='normal') if new_msgs or force:
self.chat_box.delete("1.0", tk.END) self.chat_box.config(state='normal')
for m in msgs: self.chat_box.delete("1.0", tk.END)
line = f"[{m['username']}] {m['text']}\n" for m in msgs:
self.chat_box.insert(tk.END, line) ts = m.get("created_at") or time.time()
self.chat_box.config(state='disabled') try:
self.chat_box.yview(tk.END) ts_str = datetime.fromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M:%S")
for m in new: except:
if m["username"] != self.username: ts_str = str(ts)
self.notify_user(m["username"], m["text"])
LAST_MSG_IDS.clear() if m.get("attachment"):
LAST_MSG_IDS.update(new_ids) attach_name = m["attachment"]
except Exception: tag_name = f"attach{m['id']}"
pass
self.root.after(1500, self.poll_messages) # Grey italic text for "sent a file"
threading.Thread(target=_poll, daemon=True).start() 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 ---------- # ---------- Notifications ----------
def notify_user(self, sender, message): def notify_user(self, sender, message):
"""Triggers all enabled notifications."""
if self.settings.get("popup_notification"): if self.settings.get("popup_notification"):
threading.Thread( threading.Thread(target=lambda: self.show_system_notification(sender, message), daemon=True).start()
target=lambda: self.show_system_notification(sender, message),
daemon=True
).start()
if self.settings.get("camera_notification"): if self.settings.get("camera_notification"):
threading.Thread( threading.Thread(target=self.blink_camera_light, daemon=True).start()
target=self.blink_camera_light,
daemon=True
).start()
def show_system_notification(self, title, message): def show_system_notification(self, title, message):
"""Show a real system notification using plyer."""
try: try:
notification.notify( notification.notify(
title=f"New message from {title}", title=title,
app_name="chatter", app_name="chatter",
message=message[:200], message=message[:200],
timeout=5 timeout=5
) )
except Exception as e: except Exception as e:
print(f"[Notification failed] {e}") print(f"Notification failed: {e}")
def blink_camera_light(self, blink_time=3): def blink_camera_light(self, blink_time=3):
"""Turn camera on briefly to activate LED."""
try: try:
cap = cv2.VideoCapture(0) cap = cv2.VideoCapture(0)
if cap.isOpened(): if cap.isOpened():
ret, frame = cap.read() # read one frame so LED actually activates ret, frame = cap.read()
if ret: if ret:
time.sleep(blink_time) time.sleep(blink_time)
cap.release() cap.release()
else:
print("[⚠] Could not access camera.")
except Exception as e: except Exception as e:
print(f"[Camera blink failed] {e}") print(f"Camera blink failed: {e}")
# --- Run GUI --- # --- Run GUI ---