Update desktop.py

This commit is contained in:
2025-10-11 01:46:09 +00:00
parent 58770684db
commit 2184c25a2e

View File

@@ -1,41 +1,47 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Desktop GUI wrapper for chatter.py using Tkinter. Desktop GUI wrapper for chatter.py using Tkinter.
Supports text messages and attachments with clickable filenames. Supports text messages and attachments (files) with clickable filenames.
Polls server every 1.5s for updates. Polls server every 1.5s for updates.
Includes 24-hour toggle and timezone selection. Settings now apply instantly.
Attachment preview images resize dynamically with the popup.
App icon set to chatter.png / chatter.ico for notifications.
""" """
import threading import threading
import tkinter as tk import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox from tkinter import filedialog, scrolledtext, messagebox
import requests, os, time, webbrowser import requests, os, time, webbrowser, sys
from datetime import datetime from datetime import datetime
from dateutil import parser, tz
from plyer import notification from plyer import notification
import cv2 import cv2
from PIL import Image, ImageTk from PIL import Image, ImageTk
from io import BytesIO from io import BytesIO
from dateutil import tz
SERVER = "https://chatter.wholeworldcoding.com" SERVER = "https://chatter.wholeworldcoding.com"
SESSION = requests.Session() SESSION = requests.Session()
LAST_MSG_IDS = set() LAST_MSG_IDS = set()
# Default font sizes
BASE_FONT_SIZE = 10
BASE_PADDING = 5
# ---------------- ChatApp ----------------
class ChatApp: class ChatApp:
def __init__(self, root): def __init__(self, root):
self.root = root self.root = root
root.title("chatter") root.title("chatter")
root.geometry("800x600") root.geometry("900x700")
root.minsize(500, 400) root.minsize(600, 500)
# Settings
self.settings = { self.settings = {
"popup_notification": True, "popup_notification": True,
"camera_notification": False, "camera_notification": False,
"time_24h": False, "use_24_hour": False,
"timezone": time.tzname[0] # Default local timezone "date_position": "before",
"timezone": tz.tzlocal(),
"zoom": 1.0,
"dark_mode": False,
} }
self.username = None self.username = None
@@ -85,45 +91,75 @@ 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)
self.msg_entry.bind("<Return>", lambda e: self.send_message())
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)
self.apply_theme()
self.poll_messages() self.poll_messages()
# ---------- Settings ---------- # ---------- Settings ----------
def open_settings(self): def open_settings(self):
win = tk.Toplevel(self.root) win = tk.Toplevel(self.root)
win.title("Settings") win.title("Settings")
win.geometry("350x250") win.geometry("400x400")
win.resizable(False, False)
canvas = tk.Canvas(win)
scrollbar = tk.Scrollbar(win, orient="vertical", command=canvas.yview)
scroll_frame = tk.Frame(canvas)
scroll_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scroll_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Enable mouse wheel scrolling
def _on_mousewheel(event):
canvas.yview_scroll(-int(event.delta/120), "units")
scroll_frame.bind_all("<MouseWheel>", _on_mousewheel)
# Variables
popup_var = tk.BooleanVar(value=self.settings["popup_notification"]) popup_var = tk.BooleanVar(value=self.settings["popup_notification"])
cam_var = tk.BooleanVar(value=self.settings["camera_notification"]) cam_var = tk.BooleanVar(value=self.settings["camera_notification"])
time24_var = tk.BooleanVar(value=self.settings.get("time_24h", False)) hour24_var = tk.BooleanVar(value=self.settings.get("use_24_hour", False))
tz_var = tk.StringVar(value=self.settings.get("timezone", time.tzname[0])) datepos_var = tk.StringVar(value=self.settings.get("date_position", "before"))
zoom_var = tk.DoubleVar(value=self.settings.get("zoom", 1.0))
dark_var = tk.BooleanVar(value=self.settings.get("dark_mode", False))
tk.Label(win, text="Notification Options", font=("Courier", 12, "bold")).pack(pady=5) tk.Label(scroll_frame, text="Notification Options", font=("Courier", 12, "bold")).pack(pady=10)
tk.Checkbutton(win, text="Popup Notification", variable=popup_var).pack(anchor="w", padx=20) tk.Checkbutton(scroll_frame, text="Popup Notification", variable=popup_var,
tk.Checkbutton(win, text="Camera Notification", variable=cam_var).pack(anchor="w", padx=20) command=lambda: self.update_setting("popup_notification", popup_var.get())).pack(anchor="w", padx=20, pady=5)
tk.Checkbutton(scroll_frame, text="Camera Notification", variable=cam_var,
command=lambda: self.update_setting("camera_notification", cam_var.get())).pack(anchor="w", padx=20, pady=5)
tk.Label(win, text="Time Display", font=("Courier", 12, "bold")).pack(pady=5) tk.Label(scroll_frame, text="Clock Options", font=("Courier", 12, "bold")).pack(pady=10)
tk.Checkbutton(win, text="24-Hour Clock", variable=time24_var).pack(anchor="w", padx=20) tk.Checkbutton(scroll_frame, text="Use 24-hour time", variable=hour24_var,
command=lambda: self.update_setting("use_24_hour", hour24_var.get())).pack(anchor="w", padx=20, pady=5)
tk.Label(win, text="Timezone", font=("Courier", 12, "bold")).pack(pady=5) tk.Label(scroll_frame, text="Date Position", font=("Courier", 12, "bold")).pack(pady=10)
# Common timezones for dropdown tk.Radiobutton(scroll_frame, text="Before username", variable=datepos_var, value="before",
tz_options = ["UTC", "US/Eastern", "US/Central", "US/Mountain", "US/Pacific", "Europe/London", "Europe/Berlin"] command=lambda: self.update_setting("date_position", datepos_var.get())).pack(anchor="w", padx=20)
tk.OptionMenu(win, tz_var, *tz_options).pack(anchor="w", padx=20) tk.Radiobutton(scroll_frame, text="After username", variable=datepos_var, value="after",
command=lambda: self.update_setting("date_position", datepos_var.get())).pack(anchor="w", padx=20)
def save_settings(): tk.Label(scroll_frame, text="Zoom (Font Size)", font=("Courier", 12, "bold")).pack(pady=10)
self.settings["popup_notification"] = popup_var.get() tk.Scale(scroll_frame, variable=zoom_var, from_=0.5, to=3.0, resolution=0.1, orient="horizontal",
self.settings["camera_notification"] = cam_var.get() command=lambda e: self.update_setting("zoom", zoom_var.get())).pack(padx=20, pady=5)
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) tk.Label(scroll_frame, text="Appearance", font=("Courier", 12, "bold")).pack(pady=10)
tk.Checkbutton(scroll_frame, text="Dark Mode", variable=dark_var,
command=lambda: self.update_setting("dark_mode", dark_var.get())).pack(anchor="w", padx=20, pady=5)
# ---------- Settings updater ----------
def update_setting(self, key, value):
self.settings[key] = value
if key in ["zoom", "dark_mode"]:
self.apply_theme()
self.poll_messages(force=True)
# ---------- Auth ---------- # ---------- Auth ----------
def login(self): def login(self):
@@ -173,6 +209,7 @@ class ChatApp:
except Exception as e: except Exception as e:
print(f"File send error: {e}") print(f"File send error: {e}")
# ---------- Attachment Preview ----------
def show_attachment_popup(self, file_name, username): def show_attachment_popup(self, file_name, username):
popup = tk.Toplevel(self.root) popup = tk.Toplevel(self.root)
popup.title(f"{username} sent a file") popup.title(f"{username} sent a file")
@@ -181,79 +218,55 @@ class ChatApp:
file_url = f"{SERVER}/uploads/{file_name}" file_url = f"{SERVER}/uploads/{file_name}"
preview_url = file_url.replace("/uploads/", "/preview/") 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) # Title
title_label = tk.Label(popup, wraplength=380,
try: text=f"{username} sent: {file_name}",
resp = requests.get(file_url) font=self.get_font("italic"),
pil_img = Image.open(BytesIO(resp.content)) fg="grey")
pil_img.thumbnail((350, 350)) title_label.pack(pady=5)
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)
# Buttons
btn_frame = tk.Frame(popup) btn_frame = tk.Frame(popup)
btn_frame.pack(pady=10) btn_frame.pack(pady=5)
def download_file(): def download_file():
save_path = filedialog.asksaveasfilename(initialfile=file_name) save_path = filedialog.asksaveasfilename(initialfile=file_name)
if save_path: if save_path:
with open(save_path, "wb") as f: with open(save_path, "wb") as f:
f.write(requests.get(file_url).content) f.write(requests.get(file_url).content)
messagebox.showinfo("Downloaded", f"File saved to {save_path}") 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="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) tk.Button(btn_frame, text="Preview", command=lambda: webbrowser.open(preview_url)).pack(side=tk.LEFT, padx=5)
# ---------- Timestamp helper ---------- # Image frame
def format_timestamp(self, ts): img_frame = tk.Frame(popup)
img_frame.pack(expand=True, fill="both", pady=5)
try: try:
dt = parser.parse(ts) resp = requests.get(file_url)
dt = dt.astimezone(tz.gettz(self.settings.get("timezone", time.tzname[0]))) pil_img = Image.open(BytesIO(resp.content))
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: except Exception:
return str(ts) tk.Label(img_frame, wraplength=380, text="You can preview the file, or download it.").pack(pady=5)
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 return
msgs = r.json()
self.chat_box.config(state='normal') # Display image function
self.chat_box.delete("1.0", tk.END) label_img = tk.Label(img_frame)
label_img.pack(expand=True)
for m in msgs: def update_image(*args):
ts = m.get("created_at") or time.time() popup.update_idletasks()
ts_str = self.format_timestamp(ts) max_width = popup.winfo_width() - 20
max_height = popup.winfo_height() - title_label.winfo_reqheight() - btn_frame.winfo_reqheight() - 40
img_copy = pil_img.copy()
img_copy.thumbnail((max_width, max_height))
tk_img = ImageTk.PhotoImage(img_copy)
label_img.config(image=tk_img)
label_img.image = tk_img
if m.get("attachment"): # Bind resize event
attach_name = m["attachment"] popup.bind("<Configure>", update_image)
tag_name = f"attach{m['id']}" update_image()
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, "<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') # ---------- Message Polling ----------
self.chat_box.yview(tk.END)
except Exception as e:
print(f"Update timestamp error: {e}")
# ---------- Poll messages ----------
def poll_messages(self, force=False): def poll_messages(self, force=False):
try: try:
r = SESSION.get(SERVER + "/api/messages") r = SESSION.get(SERVER + "/api/messages")
@@ -264,9 +277,50 @@ class ChatApp:
new_msgs = [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_msgs or force: if new_msgs or force:
self.update_chat_timestamps() self.chat_box.config(state='normal')
self.chat_box.delete("1.0", tk.END)
padding = int(BASE_PADDING * self.settings.get("zoom", 1.0))
for m in msgs:
ts = m.get("created_at") or time.time()
try:
dt = datetime.fromtimestamp(float(ts)).astimezone(self.settings["timezone"])
ts_str = dt.strftime("%m/%d/%Y, %H:%M:%S") if self.settings.get("use_24_hour") else dt.strftime("%m/%d/%Y, %I:%M:%S %p")
except Exception:
ts_str = str(ts)
ts_tag = f"ts{m['id']}"
uname_tag = f"bold{m['id']}"
msg_tag = f"msg{m['id']}"
attach_tag = f"attach{m['id']}"
if m.get("attachment"):
attach_name = m["attachment"]
if self.settings["date_position"] == "before":
self.chat_box.insert(tk.END, f"[{ts_str}] {m['username']} sent a file: ", ts_tag)
else:
self.chat_box.insert(tk.END, f"{m['username']} [{ts_str}] sent a file: ", ts_tag)
self.chat_box.tag_config(ts_tag, foreground="grey", font=self.get_font("italic"))
self.chat_box.insert(tk.END, f"{attach_name}\n", attach_tag)
self.chat_box.tag_config(attach_tag, foreground="blue", font=self.get_font("normal"), underline=True)
self.chat_box.tag_bind(attach_tag, "<Button-1>",
lambda e, fn=attach_name, un=m["username"]: self.show_attachment_popup(fn, un))
else:
if self.settings["date_position"] == "before":
self.chat_box.insert(tk.END, f"[{ts_str}] ", ts_tag)
self.chat_box.insert(tk.END, f"{m['username']}", uname_tag)
self.chat_box.insert(tk.END, f": {m.get('text','')}\n", msg_tag)
else:
self.chat_box.insert(tk.END, f"{m['username']}", uname_tag)
self.chat_box.insert(tk.END, f" [{ts_str}]: ", ts_tag)
self.chat_box.insert(tk.END, f"{m.get('text','')}\n", msg_tag)
self.chat_box.tag_config(ts_tag, foreground="grey", font=self.get_font("grey"))
self.chat_box.tag_config(uname_tag, font=self.get_font("bold"))
self.chat_box.tag_config(msg_tag, font=self.get_font("normal"), spacing1=padding, spacing3=padding)
self.chat_box.config(state='disabled')
self.chat_box.yview(tk.END)
# Notify user about new messages
for m in new_msgs: for m in new_msgs:
if m["username"] != self.username: if m["username"] != self.username:
msg_preview = m.get("text") or m.get("attachment", "Attachment") msg_preview = m.get("text") or m.get("attachment", "Attachment")
@@ -279,6 +333,31 @@ class ChatApp:
self.root.after(1500, self.poll_messages) self.root.after(1500, self.poll_messages)
# ---------- Font helpers ----------
def get_font(self, kind="normal"):
scale = self.settings.get("zoom", 1.0)
size = max(int(BASE_FONT_SIZE * scale), 1)
if kind == "bold":
return ("Courier", size+1, "bold")
elif kind == "italic":
return ("Courier", size, "italic")
elif kind == "grey":
return ("Courier", max(size-1, 1))
else:
return ("Courier", size)
# ---------- Theme ----------
def apply_theme(self):
dark = self.settings.get("dark_mode", False)
bg = "#1e1e1e" if dark else "white"
fg = "#d4d4d4" if dark else "black"
entry_bg = "#2e2e2e" if dark else "white"
entry_fg = "#d4d4d4" if dark else "black"
self.chat_box.config(bg=bg, fg=fg, insertbackground=fg)
self.msg_entry.config(bg=entry_bg, fg=entry_fg, insertbackground=fg)
self.root.config(bg=bg)
# ---------- Notifications ---------- # ---------- Notifications ----------
def notify_user(self, sender, message): def notify_user(self, sender, message):
if self.settings.get("popup_notification"): if self.settings.get("popup_notification"):
@@ -288,7 +367,20 @@ class ChatApp:
def show_system_notification(self, title, message): def show_system_notification(self, title, message):
try: try:
notification.notify(title=title, app_name="chatter", message=message[:200], timeout=5) # Determine icon path for PyInstaller
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
else:
base_path = os.path.dirname(os.path.abspath(__file__))
icon_path = os.path.join(base_path, "chatter.ico")
notification.notify(
title=title,
message=message[:200],
app_name="chatter",
app_icon=icon_path, # Use .ico for Windows toast
timeout=5
)
except Exception as e: except Exception as e:
print(f"Notification failed: {e}") print(f"Notification failed: {e}")
@@ -307,5 +399,13 @@ class ChatApp:
# --- Run GUI --- # --- Run GUI ---
if __name__ == "__main__": if __name__ == "__main__":
root = tk.Tk() root = tk.Tk()
# Set app icon
try:
icon_img = tk.PhotoImage(file="chatter.png")
root.iconphoto(True, icon_img)
except Exception as e:
print(f"Failed to load icon: {e}")
app = ChatApp(root) app = ChatApp(root)
root.mainloop() root.mainloop()