diff --git a/.gitignore b/.gitignore index d26310f..bd9aee4 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ coverage.xml # Local settings (keep your secrets safe) settings.json +searchindex.json # Temporary files C:\Temp_SP\ diff --git a/sharepoint_browser.py b/sharepoint_browser.py index f80bf21..521b1a2 100644 --- a/sharepoint_browser.py +++ b/sharepoint_browser.py @@ -24,6 +24,7 @@ else: CONFIG_DIR = RESOURCE_DIR SETTINGS_FILE = os.path.join(CONFIG_DIR, 'settings.json') +SEARCH_INDEX_FILE = os.path.join(CONFIG_DIR, 'searchindex.json') def load_settings(): default_settings = { @@ -33,7 +34,8 @@ def load_settings(): "language": "da", # da eller en "favorites": [], # Liste over {id, name, type, drive_id, site_id, path} "fav_visible": True, - "license_key": "" + "license_key": "", + "enable_global_search": False } if not os.path.exists(SETTINGS_FILE): with open(SETTINGS_FILE, 'w', encoding='utf-8') as f: @@ -150,7 +152,15 @@ STRINGS = { "msg_fav_exists": "'{name}' er allerede i favoritter.", "settings_license_group": "Licens / Aktivering", "settings_license_key": "Licensnøgle:", - "settings_license_status": "Status: Ikke aktiveret" + "settings_license_status": "Status: Ikke aktiveret", + "settings_search_group": "Global Søgning & Indeksering", + "settings_search_enable": "Aktiver global søgning (Advarsel: Kan tage lang tid og bruge mange ressourcer/API kald)", + "btn_global_search": "Søg i alt", + "status_indexing": "Indekserer... ({count} fundet)", + "status_indexing_done": "Indeksering færdig. {count} filer klar.", + "col_path": "Sti", + "msg_search_results": "Søgeresultater for '{query}'", + "msg_search_placeholder": "Søg..." }, "en": { "title": "SharePoint Explorer", @@ -242,7 +252,15 @@ STRINGS = { "msg_fav_exists": "'{name}' is already in favorites.", "settings_license_group": "License / Activation", "settings_license_key": "License Key:", - "settings_license_status": "Status: Not activated" + "settings_license_status": "Status: Not activated", + "settings_search_group": "Global Search & Indexing", + "settings_search_enable": "Enable global search (Warning: indexing can be slow and resource-heavy)", + "btn_global_search": "Global Search", + "status_indexing": "Indexing... ({count} found)", + "status_indexing_done": "Indexing done. {count} files ready.", + "col_path": "Path", + "msg_search_results": "Search results for '{query}'", + "msg_search_placeholder": "Search..." } } @@ -398,6 +416,16 @@ class SettingsDialog(wx.Dialog): inner_vbox.Add(lang_sizer, 0, wx.EXPAND | wx.ALL, 10) + # --- Group: Global Search --- + search_box = wx.StaticBox(panel, label=self.get_txt("settings_search_group")) + search_sizer = wx.StaticBoxSizer(search_box, wx.VERTICAL) + + self.enable_search_cb = wx.CheckBox(panel, label=self.get_txt("settings_search_enable")) + self.enable_search_cb.SetValue(self.settings.get("enable_global_search", False)) + search_sizer.Add(self.enable_search_cb, 0, wx.ALL, 5) + + inner_vbox.Add(search_sizer, 0, wx.EXPAND | wx.ALL, 10) + panel.SetSizer(inner_vbox) inner_vbox.Fit(panel) vbox.Add(panel, 1, wx.EXPAND | wx.ALL, 0) @@ -427,6 +455,7 @@ class SettingsDialog(wx.Dialog): self.settings["temp_dir"] = self.temp_dir_picker.GetPath() self.settings["language"] = "da" if self.lang_choice.GetSelection() == 0 else "en" self.settings["license_key"] = self.license_ctrl.GetValue().strip() + self.settings["enable_global_search"] = self.enable_search_cb.GetValue() if not self.settings["client_id"] or not self.settings["tenant_id"]: wx.MessageBox("Client ID og Tenant ID skal udfyldes.", "Fejl", wx.OK | wx.ICON_ERROR) @@ -457,11 +486,17 @@ class SharePointApp(wx.Frame): self.active_edits = {} # item_id -> { "name": name, "event": Event, "waiting": bool } self.favorites = settings.get("favorites", []) self.fav_visible = settings.get("fav_visible", True) + self.enable_global_search = settings.get("enable_global_search", False) self.sort_col = 0 # Default (Navn) self.sort_asc = True self.compact_mode = False self.ext_icons = {} # Mapping fra .ext -> index i image_list self.current_web_url = None # URL til nuværende lokation i browser + self.search_text = "" + self.search_index = {} # site_id -> { item_id -> item_data } + self.is_searching_globally = False + + self.load_search_index() # System Ikoner self.image_list = wx.ImageList(16, 16) @@ -582,6 +617,22 @@ class SharePointApp(wx.Frame): nav_hbox.AddStretchSpacer(1) + # SØGEFELT + self.search_ctrl = wx.SearchCtrl(nav_panel, size=(200, 25), style=wx.TE_PROCESS_ENTER) + self.search_ctrl.ShowCancelButton(True) + self.search_ctrl.SetDescriptiveText(self.get_txt("msg_search_placeholder")) + self.search_ctrl.Bind(wx.EVT_TEXT, self.on_search_text) + self.search_ctrl.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.on_search_cancel) + self.search_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_global_search) + nav_hbox.Add(self.search_ctrl, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + + self.global_search_btn = wx.Button(nav_panel, label="", size=(30, 30)) + self.global_search_btn.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_FIND, wx.ART_BUTTON, (16, 16))) + self.global_search_btn.SetToolTip(self.get_txt("btn_global_search")) + self.global_search_btn.Bind(wx.EVT_BUTTON, self.on_global_search) + self.global_search_btn.Show(self.enable_global_search) + nav_hbox.Add(self.global_search_btn, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) + self.login_btn = wx.Button(nav_panel, label=self.get_txt("btn_login"), size=(130, 30)) self.login_btn.SetBackgroundColour(wx.Colour(40, 167, 69)) # Grøn self.login_btn.SetForegroundColour(wx.WHITE) @@ -673,6 +724,7 @@ class SharePointApp(wx.Frame): self.list_ctrl.InsertColumn(1, self.get_txt("col_type"), width=120) self.list_ctrl.InsertColumn(2, self.get_txt("col_size"), width=80) self.list_ctrl.InsertColumn(3, self.get_txt("col_modified"), width=180) + self.list_ctrl.InsertColumn(4, self.get_txt("col_path"), width=0) # Skjult som standard self.list_ctrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_item_activated) self.list_ctrl.Bind(wx.EVT_LIST_COL_CLICK, self.on_column_click) @@ -1185,6 +1237,11 @@ class SharePointApp(wx.Frame): self.show_info(self.get_txt("msg_settings_saved")) + # Opdater Global Search visning + self.enable_global_search = settings.get("enable_global_search", False) + self.global_search_btn.Show(self.enable_global_search) + self.nav_panel.Layout() + dlg.Destroy() def refresh_ui_texts(self): @@ -1211,6 +1268,7 @@ class SharePointApp(wx.Frame): self.list_ctrl.SetColumn(i, info) self.set_status(self.get_txt("status_ready")) + self.search_ctrl.SetDescriptiveText(self.get_txt("msg_search_placeholder")) self._refresh_current_view() def on_language_changed(self, event): @@ -1652,7 +1710,127 @@ class SharePointApp(wx.Frame): return (p, val) self.current_items.sort(key=sort_logic, reverse=not self.sort_asc) - self._update_list_view_only() + + # Filtrer baseret på søgning + display_items = self.current_items + if self.search_text: + terms = self.search_text.lower().split() + display_items = [i for i in self.current_items if all(t in i['name'].lower() for t in terms)] + + self._update_list_view_only(display_items) + + def on_search_text(self, event): + self.search_text = self.search_ctrl.GetValue().strip() + if self.is_searching_globally and not self.search_text: + self.on_search_cancel(None) + else: + self.apply_sorting() + + def on_search_cancel(self, event): + self.search_ctrl.ChangeValue("") + self.search_text = "" + self.is_searching_globally = False + # Vis normale kolonner igen + self.list_ctrl.SetColumnWidth(4, 0) + self.apply_sorting() + + def on_global_search(self, event): + if not self.enable_global_search: return + query = self.search_ctrl.GetValue().strip().lower() + if not query: return + + terms = query.split() + self.search_text = query + self.is_searching_globally = True + + results = [] + # Søg på tværs af alle indekserede sites/drives + for site_id, items in self.search_index.items(): + for item_id, item in items.items(): + name_lower = item['name'].lower() + if all(t in name_lower for t in terms): + results.append(item) + + self.current_items = results + self.set_status(f"Fandt {len(results)} emner i indekset.") + + # Vis 'Sti' kolonnen + self.list_ctrl.SetColumnWidth(4, 300) + self._update_list_view_only(results) + + def load_search_index(self): + if os.path.exists(SEARCH_INDEX_FILE): + try: + with open(SEARCH_INDEX_FILE, 'r', encoding='utf-8') as f: + self.search_index = json.load(f) + except: + self.search_index = {} + + def save_search_index(self): + try: + with open(SEARCH_INDEX_FILE, 'w', encoding='utf-8') as f: + json.dump(self.search_index, f, indent=4) + except: + pass + + def start_indexing(self, site_id, drive_id): + if not self.enable_global_search: return + threading.Thread(target=self._index_worker, args=(site_id, drive_id), daemon=True).start() + + def _index_worker(self, site_id, drive_id): + if not self.ensure_valid_token(): return + if site_id not in self.search_index: + self.search_index[site_id] = {} + + found_count = 0 + def crawl(folder_id, current_path_parts): + nonlocal found_count + if folder_id == "root": + 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/{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_id = item['id'] + name = item['name'] + is_folder = 'folder' in item + + full_path = " / ".join(current_path_parts + [name]) + + # Gem i indeks + data = { + "type": "FOLDER" if is_folder else "FILE", + "id": item_id, + "name": name, + "drive_id": drive_id, + "site_id": site_id, + "modified": item.get('lastModifiedDateTime', '').replace('T', ' ').split('.')[0], + "size": item.get('size') if not is_folder else None, + "web_url": item.get('webUrl'), + "path_display": full_path, + "parent_path_parts": current_path_parts + } + self.search_index[site_id][item_id] = data + found_count += 1 + + if found_count % 50 == 0: + wx.CallAfter(self.set_status, self.get_txt("status_indexing", count=found_count)) + self.save_search_index() # Gem løbende + + if not self.enable_global_search: return # Stop med det samme + if is_folder: + crawl(item_id, current_path_parts + [name]) + + try: + if not self.enable_global_search: return + crawl("root", []) + self.save_search_index() + wx.CallAfter(self.set_status, self.get_txt("status_indexing_done", count=found_count)) + except: + pass def get_icon_idx_for_file(self, filename): ext = os.path.splitext(filename)[1].lower() @@ -1721,9 +1899,12 @@ class SharePointApp(wx.Frame): self.ext_icons[ext_clean] = self.idx_file return self.idx_file - def _update_list_view_only(self): + def _update_list_view_only(self, items_to_show=None): + if items_to_show is None: + items_to_show = self.current_items + self.list_ctrl.DeleteAllItems() - for i, item in enumerate(self.current_items): + for i, item in enumerate(items_to_show): img_idx = self.idx_file if item['type'] == "FOLDER": img_idx = self.idx_folder elif item['type'] == "DRIVE": img_idx = self.idx_drive @@ -1738,6 +1919,9 @@ class SharePointApp(wx.Frame): self.list_ctrl.SetItem(i, 2, size_str) self.list_ctrl.SetItem(i, 3, item['modified']) + if self.is_searching_globally: + self.list_ctrl.SetItem(i, 4, item.get('path_display', "")) + # Opdater kolonne ikoner for col in range(4): info = self.list_ctrl.GetColumn(col) @@ -1750,6 +1934,10 @@ class SharePointApp(wx.Frame): def _populate_list_ctrl(self, items_data, parent_data): if not self: return try: + # Nulstil søgning ved ny lokation + wx.CallAfter(lambda: self.search_ctrl.SetValue("")) + self.search_text = "" + self.current_items = items_data # Anvend sortering før visning @@ -1767,6 +1955,10 @@ class SharePointApp(wx.Frame): self.current_drive_id = parent_data['drive_id'] self.current_folder_id = parent_data['id'] + # Start indeksering af dette bibliotek hvis det er nyt + if self.current_site_id and self.current_drive_id and not self.is_searching_globally: + self.start_indexing(self.current_site_id, self.current_drive_id) + # Opdater knap-synlighed can_upload = self.current_drive_id is not None wx.CallAfter(lambda: self._safe_update_buttons(can_upload)) @@ -1788,7 +1980,26 @@ class SharePointApp(wx.Frame): item = self.current_items[item_idx] if item['type'] in ["SITE", "DRIVE", "FOLDER"]: - self._sync_tree_selection(item['id']) + if self.is_searching_globally: + # Ved global søgning skal vi "hoppe" til lokationen + path_parts = item.get("parent_path_parts", []) + [item['name']] + self.current_path = ["SharePoint"] + path_parts + item['path'] = self.current_path # Sørg for at baggrundstråden ved hvor vi er + + # Sæt tilstand + self.current_site_id = item.get('site_id') + self.current_drive_id = item.get('drive_id') + if item['type'] == "FOLDER": + self.current_folder_id = item['id'] + elif item['type'] == "DRIVE": + self.current_folder_id = "root" + + # Ryd søgning og indlæs indhold + self.on_search_cancel(None) + self.update_path_display() + threading.Thread(target=self._fetch_list_contents_bg, args=(item,), daemon=True).start() + else: + self._sync_tree_selection(item['id']) elif item['type'] == "FILE": self.open_file(item)