feature pack 1
This commit is contained in:
115
desktop.py
115
desktop.py
@@ -1,8 +1,9 @@
|
|||||||
#!/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 (files) with clickable filenames.
|
Supports text messages and attachments with clickable filenames.
|
||||||
Polls server every 1.5s for updates.
|
Polls server every 1.5s for updates.
|
||||||
|
Includes 24-hour toggle and timezone selection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
@@ -10,6 +11,7 @@ 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
|
||||||
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
|
||||||
@@ -20,6 +22,7 @@ SESSION = requests.Session()
|
|||||||
LAST_MSG_IDS = set()
|
LAST_MSG_IDS = set()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- ChatApp ----------------
|
||||||
class ChatApp:
|
class ChatApp:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
@@ -27,9 +30,12 @@ class ChatApp:
|
|||||||
root.geometry("800x600")
|
root.geometry("800x600")
|
||||||
root.minsize(500, 400)
|
root.minsize(500, 400)
|
||||||
|
|
||||||
|
# Settings
|
||||||
self.settings = {
|
self.settings = {
|
||||||
"popup_notification": True,
|
"popup_notification": True,
|
||||||
"camera_notification": False,
|
"camera_notification": False,
|
||||||
|
"time_24h": False,
|
||||||
|
"timezone": time.tzname[0] # Default local timezone
|
||||||
}
|
}
|
||||||
|
|
||||||
self.username = None
|
self.username = None
|
||||||
@@ -88,19 +94,32 @@ class ChatApp:
|
|||||||
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("300x200")
|
win.geometry("350x250")
|
||||||
win.resizable(False, False)
|
win.resizable(False, False)
|
||||||
|
|
||||||
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))
|
||||||
|
tz_var = tk.StringVar(value=self.settings.get("timezone", time.tzname[0]))
|
||||||
|
|
||||||
tk.Label(win, text="Notification Options", font=("Courier", 12, "bold")).pack(pady=10)
|
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, 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, pady=5)
|
tk.Checkbutton(win, text="Camera Notification", variable=cam_var).pack(anchor="w", padx=20)
|
||||||
|
|
||||||
|
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(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)
|
||||||
|
|
||||||
def save_settings():
|
def save_settings():
|
||||||
self.settings["popup_notification"] = popup_var.get()
|
self.settings["popup_notification"] = popup_var.get()
|
||||||
self.settings["camera_notification"] = cam_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.")
|
messagebox.showinfo("Saved", "Settings updated.")
|
||||||
win.destroy()
|
win.destroy()
|
||||||
|
|
||||||
@@ -164,7 +183,6 @@ class ChatApp:
|
|||||||
|
|
||||||
tk.Label(popup, wrap="400", text=f"{username} sent: {file_name}", font=("Courier", 12, "bold")).pack(pady=10)
|
tk.Label(popup, wrap="400", text=f"{username} sent: {file_name}", font=("Courier", 12, "bold")).pack(pady=10)
|
||||||
|
|
||||||
# Attempt to preview image inline
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(file_url)
|
resp = requests.get(file_url)
|
||||||
pil_img = Image.open(BytesIO(resp.content))
|
pil_img = Image.open(BytesIO(resp.content))
|
||||||
@@ -189,6 +207,53 @@ class ChatApp:
|
|||||||
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 ----------
|
||||||
|
def format_timestamp(self, ts):
|
||||||
|
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")
|
||||||
|
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:
|
||||||
|
return
|
||||||
|
msgs = r.json()
|
||||||
|
|
||||||
|
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()
|
||||||
|
ts_str = self.format_timestamp(ts)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
self.chat_box.config(state='disabled')
|
||||||
|
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")
|
||||||
@@ -199,34 +264,7 @@ 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.chat_box.config(state='normal')
|
self.update_chat_timestamps()
|
||||||
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
|
# Notify user about new messages
|
||||||
for m in new_msgs:
|
for m in new_msgs:
|
||||||
@@ -250,12 +288,7 @@ class ChatApp:
|
|||||||
|
|
||||||
def show_system_notification(self, title, message):
|
def show_system_notification(self, title, message):
|
||||||
try:
|
try:
|
||||||
notification.notify(
|
notification.notify(title=title, app_name="chatter", message=message[:200], timeout=5)
|
||||||
title=title,
|
|
||||||
app_name="chatter",
|
|
||||||
message=message[:200],
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Notification failed: {e}")
|
print(f"Notification failed: {e}")
|
||||||
|
|
||||||
@@ -275,4 +308,4 @@ class ChatApp:
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
app = ChatApp(root)
|
app = ChatApp(root)
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
Reference in New Issue
Block a user