diff --git a/Wox.UI.Flutter/wox/lib/components/wox_list_item_view.dart b/Wox.UI.Flutter/wox/lib/components/wox_list_item_view.dart index 0b46cb379..7dd6bab4a 100644 --- a/Wox.UI.Flutter/wox/lib/components/wox_list_item_view.dart +++ b/Wox.UI.Flutter/wox/lib/components/wox_list_item_view.dart @@ -148,7 +148,9 @@ class WoxListItemView extends StatelessWidget { title.value, style: TextStyle( fontSize: 16, - color: isAction() ? fromCssColor(isActive ? woxTheme.actionItemActiveFontColor : woxTheme.actionItemFontColor) : fromCssColor(isActive ? woxTheme.resultItemActiveTitleColor : woxTheme.resultItemTitleColor), + color: isAction() + ? fromCssColor(isActive ? woxTheme.actionItemActiveFontColor : woxTheme.actionItemFontColor) + : fromCssColor(isActive ? woxTheme.resultItemActiveTitleColor : woxTheme.resultItemTitleColor), ), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/Wox.UI.Flutter/wox/lib/entity/wox_toolbar.dart b/Wox.UI.Flutter/wox/lib/entity/wox_toolbar.dart index ce0f92b7d..2e5cbb462 100644 --- a/Wox.UI.Flutter/wox/lib/entity/wox_toolbar.dart +++ b/Wox.UI.Flutter/wox/lib/entity/wox_toolbar.dart @@ -33,3 +33,23 @@ class ToolbarInfo { return !isEmpty(); } } + +class ToolbarMsg { + final WoxImage? icon; + final String? text; + final int displaySeconds; // how long to display the message, 0 for forever + + ToolbarMsg({ + this.icon, + this.text, + this.displaySeconds = 10, + }); + + static ToolbarMsg fromJson(Map json) { + return ToolbarMsg( + icon: WoxImage.parse(json['Icon']), + text: json['Text'] ?? '', + displaySeconds: json['DisplaySeconds'] ?? 10, + ); + } +} diff --git a/Wox.UI.Flutter/wox/lib/modules/launcher/views/wox_query_toolbar_view.dart b/Wox.UI.Flutter/wox/lib/modules/launcher/views/wox_query_toolbar_view.dart index 7e0390e3b..6d71edb10 100644 --- a/Wox.UI.Flutter/wox/lib/modules/launcher/views/wox_query_toolbar_view.dart +++ b/Wox.UI.Flutter/wox/lib/modules/launcher/views/wox_query_toolbar_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:from_css_color/from_css_color.dart'; import 'package:get/get.dart'; import 'package:wox/components/wox_hotkey_view.dart'; @@ -13,42 +14,103 @@ class WoxQueryToolbarView extends GetView { Widget leftTip() { return Obx(() { final toolbarInfo = controller.toolbar.value; + return SizedBox( + width: 550, + child: Row( + children: [ + if (toolbarInfo.icon != null) + Padding( + padding: const EdgeInsets.only(right: 8), + child: WoxImageView(woxImage: toolbarInfo.icon!, width: 24, height: 24), + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final textSpan = TextSpan( + text: toolbarInfo.text ?? '', + style: TextStyle(color: fromCssColor(controller.woxTheme.value.toolbarFontColor)), + ); + final textPainter = TextPainter( + text: textSpan, + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(maxWidth: constraints.maxWidth); + + final isTextOverflow = textPainter.didExceedMaxLines; + + return Row( + children: [ + Expanded( + child: Text( + toolbarInfo.text ?? '', + style: TextStyle(color: fromCssColor(controller.woxTheme.value.toolbarFontColor)), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + if (isTextOverflow) + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: toolbarInfo.text ?? '')); + controller.toolbarCopyText.value = 'Copied'; // 更新状态为 "Copied" + Future.delayed(const Duration(seconds: 3), () { + controller.toolbarCopyText.value = 'Copy'; // 3秒后恢复为 "Copy" + }); + }, + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Obx(() => Text( + controller.toolbarCopyText.value, // 使用状态变量 + style: TextStyle( + color: fromCssColor(controller.woxTheme.value.toolbarFontColor), + fontSize: 12, + decoration: TextDecoration.underline, + ), + )), + ), + ), + ), + ], + ); + }, + ), + ), + ], + ), + ); + }); + } + + Widget rightTip() { + return Obx(() { + final toolbarInfo = controller.toolbar.value; + if (toolbarInfo.hotkey == null || toolbarInfo.hotkey!.isEmpty) { + return const SizedBox(); + } + + var hotkey = WoxHotkey.parseHotkeyFromString(toolbarInfo.hotkey!); return Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - if (toolbarInfo.icon != null) - Padding( - padding: const EdgeInsets.only(right: 8), - child: WoxImageView(woxImage: toolbarInfo.icon!, width: 24, height: 24), - ), Text( - toolbarInfo.text ?? '', + toolbarInfo.actionName ?? '', style: TextStyle(color: fromCssColor(controller.woxTheme.value.toolbarFontColor)), + overflow: TextOverflow.ellipsis, ), + const SizedBox(width: 8), + WoxHotkeyView( + hotkey: hotkey!, + backgroundColor: fromCssColor(controller.woxTheme.value.toolbarBackgroundColor), + borderColor: fromCssColor(controller.woxTheme.value.toolbarFontColor), + textColor: fromCssColor(controller.woxTheme.value.toolbarFontColor), + ) ], ); }); } - Widget rightTip() { - final toolbarInfo = controller.toolbar.value; - if (toolbarInfo.hotkey == null || toolbarInfo.hotkey!.isEmpty) { - return const SizedBox(); - } - - var hotkey = WoxHotkey.parseHotkeyFromString(toolbarInfo.hotkey!); - return Row( - children: [ - Text(toolbarInfo.actionName ?? '', style: TextStyle(color: fromCssColor(controller.woxTheme.value.toolbarFontColor))), - const SizedBox(width: 8), - WoxHotkeyView( - hotkey: hotkey!, - backgroundColor: fromCssColor(controller.woxTheme.value.toolbarBackgroundColor), - borderColor: fromCssColor(controller.woxTheme.value.toolbarFontColor), - textColor: fromCssColor(controller.woxTheme.value.toolbarFontColor), - ) - ], - ); - } @override Widget build(BuildContext context) { return Obx(() { diff --git a/Wox.UI.Flutter/wox/lib/modules/launcher/wox_launcher_controller.dart b/Wox.UI.Flutter/wox/lib/modules/launcher/wox_launcher_controller.dart index c84e28065..47d173843 100644 --- a/Wox.UI.Flutter/wox/lib/modules/launcher/wox_launcher_controller.dart +++ b/Wox.UI.Flutter/wox/lib/modules/launcher/wox_launcher_controller.dart @@ -87,6 +87,8 @@ class WoxLauncherController extends GetxController { /// The result of the doctor check. var doctorCheckPassed = true; + final toolbarCopyText = 'Copy'.obs; + /// Triggered when received query results from the server. void onReceivedQueryResults(String traceId, List receivedResults) { if (receivedResults.isEmpty) { @@ -499,6 +501,9 @@ class WoxLauncherController extends GetxController { } else if (msg.method == "OpenSettingWindow") { openSettingWindow(msg.traceId, SettingWindowContext.fromJson(msg.data)); responseWoxWebsocketRequest(msg, true, null); + } else if (msg.method == "ShowToolbarMsg") { + showToolbarMsg(msg.traceId, ToolbarMsg.fromJson(msg.data)); + responseWoxWebsocketRequest(msg, true, null); } } @@ -508,7 +513,7 @@ class WoxLauncherController extends GetxController { for (var item in msg.data) { results.add(WoxQueryResult.fromJson(item)); } - Logger.instance.info(msg.traceId, "Received message: ${msg.method}, results count: ${results.length}"); + Logger.instance.info(msg.traceId, "Received websocket message: ${msg.method}, results count: ${results.length}"); onReceivedQueryResults(msg.traceId, results); } @@ -843,6 +848,30 @@ class WoxLauncherController extends GetxController { }); } + void showToolbarMsg(String traceId, ToolbarMsg msg) { + toolbar.value = ToolbarInfo( + text: msg.text, + icon: msg.icon, + action: toolbar.value.action, + actionName: toolbar.value.actionName, + hotkey: toolbar.value.hotkey, + ); + if (msg.displaySeconds > 0) { + Future.delayed(Duration(seconds: msg.displaySeconds), () { + // only hide toolbar msg when the text is the same as the one we are showing + if (toolbar.value.text == msg.text) { + toolbar.value = ToolbarInfo( + text: "", + icon: WoxImage.empty(), + action: toolbar.value.action, + actionName: toolbar.value.actionName, + hotkey: toolbar.value.hotkey, + ); + } + }); + } + } + void moveQueryBoxCursorToStart() { queryBoxTextFieldController.selection = TextSelection.fromPosition(const TextPosition(offset: 0)); if (queryBoxScrollController.hasClients) { diff --git a/Wox.UI.Flutter/wox/lib/utils/wox_websocket_msg_util.dart b/Wox.UI.Flutter/wox/lib/utils/wox_websocket_msg_util.dart index 29647c678..1ac9d58f7 100644 --- a/Wox.UI.Flutter/wox/lib/utils/wox_websocket_msg_util.dart +++ b/Wox.UI.Flutter/wox/lib/utils/wox_websocket_msg_util.dart @@ -1,10 +1,15 @@ import 'dart:async'; import 'dart:convert'; +import 'package:get/get.dart'; import 'package:uuid/v4.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:wox/entity/wox_image.dart'; +import 'package:wox/entity/wox_toolbar.dart'; import 'package:wox/entity/wox_websocket_msg.dart'; +import 'package:wox/enums/wox_image_type_enum.dart'; import 'package:wox/enums/wox_msg_method_enum.dart'; +import 'package:wox/modules/launcher/wox_launcher_controller.dart'; import 'package:wox/utils/log.dart'; class WoxWebsocketMsgUtil { @@ -36,7 +41,16 @@ class WoxWebsocketMsgUtil { isConnecting = false; var msg = WoxWebsocketMsg.fromJson(jsonDecode(event)); if (msg.success == false) { - Logger.instance.error(msg.traceId, "Received error message: ${msg.toJson()}"); + Logger.instance.error(msg.traceId, "Received error websocket message: ${msg.toJson()}"); + Get.find().showToolbarMsg( + msg.traceId, + ToolbarMsg( + icon: WoxImage( + imageType: WoxImageTypeEnum.WOX_IMAGE_TYPE_SVG.code, + imageData: + ''), + text: msg.data, + )); return; } diff --git a/Wox/plugin/manager.go b/Wox/plugin/manager.go index 05c577746..2e6c1aa07 100644 --- a/Wox/plugin/manager.go +++ b/Wox/plugin/manager.go @@ -984,16 +984,14 @@ func (m *Manager) expandQueryShortcut(ctx context.Context, query string, querySh return newQuery } -func (m *Manager) ExecuteAction(ctx context.Context, resultId string, actionId string) { +func (m *Manager) ExecuteAction(ctx context.Context, resultId string, actionId string) error { resultCache, found := m.resultCache.Load(resultId) if !found { - logger.Error(ctx, fmt.Sprintf("result cache not found for result id (execute action): %s", resultId)) - return + return fmt.Errorf("result cache not found for result id (execute action): %s", resultId) } action, exist := resultCache.Actions.Load(actionId) if !exist { - logger.Error(ctx, fmt.Sprintf("action not found for result id: %s, action id: %s", resultId, actionId)) - return + return fmt.Errorf("action not found for result id: %s, action id: %s", resultId, actionId) } action(ctx, ActionContext{ @@ -1001,6 +999,7 @@ func (m *Manager) ExecuteAction(ctx context.Context, resultId string, actionId s }) setting.GetSettingManager().AddActionedResult(ctx, resultCache.PluginInstance.Metadata.Id, resultCache.ResultTitle, resultCache.ResultSubTitle) + return nil } func (m *Manager) ExecuteRefresh(ctx context.Context, refreshableResultWithId RefreshableResultWithResultId) (RefreshableResultWithResultId, error) { @@ -1012,8 +1011,7 @@ func (m *Manager) ExecuteRefresh(ctx context.Context, refreshableResultWithId Re resultCache, found := m.resultCache.Load(refreshableResultWithId.ResultId) if !found { - logger.Error(ctx, fmt.Sprintf("result cache not found for result id (execute refresh): %s", refreshableResultWithId.ResultId)) - return refreshableResultWithId, errors.New("result cache not found") + return refreshableResultWithId, fmt.Errorf("result cache not found for result id (execute refresh): %s", refreshableResultWithId.ResultId) } newResult := resultCache.Refresh(ctx, refreshableResult) @@ -1039,8 +1037,7 @@ func (m *Manager) ExecuteRefresh(ctx context.Context, refreshableResultWithId Re func (m *Manager) GetResultPreview(ctx context.Context, resultId string) (WoxPreview, error) { resultCache, found := m.resultCache.Load(resultId) if !found { - logger.Error(ctx, fmt.Sprintf("result cache not found for result id (get preview): %s", resultId)) - return WoxPreview{}, errors.New("result cache not found") + return WoxPreview{}, fmt.Errorf("result cache not found for result id (get preview): %s", resultId) } preview := m.polishPreview(ctx, resultCache.Preview) @@ -1118,10 +1115,7 @@ func (m *Manager) GetAIProvider(ctx context.Context, provider ai.ProviderName) ( //check if provider has setting aiProviderSettings := setting.GetSettingManager().GetWoxSetting(ctx).AIProviders providerSetting, providerSettingExist := lo.Find(aiProviderSettings, func(item setting.AIProvider) bool { - if item.Name == string(provider) { - return true - } - return false + return item.Name == string(provider) }) if !providerSettingExist { return nil, fmt.Errorf("ai provider setting not found: %s", provider) diff --git a/Wox/share/ui.go b/Wox/share/ui.go index 7e02a0526..a7e873e92 100644 --- a/Wox/share/ui.go +++ b/Wox/share/ui.go @@ -48,6 +48,7 @@ type UI interface { InstallTheme(ctx context.Context, theme Theme) UninstallTheme(ctx context.Context, theme Theme) RestoreTheme(ctx context.Context) + ShowToolbarMsg(ctx context.Context, msg ToolbarMsg) } type ShowContext struct { @@ -57,3 +58,9 @@ type ShowContext struct { type PickFilesParams struct { IsDirectory bool } + +type ToolbarMsg struct { + Icon string // WoxImage.String(), can be empty + Text string // can be empty + DisplaySeconds int // 0 means display forever +} diff --git a/Wox/ui/http.go b/Wox/ui/http.go index 6cc020605..3bed7dc9b 100644 --- a/Wox/ui/http.go +++ b/Wox/ui/http.go @@ -161,6 +161,7 @@ func responseUISuccess(ctx context.Context, request WebsocketMsg) { func responseUIError(ctx context.Context, request WebsocketMsg, errMsg string) { responseUI(ctx, WebsocketMsg{ RequestId: request.RequestId, + TraceId: util.GetContextTraceId(ctx), Type: WebsocketMsgTypeResponse, Method: request.Method, Success: false, diff --git a/Wox/ui/ui_impl.go b/Wox/ui/ui_impl.go index 61ef376db..274cff29c 100644 --- a/Wox/ui/ui_impl.go +++ b/Wox/ui/ui_impl.go @@ -84,6 +84,10 @@ func (u *uiImpl) RestoreTheme(ctx context.Context) { GetUIManager().RestoreTheme(ctx) } +func (u *uiImpl) ShowToolbarMsg(ctx context.Context, msg share.ToolbarMsg) { + u.invokeWebsocketMethod(ctx, "ShowToolbarMsg", msg) +} + func (u *uiImpl) PickFiles(ctx context.Context, params share.PickFilesParams) []string { respData, err := u.invokeWebsocketMethod(ctx, "PickFiles", params) if err != nil { @@ -356,7 +360,12 @@ func handleWebsocketAction(ctx context.Context, request WebsocketMsg) { return } - plugin.GetPluginManager().ExecuteAction(ctx, resultId, actionId) + executeErr := plugin.GetPluginManager().ExecuteAction(ctx, resultId, actionId) + if executeErr != nil { + logger.Error(ctx, executeErr.Error()) + responseUIError(ctx, request, executeErr.Error()) + return + } responseUISuccess(ctx, request) } @@ -417,7 +426,7 @@ func getWebsocketMsgParameter(ctx context.Context, msg WebsocketMsg, key string) paramterData := gjson.GetBytes(jsonData, key) if !paramterData.Exists() { - return "", errors.New(fmt.Sprintf("%s parameter not found", key)) + return "", fmt.Errorf("%s parameter not found", key) } return paramterData.String(), nil