revert color an aicon an better ui and ux

This commit is contained in:
MOJ1403 2026-03-23 21:07:08 +03:30
parent ee1bebcc90
commit fe8a02b742
4 changed files with 339 additions and 165 deletions

View File

@ -95,6 +95,10 @@ class AppController:
self.service.add_or_update_contact(name, phone) self.service.add_or_update_contact(name, phone)
self._notify_ui() self._notify_ui()
def delete_contact(self, phone: str):
self.service.delete_contact(phone)
self._notify_ui()
def get_messages(self, phone: str): def get_messages(self, phone: str):
return self.service.get_messages(phone) return self.service.get_messages(phone)

View File

@ -111,6 +111,9 @@ class SecureMessagingService:
def add_or_update_contact(self, name: str, phone: str): def add_or_update_contact(self, name: str, phone: str):
self.db.upsert_contact(phone, self._enc(name)) self.db.upsert_contact(phone, self._enc(name))
def delete_contact(self, phone: str):
self.db.delete_contact(phone)
def ensure_contact(self, phone: str, fallback_name: Optional[str] = None): def ensure_contact(self, phone: str, fallback_name: Optional[str] = None):
fallback = fallback_name or SYSTEM_CONTACT_LABEL fallback = fallback_name or SYSTEM_CONTACT_LABEL
self.db.ensure_contact_exists(phone, self._enc(fallback)) self.db.ensure_contact_exists(phone, self._enc(fallback))

View File

@ -200,6 +200,14 @@ class Database:
cursor.execute("SELECT * FROM contacts WHERE phone = ?", (phone,)) cursor.execute("SELECT * FROM contacts WHERE phone = ?", (phone,))
return cursor.fetchone() return cursor.fetchone()
def delete_contact(self, phone: str):
with self._connect() as conn:
conn.execute("DELETE FROM contacts WHERE phone = ?", (phone,))
conn.execute("DELETE FROM messages WHERE phone = ?", (phone,))
conn.execute("DELETE FROM packet_fragments WHERE phone = ?", (phone,))
conn.execute("DELETE FROM secure_events WHERE phone = ?", (phone,))
conn.commit()
def list_contact_rows(self) -> list[sqlite3.Row]: def list_contact_rows(self) -> list[sqlite3.Row]:
with self._connect() as conn: with self._connect() as conn:
cursor = conn.cursor() cursor = conn.cursor()

View File

@ -163,6 +163,12 @@ class SecureSmsApp(ctk.CTk):
self.option_add("*Cursor", "none") self.option_add("*Cursor", "none")
self._build_shell() self._build_shell()
self.bind_all("<Button-1>", self._handle_global_tap, add="+") self.bind_all("<Button-1>", self._handle_global_tap, add="+")
self.bind_all("<ButtonPress-1>", self._handle_global_drag_start, add="+")
self.bind_all("<B1-Motion>", self._handle_global_drag, add="+")
self.bind_all("<ButtonRelease-1>", self._handle_global_drag_stop, add="+")
self._touch_start_y = None
self._active_scroll_frame = None
self._show_lock_screen() self._show_lock_screen()
self.after(50, self._enable_touch_kiosk_mode) self.after(50, self._enable_touch_kiosk_mode)
@ -258,6 +264,38 @@ class SecureSmsApp(ctk.CTk):
return return
self.after(20, self._hide_virtual_keyboard) self.after(20, self._hide_virtual_keyboard)
def _find_scrollable_ancestor(self, widget):
current = widget
while current is not None:
if isinstance(current, ctk.CTkScrollableFrame):
return current
current = getattr(current, "master", None)
return None
def _handle_global_drag_start(self, event):
self._touch_start_y = event.y_root
self._active_scroll_frame = self._find_scrollable_ancestor(event.widget)
def _handle_global_drag(self, event):
if self._touch_start_y is None or not self._active_scroll_frame:
return
try:
current_y = event.y_root
delta_y = current_y - self._touch_start_y
# 1 unit is roughly 15 pixels of drag
units = int(delta_y / 12)
if units != 0:
self._active_scroll_frame._parent_canvas.yview_scroll(-units, "units")
self._touch_start_y = current_y
except Exception:
pass
def _handle_global_drag_stop(self, event):
self._touch_start_y = None
self._active_scroll_frame = None
def _register_keyboard_interaction(self): def _register_keyboard_interaction(self):
self._keyboard_interaction_guard = True self._keyboard_interaction_guard = True
self.after(160, self._clear_keyboard_interaction_guard) self.after(160, self._clear_keyboard_interaction_guard)
@ -647,19 +685,8 @@ class SecureSmsApp(ctk.CTk):
self.after(80, lambda: self._focus_registered_input(self.password_entry)) self.after(80, lambda: self._focus_registered_input(self.password_entry))
def _configure_root_layout(self): def _configure_root_layout(self):
for column in range(2): self.root_frame.grid_columnconfigure(0, weight=1)
self.root_frame.grid_columnconfigure(column, weight=0, minsize=0) self.root_frame.grid_rowconfigure(0, weight=1)
for row in range(2):
self.root_frame.grid_rowconfigure(row, weight=0, minsize=0)
if self.is_portrait:
top_height = min(max(270, int(self.window_height * 0.36)), 320)
self.root_frame.grid_columnconfigure(0, weight=1)
self.root_frame.grid_rowconfigure(0, weight=0, minsize=top_height)
self.root_frame.grid_rowconfigure(1, weight=1)
else:
self.root_frame.grid_columnconfigure(0, weight=0, minsize=248)
self.root_frame.grid_columnconfigure(1, weight=1)
self.root_frame.grid_rowconfigure(0, weight=1)
def _configure_profile_card_layout(self): def _configure_profile_card_layout(self):
for widget in ( for widget in (
@ -726,8 +753,6 @@ class SecureSmsApp(ctk.CTk):
title_size = 24 if self.is_portrait else 28 title_size = 24 if self.is_portrait else 28
subtitle_size = 12 if self.is_portrait else 13 subtitle_size = 12 if self.is_portrait else 13
action_height = 46 if self.is_portrait else 42 action_height = 46 if self.is_portrait else 42
main_row = 1 if self.is_portrait else 0
main_column = 0 if self.is_portrait else 1
self.sidebar = ctk.CTkFrame(self.root_frame, fg_color=SIDEBAR, corner_radius=0, border_width=0) self.sidebar = ctk.CTkFrame(self.root_frame, fg_color=SIDEBAR, corner_radius=0, border_width=0)
self.sidebar.grid(row=0, column=0, sticky="nsew") self.sidebar.grid(row=0, column=0, sticky="nsew")
@ -735,162 +760,123 @@ class SecureSmsApp(ctk.CTk):
self.sidebar.grid_rowconfigure(5, weight=1) self.sidebar.grid_rowconfigure(5, weight=1)
sidebar_header = ctk.CTkFrame(self.sidebar, fg_color="transparent") sidebar_header = ctk.CTkFrame(self.sidebar, fg_color="transparent")
sidebar_header.grid(row=0, column=0, padx=14, pady=(12, 2), sticky="ew") sidebar_header.grid(row=0, column=0, padx=14, pady=(12, 10), sticky="ew")
sidebar_header.grid_columnconfigure(0, weight=1) sidebar_header.grid_columnconfigure(0, weight=1)
sidebar_header.grid_columnconfigure(1, weight=0)
RTLButton(
sidebar_header,
text='',
width=40,
height=40,
corner_radius=20,
fg_color="transparent",
hover_color=BORDER,
text_color="white",
font=ctk.CTkFont(family=FONT_TITLE, size=24, weight="bold"),
command=self._show_drawer_menu
).grid(row=0, column=1, padx=(6, 0), sticky="e")
RTLLabel( RTLLabel(
sidebar_header, sidebar_header,
text='صبا', text='صبا',
text_color="white", text_color="white",
font=ctk.CTkFont(family=FONT_TITLE, size=title_size, weight="bold"), font=ctk.CTkFont(family=FONT_TITLE, size=title_size, weight="bold"),
).grid(row=0, column=0, sticky="e") ).grid(row=0, column=0, sticky="e")
RTLLabel(
sidebar_header,
text='پیام\u200cرسان امن',
text_color=PRIMARY_SOFT,
font=ctk.CTkFont(family=FONT_BODY, size=subtitle_size),
).grid(row=1, column=0, sticky="e")
self.connection_badge = RTLLabel( self.fab = RTLButton(
self.sidebar, self.sidebar,
text="", text='+',
corner_radius=6, width=56,
fg_color="#2E7D62", height=56,
text_color="white", corner_radius=28,
font=ctk.CTkFont(family=FONT_BODY, size=13, weight="bold"),
padx=10,
pady=6,
)
self.connection_badge.grid(row=2, column=0, padx=14, pady=(6, 8), sticky="ew")
top_actions = ctk.CTkFrame(self.sidebar, fg_color="transparent")
top_actions.grid(row=3, column=0, padx=14, pady=(0, 6), sticky="ew")
top_actions.grid_columnconfigure((0, 1), weight=1)
RTLButton(
top_actions,
text='گفتگوی جدید',
command=self._open_contact_dialog,
fg_color=PRIMARY, fg_color=PRIMARY,
text_color="#FFFFFF",
hover_color=PRIMARY_DARK, hover_color=PRIMARY_DARK,
corner_radius=8,
height=action_height,
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
).grid(row=0, column=0, padx=4, sticky="ew")
RTLButton(
top_actions,
text='⚙ تنظیمات',
command=self._open_settings_panel,
fg_color=INPUT_BG,
text_color=TEXT,
hover_color=BORDER,
corner_radius=8,
height=action_height,
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
).grid(row=0, column=1, padx=4, sticky="ew")
self.contact_form_card = ctk.CTkFrame(
self.sidebar,
fg_color=SIDEBAR_SOFT,
corner_radius=10,
border_width=1,
border_color=BORDER,
)
self.contact_form_card.grid(row=4, column=0, padx=14, pady=(0, 10), sticky="ew")
self.contact_form_card.grid_columnconfigure(0, weight=1)
RTLLabel(
self.contact_form_card,
text='مخاطب جدید',
text_color="white", text_color="white",
font=ctk.CTkFont(family=FONT_BODY, size=17, weight="bold"), font=ctk.CTkFont(family=FONT_TITLE, size=32, weight="bold"),
).grid(row=0, column=0, padx=14, pady=(14, 8), sticky="e") command=self._open_contact_dialog
self.contact_name_entry = RTLEntry(
self.contact_form_card,
placeholder_text='نام مخاطب',
height=44,
font=ctk.CTkFont(family=FONT_BODY, size=14 if self.is_portrait else 15),
) )
self.contact_name_entry.grid(row=1, column=0, padx=14, pady=6, sticky="ew")
self.contact_phone_entry = RTLEntry( self.add_contact_panel = ctk.CTkFrame(self.root_frame, fg_color=BACKGROUND, corner_radius=0)
self.contact_form_card, self.add_contact_panel.grid_rowconfigure(1, weight=1)
placeholder_text='شماره موبایل', self.add_contact_panel.grid_columnconfigure(0, weight=1)
height=44,
font=ctk.CTkFont(family=FONT_BODY, size=14 if self.is_portrait else 15), add_contact_header = ctk.CTkFrame(self.add_contact_panel, fg_color=CARD, corner_radius=0, border_width=0)
) add_contact_header.grid(row=0, column=0, sticky="ew")
self.contact_phone_entry.grid(row=2, column=0, padx=14, pady=6, sticky="ew") add_contact_header.grid_columnconfigure(0, weight=1)
self.contact_form_message = RTLLabel( add_contact_header.grid_columnconfigure(1, weight=0)
self.contact_form_card,
text="", RTLLabel(
text_color="#FDE68A", add_contact_header,
font=ctk.CTkFont(family=FONT_BODY, size=13), text='مخاطب جدید',
) text_color=TEXT,
self.contact_form_message.grid(row=3, column=0, padx=14, pady=(4, 2), sticky="e") font=ctk.CTkFont(family=FONT_TITLE, size=18, weight="bold"),
contact_actions = ctk.CTkFrame(self.contact_form_card, fg_color="transparent") ).grid(row=0, column=0, padx=16, pady=14, sticky="e")
contact_actions.grid(row=4, column=0, padx=10, pady=(4, 14), sticky="ew")
contact_actions.grid_columnconfigure((0, 1), weight=1)
RTLButton( RTLButton(
contact_actions, add_contact_header,
text='بازگشت',
width=64,
height=36,
corner_radius=18,
fg_color=INPUT_BG,
hover_color=BORDER,
text_color=TEXT,
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
command=self._hide_contact_form
).grid(row=0, column=1, padx=(4, 14), sticky="e")
self.contact_form_card = ctk.CTkFrame(self.add_contact_panel, fg_color="transparent")
self.contact_form_card.grid(row=1, column=0, padx=20, pady=20, sticky="nwe")
self.contact_form_card.grid_columnconfigure(0, weight=1)
self.contact_name_entry = RTLEntry(self.contact_form_card, placeholder_text='نام مخاطب', height=48, font=ctk.CTkFont(family=FONT_BODY, size=16))
self.contact_name_entry.grid(row=0, column=0, pady=8, sticky="ew")
self.contact_phone_entry = RTLEntry(self.contact_form_card, placeholder_text='شماره موبایل', height=48, font=ctk.CTkFont(family=FONT_BODY, size=16))
self.contact_phone_entry.grid(row=1, column=0, pady=8, sticky="ew")
self.contact_form_message = RTLLabel(self.contact_form_card, text="", text_color=DANGER, font=ctk.CTkFont(family=FONT_BODY, size=13))
self.contact_form_message.grid(row=2, column=0, pady=(2, 6), sticky="e")
RTLButton(
self.contact_form_card,
text='ذخیره', text='ذخیره',
fg_color=PRIMARY, fg_color=PRIMARY,
text_color="#FFFFFF", text_color="white",
hover_color=PRIMARY_DARK, hover_color=PRIMARY_DARK,
command=self._save_contact_inline, command=self._save_contact_inline,
corner_radius=8, corner_radius=12,
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=40, height=48,
).grid(row=0, column=0, padx=4, sticky="ew") ).grid(row=3, column=0, pady=10, sticky="ew")
RTLButton(
contact_actions,
text='بستن',
fg_color=INPUT_BG,
text_color=TEXT,
hover_color=BORDER,
command=self._hide_contact_form,
corner_radius=8,
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
height=40,
).grid(row=0, column=1, padx=4, sticky="ew")
self.contact_phone_entry.bind("<Return>", lambda _event: self._save_contact_inline()) self.contact_phone_entry.bind("<Return>", lambda _event: self._save_contact_inline())
self._register_text_input(self.contact_name_entry, title="نام مخاطب", layout="fa") self._register_text_input(self.contact_name_entry, title="نام مخاطب", layout="fa")
self._register_text_input( self._register_text_input(self.contact_phone_entry, title="شماره موبایل", layout="numeric", submit=self._save_contact_inline)
self.contact_phone_entry,
title="شماره موبایل",
layout="numeric",
submit=self._save_contact_inline,
)
self.contact_form_card.grid_remove()
self.contacts_frame = RTLScrollableFrame( self.contacts_frame = RTLScrollableFrame(
self.sidebar, self.sidebar,
height=160 if self.is_portrait else 320, height=300 if self.is_portrait else 320,
label_text='گفتگو\u200cها', label_text='گفتگو\u200cها',
label_font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), label_font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
fg_color="transparent", fg_color="transparent",
) )
self.contacts_frame.grid(row=5, column=0, padx=8, pady=4, sticky="nsew") self.contacts_frame.grid(row=1, column=0, padx=8, pady=4, sticky="nsew")
self.fab.place(relx=0.06, rely=0.94, anchor="sw")
self.fab.lift()
self.main_panel = ctk.CTkFrame(self.root_frame, fg_color=BACKGROUND, corner_radius=0) self.main_panel = ctk.CTkFrame(self.root_frame, fg_color=BACKGROUND, corner_radius=0)
self.main_panel.grid(row=main_row, column=main_column, sticky="nsew") self.main_panel.grid(row=0, column=0, sticky="nsew")
self.main_panel.grid_rowconfigure(1, weight=1) self.main_panel.grid_rowconfigure(1, weight=1)
self.main_panel.grid_columnconfigure(0, weight=1) self.main_panel.grid_columnconfigure(0, weight=1)
self.header_card = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=0, border_width=0) self.header_card = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=0, border_width=0)
self.header_card.grid(row=0, column=0, sticky="ew") self.header_card.grid(row=0, column=0, sticky="ew")
self.header_card.grid_columnconfigure(0, weight=1) self.header_card.grid_columnconfigure(0, weight=1)
self.header_card.grid_columnconfigure(1, weight=0) self.header_card.grid_columnconfigure(1, weight=0)
self.chat_title = RTLLabel( self.header_card.grid_columnconfigure(2, weight=0)
self.header_card,
text='یک مخاطب را انتخاب کن',
text_color=TEXT,
font=ctk.CTkFont(family=FONT_TITLE, size=16 if self.is_portrait else 18, weight="bold"),
)
self.chat_title.grid(row=0, column=0, padx=16, pady=(10, 2), sticky="e")
self.chat_subtitle = RTLLabel(
self.header_card,
text='در اینجا فقط دو حالت داری: عادی یا امن',
text_color=MUTED,
font=ctk.CTkFont(family=FONT_BODY, size=12),
)
self.chat_subtitle.grid(row=1, column=0, padx=16, pady=(0, 10), sticky="e")
self.mode_badge = RTLLabel( self.mode_badge = RTLLabel(
self.header_card, self.header_card,
text='عادی', text='عادی',
@ -901,7 +887,35 @@ class SecureSmsApp(ctk.CTk):
pady=6, pady=6,
font=ctk.CTkFont(family=FONT_BODY, size=12, weight="bold"), font=ctk.CTkFont(family=FONT_BODY, size=12, weight="bold"),
) )
self.mode_badge.grid(row=0, column=1, rowspan=2, padx=14, sticky="e") self.mode_badge.grid(row=0, column=0, rowspan=2, padx=14, sticky="w")
self.chat_title = RTLLabel(
self.header_card,
text='یک مخاطب را انتخاب کن',
text_color=TEXT,
font=ctk.CTkFont(family=FONT_TITLE, size=16 if self.is_portrait else 18, weight="bold"),
)
self.chat_title.grid(row=0, column=1, padx=8, pady=(10, 2), sticky="e")
self.chat_subtitle = RTLLabel(
self.header_card,
text='در اینجا فقط دو حالت داری: عادی یا امن',
text_color=MUTED,
font=ctk.CTkFont(family=FONT_BODY, size=12),
)
self.chat_subtitle.grid(row=1, column=1, padx=8, pady=(0, 10), sticky="e")
RTLButton(
self.header_card,
text='بازگشت',
width=64,
height=36,
corner_radius=18,
fg_color=INPUT_BG,
hover_color=BORDER,
text_color=TEXT,
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
command=self._show_home_screen
).grid(row=0, column=2, rowspan=2, padx=(4, 14), sticky="e")
content = ctk.CTkFrame(self.main_panel, fg_color="transparent") content = ctk.CTkFrame(self.main_panel, fg_color="transparent")
content.grid(row=1, column=0, padx=outer_pad, pady=(0, inner_pad), sticky="nsew") content.grid(row=1, column=0, padx=outer_pad, pady=(0, inner_pad), sticky="nsew")
@ -1031,6 +1045,20 @@ class SecureSmsApp(ctk.CTk):
self.overlay_frame.grid_remove() self.overlay_frame.grid_remove()
self.refresh_all() self.refresh_all()
self._show_home_screen()
def _show_home_screen(self):
self.current_contact_phone = None
self._hide_virtual_keyboard()
self.main_panel.grid_remove()
self.sidebar.grid(row=0, column=0, sticky="nsew")
self._refresh_contacts()
def _show_chat_screen(self):
self._hide_virtual_keyboard()
self.sidebar.grid_remove()
self.main_panel.grid(row=0, column=0, sticky="nsew")
self._refresh_current_chat()
def refresh_all(self): def refresh_all(self):
self._refresh_connection_badge() self._refresh_connection_badge()
@ -1042,11 +1070,11 @@ class SecureSmsApp(ctk.CTk):
self.refresh_all() self.refresh_all()
def _refresh_connection_badge(self): def _refresh_connection_badge(self):
modem = self.controller.modem_status() if hasattr(self, 'drawer_modem_label') and self.drawer_modem_label.winfo_exists():
if modem["connected"]: modem = self.controller.modem_status()
self.connection_badge.configure(text=f"مودم متصل | {modem['port']}", fg_color="#2E7D62", text_color="white") text = f"وضعیت مودم: {'متصل' if modem['connected'] else 'آفلاین'} | پورت: {modem['port']}"
else: color = "#2E7D62" if modem['connected'] else "#B6465F"
self.connection_badge.configure(text=f"مودم آفلاین | {modem['port']}", fg_color="#9A6C3C", text_color="white") self.drawer_modem_label.configure(text=text, text_color=color)
def _refresh_contacts(self): def _refresh_contacts(self):
for widget in self.contacts_frame.winfo_children(): for widget in self.contacts_frame.winfo_children():
@ -1056,30 +1084,83 @@ class SecureSmsApp(ctk.CTk):
RTLLabel( RTLLabel(
self.contacts_frame, self.contacts_frame,
text='هنوز مخاطبی اضافه نشده است.', text='هنوز مخاطبی اضافه نشده است.',
text_color="#D5E8E1", text_color=MUTED,
font=ctk.CTkFont(family=FONT_BODY, size=16), font=ctk.CTkFont(family=FONT_BODY, size=16),
).grid(row=0, column=0, padx=12, pady=12, sticky="e") ).grid(row=0, column=0, padx=12, pady=12, sticky="e")
return return
colors = ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#00BCD4", "#009688", "#4CAF50", "#FF9800", "#FF5722"]
for index, contact in enumerate(contacts): for index, contact in enumerate(contacts):
selected = self.current_contact_phone == contact.phone card = ctk.CTkFrame(self.contacts_frame, fg_color="transparent", corner_radius=12)
card = RTLButton( card.grid(row=index, column=0, padx=4, pady=4, sticky="ew")
self.contacts_frame, self.contacts_frame.grid_columnconfigure(0, weight=1)
text=f"{contact.name}\n{contact.phone}\n{contact.last_message_preview or 'آماده گفتگو'}", card.grid_columnconfigure(0, weight=1)
anchor="e", card.grid_columnconfigure(1, weight=0)
height=72 if self.is_portrait else 80,
corner_radius=8,
command=lambda phone=contact.phone: self._select_contact(phone),
fg_color=SURFACE if selected else "transparent",
hover_color=BORDER,
text_color=TEXT if selected else "white",
font=ctk.CTkFont(family=FONT_BODY, size=14),
)
card.grid(row=index, column=0, padx=4, pady=2, sticky="ew")
def make_on_press(p=contact.phone, n=contact.name):
def press_handler(e):
self._long_press_triggered = False
self._long_press_timer = self.after(800, lambda: self._trigger_long_press(p, n))
return press_handler
def make_on_release():
def release_handler(e):
if hasattr(self, "_long_press_timer") and self._long_press_timer:
self.after_cancel(self._long_press_timer)
self._long_press_timer = None
return release_handler
def make_onclick(p=contact.phone):
def click_handler(e):
if getattr(self, "_long_press_triggered", False):
return
self.after(10, lambda: self._select_contact(p))
return click_handler
info_frame = ctk.CTkFrame(card, fg_color="transparent")
info_frame.grid(row=0, column=0, sticky="ew", padx=(10, 14), pady=10)
RTLLabel(info_frame, text=contact.name, text_color=TEXT, font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold")).pack(anchor="e")
preview = contact.last_message_preview or 'آماده گفتگو'
preview_str = preview if len(preview) < 32 else preview[:29] + "..."
RTLLabel(info_frame, text=preview_str, text_color=MUTED, font=ctk.CTkFont(family=FONT_BODY, size=14)).pack(anchor="e", pady=(4, 0))
avatar_color = colors[hash(contact.name or contact.phone) % len(colors)]
avatar_size = 52
avatar = ctk.CTkFrame(card, fg_color=avatar_color, corner_radius=avatar_size // 2, width=avatar_size, height=avatar_size)
avatar.grid(row=0, column=1, padx=(0, 10), pady=10)
avatar.grid_propagate(False)
first_letter = (contact.name or contact.phone)[0].upper()
RTLLabel(avatar, text=first_letter, text_color="white", font=ctk.CTkFont(family=FONT_TITLE, size=22, weight="bold")).place(relx=0.5, rely=0.5, anchor="center")
card.bind("<ButtonPress-1>", make_on_press(), add="+")
card.bind("<ButtonRelease-1>", make_on_release(), add="+")
card.bind("<Button-1>", make_onclick())
info_frame.bind("<ButtonPress-1>", make_on_press(), add="+")
info_frame.bind("<ButtonRelease-1>", make_on_release(), add="+")
info_frame.bind("<Button-1>", make_onclick())
avatar.bind("<ButtonPress-1>", make_on_press(), add="+")
avatar.bind("<ButtonRelease-1>", make_on_release(), add="+")
avatar.bind("<Button-1>", make_onclick())
for child in info_frame.winfo_children():
child.bind("<ButtonPress-1>", make_on_press(), add="+")
child.bind("<ButtonRelease-1>", make_on_release(), add="+")
child.bind("<Button-1>", make_onclick())
for child in avatar.winfo_children():
child.bind("<ButtonPress-1>", make_on_press(), add="+")
child.bind("<ButtonRelease-1>", make_on_release(), add="+")
child.bind("<Button-1>", make_onclick())
# Bind hover effects manually if needed, or rely on transparent bg
def _select_contact(self, phone: str): def _select_contact(self, phone: str):
self.current_contact_phone = phone self.current_contact_phone = phone
self._refresh_contacts() self._show_chat_screen()
self._refresh_current_chat()
def _refresh_current_chat(self): def _refresh_current_chat(self):
if not self.current_contact_phone: if not self.current_contact_phone:
@ -1218,19 +1299,18 @@ class SecureSmsApp(ctk.CTk):
self.refresh_all() self.refresh_all()
def _open_contact_dialog(self): def _open_contact_dialog(self):
if self.contact_form_card.winfo_ismapped():
self._hide_contact_form()
return
self.contact_form_message.configure(text="") self.contact_form_message.configure(text="")
self.contact_name_entry.delete(0, "end") self.contact_name_entry.delete(0, "end")
self.contact_phone_entry.delete(0, "end") self.contact_phone_entry.delete(0, "end")
self.contact_form_card.grid() self.sidebar.grid_remove()
self.add_contact_panel.grid(row=0, column=0, sticky="nsew")
self.add_contact_panel.lift()
self.after(80, lambda: self._focus_registered_input(self.contact_name_entry)) self.after(80, lambda: self._focus_registered_input(self.contact_name_entry))
def _hide_contact_form(self): def _hide_contact_form(self):
self._hide_virtual_keyboard() self._hide_virtual_keyboard()
self.contact_form_card.grid_remove() self.add_contact_panel.grid_remove()
self.contact_form_message.configure(text="") self.sidebar.grid(row=0, column=0, sticky="nsew")
def _save_contact_inline(self): def _save_contact_inline(self):
name = self.contact_name_entry.get().strip() name = self.contact_name_entry.get().strip()
@ -1245,7 +1325,86 @@ class SecureSmsApp(ctk.CTk):
self.contact_phone_entry.delete(0, "end") self.contact_phone_entry.delete(0, "end")
self._hide_virtual_keyboard() self._hide_virtual_keyboard()
self.refresh_all() self.refresh_all()
self.after(900, self._hide_contact_form) self.after(200, self._hide_contact_form)
def _trigger_long_press(self, phone, name):
self._long_press_triggered = True
self._long_press_timer = None
self._show_delete_contact_dialog(phone, name)
def _show_delete_contact_dialog(self, phone, name):
self._show_overlay()
header = self._build_overlay_header('حذف مخاطب', f"آیا از حذف گفتگو با {name} مطمئن هستید؟ تمام پیام‌ها پاک خواهد شد.")
header.pack(fill="x", padx=16, pady=(16, 10))
body = ctk.CTkFrame(self.overlay_frame, fg_color=SURFACE, corner_radius=22)
body.pack(fill="both", expand=True, padx=16, pady=(0, 16))
def confirm_delete():
self.controller.delete_contact(phone)
self._hide_overlay()
self._show_home_screen()
RTLButton(
body,
text='بله، حذف کن',
fg_color=DANGER,
text_color="white",
hover_color="#9C3A4D",
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=48,
command=confirm_delete,
).pack(fill="x", padx=18, pady=(24, 8))
RTLButton(
body,
text='انصراف',
fg_color=INPUT_BG,
text_color=TEXT,
hover_color=BORDER,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=48,
command=self._hide_overlay,
).pack(fill="x", padx=18, pady=(8, 24))
def _show_drawer_menu(self):
self._show_overlay()
header = self._build_overlay_header('منو', 'گزینه‌های برنامه')
header.pack(fill="x", padx=16, pady=(16, 10))
body = ctk.CTkFrame(self.overlay_frame, fg_color=SURFACE, corner_radius=22)
body.pack(fill="both", expand=True, padx=16, pady=(0, 16))
self.drawer_modem_label = RTLLabel(
body,
text="",
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
justify="right"
)
self.drawer_modem_label.pack(anchor="e", padx=18, pady=(24, 14))
self._refresh_connection_badge()
RTLButton(
body,
text='تنظیمات',
fg_color=PRIMARY,
text_color="white",
hover_color=PRIMARY_DARK,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=48,
command=self._open_settings_panel,
).pack(fill="x", padx=18, pady=8)
RTLButton(
body,
text='بخش ادمین',
fg_color=INPUT_BG,
text_color=TEXT,
hover_color=BORDER,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=48,
command=self._open_admin_login,
).pack(fill="x", padx=18, pady=(8, 24))
def _open_settings_panel(self): def _open_settings_panel(self):
self._show_overlay() self._show_overlay()