mirror of https://github.com/Wox-launcher/Wox
feat(setting): add custom Python and Node.js path configuration, fix #4220
* Introduced settings for custom executable paths for Python and Node.js. * Implemented validation for the provided paths to ensure they are executable. * Updated UI to allow users to browse and set custom paths. * Added corresponding translations for new UI elements in English and Chinese.
This commit is contained in:
parent
ea7fe65a0e
commit
4750fc92ce
|
@ -6,6 +6,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"wox/plugin"
|
"wox/plugin"
|
||||||
|
"wox/setting"
|
||||||
"wox/util"
|
"wox/util"
|
||||||
"wox/util/shell"
|
"wox/util/shell"
|
||||||
|
|
||||||
|
@ -37,6 +38,17 @@ func (n *NodejsHost) Start(ctx context.Context) error {
|
||||||
func (n *NodejsHost) findNodejsPath(ctx context.Context) string {
|
func (n *NodejsHost) findNodejsPath(ctx context.Context) string {
|
||||||
util.GetLogger().Debug(ctx, "start finding nodejs path")
|
util.GetLogger().Debug(ctx, "start finding nodejs path")
|
||||||
|
|
||||||
|
// Check if user has configured a custom Node.js path
|
||||||
|
customPath := setting.GetSettingManager().GetWoxSetting(ctx).CustomNodejsPath.Get()
|
||||||
|
if customPath != "" {
|
||||||
|
if util.IsFileExists(customPath) {
|
||||||
|
util.GetLogger().Info(ctx, fmt.Sprintf("using custom nodejs path: %s", customPath))
|
||||||
|
return customPath
|
||||||
|
} else {
|
||||||
|
util.GetLogger().Warn(ctx, fmt.Sprintf("custom nodejs path not found, falling back to auto-detection: %s", customPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var possibleNodejsPaths = []string{
|
var possibleNodejsPaths = []string{
|
||||||
"/opt/homebrew/bin/node",
|
"/opt/homebrew/bin/node",
|
||||||
"/usr/local/bin/node",
|
"/usr/local/bin/node",
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"wox/plugin"
|
"wox/plugin"
|
||||||
|
"wox/setting"
|
||||||
"wox/util"
|
"wox/util"
|
||||||
"wox/util/shell"
|
"wox/util/shell"
|
||||||
|
|
||||||
|
@ -37,6 +38,17 @@ func (n *PythonHost) Start(ctx context.Context) error {
|
||||||
func (n *PythonHost) findPythonPath(ctx context.Context) string {
|
func (n *PythonHost) findPythonPath(ctx context.Context) string {
|
||||||
util.GetLogger().Debug(ctx, "start finding python path")
|
util.GetLogger().Debug(ctx, "start finding python path")
|
||||||
|
|
||||||
|
// Check if user has configured a custom Python path
|
||||||
|
customPath := setting.GetSettingManager().GetWoxSetting(ctx).CustomPythonPath.Get()
|
||||||
|
if customPath != "" {
|
||||||
|
if util.IsFileExists(customPath) {
|
||||||
|
util.GetLogger().Info(ctx, fmt.Sprintf("using custom python path: %s", customPath))
|
||||||
|
return customPath
|
||||||
|
} else {
|
||||||
|
util.GetLogger().Warn(ctx, fmt.Sprintf("custom python path not found, falling back to auto-detection: %s", customPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var possiblePythonPaths = []string{
|
var possiblePythonPaths = []string{
|
||||||
"/opt/homebrew/bin/python3",
|
"/opt/homebrew/bin/python3",
|
||||||
"/usr/local/bin/python3",
|
"/usr/local/bin/python3",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"wox/plugin"
|
"wox/plugin"
|
||||||
|
"wox/setting"
|
||||||
"wox/util"
|
"wox/util"
|
||||||
"wox/util/shell"
|
"wox/util/shell"
|
||||||
)
|
)
|
||||||
|
@ -201,7 +202,7 @@ func (sp *ScriptPlugin) executeScriptRaw(ctx context.Context, request map[string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the interpreter based on file extension
|
// Determine the interpreter based on file extension
|
||||||
interpreter, err := sp.getInterpreter()
|
interpreter, err := sp.getInterpreter(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -307,13 +308,23 @@ func (sp *ScriptPlugin) handleActionResult(ctx context.Context, result map[strin
|
||||||
}
|
}
|
||||||
|
|
||||||
// getInterpreter determines the appropriate interpreter for the script based on file extension
|
// getInterpreter determines the appropriate interpreter for the script based on file extension
|
||||||
func (sp *ScriptPlugin) getInterpreter() (string, error) {
|
func (sp *ScriptPlugin) getInterpreter(ctx context.Context) (string, error) {
|
||||||
ext := strings.ToLower(filepath.Ext(sp.scriptPath))
|
ext := strings.ToLower(filepath.Ext(sp.scriptPath))
|
||||||
|
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".py":
|
case ".py":
|
||||||
|
// Check if user has configured a custom Python path
|
||||||
|
customPath := setting.GetSettingManager().GetWoxSetting(ctx).CustomPythonPath.Get()
|
||||||
|
if customPath != "" && util.IsFileExists(customPath) {
|
||||||
|
return customPath, nil
|
||||||
|
}
|
||||||
return "python3", nil
|
return "python3", nil
|
||||||
case ".js":
|
case ".js":
|
||||||
|
// Check if user has configured a custom Node.js path
|
||||||
|
customPath := setting.GetSettingManager().GetWoxSetting(ctx).CustomNodejsPath.Get()
|
||||||
|
if customPath != "" && util.IsFileExists(customPath) {
|
||||||
|
return customPath, nil
|
||||||
|
}
|
||||||
return "node", nil
|
return "node", nil
|
||||||
case ".sh":
|
case ".sh":
|
||||||
return "bash", nil
|
return "bash", nil
|
||||||
|
|
|
@ -58,6 +58,18 @@
|
||||||
"ui_plugins": "Plugins",
|
"ui_plugins": "Plugins",
|
||||||
"ui_store_plugins": "Store Plugins",
|
"ui_store_plugins": "Store Plugins",
|
||||||
"ui_installed_plugins": "Installed Plugins",
|
"ui_installed_plugins": "Installed Plugins",
|
||||||
|
"ui_runtime_settings": "Runtime Settings",
|
||||||
|
"ui_runtime_python_path": "Python Path",
|
||||||
|
"ui_runtime_python_path_tips": "Custom Python executable path. Leave empty to use auto-detection.",
|
||||||
|
"ui_runtime_python_path_placeholder": "e.g., /usr/local/bin/python3",
|
||||||
|
"ui_runtime_nodejs_path": "Node.js Path",
|
||||||
|
"ui_runtime_nodejs_path_tips": "Custom Node.js executable path. Leave empty to use auto-detection.",
|
||||||
|
"ui_runtime_nodejs_path_placeholder": "e.g., /usr/local/bin/node",
|
||||||
|
"ui_runtime_browse": "Browse",
|
||||||
|
"ui_runtime_clear": "Clear",
|
||||||
|
"ui_runtime_validating": "Validating...",
|
||||||
|
"ui_runtime_validation_failed": "Invalid executable",
|
||||||
|
"ui_runtime_validation_error": "Validation error",
|
||||||
"ui_themes": "Themes",
|
"ui_themes": "Themes",
|
||||||
"ui_store_themes": "Store Themes",
|
"ui_store_themes": "Store Themes",
|
||||||
"ui_installed_themes": "Installed Themes",
|
"ui_installed_themes": "Installed Themes",
|
||||||
|
|
|
@ -58,6 +58,18 @@
|
||||||
"ui_plugins": "插件",
|
"ui_plugins": "插件",
|
||||||
"ui_store_plugins": "插件商店",
|
"ui_store_plugins": "插件商店",
|
||||||
"ui_installed_plugins": "已安装插件",
|
"ui_installed_plugins": "已安装插件",
|
||||||
|
"ui_runtime_settings": "运行时设置",
|
||||||
|
"ui_runtime_python_path": "Python 路径",
|
||||||
|
"ui_runtime_python_path_tips": "自定义 Python 可执行文件路径。留空则使用自动检测。",
|
||||||
|
"ui_runtime_python_path_placeholder": "例如:/usr/local/bin/python3",
|
||||||
|
"ui_runtime_nodejs_path": "Node.js 路径",
|
||||||
|
"ui_runtime_nodejs_path_tips": "自定义 Node.js 可执行文件路径。留空则使用自动检测。",
|
||||||
|
"ui_runtime_nodejs_path_placeholder": "例如:/usr/local/bin/node",
|
||||||
|
"ui_runtime_browse": "浏览",
|
||||||
|
"ui_runtime_clear": "清除",
|
||||||
|
"ui_runtime_validating": "正在验证...",
|
||||||
|
"ui_runtime_validation_failed": "无效的可执行文件",
|
||||||
|
"ui_runtime_validation_error": "验证错误",
|
||||||
"ui_themes": "主题",
|
"ui_themes": "主题",
|
||||||
"ui_store_themes": "主题商店",
|
"ui_store_themes": "主题商店",
|
||||||
"ui_installed_themes": "已安装主题",
|
"ui_installed_themes": "已安装主题",
|
||||||
|
|
|
@ -252,6 +252,10 @@ func (m *Manager) UpdateWoxSetting(ctx context.Context, key, value string) error
|
||||||
return parseErr
|
return parseErr
|
||||||
}
|
}
|
||||||
m.woxSetting.MaxResultCount = maxResultCount
|
m.woxSetting.MaxResultCount = maxResultCount
|
||||||
|
} else if key == "CustomPythonPath" {
|
||||||
|
m.woxSetting.CustomPythonPath.Set(value)
|
||||||
|
} else if key == "CustomNodejsPath" {
|
||||||
|
m.woxSetting.CustomNodejsPath.Set(value)
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("unknown key: %s", key)
|
return fmt.Errorf("unknown key: %s", key)
|
||||||
}
|
}
|
||||||
|
@ -486,6 +490,10 @@ func (m *Manager) tryPartialLoad(settingPath string, defaultSetting WoxSetting)
|
||||||
m.extractStringFieldSafely(rawData, "LastQueryMode", &woxSetting.LastQueryMode)
|
m.extractStringFieldSafely(rawData, "LastQueryMode", &woxSetting.LastQueryMode)
|
||||||
m.extractStringFieldSafely(rawData, "ThemeId", &woxSetting.ThemeId)
|
m.extractStringFieldSafely(rawData, "ThemeId", &woxSetting.ThemeId)
|
||||||
|
|
||||||
|
// Extract platform-specific string fields
|
||||||
|
m.extractPlatformStringFieldSafely(rawData, "CustomPythonPath", &woxSetting.CustomPythonPath)
|
||||||
|
m.extractPlatformStringFieldSafely(rawData, "CustomNodejsPath", &woxSetting.CustomNodejsPath)
|
||||||
|
|
||||||
// Sanitize the loaded values
|
// Sanitize the loaded values
|
||||||
m.sanitizeWoxSetting(&woxSetting, defaultSetting)
|
m.sanitizeWoxSetting(&woxSetting, defaultSetting)
|
||||||
|
|
||||||
|
@ -524,6 +532,29 @@ func (m *Manager) extractStringFieldSafely(rawData map[string]interface{}, field
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractPlatformStringFieldSafely safely extracts a platform-specific string field from raw JSON data
|
||||||
|
func (m *Manager) extractPlatformStringFieldSafely(rawData map[string]interface{}, fieldName string, target *PlatformSettingValue[string]) {
|
||||||
|
if value, exists := rawData[fieldName]; exists {
|
||||||
|
if platformValue, ok := value.(map[string]interface{}); ok {
|
||||||
|
if winVal, exists := platformValue["WinValue"]; exists {
|
||||||
|
if strVal, ok := winVal.(string); ok {
|
||||||
|
target.WinValue = strVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if macVal, exists := platformValue["MacValue"]; exists {
|
||||||
|
if strVal, ok := macVal.(string); ok {
|
||||||
|
target.MacValue = strVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if linuxVal, exists := platformValue["LinuxValue"]; exists {
|
||||||
|
if strVal, ok := linuxVal.(string); ok {
|
||||||
|
target.LinuxValue = strVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sanitizeWoxSetting ensures all values are within acceptable ranges
|
// sanitizeWoxSetting ensures all values are within acceptable ranges
|
||||||
func (m *Manager) sanitizeWoxSetting(setting *WoxSetting, defaultSetting WoxSetting) {
|
func (m *Manager) sanitizeWoxSetting(setting *WoxSetting, defaultSetting WoxSetting) {
|
||||||
// Sanitize AppWidth
|
// Sanitize AppWidth
|
||||||
|
|
|
@ -24,8 +24,10 @@ type WoxSetting struct {
|
||||||
LastQueryMode LastQueryMode
|
LastQueryMode LastQueryMode
|
||||||
ShowPosition PositionType
|
ShowPosition PositionType
|
||||||
AIProviders []AIProvider
|
AIProviders []AIProvider
|
||||||
EnableAutoBackup bool // Enable automatic data backup
|
EnableAutoBackup bool // Enable automatic data backup
|
||||||
EnableAutoUpdate bool // Enable automatic update check and download
|
EnableAutoUpdate bool // Enable automatic update check and download
|
||||||
|
CustomPythonPath PlatformSettingValue[string] // Custom Python executable path
|
||||||
|
CustomNodejsPath PlatformSettingValue[string] // Custom Node.js executable path
|
||||||
|
|
||||||
// HTTP proxy settings
|
// HTTP proxy settings
|
||||||
HttpProxyEnabled PlatformSettingValue[bool]
|
HttpProxyEnabled PlatformSettingValue[bool]
|
||||||
|
@ -127,6 +129,16 @@ func GetDefaultWoxSetting(ctx context.Context) WoxSetting {
|
||||||
MacValue: "",
|
MacValue: "",
|
||||||
LinuxValue: "",
|
LinuxValue: "",
|
||||||
},
|
},
|
||||||
|
CustomPythonPath: PlatformSettingValue[string]{
|
||||||
|
WinValue: "",
|
||||||
|
MacValue: "",
|
||||||
|
LinuxValue: "",
|
||||||
|
},
|
||||||
|
CustomNodejsPath: PlatformSettingValue[string]{
|
||||||
|
WinValue: "",
|
||||||
|
MacValue: "",
|
||||||
|
LinuxValue: "",
|
||||||
|
},
|
||||||
EnableAutoBackup: true,
|
EnableAutoBackup: true,
|
||||||
EnableAutoUpdate: true,
|
EnableAutoUpdate: true,
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ type WoxSettingDto struct {
|
||||||
ShowPosition setting.PositionType
|
ShowPosition setting.PositionType
|
||||||
EnableAutoBackup bool
|
EnableAutoBackup bool
|
||||||
EnableAutoUpdate bool
|
EnableAutoUpdate bool
|
||||||
|
CustomPythonPath string
|
||||||
|
CustomNodejsPath string
|
||||||
|
|
||||||
// UI related
|
// UI related
|
||||||
AppWidth int
|
AppWidth int
|
||||||
|
|
|
@ -500,6 +500,8 @@ func handleSettingWox(w http.ResponseWriter, r *http.Request) {
|
||||||
settingDto.QueryHotkeys = woxSetting.QueryHotkeys.Get()
|
settingDto.QueryHotkeys = woxSetting.QueryHotkeys.Get()
|
||||||
settingDto.HttpProxyEnabled = woxSetting.HttpProxyEnabled.Get()
|
settingDto.HttpProxyEnabled = woxSetting.HttpProxyEnabled.Get()
|
||||||
settingDto.HttpProxyUrl = woxSetting.HttpProxyUrl.Get()
|
settingDto.HttpProxyUrl = woxSetting.HttpProxyUrl.Get()
|
||||||
|
settingDto.CustomPythonPath = woxSetting.CustomPythonPath.Get()
|
||||||
|
settingDto.CustomNodejsPath = woxSetting.CustomNodejsPath.Get()
|
||||||
|
|
||||||
writeSuccessResponse(w, settingDto)
|
writeSuccessResponse(w, settingDto)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ class WoxSetting {
|
||||||
late String httpProxyUrl;
|
late String httpProxyUrl;
|
||||||
late bool enableAutoBackup;
|
late bool enableAutoBackup;
|
||||||
late bool enableAutoUpdate;
|
late bool enableAutoUpdate;
|
||||||
|
late String customPythonPath;
|
||||||
|
late String customNodejsPath;
|
||||||
|
|
||||||
WoxSetting({
|
WoxSetting({
|
||||||
required this.enableAutostart,
|
required this.enableAutostart,
|
||||||
|
@ -45,6 +47,8 @@ class WoxSetting {
|
||||||
required this.httpProxyUrl,
|
required this.httpProxyUrl,
|
||||||
required this.enableAutoBackup,
|
required this.enableAutoBackup,
|
||||||
required this.enableAutoUpdate,
|
required this.enableAutoUpdate,
|
||||||
|
required this.customPythonPath,
|
||||||
|
required this.customNodejsPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
WoxSetting.fromJson(Map<String, dynamic> json) {
|
WoxSetting.fromJson(Map<String, dynamic> json) {
|
||||||
|
@ -95,6 +99,8 @@ class WoxSetting {
|
||||||
httpProxyUrl = json['HttpProxyUrl'] ?? '';
|
httpProxyUrl = json['HttpProxyUrl'] ?? '';
|
||||||
enableAutoBackup = json['EnableAutoBackup'] ?? false;
|
enableAutoBackup = json['EnableAutoBackup'] ?? false;
|
||||||
enableAutoUpdate = json['EnableAutoUpdate'] ?? true;
|
enableAutoUpdate = json['EnableAutoUpdate'] ?? true;
|
||||||
|
customPythonPath = json['CustomPythonPath'] ?? '';
|
||||||
|
customNodejsPath = json['CustomNodejsPath'] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
|
@ -120,6 +126,8 @@ class WoxSetting {
|
||||||
data['HttpProxyUrl'] = httpProxyUrl;
|
data['HttpProxyUrl'] = httpProxyUrl;
|
||||||
data['EnableAutoBackup'] = enableAutoBackup;
|
data['EnableAutoBackup'] = enableAutoBackup;
|
||||||
data['EnableAutoUpdate'] = enableAutoUpdate;
|
data['EnableAutoUpdate'] = enableAutoUpdate;
|
||||||
|
data['CustomPythonPath'] = customPythonPath;
|
||||||
|
data['CustomNodejsPath'] = customNodejsPath;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:uuid/v4.dart';
|
||||||
|
import 'package:wox/modules/setting/views/wox_setting_base.dart';
|
||||||
|
import 'package:wox/utils/picker.dart';
|
||||||
|
|
||||||
|
class WoxSettingRuntimeView extends WoxSettingBaseView {
|
||||||
|
WoxSettingRuntimeView({super.key});
|
||||||
|
|
||||||
|
// Validation states
|
||||||
|
final RxString pythonValidationMessage = ''.obs;
|
||||||
|
final RxString nodejsValidationMessage = ''.obs;
|
||||||
|
final RxBool isPythonValidating = false.obs;
|
||||||
|
final RxBool isNodejsValidating = false.obs;
|
||||||
|
|
||||||
|
// Text controllers for immediate updates
|
||||||
|
late final TextEditingController pythonController;
|
||||||
|
late final TextEditingController nodejsController;
|
||||||
|
|
||||||
|
// Debounce timers for validation
|
||||||
|
Timer? _pythonValidationTimer;
|
||||||
|
Timer? _nodejsValidationTimer;
|
||||||
|
|
||||||
|
// Validation methods
|
||||||
|
Future<void> validatePythonPath(String path) async {
|
||||||
|
if (path.isEmpty) {
|
||||||
|
pythonValidationMessage.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPythonValidating.value = true;
|
||||||
|
try {
|
||||||
|
final result = await Process.run(path, ['--version']);
|
||||||
|
if (result.exitCode == 0) {
|
||||||
|
final version = result.stdout.toString().trim();
|
||||||
|
pythonValidationMessage.value = '✓ $version';
|
||||||
|
} else {
|
||||||
|
pythonValidationMessage.value = '✗ ${controller.tr("ui_runtime_validation_failed")}';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
pythonValidationMessage.value = '✗ ${controller.tr("ui_runtime_validation_error")}: ${e.toString()}';
|
||||||
|
} finally {
|
||||||
|
isPythonValidating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> validateNodejsPath(String path) async {
|
||||||
|
if (path.isEmpty) {
|
||||||
|
nodejsValidationMessage.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNodejsValidating.value = true;
|
||||||
|
try {
|
||||||
|
final result = await Process.run(path, ['-v']);
|
||||||
|
if (result.exitCode == 0) {
|
||||||
|
final version = result.stdout.toString().trim();
|
||||||
|
nodejsValidationMessage.value = '✓ $version';
|
||||||
|
} else {
|
||||||
|
nodejsValidationMessage.value = '✗ ${controller.tr("ui_runtime_validation_failed")}';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
nodejsValidationMessage.value = '✗ ${controller.tr("ui_runtime_validation_error")}: ${e.toString()}';
|
||||||
|
} finally {
|
||||||
|
isNodejsValidating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePythonPath(String value) {
|
||||||
|
controller.updateConfig("CustomPythonPath", value);
|
||||||
|
|
||||||
|
// Cancel previous timer
|
||||||
|
_pythonValidationTimer?.cancel();
|
||||||
|
|
||||||
|
// Start new timer for debounced validation
|
||||||
|
_pythonValidationTimer = Timer(const Duration(milliseconds: 500), () {
|
||||||
|
validatePythonPath(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateNodejsPath(String value) {
|
||||||
|
controller.updateConfig("CustomNodejsPath", value);
|
||||||
|
|
||||||
|
// Cancel previous timer
|
||||||
|
_nodejsValidationTimer?.cancel();
|
||||||
|
|
||||||
|
// Start new timer for debounced validation
|
||||||
|
_nodejsValidationTimer = Timer(const Duration(milliseconds: 500), () {
|
||||||
|
validateNodejsPath(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_pythonValidationTimer?.cancel();
|
||||||
|
_nodejsValidationTimer?.cancel();
|
||||||
|
pythonController.dispose();
|
||||||
|
nodejsController.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Initialize controllers with current values
|
||||||
|
pythonController = TextEditingController(text: controller.woxSetting.value.customPythonPath);
|
||||||
|
nodejsController = TextEditingController(text: controller.woxSetting.value.customNodejsPath);
|
||||||
|
|
||||||
|
// Initial validation
|
||||||
|
if (pythonController.text.isNotEmpty) {
|
||||||
|
validatePythonPath(pythonController.text);
|
||||||
|
}
|
||||||
|
if (nodejsController.text.isNotEmpty) {
|
||||||
|
validateNodejsPath(nodejsController.text);
|
||||||
|
}
|
||||||
|
return Obx(() {
|
||||||
|
return form(children: [
|
||||||
|
formField(
|
||||||
|
label: controller.tr("ui_runtime_python_path"),
|
||||||
|
tips: controller.tr("ui_runtime_python_path_tips"),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextBox(
|
||||||
|
controller: pythonController,
|
||||||
|
placeholder: controller.tr("ui_runtime_python_path_placeholder"),
|
||||||
|
onChanged: (value) {
|
||||||
|
updatePythonPath(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Button(
|
||||||
|
child: Text(controller.tr("ui_runtime_browse")),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await FileSelector.pick(
|
||||||
|
const UuidV4().generate(),
|
||||||
|
FileSelectorParams(isDirectory: false),
|
||||||
|
);
|
||||||
|
if (result.isNotEmpty) {
|
||||||
|
pythonController.text = result.first;
|
||||||
|
updatePythonPath(result.first);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Button(
|
||||||
|
child: Text(controller.tr("ui_runtime_clear")),
|
||||||
|
onPressed: () {
|
||||||
|
pythonController.clear();
|
||||||
|
updatePythonPath("");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Obx(() {
|
||||||
|
if (isPythonValidating.value) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 16, height: 16, child: ProgressRing()),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(controller.tr("ui_runtime_validating")),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (pythonValidationMessage.value.isNotEmpty) {
|
||||||
|
return Text(
|
||||||
|
pythonValidationMessage.value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: pythonValidationMessage.value.startsWith('✓') ? Colors.green : Colors.red,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
formField(
|
||||||
|
label: controller.tr("ui_runtime_nodejs_path"),
|
||||||
|
tips: controller.tr("ui_runtime_nodejs_path_tips"),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextBox(
|
||||||
|
controller: nodejsController,
|
||||||
|
placeholder: controller.tr("ui_runtime_nodejs_path_placeholder"),
|
||||||
|
onChanged: (value) {
|
||||||
|
updateNodejsPath(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Button(
|
||||||
|
child: Text(controller.tr("ui_runtime_browse")),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await FileSelector.pick(
|
||||||
|
const UuidV4().generate(),
|
||||||
|
FileSelectorParams(isDirectory: false),
|
||||||
|
);
|
||||||
|
if (result.isNotEmpty) {
|
||||||
|
nodejsController.text = result.first;
|
||||||
|
updateNodejsPath(result.first);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Button(
|
||||||
|
child: Text(controller.tr("ui_runtime_clear")),
|
||||||
|
onPressed: () {
|
||||||
|
nodejsController.clear();
|
||||||
|
updateNodejsPath("");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Obx(() {
|
||||||
|
if (isNodejsValidating.value) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 16, height: 16, child: ProgressRing()),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(controller.tr("ui_runtime_validating")),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (nodejsValidationMessage.value.isNotEmpty) {
|
||||||
|
return Text(
|
||||||
|
nodejsValidationMessage.value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: nodejsValidationMessage.value.startsWith('✓') ? Colors.green : Colors.red,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import 'package:wox/utils/wox_theme_util.dart';
|
||||||
import 'wox_setting_plugin_view.dart';
|
import 'wox_setting_plugin_view.dart';
|
||||||
import 'wox_setting_general_view.dart';
|
import 'wox_setting_general_view.dart';
|
||||||
import 'wox_setting_network_view.dart';
|
import 'wox_setting_network_view.dart';
|
||||||
|
import 'wox_setting_runtime_view.dart';
|
||||||
|
|
||||||
class WoxSettingView extends GetView<WoxSettingController> {
|
class WoxSettingView extends GetView<WoxSettingController> {
|
||||||
const WoxSettingView({super.key});
|
const WoxSettingView({super.key});
|
||||||
|
@ -118,6 +119,11 @@ class WoxSettingView extends GetView<WoxSettingController> {
|
||||||
await controller.switchToPluginList(false);
|
await controller.switchToPluginList(false);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(FluentIcons.code),
|
||||||
|
title: Text(controller.tr('ui_runtime_settings')),
|
||||||
|
body: WoxSettingRuntimeView(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
PaneItemExpander(
|
PaneItemExpander(
|
||||||
|
|
|
@ -19,6 +19,15 @@ class FileSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.any,
|
||||||
|
allowMultiple: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null && result.files.isNotEmpty) {
|
||||||
|
return result.files.map((e) => e.path ?? "").toList();
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue