@@ -228,7 +228,7 @@ def _enum_proc(h, lparam):
228228FONT_ITALIC = os .path .join (SCRIPT_DIR , "Fonts" , "Nunito-Italic.ttf" )
229229FONT_SEMIBOLD = os .path .join (SCRIPT_DIR , "Fonts" , "Nunito-SemiBold.ttf" )
230230LICENSE_TEXT = os .path .join (SCRIPT_DIR , "LICENSE.txt" )
231- VERSION = "1.8.1 "
231+ VERSION = "1.9.0 "
232232# Load all the font files for Tkinter (on Windows)
233233if platform .system () == "Windows" :
234234 fonts = [FONT_REGULAR , FONT_MEDIUM , FONT_BOLD , FONT_LIGHT , FONT_ITALIC , FONT_SEMIBOLD ]
@@ -1757,10 +1757,12 @@ def update_strength(*args):
17571757 strength = self .evaluate_password_strength (pwd_var .get ())
17581758 colors = {"Weak" : "#ff0000" , "Medium" : "#ffa500" , "Strong" : "#00ff00" }
17591759 strength_label .configure (text = f"Strength: { strength } " , text_color = colors [strength ])
1760- pwd_var .trace ( "w " , update_strength )
1760+ pwd_var .trace_add ( "write " , update_strength )
17611761 update_strength () # Initial
17621762 # Password generator button
17631763 def generate_password ():
1764+ if not messagebox .askyesno ("Generate Password" , "This will overwrite the current password. Continue?" , parent = popup ):
1765+ return
17641766 length = 16 # Default; can add options popup
17651767 chars = string .ascii_letters + string .digits + string .punctuation
17661768 new_pwd = '' .join (secrets .choice (chars ) for _ in range (length ))
@@ -1777,27 +1779,137 @@ def toggle_pwd_entry():
17771779 new_icon_path = None
17781780 def upload_icon ():
17791781 file = filedialog .askopenfilename (filetypes = [("Images" , "*.png *.jpg *.jpeg *.ico" )])
1780- if file :
1781- try :
1782- pil_img = Image .open (file )
1783- # Auto-resize to 350x350
1784- pil_img = pil_img .resize ((350 , 350 ), Image .LANCZOS )
1785- orig_ext = os .path .splitext (file )[1 ].lower ()
1786- if orig_ext == '.ico' :
1787- orig_ext = '.png'
1788- dest = os .path .join (stored_icons_path , f"{ id_ } { orig_ext } " )
1789- format_to_save = 'JPEG' if orig_ext == '.jpg' or orig_ext == '.jpeg' else 'PNG'
1790- pil_img .save (dest , format = format_to_save )
1791- # Remove old icons with different extensions
1792- for other_ext in ['.png' , '.jpg' , '.jpeg' ]:
1793- if other_ext != orig_ext :
1794- old_path = os .path .join (stored_icons_path , f"{ id_ } { other_ext } " )
1795- if os .path .exists (old_path ):
1796- os .remove (old_path )
1797- new_icon_path = dest
1798- messagebox .showinfo ("Uploaded" , "Icon uploaded, resized, and saved!" , parent = popup )
1799- except Exception as e :
1800- messagebox .showerror ("Error" , f"Failed to process icon: { e } " , parent = popup )
1782+ if not file :
1783+ return
1784+ original_img = Image .open (file )
1785+ editor_popup = ctk .CTkToplevel (popup ) # 'popup' is the edit_card_popup window
1786+ editor_popup .title ("Crop and Zoom Image" )
1787+ editor_popup .configure (fg_color = BG )
1788+ editor_popup .resizable (False , False )
1789+ PasswordManager .set_window_icon (editor_popup )
1790+
1791+ canvas_size = 350
1792+ center = canvas_size / 2
1793+ zoom_level = 1.0
1794+ image_x = center
1795+ image_y = center
1796+ photo = None # To keep reference
1797+ start_pan_x = 0
1798+ start_pan_y = 0
1799+ start_image_x = 0
1800+ start_image_y = 0
1801+
1802+ canvas_frame = ctk .CTkFrame (editor_popup , fg_color = CARD )
1803+ canvas_frame .pack (pady = 10 , padx = 10 )
1804+ canvas = ctk .CTkCanvas (canvas_frame , width = canvas_size , height = canvas_size , bg = "gray" )
1805+ canvas .pack ()
1806+
1807+ def display_image ():
1808+ nonlocal photo
1809+ w , h = original_img .size
1810+ new_w = int (w * zoom_level )
1811+ new_h = int (h * zoom_level )
1812+ resized = original_img .resize ((new_w , new_h ), Image .LANCZOS )
1813+ photo = ImageTk .PhotoImage (resized )
1814+ canvas .delete ("all" )
1815+ canvas .create_image (image_x , image_y , image = photo , anchor = "center" )
1816+
1817+ display_image ()
1818+
1819+ def zoom_at (mx , my , scale ):
1820+ nonlocal zoom_level , image_x , image_y
1821+ old_zoom = zoom_level
1822+ zoom_level *= scale
1823+ zoom_level = max (0.1 , zoom_level )
1824+ image_x = mx - (mx - image_x ) * (zoom_level / old_zoom )
1825+ image_y = my - (my - image_y ) * (zoom_level / old_zoom )
1826+ display_image ()
1827+
1828+ def zoom_in_center ():
1829+ zoom_at (center , center , 1.2 )
1830+
1831+ def zoom_out_center ():
1832+ zoom_at (center , center , 1 / 1.2 )
1833+
1834+ def on_mouse_wheel (event ):
1835+ mx = canvas .winfo_pointerx () - canvas .winfo_rootx ()
1836+ my = canvas .winfo_pointery () - canvas .winfo_rooty ()
1837+ scale = 1.1 if event .delta > 0 else 1 / 1.1
1838+ zoom_at (mx , my , scale )
1839+
1840+ def start_pan (event ):
1841+ nonlocal start_pan_x , start_pan_y , start_image_x , start_image_y
1842+ start_pan_x = event .x
1843+ start_pan_y = event .y
1844+ start_image_x = image_x
1845+ start_image_y = image_y
1846+
1847+ def pan (event ):
1848+ nonlocal image_x , image_y
1849+ dx = event .x - start_pan_x
1850+ dy = event .y - start_pan_y
1851+ image_x = start_image_x + dx
1852+ image_y = start_image_y + dy
1853+ display_image ()
1854+
1855+ def reset_zoom ():
1856+ nonlocal zoom_level , image_x , image_y
1857+ zoom_level = 1.0
1858+ image_x = center
1859+ image_y = center
1860+ display_image ()
1861+
1862+ canvas .bind ("<MouseWheel>" , on_mouse_wheel )
1863+ canvas .bind ("<Button-4>" , lambda e : zoom_at (e .x , e .y , 1.1 ))
1864+ canvas .bind ("<Button-5>" , lambda e : zoom_at (e .x , e .y , 1 / 1.1 ))
1865+ canvas .bind ("<Button-1>" , start_pan )
1866+ canvas .bind ("<B1-Motion>" , pan )
1867+
1868+ btn_frame = ctk .CTkFrame (editor_popup , fg_color = BG )
1869+ btn_frame .pack (pady = 10 )
1870+ ctk .CTkButton (btn_frame , text = "+" , command = zoom_in_center , fg_color = ACCENT , text_color = BG , hover_color = ACCENT_DIM , width = 40 ).pack (side = "left" , padx = 5 )
1871+ ctk .CTkButton (btn_frame , text = "-" , command = zoom_out_center , fg_color = ACCENT , text_color = BG , hover_color = ACCENT_DIM , width = 40 ).pack (side = "left" , padx = 5 )
1872+ ctk .CTkButton (btn_frame , text = "Reset" , command = reset_zoom , fg_color = ACCENT , text_color = BG , hover_color = ACCENT_DIM , width = 80 ).pack (side = "left" , padx = 5 )
1873+
1874+ def apply_crop ():
1875+ w , h = original_img .size
1876+ orig_left = (w / 2 ) + (0 - image_x ) / zoom_level
1877+ orig_top = (h / 2 ) + (0 - image_y ) / zoom_level
1878+ orig_right = (w / 2 ) + (canvas_size - image_x ) / zoom_level
1879+ orig_bottom = (h / 2 ) + (canvas_size - image_y ) / zoom_level
1880+ crop_box = (max (0 , orig_left ), max (0 , orig_top ), min (w , orig_right ), min (h , orig_bottom ))
1881+ if crop_box [0 ] >= crop_box [2 ] or crop_box [1 ] >= crop_box [3 ]:
1882+ messagebox .showerror ("Error" , "No visible image area!" , parent = editor_popup )
1883+ return
1884+ cropped = original_img .crop (crop_box )
1885+ crop_w = crop_box [2 ] - crop_box [0 ]
1886+ crop_h = crop_box [3 ] - crop_box [1 ]
1887+ visible_w = crop_w * zoom_level
1888+ visible_h = crop_h * zoom_level
1889+ resized_cropped = cropped .resize ((int (visible_w ), int (visible_h )), Image .LANCZOS )
1890+ pos_left_in_canvas = image_x + (crop_box [0 ] - w / 2 ) * zoom_level
1891+ pos_top_in_canvas = image_y + (crop_box [1 ] - h / 2 ) * zoom_level
1892+ final_img = Image .new ("RGBA" , (canvas_size , canvas_size ), (0 , 0 , 0 , 0 ))
1893+ final_img .paste (resized_cropped , (int (pos_left_in_canvas ), int (pos_top_in_canvas )))
1894+ final_img = final_img .resize ((350 , 350 ), Image .LANCZOS ) # Ensure final size
1895+ orig_ext = os .path .splitext (file )[1 ].lower ()
1896+ if orig_ext == '.ico' :
1897+ orig_ext = '.png'
1898+ dest = os .path .join (stored_icons_path , f"{ id_ } { orig_ext } " )
1899+ format_to_save = 'JPEG' if orig_ext in ('.jpg' , '.jpeg' ) else 'PNG'
1900+ final_img .save (dest , format = format_to_save )
1901+ # Remove old icons with different extensions
1902+ for other_ext in ['.png' , '.jpg' , '.jpeg' ]:
1903+ if other_ext != orig_ext :
1904+ old_path = os .path .join (stored_icons_path , f"{ id_ } { other_ext } " )
1905+ if os .path .exists (old_path ):
1906+ os .remove (old_path )
1907+ messagebox .showinfo ("Success" , "Image cropped, resized, and saved!" , parent = editor_popup )
1908+ editor_popup .destroy ()
1909+
1910+ ctk .CTkButton (editor_popup , text = "Apply & Save" , command = apply_crop , fg_color = ACCENT , text_color = BG , hover_color = ACCENT_DIM , width = 200 ).pack (pady = 10 )
1911+ safe_modal (editor_popup )
1912+ popup .wait_window (editor_popup ) # 'popup' is the edit_card_popup
18011913 ctk .CTkButton (popup , text = "Upload Icon" , command = upload_icon , fg_color = ACCENT , text_color = BG ).pack (pady = (0 ,8 ))
18021914 # Auto-query Google Images
18031915 def auto_find_icon ():
0 commit comments