Files
admin f21fa5b5e8 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)。

本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
2026-06-15 21:38:28 +08:00

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];
});
}