From 95d8126d65be6a924297043543aa99503d8d300e Mon Sep 17 00:00:00 2001 From: truewhile <779943132@qq.com> Date: Mon, 14 Sep 2026 15:48:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96windows=E5=8C=85=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96windos=E5=8F=AF=E6=89=A7=E8=A1=8C=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/Auto-docker-publish.yml | 6 +- .github/workflows/beta-build.yml | 2 +- README.md | 8 + README_EN.md | 8 + cmd/server/application.go | 197 +++++++++++++ cmd/server/main.go | 115 +------- cmd/server/program_other.go | 33 +++ cmd/server/program_windows.go | 330 ++++++++++++++++++++++ cmd/server/resources_windows.go | 11 + cmd/server/rsrc_windows_amd64.syso | Bin 0 -> 6732 bytes cmd/server/rsrc_windows_arm64.syso | Bin 0 -> 6732 bytes cmd/server/winres/mebox.manifest | 28 ++ go.mod | 2 + go.sum | 4 + internal/brand/brand.go | 10 + internal/brand/logo.ico | Bin 0 -> 5217 bytes scripts/build-windows.ps1 | 42 +++ 17 files changed, 680 insertions(+), 116 deletions(-) create mode 100644 cmd/server/application.go create mode 100644 cmd/server/program_other.go create mode 100644 cmd/server/program_windows.go create mode 100644 cmd/server/resources_windows.go create mode 100644 cmd/server/rsrc_windows_amd64.syso create mode 100644 cmd/server/rsrc_windows_arm64.syso create mode 100644 cmd/server/winres/mebox.manifest create mode 100644 internal/brand/brand.go create mode 100644 internal/brand/logo.ico create mode 100644 scripts/build-windows.ps1 diff --git a/.github/workflows/Auto-docker-publish.yml b/.github/workflows/Auto-docker-publish.yml index d20cfaa..fd02333 100644 --- a/.github/workflows/Auto-docker-publish.yml +++ b/.github/workflows/Auto-docker-publish.yml @@ -196,8 +196,12 @@ jobs: path: web/dist - name: Build binary run: | + LDFLAGS="-s -w -X main.version=${{ needs.build-image.outputs.release_tag }}" + if [ "${{ matrix.goos }}" = "windows" ]; then + LDFLAGS="$LDFLAGS -H=windowsgui" + fi CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ - go build -trimpath -ldflags="-s -w -X main.version=${{ needs.build-image.outputs.release_tag }}" \ + go build -trimpath -ldflags="$LDFLAGS" \ -o "dist/mebox-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}" ./cmd/server - name: Package run: | diff --git a/.github/workflows/beta-build.yml b/.github/workflows/beta-build.yml index c03e6dd..44c7208 100644 --- a/.github/workflows/beta-build.yml +++ b/.github/workflows/beta-build.yml @@ -77,7 +77,7 @@ jobs: - name: Build linux/arm64 run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags="-s -w -X main.version=${{ steps.version.outputs.full_version }}" -o dist/mebox-beta-linux-arm64 ./cmd/server - name: Build windows/amd64 - run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w -X main.version=${{ steps.version.outputs.full_version }}" -o dist/mebox-beta-windows-amd64.exe ./cmd/server + run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w -H=windowsgui -X main.version=${{ steps.version.outputs.full_version }}" -o dist/mebox-beta-windows-amd64.exe ./cmd/server - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index f108090..b48e5d1 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,14 @@ npm --prefix web run dev # http://127.0.0.1:3000 CI 会在 Release 中提供 Windows / Linux / macOS 的 amd64、arm64 单文件可执行程序。 +Windows 本地打包: + +```powershell +.\scripts\build-windows.ps1 -Version dev +``` + +Windows 可执行程序使用项目 Logo,不显示控制台窗口;启动后会常驻系统托盘。托盘菜单可打开 MeBox、切换开机自启、查看日志、重启或退出。 + --- ## 鸣谢 diff --git a/README_EN.md b/README_EN.md index d1004ef..18ab276 100644 --- a/README_EN.md +++ b/README_EN.md @@ -199,6 +199,14 @@ npm --prefix web run dev Release builds ship single-file binaries for Windows, Linux, and macOS on amd64 and arm64. +Build the Windows executable locally: + +```powershell +.\scripts\build-windows.ps1 -Version dev +``` + +The Windows executable uses the project logo and runs without a console window. It stays in the notification area, with menu actions for opening MeBox, toggling auto-start, viewing logs, restarting, and exiting. + --- ## Acknowledgements diff --git a/cmd/server/application.go b/cmd/server/application.go new file mode 100644 index 0000000..2056c29 --- /dev/null +++ b/cmd/server/application.go @@ -0,0 +1,197 @@ +package main + +import ( + "context" + "fmt" + "os" + "sync" + "time" + + "go.uber.org/zap" + + "github.com/truewhile/MeBox/internal/config" + "github.com/truewhile/MeBox/internal/database" + "github.com/truewhile/MeBox/internal/helper" + "github.com/truewhile/MeBox/internal/repository" + "github.com/truewhile/MeBox/internal/service" +) + +// application owns the long-running MeBox server and all resources created +// during startup. Platform entry points decide how the application is +// controlled: a console signal loop or a Windows notification-area icon. +type application struct { + cfg *config.Config + logger *zap.Logger + embyCompatLogger *zap.Logger + serverManager *serverManager + services *service.Container + + closeMu sync.Mutex + closeFuncs []func() + shutdown sync.Once + shutdownErr error +} + +func newApplication() (*application, error) { + cfg, err := config.Load() + if err != nil { + return nil, fmt.Errorf("config load failed: %w", err) + } + + logger, closeLogger, err := newLoggerWithCloser(cfg) + if err != nil { + return nil, fmt.Errorf("logger init failed: %w", err) + } + + app := &application{ + cfg: cfg, + logger: logger, + closeFuncs: []func(){closeLogger}, + } + if err := app.start(); err != nil { + app.closeLoggers() + return nil, err + } + return app, nil +} + +func (a *application) start() error { + embyCompatLogger, closeEmbyCompatLogger, err := newEmbyCompatLogger(a.cfg) + if err != nil { + a.logger.Error("Emby compatibility logger init failed", zap.Error(err)) + return fmt.Errorf("Emby compatibility logger init failed: %w", err) + } + a.embyCompatLogger = embyCompatLogger + a.addCloser(closeEmbyCompatLogger) + + appVersion := effectiveVersion(version) + a.logger.Info("starting MeBox", + zap.String("version", appVersion), + zap.Int("port", a.cfg.App.Port), + zap.String("data_dir", a.cfg.App.DataDir), + zap.String("emby_compat_log", embyCompatLogPath(a.cfg)), + ) + + for _, dir := range []string{a.cfg.App.DataDir, a.cfg.Cache.CacheDir} { + if err := os.MkdirAll(dir, 0o750); err != nil { + a.logger.Error("create dir failed", zap.String("dir", dir), zap.Error(err)) + return fmt.Errorf("create dir %s failed: %w", dir, err) + } + } + + db, err := database.Open(a.cfg, a.logger) + if err != nil { + a.logger.Error("database open failed", zap.Error(err)) + return fmt.Errorf("database open failed: %w", err) + } + if err := waitForDatabase(db, a.logger); err != nil { + a.logger.Error("database not ready", zap.Error(err)) + return fmt.Errorf("database not ready: %w", err) + } + if err := database.AutoMigrate(db); err != nil { + a.logger.Error("auto-migrate failed", zap.Error(err)) + return fmt.Errorf("auto-migrate failed: %w", err) + } + if err := database.MigrateSQLiteToCurrentIfNeeded(a.cfg, db, a.logger); err != nil { + a.logger.Error("sqlite to postgres migration failed", zap.Error(err)) + return fmt.Errorf("sqlite to postgres migration failed: %w", err) + } + + repos := repository.New(db) + service.ApplyRuntimeSettings(context.Background(), a.cfg, repos, a.logger) + applyCPUThreadLimit(a.cfg, a.logger) + a.services = service.NewWithVersion(a.cfg, a.logger, repos, appVersion) + + // 一次性清洗历史脏数据: 老版本把单集 episode id / 单集名写进整剧字段, 导致 + // 同一部剧被拆成多张单集卡。清空被污染的字段并重置为 pending(借后续重刮修正)。 + if cleaned, err := a.services.NormalizePollutedEpisodeMetadata(context.Background()); err != nil { + a.logger.Warn("polluted episode metadata cleanup failed", zap.Error(err)) + } else if cleaned > 0 { + a.logger.Info("polluted episode metadata cleanup completed", zap.Int("media_count", cleaned)) + } + + if err := a.services.Auth.SeedAdmin(context.Background()); err != nil { + a.logger.Warn("seed admin failed", zap.Error(err)) + } + + router := buildRouter(a.cfg, a.logger, a.embyCompatLogger, a.services) + a.serverManager = newServerManager(a.cfg, a.logger, router) + a.services.ReloadHTTPServer = a.serverManager.Reload + if err := a.serverManager.Start(); err != nil { + a.logger.Error("listen failed", zap.Error(err)) + return fmt.Errorf("listen failed: %w", err) + } + + go func() { + scheme := "http" + if a.cfg.App.HTTPSEnabled { + scheme = "https" + } + if publicIP := getPublicIP(3 * time.Second); publicIP != "" { + a.logger.Info("server public endpoint", + zap.String("public", fmt.Sprintf("%s://%s:%d", scheme, publicIP, a.cfg.App.Port)), + ) + } + }() + helper.Go(a.logger, "services.boot", a.services.Boot) + return nil +} + +// Shutdown is idempotent and safe to call from both the tray handler and the +// systray exit callback. +func (a *application) Shutdown() error { + a.shutdown.Do(func() { + a.logger.Info("shutdown requested") + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + if a.serverManager != nil { + if err := a.serverManager.Shutdown(ctx); err != nil { + a.shutdownErr = err + a.logger.Error("graceful shutdown failed", zap.Error(err)) + } + } + cancel() + + if a.services != nil { + a.services.Close() + } + a.logger.Info("MeBox stopped") + _ = a.logger.Sync() + if a.embyCompatLogger != nil { + _ = a.embyCompatLogger.Sync() + } + a.closeLoggers() + }) + return a.shutdownErr +} + +func (a *application) addCloser(fn func()) { + if fn == nil { + return + } + a.closeMu.Lock() + a.closeFuncs = append(a.closeFuncs, fn) + a.closeMu.Unlock() +} + +func (a *application) closeLoggers() { + a.closeMu.Lock() + closers := append([]func(){}, a.closeFuncs...) + a.closeFuncs = nil + a.closeMu.Unlock() + + for i := len(closers) - 1; i >= 0; i-- { + closers[i]() + } +} + +func (a *application) localURL() string { + config.RuntimeMu.RLock() + scheme := "http" + if a.cfg.App.HTTPSEnabled { + scheme = "https" + } + port := a.cfg.App.Port + config.RuntimeMu.RUnlock() + return fmt.Sprintf("%s://127.0.0.1:%d", scheme, port) +} diff --git a/cmd/server/main.go b/cmd/server/main.go index d8161a6..083d53e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -11,21 +11,8 @@ package main import ( - "context" - "fmt" "os" - "os/signal" "strings" - "syscall" - "time" - - "go.uber.org/zap" - - "github.com/truewhile/MeBox/internal/config" - "github.com/truewhile/MeBox/internal/database" - "github.com/truewhile/MeBox/internal/helper" - "github.com/truewhile/MeBox/internal/repository" - "github.com/truewhile/MeBox/internal/service" ) // version is overwritten at build time via -ldflags="-X main.version=...". @@ -46,105 +33,5 @@ func effectiveVersion(buildVersion string) string { } func main() { - cfg, err := config.Load() - if err != nil { - fmt.Fprintf(os.Stderr, "config load failed: %v\n", err) - os.Exit(1) - } - - logger, err := newLogger(cfg) - if err != nil { - fmt.Fprintf(os.Stderr, "logger init failed: %v\n", err) - os.Exit(1) - } - defer func() { _ = logger.Sync() }() - - embyCompatLogger, closeEmbyCompatLogger, err := newEmbyCompatLogger(cfg) - if err != nil { - logger.Fatal("Emby compatibility logger init failed", zap.Error(err)) - } - defer func() { - _ = embyCompatLogger.Sync() - closeEmbyCompatLogger() - }() - - appVersion := effectiveVersion(version) - logger.Info("starting MeBox", - zap.String("version", appVersion), - zap.Int("port", cfg.App.Port), - zap.String("data_dir", cfg.App.DataDir), - zap.String("emby_compat_log", embyCompatLogPath(cfg)), - ) - - // Ensure data / cache / web dirs exist. - for _, d := range []string{cfg.App.DataDir, cfg.Cache.CacheDir} { - if err := os.MkdirAll(d, 0o750); err != nil { - logger.Fatal("create dir failed", zap.String("dir", d), zap.Error(err)) - } - } - - db, err := database.Open(cfg, logger) - if err != nil { - logger.Fatal("database open failed", zap.Error(err)) - } - if err := waitForDatabase(db, logger); err != nil { - logger.Fatal("database not ready", zap.Error(err)) - } - if err := database.AutoMigrate(db); err != nil { - logger.Fatal("auto-migrate failed", zap.Error(err)) - } - if err := database.MigrateSQLiteToCurrentIfNeeded(cfg, db, logger); err != nil { - logger.Fatal("sqlite to postgres migration failed", zap.Error(err)) - } - - repos := repository.New(db) - service.ApplyRuntimeSettings(context.Background(), cfg, repos, logger) - applyCPUThreadLimit(cfg, logger) - services := service.NewWithVersion(cfg, logger, repos, appVersion) - - // 一次性清洗历史脏数据: 老版本把单集 episode id / 单集名写进整剧字段, 导致 - // 同一部剧被拆成多张单集卡。清空被污染的字段并重置为 pending(借后续重刮修正)。 - if cleaned, err := services.NormalizePollutedEpisodeMetadata(context.Background()); err != nil { - logger.Warn("polluted episode metadata cleanup failed", zap.Error(err)) - } else if cleaned > 0 { - logger.Info("polluted episode metadata cleanup completed", zap.Int("media_count", cleaned)) - } - - if err := services.Auth.SeedAdmin(context.Background()); err != nil { - logger.Warn("seed admin failed", zap.Error(err)) - } - - router := buildRouter(cfg, logger, embyCompatLogger, services) - - serverMgr := newServerManager(cfg, logger, router) - services.ReloadHTTPServer = serverMgr.Reload - if err := serverMgr.Start(); err != nil { - logger.Fatal("listen failed", zap.Error(err)) - } - go func() { - scheme := "http" - if cfg.App.HTTPSEnabled { - scheme = "https" - } - if publicIP := getPublicIP(3 * time.Second); publicIP != "" { - logger.Info("server public endpoint", - zap.String("public", fmt.Sprintf("%s://%s:%d", scheme, publicIP, cfg.App.Port)), - ) - } - }() - helper.Go(logger, "services.boot", services.Boot) - - // Graceful shutdown. - stop := make(chan os.Signal, 1) - signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) - <-stop - logger.Info("shutdown requested") - - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - if err := serverMgr.Shutdown(ctx); err != nil { - logger.Error("graceful shutdown failed", zap.Error(err)) - } - services.Close() - logger.Info("MeBox stopped") + runProgram() } diff --git a/cmd/server/program_other.go b/cmd/server/program_other.go new file mode 100644 index 0000000..43f3825 --- /dev/null +++ b/cmd/server/program_other.go @@ -0,0 +1,33 @@ +//go:build !windows + +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" +) + +func runProgram() { + app, err := newApplication() + if err != nil { + reportError("MeBox 启动失败", err) + os.Exit(1) + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + <-stop + + if err := app.Shutdown(); err != nil { + reportError("MeBox 退出失败", err) + } +} + +func reportError(title string, err error) { + if err == nil { + return + } + fmt.Fprintf(os.Stderr, "%s: %v\n", title, err) +} diff --git a/cmd/server/program_windows.go b/cmd/server/program_windows.go new file mode 100644 index 0000000..623b03a --- /dev/null +++ b/cmd/server/program_windows.go @@ -0,0 +1,330 @@ +//go:build windows + +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "sync/atomic" + "syscall" + + "fyne.io/systray" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" + + "github.com/truewhile/MeBox/internal/brand" +) + +const ( + runRegistryKey = `Software\Microsoft\Windows\CurrentVersion\Run` + runRegistryValue = "MeBox" + singleInstanceName = `Local\MeBox-Server` +) + +var errAlreadyRunning = errors.New("MeBox is already running") + +func runProgram() { + if err := prepareWorkingDirectory(); err != nil { + reportError("MeBox 启动失败", fmt.Errorf("切换工作目录失败: %w", err)) + return + } + + instance, err := acquireSingleInstance() + if errors.Is(err, errAlreadyRunning) { + reportError("MeBox", errors.New("MeBox 已在运行,请查看右下角托盘图标")) + return + } + if err != nil { + reportError("MeBox 启动失败", fmt.Errorf("创建单实例锁失败: %w", err)) + return + } + + app, err := newApplication() + if err != nil { + _ = instance.Close() + reportError("MeBox 启动失败", err) + return + } + + controller := &trayController{app: app} + systray.Run(controller.onReady, controller.onExit) + + // Release the mutex before starting the replacement process. The new + // process must be able to acquire it immediately after the old one exits. + _ = instance.Close() + + if !controller.readyClosed.Load() { + _ = app.Shutdown() + reportError("MeBox 启动失败", errors.New("系统托盘初始化失败")) + return + } + if controller.restartRequested.Load() { + if err := launchSelf(); err != nil { + reportError("MeBox 重启失败", err) + } + } +} + +type trayController struct { + app *application + + readyClosed atomic.Bool + restartRequested atomic.Bool + shutdownStarted atomic.Bool +} + +func (c *trayController) onReady() { + defer func() { + c.readyClosed.Store(true) + }() + + systray.SetIcon(brand.Icon) + systray.SetTooltip("MeBox") + + mOpen := systray.AddMenuItem("打开 MeBox", "在浏览器中打开 MeBox") + mAutoStart := systray.AddMenuItemCheckbox("开机自启", "登录 Windows 后自动启动 MeBox", autoStartEnabled()) + mLogs := systray.AddMenuItem("查看日志", "打开 MeBox 应用日志") + systray.AddSeparator() + mRestart := systray.AddMenuItem("重启 MeBox", "重启 MeBox 服务") + mQuit := systray.AddMenuItem("退出 MeBox", "停止服务并退出") + + initialAutoStart := mAutoStart.Checked() + go func() { + for { + select { + case <-mOpen.ClickedCh: + if err := openURL(c.app.localURL()); err != nil { + reportError("MeBox", fmt.Errorf("打开 MeBox 失败: %w", err)) + } + case <-mAutoStart.ClickedCh: + enable := !mAutoStart.Checked() + if err := setAutoStart(enable); err != nil { + if initialAutoStart { + mAutoStart.Check() + } else { + mAutoStart.Uncheck() + } + reportError("MeBox", fmt.Errorf("更新开机自启设置失败: %w", err)) + continue + } + if enable { + mAutoStart.Check() + } else { + mAutoStart.Uncheck() + } + initialAutoStart = enable + case <-mLogs.ClickedCh: + if err := c.app.openLog(); err != nil { + reportError("MeBox", fmt.Errorf("打开日志失败: %w", err)) + } + case <-mRestart.ClickedCh: + c.restart() + case <-mQuit.ClickedCh: + c.quit() + } + } + }() +} + +func (c *trayController) onExit() { + _ = c.app.Shutdown() +} + +func (c *trayController) quit() { + if !c.shutdownStarted.CompareAndSwap(false, true) { + return + } + go func() { + _ = c.app.Shutdown() + systray.Quit() + }() +} + +func (c *trayController) restart() { + if !c.shutdownStarted.CompareAndSwap(false, true) { + return + } + c.restartRequested.Store(true) + go func() { + _ = c.app.Shutdown() + systray.Quit() + }() +} + +func (a *application) openLog() error { + appLog, _, _ := logFilePaths(a.cfg) + if appLog != "" { + if _, err := os.Stat(appLog); err == nil { + return openPath(appLog) + } + _ = os.MkdirAll(filepath.Dir(appLog), 0o750) + return openPath(filepath.Dir(appLog)) + } + + logDir := filepath.Join(a.cfg.App.DataDir, "logs") + if err := os.MkdirAll(logDir, 0o750); err != nil { + return err + } + return openPath(logDir) +} + +func prepareWorkingDirectory() error { + exe, err := os.Executable() + if err != nil { + return err + } + exeDir := filepath.Dir(exe) + cwd, err := os.Getwd() + if err != nil { + return err + } + if samePath(exeDir, cwd) || looksLikeProjectDirectory(cwd) { + return nil + } + return os.Chdir(exeDir) +} + +func looksLikeProjectDirectory(dir string) bool { + for _, name := range []string{"go.mod", "config.yaml", "data", filepath.Join("web", "dist")} { + if _, err := os.Stat(filepath.Join(dir, name)); err == nil { + return true + } + } + return false +} + +func samePath(left, right string) bool { + return strings.EqualFold(filepath.Clean(left), filepath.Clean(right)) +} + +type singleInstance struct { + handle windows.Handle +} + +func acquireSingleInstance() (*singleInstance, error) { + name, err := windows.UTF16PtrFromString(singleInstanceName) + if err != nil { + return nil, err + } + handle, err := windows.CreateMutex(nil, false, name) + if errors.Is(err, windows.ERROR_ALREADY_EXISTS) { + if handle != 0 { + _ = windows.CloseHandle(handle) + } + return nil, errAlreadyRunning + } + if err != nil { + return nil, err + } + return &singleInstance{handle: handle}, nil +} + +func (s *singleInstance) Close() error { + if s == nil || s.handle == 0 { + return nil + } + err := windows.CloseHandle(s.handle) + s.handle = 0 + return err +} + +func launchSelf() error { + exe, err := os.Executable() + if err != nil { + return err + } + cwd, err := os.Getwd() + if err != nil { + return err + } + + cmd := exec.Command(exe, os.Args[1:]...) + cmd.Dir = cwd + cmd.Env = os.Environ() + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + CreationFlags: windows.CREATE_NEW_PROCESS_GROUP | windows.DETACHED_PROCESS, + } + if err := cmd.Start(); err != nil { + return err + } + return cmd.Process.Release() +} + +func autoStartEnabled() bool { + key, err := registry.OpenKey(registry.CURRENT_USER, runRegistryKey, registry.QUERY_VALUE) + if err != nil { + return false + } + defer key.Close() + + value, _, err := key.GetStringValue(runRegistryValue) + if err != nil { + return false + } + exe, err := os.Executable() + if err != nil { + return false + } + return strings.EqualFold(strings.TrimSpace(strings.Trim(value, `"`)), filepath.Clean(exe)) +} + +func setAutoStart(enabled bool) error { + key, _, err := registry.CreateKey(registry.CURRENT_USER, runRegistryKey, registry.SET_VALUE) + if err != nil { + return err + } + defer key.Close() + + if !enabled { + if err := key.DeleteValue(runRegistryValue); err != nil && !errors.Is(err, registry.ErrNotExist) { + return err + } + return nil + } + + exe, err := os.Executable() + if err != nil { + return err + } + return key.SetStringValue(runRegistryValue, syscall.EscapeArg(filepath.Clean(exe))) +} + +func openURL(url string) error { + return shellOpen(url) +} + +func openPath(path string) error { + return shellOpen(path) +} + +func shellOpen(target string) error { + targetPtr, err := windows.UTF16PtrFromString(target) + if err != nil { + return err + } + verbPtr, err := windows.UTF16PtrFromString("open") + if err != nil { + return err + } + return windows.ShellExecute(0, verbPtr, targetPtr, nil, nil, 1) +} + +func reportError(title string, err error) { + if err == nil { + return + } + text, textErr := windows.UTF16PtrFromString(title + "\r\n\r\n" + err.Error()) + if textErr != nil { + return + } + caption, captionErr := windows.UTF16PtrFromString("MeBox") + if captionErr != nil { + return + } + _, _ = windows.MessageBox(0, text, caption, windows.MB_OK|windows.MB_ICONERROR|windows.MB_SETFOREGROUND) +} diff --git a/cmd/server/resources_windows.go b/cmd/server/resources_windows.go new file mode 100644 index 0000000..d668f2c --- /dev/null +++ b/cmd/server/resources_windows.go @@ -0,0 +1,11 @@ +//go:build windows + +package main + +// Regenerate the linked Windows resources after changing the project logo or +// manifest: +// +// go generate ./cmd/server +// +//go:generate go run github.com/akavel/rsrc@v0.10.2 -arch amd64 -ico ../../internal/brand/logo.ico -manifest winres/mebox.manifest -o rsrc_windows_amd64.syso +//go:generate go run github.com/akavel/rsrc@v0.10.2 -arch arm64 -ico ../../internal/brand/logo.ico -manifest winres/mebox.manifest -o rsrc_windows_arm64.syso diff --git a/cmd/server/rsrc_windows_amd64.syso b/cmd/server/rsrc_windows_amd64.syso new file mode 100644 index 0000000000000000000000000000000000000000..d704fd4eecbeaf0a64bbdfa816fd034f54528d83 GIT binary patch literal 6732 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 E1y(CJ$p8QV literal 0 HcmV?d00001 diff --git a/cmd/server/rsrc_windows_arm64.syso b/cmd/server/rsrc_windows_arm64.syso new file mode 100644 index 0000000000000000000000000000000000000000..2125468fa6d4af0b6ec46c5308a2b5e6717957b5 GIT binary patch literal 6732 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 + + + MeBox media server + + + + + + + + + + + + + + + true/pm + true + + + diff --git a/go.mod b/go.mod index 9109147..fa66d6b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/truewhile/MeBox go 1.25.0 require ( + fyne.io/systray v1.12.2 github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.5.2 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-contrib/gzip v1.2.6 @@ -48,6 +49,7 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect diff --git a/go.sum b/go.sum index 3e60851..6d54e59 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA= +fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.5.2 h1:40yUSXwdkWN851BHCq6uiDhleh7A4+0yIBS+IUAqZVY= github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.5.2/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -56,6 +58,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= diff --git a/internal/brand/brand.go b/internal/brand/brand.go new file mode 100644 index 0000000..8e34095 --- /dev/null +++ b/internal/brand/brand.go @@ -0,0 +1,10 @@ +// Package brand contains the embedded MeBox application artwork. +package brand + +import _ "embed" + +// Icon is the Windows ICO used by both the executable resource and the +// notification-area icon. +// +//go:embed logo.ico +var Icon []byte diff --git a/internal/brand/logo.ico b/internal/brand/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..71e3b7473da7034c1e3c60acad8dc7a9b860b6e1 GIT binary patch literal 5217 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 literal 0 HcmV?d00001 diff --git a/scripts/build-windows.ps1 b/scripts/build-windows.ps1 new file mode 100644 index 0000000..0617485 --- /dev/null +++ b/scripts/build-windows.ps1 @@ -0,0 +1,42 @@ +param( + [string]$Output = "dist\mebox-windows-amd64.exe", + [string]$Version = "dev", + [switch]$SkipFrontend +) + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +Push-Location $root +try { + if (-not $SkipFrontend) { + Push-Location web + try { + npm ci + npm run build + } + finally { + Pop-Location + } + } + + if (-not (Test-Path "web\dist\index.html")) { + throw "web\dist\index.html is missing. Run without -SkipFrontend or build the frontend first." + } + + $outputDir = Split-Path -Parent $Output + if ($outputDir) { + New-Item -ItemType Directory -Force -Path $outputDir | Out-Null + } + $env:CGO_ENABLED = "0" + $env:GOOS = "windows" + $env:GOARCH = "amd64" + go build -trimpath ` + -ldflags="-s -w -H=windowsgui -X main.version=$Version" ` + -o $Output ` + ./cmd/server + + Write-Host "Built $Output" +} +finally { + Pop-Location +}