f21fa5b5e8
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。 - 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。 - 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。 - 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。 - 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。 架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
134 lines
5.7 KiB
Objective-C
134 lines
5.7 KiB
Objective-C
//go:build darwin
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
#include "_cgo_export.h"
|
|
|
|
// CdropStatusBarController owns the menu bar item and routes the three menu
|
|
// actions back into Go. It is deliberately NOT the NSApplication delegate —
|
|
// Wails owns that, and stealing it would break Wails' window / runtime / custom
|
|
// scheme handling. A status item can be created independently after launch, so
|
|
// we never touch the delegate.
|
|
@interface CdropStatusBarController : NSObject
|
|
@property (strong) NSStatusItem *item;
|
|
@property (strong) NSMenu *menu;
|
|
- (void)showWindow:(id)sender;
|
|
- (void)openSettings:(id)sender;
|
|
- (void)quit:(id)sender;
|
|
- (void)statusItemClicked:(id)sender;
|
|
@end
|
|
|
|
@implementation CdropStatusBarController
|
|
- (void)showWindow:(id)sender { cdropOnShowWindow(); }
|
|
- (void)openSettings:(id)sender { cdropOnOpenSettings(); }
|
|
- (void)quit:(id)sender { cdropOnQuit(); }
|
|
|
|
// showMenu pops the menu now. Temporarily attaching item.menu and performClick
|
|
// shows it and BLOCKS until dismissed (menu tracking is modal); detaching after
|
|
// keeps left-clicks flowing through statusItemClicked: (so left-click opens the
|
|
// window directly instead of popping the menu).
|
|
- (void)showMenu {
|
|
self.item.menu = self.menu;
|
|
[self.item.button performClick:nil];
|
|
self.item.menu = nil;
|
|
}
|
|
|
|
// statusItemClicked fires on left/right mouse-up. Click routing (the Windows tray
|
|
// should mirror this once implemented):
|
|
// - left-click → open the main window
|
|
// - right-click / control-click → menu (Show / Settings / Quit)
|
|
// We route clicks manually instead of setting item.menu — item.menu would pop the
|
|
// menu on left-click too, leaving no way for left-click to open the window.
|
|
- (void)statusItemClicked:(id)sender {
|
|
NSEvent *e = [NSApp currentEvent];
|
|
BOOL secondary = (e.type == NSEventTypeRightMouseUp) ||
|
|
((e.modifierFlags & NSEventModifierFlagControl) != 0);
|
|
if (secondary) {
|
|
[self showMenu]; // right-click / control-click → menu
|
|
} else {
|
|
cdropOnShowWindow(); // left-click → open the main window
|
|
}
|
|
}
|
|
@end
|
|
|
|
// File-scope strong reference (ARC) keeps the controller and its status item
|
|
// alive for the process lifetime.
|
|
static CdropStatusBarController *gController = nil;
|
|
|
|
// cdropStatusBarInstall creates the menu bar item + its menu on the main thread.
|
|
// The icon is the embedded brand mascot (a colored, NON-template image — keeps
|
|
// brand color instead of tinting to the menu bar). It's displayed at the menu
|
|
// bar thickness (logical points); the high-res (256px) source rep keeps it crisp
|
|
// on retina. If decoding fails it falls back to the text title. The PNG bytes are
|
|
// copied into NSData synchronously here (before the async block) so Go can free
|
|
// its C buffer right after this call. Labels arrive as UTF-8 from Go so this
|
|
// source stays ASCII-only. Idempotent.
|
|
void cdropStatusBarInstall(const void *iconPNG, int iconLen, const char *title,
|
|
const char *showLabel, const char *settingsLabel,
|
|
const char *quitLabel) {
|
|
NSData *iconData = (iconPNG != NULL && iconLen > 0)
|
|
? [NSData dataWithBytes:iconPNG length:(NSUInteger)iconLen]
|
|
: nil;
|
|
NSString *t = [NSString stringWithUTF8String:title];
|
|
NSString *sl = [NSString stringWithUTF8String:showLabel];
|
|
NSString *gl = [NSString stringWithUTF8String:settingsLabel];
|
|
NSString *ql = [NSString stringWithUTF8String:quitLabel];
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (gController != nil) { return; }
|
|
gController = [[CdropStatusBarController alloc] init];
|
|
|
|
NSStatusItem *item = [[NSStatusBar systemStatusBar]
|
|
statusItemWithLength:NSVariableStatusItemLength];
|
|
|
|
NSImage *icon = (iconData != nil) ? [[NSImage alloc] initWithData:iconData] : nil;
|
|
if (icon != nil) {
|
|
CGFloat th = [[NSStatusBar systemStatusBar] thickness];
|
|
[icon setSize:NSMakeSize(th, th)]; // fill bar height; retina uses 256px rep
|
|
icon.template = NO; // keep brand color, don't tint
|
|
item.button.image = icon;
|
|
item.button.accessibilityLabel = t; // image has no text → a11y label
|
|
} else {
|
|
item.button.title = t;
|
|
}
|
|
|
|
NSMenu *menu = [[NSMenu alloc] init];
|
|
|
|
NSMenuItem *show = [[NSMenuItem alloc] initWithTitle:sl
|
|
action:@selector(showWindow:) keyEquivalent:@""];
|
|
show.target = gController;
|
|
[menu addItem:show];
|
|
|
|
NSMenuItem *settings = [[NSMenuItem alloc] initWithTitle:gl
|
|
action:@selector(openSettings:) keyEquivalent:@""];
|
|
settings.target = gController;
|
|
[menu addItem:settings];
|
|
|
|
[menu addItem:[NSMenuItem separatorItem]];
|
|
|
|
NSMenuItem *quit = [[NSMenuItem alloc] initWithTitle:ql
|
|
action:@selector(quit:) keyEquivalent:@""];
|
|
quit.target = gController;
|
|
[menu addItem:quit];
|
|
|
|
// Don't set item.menu (that auto-pops on mouse-down, killing double-click
|
|
// detection). Store the menu and route clicks through statusItemClicked:.
|
|
gController.item = item;
|
|
gController.menu = menu;
|
|
item.button.target = gController;
|
|
item.button.action = @selector(statusItemClicked:);
|
|
[item.button sendActionOn:(NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp)];
|
|
});
|
|
}
|
|
|
|
// cdropSetActivationPolicy flips the Dock presence on the main thread.
|
|
// policy: 0 = regular (Dock icon shown), 1 = accessory (Dock hidden, menu bar
|
|
// item only).
|
|
void cdropSetActivationPolicy(int policy) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSApplicationActivationPolicy p =
|
|
(policy == 1) ? NSApplicationActivationPolicyAccessory
|
|
: NSApplicationActivationPolicyRegular;
|
|
[NSApp setActivationPolicy:p];
|
|
});
|
|
}
|