add: settings page
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -19,9 +20,15 @@ type WindowState struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
mu sync.RWMutex
|
||||
v *viper.Viper
|
||||
mu sync.RWMutex
|
||||
|
||||
WindowState WindowState `mapstructure:"window_state"`
|
||||
ID string `mapstructure:"id"`
|
||||
SavePath string `mapstructure:"save_path"`
|
||||
HostName string `mapstructure:"host_name"`
|
||||
AutoAccept bool `mapstructure:"auto_accept"`
|
||||
SaveHistory bool `mapstructure:"save_history"`
|
||||
}
|
||||
|
||||
// 默认窗口配置
|
||||
@@ -32,7 +39,7 @@ var defaultWindowState = WindowState{
|
||||
Y: -1,
|
||||
}
|
||||
|
||||
func getConfigDir() string {
|
||||
func GetConfigDir() string {
|
||||
configPath, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
configPath = "/tmp"
|
||||
@@ -40,7 +47,7 @@ func getConfigDir() string {
|
||||
return filepath.Join(configPath, "mesh-drop")
|
||||
}
|
||||
|
||||
func getUserHomeDir() string {
|
||||
func GetUserHomeDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "/tmp"
|
||||
@@ -50,7 +57,8 @@ func getUserHomeDir() string {
|
||||
|
||||
// New 读取配置
|
||||
func Load() *Config {
|
||||
configDir := getConfigDir()
|
||||
v := viper.New()
|
||||
configDir := GetConfigDir()
|
||||
err := os.MkdirAll(configDir, 0755)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create config directory", "error", err)
|
||||
@@ -58,15 +66,21 @@ func Load() *Config {
|
||||
configFile := filepath.Join(configDir, "config.json")
|
||||
|
||||
// 设置默认值
|
||||
defaultSavePath := filepath.Join(getUserHomeDir(), "Downloads")
|
||||
viper.SetDefault("window_state", defaultWindowState)
|
||||
viper.SetDefault("save_path", defaultSavePath)
|
||||
defaultSavePath := filepath.Join(GetUserHomeDir(), "Downloads")
|
||||
v.SetDefault("window_state", defaultWindowState)
|
||||
v.SetDefault("save_path", defaultSavePath)
|
||||
defaultHostName, err := os.Hostname()
|
||||
if err != nil {
|
||||
defaultHostName = "localhost"
|
||||
}
|
||||
v.SetDefault("host_name", defaultHostName)
|
||||
v.SetDefault("id", uuid.New().String())
|
||||
|
||||
viper.SetConfigFile(configFile)
|
||||
viper.SetConfigType("json")
|
||||
v.SetConfigFile(configFile)
|
||||
v.SetConfigType("json")
|
||||
|
||||
// 尝试读取配置
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
slog.Info("Config file not found, using defaults")
|
||||
} else {
|
||||
@@ -75,10 +89,12 @@ func Load() *Config {
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
if err := v.Unmarshal(&config); err != nil {
|
||||
slog.Error("Failed to unmarshal config", "error", err)
|
||||
}
|
||||
|
||||
config.v = v
|
||||
|
||||
return &config
|
||||
}
|
||||
|
||||
@@ -87,12 +103,12 @@ func (c *Config) Save() error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
configDir := getConfigDir()
|
||||
configDir := GetConfigDir()
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
if err := c.v.WriteConfig(); err != nil {
|
||||
slog.Error("Failed to write config", "error", err)
|
||||
return err
|
||||
}
|
||||
@@ -106,7 +122,8 @@ func (c *Config) SetSavePath(savePath string) {
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.SavePath = savePath
|
||||
viper.Set("save_path", savePath)
|
||||
c.v.Set("save_path", savePath)
|
||||
_ = os.MkdirAll(savePath, 0755)
|
||||
}
|
||||
|
||||
func (c *Config) GetSavePath() string {
|
||||
@@ -114,3 +131,48 @@ func (c *Config) GetSavePath() string {
|
||||
defer c.mu.RUnlock()
|
||||
return c.SavePath
|
||||
}
|
||||
|
||||
func (c *Config) SetHostName(hostName string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.HostName = hostName
|
||||
c.v.Set("host_name", hostName)
|
||||
}
|
||||
|
||||
func (c *Config) GetHostName() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.HostName
|
||||
}
|
||||
|
||||
func (c *Config) GetID() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.ID
|
||||
}
|
||||
|
||||
func (c *Config) SetAutoAccept(autoAccept bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.AutoAccept = autoAccept
|
||||
c.v.Set("auto_accept", autoAccept)
|
||||
}
|
||||
|
||||
func (c *Config) GetAutoAccept() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.AutoAccept
|
||||
}
|
||||
|
||||
func (c *Config) SetSaveHistory(saveHistory bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.SaveHistory = saveHistory
|
||||
c.v.Set("save_history", saveHistory)
|
||||
}
|
||||
|
||||
func (c *Config) GetSaveHistory() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.SaveHistory
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ type Peer struct {
|
||||
// Port 是文件传输服务的监听端口。
|
||||
Port int `json:"port"`
|
||||
|
||||
// IsOnline 标记该端点当前是否活跃 (UI 渲染用)。
|
||||
IsOnline bool `json:"is_online"`
|
||||
|
||||
OS OS `json:"os"`
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"mesh-drop/internal/config"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
@@ -25,7 +23,7 @@ type Service struct {
|
||||
app *application.App
|
||||
|
||||
ID string
|
||||
Name string
|
||||
config *config.Config
|
||||
FileServerPort int
|
||||
|
||||
// key 使用 peer.id 和 peer.ip 组合而成的 hash
|
||||
@@ -37,33 +35,11 @@ func init() {
|
||||
application.RegisterEvent[[]Peer]("peers:update")
|
||||
}
|
||||
|
||||
// getOrInitDeviceID 获取或初始化设备 ID
|
||||
func getOrInitDeviceID() string {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
appDir := filepath.Join(configDir, "mesh-drop")
|
||||
if err := os.MkdirAll(appDir, 0755); err != nil {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
idFile := filepath.Join(appDir, "device_id")
|
||||
if data, err := os.ReadFile(idFile); err == nil {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
id := uuid.New().String()
|
||||
_ = os.WriteFile(idFile, []byte(id), 0644)
|
||||
return id
|
||||
}
|
||||
|
||||
func NewService(app *application.App, name string, port int) *Service {
|
||||
func NewService(config *config.Config, app *application.App, port int) *Service {
|
||||
return &Service{
|
||||
app: app,
|
||||
ID: getOrInitDeviceID(),
|
||||
Name: name,
|
||||
ID: config.GetID(),
|
||||
config: config,
|
||||
FileServerPort: port,
|
||||
peers: make(map[string]*Peer),
|
||||
}
|
||||
@@ -79,7 +55,7 @@ func (s *Service) startBroadcasting() {
|
||||
}
|
||||
packet := PresencePacket{
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
Name: s.config.GetHostName(),
|
||||
Port: s.FileServerPort,
|
||||
OS: OS(runtime.GOOS),
|
||||
}
|
||||
@@ -163,7 +139,7 @@ func (s *Service) startListening() {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理心跳包
|
||||
// handleHeartbeat 处理心跳包
|
||||
func (s *Service) handleHeartbeat(pkt PresencePacket, ip string) {
|
||||
s.peersMutex.Lock()
|
||||
|
||||
@@ -186,13 +162,14 @@ func (s *Service) handleHeartbeat(pkt PresencePacket, ip string) {
|
||||
slog.Info("New device found", "name", pkt.Name, "ip", ip, "component", "discovery")
|
||||
} else {
|
||||
// 更新节点
|
||||
peer.Name = pkt.Name
|
||||
peer.OS = pkt.OS
|
||||
peer.Routes[ip] = &RouteState{
|
||||
IP: ip,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
peer.IsOnline = true
|
||||
s.peersMutex.Unlock()
|
||||
|
||||
// 触发前端更新 (防抖逻辑可以之后加,这里每次变动都推)
|
||||
@@ -260,10 +237,6 @@ func (s *Service) GetPeers() []Peer {
|
||||
return list
|
||||
}
|
||||
|
||||
func (s *Service) GetName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *Service) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath str
|
||||
taskID,
|
||||
Sender{
|
||||
ID: s.discoveryService.GetID(),
|
||||
Name: s.discoveryService.GetName(),
|
||||
Name: s.config.GetHostName(),
|
||||
},
|
||||
WithFileName(filepath.Base(filePath)),
|
||||
WithFileSize(stat.Size()),
|
||||
@@ -105,7 +105,7 @@ func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath
|
||||
taskID,
|
||||
Sender{
|
||||
ID: s.discoveryService.GetID(),
|
||||
Name: s.discoveryService.GetName(),
|
||||
Name: s.config.GetHostName(),
|
||||
},
|
||||
WithFileName(filepath.Base(folderPath)),
|
||||
WithFileSize(size),
|
||||
@@ -159,7 +159,7 @@ func (s *Service) SendText(target *discovery.Peer, targetIP string, text string)
|
||||
taskID,
|
||||
Sender{
|
||||
ID: s.discoveryService.GetID(),
|
||||
Name: s.discoveryService.GetName(),
|
||||
Name: s.config.GetHostName(),
|
||||
},
|
||||
WithFileSize(int64(len(text))),
|
||||
WithType(TransferTypeSend),
|
||||
|
||||
45
internal/transfer/history.go
Normal file
45
internal/transfer/history.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"mesh-drop/internal/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (s *Service) SaveHistory() {
|
||||
configDir := config.GetConfigDir()
|
||||
historyPath := filepath.Join(configDir, "history.json")
|
||||
historyJson, err := json.Marshal(s.GetTransferList())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
file, err := os.OpenFile(historyPath, os.O_CREATE|os.O_RDWR, os.FileMode(0644))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = file.Write(historyJson)
|
||||
if err != nil {
|
||||
slog.Error("Failed to write history", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) LoadHistory() {
|
||||
configDir := config.GetConfigDir()
|
||||
historyPath := filepath.Join(configDir, "history.json")
|
||||
file, err := os.Open(historyPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
var history []Transfer
|
||||
err = json.NewDecoder(file).Decode(&history)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, item := range history {
|
||||
s.StoreTransferToList(&item)
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,13 @@ func (s *Service) handleAsk(c *gin.Context) {
|
||||
task.DecisionChan = make(chan Decision)
|
||||
s.StoreTransferToList(&task)
|
||||
|
||||
// 通知 Wails 前端
|
||||
if s.config.GetAutoAccept() {
|
||||
task.DecisionChan <- Decision{
|
||||
ID: task.ID,
|
||||
Accepted: true,
|
||||
SavePath: s.config.GetSavePath(),
|
||||
}
|
||||
}
|
||||
|
||||
// 等待用户决策或发送端放弃
|
||||
select {
|
||||
@@ -53,15 +59,14 @@ func (s *Service) handleAsk(c *gin.Context) {
|
||||
task.SavePath = decision.SavePath
|
||||
token := uuid.New().String()
|
||||
task.Token = token
|
||||
c.JSON(http.StatusOK, TransferAskResponse{
|
||||
ID: task.ID,
|
||||
Accepted: decision.Accepted,
|
||||
Token: task.Token,
|
||||
})
|
||||
} else {
|
||||
task.Status = TransferStatusRejected
|
||||
}
|
||||
c.JSON(http.StatusOK, TransferAskResponse{
|
||||
ID: task.ID,
|
||||
Accepted: decision.Accepted,
|
||||
Token: task.Token,
|
||||
})
|
||||
|
||||
case <-c.Request.Context().Done():
|
||||
// 发送端放弃
|
||||
task.Status = TransferStatusCanceled
|
||||
|
||||
Reference in New Issue
Block a user