diff --git a/a.ps1 b/a.ps1 index 1372882..69c0076 100644 --- a/a.ps1 +++ b/a.ps1 @@ -3,7 +3,7 @@ # ----------------------------- $LocalPath = "C:\Users\Pars\Desktop\saba-python" # مسیر پروژه روی ویندوز $PiUser = "pars" # کاربر روی Raspberry Pi -$PiHost = "10.65.40.150" # آی‌پی Raspberry Pi +$PiHost = "192.168.1.25" # آی‌پی Raspberry Pi $RemotePath = "/home/pars/Desktop/" # مسیر پروژه روی Pi $MainPy = "saba-python/main.py" # فایل اصلی پایتون @@ -29,11 +29,19 @@ $command = "mkdir -p ~/.ssh; chmod 700 ~/.ssh; echo '$pubKey' >> ~/.ssh/authoriz ssh "$PiUser@$PiHost" $command # ----------------------------- -# انتقال پروژه با scp +# انتقال پروژه (بدون فایل‌های مخفی) # ----------------------------- -Write-Host "Transferring project to Raspberry Pi..." -$scpTarget = "$PiUser@$PiHost`:$RemotePath" -scp -r $LocalPath $scpTarget +Write-Host "Transferring project to Raspberry Pi (excluding .git)..." +$ParentDir = Split-Path $LocalPath -Parent +$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 diff --git a/secure_sms/application/__pycache__/services.cpython-313.pyc b/secure_sms/application/__pycache__/services.cpython-313.pyc index b2e35e2..0e93f75 100644 Binary files a/secure_sms/application/__pycache__/services.cpython-313.pyc and b/secure_sms/application/__pycache__/services.cpython-313.pyc differ diff --git a/secure_sms/application/services.py b/secure_sms/application/services.py index 68500a4..7e3b4c3 100644 --- a/secure_sms/application/services.py +++ b/secure_sms/application/services.py @@ -46,7 +46,8 @@ class SecureMessagingService: public_key_enc=self.cipher.encrypt_text(public_key), 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 = { "private_key": private_key, "public_key": public_key, diff --git a/secure_sms/infrastructure/__pycache__/database.cpython-313.pyc b/secure_sms/infrastructure/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..228c0cf Binary files /dev/null and b/secure_sms/infrastructure/__pycache__/database.cpython-313.pyc differ diff --git a/secure_sms/infrastructure/database.py b/secure_sms/infrastructure/database.py index 0351d7a..bcc0696 100644 --- a/secure_sms/infrastructure/database.py +++ b/secure_sms/infrastructure/database.py @@ -137,7 +137,11 @@ class Database: return row["value"] if row else default 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") return port, baudrate diff --git a/secure_sms/infrastructure/gsm.py b/secure_sms/infrastructure/gsm.py index f821f19..0d807bf 100644 --- a/secure_sms/infrastructure/gsm.py +++ b/secure_sms/infrastructure/gsm.py @@ -98,34 +98,44 @@ class GSMGateway(IMessageGateway): def _send_single_sms(self, phone: str, body: str) -> bool: with self.lock: try: + print(f"[GSM] Sending SMS to {phone}, payload_len={len(body)}") self.serial_conn.reset_input_buffer() self.serial_conn.write(f'AT+CMGS="{phone}"\r\n'.encode("ascii")) start = time.time() prompt_ready = False - while time.time() - start < 2: + while time.time() - start < 5.0: if self.serial_conn.in_waiting: char = self.serial_conn.read().decode("ascii", errors="ignore") if char == ">": prompt_ready = True + print("[GSM] Received '>' prompt.") break time.sleep(0.05) if not prompt_ready: + print("[GSM] Error: Did not receive '>' prompt in time. Canceling.") self.serial_conn.write(chr(27).encode("ascii")) return False self.serial_conn.write(body.encode("ascii") + chr(26).encode("ascii")) start = time.time() - while time.time() - start < 10: + while time.time() - start < 45.0: if self.serial_conn.in_waiting: line = self.serial_conn.readline().decode("ascii", errors="ignore").strip() + if line: + print(f"[GSM] Modem response: {line}") if "OK" in line: + print("[GSM] SMS sent successfully.") return True if "ERROR" in line: + print("[GSM] SMS encountered an ERROR.") return False else: time.sleep(0.1) - except Exception: + print("[GSM] SMS Send Timed Out waiting for OK/ERROR (45s).") return False - return False + except Exception as e: + print(f"[GSM] Exception during SMS transmission: {e}") + return False + def _read_loop(self): while self.is_running: diff --git a/secure_sms/ui/__pycache__/main_window.cpython-313.pyc b/secure_sms/ui/__pycache__/main_window.cpython-313.pyc index 447d5ff..737a6c8 100644 Binary files a/secure_sms/ui/__pycache__/main_window.cpython-313.pyc and b/secure_sms/ui/__pycache__/main_window.cpython-313.pyc differ diff --git a/secure_sms/ui/main_window.py b/secure_sms/ui/main_window.py index 1b094a0..4dd30dd 100644 --- a/secure_sms/ui/main_window.py +++ b/secure_sms/ui/main_window.py @@ -16,11 +16,11 @@ ctk.set_appearance_mode("light") ctk.set_default_color_theme("blue") -PRIMARY = "#175B4B" -PRIMARY_DARK = "#0E4236" -PRIMARY_SOFT = "#DFF1E8" -ACCENT = "#E8A04D" -ACCENT_DARK = "#C97E2D" +PRIMARY = "#00B4D8" +PRIMARY_DARK = "#0096C7" +PRIMARY_SOFT = "#E6F7FA" +ACCENT = "#48CAE4" +ACCENT_DARK = "#0077B6" BACKGROUND = "#F5EFE7" CARD = "#FFFDFC" SURFACE = "#FBF7F2" @@ -46,21 +46,21 @@ KEYBOARD_LAYOUTS = { ['۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹', '۰'], ['ض', 'ص', 'ث', 'ق', 'ف', 'غ', 'ع', 'ه', 'خ', 'ح', 'ج', 'چ'], ['ش', 'س', 'ی', 'ب', 'ل', 'ا', 'ت', 'ن', 'م', 'ک', 'گ'], - ['ظ', 'ط', 'ز', 'ر', 'ذ', 'د', 'پ', 'و', '⌫'], - ['123', 'انگلیسی', '،', 'فاصله', '.', 'تایید'], + ['ظ', 'ط', 'ز', 'ر', 'ذ', 'د', 'پ', 'و', '⌫ پاک'], + ['123', 'انگلیسی', '،', 'فاصله (Space)', '.', 'تایید ⏎'], ], "en": [ ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], - ['z', 'x', 'c', 'v', 'b', 'n', 'm', '⌫'], - ['123', 'فارسی', ',', 'فاصله', '.', 'تایید'], + ['z', 'x', 'c', 'v', 'b', 'n', 'm', '⌫ Delete'], + ['123', 'فارسی', ',', ' Space ', '.', 'Enter ⏎'], ], "numeric": [ ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], ['+', '-', '/', '@', '_', '.', ':', '(', ')', '?'], - ['فارسی', 'انگلیسی', '⌫'], - ['بستن', 'فاصله', 'تایید'], + ['فارسی', 'انگلیسی', '⌫ Delete'], + ['بستن', 'فاصله (Space)', 'تایید ⏎'], ], } KEYBOARD_ACTIONS = [ @@ -117,14 +117,74 @@ class RTLEntry(_RTLTextMixin, ctk.CTkEntry): kwargs.setdefault("border_color", BORDER) kwargs.setdefault("corner_radius", 16) 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("", 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): - 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): + 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("", 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): - 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): @@ -239,6 +299,11 @@ class SecureSmsApp(ctk.CTk): self._input_aliases[str(bind_target)] = widget bind_target.bind("", lambda _event, target=widget: self._activate_text_input(target), add="+") bind_target.bind("", 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("", lambda _event, target=widget: self.after(10, lambda: self._activate_text_input(target)), add="+") + widget.bind("", lambda _event, target=widget: self.after(10, lambda: self._activate_text_input(target)), add="+") def _focus_registered_input(self, widget): if widget not in self._text_inputs or not widget.winfo_exists(): @@ -328,6 +393,10 @@ class SecureSmsApp(ctk.CTk): text = target.get("1.0", "end-1c") else: text = target.get() + + if hasattr(widget, "_sync_display"): + widget._sync_display(text) + text = text.replace('\n', ' ') if len(text) > 40: text = "..." + text[-37:] @@ -342,7 +411,7 @@ class SecureSmsApp(ctk.CTk): self._refresh_keyboard_action_bar() self._update_keyboard_preview() 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._render_keyboard(self.current_keyboard_layout) self.after(0, self._hide_mouse_cursor) @@ -350,7 +419,8 @@ class SecureSmsApp(ctk.CTk): def _hide_virtual_keyboard(self): self.active_input = None 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): if self.active_input in self._text_inputs: @@ -359,14 +429,13 @@ class SecureSmsApp(ctk.CTk): def _keyboard_weight(self, key): return { - 'فاصله': 50, - "Space": 50, - 'تایید': 20, - "Enter": 20, + 'فاصله (Space)': 60, + " Space ": 60, + 'تایید ⏎': 20, + "Enter ⏎": 20, 'بستن': 15, - 'حذف': 15, - "Back": 15, - '⌫': 15, + '⌫ پاک': 25, + "⌫ Delete": 25, 'فارسی': 15, "English": 15, 'انگلیسی': 15, @@ -374,9 +443,9 @@ class SecureSmsApp(ctk.CTk): }.get(key, 10) def _keyboard_style(self, key): - if key in {'تایید', "Enter"}: + if key in {'تایید ⏎', "Enter ⏎"}: 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_FACE, "#F0F0F0", KEY_TEXT @@ -407,7 +476,7 @@ class SecureSmsApp(ctk.CTk): key_height = 44 if self.compact_mode else 40 key_font_size = 17 if self.compact_mode else 15 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) for column_index, key in enumerate(row): row_frame.grid_columnconfigure(column_index, weight=self._keyboard_weight(key)) @@ -415,6 +484,7 @@ class SecureSmsApp(ctk.CTk): RTLButton( row_frame, text=key, + width=20, height=key_height, corner_radius=6, fg_color=fg_color, @@ -434,16 +504,16 @@ class SecureSmsApp(ctk.CTk): if key in {'۱۲۳', "123"}: self._show_virtual_keyboard("numeric") return - if key in {'فاصله', "Space"}: + if key in {'فاصله', 'فاصله (Space)', " Space ", "Space"}: self._insert_into_active_input(" ") return - if key in {'حذف', "Back", '⌫'}: + if key in {'⌫ پاک', "⌫ Delete"}: self._backspace_active_input() return if key == 'بستن': self._hide_virtual_keyboard() return - if key in {'تایید', "Enter"}: + if key in {'تایید ⏎', "Enter ⏎"}: self._submit_active_input() return self._insert_into_active_input(key) @@ -459,6 +529,8 @@ class SecureSmsApp(ctk.CTk): if len(selection) == 2: target.delete(selection[0], selection[1]) target.insert("insert", text) + target.tag_add("right_align", "1.0", "end") + target.tag_configure("right_align", justify="right") target.see("insert") else: try: @@ -467,6 +539,10 @@ class SecureSmsApp(ctk.CTk): except Exception: pass target.insert("insert", text) + try: + target.configure(justify="right") + except Exception: + pass target.focus_set() self._update_keyboard_preview() 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.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.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_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_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_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_columnconfigure(0, weight=1) self._refresh_keyboard_action_bar() 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): self.active_input = None @@ -670,10 +747,12 @@ class SecureSmsApp(ctk.CTk): frame, text=action_text, height=44, - corner_radius=8, - fg_color=PRIMARY, - hover_color=PRIMARY_DARK, - text_color="#FFFFFF", + corner_radius=22, + fg_color=BACKGROUND, + hover_color=PRIMARY_SOFT, + text_color=PRIMARY_DARK, + border_color=PRIMARY, + border_width=2, command=self._submit_lock_screen, font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), ).pack(fill="x", padx=36, pady=(4, 16)) @@ -686,6 +765,7 @@ class SecureSmsApp(ctk.CTk): def _configure_root_layout(self): self.root_frame.grid_columnconfigure(0, weight=1) + self.root_frame.grid_columnconfigure(1, weight=0) self.root_frame.grid_rowconfigure(0, weight=1) def _configure_profile_card_layout(self): @@ -759,21 +839,21 @@ class SecureSmsApp(ctk.CTk): self.sidebar.grid_columnconfigure(0, 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_columnconfigure(0, weight=1) sidebar_header.grid_columnconfigure(1, weight=0) RTLButton( sidebar_header, - text='☰', - width=40, + text='منو', + width=50, height=40, corner_radius=20, - fg_color="transparent", + fg_color=SIDEBAR, hover_color=BORDER, 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 ).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.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_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") @@ -840,11 +920,13 @@ class SecureSmsApp(ctk.CTk): RTLButton( self.contact_form_card, text='ذخیره', - fg_color=PRIMARY, - text_color="white", - hover_color=PRIMARY_DARK, + corner_radius=24, + fg_color=BACKGROUND, + hover_color=PRIMARY_SOFT, + text_color=PRIMARY_DARK, + border_color=PRIMARY, + border_width=2, command=self._save_contact_inline, - corner_radius=12, font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), height=48, ).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_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.sidebar, height=300 if self.is_portrait else 320, label_text='گفتگو\u200cها', 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") @@ -903,6 +1055,13 @@ class SecureSmsApp(ctk.CTk): font=ctk.CTkFont(family=FONT_BODY, size=12), ) 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("", open_contact_page, add="+") + self.chat_subtitle.bind("", open_contact_page, add="+") RTLButton( self.header_card, @@ -917,7 +1076,7 @@ class SecureSmsApp(ctk.CTk): 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=BACKGROUND, corner_radius=0) content.grid(row=1, column=0, padx=outer_pad, pady=(0, inner_pad), sticky="nsew") if self.is_portrait: content.grid_rowconfigure(0, weight=1) @@ -930,7 +1089,7 @@ class SecureSmsApp(ctk.CTk): self.chat_container = RTLScrollableFrame( content, - fg_color="transparent" + fg_color=BACKGROUND ) if self.is_portrait: 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.profile_card, text='فعال\u200cسازی ارتباط امن', - fg_color=PRIMARY, - hover_color=PRIMARY_DARK, - text_color="#FFFFFF", - corner_radius=8, + corner_radius=21, + fg_color=CARD, + hover_color=PRIMARY_SOFT, + text_color=PRIMARY_DARK, + border_color=PRIMARY, + border_width=2, font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), command=self._toggle_secure_mode, height=42, @@ -984,10 +1145,12 @@ class SecureSmsApp(ctk.CTk): self.normal_button = RTLButton( self.profile_card, text='بازگشت به حالت عادی', - fg_color=INPUT_BG, + corner_radius=20, + fg_color=CARD, + hover_color="#F4F4F4", text_color=TEXT, - hover_color=BORDER, - corner_radius=8, + border_color=BORDER, + border_width=2, font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), command=self._switch_to_normal, height=40, @@ -1034,16 +1197,21 @@ class SecureSmsApp(ctk.CTk): self._register_text_input(self.message_entry, title="متن پیام", layout="fa", multiline=True) self.overlay_frame = ctk.CTkFrame( - self.main_panel, + self.root_frame, fg_color=CARD, corner_radius=12, border_width=1, 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_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._show_home_screen() @@ -1051,6 +1219,8 @@ class SecureSmsApp(ctk.CTk): self.current_contact_phone = None self._hide_virtual_keyboard() 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._refresh_contacts() @@ -1067,7 +1237,9 @@ class SecureSmsApp(ctk.CTk): self.after(0, self._hide_mouse_cursor) 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): 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"] 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") self.contacts_frame.grid_columnconfigure(0, weight=1) card.grid_columnconfigure(0, weight=1) 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 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 = ctk.CTkFrame(card, fg_color=SIDEBAR, corner_radius=0) 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") @@ -1135,25 +1292,13 @@ class SecureSmsApp(ctk.CTk): 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("", make_on_press(), add="+") - card.bind("", make_on_release(), add="+") card.bind("", make_onclick()) - - info_frame.bind("", make_on_press(), add="+") - info_frame.bind("", make_on_release(), add="+") info_frame.bind("", make_onclick()) - - avatar.bind("", make_on_press(), add="+") - avatar.bind("", make_on_release(), add="+") avatar.bind("", make_onclick()) for child in info_frame.winfo_children(): - child.bind("", make_on_press(), add="+") - child.bind("", make_on_release(), add="+") child.bind("", make_onclick()) for child in avatar.winfo_children(): - child.bind("", make_on_press(), add="+") - child.bind("", make_on_release(), add="+") child.bind("", make_onclick()) # 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") - 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, text=message.body, text_color=TEXT, font=ctk.CTkFont(family=FONT_BODY, size=15), wraplength=max(180, int(self.window_width * 0.5)), 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 - RTLLabel( + badge_label = RTLLabel( bubble, text=badge_text, text_color=MUTED, font=ctk.CTkFont(family=FONT_BODY, size=10), 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("", toggle_fn, add="+") + status_label.bind("", toggle_fn, add="+") + text_label.bind("", toggle_fn, add="+") + badge_label.bind("", toggle_fn, add="+") try: 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: self.contact_form_message.configure(text='نام و شماره هر دو لازم هستند.') 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.current_contact_phone = phone self.contact_form_message.configure(text='مخاطب ذخیره شد.') @@ -1348,9 +1533,12 @@ class SecureSmsApp(ctk.CTk): RTLButton( body, text='بله، حذف کن', - fg_color=DANGER, - text_color="white", - hover_color="#9C3A4D", + corner_radius=24, + fg_color=SURFACE, + 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=confirm_delete, @@ -1368,43 +1556,72 @@ class SecureSmsApp(ctk.CTk): ).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._hide_virtual_keyboard() + self.sidebar.grid_remove() + self.drawer_panel.grid(row=0, column=0, sticky="nsew") + self.drawer_panel.lift() self._refresh_connection_badge() + def _hide_drawer_menu(self): + self.drawer_panel.grid_remove() + self.sidebar.grid(row=0, column=0, sticky="nsew") + + def _show_contact_details_page(self): + self._hide_virtual_keyboard() + self.main_panel.grid_remove() + self.contact_details_panel.grid(row=0, column=0, sticky="nsew") + self.contact_details_panel.lift() + + 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( - 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) + 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_details_page + ).grid(row=0, column=1, padx=(4, 14), pady=(10, 10), sticky="e") + + RTLLabel( + header, text='پروفایل مخاطب', text_color=TEXT, + 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='بخش ادمین', - 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)) + 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): self._show_overlay() @@ -1435,8 +1652,8 @@ class SecureSmsApp(ctk.CTk): RTLButton( body, text='ورود به پنل ادمین', - fg_color=PRIMARY, - hover_color=PRIMARY_DARK, + corner_radius=21, + 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"), height=42, command=self._open_admin_login,