feat: download dirs, file transfer resume, Wails desktop app, PWA, CI
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>
This commit is contained in:
130
cmd/app/app.go
Normal file
130
cmd/app/app.go
Normal file
@@ -0,0 +1,130 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user