Compare commits

...

4 Commits

View File

@@ -8,6 +8,7 @@ import requests
import msal import msal
import wx import wx
import wx.lib.newevent import wx.lib.newevent
import webbrowser
# --- STIHÅNDTERING (Til EXE-brug) --- # --- STIHÅNDTERING (Til EXE-brug) ---
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
@@ -23,29 +24,209 @@ def load_settings():
default_settings = { default_settings = {
"client_id": "DIN_CLIENT_ID_HER", "client_id": "DIN_CLIENT_ID_HER",
"tenant_id": "DIN_TENANT_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): 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) json.dump(default_settings, f, indent=4)
return default_settings return default_settings
with open(SETTINGS_FILE, 'r') as f: with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
try: try:
return json.load(f) return json.load(f)
except: except:
return default_settings 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() settings = load_settings()
CLIENT_ID = settings.get("client_id") CLIENT_ID = settings.get("client_id")
TENANT_ID = settings.get("tenant_id") TENANT_ID = settings.get("tenant_id")
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}" AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPES = ["Files.ReadWrite.All", "Sites.Read.All", "User.Read"] SCOPES = ["Files.ReadWrite.All", "Sites.Read.All", "User.Read"]
TEMP_DIR = settings.get("temp_dir", "C:\\Temp_SP") 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_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",
"status_login_needed": "Session udløbet. Log ind igen."
},
"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_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",
"status_login_needed": "Session expired. Please login again."
}
}
if not os.path.exists(TEMP_DIR): if not os.path.exists(TEMP_DIR):
os.makedirs(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): def format_size(bytes_num):
if bytes_num is None: if bytes_num is None:
return "" return ""
@@ -60,7 +241,8 @@ def format_size(bytes_num):
class SharePointApp(wx.Frame): class SharePointApp(wx.Frame):
def __init__(self): 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 # State
self.access_token = None self.access_token = None
@@ -86,11 +268,23 @@ class SharePointApp(wx.Frame):
# Threading/Sync til filredigering # Threading/Sync til filredigering
self.edit_wait_event = threading.Event() self.edit_wait_event = threading.Event()
# MSAL Cache
self.msal_app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY)
self.InitUI() self.InitUI()
self.Centre() self.Centre()
self.Show() self.Show()
self.Bind(wx.EVT_CLOSE, self.on_close_window) 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): def InitUI(self):
panel = wx.Panel(self) panel = wx.Panel(self)
main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer = wx.BoxSizer(wx.VERTICAL)
@@ -111,32 +305,54 @@ class SharePointApp(wx.Frame):
nav_panel = wx.Panel(panel) nav_panel = wx.Panel(panel)
nav_hbox = wx.BoxSizer(wx.HORIZONTAL) nav_hbox = wx.BoxSizer(wx.HORIZONTAL)
self.back_btn = wx.Button(nav_panel, label="← Tilbage", size=(100, 30)) self.back_btn = wx.Button(nav_panel, label=self.get_txt("btn_back"), size=(100, 30))
self.back_btn.Disable() self.back_btn.Disable()
self.back_btn.Bind(wx.EVT_BUTTON, self.go_back) self.back_btn.Bind(wx.EVT_BUTTON, self.go_back)
nav_hbox.Add(self.back_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 10) nav_hbox.Add(self.back_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 10)
self.home_btn = wx.Button(nav_panel, label="🏠 Hjem", size=(100, 30)) self.home_btn = wx.Button(nav_panel, label=self.get_txt("btn_home"), size=(100, 30))
self.home_btn.Disable() self.home_btn.Disable()
self.home_btn.Bind(wx.EVT_BUTTON, self.load_sites) self.home_btn.Bind(wx.EVT_BUTTON, self.load_sites)
nav_hbox.Add(self.home_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) nav_hbox.Add(self.home_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5)
# NY KNAP: Gem ændringer (Vises kun ved redigering) # NY KNAP: Gem ændringer (Vises kun ved redigering)
self.done_btn = wx.Button(nav_panel, label="💾 Gem ændringer i SharePoint", size=(200, 30)) self.done_btn = wx.Button(nav_panel, label=self.get_txt("btn_save_changes"), size=(200, 30))
self.done_btn.SetBackgroundColour(wx.Colour(255, 69, 0)) # OrangeRed self.done_btn.SetBackgroundColour(wx.Colour(255, 69, 0)) # OrangeRed
self.done_btn.SetForegroundColour(wx.WHITE) self.done_btn.SetForegroundColour(wx.WHITE)
self.done_btn.Hide() self.done_btn.Hide()
self.done_btn.Bind(wx.EVT_BUTTON, self.on_done_editing_clicked) 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) 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=(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=self.get_txt("btn_upload_folder"), 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=self.get_txt("btn_new_folder"), 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) 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.SetBackgroundColour(wx.Colour(40, 167, 69)) # Grøn
self.login_btn.SetForegroundColour(wx.WHITE) self.login_btn.SetForegroundColour(wx.WHITE)
self.login_btn.Bind(wx.EVT_BUTTON, self.login) self.login_btn.Bind(wx.EVT_BUTTON, self.login)
nav_hbox.Add(self.login_btn, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) 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) nav_panel.SetSizer(nav_hbox)
vbox.Add(nav_panel, 0, wx.EXPAND | wx.ALL, 5) vbox.Add(nav_panel, 0, wx.EXPAND | wx.ALL, 5)
@@ -160,14 +376,17 @@ class SharePointApp(wx.Frame):
# Right side: File Area - ListCtrl # Right side: File Area - ListCtrl
self.list_ctrl = wx.ListCtrl(self.splitter, style=wx.LC_REPORT | wx.BORDER_SUNKEN) 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.AssignImageList(self.image_list, wx.IMAGE_LIST_SMALL)
self.list_ctrl.InsertColumn(0, "Navn", width=450) self.list_ctrl.InsertColumn(0, self.get_txt("col_name"), width=450)
self.list_ctrl.InsertColumn(1, "Type", width=120) self.list_ctrl.InsertColumn(1, self.get_txt("col_type"), width=120)
self.list_ctrl.InsertColumn(2, "Størrelse", width=80) self.list_ctrl.InsertColumn(2, self.get_txt("col_size"), width=80)
self.list_ctrl.InsertColumn(3, "Sidst ændret", width=180) 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_LIST_ITEM_ACTIVATED, self.on_item_activated)
self.list_ctrl.Bind(wx.EVT_CONTEXT_MENU, self.on_right_click) 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.SplitVertically(self.tree_ctrl, self.list_ctrl, 250)
self.splitter.SetMinimumPaneSize(100) self.splitter.SetMinimumPaneSize(100)
@@ -175,22 +394,263 @@ class SharePointApp(wx.Frame):
# 4. STATUS BAR # 4. STATUS BAR
self.status_bar = self.CreateStatusBar() self.status_bar = self.CreateStatusBar()
self.status_bar.SetStatusText("Klar") self.status_bar.SetStatusText(self.get_txt("status_ready"))
panel.SetSizer(vbox) panel.SetSizer(vbox)
self.Layout() self.Layout()
def on_right_click(self, event): def on_right_click(self, event):
item_idx = self.list_ctrl.GetFirstSelected() selected_indices = []
if item_idx == -1: return idx = self.list_ctrl.GetFirstSelected()
item = self.current_items[item_idx] while idx != -1:
if item['type'] == "FILE": selected_indices.append(idx)
idx = self.list_ctrl.GetNextSelected(idx)
menu = wx.Menu() menu = wx.Menu()
edit_item = menu.Append(wx.ID_ANY, "Rediger fil")
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.get("web_url"):
browser_item = menu.Append(wx.ID_ANY, self.get_txt("msg_open_browser"))
self.Bind(wx.EVT_MENU, lambda e: webbrowser.open(item["web_url"]), browser_item)
download_item = menu.Append(wx.ID_ANY, self.get_txt("msg_download"))
self.Bind(wx.EVT_MENU, lambda e: self.on_download_clicked(item), download_item)
menu.AppendSeparator()
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) 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) 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):
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): def set_status(self, text):
wx.CallAfter(self.status_bar.SetStatusText, text) wx.CallAfter(self.status_bar.SetStatusText, text)
@@ -210,9 +670,48 @@ class SharePointApp(wx.Frame):
self.edit_wait_event.set() self.edit_wait_event.set()
self.info_bar.Dismiss() 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): def on_close_window(self, event):
if self.is_editing: 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 return
event.Skip() event.Skip()
@@ -254,7 +753,7 @@ class SharePointApp(wx.Frame):
curr = self.tree_ctrl.GetItemParent(curr) curr = self.tree_ctrl.GetItemParent(curr)
# Start ikon/label # Start ikon/label
self._add_path_segment("📍 SharePoint", "ROOT") self._add_path_segment("📍 " + self.get_txt("title"), "ROOT")
for node in nodes: for node in nodes:
arrow = wx.StaticText(self.path_panel, label=" > ") arrow = wx.StaticText(self.path_panel, label=" > ")
@@ -283,30 +782,50 @@ class SharePointApp(wx.Frame):
self.path_sizer.Add(btn, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) 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): 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 = self.msal_app.get_accounts()
accounts = app.get_accounts()
result = None result = None
if accounts: if accounts:
result = app.acquire_token_silent(SCOPES, account=accounts[0]) result = self.msal_app.acquire_token_silent(SCOPES, account=accounts[0])
if not result:
result = app.acquire_token_interactive(scopes=SCOPES) if not result or "access_token" not in result:
result = self.msal_app.acquire_token_interactive(scopes=SCOPES)
if "access_token" in result: if "access_token" in result:
self.access_token = result["access_token"] self.access_token = result["access_token"]
self.headers = {'Authorization': f'Bearer {self.access_token}'} self.headers = {'Authorization': f'Bearer {self.access_token}'}
self.login_btn.Disable() self.login_btn.Disable()
self.login_btn.SetLabel("Logget ind") # 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.login_btn.SetBackgroundColour(wx.Colour(200, 200, 200)) # Grå
self.home_btn.Enable() self.home_btn.Enable()
self.load_sites() self.load_sites()
else: else:
self.set_status("Login fejlede.") self.set_status(self.get_txt("status_login_failed"))
wx.MessageBox(result.get("error_description", "Unknown error"), "Login Error", wx.OK | wx.ICON_ERROR) 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): 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.tree_ctrl.DeleteAllItems()
self.list_ctrl.DeleteAllItems() self.list_ctrl.DeleteAllItems()
self.current_items = [] self.current_items = []
@@ -321,6 +840,7 @@ class SharePointApp(wx.Frame):
threading.Thread(target=self._fetch_sites_bg, daemon=True).start() threading.Thread(target=self._fetch_sites_bg, daemon=True).start()
def _fetch_sites_bg(self): def _fetch_sites_bg(self):
if not self.ensure_valid_token(): return
url = "https://graph.microsoft.com/v1.0/sites?search=*" url = "https://graph.microsoft.com/v1.0/sites?search=*"
res = requests.get(url, headers=self.headers) res = requests.get(url, headers=self.headers)
if res.status_code == 200: if res.status_code == 200:
@@ -328,10 +848,10 @@ class SharePointApp(wx.Frame):
sites.sort(key=lambda x: x.get('displayName', x.get('name', '')).lower()) sites.sort(key=lambda x: x.get('displayName', x.get('name', '')).lower())
wx.CallAfter(self._populate_sites_tree, sites) wx.CallAfter(self._populate_sites_tree, sites)
else: else:
self.set_status("Kunne ikke hente sites.") self.set_status(self.get_txt("msg_unknown_error"))
def _populate_sites_tree(self, 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: for site in sites:
name = site.get('displayName', site.get('name')) name = site.get('displayName', site.get('name'))
node = self.tree_ctrl.AppendItem(self.tree_root, name, image=self.idx_site) node = self.tree_ctrl.AppendItem(self.tree_root, name, image=self.idx_site)
@@ -347,10 +867,13 @@ class SharePointApp(wx.Frame):
for i, site in enumerate(sites): for i, site in enumerate(sites):
name = site.get('displayName', site.get('name')) name = site.get('displayName', site.get('name'))
self.list_ctrl.InsertItem(i, name, self.idx_site) 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, 2, "") # Størrelse
self.list_ctrl.SetItem(i, 3, "") # Sidst ændret self.list_ctrl.SetItem(i, 3, "") # Sidst ændret
self.current_items.append({"type": "SITE", "id": site['id'], "name": name, "size": None, "modified": ""}) 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): def on_tree_expanding(self, event):
item = event.GetItem() item = event.GetItem()
@@ -358,10 +881,11 @@ class SharePointApp(wx.Frame):
if not data or data.get("loaded"): if not data or data.get("loaded"):
return 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() threading.Thread(target=self._fetch_tree_children_bg, args=(item, data), daemon=True).start()
def _fetch_tree_children_bg(self, parent_node, data): def _fetch_tree_children_bg(self, parent_node, data):
if not self.ensure_valid_token(): return
if data['type'] == "SITE": if data['type'] == "SITE":
url = f"https://graph.microsoft.com/v1.0/sites/{data['id']}/drives" url = f"https://graph.microsoft.com/v1.0/sites/{data['id']}/drives"
res = requests.get(url, headers=self.headers) res = requests.get(url, headers=self.headers)
@@ -392,7 +916,7 @@ class SharePointApp(wx.Frame):
target_node = None target_node = None
for drive in drives: for drive in drives:
name = drive.get('name', 'Ukendt') name = drive.get('name', self.get_txt("type_unknown"))
drive_id = drive['id'] drive_id = drive['id']
node = self.tree_ctrl.AppendItem(parent_node, name, image=self.idx_drive) node = self.tree_ctrl.AppendItem(parent_node, name, image=self.idx_drive)
self.tree_item_data[node] = { self.tree_item_data[node] = {
@@ -429,27 +953,33 @@ class SharePointApp(wx.Frame):
self.tree_ctrl.SelectItem(target_node) self.tree_ctrl.SelectItem(target_node)
def on_tree_selected(self, event): def on_tree_selected(self, event):
if self.is_editing: return if not self or self.is_editing: return
item = event.GetItem() item = event.GetItem()
data = self.tree_item_data.get(item) data = self.tree_item_data.get(item)
if not data: if not data:
return return
try:
self.current_path = data["path"] self.current_path = data["path"]
if self:
self.update_path_display() self.update_path_display()
if not self.is_navigating_back: if not self.is_navigating_back:
self.history.append(item) self.history.append(item)
# Check if button still exists
if self.back_btn:
self.back_btn.Enable(len(self.history) > 1) self.back_btn.Enable(len(self.history) > 1)
self.list_ctrl.DeleteAllItems() self.list_ctrl.DeleteAllItems()
self.current_items = [] self.current_items = []
self.set_status("Indlæser indhold...") self.set_status(self.get_txt("status_loading_content"))
threading.Thread(target=self._fetch_list_contents_bg, args=(data,), daemon=True).start() threading.Thread(target=self._fetch_list_contents_bg, args=(data,), daemon=True).start()
except RuntimeError:
pass
def _fetch_list_contents_bg(self, data): def _fetch_list_contents_bg(self, data):
if not self.ensure_valid_token(): return
items_data = [] items_data = []
if data['type'] == "SITE": if data['type'] == "SITE":
url = f"https://graph.microsoft.com/v1.0/sites/{data['id']}/drives" url = f"https://graph.microsoft.com/v1.0/sites/{data['id']}/drives"
@@ -460,7 +990,8 @@ class SharePointApp(wx.Frame):
for drive in drives: for drive in drives:
items_data.append({ items_data.append({
"type": "DRIVE", "id": drive['id'], "name": drive.get('name', ''), "type": "DRIVE", "id": drive['id'], "name": drive.get('name', ''),
"drive_id": drive['id'], "modified": "", "size": None "drive_id": drive['id'], "modified": "", "size": None,
"web_url": drive.get('webUrl')
}) })
elif data['type'] in ["DRIVE", "FOLDER"]: elif data['type'] in ["DRIVE", "FOLDER"]:
drive_id = data['drive_id'] drive_id = data['drive_id']
@@ -480,12 +1011,15 @@ class SharePointApp(wx.Frame):
"type": "FOLDER" if is_folder else "FILE", "type": "FOLDER" if is_folder else "FILE",
"id": item['id'], "name": item['name'], "id": item['id'], "name": item['name'],
"drive_id": drive_id, "modified": modified, "drive_id": drive_id, "modified": modified,
"size": item.get('size') if not is_folder else None "size": item.get('size') if not is_folder else None,
"web_url": item.get('webUrl')
}) })
wx.CallAfter(self._populate_list_ctrl, items_data, data) wx.CallAfter(self._populate_list_ctrl, items_data, data)
def _populate_list_ctrl(self, items_data, parent_data): def _populate_list_ctrl(self, items_data, parent_data):
if not self: return
try:
self.list_ctrl.DeleteAllItems() self.list_ctrl.DeleteAllItems()
self.current_items = [] self.current_items = []
for i, item in enumerate(items_data): for i, item in enumerate(items_data):
@@ -495,14 +1029,14 @@ class SharePointApp(wx.Frame):
elif item['type'] == "SITE": img_idx = self.idx_site elif item['type'] == "SITE": img_idx = self.idx_site
self.list_ctrl.InsertItem(i, item['name'], img_idx) self.list_ctrl.InsertItem(i, item['name'], img_idx)
type_str = "Mappe" if item['type'] == "FOLDER" else "Fil" if item['type'] == "FILE" else "Bibliotek" 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) self.list_ctrl.SetItem(i, 1, type_str)
size_str = format_size(item['size']) if item['size'] is not None else "" 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, 2, size_str)
self.list_ctrl.SetItem(i, 3, item['modified']) self.list_ctrl.SetItem(i, 3, item['modified'])
self.current_items.append(item) self.current_items.append(item)
self.set_status("Klar") self.set_status(self.get_txt("status_ready"))
if parent_data['type'] == "SITE": if parent_data['type'] == "SITE":
self.current_site_id = parent_data['id'] self.current_site_id = parent_data['id']
@@ -513,6 +1047,22 @@ class SharePointApp(wx.Frame):
self.current_drive_id = parent_data['drive_id'] self.current_drive_id = parent_data['drive_id']
self.current_folder_id = parent_data['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): def on_item_activated(self, event):
if self.is_editing: return if self.is_editing: return
item_idx = event.GetIndex() item_idx = event.GetIndex()
@@ -554,6 +1104,7 @@ class SharePointApp(wx.Frame):
self.is_navigating_back = False self.is_navigating_back = False
def process_file(self, item_id, file_name): def process_file(self, item_id, file_name):
if not self.ensure_valid_token(): return
self.is_editing = True self.is_editing = True
self.lock_ui(True) self.lock_ui(True)
try: try:
@@ -571,23 +1122,26 @@ class SharePointApp(wx.Frame):
local_path = os.path.join(working_dir, file_name) local_path = os.path.join(working_dir, file_name)
# 2. Download # 2. Download
self.set_status(f"Henter '{file_name}'...") self.set_status(self.get_txt("msg_fetching_file", name=file_name))
res = requests.get(f"{base_url}/content", headers=self.headers) res = requests.get(f"{base_url}/content", headers=self.headers)
if res.status_code != 200: if res.status_code != 200:
raise Exception(f"Kunne ikke hente fil: {res.status_code}") raise Exception(f"{self.get_txt('msg_unknown_error')}: {res.status_code}")
with open(local_path, 'wb') as f: with open(local_path, 'wb') as f:
f.write(res.content) f.write(res.content)
# Beregn udgangspunkt hash
original_hash = get_file_hash(local_path)
# Checkout # Checkout
requests.post(f"{base_url}/checkout", headers=self.headers) requests.post(f"{base_url}/checkout", headers=self.headers)
# 3. Åbn & Overvåg # 3. Åbn & Overvåg
self.set_status(f"Åbner '{file_name}'...") self.set_status(self.get_txt("msg_opening_file", name=file_name))
os.startfile(local_path) os.startfile(local_path)
locked = False locked = False
self.set_status(f"Venter på '{file_name}'...") self.set_status(self.get_txt("msg_waiting_for_file", name=file_name))
for _ in range(10): for _ in range(10):
time.sleep(1) time.sleep(1)
try: try:
@@ -597,7 +1151,7 @@ class SharePointApp(wx.Frame):
break break
if locked: if locked:
self.set_status(f"Redigerer '{file_name}' - Luk for at gemme.") self.set_status(self.get_txt("msg_editing_file", name=file_name))
while True: while True:
time.sleep(2) time.sleep(2)
try: try:
@@ -606,7 +1160,7 @@ class SharePointApp(wx.Frame):
except OSError: except OSError:
pass pass
else: else:
self.set_status("Fillås ikke detekteret.") self.set_status(self.get_txt("msg_waiting_for_file", name=file_name))
self.edit_wait_event.clear() self.edit_wait_event.clear()
wx.CallAfter(self.done_btn.Show) wx.CallAfter(self.done_btn.Show)
wx.CallAfter(self.Layout) wx.CallAfter(self.Layout)
@@ -614,16 +1168,21 @@ class SharePointApp(wx.Frame):
wx.CallAfter(self.done_btn.Hide) wx.CallAfter(self.done_btn.Hide)
wx.CallAfter(self.Layout) wx.CallAfter(self.Layout)
# 4. Upload # 4. Tjek om noget er ændret
self.set_status(f"Uploader ændringer...") 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: with open(local_path, 'rb') as f:
upload_res = requests.put(f"{base_url}/content", headers=self.headers, data=f) upload_res = requests.put(f"{base_url}/content", headers=self.headers, data=f)
if upload_res.status_code not in [200, 201]: if upload_res.status_code not in [200, 201]:
raise Exception(f"Upload fejlede: {upload_res.status_code}") raise Exception(f"{self.get_txt('msg_update_failed_code', code=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...") self.set_status(self.get_txt("msg_checking_in", name=file_name))
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 # Oprydning: Slet fil og derefter mappe
try: try:
@@ -632,13 +1191,13 @@ class SharePointApp(wx.Frame):
except: except:
pass pass
self.set_status(f"Succes! '{file_name}' er opdateret.") self.set_status(self.get_txt("msg_update_success", name=file_name))
self.show_info(f"Filen '{file_name}' er gemt og tjekket ind korrekt.", wx.ICON_INFORMATION) self.show_info(self.get_txt("msg_update_success", name=file_name), wx.ICON_INFORMATION)
self._refresh_current_view() self._refresh_current_view()
except Exception as e: except Exception as e:
self.set_status(f"Fejl: {str(e)}") self.set_status(f"{self.get_txt('msg_error')}: {str(e)}")
self.show_info(f"Der skete en fejl: {e}", wx.ICON_ERROR) self.show_info(f"{self.get_txt('msg_error')}: {e}", wx.ICON_ERROR)
finally: finally:
self.is_editing = False self.is_editing = False
self.lock_ui(False) self.lock_ui(False)