diff --git a/everything/Everything-vortex_backend.ini b/everything/Everything-vortex_backend.ini index 4630336..91646d8 100644 --- a/everything/Everything-vortex_backend.ini +++ b/everything/Everything-vortex_backend.ini @@ -38,8 +38,8 @@ allow_rescan_now=1 allow_admin_options=1 allow_advanced_settings=1 allow_dark_mode=1 -window_x=1120 -window_y=78 +window_x=0 +window_y=0 window_wide=800 window_high=680 maximized=0 diff --git a/src/main/java/tech/minediamond/vortex/service/search/EverythingService.java b/src/main/java/tech/minediamond/vortex/service/search/EverythingService.java index 94478f4..9dd3442 100644 --- a/src/main/java/tech/minediamond/vortex/service/search/EverythingService.java +++ b/src/main/java/tech/minediamond/vortex/service/search/EverythingService.java @@ -24,17 +24,18 @@ import com.sun.jna.Native; import com.sun.jna.WString; import com.sun.jna.platform.win32.WinDef; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; import lombok.extern.slf4j.Slf4j; +import tech.minediamond.vortex.model.fileData.FileData; import tech.minediamond.vortex.model.fileData.FileType; import tech.minediamond.vortex.model.search.EverythingQuery; -import tech.minediamond.vortex.model.fileData.FileData; import tech.minediamond.vortex.model.search.SearchMode; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -66,20 +67,31 @@ public class EverythingService { private static final int MAX_PATH = 32767; private static final String EVERYTHING_PATH = Paths.get("everything\\Everything64.exe").toFile().getAbsolutePath(); + private static final Pattern FORBIDDEN_CHAR_PATTERN = Pattern.compile( + Pattern.quote("\"") + "|" + + Pattern.quote("*") + "|" + + Pattern.quote("?") + "|" + + Pattern.quote("<") + "|" + + Pattern.quote(">") + "|" + + Pattern.quote("|") + ); private Everything3.EverythingClient client = null; private final Everything3 lib = Everything3.INSTANCE; - Thread linkEverythingThread; + private Thread linkEverythingThread; + private final Runnable linkEverythingRunnable; + private Process everythingProcess; + private final ReadOnlyBooleanWrapper searchServiceHealthProperty = new ReadOnlyBooleanWrapper(false);//供外部调用 + @Inject public EverythingService() throws IOException, InterruptedException { - StartEverythingInstance(); - linkEverythingThread = new Thread(() -> { + linkEverythingRunnable = () -> { try { TimeUnit.SECONDS.sleep(1); for (int i = 0; i < 20; i++) { - client = LinkEverythingInstance(); - if (client != null) { + boolean linkSuccess = linkEverythingInstance(); + if (linkSuccess) { log.info("everything连接成功"); return; } else { @@ -90,9 +102,20 @@ public EverythingService() throws IOException, InterruptedException { } catch (InterruptedException e) { log.warn("linkEverythingThread被中断"); } + }; + + linkEverythingThread = new Thread(linkEverythingRunnable); + StartEverythingInstance(); + connectEverythingInstance(); + searchServiceHealthProperty.addListener((observable,oldValue,newValue) -> { + if (!newValue){ + connectEverythingInstance(); + } }); - linkEverythingThread.setName("Link Everything Instance Thread"); - linkEverythingThread.start(); + } + + public ReadOnlyBooleanProperty getSearchServiceHealthProperty(){ + return searchServiceHealthProperty.getReadOnlyProperty(); } @@ -117,7 +140,7 @@ public void StartEverythingInstance() throws IOException { pb.redirectErrorStream(true); pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); pb.redirectError(ProcessBuilder.Redirect.DISCARD); - pb.start(); + everythingProcess = pb.start(); } /** @@ -142,25 +165,65 @@ public void stopEverythingInstance() throws IOException { } /** - * 连接到正在运行的Everything实例。 + * 连接到正在运行的Everything实例(尝试多次,异步执行)。 + */ + public void connectEverythingInstance() { + log.debug("尝试连接/重新连接EverythingInstance"); + if (everythingProcess == null || !everythingProcess.isAlive()) {//当everything未运行 + log.debug("everything未运行"); + try { + client = null; + StartEverythingInstance(); + if (!linkEverythingThread.isAlive()){ + startLinkEverythingThread(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } else {//当everything已运行 + if (!linkEverythingThread.isAlive()){ + startLinkEverythingThread(); + } + } + log.debug("执行到这里了"); + } + + private void startLinkEverythingThread(){ + linkEverythingThread = new Thread(linkEverythingRunnable); + linkEverythingThread.setName("Link Everything Instance Thread"); + linkEverythingThread.start(); + } + + /** + * 连接到正在运行的Everything实例(仅尝试一次)。 * * @return Everything客户端句柄,连接失败时返回null */ - public Everything3.EverythingClient LinkEverythingInstance() { + public boolean linkEverythingInstance() { client = lib.Everything3_ConnectW(new WString("vortex_backend")); - return client; + if (client != null) { + searchServiceHealthProperty.set(true); + return true; + } + return false; } public List query(EverythingQuery query) { // 防御性检查 + if (!searchServiceHealthProperty.get()){ + return Collections.emptyList(); + } + if (client == null) { log.error("无法执行查询:Everything 服务未连接。"); + searchServiceHealthProperty.set(false); return Collections.emptyList(); } if (!lib.Everything3_IsDBLoaded(client)) { log.error("无法执行查询:Everything数据库未加载。"); + searchServiceHealthProperty.set(false); return Collections.emptyList(); } @@ -172,9 +235,8 @@ public List query(EverythingQuery query) { searchState = lib.Everything3_CreateSearchState(); if (searchState == null) { log.error("无法执行查询:创建搜索失败。"); + return Collections.emptyList(); } - // 设置搜索关键字 - String finalQueryString; // 设置返回结果的最大数量 lib.Everything3_SetSearchViewportCount(searchState, new WinDef.DWORD(200)); //设置搜索内容 @@ -183,30 +245,8 @@ public List query(EverythingQuery query) { lib.Everything3_AddSearchPropertyRequest(searchState, Everything3.PropertyType.FILE_NAME.getID()); lib.Everything3_AddSearchPropertyRequest(searchState, Everything3.PropertyType.IS_FOLDER.getID()); - //生成搜索词字符串 - String queryKeywords = "\"" + query.query() + "\""; - //去除部分关键字 - List forbiddenChar = Arrays.asList("\"", "*", "?", "<", ">", "|"); - String regex = forbiddenChar.stream() - .map(Pattern::quote) // 使用Pattern.quote处理特殊字符 - .collect(Collectors.joining("|")); - - queryKeywords = queryKeywords.replaceAll(regex, ""); - - //生成文件夹字符串 - String pathQueryPart = ""; - if (query.targetFolders().isPresent()) { - pathQueryPart = buildPathQueryPart(query); - - } - - //生成搜索模式字符串 - String searchModeQueryPart = ""; - if (query.searchMode().isPresent()) { - searchModeQueryPart = buildSearchModeQueryPart(query); - } - - finalQueryString = pathQueryPart + " "+ searchModeQueryPart + queryKeywords; + // 设置搜索关键字 + String finalQueryString = buildQueryString(query); //执行搜索 log.info("正在执行搜索 '{}'...", query); @@ -220,29 +260,29 @@ public List query(EverythingQuery query) { WinDef.DWORD numResults = lib.Everything3_GetResultListViewportCount(resultList); log.info("找到 {} 个结果:", numResults.intValue()); - char[] buffer = new char[MAX_PATH]; - WinDef.DWORD resultListindex = new WinDef.DWORD(); - for (int i = 0; i < numResults.intValue(); i++) { + WinDef.DWORD resultListIndex = new WinDef.DWORD(); + WinDef.DWORD maxPathLength = new WinDef.DWORD(MAX_PATH); + for (int i = 0; i < numResults.intValue(); i++) {//遍历搜索结果 FileData fileData = new FileData(); - resultListindex.setValue(i); + resultListIndex.setValue(i); //获取搜索结果的完整路径 - lib.Everything3_GetResultPropertyTextW(resultList, resultListindex, Everything3.PropertyType.FULL_PATH.getID(), buffer, new WinDef.DWORD(MAX_PATH)); + lib.Everything3_GetResultPropertyTextW(resultList, resultListIndex, Everything3.PropertyType.FULL_PATH.getID(), buffer, maxPathLength); String pathname = Native.toString(buffer); fileData.setFullPath(pathname); //获取搜索结果的名称 - lib.Everything3_GetResultPropertyTextW(resultList, resultListindex, Everything3.PropertyType.FILE_NAME.getID(), buffer, new WinDef.DWORD(MAX_PATH)); + lib.Everything3_GetResultPropertyTextW(resultList, resultListIndex, Everything3.PropertyType.FILE_NAME.getID(), buffer, maxPathLength); String filename = Native.toString(buffer); fileData.setFileName(filename); //获取搜索结果的大小(单位:Byte) //这里直接将返回的无符号int64转换为long,但是考虑到无符号int64达到最大位需要文件8EB以上的大小,因此直接赋值问题不大 - long size = lib.Everything3_GetResultSize(resultList, resultListindex); + long size = lib.Everything3_GetResultSize(resultList, resultListIndex); fileData.setSize(size); //获取文件的类型 - byte type = lib.Everything3_GetResultPropertyBYTE(resultList, resultListindex, Everything3.PropertyType.IS_FOLDER.getID()); + byte type = lib.Everything3_GetResultPropertyBYTE(resultList, resultListIndex, Everything3.PropertyType.IS_FOLDER.getID()); int intType = type & 0xFF; if (intType != 0) { fileData.setType(FileType.FOLDER); @@ -260,10 +300,35 @@ public List query(EverythingQuery query) { lib.Everything3_DestroySearchState(searchState); } } - log.info(results.toString()); + searchServiceHealthProperty.set(true); + log.debug(results.toString()); return results; } + // 构建查询字符串 + private String buildQueryString(EverythingQuery query) { + StringBuilder queryString = new StringBuilder(); + + // 添加路径查询部分 + if(query.targetFolders().isPresent()){ + String pathQueryPart = buildPathQueryPart(query); + queryString.append(pathQueryPart).append(" "); + } + + // 添加搜索模式部分 + if (query.searchMode().isPresent()) { + String searchModeQueryPart = buildSearchModeQueryPart(query); + queryString.append(searchModeQueryPart); + } + + // 添加关键词部分 + String queryKeywords = "\"" + query.query() + "\""; + queryKeywords = FORBIDDEN_CHAR_PATTERN.matcher(queryKeywords).replaceAll(""); + queryString.append(queryKeywords); + + return queryString.toString(); + } + //构建搜索路径部分字符串 private String buildPathQueryPart(EverythingQuery query) { List targetFolders = query.targetFolders().orElseGet(ArrayList::new); diff --git a/src/main/java/tech/minediamond/vortex/service/search/SearchService.java b/src/main/java/tech/minediamond/vortex/service/search/SearchService.java index 3e4dd76..5c26686 100644 --- a/src/main/java/tech/minediamond/vortex/service/search/SearchService.java +++ b/src/main/java/tech/minediamond/vortex/service/search/SearchService.java @@ -21,6 +21,7 @@ import com.google.inject.Inject; import com.google.inject.Injector; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.Service; @@ -40,6 +41,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; +/** + * 连接 {@link tech.minediamond.vortex.ui.controller.SearchPanel} 和 {@link EverythingService}的关键服务 + */ @Slf4j public class SearchService extends Service { @@ -48,6 +52,7 @@ public class SearchService extends Service { private final EverythingService everythingService; private final I18nService i18n; private final Injector injector; + private final ReadOnlyBooleanProperty searchServiceHealthProperty; private final StringProperty keyword = new SimpleStringProperty(); private final ThreadFactory searchThreadFactory; @@ -58,6 +63,7 @@ public SearchService(EverythingService everythingService, I18nService i18n, Inje this.everythingService = everythingService; this.i18n = i18n; this.injector = injector; + this.searchServiceHealthProperty = everythingService.getSearchServiceHealthProperty(); searchThreadFactory = r -> { Thread t = new Thread(r, NAME); @@ -92,8 +98,7 @@ protected ComponentList call() throws Exception { ComponentList componentList = new ComponentList(); if (results.isEmpty()) { - updateProgress(0, 1); - return null; + throw new Exception("Result is Empty"); } for (FileData result : results) { @@ -108,6 +113,22 @@ protected ComponentList call() throws Exception { log.info("搜索成功"); return componentList; } + + @Override + public void succeeded() { + super.succeeded(); + updateProgress(1, 1); + } + + @Override + public void failed() { + super.failed(); + if (searchServiceHealthProperty.get()) { + updateProgress(0, 1); + } else { + updateProgress(0.5, 1); + } + } }; } diff --git a/src/main/java/tech/minediamond/vortex/ui/controller/SearchPanel.java b/src/main/java/tech/minediamond/vortex/ui/controller/SearchPanel.java index 9f0a948..039a25c 100644 --- a/src/main/java/tech/minediamond/vortex/ui/controller/SearchPanel.java +++ b/src/main/java/tech/minediamond/vortex/ui/controller/SearchPanel.java @@ -34,6 +34,9 @@ import tech.minediamond.vortex.service.i18n.I18nService; import tech.minediamond.vortex.service.search.SearchService; +/** + * 搜索面板控制器类,负责处理搜索界面的逻辑和状态管理 + */ @Singleton @Slf4j public class SearchPanel { @@ -41,18 +44,25 @@ public class SearchPanel { @FXML private ScrollPane scrollPane; + // 防抖机制:延迟300毫秒执行搜索,避免频繁触发搜索请求 private final PauseTransition debounce = new PauseTransition(Duration.millis(300)); private String keyword; private final SearchService searchService; private final I18nService i18n; + // 搜索状态属性,用于监控和响应搜索状态变化 ObjectProperty searchStatusProperty = new SimpleObjectProperty<>(); + // 搜索提示和未找到结果的提示容器 HBox searchTiphbox = new HBox(); HBox searchNotFoundTiphbox = new HBox(); + HBox serviceErrorTiphbox = new HBox(); + /** + * 搜索状态枚举,定义搜索过程中的不同状态 + */ enum SearchStatus { - SEARCHING, PENDING,NOT_FOUND + SEARCHING, SEARCHED, PENDING, NOT_FOUND, SERVICE_ERROR } @Inject @@ -68,38 +78,58 @@ public SearchPanel(SearchService searchService, I18nService i18n) { } public void initialize() { - scrollPane.contentProperty().bind(searchService.valueProperty()); - + // 等待搜索提示 Label searchtipLabel = new Label(i18n.t("search.pending.text")); searchTiphbox.getChildren().add(searchtipLabel); searchTiphbox.setAlignment(Pos.CENTER); + // 未找到结果提示 Label searchNotFoundTipLabel = new Label(i18n.t("search.result.notFound.text")); searchNotFoundTiphbox.getChildren().add(searchNotFoundTipLabel); searchNotFoundTiphbox.setAlignment(Pos.CENTER); + Label serviceErrorTipLabel = new Label("abc"); + serviceErrorTiphbox.getChildren().add(serviceErrorTipLabel); + serviceErrorTiphbox.setAlignment(Pos.CENTER); + + // 监听搜索状态变化,根据状态更新界面显示 searchStatusProperty.addListener((observable, oldValue, newValue) -> {//监控不同的状态展示不同的界面 switch (newValue) { case PENDING -> { - scrollPane.contentProperty().unbind(); scrollPane.contentProperty().set(searchTiphbox); } case NOT_FOUND -> { - scrollPane.contentProperty().unbind(); scrollPane.contentProperty().set(searchNotFoundTiphbox); } - case SEARCHING -> {scrollPane.contentProperty().bind(searchService.valueProperty());} + case SERVICE_ERROR -> { + scrollPane.contentProperty().set(serviceErrorTiphbox); + } + case SEARCHING -> { + }//显示处于搜索状态时显示之前的画面,考虑到Everything引擎搜索速度极快,不显示专门的搜索中页面 + case SEARCHED -> { + scrollPane.contentProperty().set(searchService.valueProperty().get()); + } } }); searchStatusProperty.set(SearchStatus.PENDING); + // 监听搜索进度变化,根据进度更新搜索状态,0代表未找到结果,1代表搜索完成 searchService.progressProperty().addListener((observable, oldValue, newValue) -> { if (newValue.equals(0.0)) { searchStatusProperty.set(SearchStatus.NOT_FOUND); + } else if (newValue.equals(1.0)) { + searchStatusProperty.set(SearchStatus.SEARCHED); + } else if (newValue.equals(0.5)) { + searchStatusProperty.set(SearchStatus.SERVICE_ERROR); } }); } + /** + * 执行搜索方法 + * + * @param keyword 搜索关键词 + */ public void search(String keyword) { log.debug("即将搜索"); searchStatusProperty.set(SearchStatus.SEARCHING); @@ -107,7 +137,10 @@ public void search(String keyword) { debounce.playFromStart(); } - public void searchClear(){ + /** + * 清除搜索状态,重置为等待搜索状态 + */ + public void searchClear() { searchStatusProperty.set(SearchStatus.PENDING); } diff --git a/src/main/resources/lang/I18N.properties b/src/main/resources/lang/I18N.properties index 2322c21..59be553 100644 --- a/src/main/resources/lang/I18N.properties +++ b/src/main/resources/lang/I18N.properties @@ -44,6 +44,7 @@ alert.exit.headText=System not supported alert.exit.contentText=Vortex only supports running on Windows systems\nIt does not support running on Linux, Mac, or other\nsystems search.pending.text=Type text to search search.result.notFound.text=No results found, try searching with a different keyword +search.serviceError.text=Search service is not loaded or has encountered an error file.open.tip=open file.openInFolder.tip=show in Folder diff --git a/src/main/resources/lang/I18N_en.properties b/src/main/resources/lang/I18N_en.properties index 447587d..71e7074 100644 --- a/src/main/resources/lang/I18N_en.properties +++ b/src/main/resources/lang/I18N_en.properties @@ -44,6 +44,7 @@ alert.exit.headText=System not supported alert.exit.contentText=Vortex only supports running on Windows systems\nIt does not support running on Linux, Mac, or other\nsystems search.pending.text=Type text to search search.result.notFound.text=No results found, try searching with a different keyword +search.serviceError.text=Search service is not loaded or has encountered an error file.open.tip=open file.openInFolder.tip=show in Folder file.copyPath.tip=copy File Path diff --git a/src/main/resources/lang/I18N_zh_CN.properties b/src/main/resources/lang/I18N_zh_CN.properties index dda7394..18f1117 100644 --- a/src/main/resources/lang/I18N_zh_CN.properties +++ b/src/main/resources/lang/I18N_zh_CN.properties @@ -44,6 +44,7 @@ alert.exit.headText=\u7CFB\u7EDF\u4E0D\u652F\u6301 alert.exit.contentText=Vortex \u4EC5\u652F\u6301\u5728 Windows \u7CFB\u7EDF\u4E0A\u8FD0\u884C\n\u4E0D\u652F\u6301\u5728 Linux\u3001Mac \u6216\u5176\u4ED6\u7CFB\u7EDF\u4E0A\u8FD0\u884C search.pending.text=\u952E\u5165\u6587\u672C\u4EE5\u641C\u7D22 search.result.notFound.text=\u6CA1\u627E\u5230\u4EFB\u4F55\u7ED3\u679C\uFF0C\u5C1D\u8BD5\u6362\u4E00\u4E2A\u5173\u952E\u8BCD\u641C\u7D22 +search.serviceError.text=\u641C\u7D22\u670D\u52A1\u672A\u52A0\u8F7D\u6216\u51FA\u9519 file.open.tip=\u6253\u5F00 file.openInFolder.tip=\u5728\u6587\u4EF6\u5939\u4E2D\u663E\u793A file.copyPath.tip=\u590D\u5236\u6587\u4EF6\u8DEF\u5F84 diff --git a/src/main/resources/lang/I18N_zh_TW.properties b/src/main/resources/lang/I18N_zh_TW.properties index ecc3109..a9115c3 100644 --- a/src/main/resources/lang/I18N_zh_TW.properties +++ b/src/main/resources/lang/I18N_zh_TW.properties @@ -44,6 +44,7 @@ alert.exit.headText=\u7CFB\u7D71\u4E0D\u652F\u63F4 alert.exit.contentText=Vortex \u50C5\u652F\u63F4\u5728 Windows \u7CFB\u7D71\u4E0A\u57F7\u884C\n\u4E0D\u652F\u63F4\u5728 Linux\u3001Mac \u6216\u5176\u4ED6\u7CFB\u7D71\u4E0A\u57F7\u884C search.pending.text=\u9375\u5165\u6587\u5B57\u4EE5\u641C\u5C0B search.result.notFound.text=\u6C92\u627E\u5230\u4EFB\u4F55\u7D50\u679C\uFF0C\u5617\u8A66\u63DB\u4E00\u500B\u95DC\u9375\u8A5E\u641C\u5C0B +search.serviceError.text=\u641C\u5C0B\u670D\u52D9\u672A\u8F09\u5165\u6216\u51FA\u932F file.open.tip=\u958B\u555F file.openInFolder.tip=\u5728\u8CC7\u6599\u593E\u4E2D\u986F\u793A file.copyPath.tip=\u8907\u88FD\u6A94\u6848\u8DEF\u5F91