|
1 | 1 | import customtkinter as ctk |
2 | 2 | from tkinter.filedialog import asksaveasfilename, askopenfilename |
3 | 3 | from tkinter import Menu |
4 | | -from tkinter.messagebox import showinfo, askyesno, askyesnocancel |
| 4 | +from tkinter.messagebox import showinfo, askyesno, askyesnocancel, showerror |
5 | 5 | from tkinter import simpledialog |
6 | | -import time, threading |
7 | | -import os, json, platform |
8 | | -import venv, sys |
| 6 | +import time, threading, shutil |
| 7 | +import os, json, platform, tempfile |
| 8 | +import venv, sys, urllib.parse, urllib.request |
| 9 | +from io import BytesIO |
| 10 | +from zipfile import ZipFile |
| 11 | + |
| 12 | +VERSION = '1.3.67' |
| 13 | +ONL_VER_URL = 'https://raw.githubusercontent.com/maxhatei2/kPad/refs/heads/main/.kv' |
| 14 | +DOWNLOAD_URLS = { |
| 15 | + ('Darwin', 'arm64'): 'https://github.com/maxhatei2/kPad/releases/latest/kPad-mac_arm64.zip', |
| 16 | + ('Darwin', 'x86_64'): 'https://github.com/maxhatei2/kPad/releases/latest/kPad-mac_x86_64.zip', |
| 17 | + (('Windows', 'arm64'), ('Windows', 'x86_64')): 'https://github.com/maxhatei2/kPad/releases/latest/kPad-Windows_x86_64.exe.zip' |
| 18 | +} |
9 | 19 |
|
10 | 20 | # ██╗░░██╗██████╗░░█████╗░██████╗░ |
11 | 21 | # ██║░██╔╝██╔══██╗██╔══██╗██╔══██╗ |
12 | 22 | # █████═╝░██████╔╝███████║██║░░██║ |
13 | 23 | # ██╔═██╗░██╔═══╝░██╔══██║██║░░██║ |
14 | 24 | # ██║░╚██╗██║░░░░░██║░░██║██████╔╝ |
15 | 25 | # ╚═╝░░╚═╝╚═╝░░░░░╚═╝░░╚═╝╚═════╝░ |
16 | | -# Version 1.2.2 [SOURCE CODE] |
| 26 | +# Version 1.3.0 [SOURCE CODE] |
17 | 27 |
|
18 | 28 |
|
19 | 29 | if platform.system() == 'Darwin': |
|
52 | 62 |
|
53 | 63 | _fonts = ['Menlo', 'Monaco', 'Helvetica', 'Arial', 'Times New Roman', 'Georgia', 'Avenir', 'Baskerville', 'Futura', 'Verdana', 'Gill Sans', 'Courier', 'Optima', 'American Typewriter'] |
54 | 64 |
|
| 65 | +try: |
| 66 | + PROTOCOL_URL = sys.argv[1] |
| 67 | + parsed = urllib.parse.urlparse(PROTOCOL_URL) |
| 68 | + if parsed.netloc == 'InstallPlugin': |
| 69 | + url = parsed.query.split('=')[1] |
| 70 | + try: |
| 71 | + response = urllib.request.urlopen(url) |
| 72 | + data = response.read() |
| 73 | + with ZipFile(BytesIO(data)) as z: |
| 74 | + z.extract(plugin_dir) |
| 75 | + showinfo('Plugin installed', 'Plugin was installed from the link. If everything is correct, you\'ll find it to run in the \'Plugins\' tab to run.') |
| 76 | + except Exception as e: showerror('Error while installing plugin', f'Error while installing the plugin: -> {e} <- Check your Internet connection or the URL in the link.') |
| 77 | +except: pass |
| 78 | + |
| 79 | +def AutoUpdate(gimme_your_self): |
| 80 | + try: |
| 81 | + online_ver = urllib.request.urlopen(ONL_VER_URL).read().decode().strip() |
| 82 | + if online_ver != VERSION: |
| 83 | + to_update = askyesno('Update available!', f'A new update to kPad {online_ver}, is available to install! Do it now?') |
| 84 | + if to_update: |
| 85 | + DownloadUpdateWindow(gimme_your_self) |
| 86 | + else: |
| 87 | + gimme_your_self.stats_line_col.configure(text='User rejected update | Ln: 1 Col: 1 Ch: 0') |
| 88 | + else: |
| 89 | + gimme_your_self.stats_line_col.configure(text='Up to date | Ln: 1 Col: 1 Ch: 0') |
| 90 | + except Exception as e: |
| 91 | + gimme_your_self.stats_line_col.configure(text=f'⚠️ Warning! Could not check for updates: {e} | Ln: 1 Col: 1 Ch: 0') |
| 92 | + |
55 | 93 | class PluginAPI: |
56 | 94 | def __init__(self, textbox, appinstance): |
57 | 95 | self.textbox = textbox |
@@ -141,6 +179,8 @@ class App(ctk.CTk): |
141 | 179 | def __init__(self, title, geometry): |
142 | 180 | super().__init__() |
143 | 181 |
|
| 182 | + self.after(1, lambda: threading.Thread(AutoUpdate(gimme_your_self=self))) |
| 183 | + |
144 | 184 | import importlib.util |
145 | 185 |
|
146 | 186 | self.textbox = ctk.CTkTextbox(self, undo=CONFIGURATION['undo']['enabled'], autoseparators=CONFIGURATION['undo']['separate_edits_from_undos'], maxundo=CONFIGURATION['undo']['max_undo']) |
@@ -482,4 +522,109 @@ def make_separator(): |
482 | 522 | make_separator() |
483 | 523 | self.main.pack(fill='both', expand='True') |
484 | 524 |
|
485 | | -App('kPad - Untitled', CONFIGURATION['window_geometry']).mainloop() |
| 525 | +class DownloadUpdateWindow(ctk.CTkToplevel): |
| 526 | + def __init__(self, parent): |
| 527 | + super().__init__(parent) |
| 528 | + |
| 529 | + syst = platform.system() |
| 530 | + arch = platform.machine().lower() |
| 531 | + if arch in ("x86_64", "amd64"): |
| 532 | + arch = "x86_64" |
| 533 | + elif arch in ("arm64", "aarch64"): |
| 534 | + arch = "arm64" |
| 535 | + |
| 536 | + |
| 537 | + def create_start_menu_shortcut(exe_path, name="kPad"): |
| 538 | + start_menu = os.path.join(os.environ["APPDATA"], r"Microsoft\Windows\Start Menu\Programs") |
| 539 | + shortcut_path = os.path.join(start_menu, f"{name}.lnk") |
| 540 | + ps_cmd = f''' |
| 541 | + $WshShell = New-Object -ComObject WScript.Shell; |
| 542 | + $Shortcut = $WshShell.CreateShortcut("{shortcut_path}"); |
| 543 | + $Shortcut.TargetPath = "{exe_path}"; |
| 544 | + $Shortcut.WorkingDirectory = "{os.path.dirname(exe_path)}"; |
| 545 | + $Shortcut.Save(); |
| 546 | + ''' |
| 547 | + os.system(f'powershell -Command "{ps_cmd}"') |
| 548 | + |
| 549 | + def Download(): |
| 550 | + syst = platform.system() |
| 551 | + arch = platform.machine().lower() |
| 552 | + if arch in ("x86_64", "amd64"): |
| 553 | + arch = "x86_64" |
| 554 | + elif arch in ("arm64", "aarch64"): |
| 555 | + arch = "arm64" |
| 556 | + requiredUrl = DOWNLOAD_URLS.get((syst, arch)) |
| 557 | + if not requiredUrl: |
| 558 | + showerror("Error", f"No download available for {syst} {arch}") |
| 559 | + return |
| 560 | + if syst == 'Darwin': |
| 561 | + try: |
| 562 | + data = urllib.request.urlopen(requiredUrl).read() |
| 563 | + with tempfile.TemporaryDirectory() as tmp_outer: |
| 564 | + outer_zip_path = os.path.join(tmp_outer, os.path.basename(requiredUrl)) |
| 565 | + with open(outer_zip_path, "wb") as f: |
| 566 | + f.write(data) |
| 567 | + with ZipFile(outer_zip_path) as outer_zip: |
| 568 | + outer_zip.extractall(tmp_outer) |
| 569 | + inner_zip_path = None |
| 570 | + for root, _, files in os.walk(tmp_outer): |
| 571 | + for file in files: |
| 572 | + if file.endswith(".zip"): |
| 573 | + inner_zip_path = os.path.join(root, file) |
| 574 | + break |
| 575 | + if inner_zip_path: |
| 576 | + break |
| 577 | + if not inner_zip_path: |
| 578 | + showerror("Error", "Could not find inner zip containing the .app") |
| 579 | + self.destroy() |
| 580 | + return |
| 581 | + with tempfile.TemporaryDirectory() as tmp_inner: |
| 582 | + with ZipFile(inner_zip_path) as inner_zip: |
| 583 | + inner_zip.extractall(tmp_inner) |
| 584 | + app_folder = None |
| 585 | + for item in os.listdir(tmp_inner): |
| 586 | + if item.endswith(".app"): |
| 587 | + app_folder = os.path.join(tmp_inner, item) |
| 588 | + break |
| 589 | + if not app_folder: |
| 590 | + showerror("Error", "Could not find .app inside the inner zip") |
| 591 | + self.destroy() |
| 592 | + return |
| 593 | + dest = "/Applications/kPad.app" |
| 594 | + if os.path.exists(dest): |
| 595 | + shutil.rmtree(dest) |
| 596 | + shutil.move(app_folder, dest) |
| 597 | + showinfo("Update Installed", f"kPad installed to {dest}") |
| 598 | + except Exception as e: |
| 599 | + showerror("Error", f"[Errno EXCEPTION]: Failed to install update: {e}") |
| 600 | + self.destroy() |
| 601 | + return |
| 602 | + elif syst == 'Windows': |
| 603 | + dest_folder = os.path.join(os.getenv("LOCALAPPDATA"), "kPad") |
| 604 | + os.makedirs(dest_folder, exist_ok=True) |
| 605 | + dest_file = os.path.join(dest_folder, "kPad.exe") |
| 606 | + try: |
| 607 | + data = urllib.request.urlopen(requiredUrl).read() |
| 608 | + with tempfile.TemporaryDirectory() as tmp: |
| 609 | + with ZipFile(BytesIO(data)) as zip: |
| 610 | + zip.extract(tmp) |
| 611 | + shutil.move(f'{tmp}/kPad-Windows_x86_64/kPad-Windows_x86_64.exe', dest_file) |
| 612 | + create_start_menu_shortcut(dest_file, f'kPad') |
| 613 | + except Exception as e: |
| 614 | + showerror('Failed!', f'[Errno EXCEPTION]: {e}') |
| 615 | + self.destroy() |
| 616 | + return |
| 617 | + |
| 618 | + downloading_new_version = ctk.CTkLabel(self, text='Downloading update...') |
| 619 | + downloading_new_version.pack() |
| 620 | + this_might_etc = ctk.CTkLabel(self, text='This might take a bit, depending on your Internet connection. When done, the app will close.') |
| 621 | + this_might_etc.pack() |
| 622 | + load_pbar = ctk.CTkProgressBar(self, mode='indeterminate') |
| 623 | + load_pbar.pack() |
| 624 | + load_pbar.start() |
| 625 | + |
| 626 | + self.after(1, Download) |
| 627 | + |
| 628 | + |
| 629 | +app = App('kPad - Untitled', CONFIGURATION['window_geometry']) |
| 630 | +app.mainloop() |
0 commit comments