From 9ccbcbaf0c90a3aea1a34f7d970690ee8d238ca5 Mon Sep 17 00:00:00 2001 From: Martin Tranberg Date: Tue, 31 Mar 2026 11:21:41 +0200 Subject: [PATCH] feat: implement multi-language support, file drag-and-drop, and new UI controls for file management --- sharepoint_browser.py | 561 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 493 insertions(+), 68 deletions(-) diff --git a/sharepoint_browser.py b/sharepoint_browser.py index 48d221e..79df2b3 100644 --- a/sharepoint_browser.py +++ b/sharepoint_browser.py @@ -23,29 +23,177 @@ def load_settings(): default_settings = { "client_id": "DIN_CLIENT_ID_HER", "tenant_id": "DIN_TENANT_ID_HER", - "temp_dir": "C:\\Temp_SP" + "temp_dir": "C:\\Temp_SP", + "language": "da" # da eller en } if not os.path.exists(SETTINGS_FILE): - with open(SETTINGS_FILE, 'w') as f: + with open(SETTINGS_FILE, 'w', encoding='utf-8') as f: json.dump(default_settings, f, indent=4) return default_settings - with open(SETTINGS_FILE, 'r') as f: + with open(SETTINGS_FILE, 'r', encoding='utf-8') as f: try: return json.load(f) except: return default_settings +def save_settings(new_settings): + with open(SETTINGS_FILE, 'w', encoding='utf-8') as f: + json.dump(new_settings, f, indent=4) + settings = load_settings() CLIENT_ID = settings.get("client_id") TENANT_ID = settings.get("tenant_id") AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}" SCOPES = ["Files.ReadWrite.All", "Sites.Read.All", "User.Read"] TEMP_DIR = settings.get("temp_dir", "C:\\Temp_SP") +CURRENT_LANG = settings.get("language", "da") + +# --- TRANSLATIONS --- +STRINGS = { + "da": { + "title": "SharePoint Explorer", + "btn_back": "← Tilbage", + "btn_home": "🏠 Hjem", + "btn_save_changes": "💾 Gem ændringer i SharePoint", + "btn_login": "Log ind", + "btn_logged_in": "Logget ind", + "btn_upload_file": "📤 Upload Fil", + "btn_upload_folder": "📁 Upload Mappe", + "btn_new_folder": "➕ Ny Mappe", + "col_name": "Navn", + "col_type": "Type", + "col_size": "Størrelse", + "col_modified": "Sidst ændret", + "type_folder": "Mappe", + "type_file": "Fil", + "type_drive": "Bibliotek", + "type_site": "Site", + "status_ready": "Klar", + "status_logging_in": "Logger ind...", + "status_fetching_sites": "Henter sites...", + "status_loading": "Indlæser...", + "status_loading_content": "Indlæser indhold...", + "status_fetching_drives": "Henter biblioteker...", + "msg_confirm_delete_single": "Er du sikker på, at du vil slette '{name}' permanent fra SharePoint?", + "msg_confirm_delete_multi": "Er du sikker på, at du vil slette {num} markerede emner permanent fra SharePoint?\n\n{names}", + "msg_delete_title": "Bekræft sletning", + "msg_error": "Fejl", + "msg_success": "Succes!", + "msg_edit_file": "Rediger fil", + "msg_delete": "Slet", + "msg_rename": "Omdøb", + "msg_rename_prompt": "Indtast det nye navn for '{name}':", + "msg_rename_title": "Omdøb emne", + "msg_upload_here": "Upload fil her", + "msg_upload_folder_here": "Upload mappe her", + "msg_new_folder_here": "Opret ny mappe her", + "msg_uploading": "Uploader '{name}'...", + "msg_creating_folder": "Opretter mappen '{name}'...", + "msg_folder_done": "Mappe '{name}' færdig.", + "msg_new_folder_prompt": "Indtast navnet på den nye mappe:", + "msg_new_folder_title": "Ny Mappe", + "msg_drop_info": "Du kan kun uploade filer, når du er inde i et bibliotek eller en mappe.", + "msg_drop_title": "Vælg lokation", + "msg_select_file": "Vælg fil til upload", + "msg_select_folder": "Vælg mappe til upload", + "msg_edit_warning": "Du er i gang med at redigere en fil. Luk din editor og gem ændringerne før du lukker programmet.", + "msg_login_failed": "Login fejlede.", + "msg_upload_success": "'{name}' uploadet succesfuldt.", + "msg_upload_failed": "Upload af '{name}' fejlede med kode {code}", + "msg_delete_failed": "Kunne ikke slette '{name}'. Stopper...", + "msg_deleted_status": "Slettet {count} af {total} emner." + }, + "en": { + "title": "SharePoint Explorer", + "btn_back": "← Back", + "btn_home": "🏠 Home", + "btn_save_changes": "💾 Save changes to SharePoint", + "btn_login": "Login", + "btn_logged_in": "Logged in", + "btn_upload_file": "📤 Upload File", + "btn_upload_folder": "📁 Upload Folder", + "btn_new_folder": "➕ New Folder", + "col_name": "Name", + "col_type": "Type", + "col_size": "Size", + "col_modified": "Last Modified", + "type_folder": "Folder", + "type_file": "File", + "type_drive": "Library", + "type_site": "Site", + "status_ready": "Ready", + "status_logging_in": "Logging in...", + "status_fetching_sites": "Fetching sites...", + "status_loading": "Loading...", + "status_loading_content": "Loading content...", + "status_fetching_drives": "Fetching libraries...", + "msg_confirm_delete_single": "Are you sure you want to permanently delete '{name}' from SharePoint?", + "msg_confirm_delete_multi": "Are you sure you want to permanently delete {num} selected items from SharePoint?\n\n{names}", + "msg_delete_title": "Confirm Delete", + "msg_error": "Error", + "msg_success": "Success!", + "msg_edit_file": "Edit file", + "msg_delete": "Delete", + "msg_rename": "Rename", + "msg_rename_prompt": "Enter new name for '{name}':", + "msg_rename_title": "Rename item", + "msg_upload_here": "Upload file here", + "msg_upload_folder_here": "Upload folder here", + "msg_new_folder_here": "Create new folder here", + "msg_uploading": "Uploading '{name}'...", + "msg_creating_folder": "Creating folder '{name}'...", + "msg_folder_done": "Folder '{name}' finished.", + "msg_new_folder_prompt": "Enter the name of the new folder:", + "msg_new_folder_title": "New Folder", + "msg_drop_info": "You can only upload files when you are inside a library or a folder.", + "msg_drop_title": "Select Location", + "msg_select_file": "Select file to upload", + "msg_select_folder": "Select folder to upload", + "msg_edit_warning": "An editing task is active. Please close your editor and save changes before closing the app.", + "msg_login_failed": "Login failed.", + "msg_upload_success": "'{name}' uploaded successfully.", + "msg_upload_failed": "Upload of '{name}' failed with status {code}", + "msg_delete_failed": "Could not delete '{name}'. Stopping...", + "msg_deleted_status": "Deleted {count} of {total} items." + } +} if not os.path.exists(TEMP_DIR): os.makedirs(TEMP_DIR) +class UploadDropTarget(wx.FileDropTarget): + def __init__(self, window, app): + wx.FileDropTarget.__init__(self) + self.window = window + self.app = app + + def OnDropFiles(self, x, y, filenames): + if not self.app.current_drive_id: + wx.MessageBox(self.app.get_txt("msg_drop_info"), + self.app.get_txt("msg_drop_title"), wx.OK | wx.ICON_INFORMATION) + return False + + for path in filenames: + if os.path.isfile(path): + threading.Thread(target=self.app._upload_file_bg, + args=(path, self.app.current_drive_id, self.app.current_folder_id), + daemon=True).start() + elif os.path.isdir(path): + threading.Thread(target=self.app._upload_folder_bg, + args=(path, self.app.current_drive_id, self.app.current_folder_id), + daemon=True).start() + return True + +def get_file_hash(path): + if not os.path.exists(path): + return None + sha256_hash = hashlib.sha256() + with open(path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() + def format_size(bytes_num): if bytes_num is None: return "" @@ -60,7 +208,8 @@ def format_size(bytes_num): class SharePointApp(wx.Frame): def __init__(self): - super().__init__(None, title="SharePoint Explorer", size=(1000, 750)) + self.lang = CURRENT_LANG + super().__init__(None, title=self.get_txt("title"), size=(1000, 750)) # State self.access_token = None @@ -91,6 +240,15 @@ class SharePointApp(wx.Frame): self.Show() self.Bind(wx.EVT_CLOSE, self.on_close_window) + def get_txt(self, key, **kwargs): + text = STRINGS[self.lang].get(key, key) + if kwargs: + try: + return text.format(**kwargs) + except: + pass + return text + def InitUI(self): panel = wx.Panel(self) main_sizer = wx.BoxSizer(wx.VERTICAL) @@ -129,14 +287,36 @@ class SharePointApp(wx.Frame): self.done_btn.Bind(wx.EVT_BUTTON, self.on_done_editing_clicked) nav_hbox.Add(self.done_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 10) + # NYE KNAPPER: Upload og Ny Mappe (Vises kun når man er inde i et drev/mappe) + self.upload_btn = wx.Button(nav_panel, label="📤 Upload Fil", size=(120, 30)) + self.upload_btn.Hide() + self.upload_btn.Bind(wx.EVT_BUTTON, self.on_upload_clicked) + nav_hbox.Add(self.upload_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) + + self.upload_folder_btn = wx.Button(nav_panel, label="📁 Upload Mappe", size=(120, 30)) + self.upload_folder_btn.Hide() + self.upload_folder_btn.Bind(wx.EVT_BUTTON, self.on_upload_folder_clicked) + nav_hbox.Add(self.upload_folder_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) + + self.new_folder_btn = wx.Button(nav_panel, label="➕ Ny Mappe", size=(100, 30)) + self.new_folder_btn.Hide() + self.new_folder_btn.Bind(wx.EVT_BUTTON, self.on_new_folder_clicked) + nav_hbox.Add(self.new_folder_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) + nav_hbox.AddStretchSpacer(1) - self.login_btn = wx.Button(nav_panel, label="Log ind", size=(120, 30)) + self.login_btn = wx.Button(nav_panel, label=self.get_txt("btn_login"), 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) + # SPROG VÆLGER + self.lang_choice = wx.Choice(nav_panel, choices=["Dansk", "English"]) + self.lang_choice.SetSelection(0 if self.lang == "da" else 1) + self.lang_choice.Bind(wx.EVT_CHOICE, self.on_language_changed) + nav_hbox.Add(self.lang_choice, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) + nav_panel.SetSizer(nav_hbox) vbox.Add(nav_panel, 0, wx.EXPAND | wx.ALL, 5) @@ -167,6 +347,9 @@ class SharePointApp(wx.Frame): 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) + + # AKTIVER DRAG & DROP + self.list_ctrl.SetDropTarget(UploadDropTarget(self.list_ctrl, self)) self.splitter.SplitVertically(self.tree_ctrl, self.list_ctrl, 250) self.splitter.SetMinimumPaneSize(100) @@ -181,15 +364,187 @@ class SharePointApp(wx.Frame): 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) + selected_indices = [] + idx = self.list_ctrl.GetFirstSelected() + while idx != -1: + selected_indices.append(idx) + idx = self.list_ctrl.GetNextSelected(idx) + + menu = wx.Menu() + + if selected_indices: + # Menu for de valgte emner + selected_items = [self.current_items[i] for i in selected_indices] + + if len(selected_indices) == 1: + item = selected_items[0] + if item['type'] == "FILE": + edit_item = menu.Append(wx.ID_ANY, self.get_txt("msg_edit_file")) + self.Bind(wx.EVT_MENU, lambda e: threading.Thread(target=self.process_file, args=(item['id'], item['name']), daemon=True).start(), edit_item) + + if item['type'] in ["FILE", "FOLDER"]: + rename_item = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_rename')} '{item['name']}'") + self.Bind(wx.EVT_MENU, lambda e: self.on_rename_clicked(item), rename_item) + + delete_item = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_delete')} '{item['name']}'") + self.Bind(wx.EVT_MENU, lambda e: self.on_delete_items_clicked(selected_items), delete_item) + else: + # Flere emner valgt + delete_items = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_delete')} {len(selected_indices)} " + ("emner" if self.lang == "da" else "items")) + self.Bind(wx.EVT_MENU, lambda e: self.on_delete_items_clicked(selected_items), delete_items) + else: + # Menu for selve mappen (hvis man trykker på det tomme felt) + if self.current_drive_id: + upload_item = menu.Append(wx.ID_ANY, self.get_txt("msg_upload_here")) + self.Bind(wx.EVT_MENU, self.on_upload_clicked, upload_item) + + upload_dir_item = menu.Append(wx.ID_ANY, self.get_txt("msg_upload_folder_here")) + self.Bind(wx.EVT_MENU, self.on_upload_folder_clicked, upload_dir_item) + + new_folder_item = menu.Append(wx.ID_ANY, self.get_txt("msg_new_folder_here")) + self.Bind(wx.EVT_MENU, self.on_new_folder_clicked, new_folder_item) + + if menu.GetMenuItemCount() > 0: self.PopupMenu(menu) - menu.Destroy() + menu.Destroy() + + # --- FILHÅNDTERING (Upload, Slet, Ny Mappe) --- + def on_delete_items_clicked(self, items): + if not items: return + + names = ", ".join([f"'{i['name']}'" for i in items[:3]]) + if len(items) > 3: + names += f" og {len(items)-3} andre..." if self.lang == "da" else f" and {len(items)-3} others..." + + msg = self.get_txt("msg_confirm_delete_multi", num=len(items), names=names) + res = wx.MessageBox(msg, self.get_txt("msg_delete_title"), wx.YES_NO | wx.ICON_WARNING) + if res == wx.YES: + threading.Thread(target=self._delete_multiple_bg, args=(items,), daemon=True).start() + + def _delete_multiple_bg(self, items): + self.lock_ui(True) + count = 0 + total = len(items) + + for item in items: + status_text = "Sletter" if self.lang == "da" else "Deleting" + self.set_status(f"{status_text} {count+1}/{total}: '{item['name']}'...") + url = f"https://graph.microsoft.com/v1.0/drives/{item['drive_id']}/items/{item['id']}" + res = requests.delete(url, headers=self.headers) + if res.status_code in [204, 200]: + count += 1 + else: + self.set_status(self.get_txt("msg_delete_failed", name=item['name'])) + wx.CallAfter(wx.MessageBox, f"Error deleting '{item['name']}': {res.status_code}", self.get_txt("msg_error"), wx.OK | wx.ICON_ERROR) + break + + self._refresh_current_view() + self.lock_ui(False) + self.set_status(self.get_txt("msg_deleted_status", count=count, total=total)) + + def on_upload_clicked(self, event): + if not self.current_drive_id: return + with wx.FileDialog(self, self.get_txt("msg_select_file"), style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fd: + if fd.ShowModal() == wx.ID_OK: + path = fd.GetPath() + threading.Thread(target=self._upload_file_bg, args=(path, self.current_drive_id, self.current_folder_id), daemon=True).start() + + def _upload_file_bg(self, local_path, drive_id, parent_id): + filename = os.path.basename(local_path) + self.set_status(self.get_txt("msg_uploading", name=filename)) + # Simpel upload (virker op til 4MB) + url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{parent_id}:/{filename}:/content" + try: + with open(local_path, 'rb') as f: + res = requests.put(url, headers=self.headers, data=f) + if res.status_code in [200, 201]: + self.set_status(self.get_txt("msg_upload_success", name=filename)) + self._refresh_current_view() + else: + self.set_status(self.get_txt("msg_upload_failed", name=filename, code=res.status_code)) + wx.CallAfter(wx.MessageBox, self.get_txt("msg_upload_failed", name=filename, code=res.status_code), self.get_txt("msg_error"), wx.OK | wx.ICON_ERROR) + except Exception as e: + self.set_status(f"Upload error: {e}") + + def on_upload_folder_clicked(self, event): + if not self.current_drive_id: return + with wx.DirDialog(self, self.get_txt("msg_select_folder"), style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST) as dd: + if dd.ShowModal() == wx.ID_OK: + path = dd.GetPath() + threading.Thread(target=self._upload_folder_bg, args=(path, self.current_drive_id, self.current_folder_id), daemon=True).start() + + def _upload_folder_bg(self, local_dir, drive_id, parent_id): + dirname = os.path.basename(local_dir) + self.set_status(self.get_txt("msg_creating_folder", name=dirname)) + + # 1. Opret mappen på SharePoint + folder_id = self._create_folder_sync(dirname, drive_id, parent_id) + if not folder_id: + return + + # 2. Upload filer i mappen + for item in os.listdir(local_dir): + full_path = os.path.join(local_dir, item) + if os.path.isfile(full_path): + self._upload_file_bg_sync(full_path, drive_id, folder_id) + elif os.path.isdir(full_path): + self._upload_folder_bg(full_path, drive_id, folder_id) # Rekursivt + + self.set_status(self.get_txt("msg_folder_done", name=dirname)) + self._refresh_current_view() + + def _create_folder_sync(self, name, drive_id, parent_id): + url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{parent_id}/children" + body = {"name": name, "folder": {}, "@microsoft.graph.conflictBehavior": "rename"} + res = requests.post(url, headers=self.headers, json=body) + if res.status_code in [200, 201]: + return res.json().get('id') + return None + + def _upload_file_bg_sync(self, local_path, drive_id, parent_id): + # Hjælper til sync upload brugt af mappe-upload + filename = os.path.basename(local_path) + url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{parent_id}:/{filename}:/content" + with open(local_path, 'rb') as f: + requests.put(url, headers=self.headers, data=f) + + def on_new_folder_clicked(self, event): + if not self.current_drive_id: return + dlg = wx.TextEntryDialog(self, self.get_txt("msg_new_folder_prompt"), self.get_txt("msg_new_folder_title")) + if dlg.ShowModal() == wx.ID_OK: + name = dlg.GetValue() + if name: + threading.Thread(target=self._create_folder_bg, args=(name, self.current_drive_id, self.current_folder_id), daemon=True).start() + dlg.Destroy() + + def _create_folder_bg(self, name, drive_id, parent_id): + self.set_status(self.get_txt("msg_creating_folder", name=name)) + folder_id = self._create_folder_sync(name, drive_id, parent_id) + if folder_id: + self.set_status(self.get_txt("msg_success")) + self._refresh_current_view() + else: + self.set_status(self.get_txt("msg_error")) + + def on_rename_clicked(self, item): + dlg = wx.TextEntryDialog(self, self.get_txt("msg_rename_prompt", name=item['name']), self.get_txt("msg_rename_title"), item['name']) + if dlg.ShowModal() == wx.ID_OK: + new_name = dlg.GetValue() + if new_name and new_name != item['name']: + threading.Thread(target=self._rename_item_bg, args=(item, new_name), daemon=True).start() + dlg.Destroy() + + def _rename_item_bg(self, item, new_name): + self.set_status(f"{self.get_txt('msg_rename')}...") + url = f"https://graph.microsoft.com/v1.0/drives/{item['drive_id']}/items/{item['id']}" + body = {"name": new_name} + res = requests.patch(url, headers=self.headers, json=body) + if res.status_code in [200, 201]: + self.set_status(self.get_txt("msg_success")) + self._refresh_current_view() + else: + self.set_status(self.get_txt("msg_error")) + wx.CallAfter(wx.MessageBox, f"Rename failed: {res.status_code}", self.get_txt("msg_error"), wx.OK | wx.ICON_ERROR) def set_status(self, text): wx.CallAfter(self.status_bar.SetStatusText, text) @@ -210,9 +565,48 @@ class SharePointApp(wx.Frame): self.edit_wait_event.set() self.info_bar.Dismiss() + def on_language_changed(self, event): + selection = self.lang_choice.GetSelection() + self.lang = "da" if selection == 0 else "en" + + # Gem til settings + settings["language"] = self.lang + save_settings(settings) + + # Opdater UI tekster med det samme + self.SetTitle(self.get_txt("title")) + self.back_btn.SetLabel(self.get_txt("btn_back")) + self.home_btn.SetLabel(self.get_txt("btn_home")) + self.done_btn.SetLabel(self.get_txt("btn_save_changes")) + self.upload_btn.SetLabel(self.get_txt("btn_upload_file")) + self.upload_folder_btn.SetLabel(self.get_txt("btn_upload_folder")) + self.new_folder_btn.SetLabel(self.get_txt("btn_new_folder")) + + if self.access_token: + self.login_btn.SetLabel(self.get_txt("btn_logged_in")) + else: + self.login_btn.SetLabel(self.get_txt("btn_login")) + + # Opdater kolonner i ListCtrl + self.list_ctrl.SetColumnWidth(0, 450) # Refresh widths + item = self.list_ctrl.GetColumn(0) + self.list_ctrl.SetColumn(0, wx.ListItem()) # Reset column header logic could be complex in wx, + # but the simplest is to just re-insert columns or set text + + # Re-set headers (Fix: explicitly set image to -1 to avoid icons in headers) + cols = [self.get_txt("col_name"), self.get_txt("col_type"), self.get_txt("col_size"), self.get_txt("col_modified")] + for i, text in enumerate(cols): + info = self.list_ctrl.GetColumn(i) + info.SetText(text) + info.SetImage(-1) + self.list_ctrl.SetColumn(i, info) + + self.set_status(self.get_txt("status_ready")) + self._refresh_current_view() # Gendanner list-item tekster (Mappe/Fil) + def on_close_window(self, event): if self.is_editing: - self.show_info("Du er i gang med at redigere en fil. Luk din editor og gem ændringerne før du lukker programmet.", wx.ICON_WARNING, auto_hide=False) + self.show_info(self.get_txt("msg_edit_warning"), wx.ICON_WARNING, auto_hide=False) return event.Skip() @@ -284,7 +678,7 @@ class SharePointApp(wx.Frame): self.path_sizer.Add(btn, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) def login(self, event): - self.set_status("Logger ind...") + self.set_status(self.get_txt("status_logging_in")) app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY) accounts = app.get_accounts() result = None @@ -297,16 +691,16 @@ class SharePointApp(wx.Frame): self.access_token = result["access_token"] self.headers = {'Authorization': f'Bearer {self.access_token}'} self.login_btn.Disable() - self.login_btn.SetLabel("Logget ind") + self.login_btn.SetLabel(self.get_txt("btn_logged_in")) self.login_btn.SetBackgroundColour(wx.Colour(200, 200, 200)) # Grå self.home_btn.Enable() self.load_sites() else: - self.set_status("Login fejlede.") - wx.MessageBox(result.get("error_description", "Unknown error"), "Login Error", wx.OK | wx.ICON_ERROR) + self.set_status(self.get_txt("status_login_failed")) + wx.MessageBox(result.get("error_description", "Unknown error"), self.get_txt("msg_error"), wx.OK | wx.ICON_ERROR) def load_sites(self, event=None): - self.set_status("Henter sites...") + self.set_status(self.get_txt("status_fetching_sites")) self.tree_ctrl.DeleteAllItems() self.list_ctrl.DeleteAllItems() self.current_items = [] @@ -331,7 +725,7 @@ class SharePointApp(wx.Frame): self.set_status("Kunne ikke hente sites.") def _populate_sites_tree(self, sites): - self.set_status(f"Fandt {len(sites)} sites.") + self.set_status(f"{len(sites)} sites.") for site in sites: name = site.get('displayName', site.get('name')) node = self.tree_ctrl.AppendItem(self.tree_root, name, image=self.idx_site) @@ -347,7 +741,7 @@ class SharePointApp(wx.Frame): for i, site in enumerate(sites): name = site.get('displayName', site.get('name')) self.list_ctrl.InsertItem(i, name, self.idx_site) - self.list_ctrl.SetItem(i, 1, "Site") + self.list_ctrl.SetItem(i, 1, self.get_txt("type_site")) self.list_ctrl.SetItem(i, 2, "") # Størrelse self.list_ctrl.SetItem(i, 3, "") # Sidst ændret self.current_items.append({"type": "SITE", "id": site['id'], "name": name, "size": None, "modified": ""}) @@ -358,7 +752,7 @@ class SharePointApp(wx.Frame): if not data or data.get("loaded"): return - loading_node = self.tree_ctrl.AppendItem(item, "Indlæser...") + loading_node = self.tree_ctrl.AppendItem(item, self.get_txt("status_loading")) threading.Thread(target=self._fetch_tree_children_bg, args=(item, data), daemon=True).start() def _fetch_tree_children_bg(self, parent_node, data): @@ -429,25 +823,30 @@ class SharePointApp(wx.Frame): self.tree_ctrl.SelectItem(target_node) def on_tree_selected(self, event): - if self.is_editing: return + if not self or self.is_editing: return item = event.GetItem() data = self.tree_item_data.get(item) if not data: return - self.current_path = data["path"] - if self: + try: + 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() + + if not self.is_navigating_back: + self.history.append(item) + + # Check if button still exists + if self.back_btn: + self.back_btn.Enable(len(self.history) > 1) + + self.list_ctrl.DeleteAllItems() + self.current_items = [] + self.set_status(self.get_txt("status_loading_content")) + + threading.Thread(target=self._fetch_list_contents_bg, args=(data,), daemon=True).start() + except RuntimeError: + pass def _fetch_list_contents_bg(self, data): items_data = [] @@ -486,32 +885,50 @@ class SharePointApp(wx.Frame): 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): - img_idx = self.idx_file - if item['type'] == "FOLDER": img_idx = self.idx_folder - elif item['type'] == "DRIVE": img_idx = self.idx_drive - elif item['type'] == "SITE": img_idx = self.idx_site + if not self: return + try: + self.list_ctrl.DeleteAllItems() + self.current_items = [] + for i, item in enumerate(items_data): + img_idx = self.idx_file + if item['type'] == "FOLDER": img_idx = self.idx_folder + elif item['type'] == "DRIVE": img_idx = self.idx_drive + elif item['type'] == "SITE": img_idx = self.idx_site + + self.list_ctrl.InsertItem(i, item['name'], img_idx) + type_str = self.get_txt("type_folder") if item['type'] == "FOLDER" else self.get_txt("type_file") if item['type'] == "FILE" else self.get_txt("type_drive") + self.list_ctrl.SetItem(i, 1, type_str) + size_str = format_size(item['size']) if item['size'] is not None else "" + self.list_ctrl.SetItem(i, 2, size_str) + self.list_ctrl.SetItem(i, 3, item['modified']) + self.current_items.append(item) + + self.set_status(self.get_txt("status_ready")) - self.list_ctrl.InsertItem(i, item['name'], img_idx) - type_str = "Mappe" if item['type'] == "FOLDER" else "Fil" if item['type'] == "FILE" else "Bibliotek" - self.list_ctrl.SetItem(i, 1, type_str) - size_str = format_size(item['size']) if item['size'] is not None else "" - self.list_ctrl.SetItem(i, 2, size_str) - self.list_ctrl.SetItem(i, 3, 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'] + 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'] + + # Opdater knap-synlighed + can_upload = self.current_drive_id is not None + wx.CallAfter(lambda: self._safe_update_buttons(can_upload)) + except RuntimeError: + pass + + def _safe_update_buttons(self, can_upload): + try: + if not self: return + self.upload_btn.Show(can_upload) + self.upload_folder_btn.Show(can_upload) + self.new_folder_btn.Show(can_upload) + self.Layout() + except RuntimeError: + pass def on_item_activated(self, event): if self.is_editing: return @@ -579,6 +996,9 @@ class SharePointApp(wx.Frame): with open(local_path, 'wb') as f: f.write(res.content) + # Beregn udgangspunkt hash + original_hash = get_file_hash(local_path) + # Checkout requests.post(f"{base_url}/checkout", headers=self.headers) @@ -614,16 +1034,21 @@ class SharePointApp(wx.Frame): wx.CallAfter(self.done_btn.Hide) wx.CallAfter(self.Layout) - # 4. Upload - 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}") + # 4. Tjek om noget er ændret + new_hash = get_file_hash(local_path) + if original_hash == new_hash: + self.set_status("Ingen ændringer fundet. Springer upload over.") + else: + # 5. Upload (kun hvis ændret) + 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}") - # 5. Checkin + # 6. Checkin (Uanset om ændret eller ej, for at frigive lås) self.set_status(f"Tjekker '{file_name}' ind...") - requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "Opdateret via SP Explorer"}) + requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "SP Explorer Edit"}) # Oprydning: Slet fil og derefter mappe try: