feat: implement multi-language support, file drag-and-drop, and new UI controls for file management

This commit is contained in:
Martin Tranberg
2026-03-31 11:21:41 +02:00
parent 07913c0224
commit 9ccbcbaf0c

View File

@@ -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: