revert color an aicon an better ui and ux
This commit is contained in:
parent
ee1bebcc90
commit
fe8a02b742
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
def make_on_press(p=contact.phone, n=contact.name):
|
||||||
command=lambda phone=contact.phone: self._select_contact(phone),
|
def press_handler(e):
|
||||||
fg_color=SURFACE if selected else "transparent",
|
self._long_press_triggered = False
|
||||||
hover_color=BORDER,
|
self._long_press_timer = self.after(800, lambda: self._trigger_long_press(p, n))
|
||||||
text_color=TEXT if selected else "white",
|
return press_handler
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=14),
|
|
||||||
)
|
def make_on_release():
|
||||||
card.grid(row=index, column=0, padx=4, pady=2, sticky="ew")
|
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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user