diff --git a/GEMINI.md b/GEMINI.md index 828a2a9..ae17096 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -18,7 +18,7 @@ A modern Python-based file browser for Microsoft SharePoint, specifically design ## Tech Stack - **Language:** Python 3.x -- **GUI Framework:** [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter) (Modern, responsive UI) +- **GUI Framework:** [wxPython](https://www.wxpython.org/) (Native Windows UI) - **Authentication:** [MSAL (Microsoft Authentication Library)](https://github.com/AzureAD/microsoft-authentication-library-for-python) - **API Interaction:** Microsoft Graph API via `requests` - **File Monitoring:** Local file lock polling @@ -30,7 +30,7 @@ A modern Python-based file browser for Microsoft SharePoint, specifically design Ensure you have Python installed. You will also need to install the following dependencies: ```bash -pip install customtkinter msal requests +pip install wxPython msal requests ``` ### Running the Application diff --git a/sharepoint_browser.py b/sharepoint_browser.py index cfa20e5..bcea58d 100644 --- a/sharepoint_browser.py +++ b/sharepoint_browser.py @@ -6,9 +6,8 @@ import json import sys import requests import msal -import customtkinter as ctk -import tkinter as tk -from tkinter import messagebox +import wx +import wx.lib.newevent # --- STIHÅNDTERING (Til EXE-brug) --- if getattr(sys, 'frozen', False): @@ -32,7 +31,10 @@ def load_settings(): return default_settings with open(SETTINGS_FILE, 'r') as f: - return json.load(f) + try: + return json.load(f) + except: + return default_settings settings = load_settings() CLIENT_ID = settings.get("client_id") @@ -44,65 +46,118 @@ TEMP_DIR = settings.get("temp_dir", "C:\\Temp_SP") if not os.path.exists(TEMP_DIR): os.makedirs(TEMP_DIR) -ctk.set_appearance_mode("System") -ctk.set_default_color_theme("blue") - -class SharePointApp(ctk.CTk): +class SharePointApp(wx.Frame): def __init__(self): - super().__init__() - self.title("SharePoint Explorer") - self.geometry("1000x750") + super().__init__(None, title="SharePoint Explorer", size=(1000, 750)) + # State self.access_token = None self.headers = {} - - # Navigation State self.history = [] # Stack af (mode, id, path_segment) self.current_path = ["SharePoint"] self.current_site_id = None self.current_drive_id = None self.current_folder_id = "root" + self.current_items = [] # Gemmer graf-objekterne for rækkerne + self.tree_item_data = {} # Mappenoder -> {type, id, name, drive_id, path} + self.tree_root = None + self.is_navigating_back = False - # UI Layout - 3 Rækker: Top (Nav), Midt (Filer), Bund (Info) - self.grid_rowconfigure(1, weight=1) - self.grid_columnconfigure(0, weight=1) + self.InitUI() + self.Centre() + self.Show() + + def InitUI(self): + panel = wx.Panel(self) + main_sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(main_sizer) + main_sizer.Add(panel, 1, wx.EXPAND) + + vbox = wx.BoxSizer(wx.VERTICAL) # 1. TOP NAVIGATION BAR - self.nav_frame = ctk.CTkFrame(self, height=60, corner_radius=0) - self.nav_frame.grid(row=0, column=0, sticky="ew", padx=0, pady=0) + nav_panel = wx.Panel(panel) + nav_hbox = wx.BoxSizer(wx.HORIZONTAL) - self.back_btn = ctk.CTkButton(self.nav_frame, text="← Tilbage", width=100, command=self.go_back, state="disabled") - self.back_btn.pack(side="left", padx=10, pady=10) + self.back_btn = wx.Button(nav_panel, label="← Tilbage", size=(100, 30)) + self.back_btn.Disable() + self.back_btn.Bind(wx.EVT_BUTTON, self.go_back) + nav_hbox.Add(self.back_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 10) - self.home_btn = ctk.CTkButton(self.nav_frame, text="🏠 Hjem", width=100, command=self.load_sites, state="disabled") - self.home_btn.pack(side="left", padx=5, pady=10) + self.home_btn = wx.Button(nav_panel, label="🏠 Hjem", size=(100, 30)) + self.home_btn.Disable() + self.home_btn.Bind(wx.EVT_BUTTON, self.load_sites) + nav_hbox.Add(self.home_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) - self.login_btn = ctk.CTkButton(self.nav_frame, text="Log ind", width=120, fg_color="#28a745", hover_color="#218838", command=self.login) - self.login_btn.pack(side="right", padx=10, pady=10) + nav_hbox.AddStretchSpacer(1) + + self.login_btn = wx.Button(nav_panel, label="Log ind", size=(120, 30)) + self.login_btn.SetBackgroundColour(wx.Colour(40, 167, 69)) # Grøn + self.login_btn.SetForegroundColour(wx.WHITE) + self.login_btn.Bind(wx.EVT_BUTTON, self.login) + nav_hbox.Add(self.login_btn, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) + + nav_panel.SetSizer(nav_hbox) + vbox.Add(nav_panel, 0, wx.EXPAND | wx.ALL, 5) - # 2. MAIN CONTENT (File Area) - self.main_frame = ctk.CTkScrollableFrame(self, fg_color=("gray95", "gray10"), corner_radius=0) - self.main_frame.grid(row=1, column=0, sticky="nsew", padx=0, pady=0) + # 2. PATH LABEL + self.path_label = wx.StaticText(panel, label="📍 SharePoint") + self.path_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) + vbox.Add(self.path_label, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) - # 3. FOOTER (Path & Status) - self.footer_frame = ctk.CTkFrame(self, height=40, corner_radius=0) - self.footer_frame.grid(row=2, column=0, sticky="ew") + # 3. SPLITTER FOR TREE AND LIST + self.splitter = wx.SplitterWindow(panel, style=wx.SP_LIVE_UPDATE | wx.SP_3DSASH) - self.path_label = ctk.CTkLabel(self.footer_frame, text="Sti: /", font=ctk.CTkFont(size=12, weight="bold")) - self.path_label.pack(side="left", padx=15, pady=5) + # Left side: Tree + self.tree_ctrl = wx.TreeCtrl(self.splitter, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.BORDER_SUNKEN) + self.tree_ctrl.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.on_tree_expanding) + self.tree_ctrl.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_selected) + + # Right side: File Area - ListCtrl + self.list_ctrl = wx.ListCtrl(self.splitter, style=wx.LC_REPORT | wx.BORDER_SUNKEN) + self.list_ctrl.InsertColumn(0, "Navn", width=450) + self.list_ctrl.InsertColumn(1, "Type", width=150) + self.list_ctrl.InsertColumn(2, "Sidst ændret", width=200) - self.status_label = ctk.CTkLabel(self.footer_frame, text="Klar", text_color="gray") - self.status_label.pack(side="right", padx=15, pady=5) + self.list_ctrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_item_activated) + self.list_ctrl.Bind(wx.EVT_CONTEXT_MENU, self.on_right_click) + + self.splitter.SplitVertically(self.tree_ctrl, self.list_ctrl, 250) + self.splitter.SetMinimumPaneSize(100) + + vbox.Add(self.splitter, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) + + # 4. STATUS BAR + self.status_bar = self.CreateStatusBar() + self.status_bar.SetStatusText("Klar") + + panel.SetSizer(vbox) + self.Layout() + + def on_right_click(self, event): + item_idx = self.list_ctrl.GetFirstSelected() + if item_idx == -1: return + item = self.current_items[item_idx] + if item['type'] == "FILE": + menu = wx.Menu() + edit_item = menu.Append(wx.ID_ANY, "Rediger fil") + self.Bind(wx.EVT_MENU, lambda e: threading.Thread(target=self.process_file, args=(item['id'], item['name']), daemon=True).start(), edit_item) + self.PopupMenu(menu) + menu.Destroy() def set_status(self, text): - self.status_label.configure(text=text) - self.update_idletasks() + wx.CallAfter(self.status_bar.SetStatusText, text) + + def clear_main(self): + self.list_ctrl.DeleteAllItems() + self.current_items = [] + self.update_path_display() def update_path_display(self): path_str = " > ".join(self.current_path) - self.path_label.configure(text=f"📍 {path_str}") + wx.CallAfter(self.path_label.SetLabel, f"📍 {path_str}") - def login(self): + def login(self, event): self.set_status("Logger ind...") app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY) accounts = app.get_accounts() @@ -115,130 +170,245 @@ class SharePointApp(ctk.CTk): if "access_token" in result: self.access_token = result["access_token"] self.headers = {'Authorization': f'Bearer {self.access_token}'} - self.login_btn.configure(state="disabled", text="Logget ind", fg_color="gray") - self.home_btn.configure(state="normal") + self.login_btn.Disable() + self.login_btn.SetLabel("Logget ind") + self.login_btn.SetBackgroundColour(wx.Colour(200, 200, 200)) # Grå + self.home_btn.Enable() self.load_sites() else: self.set_status("Login fejlede.") - messagebox.showerror("Login Error", result.get("error_description", "Unknown error")) + wx.MessageBox(result.get("error_description", "Unknown error"), "Login Error", wx.OK | wx.ICON_ERROR) - def clear_main(self): - for widget in self.main_frame.winfo_children(): - widget.destroy() - self.back_btn.configure(state="normal" if self.history else "disabled") - self.update_path_display() - - def create_explorer_item(self, text, icon, command, is_file=False): - # En knap der ligner et række-element i Windows Explorer - frame = ctk.CTkFrame(self.main_frame, fg_color="transparent") - frame.pack(fill="x", padx=10, pady=1) - - btn = ctk.CTkButton( - frame, - text=f" {icon} {text}", - anchor="w", - height=35, - fg_color="transparent", - text_color=("gray10", "gray90"), - hover_color=("gray80", "gray25"), - font=ctk.CTkFont(size=13), - command=command - ) - btn.pack(fill="x") - return btn - - def load_sites(self): + def load_sites(self, event=None): self.set_status("Henter sites...") - self.clear_main() + self.tree_ctrl.DeleteAllItems() + self.list_ctrl.DeleteAllItems() + self.current_items = [] + self.tree_item_data = {} + self._pending_tree_selection_id = None + + self.tree_root = self.tree_ctrl.AddRoot("HiddenRoot") self.current_path = ["SharePoint"] self.history = [] self.update_path_display() + threading.Thread(target=self._fetch_sites_bg, daemon=True).start() + + def _fetch_sites_bg(self): url = "https://graph.microsoft.com/v1.0/sites?search=*" res = requests.get(url, headers=self.headers) if res.status_code == 200: sites = res.json().get('value', []) - # Sorter sites alfabetisk sites.sort(key=lambda x: x.get('displayName', x.get('name', '')).lower()) - for site in sites: - name = site.get('displayName', site.get('name')) - self.create_explorer_item(name, "🌐", lambda s=site['id'], n=name: self.select_site(s, n)) - self.set_status(f"Fandt {len(sites)} sites.") + wx.CallAfter(self._populate_sites_tree, sites) else: self.set_status("Kunne ikke hente sites.") - def select_site(self, site_id, name): - self.history.append(("SITES", None, "SharePoint")) - self.current_site_id = site_id - self.current_path.append(name) - self.load_drives(site_id) - - def load_drives(self, site_id): - self.set_status("Henter dokumentbiblioteker...") - self.clear_main() - url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives" - res = requests.get(url, headers=self.headers) - if res.status_code == 200: - drives = res.json().get('value', []) - # Sorter biblioteker alfabetisk - drives.sort(key=lambda x: x.get('name', '').lower()) - for drive in drives: - name = drive.get('name') - self.create_explorer_item(name, "📚", lambda d=drive['id'], n=name: self.select_drive(d, n)) - self.set_status("Vælg et bibliotek.") - else: - self.set_status("Fejl ved hentning af biblioteker.") - - def select_drive(self, drive_id, name): - self.history.append(("DRIVES", self.current_site_id, self.current_path[-1])) - self.current_drive_id = drive_id - self.current_path.append(name) - self.load_folder("root") - - def load_folder(self, folder_id): - self.set_status("Indlæser filer...") - self.clear_main() - - url = f"https://graph.microsoft.com/v1.0/drives/{self.current_drive_id}/root/children" if folder_id == "root" else \ - f"https://graph.microsoft.com/v1.0/drives/{self.current_drive_id}/items/{folder_id}/children" + def _populate_sites_tree(self, sites): + self.set_status(f"Fandt {len(sites)} sites.") + for site in sites: + name = site.get('displayName', site.get('name')) + node = self.tree_ctrl.AppendItem(self.tree_root, name) + self.tree_item_data[node] = { + "type": "SITE", "id": site['id'], "name": name, + "drive_id": None, "path": ["SharePoint", name], "loaded": False + } + self.tree_ctrl.SetItemHasChildren(node, True) - res = requests.get(url, headers=self.headers) - if res.status_code == 200: - items = res.json().get('value', []) - # Sorter: Mapper først, derefter filer. Begge alfabetisk. - items.sort(key=lambda x: (not 'folder' in x, x['name'].lower())) + # Select the first site or just show in list (defaulting to showing root sites in list) + self.list_ctrl.DeleteAllItems() + self.current_items = [] + for i, site in enumerate(sites): + name = site.get('displayName', site.get('name')) + self.list_ctrl.InsertItem(i, name) + self.list_ctrl.SetItem(i, 1, "Site") + self.current_items.append({"type": "SITE", "id": site['id'], "name": name}) + + def on_tree_expanding(self, event): + item = event.GetItem() + data = self.tree_item_data.get(item) + if not data or data.get("loaded"): + return - self.current_folder_id = folder_id - for item in items: - name, item_id, is_folder = item['name'], item['id'], 'folder' in item - icon = "📁" if is_folder else "📄" - cmd = lambda i=item_id, f=is_folder, n=name: self.item_clicked(i, f, n) - self.create_explorer_item(name, icon, cmd, not is_folder) - self.set_status("Klar") - else: - self.set_status(f"Fejl: {res.status_code}") + loading_node = self.tree_ctrl.AppendItem(item, "Indlæser...") + threading.Thread(target=self._fetch_tree_children_bg, args=(item, data), daemon=True).start() - def item_clicked(self, item_id, is_folder, name): - if is_folder: - # Gem det ID vi er i NU, før vi går dybere, så vi kan vende tilbage til det - self.history.append(("FOLDERS", self.current_folder_id, self.current_path[-1])) - self.current_path.append(name) - self.load_folder(item_id) - else: - threading.Thread(target=self.process_file, args=(item_id, name), daemon=True).start() + def _fetch_tree_children_bg(self, parent_node, data): + if data['type'] == "SITE": + url = f"https://graph.microsoft.com/v1.0/sites/{data['id']}/drives" + res = requests.get(url, headers=self.headers) + if res.status_code == 200: + drives = res.json().get('value', []) + drives.sort(key=lambda x: x.get('name', '').lower()) + wx.CallAfter(self._populate_tree_drives, parent_node, drives, data) + elif data['type'] == "DRIVE": + url = f"https://graph.microsoft.com/v1.0/drives/{data['id']}/root/children" + res = requests.get(url, headers=self.headers) + if res.status_code == 200: + items = res.json().get('value', []) + folders = [x for x in items if 'folder' in x] + folders.sort(key=lambda x: x['name'].lower()) + wx.CallAfter(self._populate_tree_folders, parent_node, folders, data) + elif data['type'] == "FOLDER": + url = f"https://graph.microsoft.com/v1.0/drives/{data['drive_id']}/items/{data['id']}/children" + res = requests.get(url, headers=self.headers) + if res.status_code == 200: + items = res.json().get('value', []) + folders = [x for x in items if 'folder' in x] + folders.sort(key=lambda x: x['name'].lower()) + wx.CallAfter(self._populate_tree_folders, parent_node, folders, data) - def go_back(self): - if not self.history: return - mode, prev_id, path_segment = self.history.pop() - self.current_path.pop() + def _populate_tree_drives(self, parent_node, drives, parent_data): + self.tree_ctrl.DeleteChildren(parent_node) + parent_data["loaded"] = True + target_node = None - if mode == "SITES": - self.load_sites() - elif mode == "DRIVES": - self.load_drives(prev_id) - elif mode == "FOLDERS": - # Nu bruger vi det præcise ID vi gemte før (prev_id er folder_id) - self.load_folder(prev_id) + for drive in drives: + name = drive.get('name', 'Ukendt') + drive_id = drive['id'] + node = self.tree_ctrl.AppendItem(parent_node, name) + self.tree_item_data[node] = { + "type": "DRIVE", "id": drive_id, "name": name, + "drive_id": drive_id, "path": parent_data["path"] + [name], "loaded": False + } + self.tree_ctrl.SetItemHasChildren(node, True) + if drive_id == getattr(self, '_pending_tree_selection_id', None): + target_node = node + + if target_node: + self._pending_tree_selection_id = None + self.tree_ctrl.SelectItem(target_node) + + def _populate_tree_folders(self, parent_node, folders, parent_data): + self.tree_ctrl.DeleteChildren(parent_node) + parent_data["loaded"] = True + target_node = None + + for folder in folders: + name = folder['name'] + folder_id = folder['id'] + node = self.tree_ctrl.AppendItem(parent_node, name) + self.tree_item_data[node] = { + "type": "FOLDER", "id": folder_id, "name": name, + "drive_id": parent_data["drive_id"], "path": parent_data["path"] + [name], "loaded": False + } + self.tree_ctrl.SetItemHasChildren(node, True) + if folder_id == getattr(self, '_pending_tree_selection_id', None): + target_node = node + + if target_node: + self._pending_tree_selection_id = None + self.tree_ctrl.SelectItem(target_node) + + def on_tree_selected(self, event): + item = event.GetItem() + data = self.tree_item_data.get(item) + if not data: + return + + self.current_path = data["path"] + self.update_path_display() + + if not self.is_navigating_back: + self.history.append(item) + self.back_btn.Enable(len(self.history) > 1) + + self.list_ctrl.DeleteAllItems() + self.current_items = [] + self.set_status("Indlæser indhold...") + + threading.Thread(target=self._fetch_list_contents_bg, args=(data,), daemon=True).start() + + def _fetch_list_contents_bg(self, data): + items_data = [] + if data['type'] == "SITE": + url = f"https://graph.microsoft.com/v1.0/sites/{data['id']}/drives" + res = requests.get(url, headers=self.headers) + if res.status_code == 200: + drives = res.json().get('value', []) + drives.sort(key=lambda x: x.get('name', '').lower()) + for drive in drives: + items_data.append({ + "type": "DRIVE", "id": drive['id'], "name": drive.get('name', ''), + "drive_id": drive['id'], "modified": "" + }) + elif data['type'] in ["DRIVE", "FOLDER"]: + drive_id = data['drive_id'] + if data['type'] == "DRIVE": + url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/root/children" + else: + url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{data['id']}/children" + + res = requests.get(url, headers=self.headers) + if res.status_code == 200: + items = res.json().get('value', []) + items.sort(key=lambda x: (not 'folder' in x, x['name'].lower())) + for item in items: + is_folder = 'folder' in item + modified = item.get('lastModifiedDateTime', '').replace('T', ' ').split('.')[0] + items_data.append({ + "type": "FOLDER" if is_folder else "FILE", + "id": item['id'], "name": item['name'], + "drive_id": drive_id, "modified": modified + }) + + wx.CallAfter(self._populate_list_ctrl, items_data, data) + + def _populate_list_ctrl(self, items_data, parent_data): + self.list_ctrl.DeleteAllItems() + self.current_items = [] + for i, item in enumerate(items_data): + self.list_ctrl.InsertItem(i, item['name']) + type_str = "Mappe" if item['type'] == "FOLDER" else "Fil" if item['type'] == "FILE" else "Bibliotek" + self.list_ctrl.SetItem(i, 1, type_str) + self.list_ctrl.SetItem(i, 2, item['modified']) + self.current_items.append(item) + + self.set_status("Klar") + + if parent_data['type'] == "SITE": + self.current_site_id = parent_data['id'] + elif parent_data['type'] == "DRIVE": + self.current_drive_id = parent_data['id'] + self.current_folder_id = "root" + elif parent_data['type'] == "FOLDER": + self.current_drive_id = parent_data['drive_id'] + self.current_folder_id = parent_data['id'] + + def on_item_activated(self, event): + idx = event.GetIndex() + item = self.current_items[idx] + + if item['type'] in ["SITE", "DRIVE", "FOLDER"]: + self._sync_tree_selection(item['id']) + elif item['type'] == "FILE": + self.current_drive_id = item['drive_id'] + threading.Thread(target=self.process_file, args=(item['id'], item['name']), daemon=True).start() + + def _sync_tree_selection(self, target_id): + selected = self.tree_ctrl.GetSelection() + if selected.IsOk(): + data = self.tree_item_data.get(selected) + if data and not data.get("loaded"): + self._pending_tree_selection_id = target_id + self.tree_ctrl.Expand(selected) + return + + child, cookie = self.tree_ctrl.GetFirstChild(selected) + while child.IsOk(): + cdata = self.tree_item_data.get(child) + if cdata and cdata['id'] == target_id: + self.tree_ctrl.SelectItem(child) + return + child, cookie = self.tree_ctrl.GetNextChild(selected, cookie) + + def go_back(self, event=None): + if len(self.history) > 1: + self.history.pop() # Remove current + prev_item = self.history[-1] # Peak at previous + self.is_navigating_back = True + self.tree_ctrl.SelectItem(prev_item) + self.is_navigating_back = False def process_file(self, item_id, file_name): base_url = f"https://graph.microsoft.com/v1.0/drives/{self.current_drive_id}/items/{item_id}" @@ -247,8 +417,6 @@ class SharePointApp(ctk.CTk): # 1. Checkout self.set_status(f"Tjekker '{file_name}' ud...") checkout_res = requests.post(f"{base_url}/checkout", headers=self.headers) - if checkout_res.status_code not in [200, 204]: - print(f"Checkout info: {checkout_res.status_code} - {checkout_res.text}") # 2. Download self.set_status(f"Downloader '{file_name}'...") @@ -256,7 +424,6 @@ class SharePointApp(ctk.CTk): if res.status_code != 200: raise Exception(f"Download fejlede: {res.status_code}") - # Lav en unik undermappe til filen for at bevare det originale navn uden konflikter short_hash = hashlib.md5(item_id.encode()).hexdigest()[:8] file_dir = os.path.join(TEMP_DIR, short_hash) if not os.path.exists(file_dir): @@ -268,13 +435,12 @@ class SharePointApp(ctk.CTk): f.write(res.content) # 3. Åbn & Overvåg - self.set_status(f"Åbner '{file_name}'... Vent på programmet starter.") + self.set_status(f"Åbner '{file_name}'...") os.startfile(local_path) - # Vent på at programmet rent faktisk låser filen locked = False - self.set_status(f"Venter på at '{file_name}' åbnes i redigeringsprogram...") - for _ in range(10): # Vent op til 10 sekunder på at filen bliver låst + self.set_status(f"Venter på '{file_name}'...") + for _ in range(10): time.sleep(1) try: os.rename(local_path, local_path) @@ -283,42 +449,39 @@ class SharePointApp(ctk.CTk): break if locked: - self.set_status(f"Redigerer '{file_name}' - Luk programmet for at gemme.") - # Nu venter vi på at den bliver låst OP igen + self.set_status(f"Redigerer '{file_name}' - Luk for at gemme.") while True: time.sleep(2) try: os.rename(local_path, local_path) - break # Filen er ikke længere låst + break except OSError: pass else: - self.set_status("Kunne ikke detektere fillås. Tryk OK når du er færdig.") - messagebox.showinfo("Info", f"Vi kunne ikke detektere om '{file_name}' blev låst af dit program.\n\nTryk OK når du har gemt og LUKKET filen i dit redigeringsprogram.") + self.set_status("Fillås ikke detekteret.") + wx.CallAfter(wx.MessageBox, f"Vi kunne ikke detektere om '{file_name}' blev låst.\n\nTryk OK når du har gemt og LUKKET filen.", "Info", wx.OK | wx.ICON_INFORMATION) # 4. Upload - self.set_status(f"Uploader ændringer til SharePoint...") + self.set_status(f"Uploader ændringer...") with open(local_path, 'rb') as f: upload_res = requests.put(f"{base_url}/content", headers=self.headers, data=f) if upload_res.status_code not in [200, 201]: - raise Exception(f"Upload fejlede: {upload_res.status_code} - {upload_res.text}") + raise Exception(f"Upload fejlede: {upload_res.status_code}") # 5. Checkin self.set_status(f"Tjekker '{file_name}' ind...") - checkin_res = requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "Opdateret via SP Explorer"}) - if checkin_res.status_code not in [200, 204]: - print(f"Checkin info: {checkin_res.status_code} - {checkin_res.text}") + requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "Opdateret via SP Explorer"}) os.remove(local_path) - self.set_status(f"Succes! '{file_name}' er opdateret på SharePoint.") - messagebox.showinfo("Færdig", f"Filen '{file_name}' er gemt og tjekket ind korrekt.") + self.set_status(f"Succes! '{file_name}' er opdateret.") + wx.CallAfter(wx.MessageBox, f"Filen '{file_name}' er gemt og tjekket ind korrekt.", "Færdig", wx.OK | wx.ICON_INFORMATION) except Exception as e: self.set_status(f"Fejl: {str(e)}") - print(f"DETALJERET FEJL: {e}") - messagebox.showerror("Fejl", f"Der skete en fejl:\n{e}") + wx.CallAfter(wx.MessageBox, f"Der skete en fejl:\n{e}", "Fejl", wx.OK | wx.ICON_ERROR) if __name__ == "__main__": - app = SharePointApp() - app.mainloop() + app = wx.App() + SharePointApp() + app.MainLoop()