From 03f27d7a45056753d956ba4ccc1d362ec1a86c64 Mon Sep 17 00:00:00 2001 From: MOJ1403 Date: Tue, 24 Mar 2026 12:12:08 +0330 Subject: [PATCH] fix send message v0 --- a.ps1 | 18 +- .../__pycache__/services.cpython-313.pyc | Bin 23440 -> 23781 bytes secure_sms/application/services.py | 3 +- .../__pycache__/database.cpython-313.pyc | Bin 0 -> 23788 bytes secure_sms/infrastructure/database.py | 6 +- secure_sms/infrastructure/gsm.py | 18 +- .../__pycache__/main_window.cpython-313.pyc | Bin 87220 -> 109548 bytes secure_sms/ui/main_window.py | 477 +++++++++++++----- 8 files changed, 381 insertions(+), 141 deletions(-) create mode 100644 secure_sms/infrastructure/__pycache__/database.cpython-313.pyc 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 b2e35e2706a72e5701c1b890a6778b6cf1765854..0e93f75729b92ccbfe6f1a4f92999e1f72bff052 100644 GIT binary patch delta 3057 zcmZuydu&tZ6~Ev0^&^h+AP?tZ$8j);aY!5pNnmMsgajrM+QrDob2wno}S*&S<9RVbY{y0S@Kp@Uwj0!`YqhbB;As`}r~`Rtgo z&60ojeCPX}$9K;6IPwnr%Q;qXr=Y+t;qS`U4-;>mDR9ed78>R2b0rabo2-^qOOGqO zR9&UwSan8XEhbzi^h^Io zzf|abst-LZ6JF3?WUgp2aAsxM(Dj16Ops}Pi_RN_zFz44!pZ&i7InE`Gzdlj8OUn0 zsEtDF&#r%k7%it|2Qde4cEz&VBqjoCGa3d_uc$3T)2OcGD@Mx$t@@9Z0Ta8b-?o}$ zn`%;}{=%dcy6qLvk`!a+U^^&L<=(CQFiP{ti= zOXEx6i^5t~Pc~Lb%?nH0{UkH{(QrA-hu7`la!!_poJ`NyTjJVuOFF$zT?t>>XIUBS z$$!ZvPo|8Dl$M6m;c~N*j7@0Zad=kRcrDuTCNfqgv~?Uc8?Lx01$1(1#E(-Aj)XIb}ld zdcE9>U)~2#S>15Dq=;?PFO*z0VVD=4q1yX>@HIru1SM%LC>6`vyGX1d=qK1l;33#V z(1Q@LXJT(Bg-Xy1k5#N?55lpEomSGO)-t$NT&0&)-ekV@s92CQCOc`8`mvZO`7>N_ zQgK>>Q7t(<0;{Xvuh@lJT%pKfH_DkThQaD;Eghq=M&KjpMt}!h$4W$I*{YC|C*a?% zFq?vYcL2JpoU9Y}47R}AF8cwDO7y{Ms?`C>GgJ2hC}Z^|B;+znCxAbU&y zrT=4ANQ*QqDQ$Q!*Kk$O1s=;kN+hviN8xUusZ8W>ig?y+*D%Aj#u@fDoNipOh%z0A zohuG(&1K79B_Ej?lyHNI=I6)a@#ut>O2x2{Vi%de(KHH?wShp>ciDOUVAJQUBbWbz zVkqf-Q`)^18FD(s-x8TkX~{VD2Y0`SItNFCr)*~^b{?OGbuCTq+@a#Xpg<0UTsPnc zExXty{jV)Px#C@#Jx3rso4FRl!oLUJ)?xNOJl)#Sex4?UZyTQ`*+M`oU6OMu^6i1k9Uis`{?QTh z&=LdL(tS*{%Ls;fgyW6MFG6BXnEg|~u%_9>X5e3)=b00J_JBY588L4Vh-MU|V$)g_ zZAK?F+|gHP;!gykfLDQaRkLTo-PPDALVS)>M&2Sl#V_JGMK2)Ugo{_qr!dvk#;h>k zb%|ZnC)WMW+%Bs5D{`CbBcG??ZxL_}nQoEdNdj?tUVyQl=dspb_iS?JYHj3wPa7Yf zjK+q?_&X5lJ(T|&GN4jobN<|Wq({`Ac2%Yyq6?yQ9@0_21KF2GEQl6hDDfCRHx|5! z5|c^Z4reznwOpf#AYDChXLBX1)*V~6D)~PlC*nz(hh2U1<)VkS#UAoM(DWIo?q5A{ zul+@&q!a?y%sv-^zalL~=RYD46`&)XvLc9TF4prpDzcgA{gvzy{rCM=Wy@=%wQJMZ zwWv4`bnFb)Y~;uB?M#fP=yvCHlLQn_WG*oT!Lnv4{0dYJ1iNTU81|6_Po<+NO-o{) z;tY$;ppccBz(OFlkCh~ue0~832M)25U=P*dHQE%aWj5Fr@-RD$g_`pVh$rfP7*2%- z7}xKHZZTH{sT8ss>BhF=!qPFvqE(LB>9T9bun)BgM>B7B3X> zY`Dpv16~eC*{kqX_%LgNgAe!3Jws$aK_fvqv9^%(5J4Y7gg_w>J4W22<20m)M;t&y z8Be6r+(zT)38+2%MS?tnHxO|DB!<)cB#Gwq-l=ho|6G#VCBsCIgif>%2|gk)2tFmC z9mc7-ypmEARjGyjLoRj`UKw(;18{a|U16DdvwXsLSEBFjrlUI!Ic{5+x#2|5T?s$> zp&e%#*1YSH8_UG=;v|1J348=z1jE)fF&W=GuC3>*V8f#umu#`HA^A&5d<5-3P}I_$ delta 2911 zcmZuyYj9K75!Si7x{_rZ%P+~;vLsu^Mt*^@05QSFK*1%@*7mJVZ+Fkx-E+?GudtIpWtAUSR@zPYyPdk0bY8A>ODqGO()+Wk)tZQ1t&dbi z>=rZ!W!2eeIwBuoyjGPXb&-0Zl~fmLxl^!ZWV;IN2BB{#=<6cO3;N|k--t1qf|A-O zzTASwxMPidlfvljS286b~ zu>Vf+)gkt6k9Z3kmej6RVOXhlqhk*mWVKgl0_rN>IbIj&Gn{a=*{NTbLuToM1kbMM z1xMJXueIG^&{yVy8zZ&wCvSzrX9~+kfRR!J=FKYp|s@7^!m9S~dPcmFt zQ!9^~0yem955wm2FR>bUwR~Q3W#N@ECp=W)>9le$&H?X6Fysj>7c`X^J2QF9D&h?mDrCZxhcNpG@as z@f@!3;jTy6CHT1Oq9STJ59hj{*il+7e}-b@Ehu648_2&rmPkY=v}`tp)gB^)XnyoAC<)UoJxtWZ%v?D{xW%&X{wo=Qni8#gi-oksU7HpR&MHxQ| zkFV}_mktMijuKTMlsW`&tUkzI(>we8l1nG+j|fC$3)f;;_@6+cKh9o&*ZbG4y+TG2 z+s2QQtPs!+`HKYqClh}O`h%m&duYy{gH?U?`gejKNOdAZ1&)E1oYoRi9(#yif>Y~b z?sG^CEXzl)rkEnSSojcBg*U$Z@OMT>L>GK)_ zZYJM9QqW1vpRuRl`Aj6#dfuCKe<(O zV~D;*heTgQ4aGU22?*Ho11R=I`!M8044(`IryNfv55nKKw@UArU@+VSpLbHD zi|y7OpRvl@Pb1H;Ytz`Es5lwoVqrZ-c>>?PWGY*_Qvxz4GXDpG zU|9ml{e%vnR~c6868U@E+cH82qFXQ$w3IKZq6 z-wHGKCY;&rcMnpUQXz|PiHky5;m+e{s5)2dU zBajJ(2*f2So-aDhhMY>`OCp|+<7BoHoFFJ8_$C5QNfNKYGbCDaQ<;>;FOY0brg!D| z_lfp0!D|G+BB1lY-ywLHpq5r8YS4(1Q6G(FbFo}9j&BV0X~w!Jpcdz 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 0000000000000000000000000000000000000000..228c0cf5f610b07a8b7f90dd55137a671d046080 GIT binary patch literal 23788 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 literal 0 HcmV?d00001 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 447d5ffc72c48f2df979defd68dd04f5268271fb..737a6c81b4885529b43ed7daf3e3e6dcdcfe323b 100644 GIT binary patch delta 40946 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 delta 25354 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 EKgDTLmH+?% 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,