cdrop — 跨 OS 剪贴板与文件传输服务
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)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
//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];
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user