1+ # Modern Chromium Updater with Dark Mode (customtkinter)
2+ # SHA256 validation, sync/nosync selection, embedded log viewer, scheduler support
3+ # Developed by Fatih | Designed with customtkinter for a modern, user-friendly experience
4+ # Requirements: pip install customtkinter requests pefile plyer
5+
6+ # Copyright (c) 2025 Fatih
7+ # This tool is provided as-is under the MIT License.
8+
9+ import customtkinter as ctk
10+ from tkinter import filedialog
11+ import os , json , subprocess , sys , tempfile , threading , time , requests , pefile , hashlib , webbrowser
12+ from datetime import datetime
13+ from plyer import notification
14+
15+ # === CONFIG ===
16+ CONFIG_FILE = "settings.json"
17+ SCRIPT_DIR = os .path .dirname (os .path .abspath (__file__ ))
18+ LOG_FILE = os .path .join (SCRIPT_DIR , "chromium_updater.log" )
19+ ICON_FILE = os .path .join (SCRIPT_DIR , "chromium_updater_icon.ico" )
20+ TEMP_DIR = tempfile .gettempdir ()
21+ VERS_FILE = os .path .join (TEMP_DIR , "chromium_last_version.txt" )
22+ GITHUB_API = "https://api.github.com/repos/Hibbiki/chromium-win64/releases/latest" # Uses Hibbiki's Chromium builds
23+ VERSION = "1.0.0"
24+
25+ DEFAULT_CONFIG = {
26+ "install_path" : "" ,
27+ "notifications" : True ,
28+ "check_interval" : "manual" ,
29+ "enable_scheduler" : False ,
30+ "download_type" : "sync"
31+ }
32+
33+ # === CORE FUNCTIONS ===
34+ def log (msg ):
35+ ts = datetime .now ().strftime ("[%Y-%m-%d %H:%M:%S]" )
36+ line = f"{ ts } { msg } \n "
37+ with open (LOG_FILE , "a" , encoding = "utf-8" ) as f :
38+ f .write (line )
39+ if logbox :
40+ logbox .insert ("end" , line )
41+ logbox .see ("end" )
42+
43+ def clear_log ():
44+ if os .path .exists (LOG_FILE ):
45+ with open (LOG_FILE , "w" , encoding = "utf-8" ) as f :
46+ f .write ("" )
47+ if logbox :
48+ logbox .delete ("0.0" , "end" )
49+ log ("[✓] Log cleared" )
50+
51+ def notify (title , msg , enabled = True ):
52+ if enabled :
53+ try :
54+ icon_path = os .path .join (SCRIPT_DIR , "chromium_updater_icon.ico" )
55+ notification .notify (
56+ title = title ,
57+ message = msg ,
58+ app_name = "Chromium Updater" ,
59+ app_icon = icon_path ,
60+ timeout = 6
61+ )
62+ except Exception as e :
63+ log (f"[X] Notification error: { e } " )
64+
65+ def version_tuple (v ):
66+ import re
67+ m = re .match (r"(\d+)\.(\d+)\.(\d+)\.(\d+)" , v .strip ("v" ))
68+ return tuple (map (int , m .groups ())) if m else (0 ,0 ,0 ,0 )
69+
70+ def get_chrome_version (path ):
71+ try :
72+ pe = pefile .PE (path )
73+ for fileinfo in pe .FileInfo :
74+ for entry in fileinfo :
75+ if entry .Key == b'StringFileInfo' :
76+ for st in entry .StringTable :
77+ return st .entries .get (b'ProductVersion' , b'' ).decode ()
78+ except :
79+ return ""
80+
81+ def sha256sum (filepath ):
82+ h = hashlib .sha256 ()
83+ with open (filepath , 'rb' ) as f :
84+ for chunk in iter (lambda : f .read (4096 ), b"" ):
85+ h .update (chunk )
86+ return h .hexdigest ()
87+
88+ def download_with_progress (url , dest , cb = None ):
89+ r = requests .get (url , stream = True )
90+ total = int (r .headers .get ('content-length' , 0 ))
91+ downloaded = 0
92+ start = time .time ()
93+ with open (dest , 'wb' ) as f :
94+ for chunk in r .iter_content (chunk_size = 8192 ):
95+ if chunk :
96+ f .write (chunk )
97+ downloaded += len (chunk )
98+ elapsed = time .time () - start
99+ eta = int ((total - downloaded ) / (downloaded / elapsed )) if downloaded else 0
100+ if cb : cb (downloaded , total , eta )
101+
102+ def validate_sha256 (download_path , hash_url ):
103+ try :
104+ r = requests .get (hash_url )
105+ expected_hash = r .text .strip ().split ()[0 ]
106+ actual_hash = sha256sum (download_path )
107+ return expected_hash == actual_hash
108+ except :
109+ return False
110+
111+ def apply_scheduler (enabled , interval ):
112+ task_name = "ChromiumUpdaterAutoCheck"
113+ exe_path = os .path .abspath (sys .argv [0 ])
114+ if enabled :
115+ interval_map = {
116+ "daily" : "DAILY" ,
117+ "weekly" : "WEEKLY" ,
118+ "monthly" : "MONTHLY"
119+ }
120+ if interval in interval_map :
121+ subprocess .run ([
122+ "schtasks" , "/Create" , "/SC" , interval_map [interval ], "/TN" , task_name ,
123+ "/TR" , f'\" { exe_path } \" ' , "/ST" , "10:00" , "/F"
124+ ], stdout = subprocess .DEVNULL , stderr = subprocess .DEVNULL )
125+ log (f"[Scheduler] Task created: { interval_map [interval ]} " )
126+ else :
127+ subprocess .run (["schtasks" , "/Delete" , "/TN" , task_name , "/F" ], stderr = subprocess .DEVNULL )
128+ log ("[Scheduler] Task removed" )
129+
130+ # GUI bağlantısı: zamanlayıcı değiştiğinde hemen uygula
131+
132+ def scheduler_settings_updated ():
133+ apply_scheduler (scheduler_var .get (), interval_var .get ())
134+
135+
136+ def auto_detect_chrome ():
137+ candidates = [
138+ os .path .expandvars (r"%LocalAppData%\\Chromium\\Application\\chrome.exe" ),
139+ os .path .expandvars (r"%ProgramFiles%\\Chromium\\Application\\chrome.exe" )
140+ ]
141+ for path in candidates :
142+ if os .path .exists (path ):
143+ return path
144+ return ""
145+
146+
147+ def threaded_update ():
148+ threading .Thread (target = check_for_update ).start ()
149+
150+ def check_for_update ():
151+ chrome_path = path_var .get ()
152+ dl_type = dl_type_var .get ()
153+ notify_enabled = notify_var .get ()
154+
155+ if not os .path .exists (chrome_path ):
156+ progress_label .set ("Chromium not found. Please set path." )
157+ notify ("Chromium Updater" , "Chromium not found on this system." , notify_enabled )
158+ return
159+
160+ progress_label .set ("Checking for updates..." )
161+ try :
162+ r = requests .get (GITHUB_API , timeout = 10 )
163+ release = r .json ()
164+ latest = release ["tag_name" ]
165+ except Exception as e :
166+ log (f"[GitHub] Error: { e } " )
167+ notify ("GitHub Error" , str (e ), notify_enabled )
168+ progress_label .set ("GitHub error." )
169+ return
170+
171+ current = get_chrome_version (chrome_path )
172+ if version_tuple (current ) >= version_tuple (latest ):
173+ progress_label .set ("Chromium is up-to-date." )
174+ notify ("Chromium" , "Already up-to-date" , notify_enabled )
175+ return
176+
177+ asset = next ((a for a in release ["assets" ] if f".{ dl_type } .exe" in a ["name" ]), None )
178+ hash_asset = next ((a for a in release ["assets" ] if f".{ dl_type } .exe.sha256sum" in a ["name" ]), None )
179+ if not asset or not hash_asset :
180+ progress_label .set ("Installer or hash missing." )
181+ notify ("Missing Asset" , "Installer or checksum file missing" , notify_enabled )
182+ return
183+
184+ url = asset ["browser_download_url" ]
185+ hash_url = hash_asset ["browser_download_url" ]
186+ filename = os .path .join (TEMP_DIR , asset ["name" ])
187+
188+ def update_bar (done , total , eta ):
189+ percent = int ((done / total ) * 100 )
190+ bar .set (percent )
191+ m , s = divmod (eta , 60 )
192+ progress_label .set (f"Downloading... { percent } % | ETA: { m :02} :{ s :02} " )
193+
194+ download_with_progress (url , filename , cb = update_bar )
195+
196+ progress_label .set ("Verifying file integrity..." )
197+ if not validate_sha256 (filename , hash_url ):
198+ progress_label .set ("SHA256 mismatch! Aborting." )
199+ notify ("Security Warning" , "Downloaded file failed hash check" , notify_enabled )
200+ return
201+
202+ subprocess .Popen ([filename ], shell = True )
203+ with open (VERS_FILE , 'w' ) as f :
204+ f .write (latest )
205+ progress_label .set ("Installation started." )
206+ notify ("Chromium Updated" , f"{ latest } installed." , notify_enabled )
207+
208+ # === GUI SETUP ===
209+ ctk .set_appearance_mode ("dark" )
210+ ctk .set_default_color_theme ("blue" )
211+ root = ctk .CTk ()
212+ root .title ("Chromium Updater" )
213+ root .geometry ("600x680" )
214+
215+ path_var = ctk .StringVar ()
216+ notify_var = ctk .BooleanVar (value = True )
217+ dl_type_var = ctk .StringVar (value = "sync" )
218+ progress_label = ctk .StringVar (value = "Idle." )
219+ scheduler_var = ctk .BooleanVar (value = False )
220+ interval_var = ctk .StringVar (value = "daily" )
221+ logbox = None
222+
223+ config = DEFAULT_CONFIG
224+ if os .path .exists (CONFIG_FILE ):
225+ with open (CONFIG_FILE , "r" ) as f :
226+ config .update (json .load (f ))
227+
228+ if not config ["install_path" ]:
229+ detected = auto_detect_chrome ()
230+ config ["install_path" ] = detected
231+ if detected :
232+ notify ("Chromium Updater" , f"Chromium detected at: { detected } " )
233+ else :
234+ notify ("Chromium Updater" , "Chromium not detected." )
235+
236+ path_var .set (config ["install_path" ])
237+ notify_var .set (config ["notifications" ])
238+ dl_type_var .set (config .get ("download_type" , "sync" ))
239+ scheduler_var .set (config .get ("enable_scheduler" , False ))
240+ interval_var .set (config .get ("check_interval" , "daily" ))
241+
242+ frame = ctk .CTkFrame (root , corner_radius = 8 )
243+ frame .pack (padx = 6 , pady = 6 , fill = "both" , expand = True )
244+
245+ ctk .CTkLabel (frame , text = "Chromium Executable Path:" ).pack (anchor = "w" )
246+ row = ctk .CTkFrame (frame )
247+ row .pack (fill = "x" )
248+ ctk .CTkEntry (row , textvariable = path_var ).pack (side = "left" , fill = "x" , expand = True )
249+ ctk .CTkButton (row , text = "Browse" , command = lambda : path_var .set (filedialog .askopenfilename (filetypes = [("Executable Files" , "*.exe" )]))).pack (side = "left" , padx = 5 )
250+
251+ ctk .CTkLabel (frame , text = "Download Type:" ).pack (anchor = "w" , pady = (10 ,0 ))
252+ drop = ctk .CTkOptionMenu (frame , variable = dl_type_var , values = ["sync" , "nosync" ])
253+ drop .pack (fill = "x" )
254+ ctk .CTkLabel (
255+ frame ,
256+ text = "\n SYNC VERSION:\n • Standard Chromium build with Google Sign-in & Sync functionality.\n \n NO SYNC VERSION:\n • Ungoogled Chromium – Privacy-enhanced version without Google services." ,
257+ text_color = "gray" ,
258+ justify = "left" ,
259+ font = ctk .CTkFont (size = 12 )
260+ ).pack (anchor = "w" )
261+
262+ ctk .CTkLabel (frame , text = "Schedule Auto-Update:" ).pack (anchor = "w" , pady = (10 ,0 ))
263+ scheduler_frame = ctk .CTkFrame (frame )
264+ scheduler_frame .pack (fill = "x" )
265+ ctk .CTkCheckBox (scheduler_frame , text = "Enable Scheduler" , variable = scheduler_var , command = scheduler_settings_updated ).pack (side = "left" )
266+ interval_menu = ctk .CTkOptionMenu (scheduler_frame , variable = interval_var , values = ["daily" , "weekly" , "monthly" ], command = lambda _ : scheduler_settings_updated ())
267+ interval_menu .pack (side = "right" )
268+
269+ ctk .CTkCheckBox (frame , text = "Show Desktop Notifications" , variable = notify_var ).pack (anchor = "w" , pady = 10 )
270+
271+ ctk .CTkButton (frame , text = "Check for Updates" , command = threaded_update ).pack (pady = 10 )
272+
273+ bar = ctk .CTkProgressBar (frame , width = 400 )
274+ bar .set (0 )
275+ bar .pack (pady = 5 )
276+ ctk .CTkLabel (frame , textvariable = progress_label ).pack ()
277+
278+ ctk .CTkLabel (frame , text = "Update Log:" ).pack (anchor = "w" , pady = (10 ,0 ))
279+ logbox = ctk .CTkTextbox (frame , height = 160 )
280+ logbox .pack (fill = "both" , expand = True )
281+ if os .path .exists (LOG_FILE ):
282+ with open (LOG_FILE , "r" , encoding = "utf-8" ) as f :
283+ logbox .insert ("0.0" , f .read ())
284+
285+ # === LOG CLEAR BUTTON ===
286+ ctk .CTkButton (frame , text = "Clear Log" , command = clear_log ).pack (pady = 5 )
287+
288+ # Developer credit footer (clickable GitHub profile)
289+ def open_github ():
290+ webbrowser .open ("https://github.com/fatih-gh" )
291+
292+ footer_frame = ctk .CTkFrame (root , fg_color = "transparent" )
293+ footer_frame .pack (pady = (0 , 4 ), fill = "x" )
294+
295+ def open_repo ():
296+ webbrowser .open ("https://github.com/fatih-gh/ChroMate/tree/main" )
297+
298+ def open_license ():
299+ webbrowser .open ("https://github.com/fatih-gh/ChroMate/blob/main/LICENSE" )
300+
301+
302+ ctk .CTkButton (
303+ footer_frame ,
304+ text = f"Version { VERSION } " ,
305+ command = open_repo ,
306+ width = 1 ,
307+ height = 1 ,
308+ fg_color = "transparent" ,
309+ text_color = "gray" ,
310+ font = ctk .CTkFont (size = 11 ),
311+ hover = True
312+ ).pack (side = "right" , padx = 10 )
313+
314+ ctk .CTkButton (
315+ footer_frame ,
316+ text = "GPLv3 License" ,
317+ command = open_license ,
318+ width = 1 ,
319+ height = 1 ,
320+ fg_color = "transparent" ,
321+ text_color = "gray" ,
322+ font = ctk .CTkFont (size = 11 ),
323+ hover = True
324+ ).pack (side = "right" )
325+
326+
327+ ctk .CTkButton (footer_frame , text = "Developed by Fatih © 2025" , text_color = "gray" , font = ctk .CTkFont (size = 13 , weight = "bold" ), fg_color = "transparent" , hover = False , command = open_github ).pack (side = "left" , padx = 10 )
328+
329+ # Save config and apply scheduler on close
330+ def on_exit ():
331+ data = {
332+ "install_path" : path_var .get (),
333+ "notifications" : notify_var .get (),
334+ "check_interval" : interval_var .get (),
335+ "enable_scheduler" : scheduler_var .get (),
336+ "download_type" : dl_type_var .get ()
337+ }
338+ with open (CONFIG_FILE , "w" ) as f :
339+ json .dump (data , f , indent = 4 )
340+ apply_scheduler (data ["enable_scheduler" ], data ["check_interval" ])
341+ root .destroy ()
342+
343+ root .protocol ("WM_DELETE_WINDOW" , on_exit )
344+ root .mainloop ()
0 commit comments