131 lines
3.4 KiB
Go
131 lines
3.4 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"log"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
|
||
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||
|
|
|
||
|
|
"github.com/waste-go/internal/crypto"
|
||
|
|
"github.com/waste-go/internal/ipc"
|
||
|
|
"github.com/waste-go/internal/netmgr"
|
||
|
|
"github.com/waste-go/internal/proto"
|
||
|
|
)
|
||
|
|
|
||
|
|
const wsPort = 17338
|
||
|
|
|
||
|
|
// App is the Wails application backend.
|
||
|
|
// It embeds the daemon directly — no subprocess needed.
|
||
|
|
// The React frontend connects to the daemon's WebSocket IPC at ws://127.0.0.1:17338,
|
||
|
|
// exactly as it does in browser-daemon mode.
|
||
|
|
type App struct {
|
||
|
|
ctx context.Context
|
||
|
|
mgr *netmgr.Manager
|
||
|
|
}
|
||
|
|
|
||
|
|
func newApp() *App {
|
||
|
|
return &App{}
|
||
|
|
}
|
||
|
|
|
||
|
|
// startup is called when the Wails window is ready. It initialises the daemon,
|
||
|
|
// starts the WebSocket IPC listener, sets up the system tray, and begins
|
||
|
|
// forwarding message/file events to the webview as OS notifications.
|
||
|
|
func (a *App) startup(ctx context.Context) {
|
||
|
|
a.ctx = ctx
|
||
|
|
|
||
|
|
dir := dataDir()
|
||
|
|
id, err := crypto.LoadOrCreate(dir, "")
|
||
|
|
if err != nil {
|
||
|
|
log.Printf("app: identity: %v", err)
|
||
|
|
runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||
|
|
Type: runtime.ErrorDialog,
|
||
|
|
Title: "waste — startup error",
|
||
|
|
Message: "Failed to load identity: " + err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
log.Printf("app: peer id: %s alias: %s", id.PeerID().Short(), id.Alias)
|
||
|
|
|
||
|
|
a.mgr = netmgr.New(netmgr.Config{
|
||
|
|
MasterIdentity: id,
|
||
|
|
StoreDir: dir,
|
||
|
|
})
|
||
|
|
|
||
|
|
// Forward daemon events to the webview and generate OS notifications.
|
||
|
|
go a.watchEvents()
|
||
|
|
|
||
|
|
// Start the WebSocket IPC server; the webview connects here (daemon mode).
|
||
|
|
go func() {
|
||
|
|
if err := ipc.RunWS(a.mgr, wsPort); err != nil {
|
||
|
|
log.Printf("app: IPC: %v", err)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
log.Printf("app: WebSocket IPC listening on 127.0.0.1:%d", wsPort)
|
||
|
|
|
||
|
|
// System tray (Linux/Windows; no-op on macOS — see tray_darwin.go).
|
||
|
|
a.startTray()
|
||
|
|
}
|
||
|
|
|
||
|
|
// shutdown is called when the Wails app exits.
|
||
|
|
func (a *App) shutdown(ctx context.Context) {
|
||
|
|
if a.mgr != nil {
|
||
|
|
a.mgr.LeaveAll()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// watchEvents subscribes to all daemon events and emits OS notifications for
|
||
|
|
// incoming messages and completed file transfers. The "notify" event is received
|
||
|
|
// by the frontend via Wails EventsOn and displayed using the browser Notification API.
|
||
|
|
func (a *App) watchEvents() {
|
||
|
|
if a.mgr == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
events := a.mgr.Subscribe()
|
||
|
|
defer a.mgr.Unsubscribe(events)
|
||
|
|
|
||
|
|
for evt := range events {
|
||
|
|
switch evt.Type {
|
||
|
|
case proto.EvtMessageReceived:
|
||
|
|
if evt.Message == nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
// Don't notify for messages sent by the local peer.
|
||
|
|
if a.mgr.MasterIdentity() != nil && evt.Message.From == a.mgr.MasterIdentity().PeerID() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
runtime.EventsEmit(a.ctx, "notify", map[string]string{
|
||
|
|
"title": "waste — new message",
|
||
|
|
"body": evt.Message.Text,
|
||
|
|
})
|
||
|
|
|
||
|
|
case proto.EvtFileComplete:
|
||
|
|
runtime.EventsEmit(a.ctx, "notify", map[string]string{
|
||
|
|
"title": "waste — file received",
|
||
|
|
"body": filepath.Base(evt.Path),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// dataDir returns the OS-appropriate config directory for identity and stores.
|
||
|
|
// macOS: ~/Library/Application Support/waste
|
||
|
|
// Linux: ~/.config/waste
|
||
|
|
// Windows: %APPDATA%\waste
|
||
|
|
func dataDir() string {
|
||
|
|
base, err := os.UserConfigDir()
|
||
|
|
if err != nil {
|
||
|
|
if home, err := os.UserHomeDir(); err == nil {
|
||
|
|
return filepath.Join(home, ".waste")
|
||
|
|
}
|
||
|
|
return ".waste"
|
||
|
|
}
|
||
|
|
dir := filepath.Join(base, "waste")
|
||
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||
|
|
log.Printf("app: mkdir %s: %v", dir, err)
|
||
|
|
}
|
||
|
|
return dir
|
||
|
|
}
|