Daemon:
- Per-network download directories (-download-dir flag, set_download_dir IPC)
- File transfer resume after disconnection: .tmp.meta sidecars survive
interruption; resume_offset in file-accept lets sender seek and continue
- set_download_dir IPC command; download_dir reported in state_snapshot
Protocol:
- PeerMessage.ResumeOffset (EXT-006) for file transfer resume
- IpcMessage.ShareNetworks json tag changed from "networks" to "network_ids"
to fix duplicate json tag collision with Networks []NetworkInfo
Desktop app (cmd/app):
- Wails v2 shell embedding daemon logic directly (no subprocess)
- System tray on Linux/Windows via getlantern/systray; macOS hides to Dock
- OS notifications for message_received and file_complete via Wails events
- notray build tag for headless/CI builds without GTK tray headers
- build-app.sh: builds web frontend, copies dist, runs wails build
Web / PWA:
- manifest.json + Apple touch icon meta tags for mobile "Add to Home Screen"
- PNG icons (192px, 512px, 180px) generated from SVG
- Wails EventsOn("notify") hook in App.tsx for native OS notifications
CI:
- .gitea/workflows/build.yml: server binaries cross-compiled for 5 platforms,
desktop app for Linux amd64, release artifacts published on v* tags
Docs:
- README: download dir, file transfer resume, desktop app, PWA, CI sections
- EXTENSIONS.md: EXT-004 daemon mode marked shipped; EXT-006 resume added
- FUTURE.md: roadmap updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
|
|
}
|