|
|
|
@@ -6,9 +6,8 @@ import json
|
|
|
|
import sys
|
|
|
|
import sys
|
|
|
|
import requests
|
|
|
|
import requests
|
|
|
|
import msal
|
|
|
|
import msal
|
|
|
|
import customtkinter as ctk
|
|
|
|
import wx
|
|
|
|
import tkinter as tk
|
|
|
|
import wx.lib.newevent
|
|
|
|
from tkinter import messagebox
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- STIHÅNDTERING (Til EXE-brug) ---
|
|
|
|
# --- STIHÅNDTERING (Til EXE-brug) ---
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
|
|
@@ -32,7 +31,10 @@ def load_settings():
|
|
|
|
return default_settings
|
|
|
|
return default_settings
|
|
|
|
|
|
|
|
|
|
|
|
with open(SETTINGS_FILE, 'r') as f:
|
|
|
|
with open(SETTINGS_FILE, 'r') as f:
|
|
|
|
return json.load(f)
|
|
|
|
try:
|
|
|
|
|
|
|
|
return json.load(f)
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
|
|
return default_settings
|
|
|
|
|
|
|
|
|
|
|
|
settings = load_settings()
|
|
|
|
settings = load_settings()
|
|
|
|
CLIENT_ID = settings.get("client_id")
|
|
|
|
CLIENT_ID = settings.get("client_id")
|
|
|
|
@@ -44,65 +46,224 @@ TEMP_DIR = settings.get("temp_dir", "C:\\Temp_SP")
|
|
|
|
if not os.path.exists(TEMP_DIR):
|
|
|
|
if not os.path.exists(TEMP_DIR):
|
|
|
|
os.makedirs(TEMP_DIR)
|
|
|
|
os.makedirs(TEMP_DIR)
|
|
|
|
|
|
|
|
|
|
|
|
ctk.set_appearance_mode("System")
|
|
|
|
class SharePointApp(wx.Frame):
|
|
|
|
ctk.set_default_color_theme("blue")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SharePointApp(ctk.CTk):
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
super().__init__(None, title="SharePoint Explorer", size=(1000, 750))
|
|
|
|
self.title("SharePoint Explorer")
|
|
|
|
|
|
|
|
self.geometry("1000x750")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# State
|
|
|
|
self.access_token = None
|
|
|
|
self.access_token = None
|
|
|
|
self.headers = {}
|
|
|
|
self.headers = {}
|
|
|
|
|
|
|
|
|
|
|
|
# Navigation State
|
|
|
|
|
|
|
|
self.history = [] # Stack af (mode, id, path_segment)
|
|
|
|
self.history = [] # Stack af (mode, id, path_segment)
|
|
|
|
self.current_path = ["SharePoint"]
|
|
|
|
self.current_path = ["SharePoint"]
|
|
|
|
self.current_site_id = None
|
|
|
|
self.current_site_id = None
|
|
|
|
self.current_drive_id = None
|
|
|
|
self.current_drive_id = None
|
|
|
|
self.current_folder_id = "root"
|
|
|
|
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.is_editing = False # Låse-state ved filredigering
|
|
|
|
|
|
|
|
|
|
|
|
# UI Layout - 3 Rækker: Top (Nav), Midt (Filer), Bund (Info)
|
|
|
|
# System Ikoner (ArtProvider - mest basale for kompatibilitet)
|
|
|
|
self.grid_rowconfigure(1, weight=1)
|
|
|
|
self.image_list = wx.ImageList(16, 16)
|
|
|
|
self.grid_columnconfigure(0, weight=1)
|
|
|
|
self.idx_site = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_GO_HOME, wx.ART_OTHER, (16, 16))) # Site (Hus ikon)
|
|
|
|
|
|
|
|
self.idx_drive = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_HARDDISK, wx.ART_OTHER, (16, 16))) # Drive
|
|
|
|
|
|
|
|
self.idx_folder = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_OTHER, (16, 16))) # Folder
|
|
|
|
|
|
|
|
self.idx_file = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, (16, 16))) # File
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Threading/Sync til filredigering
|
|
|
|
|
|
|
|
self.edit_wait_event = threading.Event()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.InitUI()
|
|
|
|
|
|
|
|
self.Centre()
|
|
|
|
|
|
|
|
self.Show()
|
|
|
|
|
|
|
|
self.Bind(wx.EVT_CLOSE, self.on_close_window)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
# 1. TOP NAVIGATION BAR
|
|
|
|
self.nav_frame = ctk.CTkFrame(self, height=60, corner_radius=0)
|
|
|
|
nav_panel = wx.Panel(panel)
|
|
|
|
self.nav_frame.grid(row=0, column=0, sticky="ew", padx=0, pady=0)
|
|
|
|
nav_hbox = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
|
|
|
|
|
|
|
|
self.back_btn = ctk.CTkButton(self.nav_frame, text="← Tilbage", width=100, command=self.go_back, state="disabled")
|
|
|
|
self.back_btn = wx.Button(nav_panel, label="← Tilbage", size=(100, 30))
|
|
|
|
self.back_btn.pack(side="left", padx=10, pady=10)
|
|
|
|
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 = ctk.CTkButton(self.nav_frame, text="🏠 Hjem", width=100, command=self.load_sites, state="disabled")
|
|
|
|
self.home_btn = wx.Button(nav_panel, label="🏠 Hjem", size=(100, 30))
|
|
|
|
self.home_btn.pack(side="left", padx=5, pady=10)
|
|
|
|
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.login_btn = ctk.CTkButton(self.nav_frame, text="Log ind", width=120, fg_color="#28a745", hover_color="#218838", command=self.login)
|
|
|
|
# NY KNAP: Gem ændringer (Vises kun ved redigering)
|
|
|
|
self.login_btn.pack(side="right", padx=10, pady=10)
|
|
|
|
self.done_btn = wx.Button(nav_panel, label="💾 Gem ændringer i SharePoint", size=(200, 30))
|
|
|
|
|
|
|
|
self.done_btn.SetBackgroundColour(wx.Colour(255, 69, 0)) # OrangeRed
|
|
|
|
|
|
|
|
self.done_btn.SetForegroundColour(wx.WHITE)
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
# 2. MAIN CONTENT (File Area)
|
|
|
|
nav_hbox.AddStretchSpacer(1)
|
|
|
|
self.main_frame = ctk.CTkScrollableFrame(self, fg_color=("gray95", "gray10"), corner_radius=0)
|
|
|
|
|
|
|
|
self.main_frame.grid(row=1, column=0, sticky="nsew", padx=0, pady=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 3. FOOTER (Path & Status)
|
|
|
|
self.login_btn = wx.Button(nav_panel, label="Log ind", size=(120, 30))
|
|
|
|
self.footer_frame = ctk.CTkFrame(self, height=40, corner_radius=0)
|
|
|
|
self.login_btn.SetBackgroundColour(wx.Colour(40, 167, 69)) # Grøn
|
|
|
|
self.footer_frame.grid(row=2, column=0, sticky="ew")
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
self.path_label = ctk.CTkLabel(self.footer_frame, text="Sti: /", font=ctk.CTkFont(size=12, weight="bold"))
|
|
|
|
nav_panel.SetSizer(nav_hbox)
|
|
|
|
self.path_label.pack(side="left", padx=15, pady=5)
|
|
|
|
vbox.Add(nav_panel, 0, wx.EXPAND | wx.ALL, 5)
|
|
|
|
|
|
|
|
|
|
|
|
self.status_label = ctk.CTkLabel(self.footer_frame, text="Klar", text_color="gray")
|
|
|
|
# 2. PATH BREADCRUMBS (Adresselinje-stil)
|
|
|
|
self.status_label.pack(side="right", padx=15, pady=5)
|
|
|
|
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
|
|
|
|
|
|
|
|
self.tree_ctrl = wx.TreeCtrl(self.splitter, 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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, "Navn", width=450)
|
|
|
|
|
|
|
|
self.list_ctrl.InsertColumn(1, "Type", width=150)
|
|
|
|
|
|
|
|
self.list_ctrl.InsertColumn(2, "Sidst ændret", width=200)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.splitter.SplitVertically(self.tree_ctrl, self.list_ctrl, 250)
|
|
|
|
|
|
|
|
self.splitter.SetMinimumPaneSize(100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vbox.Add(self.splitter, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 4. STATUS BAR
|
|
|
|
|
|
|
|
self.status_bar = self.CreateStatusBar()
|
|
|
|
|
|
|
|
self.status_bar.SetStatusText("Klar")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
panel.SetSizer(vbox)
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
self.PopupMenu(menu)
|
|
|
|
|
|
|
|
menu.Destroy()
|
|
|
|
|
|
|
|
|
|
|
|
def set_status(self, text):
|
|
|
|
def set_status(self, text):
|
|
|
|
self.status_label.configure(text=text)
|
|
|
|
wx.CallAfter(self.status_bar.SetStatusText, text)
|
|
|
|
self.update_idletasks()
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
|
|
|
print("[DEBUG] 'Jeg er færdig'-knap klikket.")
|
|
|
|
|
|
|
|
self.set_status("Knap trykket - Uploader nu...")
|
|
|
|
|
|
|
|
self.edit_wait_event.set()
|
|
|
|
|
|
|
|
self.info_bar.Dismiss()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
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.login_btn.Enable(not lock)
|
|
|
|
|
|
|
|
wx.CallAfter(_do)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
def update_path_display(self):
|
|
|
|
path_str = " > ".join(self.current_path)
|
|
|
|
self.path_sizer.Clear(True)
|
|
|
|
self.path_label.configure(text=f"📍 {path_str}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def login(self):
|
|
|
|
# Find alle noder fra rod til nuværende selektion
|
|
|
|
|
|
|
|
nodes = []
|
|
|
|
|
|
|
|
curr = self.tree_ctrl.GetSelection()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while curr.IsOk() and curr != self.tree_root:
|
|
|
|
|
|
|
|
nodes.insert(0, curr)
|
|
|
|
|
|
|
|
curr = self.tree_ctrl.GetItemParent(curr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Start ikon/label
|
|
|
|
|
|
|
|
self._add_path_segment("📍 SharePoint", "ROOT")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for node in nodes:
|
|
|
|
|
|
|
|
arrow = wx.StaticText(self.path_panel, label=" > ")
|
|
|
|
|
|
|
|
arrow.SetForegroundColour(wx.Colour(150, 150, 150))
|
|
|
|
|
|
|
|
self.path_sizer.Add(arrow, 0, wx.ALIGN_CENTER_VERTICAL)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
name = self.tree_ctrl.GetItemText(node)
|
|
|
|
|
|
|
|
self._add_path_segment(name, node)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.path_panel.Layout()
|
|
|
|
|
|
|
|
self.path_panel.Refresh()
|
|
|
|
|
|
|
|
self.Layout() # Tving rammen til at opdatere, så stien kommer frem
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 login(self, event):
|
|
|
|
self.set_status("Logger ind...")
|
|
|
|
self.set_status("Logger ind...")
|
|
|
|
app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY)
|
|
|
|
app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY)
|
|
|
|
accounts = app.get_accounts()
|
|
|
|
accounts = app.get_accounts()
|
|
|
|
@@ -115,166 +276,290 @@ class SharePointApp(ctk.CTk):
|
|
|
|
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.configure(state="disabled", text="Logget ind", fg_color="gray")
|
|
|
|
self.login_btn.Disable()
|
|
|
|
self.home_btn.configure(state="normal")
|
|
|
|
self.login_btn.SetLabel("Logget ind")
|
|
|
|
|
|
|
|
self.login_btn.SetBackgroundColour(wx.Colour(200, 200, 200)) # Grå
|
|
|
|
|
|
|
|
self.home_btn.Enable()
|
|
|
|
self.load_sites()
|
|
|
|
self.load_sites()
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.set_status("Login fejlede.")
|
|
|
|
self.set_status("Login fejlede.")
|
|
|
|
messagebox.showerror("Login Error", result.get("error_description", "Unknown error"))
|
|
|
|
wx.MessageBox(result.get("error_description", "Unknown error"), "Login Error", wx.OK | wx.ICON_ERROR)
|
|
|
|
|
|
|
|
|
|
|
|
def clear_main(self):
|
|
|
|
def load_sites(self, event=None):
|
|
|
|
for widget in self.main_frame.winfo_children():
|
|
|
|
|
|
|
|
widget.destroy()
|
|
|
|
|
|
|
|
self.back_btn.configure(state="normal" if self.history else "disabled")
|
|
|
|
|
|
|
|
self.update_path_display()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_explorer_item(self, text, icon, command, is_file=False):
|
|
|
|
|
|
|
|
# En knap der ligner et række-element i Windows Explorer
|
|
|
|
|
|
|
|
frame = ctk.CTkFrame(self.main_frame, fg_color="transparent")
|
|
|
|
|
|
|
|
frame.pack(fill="x", padx=10, pady=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
btn = ctk.CTkButton(
|
|
|
|
|
|
|
|
frame,
|
|
|
|
|
|
|
|
text=f" {icon} {text}",
|
|
|
|
|
|
|
|
anchor="w",
|
|
|
|
|
|
|
|
height=35,
|
|
|
|
|
|
|
|
fg_color="transparent",
|
|
|
|
|
|
|
|
text_color=("gray10", "gray90"),
|
|
|
|
|
|
|
|
hover_color=("gray80", "gray25"),
|
|
|
|
|
|
|
|
font=ctk.CTkFont(size=13),
|
|
|
|
|
|
|
|
command=command
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
btn.pack(fill="x")
|
|
|
|
|
|
|
|
return btn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_sites(self):
|
|
|
|
|
|
|
|
self.set_status("Henter sites...")
|
|
|
|
self.set_status("Henter sites...")
|
|
|
|
self.clear_main()
|
|
|
|
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.current_path = ["SharePoint"]
|
|
|
|
self.history = []
|
|
|
|
self.history = []
|
|
|
|
self.update_path_display()
|
|
|
|
self.update_path_display()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=self._fetch_sites_bg, daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fetch_sites_bg(self):
|
|
|
|
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:
|
|
|
|
sites = res.json().get('value', [])
|
|
|
|
sites = res.json().get('value', [])
|
|
|
|
# Sorter sites alfabetisk
|
|
|
|
|
|
|
|
sites.sort(key=lambda x: x.get('displayName', x.get('name', '')).lower())
|
|
|
|
sites.sort(key=lambda x: x.get('displayName', x.get('name', '')).lower())
|
|
|
|
for site in sites:
|
|
|
|
wx.CallAfter(self._populate_sites_tree, sites)
|
|
|
|
name = site.get('displayName', site.get('name'))
|
|
|
|
|
|
|
|
self.create_explorer_item(name, "🌐", lambda s=site['id'], n=name: self.select_site(s, n))
|
|
|
|
|
|
|
|
self.set_status(f"Fandt {len(sites)} sites.")
|
|
|
|
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.set_status("Kunne ikke hente sites.")
|
|
|
|
self.set_status("Kunne ikke hente sites.")
|
|
|
|
|
|
|
|
|
|
|
|
def select_site(self, site_id, name):
|
|
|
|
def _populate_sites_tree(self, sites):
|
|
|
|
self.history.append(("SITES", None, "SharePoint"))
|
|
|
|
self.set_status(f"Fandt {len(sites)} sites.")
|
|
|
|
self.current_site_id = site_id
|
|
|
|
for site in sites:
|
|
|
|
self.current_path.append(name)
|
|
|
|
name = site.get('displayName', site.get('name'))
|
|
|
|
self.load_drives(site_id)
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
def load_drives(self, site_id):
|
|
|
|
# Select the first site or just show in list (defaulting to showing root sites in list)
|
|
|
|
self.set_status("Henter dokumentbiblioteker...")
|
|
|
|
self.list_ctrl.DeleteAllItems()
|
|
|
|
self.clear_main()
|
|
|
|
self.current_items = []
|
|
|
|
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives"
|
|
|
|
for i, site in enumerate(sites):
|
|
|
|
res = requests.get(url, headers=self.headers)
|
|
|
|
name = site.get('displayName', site.get('name'))
|
|
|
|
if res.status_code == 200:
|
|
|
|
self.list_ctrl.InsertItem(i, name, self.idx_site)
|
|
|
|
drives = res.json().get('value', [])
|
|
|
|
self.list_ctrl.SetItem(i, 1, "Site")
|
|
|
|
# Sorter biblioteker alfabetisk
|
|
|
|
self.current_items.append({"type": "SITE", "id": site['id'], "name": name})
|
|
|
|
drives.sort(key=lambda x: x.get('name', '').lower())
|
|
|
|
|
|
|
|
for drive in drives:
|
|
|
|
|
|
|
|
name = drive.get('name')
|
|
|
|
|
|
|
|
self.create_explorer_item(name, "📚", lambda d=drive['id'], n=name: self.select_drive(d, n))
|
|
|
|
|
|
|
|
self.set_status("Vælg et bibliotek.")
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self.set_status("Fejl ved hentning af biblioteker.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def select_drive(self, drive_id, name):
|
|
|
|
def on_tree_expanding(self, event):
|
|
|
|
self.history.append(("DRIVES", self.current_site_id, self.current_path[-1]))
|
|
|
|
item = event.GetItem()
|
|
|
|
self.current_drive_id = drive_id
|
|
|
|
data = self.tree_item_data.get(item)
|
|
|
|
self.current_path.append(name)
|
|
|
|
if not data or data.get("loaded"):
|
|
|
|
self.load_folder("root")
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
def load_folder(self, folder_id):
|
|
|
|
loading_node = self.tree_ctrl.AppendItem(item, "Indlæser...")
|
|
|
|
self.set_status("Indlæser filer...")
|
|
|
|
threading.Thread(target=self._fetch_tree_children_bg, args=(item, data), daemon=True).start()
|
|
|
|
self.clear_main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
url = f"https://graph.microsoft.com/v1.0/drives/{self.current_drive_id}/root/children" if folder_id == "root" else \
|
|
|
|
def _fetch_tree_children_bg(self, parent_node, data):
|
|
|
|
f"https://graph.microsoft.com/v1.0/drives/{self.current_drive_id}/items/{folder_id}/children"
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
res = requests.get(url, headers=self.headers)
|
|
|
|
def _populate_tree_drives(self, parent_node, drives, parent_data):
|
|
|
|
if res.status_code == 200:
|
|
|
|
self.tree_ctrl.DeleteChildren(parent_node)
|
|
|
|
items = res.json().get('value', [])
|
|
|
|
parent_data["loaded"] = True
|
|
|
|
# Sorter: Mapper først, derefter filer. Begge alfabetisk.
|
|
|
|
target_node = None
|
|
|
|
items.sort(key=lambda x: (not 'folder' in x, x['name'].lower()))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.current_folder_id = folder_id
|
|
|
|
for drive in drives:
|
|
|
|
for item in items:
|
|
|
|
name = drive.get('name', 'Ukendt')
|
|
|
|
name, item_id, is_folder = item['name'], item['id'], 'folder' in item
|
|
|
|
drive_id = drive['id']
|
|
|
|
icon = "📁" if is_folder else "📄"
|
|
|
|
node = self.tree_ctrl.AppendItem(parent_node, name, image=self.idx_drive)
|
|
|
|
cmd = lambda i=item_id, f=is_folder, n=name: self.item_clicked(i, f, n)
|
|
|
|
self.tree_item_data[node] = {
|
|
|
|
self.create_explorer_item(name, icon, cmd, not is_folder)
|
|
|
|
"type": "DRIVE", "id": drive_id, "name": name,
|
|
|
|
self.set_status("Klar")
|
|
|
|
"drive_id": drive_id, "path": parent_data["path"] + [name], "loaded": False
|
|
|
|
else:
|
|
|
|
}
|
|
|
|
self.set_status(f"Fejl: {res.status_code}")
|
|
|
|
self.tree_ctrl.SetItemHasChildren(node, True)
|
|
|
|
|
|
|
|
if drive_id == getattr(self, '_pending_tree_selection_id', None):
|
|
|
|
|
|
|
|
target_node = node
|
|
|
|
|
|
|
|
|
|
|
|
def item_clicked(self, item_id, is_folder, name):
|
|
|
|
if target_node:
|
|
|
|
if is_folder:
|
|
|
|
self._pending_tree_selection_id = None
|
|
|
|
# Gem det ID vi er i NU, før vi går dybere, så vi kan vende tilbage til det
|
|
|
|
self.tree_ctrl.SelectItem(target_node)
|
|
|
|
self.history.append(("FOLDERS", self.current_folder_id, self.current_path[-1]))
|
|
|
|
|
|
|
|
self.current_path.append(name)
|
|
|
|
|
|
|
|
self.load_folder(item_id)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
threading.Thread(target=self.process_file, args=(item_id, name), daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def go_back(self):
|
|
|
|
def _populate_tree_folders(self, parent_node, folders, parent_data):
|
|
|
|
if not self.history: return
|
|
|
|
self.tree_ctrl.DeleteChildren(parent_node)
|
|
|
|
mode, prev_id, path_segment = self.history.pop()
|
|
|
|
parent_data["loaded"] = True
|
|
|
|
self.current_path.pop()
|
|
|
|
target_node = None
|
|
|
|
|
|
|
|
|
|
|
|
if mode == "SITES":
|
|
|
|
for folder in folders:
|
|
|
|
self.load_sites()
|
|
|
|
name = folder['name']
|
|
|
|
elif mode == "DRIVES":
|
|
|
|
folder_id = folder['id']
|
|
|
|
self.load_drives(prev_id)
|
|
|
|
node = self.tree_ctrl.AppendItem(parent_node, name, image=self.idx_folder)
|
|
|
|
elif mode == "FOLDERS":
|
|
|
|
self.tree_item_data[node] = {
|
|
|
|
# Nu bruger vi det præcise ID vi gemte før (prev_id er folder_id)
|
|
|
|
"type": "FOLDER", "id": folder_id, "name": name,
|
|
|
|
self.load_folder(prev_id)
|
|
|
|
"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 self.is_editing: return
|
|
|
|
|
|
|
|
item = event.GetItem()
|
|
|
|
|
|
|
|
data = self.tree_item_data.get(item)
|
|
|
|
|
|
|
|
if not data:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fetch_list_contents_bg(self, data):
|
|
|
|
|
|
|
|
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": ""
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
self.list_ctrl.SetItem(i, 2, 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']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_item_activated(self, event):
|
|
|
|
|
|
|
|
if self.is_editing: return
|
|
|
|
|
|
|
|
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.current_drive_id = item['drive_id']
|
|
|
|
|
|
|
|
threading.Thread(target=self.process_file, args=(item['id'], item['name']), daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sync_tree_selection(self, target_id):
|
|
|
|
|
|
|
|
selected = self.tree_ctrl.GetSelection()
|
|
|
|
|
|
|
|
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 self.is_editing: return
|
|
|
|
|
|
|
|
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 process_file(self, item_id, file_name):
|
|
|
|
def process_file(self, item_id, file_name):
|
|
|
|
base_url = f"https://graph.microsoft.com/v1.0/drives/{self.current_drive_id}/items/{item_id}"
|
|
|
|
self.is_editing = True
|
|
|
|
|
|
|
|
self.lock_ui(True)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
# 1. Checkout
|
|
|
|
# 1. Lokation info
|
|
|
|
self.set_status(f"Tjekker '{file_name}' ud...")
|
|
|
|
site_id = self.current_site_id
|
|
|
|
checkout_res = requests.post(f"{base_url}/checkout", headers=self.headers)
|
|
|
|
drive_id = self.current_drive_id
|
|
|
|
if checkout_res.status_code not in [200, 204]:
|
|
|
|
base_url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives/{drive_id}/items/{item_id}"
|
|
|
|
print(f"Checkout info: {checkout_res.status_code} - {checkout_res.text}")
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
# 2. Download
|
|
|
|
self.set_status(f"Downloader '{file_name}'...")
|
|
|
|
self.set_status(f"Henter '{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"Download fejlede: {res.status_code}")
|
|
|
|
raise Exception(f"Kunne ikke hente fil: {res.status_code}")
|
|
|
|
|
|
|
|
|
|
|
|
# Lav en unik undermappe til filen for at bevare det originale navn uden konflikter
|
|
|
|
|
|
|
|
short_hash = hashlib.md5(item_id.encode()).hexdigest()[:8]
|
|
|
|
|
|
|
|
file_dir = os.path.join(TEMP_DIR, short_hash)
|
|
|
|
|
|
|
|
if not os.path.exists(file_dir):
|
|
|
|
|
|
|
|
os.makedirs(file_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local_path = os.path.join(file_dir, file_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with open(local_path, 'wb') as f:
|
|
|
|
with open(local_path, 'wb') as f:
|
|
|
|
f.write(res.content)
|
|
|
|
f.write(res.content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Checkout
|
|
|
|
|
|
|
|
requests.post(f"{base_url}/checkout", headers=self.headers)
|
|
|
|
|
|
|
|
|
|
|
|
# 3. Åbn & Overvåg
|
|
|
|
# 3. Åbn & Overvåg
|
|
|
|
self.set_status(f"Åbner '{file_name}'... Vent på programmet starter.")
|
|
|
|
self.set_status(f"Åbner '{file_name}'...")
|
|
|
|
os.startfile(local_path)
|
|
|
|
os.startfile(local_path)
|
|
|
|
|
|
|
|
|
|
|
|
# Vent på at programmet rent faktisk låser filen
|
|
|
|
|
|
|
|
locked = False
|
|
|
|
locked = False
|
|
|
|
self.set_status(f"Venter på at '{file_name}' åbnes i redigeringsprogram...")
|
|
|
|
self.set_status(f"Venter på '{file_name}'...")
|
|
|
|
for _ in range(10): # Vent op til 10 sekunder på at filen bliver låst
|
|
|
|
for _ in range(10):
|
|
|
|
time.sleep(1)
|
|
|
|
time.sleep(1)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
os.rename(local_path, local_path)
|
|
|
|
os.rename(local_path, local_path)
|
|
|
|
@@ -283,42 +568,53 @@ class SharePointApp(ctk.CTk):
|
|
|
|
break
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if locked:
|
|
|
|
if locked:
|
|
|
|
self.set_status(f"Redigerer '{file_name}' - Luk programmet for at gemme.")
|
|
|
|
self.set_status(f"Redigerer '{file_name}' - Luk for at gemme.")
|
|
|
|
# Nu venter vi på at den bliver låst OP igen
|
|
|
|
|
|
|
|
while True:
|
|
|
|
while True:
|
|
|
|
time.sleep(2)
|
|
|
|
time.sleep(2)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
os.rename(local_path, local_path)
|
|
|
|
os.rename(local_path, local_path)
|
|
|
|
break # Filen er ikke længere låst
|
|
|
|
break
|
|
|
|
except OSError:
|
|
|
|
except OSError:
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.set_status("Kunne ikke detektere fillås. Tryk OK når du er færdig.")
|
|
|
|
self.set_status("Fillås ikke detekteret.")
|
|
|
|
messagebox.showinfo("Info", f"Vi kunne ikke detektere om '{file_name}' blev låst af dit program.\n\nTryk OK når du har gemt og LUKKET filen i dit redigeringsprogram.")
|
|
|
|
self.edit_wait_event.clear()
|
|
|
|
|
|
|
|
wx.CallAfter(self.done_btn.Show)
|
|
|
|
|
|
|
|
wx.CallAfter(self.Layout)
|
|
|
|
|
|
|
|
self.edit_wait_event.wait()
|
|
|
|
|
|
|
|
wx.CallAfter(self.done_btn.Hide)
|
|
|
|
|
|
|
|
wx.CallAfter(self.Layout)
|
|
|
|
|
|
|
|
|
|
|
|
# 4. Upload
|
|
|
|
# 4. Upload
|
|
|
|
self.set_status(f"Uploader ændringer til SharePoint...")
|
|
|
|
self.set_status(f"Uploader ændringer...")
|
|
|
|
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} - {upload_res.text}")
|
|
|
|
raise Exception(f"Upload fejlede: {upload_res.status_code}")
|
|
|
|
|
|
|
|
|
|
|
|
# 5. Checkin
|
|
|
|
# 5. Checkin
|
|
|
|
self.set_status(f"Tjekker '{file_name}' ind...")
|
|
|
|
self.set_status(f"Tjekker '{file_name}' ind...")
|
|
|
|
checkin_res = 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": "Opdateret via SP Explorer"})
|
|
|
|
if checkin_res.status_code not in [200, 204]:
|
|
|
|
|
|
|
|
print(f"Checkin info: {checkin_res.status_code} - {checkin_res.text}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
os.remove(local_path)
|
|
|
|
# Oprydning: Slet fil og derefter mappe
|
|
|
|
self.set_status(f"Succes! '{file_name}' er opdateret på SharePoint.")
|
|
|
|
try:
|
|
|
|
messagebox.showinfo("Færdig", f"Filen '{file_name}' er gemt og tjekket ind korrekt.")
|
|
|
|
os.remove(local_path)
|
|
|
|
|
|
|
|
os.rmdir(working_dir)
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.set_status(f"Succes! '{file_name}' er opdateret.")
|
|
|
|
|
|
|
|
self.show_info(f"Filen '{file_name}' er gemt og tjekket ind korrekt.", wx.ICON_INFORMATION)
|
|
|
|
|
|
|
|
self._refresh_current_view()
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
self.set_status(f"Fejl: {str(e)}")
|
|
|
|
self.set_status(f"Fejl: {str(e)}")
|
|
|
|
print(f"DETALJERET FEJL: {e}")
|
|
|
|
self.show_info(f"Der skete en fejl: {e}", wx.ICON_ERROR)
|
|
|
|
messagebox.showerror("Fejl", f"Der skete en fejl:\n{e}")
|
|
|
|
finally:
|
|
|
|
|
|
|
|
self.is_editing = False
|
|
|
|
|
|
|
|
self.lock_ui(False)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
if __name__ == "__main__":
|
|
|
|
app = SharePointApp()
|
|
|
|
app = wx.App()
|
|
|
|
app.mainloop()
|
|
|
|
SharePointApp()
|
|
|
|
|
|
|
|
app.MainLoop()
|
|
|
|
|