feat(app): implement basic function
General - 配置界面支持配置对象存储桶 - 快捷键进入截图 - 一键上传截图 & 复制截图链接 MacOS - 状态栏指示器和应用图标
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,62 @@
|
||||
//go:build darwin
|
||||
|
||||
package tray
|
||||
|
||||
/*
|
||||
#cgo darwin LDFLAGS: -framework Foundation
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
// runOnMainQueue submits the Go-side callback (identified by handle) to the
|
||||
// main dispatch queue. We use dispatch_async so the caller — typically a
|
||||
// Wails OnStartup goroutine — does not block the cocoa runloop while waiting
|
||||
// for the main thread to drain.
|
||||
extern void trayDispatchMainCallback(unsigned long handle);
|
||||
|
||||
static void runOnMainQueue(unsigned long handle) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
trayDispatchMainCallback(handle);
|
||||
});
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// dispatchRegistry stores Go closures keyed by an integer handle so that the
|
||||
// C side can call back into Go without leaking unsafe.Pointer-cast function
|
||||
// pointers (which CGo disallows).
|
||||
var (
|
||||
dispatchMu sync.Mutex
|
||||
dispatchTable = map[uint64]func(){}
|
||||
dispatchNextID uint64
|
||||
)
|
||||
|
||||
// dispatchOnMain schedules fn to run on the macOS main thread. This is the
|
||||
// Cocoa contract for any API that touches NSWindow / NSStatusBar — calling
|
||||
// them from a goroutine triggers the "should only be instantiated on the
|
||||
// main thread" assertion.
|
||||
func dispatchOnMain(fn func()) {
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
id := atomic.AddUint64(&dispatchNextID, 1)
|
||||
dispatchMu.Lock()
|
||||
dispatchTable[id] = fn
|
||||
dispatchMu.Unlock()
|
||||
C.runOnMainQueue(C.ulong(id))
|
||||
}
|
||||
|
||||
//export trayDispatchMainCallback
|
||||
func trayDispatchMainCallback(handle C.ulong) {
|
||||
id := uint64(handle)
|
||||
dispatchMu.Lock()
|
||||
fn := dispatchTable[id]
|
||||
delete(dispatchTable, id)
|
||||
dispatchMu.Unlock()
|
||||
if fn != nil {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build !darwin
|
||||
|
||||
package tray
|
||||
|
||||
// dispatchOnMain is a no-op on non-darwin platforms because:
|
||||
// - On Linux (GTK/AppIndicator), systray.Run / nativeStart can be invoked
|
||||
// from any goroutine; the GTK runloop is started internally.
|
||||
// - On Windows, status icons are tied to a hidden window owned by the
|
||||
// systray library itself, also independent of the Go main goroutine.
|
||||
// Wrapping the call in dispatchOnMain on these platforms would just add a
|
||||
// pointless layer of indirection.
|
||||
func dispatchOnMain(fn func()) {
|
||||
if fn != nil {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build darwin
|
||||
|
||||
package tray
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// templateIconBytes embeds the macOS template icon as a multi-resolution TIFF
|
||||
// generated from the 1x and 2x source PNGs in assets/.
|
||||
//
|
||||
//go:embed assets/statusbarTemplate.tiff
|
||||
var templateIconBytes []byte
|
||||
|
||||
// regularIconBytes keeps a PNG fallback for APIs that expect a regular raster icon.
|
||||
//
|
||||
//go:embed assets/statusbarTemplate.png
|
||||
var regularIconBytes []byte
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build !darwin
|
||||
|
||||
package tray
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// Keep a regular PNG for non-macOS builds where template icons are unsupported.
|
||||
//
|
||||
//go:embed assets/statusbarTemplate.png
|
||||
var templateIconBytes []byte
|
||||
|
||||
//go:embed assets/statusbarTemplate.png
|
||||
var regularIconBytes []byte
|
||||
@@ -0,0 +1,84 @@
|
||||
// Package tray runs the menu-bar agent UI for SnapGo on macOS / Linux /
|
||||
// Windows. It is intentionally decoupled from the main App struct: the App
|
||||
// supplies a small set of callbacks (Capture / Settings / Quit) so that this
|
||||
// package never imports the application service stack.
|
||||
//
|
||||
// We use fyne.io/systray (a fork of getlantern/systray) because it is
|
||||
// designed to coexist with other GUI runloops — important when the host
|
||||
// process is also driving a Wails / Cocoa main loop.
|
||||
package tray
|
||||
|
||||
import (
|
||||
"fyne.io/systray"
|
||||
)
|
||||
|
||||
// Callbacks bundles the menu actions the host application wants to expose.
|
||||
// Splitting these into a struct (rather than 3 positional arguments) keeps
|
||||
// future extension cheap — we can add e.g. OnCheckForUpdates without
|
||||
// touching the call sites.
|
||||
type Callbacks struct {
|
||||
OnCapture func()
|
||||
OnSettings func()
|
||||
OnQuit func()
|
||||
}
|
||||
|
||||
// Start installs the menu-bar status item and returns immediately. The
|
||||
// fyne.io/systray fork exposes RunWithExternalLoop precisely for cases like
|
||||
// ours where the host process already owns the main thread (Wails).
|
||||
//
|
||||
// The returned `stop` function tears down the status item; call it from the
|
||||
// host's shutdown path.
|
||||
func Start(cbs Callbacks) (start, stop func()) {
|
||||
onReady := func() {
|
||||
systray.SetTemplateIcon(templateIconBytes, regularIconBytes)
|
||||
systray.SetTooltip("SnapGo")
|
||||
|
||||
mCapture := systray.AddMenuItem("Capture screenshot", "Take a region screenshot")
|
||||
mSettings := systray.AddMenuItem("Settings…", "Open settings window")
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Quit SnapGo", "Quit the application")
|
||||
|
||||
// Pump menu clicks on a dedicated goroutine. Channel sends from
|
||||
// systray are non-blocking, so a slow user callback does not back
|
||||
// up the UI thread.
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mCapture.ClickedCh:
|
||||
if cbs.OnCapture != nil {
|
||||
cbs.OnCapture()
|
||||
}
|
||||
case <-mSettings.ClickedCh:
|
||||
if cbs.OnSettings != nil {
|
||||
cbs.OnSettings()
|
||||
}
|
||||
case <-mQuit.ClickedCh:
|
||||
if cbs.OnQuit != nil {
|
||||
cbs.OnQuit()
|
||||
}
|
||||
systray.Quit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
onExit := func() { /* nothing to clean up */ }
|
||||
|
||||
// RunWithExternalLoop registers the status item without blocking the
|
||||
// caller — the returned `start` should be invoked once the host's
|
||||
// runloop is up so the icon appears. fyne.io/systray returns a
|
||||
// (start, end) pair; we delegate `end` to systray.Quit for symmetry.
|
||||
rawStart, _ := systray.RunWithExternalLoop(onReady, onExit)
|
||||
|
||||
// IMPORTANT: on macOS, `nativeStart` ends up calling
|
||||
// [NSStatusBar systemStatusBar] -> [-NSStatusBar _statusItemWithLength:...]
|
||||
// which constructs an NSWindow. Cocoa hard-asserts that NSWindow can
|
||||
// only be instantiated on the main thread. Wails v2 invokes OnStartup
|
||||
// from a worker goroutine, so calling rawStart() directly from there
|
||||
// crashes with NSInternalInconsistencyException. We wrap it in
|
||||
// dispatchOnMain so the call is forwarded to the main dispatch queue.
|
||||
start = func() { dispatchOnMain(rawStart) }
|
||||
stop = systray.Quit
|
||||
return start, stop
|
||||
}
|
||||
Reference in New Issue
Block a user