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
"""
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.
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 tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import requests, os, time, webbrowser
import requests, os, time, webbrowser, sys
from datetime import datetime
from dateutil import parser, tz
from plyer import notification
import cv2
from PIL import Image, ImageTk
from io import BytesIO
from dateutil import tz
SERVER = "https://chatter.wholeworldcoding.com"
SESSION = requests.Session()
LAST_MSG_IDS = set()
# Default font sizes
BASE_FONT_SIZE = 10
BASE_PADDING = 5
# ---------------- ChatApp ----------------
class ChatApp:
def __init__(self, root):
self.root = root
root.title("chatter")
root.geometry("800x600")
root.minsize(500, 400)
root.geometry("900x700")
root.minsize(600, 500)
# Settings
self.settings = {
"popup_notification": True,
"camera_notification": False,
"time_24h": False,
"timezone": time.tzname[0] # Default local timezone
"use_24_hour": False,
"date_position": "before",
"timezone": tz.tzlocal(),
"zoom": 1.0,
"dark_mode": False,
}
self.username = None
@@ -85,45 +91,75 @@ class ChatApp:
self.msg_entry = tk.Entry(frame)
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="📎 File", command=self.send_file).grid(row=0, column=2, padx=5)
self.apply_theme()
self.poll_messages()
# ---------- Settings ----------
def open_settings(self):
win = tk.Toplevel(self.root)
win.title("Settings")
win.geometry("350x250")
win.resizable(False, False)
win.geometry("400x400")
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"])
cam_var = tk.BooleanVar(value=self.settings["camera_notification"])
time24_var = tk.BooleanVar(value=self.settings.get("time_24h", False))
tz_var = tk.StringVar(value=self.settings.get("timezone", time.tzname[0]))
hour24_var = tk.BooleanVar(value=self.settings.get("use_24_hour", False))
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.Checkbutton(win, text="Popup Notification", variable=popup_var).pack(anchor="w", padx=20)
tk.Checkbutton(win, text="Camera Notification", variable=cam_var).pack(anchor="w", padx=20)
tk.Label(scroll_frame, text="Notification Options", font=("Courier", 12, "bold")).pack(pady=10)
tk.Checkbutton(scroll_frame, text="Popup Notification", variable=popup_var,
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.Checkbutton(win, text="24-Hour Clock", variable=time24_var).pack(anchor="w", padx=20)
tk.Label(scroll_frame, text="Clock Options", font=("Courier", 12, "bold")).pack(pady=10)
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)
# Common timezones for dropdown
tz_options = ["UTC", "US/Eastern", "US/Central", "US/Mountain", "US/Pacific", "Europe/London", "Europe/Berlin"]
tk.OptionMenu(win, tz_var, *tz_options).pack(anchor="w", padx=20)
tk.Label(scroll_frame, text="Date Position", font=("Courier", 12, "bold")).pack(pady=10)
tk.Radiobutton(scroll_frame, text="Before username", variable=datepos_var, value="before",
command=lambda: self.update_setting("date_position", datepos_var.get())).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():
self.settings["popup_notification"] = popup_var.get()
self.settings["camera_notification"] = cam_var.get()
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.Label(scroll_frame, text="Zoom (Font Size)", font=("Courier", 12, "bold")).pack(pady=10)
tk.Scale(scroll_frame, variable=zoom_var, from_=0.5, to=3.0, resolution=0.1, orient="horizontal",
command=lambda e: self.update_setting("zoom", zoom_var.get())).pack(padx=20, pady=5)
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 ----------
def login(self):
@@ -173,6 +209,7 @@ class ChatApp:
except Exception as e:
print(f"File send error: {e}")
# ---------- Attachment Preview ----------
def show_attachment_popup(self, file_name, username):
popup = tk.Toplevel(self.root)
popup.title(f"{username} sent a file")
@@ -181,79 +218,55 @@ class ChatApp:
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)
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)
# Title
title_label = tk.Label(popup, wraplength=380,
text=f"{username} sent: {file_name}",
font=self.get_font("italic"),
fg="grey")
title_label.pack(pady=5)
# Buttons
btn_frame = tk.Frame(popup)
btn_frame.pack(pady=10)
btn_frame.pack(pady=5)
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)
# ---------- Timestamp helper ----------
def format_timestamp(self, ts):
# Image frame
img_frame = tk.Frame(popup)
img_frame.pack(expand=True, fill="both", pady=5)
try:
dt = parser.parse(ts)
dt = dt.astimezone(tz.gettz(self.settings.get("timezone", time.tzname[0])))
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")
resp = requests.get(file_url)
pil_img = Image.open(BytesIO(resp.content))
except Exception:
return str(ts)
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:
tk.Label(img_frame, wraplength=380, text="You can preview the file, or download it.").pack(pady=5)
return
msgs = r.json()
self.chat_box.config(state='normal')
self.chat_box.delete("1.0", tk.END)
# Display image function
label_img = tk.Label(img_frame)
label_img.pack(expand=True)
for m in msgs:
ts = m.get("created_at") or time.time()
ts_str = self.format_timestamp(ts)
def update_image(*args):
popup.update_idletasks()
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"):
attach_name = m["attachment"]
tag_name = f"attach{m['id']}"
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")
# Bind resize event
popup.bind("<Configure>", update_image)
update_image()
self.chat_box.config(state='disabled')
self.chat_box.yview(tk.END)
except Exception as e:
print(f"Update timestamp error: {e}")
# ---------- Poll messages ----------
# ---------- Message Polling ----------
def poll_messages(self, force=False):
try:
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]
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:
if m["username"] != self.username:
msg_preview = m.get("text") or m.get("attachment", "Attachment")
@@ -279,6 +333,31 @@ class ChatApp:
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 ----------
def notify_user(self, sender, message):
if self.settings.get("popup_notification"):
@@ -288,7 +367,20 @@ class ChatApp:
def show_system_notification(self, title, message):
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:
print(f"Notification failed: {e}")
@@ -307,5 +399,13 @@ class ChatApp:
# --- Run GUI ---
if __name__ == "__main__":
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)
root.mainloop()