fix send message v0

This commit is contained in:
MOJ1403 2026-03-24 12:12:08 +03:30
parent fe8a02b742
commit 03f27d7a45
8 changed files with 381 additions and 141 deletions

18
a.ps1
View File

@ -3,7 +3,7 @@
# ----------------------------- # -----------------------------
$LocalPath = "C:\Users\Pars\Desktop\saba-python" # مسیر پروژه روی ویندوز $LocalPath = "C:\Users\Pars\Desktop\saba-python" # مسیر پروژه روی ویندوز
$PiUser = "pars" # کاربر روی Raspberry Pi $PiUser = "pars" # کاربر روی Raspberry Pi
$PiHost = "10.65.40.150" # آی‌پی Raspberry Pi $PiHost = "192.168.1.25" # آی‌پی Raspberry Pi
$RemotePath = "/home/pars/Desktop/" # مسیر پروژه روی Pi $RemotePath = "/home/pars/Desktop/" # مسیر پروژه روی Pi
$MainPy = "saba-python/main.py" # فایل اصلی پایتون $MainPy = "saba-python/main.py" # فایل اصلی پایتون
@ -29,11 +29,19 @@ $command = "mkdir -p ~/.ssh; chmod 700 ~/.ssh; echo '$pubKey' >> ~/.ssh/authoriz
ssh "$PiUser@$PiHost" $command ssh "$PiUser@$PiHost" $command
# ----------------------------- # -----------------------------
# انتقال پروژه با scp # انتقال پروژه (بدون فایل‌های مخفی)
# ----------------------------- # -----------------------------
Write-Host "Transferring project to Raspberry Pi..." Write-Host "Transferring project to Raspberry Pi (excluding .git)..."
$scpTarget = "$PiUser@$PiHost`:$RemotePath" $ParentDir = Split-Path $LocalPath -Parent
scp -r $LocalPath $scpTarget $FolderName = Split-Path $LocalPath -Leaf
$DeployTar = "$ParentDir\deploy.tar"
Set-Location $ParentDir
tar.exe -cf deploy.tar --exclude=".git" --exclude="__pycache__" $FolderName
scp deploy.tar "$PiUser@$PiHost`:$RemotePath/deploy.tar"
ssh "$PiUser@$PiHost" "cd $RemotePath && tar -xf deploy.tar && rm deploy.tar"
Remove-Item $DeployTar
Set-Location $LocalPath
# ----------------------------- # -----------------------------
# اجرای برنامه روی Raspberry Pi # اجرای برنامه روی Raspberry Pi

View File

@ -46,7 +46,8 @@ class SecureMessagingService:
public_key_enc=self.cipher.encrypt_text(public_key), public_key_enc=self.cipher.encrypt_text(public_key),
fingerprint=fingerprint, fingerprint=fingerprint,
) )
self.db.set_connection_settings("COM1", 115200) import os
self.db.set_connection_settings("/dev/ttyS0" if os.name != "nt" else "COM1", 115200)
self.identity = { self.identity = {
"private_key": private_key, "private_key": private_key,
"public_key": public_key, "public_key": public_key,

View File

@ -137,7 +137,11 @@ class Database:
return row["value"] if row else default return row["value"] if row else default
def get_connection_settings(self) -> tuple[str, int]: def get_connection_settings(self) -> tuple[str, int]:
port = self.get_config("gsm_port", "COM1") or "COM1" import os
default_port = "/dev/ttyS0" if os.name != "nt" else "COM1"
port = self.get_config("gsm_port")
if not port or port == "COM1":
port = default_port
baudrate = int(self.get_config("gsm_baudrate", "115200") or "115200") baudrate = int(self.get_config("gsm_baudrate", "115200") or "115200")
return port, baudrate return port, baudrate

View File

@ -98,34 +98,44 @@ class GSMGateway(IMessageGateway):
def _send_single_sms(self, phone: str, body: str) -> bool: def _send_single_sms(self, phone: str, body: str) -> bool:
with self.lock: with self.lock:
try: try:
print(f"[GSM] Sending SMS to {phone}, payload_len={len(body)}")
self.serial_conn.reset_input_buffer() self.serial_conn.reset_input_buffer()
self.serial_conn.write(f'AT+CMGS="{phone}"\r\n'.encode("ascii")) self.serial_conn.write(f'AT+CMGS="{phone}"\r\n'.encode("ascii"))
start = time.time() start = time.time()
prompt_ready = False prompt_ready = False
while time.time() - start < 2: while time.time() - start < 5.0:
if self.serial_conn.in_waiting: if self.serial_conn.in_waiting:
char = self.serial_conn.read().decode("ascii", errors="ignore") char = self.serial_conn.read().decode("ascii", errors="ignore")
if char == ">": if char == ">":
prompt_ready = True prompt_ready = True
print("[GSM] Received '>' prompt.")
break break
time.sleep(0.05) time.sleep(0.05)
if not prompt_ready: if not prompt_ready:
print("[GSM] Error: Did not receive '>' prompt in time. Canceling.")
self.serial_conn.write(chr(27).encode("ascii")) self.serial_conn.write(chr(27).encode("ascii"))
return False return False
self.serial_conn.write(body.encode("ascii") + chr(26).encode("ascii")) self.serial_conn.write(body.encode("ascii") + chr(26).encode("ascii"))
start = time.time() start = time.time()
while time.time() - start < 10: while time.time() - start < 45.0:
if self.serial_conn.in_waiting: if self.serial_conn.in_waiting:
line = self.serial_conn.readline().decode("ascii", errors="ignore").strip() line = self.serial_conn.readline().decode("ascii", errors="ignore").strip()
if line:
print(f"[GSM] Modem response: {line}")
if "OK" in line: if "OK" in line:
print("[GSM] SMS sent successfully.")
return True return True
if "ERROR" in line: if "ERROR" in line:
print("[GSM] SMS encountered an ERROR.")
return False return False
else: else:
time.sleep(0.1) time.sleep(0.1)
except Exception: print("[GSM] SMS Send Timed Out waiting for OK/ERROR (45s).")
return False return False
return False except Exception as e:
print(f"[GSM] Exception during SMS transmission: {e}")
return False
def _read_loop(self): def _read_loop(self):
while self.is_running: while self.is_running:

View File

@ -16,11 +16,11 @@ ctk.set_appearance_mode("light")
ctk.set_default_color_theme("blue") ctk.set_default_color_theme("blue")
PRIMARY = "#175B4B" PRIMARY = "#00B4D8"
PRIMARY_DARK = "#0E4236" PRIMARY_DARK = "#0096C7"
PRIMARY_SOFT = "#DFF1E8" PRIMARY_SOFT = "#E6F7FA"
ACCENT = "#E8A04D" ACCENT = "#48CAE4"
ACCENT_DARK = "#C97E2D" ACCENT_DARK = "#0077B6"
BACKGROUND = "#F5EFE7" BACKGROUND = "#F5EFE7"
CARD = "#FFFDFC" CARD = "#FFFDFC"
SURFACE = "#FBF7F2" SURFACE = "#FBF7F2"
@ -46,21 +46,21 @@ KEYBOARD_LAYOUTS = {
['۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹', '۰'], ['۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹', '۰'],
['ض', 'ص', 'ث', 'ق', 'ف', 'غ', 'ع', 'ه', 'خ', 'ح', 'ج', 'چ'], ['ض', 'ص', 'ث', 'ق', 'ف', 'غ', 'ع', 'ه', 'خ', 'ح', 'ج', 'چ'],
['ش', 'س', 'ی', 'ب', 'ل', 'ا', 'ت', 'ن', 'م', 'ک', 'گ'], ['ش', 'س', 'ی', 'ب', 'ل', 'ا', 'ت', 'ن', 'م', 'ک', 'گ'],
['ظ', 'ط', 'ز', 'ر', 'ذ', 'د', 'پ', 'و', ''], ['ظ', 'ط', 'ز', 'ر', 'ذ', 'د', 'پ', 'و', ' پاک'],
['123', 'انگلیسی', '،', 'فاصله', '.', 'تایید'], ['123', 'انگلیسی', '،', 'فاصله (Space)', '.', 'تایید'],
], ],
"en": [ "en": [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
['z', 'x', 'c', 'v', 'b', 'n', 'm', ''], ['z', 'x', 'c', 'v', 'b', 'n', 'm', ' Delete'],
['123', 'فارسی', ',', 'فاصله', '.', 'تایید'], ['123', 'فارسی', ',', ' Space ', '.', 'Enter ⏎'],
], ],
"numeric": [ "numeric": [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['+', '-', '/', '@', '_', '.', ':', '(', ')', '?'], ['+', '-', '/', '@', '_', '.', ':', '(', ')', '?'],
['فارسی', 'انگلیسی', ''], ['فارسی', 'انگلیسی', ' Delete'],
['بستن', 'فاصله', 'تایید'], ['بستن', 'فاصله (Space)', 'تایید'],
], ],
} }
KEYBOARD_ACTIONS = [ KEYBOARD_ACTIONS = [
@ -118,13 +118,73 @@ class RTLEntry(_RTLTextMixin, ctk.CTkEntry):
kwargs.setdefault("corner_radius", 16) kwargs.setdefault("corner_radius", 16)
super().__init__(*args, **self._normalize_kwargs(kwargs)) super().__init__(*args, **self._normalize_kwargs(kwargs))
bg_color = kwargs.get("fg_color", INPUT_BG)
try:
self._entry.configure(fg=bg_color, insertbackground=bg_color, selectbackground=bg_color)
except Exception:
pass
self._display_label = ctk.CTkLabel(
self, text="", text_color=TEXT, justify="right",
font=kwargs.get("font", ctk.CTkFont(family=FONT_BODY, size=15))
)
self._display_label.place(relwidth=0.9, relheight=0.9, relx=0.95, rely=0.5, anchor="e")
self.bind("<KeyRelease>", lambda e: self._sync_display(), add="+")
self._sync_display()
def _sync_display(self, text=None):
raw = text if text is not None else self.get()
show_char = self.cget("show")
if raw:
display_text = show_char * len(raw) if show_char else ui_text(raw)
self._display_label.configure(text=display_text + " |")
else:
placeholder = self.cget("placeholder_text") or ""
self._display_label.configure(text=ui_text(placeholder))
def configure(self, require_redraw=False, **kwargs): def configure(self, require_redraw=False, **kwargs):
return super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs)) res = super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs))
if "placeholder_text" in kwargs:
self._sync_display()
return res
class RTLTextbox(ctk.CTkTextbox): class RTLTextbox(ctk.CTkTextbox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
bg_color = kwargs.get("fg_color", INPUT_BG)
try:
self._textbox.configure(fg=bg_color, insertbackground=bg_color, selectbackground=bg_color)
self._textbox.tag_configure("right_align", justify="right")
except Exception:
pass
self._display_label = ctk.CTkLabel(
self, text="", text_color=TEXT, justify="right",
font=kwargs.get("font", ctk.CTkFont(family=FONT_BODY, size=15)),
anchor="ne"
)
self._display_label.place(relwidth=0.9, relheight=0.9, relx=0.95, rely=0.08, anchor="ne")
self.bind("<KeyRelease>", lambda e: self._sync_display(), add="+")
self._sync_display()
def _sync_display(self, text=None):
raw = text if text is not None else self.get("1.0", "end-1c")
if raw:
self._display_label.configure(text=ui_text(raw) + " |")
else:
self._display_label.configure(text=" |")
def insert(self, index, text, *tags): def insert(self, index, text, *tags):
return super().insert(index, ui_text(text), *tags) res = super().insert(index, text, *tags)
self._textbox.tag_add("right_align", "1.0", "end")
self._sync_display()
return res
def delete(self, index1, index2=None):
res = super().delete(index1, index2)
self._sync_display()
return res
class RTLScrollableFrame(_RTLTextMixin, ctk.CTkScrollableFrame): class RTLScrollableFrame(_RTLTextMixin, ctk.CTkScrollableFrame):
@ -240,6 +300,11 @@ class SecureSmsApp(ctk.CTk):
bind_target.bind("<FocusIn>", lambda _event, target=widget: self._activate_text_input(target), add="+") bind_target.bind("<FocusIn>", lambda _event, target=widget: self._activate_text_input(target), add="+")
bind_target.bind("<Button-1>", lambda _event, target=widget: self.after(10, lambda: self._activate_text_input(target)), add="+") bind_target.bind("<Button-1>", lambda _event, target=widget: self.after(10, lambda: self._activate_text_input(target)), add="+")
if hasattr(widget, "_display_label"):
self._input_aliases[str(widget._display_label)] = widget
widget._display_label.bind("<Button-1>", lambda _event, target=widget: self.after(10, lambda: self._activate_text_input(target)), add="+")
widget.bind("<Button-1>", lambda _event, target=widget: self.after(10, lambda: self._activate_text_input(target)), add="+")
def _focus_registered_input(self, widget): def _focus_registered_input(self, widget):
if widget not in self._text_inputs or not widget.winfo_exists(): if widget not in self._text_inputs or not widget.winfo_exists():
return return
@ -328,6 +393,10 @@ class SecureSmsApp(ctk.CTk):
text = target.get("1.0", "end-1c") text = target.get("1.0", "end-1c")
else: else:
text = target.get() text = target.get()
if hasattr(widget, "_sync_display"):
widget._sync_display(text)
text = text.replace('\n', ' ') text = text.replace('\n', ' ')
if len(text) > 40: if len(text) > 40:
text = "..." + text[-37:] text = "..." + text[-37:]
@ -342,7 +411,7 @@ class SecureSmsApp(ctk.CTk):
self._refresh_keyboard_action_bar() self._refresh_keyboard_action_bar()
self._update_keyboard_preview() self._update_keyboard_preview()
self._shift_layout_for_keyboard(True) self._shift_layout_for_keyboard(True)
if not self.keyboard_host.winfo_ismapped(): if hasattr(self, 'keyboard_host') and not self.keyboard_host.winfo_ismapped():
self.keyboard_host.grid() self.keyboard_host.grid()
self._render_keyboard(self.current_keyboard_layout) self._render_keyboard(self.current_keyboard_layout)
self.after(0, self._hide_mouse_cursor) self.after(0, self._hide_mouse_cursor)
@ -350,7 +419,8 @@ class SecureSmsApp(ctk.CTk):
def _hide_virtual_keyboard(self): def _hide_virtual_keyboard(self):
self.active_input = None self.active_input = None
self._shift_layout_for_keyboard(False) self._shift_layout_for_keyboard(False)
self.keyboard_host.grid_remove() if hasattr(self, 'keyboard_host'):
self.keyboard_host.grid_remove()
def _keyboard_title(self): def _keyboard_title(self):
if self.active_input in self._text_inputs: if self.active_input in self._text_inputs:
@ -359,14 +429,13 @@ class SecureSmsApp(ctk.CTk):
def _keyboard_weight(self, key): def _keyboard_weight(self, key):
return { return {
'فاصله': 50, 'فاصله (Space)': 60,
"Space": 50, " Space ": 60,
'تایید': 20, 'تایید': 20,
"Enter": 20, "Enter": 20,
'بستن': 15, 'بستن': 15,
'حذف': 15, '⌫ پاک': 25,
"Back": 15, "⌫ Delete": 25,
'': 15,
'فارسی': 15, 'فارسی': 15,
"English": 15, "English": 15,
'انگلیسی': 15, 'انگلیسی': 15,
@ -374,9 +443,9 @@ class SecureSmsApp(ctk.CTk):
}.get(key, 10) }.get(key, 10)
def _keyboard_style(self, key): def _keyboard_style(self, key):
if key in {'تایید', "Enter"}: if key in {'تایید', "Enter"}:
return PRIMARY, PRIMARY_DARK, "white" return PRIMARY, PRIMARY_DARK, "white"
if key in {"ABC", 'فا', '۱۲۳', "123", "English", 'انگلیسی', 'فارسی', 'حذف', 'بستن', "Back", ''}: if key in {"ABC", 'فا', '۱۲۳', "123", "English", 'انگلیسی', 'فارسی', 'بستن', "⌫ Delete", ' پاک'}:
return KEY_MUTED, "#A8AFB9", KEY_TEXT return KEY_MUTED, "#A8AFB9", KEY_TEXT
return KEY_FACE, "#F0F0F0", KEY_TEXT return KEY_FACE, "#F0F0F0", KEY_TEXT
@ -407,7 +476,7 @@ class SecureSmsApp(ctk.CTk):
key_height = 44 if self.compact_mode else 40 key_height = 44 if self.compact_mode else 40
key_font_size = 17 if self.compact_mode else 15 key_font_size = 17 if self.compact_mode else 15
for row_index, row in enumerate(KEYBOARD_LAYOUTS.get(layout_name, KEYBOARD_LAYOUTS["fa"])): for row_index, row in enumerate(KEYBOARD_LAYOUTS.get(layout_name, KEYBOARD_LAYOUTS["fa"])):
row_frame = ctk.CTkFrame(self.keyboard_keys, fg_color="transparent") row_frame = ctk.CTkFrame(self.keyboard_keys, fg_color=KEYBOARD_BG, corner_radius=0)
row_frame.grid(row=row_index, column=0, sticky="ew", pady=2 if self.compact_mode else 3) row_frame.grid(row=row_index, column=0, sticky="ew", pady=2 if self.compact_mode else 3)
for column_index, key in enumerate(row): for column_index, key in enumerate(row):
row_frame.grid_columnconfigure(column_index, weight=self._keyboard_weight(key)) row_frame.grid_columnconfigure(column_index, weight=self._keyboard_weight(key))
@ -415,6 +484,7 @@ class SecureSmsApp(ctk.CTk):
RTLButton( RTLButton(
row_frame, row_frame,
text=key, text=key,
width=20,
height=key_height, height=key_height,
corner_radius=6, corner_radius=6,
fg_color=fg_color, fg_color=fg_color,
@ -434,16 +504,16 @@ class SecureSmsApp(ctk.CTk):
if key in {'۱۲۳', "123"}: if key in {'۱۲۳', "123"}:
self._show_virtual_keyboard("numeric") self._show_virtual_keyboard("numeric")
return return
if key in {'فاصله', "Space"}: if key in {'فاصله', 'فاصله (Space)', " Space ", "Space"}:
self._insert_into_active_input(" ") self._insert_into_active_input(" ")
return return
if key in {'حذف', "Back", ''}: if key in {'⌫ پاک', "⌫ Delete"}:
self._backspace_active_input() self._backspace_active_input()
return return
if key == 'بستن': if key == 'بستن':
self._hide_virtual_keyboard() self._hide_virtual_keyboard()
return return
if key in {'تایید', "Enter"}: if key in {'تایید', "Enter"}:
self._submit_active_input() self._submit_active_input()
return return
self._insert_into_active_input(key) self._insert_into_active_input(key)
@ -459,6 +529,8 @@ class SecureSmsApp(ctk.CTk):
if len(selection) == 2: if len(selection) == 2:
target.delete(selection[0], selection[1]) target.delete(selection[0], selection[1])
target.insert("insert", text) target.insert("insert", text)
target.tag_add("right_align", "1.0", "end")
target.tag_configure("right_align", justify="right")
target.see("insert") target.see("insert")
else: else:
try: try:
@ -467,6 +539,10 @@ class SecureSmsApp(ctk.CTk):
except Exception: except Exception:
pass pass
target.insert("insert", text) target.insert("insert", text)
try:
target.configure(justify="right")
except Exception:
pass
target.focus_set() target.focus_set()
self._update_keyboard_preview() self._update_keyboard_preview()
except TclError: except TclError:
@ -523,7 +599,7 @@ class SecureSmsApp(ctk.CTk):
self.root_frame = ctk.CTkFrame(self, fg_color=BACKGROUND, corner_radius=0) self.root_frame = ctk.CTkFrame(self, fg_color=BACKGROUND, corner_radius=0)
self.root_frame.grid(row=0, column=0, sticky="nsew") self.root_frame.grid(row=0, column=0, sticky="nsew")
self.root_frame.grid_columnconfigure(1, weight=1) self.root_frame.grid_columnconfigure(0, weight=1)
self.root_frame.grid_rowconfigure(0, weight=1) self.root_frame.grid_rowconfigure(0, weight=1)
self.keyboard_host = ctk.CTkFrame(self, fg_color=BACKGROUND, corner_radius=0) self.keyboard_host = ctk.CTkFrame(self, fg_color=BACKGROUND, corner_radius=0)
@ -540,7 +616,7 @@ class SecureSmsApp(ctk.CTk):
self.keyboard_frame.grid(row=0, column=0, sticky="ew", padx=0, pady=0) self.keyboard_frame.grid(row=0, column=0, sticky="ew", padx=0, pady=0)
self.keyboard_frame.grid_columnconfigure(0, weight=1) self.keyboard_frame.grid_columnconfigure(0, weight=1)
keyboard_header = ctk.CTkFrame(self.keyboard_frame, fg_color="transparent") keyboard_header = ctk.CTkFrame(self.keyboard_frame, fg_color=KEYBOARD_BG, corner_radius=0)
keyboard_header.grid(row=0, column=0, sticky="ew", padx=10, pady=(8, 4)) keyboard_header.grid(row=0, column=0, sticky="ew", padx=10, pady=(8, 4))
keyboard_header.grid_columnconfigure(0, weight=1) keyboard_header.grid_columnconfigure(0, weight=1)
@ -577,15 +653,16 @@ class SecureSmsApp(ctk.CTk):
) )
self.keyboard_hint.grid(row=1, column=0, padx=14, pady=(0, 2), sticky="e") self.keyboard_hint.grid(row=1, column=0, padx=14, pady=(0, 2), sticky="e")
self.keyboard_action_bar = ctk.CTkFrame(self.keyboard_frame, fg_color="transparent", height=0) self.keyboard_action_bar = ctk.CTkFrame(self.keyboard_frame, fg_color=KEYBOARD_BG, height=0, corner_radius=0)
self.keyboard_keys = ctk.CTkFrame(self.keyboard_frame, fg_color="transparent") self.keyboard_keys = ctk.CTkFrame(self.keyboard_frame, fg_color=KEYBOARD_BG, corner_radius=0)
self.keyboard_keys.grid(row=2, column=0, sticky="ew", padx=6, pady=(2, 12)) self.keyboard_keys.grid(row=2, column=0, sticky="ew", padx=6, pady=(2, 12))
self.keyboard_keys.grid_columnconfigure(0, weight=1) self.keyboard_keys.grid_columnconfigure(0, weight=1)
self._refresh_keyboard_action_bar() self._refresh_keyboard_action_bar()
self._render_keyboard(self.current_keyboard_layout) self._render_keyboard(self.current_keyboard_layout)
self.keyboard_host.grid_remove() if hasattr(self, 'keyboard_host'):
self.keyboard_host.grid_remove()
def _reset_text_input_registry(self): def _reset_text_input_registry(self):
self.active_input = None self.active_input = None
@ -670,10 +747,12 @@ class SecureSmsApp(ctk.CTk):
frame, frame,
text=action_text, text=action_text,
height=44, height=44,
corner_radius=8, corner_radius=22,
fg_color=PRIMARY, fg_color=BACKGROUND,
hover_color=PRIMARY_DARK, hover_color=PRIMARY_SOFT,
text_color="#FFFFFF", text_color=PRIMARY_DARK,
border_color=PRIMARY,
border_width=2,
command=self._submit_lock_screen, command=self._submit_lock_screen,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
).pack(fill="x", padx=36, pady=(4, 16)) ).pack(fill="x", padx=36, pady=(4, 16))
@ -686,6 +765,7 @@ class SecureSmsApp(ctk.CTk):
def _configure_root_layout(self): def _configure_root_layout(self):
self.root_frame.grid_columnconfigure(0, weight=1) self.root_frame.grid_columnconfigure(0, weight=1)
self.root_frame.grid_columnconfigure(1, weight=0)
self.root_frame.grid_rowconfigure(0, weight=1) self.root_frame.grid_rowconfigure(0, weight=1)
def _configure_profile_card_layout(self): def _configure_profile_card_layout(self):
@ -759,21 +839,21 @@ class SecureSmsApp(ctk.CTk):
self.sidebar.grid_columnconfigure(0, weight=1) self.sidebar.grid_columnconfigure(0, weight=1)
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=SIDEBAR, corner_radius=0)
sidebar_header.grid(row=0, column=0, padx=14, pady=(12, 10), 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) sidebar_header.grid_columnconfigure(1, weight=0)
RTLButton( RTLButton(
sidebar_header, sidebar_header,
text='', text='منو',
width=40, width=50,
height=40, height=40,
corner_radius=20, corner_radius=20,
fg_color="transparent", fg_color=SIDEBAR,
hover_color=BORDER, hover_color=BORDER,
text_color="white", text_color="white",
font=ctk.CTkFont(family=FONT_TITLE, size=24, weight="bold"), font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
command=self._show_drawer_menu command=self._show_drawer_menu
).grid(row=0, column=1, padx=(6, 0), sticky="e") ).grid(row=0, column=1, padx=(6, 0), sticky="e")
@ -832,7 +912,7 @@ class SecureSmsApp(ctk.CTk):
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 = 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_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 = RTLEntry(self.contact_form_card, placeholder_text='شماره موبایل (مثبت 09)', 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_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 = 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") self.contact_form_message.grid(row=2, column=0, pady=(2, 6), sticky="e")
@ -840,11 +920,13 @@ class SecureSmsApp(ctk.CTk):
RTLButton( RTLButton(
self.contact_form_card, self.contact_form_card,
text='ذخیره', text='ذخیره',
fg_color=PRIMARY, corner_radius=24,
text_color="white", fg_color=BACKGROUND,
hover_color=PRIMARY_DARK, hover_color=PRIMARY_SOFT,
text_color=PRIMARY_DARK,
border_color=PRIMARY,
border_width=2,
command=self._save_contact_inline, command=self._save_contact_inline,
corner_radius=12,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=48, height=48,
).grid(row=3, column=0, pady=10, sticky="ew") ).grid(row=3, column=0, pady=10, sticky="ew")
@ -853,12 +935,82 @@ class SecureSmsApp(ctk.CTk):
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.contact_phone_entry, title="شماره موبایل", layout="numeric", submit=self._save_contact_inline) self._register_text_input(self.contact_phone_entry, title="شماره موبایل", layout="numeric", submit=self._save_contact_inline)
# Drawer Menu Panel (Separate Page)
self.drawer_panel = ctk.CTkFrame(self.root_frame, fg_color=BACKGROUND, corner_radius=0)
self.drawer_panel.grid_rowconfigure(1, weight=1)
self.drawer_panel.grid_columnconfigure(0, weight=1)
drawer_header = ctk.CTkFrame(self.drawer_panel, fg_color=CARD, corner_radius=0, border_width=0)
drawer_header.grid(row=0, column=0, sticky="ew")
drawer_header.grid_columnconfigure(0, weight=1)
drawer_header.grid_columnconfigure(1, weight=0)
RTLLabel(
drawer_header,
text='منوی اصلی',
text_color=TEXT,
font=ctk.CTkFont(family=FONT_TITLE, size=18, weight="bold"),
).grid(row=0, column=0, padx=16, pady=14, sticky="e")
RTLButton(
drawer_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_drawer_menu
).grid(row=0, column=1, padx=(4, 14), sticky="e")
drawer_body = ctk.CTkFrame(self.drawer_panel, fg_color=BACKGROUND, corner_radius=0)
drawer_body.grid(row=1, column=0, sticky="nsew", padx=20, pady=20)
drawer_body.grid_columnconfigure(0, weight=1)
self.drawer_modem_label = RTLLabel(
drawer_body,
text="",
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
justify="right"
)
self.drawer_modem_label.grid(row=0, column=0, pady=(10, 30), sticky="e")
RTLButton(
drawer_body,
text='تنظیمات',
corner_radius=27,
fg_color=BACKGROUND,
hover_color=PRIMARY_SOFT,
text_color=PRIMARY_DARK,
border_color=PRIMARY,
border_width=2,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=54,
command=self._open_settings_panel,
).grid(row=1, column=0, pady=8, sticky="ew")
RTLButton(
drawer_body,
text='بخش ادمین',
corner_radius=27,
fg_color=BACKGROUND,
hover_color="#F4F4F4",
text_color=TEXT,
border_color=BORDER,
border_width=2,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=54,
command=self._open_admin_login,
).grid(row=2, column=0, pady=8, sticky="ew")
self.contacts_frame = RTLScrollableFrame( self.contacts_frame = RTLScrollableFrame(
self.sidebar, self.sidebar,
height=300 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=SIDEBAR,
) )
self.contacts_frame.grid(row=1, column=0, padx=8, pady=4, sticky="nsew") self.contacts_frame.grid(row=1, column=0, padx=8, pady=4, sticky="nsew")
@ -904,6 +1056,13 @@ class SecureSmsApp(ctk.CTk):
) )
self.chat_subtitle.grid(row=1, column=1, padx=8, pady=(0, 10), sticky="e") self.chat_subtitle.grid(row=1, column=1, padx=8, pady=(0, 10), sticky="e")
def open_contact_page(e=None):
if self.current_contact_phone:
self._show_contact_details_page()
self.chat_title.bind("<Button-1>", open_contact_page, add="+")
self.chat_subtitle.bind("<Button-1>", open_contact_page, add="+")
RTLButton( RTLButton(
self.header_card, self.header_card,
text='بازگشت', text='بازگشت',
@ -917,7 +1076,7 @@ class SecureSmsApp(ctk.CTk):
command=self._show_home_screen command=self._show_home_screen
).grid(row=0, column=2, rowspan=2, padx=(4, 14), sticky="e") ).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=BACKGROUND, corner_radius=0)
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")
if self.is_portrait: if self.is_portrait:
content.grid_rowconfigure(0, weight=1) content.grid_rowconfigure(0, weight=1)
@ -930,7 +1089,7 @@ class SecureSmsApp(ctk.CTk):
self.chat_container = RTLScrollableFrame( self.chat_container = RTLScrollableFrame(
content, content,
fg_color="transparent" fg_color=BACKGROUND
) )
if self.is_portrait: if self.is_portrait:
self.chat_container.grid(row=0, column=0, sticky="nsew", pady=(0, inner_pad)) self.chat_container.grid(row=0, column=0, sticky="nsew", pady=(0, inner_pad))
@ -972,10 +1131,12 @@ class SecureSmsApp(ctk.CTk):
self.secure_button = RTLButton( self.secure_button = RTLButton(
self.profile_card, self.profile_card,
text='فعال\u200cسازی ارتباط امن', text='فعال\u200cسازی ارتباط امن',
fg_color=PRIMARY, corner_radius=21,
hover_color=PRIMARY_DARK, fg_color=CARD,
text_color="#FFFFFF", hover_color=PRIMARY_SOFT,
corner_radius=8, text_color=PRIMARY_DARK,
border_color=PRIMARY,
border_width=2,
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
command=self._toggle_secure_mode, command=self._toggle_secure_mode,
height=42, height=42,
@ -984,10 +1145,12 @@ class SecureSmsApp(ctk.CTk):
self.normal_button = RTLButton( self.normal_button = RTLButton(
self.profile_card, self.profile_card,
text='بازگشت به حالت عادی', text='بازگشت به حالت عادی',
fg_color=INPUT_BG, corner_radius=20,
fg_color=CARD,
hover_color="#F4F4F4",
text_color=TEXT, text_color=TEXT,
hover_color=BORDER, border_color=BORDER,
corner_radius=8, border_width=2,
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
command=self._switch_to_normal, command=self._switch_to_normal,
height=40, height=40,
@ -1034,16 +1197,21 @@ class SecureSmsApp(ctk.CTk):
self._register_text_input(self.message_entry, title="متن پیام", layout="fa", multiline=True) self._register_text_input(self.message_entry, title="متن پیام", layout="fa", multiline=True)
self.overlay_frame = ctk.CTkFrame( self.overlay_frame = ctk.CTkFrame(
self.main_panel, self.root_frame,
fg_color=CARD, fg_color=CARD,
corner_radius=12, corner_radius=12,
border_width=1, border_width=1,
border_color=BORDER, border_color=BORDER,
) )
self.overlay_frame.grid(row=0, column=0, rowspan=3, padx=outer_pad, pady=outer_pad, sticky="nsew") self.overlay_frame.grid(row=0, column=0, sticky="nsew")
self.overlay_frame.grid_columnconfigure(0, weight=1) self.overlay_frame.grid_columnconfigure(0, weight=1)
self.overlay_frame.grid_remove() self.overlay_frame.grid_remove()
self.contact_details_panel = ctk.CTkFrame(self.root_frame, fg_color=BACKGROUND, corner_radius=0)
self.contact_details_panel.grid_rowconfigure(1, weight=1)
self.contact_details_panel.grid_columnconfigure(0, weight=1)
self.contact_details_panel.grid_remove()
self.refresh_all() self.refresh_all()
self._show_home_screen() self._show_home_screen()
@ -1051,6 +1219,8 @@ class SecureSmsApp(ctk.CTk):
self.current_contact_phone = None self.current_contact_phone = None
self._hide_virtual_keyboard() self._hide_virtual_keyboard()
self.main_panel.grid_remove() self.main_panel.grid_remove()
if hasattr(self, 'contact_details_panel'):
self.contact_details_panel.grid_remove()
self.sidebar.grid(row=0, column=0, sticky="nsew") self.sidebar.grid(row=0, column=0, sticky="nsew")
self._refresh_contacts() self._refresh_contacts()
@ -1067,7 +1237,9 @@ class SecureSmsApp(ctk.CTk):
self.after(0, self._hide_mouse_cursor) self.after(0, self._hide_mouse_cursor)
def handle_background_refresh(self, phone=None): def handle_background_refresh(self, phone=None):
self.refresh_all() if hasattr(self, '_refresh_timer') and self._refresh_timer:
self.after_cancel(self._refresh_timer)
self._refresh_timer = self.after(300, self.refresh_all)
def _refresh_connection_badge(self): def _refresh_connection_badge(self):
if hasattr(self, 'drawer_modem_label') and self.drawer_modem_label.winfo_exists(): if hasattr(self, 'drawer_modem_label') and self.drawer_modem_label.winfo_exists():
@ -1092,33 +1264,18 @@ class SecureSmsApp(ctk.CTk):
colors = ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#00BCD4", "#009688", "#4CAF50", "#FF9800", "#FF5722"] colors = ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#00BCD4", "#009688", "#4CAF50", "#FF9800", "#FF5722"]
for index, contact in enumerate(contacts): for index, contact in enumerate(contacts):
card = ctk.CTkFrame(self.contacts_frame, fg_color="transparent", corner_radius=12) card = ctk.CTkFrame(self.contacts_frame, fg_color=SIDEBAR, corner_radius=12)
card.grid(row=index, column=0, padx=4, pady=4, sticky="ew") card.grid(row=index, column=0, padx=4, pady=4, sticky="ew")
self.contacts_frame.grid_columnconfigure(0, weight=1) self.contacts_frame.grid_columnconfigure(0, weight=1)
card.grid_columnconfigure(0, weight=1) card.grid_columnconfigure(0, weight=1)
card.grid_columnconfigure(1, weight=0) card.grid_columnconfigure(1, weight=0)
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 make_onclick(p=contact.phone):
def click_handler(e): def click_handler(e):
if getattr(self, "_long_press_triggered", False):
return
self.after(10, lambda: self._select_contact(p)) self.after(10, lambda: self._select_contact(p))
return click_handler return click_handler
info_frame = ctk.CTkFrame(card, fg_color="transparent") info_frame = ctk.CTkFrame(card, fg_color=SIDEBAR, corner_radius=0)
info_frame.grid(row=0, column=0, sticky="ew", padx=(10, 14), pady=10) 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") RTLLabel(info_frame, text=contact.name, text_color=TEXT, font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold")).pack(anchor="e")
@ -1135,25 +1292,13 @@ class SecureSmsApp(ctk.CTk):
first_letter = (contact.name or contact.phone)[0].upper() 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") 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()) 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()) 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()) avatar.bind("<Button-1>", make_onclick())
for child in info_frame.winfo_children(): 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()) child.bind("<Button-1>", make_onclick())
for child in avatar.winfo_children(): 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()) child.bind("<Button-1>", make_onclick())
# Bind hover effects manually if needed, or rely on transparent bg # Bind hover effects manually if needed, or rely on transparent bg
@ -1232,23 +1377,55 @@ class SecureSmsApp(ctk.CTk):
) )
bubble.pack(anchor=anchor, padx=8, pady=3, fill="none") bubble.pack(anchor=anchor, padx=8, pady=3, fill="none")
RTLLabel( state_val = getattr(message, 'transport_state', 'unknown').lower()
if state_val in ["sent"]: state_text = "وضعیت: ارسال شده ✓"
elif state_val in ["delivered", "read"]: state_text = "وضعیت: تحویل داده شده ✓✓"
elif state_val in ["failed", "error"]: state_text = "وضعیت: ارسال ناموفق ✗"
elif state_val in ["queued", "pending"]: state_text = "وضعیت: در صف ارسال ⏳"
elif state_val in ["offline", "local"]: state_text = "وضعیت: ذخیره محلی 📴"
else: state_text = f"وضعیت: {state_val}"
status_label = RTLLabel(
bubble,
text=state_text,
text_color="#6B8E85" if is_out else MUTED,
font=ctk.CTkFont(family=FONT_BODY, size=11, weight="bold"),
justify="right"
)
text_label = RTLLabel(
bubble, bubble,
text=message.body, text=message.body,
text_color=TEXT, text_color=TEXT,
font=ctk.CTkFont(family=FONT_BODY, size=15), font=ctk.CTkFont(family=FONT_BODY, size=15),
wraplength=max(180, int(self.window_width * 0.5)), wraplength=max(180, int(self.window_width * 0.5)),
justify="right" justify="right"
).pack(padx=12, pady=(8, 2), anchor="e") )
text_label.pack(padx=12, pady=(8, 2), anchor="e")
badge_text = f"🛡️ {message.created_at}" if message.mode == "secure" else message.created_at badge_text = f"🛡️ {message.created_at}" if message.mode == "secure" else message.created_at
RTLLabel( badge_label = RTLLabel(
bubble, bubble,
text=badge_text, text=badge_text,
text_color=MUTED, text_color=MUTED,
font=ctk.CTkFont(family=FONT_BODY, size=10), font=ctk.CTkFont(family=FONT_BODY, size=10),
justify="right" justify="right"
).pack(padx=12, pady=(0, 6), anchor="w" if is_out else "e") )
badge_label.pack(padx=12, pady=(0, 6), anchor="w" if is_out else "e")
def make_toggle(lbl=status_label, is_out_dir=is_out):
def toggle(e):
if lbl.winfo_ismapped():
lbl.pack_forget()
else:
lbl.pack(padx=12, pady=(0, 8), anchor="w" if is_out_dir else "e")
return toggle
toggle_fn = make_toggle()
bubble.bind("<Button-1>", toggle_fn, add="+")
status_label.bind("<Button-1>", toggle_fn, add="+")
text_label.bind("<Button-1>", toggle_fn, add="+")
badge_label.bind("<Button-1>", toggle_fn, add="+")
try: try:
self.after(50, lambda: self.chat_container._parent_canvas.yview_moveto(1.0)) self.after(50, lambda: self.chat_container._parent_canvas.yview_moveto(1.0))
@ -1318,6 +1495,14 @@ class SecureSmsApp(ctk.CTk):
if not name or not phone: if not name or not phone:
self.contact_form_message.configure(text='نام و شماره هر دو لازم هستند.') self.contact_form_message.configure(text='نام و شماره هر دو لازم هستند.')
return return
persian_to_english = str.maketrans('۰۱۲۳۴۵۶۷۸۹', '0123456789')
phone = phone.translate(persian_to_english)
if len(phone) != 11 or not phone.isdigit() or not phone.startswith("09"):
self.contact_form_message.configure(text='شماره باید ۱۱ عدد باشد و با 09 شروع شود.')
return
self.controller.save_contact(name, phone) self.controller.save_contact(name, phone)
self.current_contact_phone = phone self.current_contact_phone = phone
self.contact_form_message.configure(text='مخاطب ذخیره شد.') self.contact_form_message.configure(text='مخاطب ذخیره شد.')
@ -1348,9 +1533,12 @@ class SecureSmsApp(ctk.CTk):
RTLButton( RTLButton(
body, body,
text='بله، حذف کن', text='بله، حذف کن',
fg_color=DANGER, corner_radius=24,
text_color="white", fg_color=SURFACE,
hover_color="#9C3A4D", hover_color="#FDE8E8",
text_color=DANGER,
border_color=DANGER,
border_width=2,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=48, height=48,
command=confirm_delete, command=confirm_delete,
@ -1368,43 +1556,72 @@ class SecureSmsApp(ctk.CTk):
).pack(fill="x", padx=18, pady=(8, 24)) ).pack(fill="x", padx=18, pady=(8, 24))
def _show_drawer_menu(self): def _show_drawer_menu(self):
self._show_overlay() self._hide_virtual_keyboard()
header = self._build_overlay_header('منو', 'گزینه‌های برنامه') self.sidebar.grid_remove()
header.pack(fill="x", padx=16, pady=(16, 10)) self.drawer_panel.grid(row=0, column=0, sticky="nsew")
self.drawer_panel.lift()
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() self._refresh_connection_badge()
RTLButton( def _hide_drawer_menu(self):
body, self.drawer_panel.grid_remove()
text='تنظیمات', self.sidebar.grid(row=0, column=0, sticky="nsew")
fg_color=PRIMARY,
text_color="white", def _show_contact_details_page(self):
hover_color=PRIMARY_DARK, self._hide_virtual_keyboard()
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), self.main_panel.grid_remove()
height=48, self.contact_details_panel.grid(row=0, column=0, sticky="nsew")
command=self._open_settings_panel, self.contact_details_panel.lift()
).pack(fill="x", padx=18, pady=8)
for child in self.contact_details_panel.winfo_children():
child.destroy()
header = ctk.CTkFrame(self.contact_details_panel, fg_color=CARD, corner_radius=0, border_width=0)
header.grid(row=0, column=0, sticky="ew")
header.grid_columnconfigure(0, weight=1)
RTLButton( RTLButton(
body, header, text='بازگشت', width=64, height=36, corner_radius=18,
text='بخش ادمین', fg_color=INPUT_BG, hover_color=BORDER, text_color=TEXT,
fg_color=INPUT_BG, font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
text_color=TEXT, command=self._hide_contact_details_page
hover_color=BORDER, ).grid(row=0, column=1, padx=(4, 14), pady=(10, 10), sticky="e")
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=48, RTLLabel(
command=self._open_admin_login, header, text='پروفایل مخاطب', text_color=TEXT,
).pack(fill="x", padx=18, pady=(8, 24)) font=ctk.CTkFont(family=FONT_TITLE, size=18, weight="bold"),
).grid(row=0, column=0, padx=8, sticky="e")
body = ctk.CTkFrame(self.contact_details_panel, fg_color=BACKGROUND, corner_radius=0)
body.grid(row=1, column=0, sticky="nsew", padx=20, pady=20)
body.grid_columnconfigure(0, weight=1)
contact = self.controller.get_contact(self.current_contact_phone)
if not contact:
return
colors = ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#00BCD4", "#009688", "#4CAF50", "#FF9800", "#FF5722"]
avatar_color = colors[hash(contact.name or contact.phone) % len(colors)]
avatar = ctk.CTkFrame(body, width=90, height=90, corner_radius=45, fg_color=avatar_color)
avatar.pack(pady=(40, 10))
avatar.pack_propagate(False)
initial = contact.name[0] if contact.name else "?"
RTLLabel(avatar, text=initial, text_color="white", font=ctk.CTkFont(family=FONT_TITLE, size=36, weight="bold")).place(relx=0.5, rely=0.5, anchor="center")
RTLLabel(body, text=contact.name, text_color=TEXT, font=ctk.CTkFont(family=FONT_TITLE, size=24, weight="bold")).pack(pady=(12, 2))
RTLLabel(body, text=contact.phone, text_color=MUTED, font=ctk.CTkFont(family=FONT_BODY, size=16)).pack(pady=(0, 30))
RTLButton(
body, text='پاک کردن پروفایل',
corner_radius=24,
fg_color=BACKGROUND, hover_color="#FDE8E8", text_color=DANGER, border_color=DANGER, border_width=2,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), height=48,
command=lambda: self._show_delete_contact_dialog(contact.phone, contact.name)
).pack(fill="x", padx=18, pady=(24, 8))
def _hide_contact_details_page(self):
self.contact_details_panel.grid_remove()
self._show_chat_screen()
def _open_settings_panel(self): def _open_settings_panel(self):
self._show_overlay() self._show_overlay()
@ -1435,8 +1652,8 @@ class SecureSmsApp(ctk.CTk):
RTLButton( RTLButton(
body, body,
text='ورود به پنل ادمین', text='ورود به پنل ادمین',
fg_color=PRIMARY, corner_radius=21,
hover_color=PRIMARY_DARK, fg_color=SURFACE, hover_color=PRIMARY_SOFT, text_color=PRIMARY_DARK, border_color=PRIMARY, border_width=2,
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
height=42, height=42,
command=self._open_admin_login, command=self._open_admin_login,