Files
sharepoint-browser/sharepoint_browser.py

1719 lines
78 KiB
Python

import os
import time
import threading
import hashlib
import json
import sys
import requests
import msal
import wx
import wx.lib.newevent
import webbrowser
# --- STIHÅNDTERING (Til EXE-brug) ---
if getattr(sys, 'frozen', False):
# RESOURCE_DIR er mapper indeni EXE-filen (ikoner, billeder)
RESOURCE_DIR = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
# CONFIG_DIR er mappen hvor selve .exe filen ligger (settings.json)
CONFIG_DIR = os.path.dirname(sys.executable)
else:
RESOURCE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_DIR = RESOURCE_DIR
SETTINGS_FILE = os.path.join(CONFIG_DIR, 'settings.json')
def load_settings():
default_settings = {
"client_id": "DIN_CLIENT_ID_HER",
"tenant_id": "DIN_TENANT_ID_HER",
"temp_dir": "C:\\Temp_SP",
"language": "da", # da eller en
"favorites": [], # Liste over {id, name, type, drive_id, site_id, path}
"fav_visible": True,
"license_key": ""
}
if not os.path.exists(SETTINGS_FILE):
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', 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_refresh": "Opdater",
"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_open_browser": "Åbn i browser",
"msg_download": "Download",
"msg_downloading_to": "Downloader '{name}' til '{path}'...",
"msg_download_done": "'{name}' downloadet færdig.",
"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.",
"msg_fetching_file": "Henter '{name}'...",
"msg_opening_file": "Åbner '{name}'...",
"msg_waiting_for_file": "Venter på '{name}'...",
"msg_editing_file": "Redigerer '{name}' - Luk for at gemme.",
"msg_file_unchanged": "Ingen ændringer fundet. Springer upload over.",
"msg_updating_changes": "Uploader ændringer...",
"msg_checking_in": "Tjekker '{name}' ind...",
"msg_update_success": "Succes! '{name}' er opdateret.",
"msg_update_failed_code": "Upload fejlede: {code}",
"msg_unknown_error": "Ukendt fejl",
"type_unknown": "Ukendt",
"btn_settings": "Indstillinger",
"settings_title": "Indstillinger",
"settings_auth_group": "Authentication / API",
"settings_client_id": "App (Client) ID:",
"settings_tenant_id": "Tenant ID:",
"settings_path_group": "Systemstier",
"settings_temp_dir": "Midlertidig mappe:",
"settings_app_path": "Applikationssti:",
"settings_active_temp_path": "Aktuel Temp-sti:",
"settings_lang_group": "Sprog / UI",
"settings_language": "Programsprog:",
"settings_save": "Gem indstillinger",
"settings_cancel": "Annuller",
"msg_settings_saved": "Indstillingerne er gemt.",
"msg_restart_required": "Visse ændringer (f.eks. ID'er) træder først i kraft efter genstart.",
"status_login_needed": "Session udløbet. Log ind igen.",
"btn_add_fav": "Tilføj til favoritter",
"btn_remove_fav": "Fjern fra favoritter",
"label_favorites": "Favoritter",
"msg_fav_exists": "'{name}' er allerede i favoritter.",
"settings_license_group": "Licens / Aktivering",
"settings_license_key": "Licensnøgle:",
"settings_license_status": "Status: Ikke aktiveret"
},
"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_refresh": "Refresh",
"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_open_browser": "Open in browser",
"msg_download": "Download",
"msg_downloading_to": "Downloading '{name}' to '{path}'...",
"msg_download_done": "'{name}' download finished.",
"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.",
"msg_fetching_file": "Fetching '{name}'...",
"msg_opening_file": "Opening '{name}'...",
"msg_waiting_for_file": "Waiting for '{name}'...",
"msg_editing_file": "Editing '{name}' - Close window to save.",
"msg_file_unchanged": "No changes found. Skipping upload.",
"msg_updating_changes": "Uploading changes...",
"msg_checking_in": "Checking in '{name}'...",
"msg_update_success": "Success! '{name}' has been updated.",
"msg_update_failed_code": "Upload failed: {code}",
"msg_unknown_error": "Unknown error",
"type_unknown": "Unknown",
"btn_settings": "Settings",
"settings_title": "Settings",
"settings_auth_group": "Authentication / API",
"settings_client_id": "App (Client) ID:",
"settings_tenant_id": "Tenant ID:",
"settings_path_group": "System Paths",
"settings_temp_dir": "Temporary folder:",
"settings_app_path": "Application path:",
"settings_active_temp_path": "Active Temp path:",
"settings_lang_group": "Language / UI",
"settings_language": "App Language:",
"settings_save": "Save Settings",
"settings_cancel": "Cancel",
"msg_settings_saved": "Settings saved.",
"msg_restart_required": "Some changes (e.g., IDs) only take effect after restart.",
"status_login_needed": "Session expired. Please login again.",
"btn_add_fav": "Add to favorites",
"btn_remove_fav": "Remove from favorites",
"label_favorites": "Favorites",
"msg_fav_exists": "'{name}' is already in favorites.",
"settings_license_group": "License / Activation",
"settings_license_key": "License Key:",
"settings_license_status": "Status: Not activated"
}
}
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 ""
if bytes_num < 1024:
return f"{bytes_num} B"
elif bytes_num < 1024**2:
return f"{bytes_num/1024:.1f} KB"
elif bytes_num < 1024**3:
return f"{bytes_num/(1024**2):.1f} MB"
else:
return f"{bytes_num/(1024**3):.1f} GB"
def is_configured(cfg):
placeholders = ["DIN_CLIENT_ID_HER", "DIN_TENANT_ID_HER", ""]
return cfg.get("client_id") not in placeholders and cfg.get("tenant_id") not in placeholders
class SettingsDialog(wx.Dialog):
def __init__(self, parent, current_settings):
lang = current_settings.get("language", "da")
title = STRINGS[lang].get("settings_title", "Settings")
super().__init__(parent, title=title, size=(520, 720))
self.settings = current_settings.copy()
self.lang = lang
self.InitUI()
def get_txt(self, key):
return STRINGS[self.lang].get(key, key)
def InitUI(self):
vbox = wx.BoxSizer(wx.VERTICAL)
panel = wx.Panel(self)
inner_vbox = wx.BoxSizer(wx.VERTICAL)
# --- Group: Authentication ---
auth_box = wx.StaticBox(panel, label=self.get_txt("settings_auth_group"))
auth_sizer = wx.StaticBoxSizer(auth_box, wx.VERTICAL)
grid = wx.FlexGridSizer(2, 2, 10, 10)
grid.AddGrowableCol(1, 1)
grid.Add(wx.StaticText(panel, label=self.get_txt("settings_client_id")), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.client_id_ctrl = wx.TextCtrl(panel, value=self.settings.get("client_id", ""), size=(-1, 25))
grid.Add(self.client_id_ctrl, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
grid.Add(wx.StaticText(panel, label=self.get_txt("settings_tenant_id")), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.tenant_id_ctrl = wx.TextCtrl(panel, value=self.settings.get("tenant_id", ""), size=(-1, 25))
grid.Add(self.tenant_id_ctrl, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
auth_sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 10)
inner_vbox.Add(auth_sizer, 0, wx.EXPAND | wx.ALL, 10)
# --- Group: Paths ---
path_box = wx.StaticBox(panel, label=self.get_txt("settings_path_group"))
path_sizer = wx.StaticBoxSizer(path_box, wx.VERTICAL)
path_sizer.Add(wx.StaticText(panel, label=self.get_txt("settings_temp_dir")), 0, wx.BOTTOM, 5)
self.temp_dir_picker = wx.DirPickerCtrl(panel, path=self.settings.get("temp_dir", "C:\\Temp_SP"),
style=wx.DIRP_DIR_MUST_EXIST)
path_sizer.Add(self.temp_dir_picker, 0, wx.EXPAND | wx.BOTTOM, 10)
path_sizer.Add(wx.StaticText(panel, label=self.get_txt("settings_app_path")), 0, wx.BOTTOM, 5)
app_path_box = wx.TextCtrl(panel, value=CONFIG_DIR, style=wx.TE_READONLY | wx.BORDER_NONE)
app_path_box.SetBackgroundColour(panel.GetBackgroundColour())
path_sizer.Add(app_path_box, 0, wx.EXPAND | wx.BOTTOM, 10)
path_sizer.Add(wx.StaticText(panel, label=self.get_txt("settings_active_temp_path")), 0, wx.BOTTOM, 5)
temp_path_box = wx.TextCtrl(panel, value=TEMP_DIR, style=wx.TE_READONLY | wx.BORDER_NONE)
temp_path_box.SetBackgroundColour(panel.GetBackgroundColour())
path_sizer.Add(temp_path_box, 0, wx.EXPAND)
inner_vbox.Add(path_sizer, 0, wx.EXPAND | wx.ALL, 10)
# --- Group: License ---
lic_box = wx.StaticBox(panel, label=self.get_txt("settings_license_group"))
lic_sizer = wx.StaticBoxSizer(lic_box, wx.VERTICAL)
lic_sizer.Add(wx.StaticText(panel, label=self.get_txt("settings_license_key")), 0, wx.BOTTOM, 5)
self.license_ctrl = wx.TextCtrl(panel, value=self.settings.get("license_key", ""))
lic_sizer.Add(self.license_ctrl, 0, wx.EXPAND | wx.BOTTOM, 5)
status_txt = wx.StaticText(panel, label=self.get_txt("settings_license_status"))
status_txt.SetForegroundColour(wx.RED)
lic_sizer.Add(status_txt, 0, wx.TOP, 5)
inner_vbox.Add(lic_sizer, 0, wx.EXPAND | wx.ALL, 10)
# --- Group: Language ---
lang_box = wx.StaticBox(panel, label=self.get_txt("settings_lang_group"))
lang_sizer = wx.StaticBoxSizer(lang_box, wx.HORIZONTAL)
lang_sizer.Add(wx.StaticText(panel, label=self.get_txt("settings_language")), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10)
self.lang_choice = wx.Choice(panel, choices=["Dansk", "English"])
self.lang_choice.SetSelection(0 if self.settings.get("language") == "da" else 1)
lang_sizer.Add(self.lang_choice, 1, wx.EXPAND)
inner_vbox.Add(lang_sizer, 0, wx.EXPAND | wx.ALL, 10)
panel.SetSizer(inner_vbox)
inner_vbox.Fit(panel)
vbox.Add(panel, 1, wx.EXPAND | wx.ALL, 0)
# --- Buttons ---
btn_hbox = wx.BoxSizer(wx.HORIZONTAL)
save_btn = wx.Button(self, label=self.get_txt("settings_save"), size=(150, 35))
save_btn.SetBackgroundColour(wx.Colour(0, 120, 215)) # SharePoint Blue
save_btn.SetForegroundColour(wx.WHITE)
save_btn.Bind(wx.EVT_BUTTON, self.on_save)
cancel_btn = wx.Button(self, label=self.get_txt("settings_cancel"), size=(100, 35))
cancel_btn.Bind(wx.EVT_BUTTON, self.on_cancel)
btn_hbox.Add(save_btn, 0, wx.RIGHT, 10)
btn_hbox.Add(cancel_btn, 0)
vbox.Add(btn_hbox, 0, wx.ALIGN_RIGHT | wx.ALL, 15)
self.SetSizer(vbox)
self.Layout()
self.Centre()
def on_save(self, event):
self.settings["client_id"] = self.client_id_ctrl.GetValue().strip()
self.settings["tenant_id"] = self.tenant_id_ctrl.GetValue().strip()
self.settings["temp_dir"] = self.temp_dir_picker.GetPath()
self.settings["language"] = "da" if self.lang_choice.GetSelection() == 0 else "en"
self.settings["license_key"] = self.license_ctrl.GetValue().strip()
if not self.settings["client_id"] or not self.settings["tenant_id"]:
wx.MessageBox("Client ID og Tenant ID skal udfyldes.", "Fejl", wx.OK | wx.ICON_ERROR)
return
self.EndModal(wx.ID_OK)
def on_cancel(self, event):
self.EndModal(wx.ID_CANCEL)
class SharePointApp(wx.Frame):
def __init__(self):
self.lang = CURRENT_LANG
super().__init__(None, title=self.get_txt("title"), size=(1000, 750))
# State
self.access_token = None
self.headers = {}
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
self.active_edits = {} # item_id -> { "name": name, "event": Event, "waiting": bool }
self.favorites = settings.get("favorites", [])
self.fav_visible = settings.get("fav_visible", True)
# System Ikoner (Brug ART_CMN_DIALOG eller ART_TOOLBAR for mere farve på Windows)
self.image_list = wx.ImageList(16, 16)
self.idx_site = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_GO_HOME, wx.ART_CMN_DIALOG, (16, 16))) # Site
self.idx_drive = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_HARDDISK, wx.ART_CMN_DIALOG, (16, 16))) # Drive
self.idx_folder = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_CMN_DIALOG, (16, 16))) # Folder
self.idx_file = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_CMN_DIALOG, (16, 16))) # File
self.idx_star = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_CMN_DIALOG, (16, 16))) # Favorit stjerne
# Threading/Sync til filredigering
# MSAL Cache
self.msal_app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY)
self.InitUI()
self.Centre()
self.Show()
self.Bind(wx.EVT_CLOSE, self.on_close_window)
# SÆT VINDUESIKON (øverste venstre hjørne)
icon_path = os.path.join(RESOURCE_DIR, "icon.ico")
if os.path.exists(icon_path):
self.SetIcon(wx.Icon(icon_path, wx.BITMAP_TYPE_ICO))
# Start indlæsning (Check for konfiguration)
if not is_configured(settings):
wx.CallAfter(self.on_settings_clicked, None)
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)
self.SetSizer(main_sizer)
main_sizer.Add(panel, 1, wx.EXPAND)
vbox = wx.BoxSizer(wx.VERTICAL)
# 0. INFO BAR (Beskeder der ikke blokerer)
self.info_bar = wx.InfoBar(panel)
vbox.Add(self.info_bar, 0, wx.EXPAND)
# Timer til at skjule info bar
self.info_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, lambda e: self.info_bar.Dismiss(), self.info_timer)
# 1. TOP NAVIGATION BAR
nav_panel = wx.Panel(panel)
nav_hbox = wx.BoxSizer(wx.HORIZONTAL)
self.back_btn = wx.Button(nav_panel, label=self.get_txt("btn_back"), size=(110, 30))
self.back_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_GO_BACK, wx.ART_BUTTON, (16, 16)))
self.back_btn.SetBitmapMargins((12, 0))
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 = wx.Button(nav_panel, label=self.get_txt("btn_home"), size=(110, 30))
self.home_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_GO_HOME, wx.ART_BUTTON, (16, 16)))
self.home_btn.SetBitmapMargins((12, 0))
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.refresh_btn = wx.Button(nav_panel, label=self.get_txt("btn_refresh"), size=(110, 30))
self.refresh_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_REDO, wx.ART_BUTTON, (16, 16)))
self.refresh_btn.SetBitmapMargins((12, 0))
self.refresh_btn.Disable()
self.refresh_btn.Bind(wx.EVT_BUTTON, self.on_refresh)
nav_hbox.Add(self.refresh_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5)
# NY KNAP: Gem ændringer (Vises kun ved redigering)
self.done_btn = wx.Button(nav_panel, label=self.get_txt("btn_save_changes"), size=(250, 30))
self.done_btn.SetBackgroundColour(wx.Colour(255, 69, 0)) # OrangeRed
self.done_btn.SetForegroundColour(wx.WHITE)
self.done_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_BUTTON, (16, 16)))
self.done_btn.SetBitmapMargins((12, 0))
self.done_btn.Hide()
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=self.get_txt("btn_upload_file"), size=(130, 30))
self.upload_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_BUTTON, (16, 16)))
self.upload_btn.SetBitmapMargins((12, 0))
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=self.get_txt("btn_upload_folder"), size=(130, 30))
self.upload_folder_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN, wx.ART_BUTTON, (16, 16)))
self.upload_folder_btn.SetBitmapMargins((12, 0))
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=self.get_txt("btn_new_folder"), size=(120, 30))
self.new_folder_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_NEW_DIR, wx.ART_BUTTON, (16, 16)))
self.new_folder_btn.SetBitmapMargins((12, 0))
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=self.get_txt("btn_login"), size=(130, 30))
self.login_btn.SetBackgroundColour(wx.Colour(40, 167, 69)) # Grøn
self.login_btn.SetForegroundColour(wx.WHITE)
self.login_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD, wx.ART_TOOLBAR, (16, 16)))
self.login_btn.SetBitmapMargins((12, 0))
self.login_btn.Bind(wx.EVT_BUTTON, self.login)
nav_hbox.Add(self.login_btn, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 10)
# INDSTILLINGER KNAP
self.settings_btn = wx.Button(nav_panel, label=self.get_txt("btn_settings"), size=(130, 30))
self.settings_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_REPORT_VIEW, wx.ART_TOOLBAR, (16, 16)))
self.settings_btn.SetBitmapMargins((12, 0))
self.settings_btn.Bind(wx.EVT_BUTTON, self.on_settings_clicked)
nav_hbox.Add(self.settings_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. PATH BREADCRUMBS (Adresselinje-stil)
self.path_panel = wx.Panel(panel, style=wx.BORDER_SIMPLE)
self.path_panel.SetBackgroundColour(wx.WHITE)
self.path_panel.SetMinSize((-1, 38)) # Sikrer synbarhed
self.path_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.path_panel.SetSizer(self.path_sizer)
vbox.Add(self.path_panel, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
# 3. SPLITTER FOR TREE AND LIST
self.splitter = wx.SplitterWindow(panel, style=wx.SP_LIVE_UPDATE | wx.SP_3DSASH)
# Left side: Tree and Favorites Container
self.left_container = wx.Panel(self.splitter)
self.left_vbox = wx.BoxSizer(wx.VERTICAL)
self.left_container.SetSizer(self.left_vbox)
# LEFT SIDE - TOP: Tree
self.tree_ctrl = wx.TreeCtrl(self.left_container, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.BORDER_SUNKEN)
self.tree_ctrl.AssignImageList(self.image_list)
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)
self.tree_ctrl.Bind(wx.EVT_TREE_ITEM_MENU, self.on_tree_right_click)
self.left_vbox.Add(self.tree_ctrl, 1, wx.EXPAND)
# LEFT SIDE - BOTTOM: Favorites
self.fav_section = wx.Panel(self.left_container)
self.fav_vbox = wx.BoxSizer(wx.VERTICAL)
self.fav_section.SetSizer(self.fav_vbox)
# Fav Header (Toggle)
self.fav_header = wx.Panel(self.fav_section)
self.fav_header.SetBackgroundColour(wx.Colour(240, 240, 240))
h_hbox = wx.BoxSizer(wx.HORIZONTAL)
# Star Icon
star_bmp = wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_MENU, (16, 16))
self.fav_icon = wx.StaticBitmap(self.fav_header, bitmap=star_bmp)
h_hbox.Add(self.fav_icon, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5)
self.fav_label = wx.StaticText(self.fav_header, label=self.get_txt("label_favorites"))
self.fav_label.SetFont(wx.Font(9, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
h_hbox.Add(self.fav_label, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 5)
self.fav_toggle_btn = wx.Button(self.fav_header, label="" if self.fav_visible else "", size=(25, 25), style=wx.BU_EXACTFIT)
self.fav_toggle_btn.Bind(wx.EVT_BUTTON, self.toggle_favorites)
h_hbox.Add(self.fav_toggle_btn, 0, wx.ALL, 2)
self.fav_header.SetSizer(h_hbox)
self.fav_vbox.Add(self.fav_header, 0, wx.EXPAND)
# Fav List
self.fav_list = wx.ListCtrl(self.fav_section, style=wx.LC_REPORT | wx.LC_NO_HEADER | wx.BORDER_SUNKEN)
self.fav_list.AssignImageList(self.image_list, wx.IMAGE_LIST_SMALL)
self.fav_list.InsertColumn(0, "Name", width=250)
self.fav_list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_favorite_activated)
self.fav_list.Bind(wx.EVT_CONTEXT_MENU, self.on_favorite_right_click)
self.fav_vbox.Add(self.fav_list, 1, wx.EXPAND)
self.left_vbox.Add(self.fav_section, 0, wx.EXPAND)
if not self.fav_visible:
self.fav_list.Hide()
else:
self.left_vbox.SetItemMinSize(self.fav_section, -1, 200)
# Right side: File Area - ListCtrl
self.list_ctrl = wx.ListCtrl(self.splitter, style=wx.LC_REPORT | wx.BORDER_SUNKEN)
self.list_ctrl.AssignImageList(self.image_list, wx.IMAGE_LIST_SMALL)
self.list_ctrl.InsertColumn(0, self.get_txt("col_name"), width=450)
self.list_ctrl.InsertColumn(1, self.get_txt("col_type"), width=120)
self.list_ctrl.InsertColumn(2, self.get_txt("col_size"), width=80)
self.list_ctrl.InsertColumn(3, self.get_txt("col_modified"), width=180)
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.left_container, self.list_ctrl, 250)
self.splitter.SetMinimumPaneSize(100)
vbox.Add(self.splitter, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
# Load initial favorites
self.refresh_fav_list()
# 4. STATUS BAR
self.status_bar = self.CreateStatusBar()
self.status_bar.SetStatusText(self.get_txt("status_ready"))
panel.SetSizer(vbox)
self.Layout()
def on_right_click(self, event):
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'] in ["FOLDER", "DRIVE", "SITE"]:
fav_item = menu.Append(wx.ID_ANY, self.get_txt("btn_add_fav"))
fav_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, lambda e, i=item: self.add_favorite(i), fav_item)
menu.AppendSeparator()
if item['type'] == "FILE":
edit_item = menu.Append(wx.ID_ANY, self.get_txt("msg_edit_file"))
edit_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_EDIT, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, lambda e, i=item: self.open_file(i), edit_item)
if item['type'] in ["FILE", "FOLDER"]:
rename_item = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_rename')} '{item['name']}'")
rename_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_REPORT_VIEW, wx.ART_MENU, (16, 16)))
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']}'")
delete_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_DELETE, wx.ART_MENU, (16, 16)))
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"))
delete_items.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_DELETE, wx.ART_MENU, (16, 16)))
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"))
upload_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_MENU, (16, 16)))
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"))
upload_dir_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN, wx.ART_MENU, (16, 16)))
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"))
new_folder_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_NEW_DIR, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, self.on_new_folder_clicked, new_folder_item)
# Tilføj altid opdater punkt til sidst
if menu.GetMenuItemCount() > 0:
menu.AppendSeparator()
refresh_item = menu.Append(wx.ID_ANY, self.get_txt("btn_refresh"))
refresh_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_REDO, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, self.on_refresh, refresh_item)
self.PopupMenu(menu)
menu.Destroy()
def on_tree_right_click(self, event):
item = event.GetItem()
if not item.IsOk() or item == self.tree_root: return
self.tree_ctrl.SelectItem(item)
data = self.tree_item_data.get(item)
menu = wx.Menu()
if data:
fav_item = menu.Append(wx.ID_ANY, self.get_txt("btn_add_fav"))
fav_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, lambda e, d=data: self.add_favorite(d), fav_item)
menu.AppendSeparator()
refresh_item = menu.Append(wx.ID_ANY, self.get_txt("btn_refresh"))
refresh_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_REDO, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, self.on_refresh, refresh_item)
self.PopupMenu(menu)
menu.Destroy()
# --- FAVORITES LOGIC ---
def add_favorite(self, item):
# Check if exists
for fav in self.favorites:
if fav['id'] == item['id']:
self.show_info(self.get_txt("msg_fav_exists", name=item['name']), wx.ICON_INFORMATION)
return
new_fav = {
"id": item['id'],
"name": item['name'],
"type": item['type'],
"drive_id": item.get('drive_id'),
"site_id": item.get('id') if item['type'] == "SITE" else self.current_site_id,
"path": self.current_path + [item['name']]
}
self.favorites.append(new_fav)
self.save_favorites()
self.refresh_fav_list()
self.show_info(self.get_txt("msg_success"))
def remove_favorite(self, id):
self.favorites = [f for f in self.favorites if f['id'] != id]
self.save_favorites()
self.refresh_fav_list()
def save_favorites(self):
settings["favorites"] = self.favorites
save_settings(settings)
def refresh_fav_list(self):
self.fav_list.DeleteAllItems()
for i, fav in enumerate(self.favorites):
img_idx = self.idx_star
if fav['type'] == "DRIVE": img_idx = self.idx_drive
elif fav['type'] == "SITE": img_idx = self.idx_site
elif fav['type'] == "FOLDER": img_idx = self.idx_folder
self.fav_list.InsertItem(i, fav['name'], img_idx)
self.fav_list.SetItemData(i, i) # Store index
def on_favorite_activated(self, event):
idx = event.GetIndex()
if idx < 0 or idx >= len(self.favorites): return
fav = self.favorites[idx]
self.current_path = fav['path']
self.update_path_display()
# Navigate to contents
data = {
"type": fav['type'],
"id": fav['id'],
"drive_id": fav['drive_id'],
"path": fav['path']
}
if fav['type'] == "SITE": self.current_site_id = fav['id']
elif fav['drive_id']: self.current_drive_id = fav['drive_id']
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()
def on_favorite_right_click(self, event):
idx = self.fav_list.GetFirstSelected()
if idx < 0: return
fav = self.favorites[idx]
menu = wx.Menu()
remove_item = menu.Append(wx.ID_ANY, self.get_txt("btn_remove_fav"))
remove_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_DEL_BOOKMARK, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, lambda e: self.remove_favorite(fav['id']), remove_item)
self.PopupMenu(menu)
menu.Destroy()
def toggle_favorites(self, event=None):
self.fav_visible = not self.fav_visible
self.fav_toggle_btn.SetLabel("" if self.fav_visible else "")
if self.fav_visible:
self.fav_list.Show()
self.left_vbox.SetItemMinSize(self.fav_section, -1, 200)
else:
self.fav_list.Hide()
self.left_vbox.SetItemMinSize(self.fav_section, -1, 30)
settings["fav_visible"] = self.fav_visible
save_settings(settings)
self.left_container.Layout()
# --- 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):
if not self.ensure_valid_token(): return
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):
if not self.ensure_valid_token(): return
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):
if not self.ensure_valid_token(): return
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):
if not self.ensure_valid_token(): return
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):
if not self.ensure_valid_token(): return
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 on_download_clicked(self, item):
if not self.ensure_valid_token(): return
if item['type'] == "FILE":
with wx.FileDialog(self, self.get_txt("msg_select_file"), defaultFile=item['name'], style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fd:
if fd.ShowModal() == wx.ID_OK:
path = fd.GetPath()
threading.Thread(target=self._download_file_bg_task, args=(item, path), daemon=True).start()
else:
# Mappe eller Drev
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:
parent_path = dd.GetPath()
dest_path = os.path.join(parent_path, item['name'])
threading.Thread(target=self._download_folder_bg_task, args=(item, dest_path), daemon=True).start()
def _download_file_bg_task(self, item, dest_path):
if not self.ensure_valid_token(): return
self.set_status(self.get_txt("msg_downloading_to", name=item['name'], path=dest_path))
if self._download_file_sync_call(item['drive_id'], item['id'], dest_path):
self.set_status(self.get_txt("msg_download_done", name=item['name']))
else:
self.set_status(self.get_txt("msg_error"))
def _download_file_sync_call(self, drive_id, item_id, dest_path):
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{item_id}/content"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
with open(dest_path, 'wb') as f:
f.write(res.content)
return True
return False
def _download_folder_bg_task(self, item, dest_path):
if not self.ensure_valid_token(): return
self.set_status(self.get_txt("msg_downloading_to", name=item['name'], path=dest_path))
self._download_folder_recursive_sync(item['drive_id'], item['id'], dest_path)
self.set_status(self.get_txt("msg_download_done", name=item['name']))
def _download_folder_recursive_sync(self, drive_id, folder_id, local_dir):
if not os.path.exists(local_dir):
os.makedirs(local_dir)
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{folder_id}/children"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
items = res.json().get('value', [])
for item in items:
item_path = os.path.join(local_dir, item['name'])
if 'folder' in item:
self._download_folder_recursive_sync(drive_id, item['id'], item_path)
else:
self._download_file_sync_call(drive_id, item['id'], item_path)
def set_status(self, text):
wx.CallAfter(self.status_bar.SetStatusText, text)
def show_info(self, text, type=wx.ICON_INFORMATION, auto_hide=True):
def _do():
self.info_bar.Dismiss()
self.info_bar.ShowMessage(text, type)
if auto_hide:
self.info_timer.Start(4000, oneShot=True)
else:
self.info_timer.Stop()
wx.CallAfter(_do)
def on_done_editing_clicked(self, event):
waiting_files = [fid for fid, d in self.active_edits.items() if d.get("waiting")]
if not waiting_files:
return
if len(waiting_files) == 1:
fid = waiting_files[0]
self.active_edits[fid]["event"].set()
else:
# Show menu to let user pick which file is finished
menu = wx.Menu()
for fid in waiting_files:
name = self.active_edits[fid]["name"]
item = menu.Append(wx.ID_ANY, f"Gem '{name}'")
# closure to capture fid
def make_handler(f_id):
return lambda e: self.active_edits[f_id]["event"].set()
self.Bind(wx.EVT_MENU, make_handler(fid), item)
menu.AppendSeparator()
item_all = menu.Append(wx.ID_ANY, "Gem alle")
def handle_all(e):
for f in waiting_files:
if f in self.active_edits:
self.active_edits[f]["event"].set()
self.Bind(wx.EVT_MENU, handle_all, item_all)
self.PopupMenu(menu)
menu.Destroy()
def on_settings_clicked(self, event):
dlg = SettingsDialog(self, settings)
if dlg.ShowModal() == wx.ID_OK:
global CLIENT_ID, TENANT_ID, AUTHORITY, TEMP_DIR, CURRENT_LANG
new_settings = dlg.settings
# Check if IDs changed (need refresh)
ids_changed = (new_settings["client_id"] != settings["client_id"] or
new_settings["tenant_id"] != settings["tenant_id"])
# Save
save_settings(new_settings)
# Update global variables for current session
settings.update(new_settings)
CLIENT_ID = settings["client_id"]
TENANT_ID = settings["tenant_id"]
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
TEMP_DIR = settings["temp_dir"]
# Apply language
if CURRENT_LANG != new_settings["language"]:
CURRENT_LANG = new_settings["language"]
self.lang = CURRENT_LANG
self.refresh_ui_texts()
# Update MSAL App if IDs changed
if ids_changed:
self.msal_app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY)
self.access_token = None
self.headers = {}
self.login_btn.Enable()
self.login_btn.SetLabel(self.get_txt("btn_login"))
self.login_btn.SetBackgroundColour(wx.Colour(40, 167, 100))
self.show_info(self.get_txt("msg_restart_required"), wx.ICON_INFORMATION)
# Ensure temp dir exists
if not os.path.exists(TEMP_DIR):
os.makedirs(TEMP_DIR)
self.show_info(self.get_txt("msg_settings_saved"))
dlg.Destroy()
def refresh_ui_texts(self):
# Update UI texts for main frame and buttons
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"))
self.refresh_btn.SetLabel(self.get_txt("btn_refresh"))
self.settings_btn.SetLabel(self.get_txt("btn_settings"))
if self.access_token:
self.login_btn.SetLabel(self.get_txt("btn_logged_in"))
else:
self.login_btn.SetLabel(self.get_txt("btn_login"))
# Re-set headers for ListCtrl
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()
def on_language_changed(self, event):
# Deprecated: use on_settings_clicked instead if you want or keep for quick switch
pass # We'll just remove or redirect it later
def on_close_window(self, event):
if self.active_edits:
self.show_info(self.get_txt("msg_edit_warning"), wx.ICON_WARNING, auto_hide=False)
return
event.Skip()
def lock_ui(self, lock=True):
def _do():
self.tree_ctrl.Enable(not lock)
self.list_ctrl.Enable(not lock)
self.back_btn.Enable(not lock if len(self.history) > 1 else False)
self.home_btn.Enable(not lock)
self.refresh_btn.Enable(not lock)
self.login_btn.Enable(not lock)
wx.CallAfter(_do)
def on_refresh(self, event=None):
selected = self.tree_ctrl.GetSelection()
if not selected.IsOk() or selected == self.tree_root:
self.load_sites()
return
data = self.tree_item_data.get(selected)
if data:
self.set_status(f"Opdaterer '{data['name']}'...")
# Opdater Listekontrol (højre side)
threading.Thread(target=self._fetch_list_contents_bg, args=(data,), daemon=True).start()
# Opdater Træstruktur (venstre side) - Ryd og indlæs igen hvis det er en container
if data['type'] in ["SITE", "DRIVE", "FOLDER"]:
data["loaded"] = False # Tillad genindlæsning
if self.tree_ctrl.IsExpanded(selected):
# Genindlæs træets børn asynkront
threading.Thread(target=self._fetch_tree_children_bg, args=(selected, data), daemon=True).start()
def _refresh_current_view(self):
sel = self.tree_ctrl.GetSelection()
if sel.IsOk():
data = self.tree_item_data.get(sel)
if data:
# Kør i nuværende baggrundstråd hvis muligt, ellers ny
self._fetch_list_contents_bg(data)
def clear_main(self):
self.list_ctrl.DeleteAllItems()
self.current_items = []
self.update_path_display()
def update_path_display(self):
if not self: return
try:
self.path_sizer.Clear(True)
self._add_path_segment("📍 " + self.get_txt("title"), "ROOT")
# Vis stien fra self.current_path
path_segments = self.current_path[1:] if self.current_path and self.current_path[0] == "SharePoint" else self.current_path
# Prøv at finde matchende noder i træet for at gøre brødkrummerne klikbare
curr_node = self.tree_root
for name in path_segments:
arrow = wx.StaticText(self.path_panel, label=" > ")
arrow.SetForegroundColour(wx.Colour(150, 150, 150))
self.path_sizer.Add(arrow, 0, wx.ALIGN_CENTER_VERTICAL)
found_node = None
if curr_node:
child, cookie = self.tree_ctrl.GetFirstChild(curr_node)
while child.IsOk():
if self.tree_ctrl.GetItemText(child) == name:
found_node = child
break
child, cookie = self.tree_ctrl.GetNextChild(curr_node, cookie)
self._add_path_segment(name, found_node)
curr_node = found_node # Fortsæt ned i træet hvis muligt
self.path_panel.Layout()
self.path_panel.Refresh()
self.Layout()
except RuntimeError:
pass
def _add_path_segment(self, label, node):
btn = wx.Button(self.path_panel, label=label, style=wx.BU_EXACTFIT | wx.BORDER_NONE)
btn.SetBackgroundColour(wx.WHITE)
btn.SetFont(wx.Font(9, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
if node == "ROOT":
btn.Bind(wx.EVT_BUTTON, self.load_sites)
elif node:
btn.Bind(wx.EVT_BUTTON, lambda e: self.tree_ctrl.SelectItem(node))
self.path_sizer.Add(btn, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
def ensure_valid_token(self):
"""Sikrer at vi har et gyldigt token. Returnerer True hvis OK."""
try:
accounts = self.msal_app.get_accounts()
if not accounts:
self.set_status(self.get_txt("status_login_needed"))
return False
result = self.msal_app.acquire_token_silent(SCOPES, account=accounts[0])
if result and "access_token" in result:
self.access_token = result["access_token"]
self.headers = {'Authorization': f'Bearer {self.access_token}'}
return True
except Exception as e:
print(f"Token refresh error: {e}")
self.set_status(self.get_txt("status_login_needed"))
return False
def login(self, event):
self.set_status(self.get_txt("status_logging_in"))
accounts = self.msal_app.get_accounts()
result = None
if accounts:
result = self.msal_app.acquire_token_silent(SCOPES, account=accounts[0])
if not result or "access_token" not in result:
result = self.msal_app.acquire_token_interactive(scopes=SCOPES)
if "access_token" in result:
self.access_token = result["access_token"]
self.headers = {'Authorization': f'Bearer {self.access_token}'}
self.login_btn.Disable()
# self.login_btn.Hide() # Valgfrit: Skjul login knap helt når vi er inde
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.refresh_btn.Enable()
self.load_sites()
else:
self.set_status(self.get_txt("status_login_failed"))
wx.CallAfter(wx.MessageBox, result.get("error_description", self.get_txt("msg_unknown_error")), self.get_txt("msg_error"), wx.OK | wx.ICON_ERROR)
def load_sites(self, event=None):
self.set_status(self.get_txt("status_fetching_sites"))
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):
if not self.ensure_valid_token(): return
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', [])
sites.sort(key=lambda x: x.get('displayName', x.get('name', '')).lower())
wx.CallAfter(self._populate_sites_tree, sites)
else:
self.set_status(self.get_txt("msg_unknown_error"))
def _populate_sites_tree(self, 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)
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)
# 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.idx_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": "", "web_url": site.get('webUrl')
})
def on_tree_expanding(self, event):
item = event.GetItem()
data = self.tree_item_data.get(item)
if not data or data.get("loaded"):
return
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):
if not self.ensure_valid_token(): return
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 _populate_tree_drives(self, parent_node, drives, parent_data):
self.tree_ctrl.DeleteChildren(parent_node)
parent_data["loaded"] = True
target_node = None
for drive in drives:
name = drive.get('name', self.get_txt("type_unknown"))
drive_id = drive['id']
node = self.tree_ctrl.AppendItem(parent_node, name, image=self.idx_drive)
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, image=self.idx_folder)
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):
if not self: return
item = event.GetItem()
data = self.tree_item_data.get(item)
if not data:
return
try:
self.current_path = data["path"]
self.update_path_display()
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):
if not self.ensure_valid_token(): return
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": "", "size": None,
"web_url": drive.get('webUrl')
})
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,
"size": item.get('size') if not is_folder else None,
"web_url": item.get('webUrl')
})
wx.CallAfter(self._populate_list_ctrl, items_data, data)
def _populate_list_ctrl(self, items_data, parent_data):
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"))
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):
item_idx = event.GetIndex()
item = self.current_items[item_idx]
if item['type'] in ["SITE", "DRIVE", "FOLDER"]:
self._sync_tree_selection(item['id'])
elif item['type'] == "FILE":
self.open_file(item)
def _sync_tree_selection(self, target_id):
selected = self.tree_ctrl.GetSelection()
if not selected.IsOk():
selected = self.tree_root
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 open_file(self, item):
item_id = item['id']
file_name = item['name']
drive_id = item['drive_id']
if item_id in self.active_edits:
self.show_info(f"'{file_name}' er allerede ved at blive redigeret.", wx.ICON_INFORMATION)
return
if len(self.active_edits) >= 10:
wx.MessageBox("Du kan kun have 10 filer åbne til redigering ad gangen.", "Maksimum grænse nået", wx.OK | wx.ICON_WARNING)
return
threading.Thread(target=self.process_file, args=(item_id, file_name, drive_id), daemon=True).start()
def update_edit_ui(self):
def _do():
try:
if not self: return
count = len(self.active_edits)
waiting_count = sum(1 for d in self.active_edits.values() if d.get("waiting"))
if waiting_count > 0:
self.done_btn.SetLabel(f"{self.get_txt('btn_save_changes')} ({waiting_count})")
self.done_btn.Show()
else:
self.done_btn.Hide()
# Opdater statusbesked hvis der er aktive opgaver
if count > 0:
status_msg = f"Aktive filer: {count}"
if waiting_count > 0:
status_msg += f" ({waiting_count} venter på gem)"
self.set_status(status_msg)
else:
self.set_status(self.get_txt("status_ready"))
self.Layout()
except RuntimeError:
pass
wx.CallAfter(_do)
def process_file(self, item_id, file_name, drive_id):
if not self.ensure_valid_token(): return
edit_event = threading.Event()
self.active_edits[item_id] = {"name": file_name, "event": edit_event, "waiting": False}
self.update_edit_ui()
try:
# 1. Lokation info
base_url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{item_id}"
# Unik undermappe baseret på ID, men brug originalt filnavn indeni
item_hash = hashlib.md5(item_id.encode()).hexdigest()[:8]
working_dir = os.path.join(TEMP_DIR, item_hash)
if not os.path.exists(working_dir):
os.makedirs(working_dir)
local_path = os.path.join(working_dir, file_name)
# 2. Download
self.set_status(self.get_txt("msg_fetching_file", name=file_name))
res = requests.get(f"{base_url}/content", headers=self.headers)
if res.status_code != 200:
raise Exception(f"{self.get_txt('msg_unknown_error')}: {res.status_code}")
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)
# 3. Åbn & Overvåg
self.set_status(self.get_txt("msg_opening_file", name=file_name))
os.startfile(local_path)
locked = False
self.set_status(self.get_txt("msg_waiting_for_file", name=file_name))
for _ in range(10):
time.sleep(1)
try:
os.rename(local_path, local_path)
except OSError:
locked = True
break
if locked:
self.set_status(self.get_txt("msg_editing_file", name=file_name))
while True:
time.sleep(2)
try:
os.rename(local_path, local_path)
break
except OSError:
pass
else:
self.set_status(self.get_txt("msg_waiting_for_file", name=file_name))
edit_event.clear()
self.active_edits[item_id]["waiting"] = True
self.update_edit_ui()
edit_event.wait()
if item_id in self.active_edits:
self.active_edits[item_id]["waiting"] = False
self.update_edit_ui()
# 4. Tjek om noget er ændret
new_hash = get_file_hash(local_path)
if original_hash == new_hash:
self.set_status(self.get_txt("msg_file_unchanged"))
else:
# 5. Upload (kun hvis ændret)
self.set_status(self.get_txt("msg_updating_changes"))
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"{self.get_txt('msg_update_failed_code', code=upload_res.status_code)}")
# 6. Checkin (Uanset om ændret eller ej, for at frigive lås)
self.set_status(self.get_txt("msg_checking_in", name=file_name))
requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "SP Explorer Edit"})
# Oprydning: Slet fil og derefter mappe
try:
os.remove(local_path)
os.rmdir(working_dir)
except:
pass
self.set_status(self.get_txt("msg_update_success", name=file_name))
self.show_info(self.get_txt("msg_update_success", name=file_name), wx.ICON_INFORMATION)
self._refresh_current_view()
except Exception as e:
self.set_status(f"{self.get_txt('msg_error')}: {str(e)}")
self.show_info(f"{self.get_txt('msg_error')}: {e}", wx.ICON_ERROR)
finally:
if item_id in self.active_edits:
del self.active_edits[item_id]
self.update_edit_ui()
if __name__ == "__main__":
app = wx.App()
SharePointApp()
app.MainLoop()