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