连接器客户端

This commit is contained in:
tianmo
2026-09-05 23:35:45 +08:00
parent d10807d877
commit 41151b7f8b
28 changed files with 2195 additions and 12 deletions
+183
View File
@@ -0,0 +1,183 @@
#ifdef __APPLE__
#import <AppKit/AppKit.h>
#include "config.hpp"
#include "connector_service.hpp"
#include "platform.hpp"
using connector::ConnectorConfig;
using connector::ConnectorService;
@interface ConnectorAppDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate>
@property(nonatomic, strong) NSWindow* window;
@property(nonatomic, strong) NSTextField* pathField;
@property(nonatomic, strong) NSTextField* statusLabel;
@property(nonatomic, strong) NSArray<NSView*>* pathControls;
@property(nonatomic, strong) NSButton* connectionButton;
@property(nonatomic, assign) ConnectorService* service;
@property(nonatomic, copy) NSString* startupError;
@end
@implementation ConnectorAppDelegate
- (NSTextField*)label:(NSString*)text frame:(NSRect)frame size:(CGFloat)size color:(NSColor*)color {
NSTextField* label = [[NSTextField alloc] initWithFrame:frame];
label.stringValue = text;
label.editable = NO;
label.bezeled = NO;
label.drawsBackground = NO;
label.font = [NSFont systemFontOfSize:size];
label.textColor = color;
return label;
}
- (NSButton*)button:(NSString*)title frame:(NSRect)frame action:(SEL)action {
NSButton* button = [[NSButton alloc] initWithFrame:frame];
button.title = title;
button.bezelStyle = NSBezelStyleRounded;
button.target = self;
button.action = action;
return button;
}
- (void)applicationDidFinishLaunching:(NSNotification*)notification {
(void)notification;
self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 620, 326)
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable
backing:NSBackingStoreBuffered defer:NO];
self.window.title = @"Spine粒子连接器";
self.window.delegate = self;
[self.window center];
NSView* content = self.window.contentView;
[content addSubview:[self label:@"Spine粒子连接器" frame:NSMakeRect(28, 268, 560, 34) size:24 color:NSColor.labelColor]];
[content addSubview:[self label:@"应用打开且连接已启动时,网页可调用本机 Spine 生成工程文件。" frame:NSMakeRect(29, 237, 560, 24) size:14 color:NSColor.secondaryLabelColor]];
self.statusLabel = [self label:@"" frame:NSMakeRect(29, 201, 560, 24) size:14 color:NSColor.systemGreenColor];
[content addSubview:self.statusLabel];
NSTextField* pathLabel = [self label:@"Spine.com 路径" frame:NSMakeRect(29, 162, 130, 24) size:14 color:NSColor.labelColor];
[content addSubview:pathLabel];
self.pathField = [[NSTextField alloc] initWithFrame:NSMakeRect(29, 126, 390, 32)];
self.pathField.placeholderString = @"留空时自动检测 Spine";
const ConnectorConfig config = connector::load_config();
self.pathField.stringValue = [NSString stringWithUTF8String:config.spine_executable.c_str()];
[content addSubview:self.pathField];
NSButton* browse = [self button:@"选择 Spine.com" frame:NSMakeRect(430, 124, 160, 36) action:@selector(selectSpine:)];
[content addSubview:browse];
NSButton* automatic = [self button:@"恢复自动检测" frame:NSMakeRect(29, 78, 130, 36) action:@selector(clearPath:)];
[content addSubview:automatic];
NSButton* save = [self button:@"保存路径" frame:NSMakeRect(460, 78, 130, 36) action:@selector(savePath:)];
save.keyEquivalent = @"\r";
[content addSubview:save];
self.pathControls = @[pathLabel, self.pathField, browse, automatic, save];
self.connectionButton = [self button:@"断开连接" frame:NSMakeRect(460, 25, 130, 36) action:@selector(toggleConnection:)];
[content addSubview:self.connectionButton];
[self refreshStatus:nil];
[self.window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
}
- (void)refreshStatus:(id)sender {
(void)sender;
self.connectionButton.title = self.service && self.service->running() ? @"断开连接" : @"启动连接";
if (self.startupError.length > 0) {
self.statusLabel.stringValue = [@"● 连接器未启动 · " stringByAppendingString:self.startupError];
self.statusLabel.textColor = NSColor.systemRedColor;
return;
}
if (!self.service || !self.service->running()) {
self.statusLabel.stringValue = @"● 连接器已断开";
self.statusLabel.textColor = NSColor.secondaryLabelColor;
return;
}
const std::string spine = connector::find_spine_executable();
const BOOL showPathControls = spine.empty();
for (NSView* view in self.pathControls) view.hidden = !showPathControls;
self.statusLabel.stringValue = spine.empty() ? @"● 连接器运行中 · 未找到 Spine" : @"● 连接器运行中 · Spine 已就绪";
self.statusLabel.textColor = spine.empty() ? NSColor.systemOrangeColor : NSColor.systemGreenColor;
}
- (void)toggleConnection:(id)sender {
(void)sender;
if (self.service && self.service->running()) {
self.service->stop();
self.startupError = @"";
} else if (self.service) {
std::string error;
self.service->start(error);
self.startupError = [NSString stringWithUTF8String:error.c_str()];
}
[self refreshStatus:nil];
}
- (void)selectSpine:(id)sender {
(void)sender;
NSOpenPanel* panel = [NSOpenPanel openPanel];
panel.canChooseFiles = YES;
panel.canChooseDirectories = NO;
panel.allowsMultipleSelection = NO;
panel.message = @"请选择 Spine 命令行可执行文件(macOS 通常为 Spine.app/Contents/MacOS/Spine";
if ([panel runModal] == NSModalResponseOK) self.pathField.stringValue = panel.URL.path;
}
- (void)clearPath:(id)sender {
(void)sender;
self.pathField.stringValue = @"";
[self savePath:nil];
}
- (void)savePath:(id)sender {
(void)sender;
const std::string path = self.pathField.stringValue.UTF8String ?: "";
if (!path.empty() && !connector::valid_spine_executable(path)) {
NSAlert* alert = [[NSAlert alloc] init];
alert.messageText = @"Spine 路径无效";
alert.informativeText = @"请选择实际存在的 Spine 命令行可执行文件。";
[alert runModal];
return;
}
try {
ConnectorConfig config = connector::load_config();
config.spine_executable = path;
connector::save_config(config);
[self refreshStatus:nil];
} catch (const std::exception& error) {
NSAlert* alert = [[NSAlert alloc] init];
alert.messageText = @"保存失败";
alert.informativeText = [NSString stringWithUTF8String:error.what()];
[alert runModal];
}
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender {
(void)sender;
return YES;
}
- (void)applicationWillTerminate:(NSNotification*)notification {
(void)notification;
if (self.service) self.service->stop();
}
@end
int main(int argc, char* argv[]) {
(void)argc;
(void)argv;
@autoreleasepool {
ConnectorService service;
std::string startup_error;
service.start(startup_error);
NSApplication* application = [NSApplication sharedApplication];
application.activationPolicy = NSApplicationActivationPolicyRegular;
ConnectorAppDelegate* delegate = [[ConnectorAppDelegate alloc] init];
delegate.service = &service;
delegate.startupError = [NSString stringWithUTF8String:startup_error.c_str()];
application.delegate = delegate;
[application run];
service.stop();
}
return 0;
}
#endif