@yufengtadian/freedom-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,257 @@
1
+ // Package freedom 是一个从零自研的 WebView 桌面壳子框架(对标 Wails / Tauri)。
2
+ //
3
+ // Freedom 2.0 架构:
4
+ // - shell:跨平台渲染壳。复用各系统自带 WebView 内核(Windows WebView2 / macOS
5
+ // WKWebView / Linux WebKitGTK),通过跨平台库 webview_go 绑定,自身不携带浏览器
6
+ // 内核。壳负责窗口生命周期、前端资源 embed、后端进程管理、IPC 路由。一份壳代码
7
+ // 编译三平台。
8
+ // - backend:后端抽象。任意时刻绑定一个后端,前端 window.freedom.call 全部路由过去。
9
+ // 内置两种实现:
10
+ // * EmbedBackend:Go 方法直接注册在壳进程内(app.Bind,兼容 v1 用法)。
11
+ // * ProcBackend:后端是任意语言实现的独立进程(Go/Rust/Python/Node/Java…),
12
+ // 通过 stdin/stdout 上的 NDJSON/JSON-RPC 与壳通信。协议语言无关,
13
+ // 换一种后端语言无需改壳与前端。
14
+ // - ipc:双向桥接。前端 window.freedom.call(method, ...args) -> Promise;
15
+ // 后端 app.Emit(event, data) 向所有前端监听器推送事件。
16
+ // - assets:前端编译产物(Vite 单文件 HTML)通过 go:embed 嵌入二进制,运行时
17
+ // 直接 SetHtml 从内存加载,无需本地 HTTP 端口。
18
+ package freedom
19
+
20
+ import (
21
+ "encoding/json"
22
+ "fmt"
23
+ "unsafe"
24
+
25
+ webview "github.com/webview/webview_go"
26
+ )
27
+
28
+ // TitleBarMode 描述窗口标题栏策略。
29
+ type TitleBarMode string
30
+
31
+ const (
32
+ // TitleBarNative 保留系统原生标题栏(默认)。
33
+ TitleBarNative TitleBarMode = "native"
34
+ // TitleBarHidden 隐藏标题栏视觉,但保留系统原生最小化 / 最大化 / 关闭按钮。
35
+ // 当前仅 Windows 生效(基于 DWM 扩展实现);macOS / Linux 上回退为原生标题栏。
36
+ TitleBarHidden TitleBarMode = "hidden"
37
+ // TitleBarFrameless 完全无边框,客户区铺满整个窗口。
38
+ // 最小化 / 最大化 / 关闭按钮需由前端自绘(通过 window.freedom.window.* 控制),
39
+ // 三平台行为一致。
40
+ TitleBarFrameless TitleBarMode = "frameless"
41
+ )
42
+
43
+ // Config 描述一个 Freedom 应用的窗口与运行配置。
44
+ type Config struct {
45
+ // Title 是窗口标题。
46
+ Title string
47
+ // TitleBar 指定标题栏策略(默认 TitleBarNative)。可通过 freedom CLI 一键切换。
48
+ TitleBar TitleBarMode
49
+ // Width / Height 是窗口初始尺寸(像素)。
50
+ Width int
51
+ Height int
52
+ // Center 为 true 时窗口在屏幕居中(Windows 生效;macOS/Linux 由窗口管理器决定)。
53
+ Center bool
54
+ // MinWidth / MinHeight 为窗口最小尺寸(<=0 表示不限制)。
55
+ MinWidth int
56
+ MinHeight int
57
+ // Debug 为 true 时开启 WebView 开发者工具(目标平台支持时)。
58
+ Debug bool
59
+ // Backend 指定后端适配器。为空时默认使用内嵌 Go 后端(配合 Bind 使用)。
60
+ Backend Backend
61
+ // HTML 返回要加载到窗口的前端页面内容(内存加载,无本地端口)。
62
+ // 为 nil 时使用框架内置的默认占位页。
63
+ HTML func() (string, error)
64
+ }
65
+
66
+ // App 是 Freedom 应用实例。
67
+ type App struct {
68
+ cfg Config
69
+ view webview.WebView
70
+ backend Backend
71
+ onReady func(a *App)
72
+ }
73
+
74
+ // New 创建并初始化一个 Freedom 应用。调用 Run() 之前不会显示窗口。
75
+ func New(cfg Config) *App {
76
+ if cfg.Title == "" {
77
+ cfg.Title = "Freedom App"
78
+ }
79
+ if cfg.TitleBar == "" {
80
+ cfg.TitleBar = TitleBarNative
81
+ }
82
+ if cfg.Width <= 0 {
83
+ cfg.Width = 1024
84
+ }
85
+ if cfg.Height <= 0 {
86
+ cfg.Height = 768
87
+ }
88
+ if cfg.Backend == nil {
89
+ cfg.Backend = NewEmbedBackend()
90
+ }
91
+ return &App{cfg: cfg, backend: cfg.Backend}
92
+ }
93
+
94
+ // OnReady 注册一个回调,在窗口与桥接层就绪、页面加载前执行。
95
+ // 回调内可以安全调用 Emit 向已注入的页面发送初始化事件。
96
+ func (a *App) OnReady(fn func(a *App)) {
97
+ a.onReady = fn
98
+ }
99
+
100
+ // Bind 把后端 Go 方法暴露给前端。仅当后端为内嵌 Go 后端(默认)时有效;
101
+ // 进程后端的方法由后端进程自身注册,此处调用会返回错误。
102
+ //
103
+ // - fn 必须是函数。
104
+ // - 参数与返回值通过 JSON 编解码,前端用 window.freedom.call(name, ...args) 调用,
105
+ // 返回 Promise(resolve 为返回值,reject 为 error 的字符串表示)。
106
+ // - 返回值约定:可返回 (T, error) 或仅 (T) 或仅 error 或空。
107
+ func (a *App) Bind(name string, fn interface{}) error {
108
+ eb, ok := a.backend.(*EmbedBackend)
109
+ if !ok {
110
+ return fmt.Errorf("freedom: Bind 仅适用于内嵌 Go 后端;当前后端为 %T,方法请在进程后端中注册", a.backend)
111
+ }
112
+ return eb.Bind(name, fn)
113
+ }
114
+
115
+ // Unbind 移除先前 Bind 的方法(内嵌后端)。
116
+ func (a *App) Unbind(name string) {
117
+ if eb, ok := a.backend.(*EmbedBackend); ok {
118
+ eb.Unbind(name)
119
+ }
120
+ }
121
+
122
+ // Run 启动窗口并进入主事件循环,阻塞直到窗口被关闭。
123
+ func (a *App) Run() {
124
+ html, err := a.resolveHTML()
125
+ if err != nil {
126
+ fmt.Printf("freedom: failed to resolve HTML: %v\n", err)
127
+ return
128
+ }
129
+
130
+ w := webview.New(a.cfg.Debug)
131
+ if w == nil {
132
+ fmt.Println("freedom: failed to create webview")
133
+ return
134
+ }
135
+ a.view = w
136
+ defer func() {
137
+ _ = a.backend.Close()
138
+ w.Destroy()
139
+ }()
140
+
141
+ // 进程后端:启动后端进程并把其推送的事件转发到前端。
142
+ if pb, ok := a.backend.(*ProcBackend); ok {
143
+ pb.OnEvent(func(event string, data interface{}) {
144
+ a.Emit(event, data)
145
+ })
146
+ if err := pb.start(); err != nil {
147
+ fmt.Printf("freedom: backend start failed: %v\n", err)
148
+ return
149
+ }
150
+ }
151
+
152
+ w.SetTitle(a.cfg.Title)
153
+ w.SetSize(a.cfg.Width, a.cfg.Height, webview.HintNone)
154
+ if a.cfg.MinWidth > 0 && a.cfg.MinHeight > 0 {
155
+ w.SetSize(a.cfg.MinWidth, a.cfg.MinHeight, webview.HintMin)
156
+ }
157
+ a.applyCenter()
158
+ a.applyTitleBar()
159
+
160
+ // 注入前端 SDK:window.freedom 全局对象。
161
+ w.Init(jsSDK)
162
+
163
+ // 绑定 IPC 桥接入口:前端 window.__freedom_bridge(method, paramsJson)。
164
+ // webview_go 的 Bind 会让前端调用返回 Promise,Go 侧结果自动 JSON 序列化回传。
165
+ if err := w.Bind("__freedom_bridge", a.bridge); err != nil {
166
+ fmt.Printf("freedom: failed to bind bridge: %v\n", err)
167
+ return
168
+ }
169
+ // 框架内置方法:健康检查。
170
+ if err := w.Bind("__freedom__ping", func() string { return "pong" }); err != nil {
171
+ fmt.Printf("freedom: failed to bind ping: %v\n", err)
172
+ return
173
+ }
174
+ // 框架内置方法:窗口控制(无边框模式下前端自绘按钮使用)。
175
+ // 支持动作:minimize / maximize / unmaximize / toggleMaximize / close / isMaximized / isFrameless。
176
+ if err := w.Bind("__freedom_window", a.windowControl); err != nil {
177
+ fmt.Printf("freedom: failed to bind window control: %v\n", err)
178
+ return
179
+ }
180
+
181
+ if a.onReady != nil {
182
+ a.onReady(a)
183
+ }
184
+
185
+ w.SetHtml(html)
186
+ w.Run()
187
+ }
188
+
189
+ // bridge 是前端调用后端的统一入口(JSON-RPC 风格)。
190
+ // 前端 SDK 通过 window.__freedom_bridge(method, paramsJson) 调用,
191
+ // 桥接层把请求转发给当前绑定的后端(内嵌 Go 方法或任意语言进程)。
192
+ func (a *App) bridge(method string, paramsJSON string) (json.RawMessage, error) {
193
+ var params []json.RawMessage
194
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
195
+ if err := json.Unmarshal([]byte(paramsJSON), &params); err != nil {
196
+ return nil, fmt.Errorf("freedom: method %q: invalid params: %w", method, err)
197
+ }
198
+ }
199
+
200
+ result, err := a.backend.Handle(method, params)
201
+ if err != nil {
202
+ return nil, err
203
+ }
204
+ data, err := json.Marshal(result)
205
+ if err != nil {
206
+ return nil, fmt.Errorf("freedom: method %q: cannot marshal result: %w", method, err)
207
+ }
208
+ return json.RawMessage(data), nil
209
+ }
210
+
211
+ // Emit 把事件推送到前端。前端通过 window.freedom.on(event, cb) 订阅。
212
+ // 线程安全:可从任意 goroutine 调用(进程后端推送的事件亦经由本函数)。
213
+ func (a *App) Emit(event string, data interface{}) {
214
+ if a.view == nil {
215
+ return
216
+ }
217
+ eb, _ := json.Marshal(event)
218
+ db, err := json.Marshal(data)
219
+ if err != nil {
220
+ db = []byte("null")
221
+ }
222
+ js := "window.freedom && window.freedom.emit(" + string(eb) + "," + string(db) + ");"
223
+ a.view.Dispatch(func() {
224
+ a.view.Eval(js)
225
+ })
226
+ }
227
+
228
+ // Quit 关闭窗口并退出应用。可从任意 goroutine 调用。
229
+ func (a *App) Quit() {
230
+ if a.view != nil {
231
+ a.view.Dispatch(func() {
232
+ a.view.Terminate()
233
+ })
234
+ }
235
+ }
236
+
237
+ // WindowHandle 返回底层原生窗口句柄(Windows 上为 HWND)。
238
+ func (a *App) WindowHandle() uintptr {
239
+ if a.view == nil {
240
+ return 0
241
+ }
242
+ return uintptr(unsafe.Pointer(a.view.Window()))
243
+ }
244
+
245
+ // windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
246
+ // 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
247
+ func (a *App) windowControl(action string) (interface{}, error) {
248
+ return windowControl(a.WindowHandle(), action, a.cfg.TitleBar)
249
+ }
250
+
251
+ // resolveHTML 依据配置返回页面内容。
252
+ func (a *App) resolveHTML() (string, error) {
253
+ if a.cfg.HTML != nil {
254
+ return a.cfg.HTML()
255
+ }
256
+ return defaultHTML, nil
257
+ }
@@ -0,0 +1,8 @@
1
+ //go:build !windows
2
+
3
+ package freedom
4
+
5
+ import "os/exec"
6
+
7
+ // hideWindow 非 Windows 平台无控制台窗口概念,空实现占位。
8
+ func hideWindow(_ *exec.Cmd) {}
@@ -0,0 +1,15 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ import (
6
+ "os/exec"
7
+ "syscall"
8
+ )
9
+
10
+ // hideWindow 让后端子进程(即使自身是控制台程序)不弹出 cmd 黑窗。
11
+ // GUI 子系统父进程拉起 console 子进程时,Windows 默认会为其新建控制台窗口,
12
+ // 设置 HideWindow 可使其在后台静默运行。
13
+ func hideWindow(cmd *exec.Cmd) {
14
+ cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
15
+ }
@@ -0,0 +1,25 @@
1
+ //go:build !windows
2
+
3
+ package freedom
4
+
5
+ import "fmt"
6
+
7
+ // applyTitleBar 在 macOS / Linux 上为空实现:
8
+ // 原生标题栏由窗口管理器接管,hidden / frameless 模式暂回退为原生标题栏,
9
+ // 前端仍可通过 window.freedom.window.isFrameless() 感知当前是否无边框。
10
+ func (a *App) applyTitleBar() {}
11
+
12
+ // windowControl 处理前端 window.freedom.window.* 请求(macOS / Linux 占位实现)。
13
+ func windowControl(hwnd uintptr, action string, mode TitleBarMode) (interface{}, error) {
14
+ switch action {
15
+ case "isFrameless":
16
+ return mode == TitleBarFrameless || mode == TitleBarHidden, nil
17
+ case "isMaximized":
18
+ return false, nil
19
+ case "minimize", "maximize", "unmaximize", "restore", "toggleMaximize", "close":
20
+ // 非 Windows 平台暂不提供底层窗口控制,前端自绘按钮可对事件静默处理
21
+ return nil, nil
22
+ default:
23
+ return nil, fmt.Errorf("unknown window action %q", action)
24
+ }
25
+ }
@@ -0,0 +1,133 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ import (
6
+ "fmt"
7
+ "syscall"
8
+ "unsafe"
9
+ )
10
+
11
+ // Windows 平台原生窗口操作(user32 / dwmapi)。
12
+ // 供标题栏策略(applyTitleBar)与前端 window.freedom.window.* 控制使用。
13
+
14
+ var (
15
+ user32win = syscall.NewLazyDLL("user32.dll")
16
+
17
+ procGetWindowLongPtr = user32win.NewProc("GetWindowLongPtrW")
18
+ procSetWindowLongPtr = user32win.NewProc("SetWindowLongPtrW")
19
+ procShowWindow = user32win.NewProc("ShowWindow")
20
+ procCloseWindow = user32win.NewProc("CloseWindow")
21
+ procIsZoomed = user32win.NewProc("IsZoomed")
22
+ procSetWindowPos = user32win.NewProc("SetWindowPos")
23
+ procSetWindowText = user32win.NewProc("SetWindowTextW")
24
+
25
+ dwmapi = syscall.NewLazyDLL("dwmapi.dll")
26
+ procDwmExtendFrameIntoArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
27
+ )
28
+
29
+ const (
30
+ wsCaption = 0x00C00000 // WS_CAPTION = WS_BORDER | WS_DLGFRAME
31
+ wsSysMenu = 0x00080000
32
+ wsThickFrame = 0x00040000
33
+
34
+ swHide = 0
35
+ swShow = 5
36
+ swMinimize = 6
37
+ swRestore = 9
38
+ swMaximize = 3
39
+
40
+ swpFrameChanged = 0x0020
41
+ swpNoMove = 0x0002
42
+ swpNoSize = 0x0001
43
+ swpNoZOrder = 0x0004
44
+ swpNoActivate = 0x0010
45
+ )
46
+
47
+ // gwlStyle = GWL_STYLE(-16)。用变量声明,避免 uintptr 常量转换溢出。
48
+ var gwlStyle = -16
49
+
50
+ // margins 对应 DWM 的 MARGINS 结构(DwmExtendFrameIntoClientArea)。
51
+ type margins struct {
52
+ cxLeftWidth, cxRightWidth, cyTopHeight, cyBottomHeight int32
53
+ }
54
+
55
+ func getWindowStyle(hwnd uintptr) uintptr {
56
+ r, _, _ := procGetWindowLongPtr.Call(hwnd, uintptr(int(gwlStyle)))
57
+ return r
58
+ }
59
+
60
+ func setWindowStyle(hwnd, style uintptr) {
61
+ procSetWindowLongPtr.Call(hwnd, uintptr(int(gwlStyle)), style)
62
+ }
63
+
64
+ func refreshFrame(hwnd uintptr) {
65
+ procSetWindowPos.Call(hwnd, 0, 0, 0, 0, 0,
66
+ swpFrameChanged|swpNoMove|swpNoSize|swpNoZOrder|swpNoActivate)
67
+ }
68
+
69
+ // windowControl 处理前端 window.freedom.window.* 请求(Windows 实现)。
70
+ func windowControl(hwnd uintptr, action string, mode TitleBarMode) (interface{}, error) {
71
+ if hwnd == 0 {
72
+ return nil, fmt.Errorf("window not ready")
73
+ }
74
+ switch action {
75
+ case "minimize":
76
+ procShowWindow.Call(hwnd, swMinimize)
77
+ return nil, nil
78
+ case "maximize":
79
+ procShowWindow.Call(hwnd, swMaximize)
80
+ return nil, nil
81
+ case "unmaximize", "restore":
82
+ procShowWindow.Call(hwnd, swRestore)
83
+ return nil, nil
84
+ case "toggleMaximize":
85
+ if isZoomed(hwnd) {
86
+ procShowWindow.Call(hwnd, swRestore)
87
+ } else {
88
+ procShowWindow.Call(hwnd, swMaximize)
89
+ }
90
+ return nil, nil
91
+ case "close":
92
+ procCloseWindow.Call(hwnd)
93
+ return nil, nil
94
+ case "isMaximized":
95
+ return isZoomed(hwnd), nil
96
+ case "isFrameless":
97
+ return mode == TitleBarFrameless || mode == TitleBarHidden, nil
98
+ default:
99
+ return nil, fmt.Errorf("unknown window action %q", action)
100
+ }
101
+ }
102
+
103
+ func isZoomed(hwnd uintptr) bool {
104
+ r, _, _ := procIsZoomed.Call(hwnd)
105
+ return r != 0
106
+ }
107
+
108
+ // applyTitleBar 依据配置调整窗口标题栏(Windows 实现)。
109
+ func (a *App) applyTitleBar() {
110
+ hwnd := a.WindowHandle()
111
+ if hwnd == 0 {
112
+ return
113
+ }
114
+ switch a.cfg.TitleBar {
115
+ case TitleBarFrameless:
116
+ // 完全无边框:去掉标题栏 / 系统菜单,客户区铺满整个窗口。
117
+ // 最小化 / 最大化 / 关闭按钮由前端自绘(window.freedom.window.*)。
118
+ style := getWindowStyle(hwnd)
119
+ style &^= wsCaption | wsSysMenu
120
+ setWindowStyle(hwnd, style)
121
+ refreshFrame(hwnd)
122
+ case TitleBarHidden:
123
+ // 隐藏标题栏视觉但保留系统原生按钮:DWM 玻璃扩展。
124
+ // 标题栏区域透明化并并入客户区,右上角的最小化 / 最大化 / 关闭按钮
125
+ // 由 DWM 继续原生绘制,标题文字置空。
126
+ m := margins{cxLeftWidth: 0, cxRightWidth: 0, cyTopHeight: 0, cyBottomHeight: 1}
127
+ procDwmExtendFrameIntoArea.Call(hwnd, uintptr(unsafe.Pointer(&m)))
128
+ // 标题文字一并清除,标题栏区域只保留系统按钮
129
+ procSetWindowText.Call(hwnd, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(""))))
130
+ refreshFrame(hwnd)
131
+ default: // TitleBarNative:不处理
132
+ }
133
+ }
@@ -0,0 +1,42 @@
1
+ // Freedom 应用配置。
2
+ // 构建时由 freedom CLI 读取,生成对应的壳层配置。
3
+ export default {
4
+ // 应用名(窗口标题 / 可执行文件名)。
5
+ name: 'freedom-app',
6
+
7
+ // 窗口初始尺寸(像素)。
8
+ width: 1024,
9
+ height: 720,
10
+
11
+ // 窗口最小尺寸(设为 0 表示不限制)。
12
+ minWidth: 400,
13
+ minHeight: 300,
14
+
15
+ // 启动时是否在屏幕居中。
16
+ center: true,
17
+
18
+ // 是否开启 WebView 开发者工具。
19
+ debug: false,
20
+
21
+ // 标题栏策略,在终端随时可改,三选一:
22
+ // 'native' - 保留系统原生标题栏(默认)
23
+ // 'hidden' - 隐藏标题栏视觉,仅保留 Windows 原生最小化 / 最大化 / 关闭按钮
24
+ // 'frameless' - 完全无边框,按钮由前端自绘(模板已内置自绘标题栏示例)
25
+ // 终端命令:freedom titlebar <native|hidden|frameless>
26
+ titlebar: 'native',
27
+
28
+ // 产物输出目录(相对项目根目录或绝对路径):
29
+ // 'dist' - 输出到 dist/ 子目录(默认)
30
+ // '.' - 直接输出到项目根目录(dist 的上级),exe 与单文件 index.html 就地生成
31
+ // 'build/app' 等任意路径亦可
32
+ // 注意:输出到项目根目录时不会覆盖项目源文件(index.html 仅保留 src 源文件)。
33
+ outDir: 'dist',
34
+
35
+ // 后端策略:
36
+ // undefined - 仅使用框架内置能力(ping / 窗口控制),不启动独立后端进程
37
+ // { command, args } - 启动任意语言后端进程(Go/Node/Python/Rust 均可),
38
+ // 经 stdin/stdout NDJSON 与壳通信,方法直接暴露给前端
39
+ // 示例(Node 后端):
40
+ // backend: { command: 'node', args: ['backend/main.mjs'] }
41
+ // backend: undefined,
42
+ };
@@ -0,0 +1,82 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Freedom App</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ html, body { height: 100%; }
10
+ body {
11
+ font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
12
+ background: #111827;
13
+ color: #e5e7eb;
14
+ display: flex;
15
+ flex-direction: column;
16
+ overflow: hidden;
17
+ user-select: none;
18
+ }
19
+
20
+ /* 无边框模式下的自绘标题栏(titlebar: 'frameless' 时显示) */
21
+ .titlebar {
22
+ display: none;
23
+ height: 38px;
24
+ flex: 0 0 38px;
25
+ align-items: center;
26
+ justify-content: space-between;
27
+ padding-left: 14px;
28
+ background: rgba(17, 24, 39, .92);
29
+ border-bottom: 1px solid rgba(156, 163, 175, .15);
30
+ -webkit-app-region: drag;
31
+ }
32
+ .titlebar .tb-title { font-size: 13px; color: #9ca3af; }
33
+ .tb-btns { display: flex; height: 100%; -webkit-app-region: no-drag; }
34
+ .tb-btn {
35
+ width: 46px; height: 100%;
36
+ display: flex; align-items: center; justify-content: center;
37
+ background: transparent; border: none;
38
+ color: #9ca3af; font-size: 13px;
39
+ font-family: "Segoe MDL2 Assets", "Segoe UI Symbol", sans-serif;
40
+ }
41
+ .tb-btn:hover { background: rgba(156, 163, 175, .2); color: #fff; }
42
+ .tb-btn.close:hover { background: #e81123; color: #fff; }
43
+ body.freedom-frameless .titlebar { display: flex; }
44
+
45
+ main {
46
+ flex: 1;
47
+ display: flex; flex-direction: column;
48
+ align-items: center; justify-content: center; gap: 14px;
49
+ }
50
+ h1 { font-size: 30px; font-weight: 700; }
51
+ .desc { color: #9ca3af; font-size: 14px; }
52
+ button {
53
+ padding: 10px 22px; border: none; border-radius: 10px;
54
+ background: #6366f1; color: #fff; font-size: 14px; cursor: pointer;
55
+ }
56
+ button:hover { background: #4f46e5; }
57
+ .result {
58
+ margin-top: 6px; padding: 8px 14px; border-radius: 8px;
59
+ background: rgba(99, 102, 241, .12); color: #a5b4fc; font-size: 13px;
60
+ }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div class="titlebar">
65
+ <div class="tb-title">Freedom App</div>
66
+ <div class="tb-btns">
67
+ <button class="tb-btn" id="tbMin">&#xE921;</button>
68
+ <button class="tb-btn" id="tbMax">&#xE922;</button>
69
+ <button class="tb-btn close" id="tbClose">&#xE8BB;</button>
70
+ </div>
71
+ </div>
72
+
73
+ <main>
74
+ <h1>Freedom App</h1>
75
+ <div class="desc">你的桌面应用已就绪。</div>
76
+ <button id="pingBtn">测试桥接</button>
77
+ <div class="result" id="result">点击上方按钮验证前后端桥接。</div>
78
+ </main>
79
+
80
+ <script type="module" src="/src/main.js"></script>
81
+ </body>
82
+ </html>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "freedom-app",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build"
9
+ },
10
+ "devDependencies": {
11
+ "vite": "^5.4.0",
12
+ "vite-plugin-singlefile": "^2.0.2"
13
+ }
14
+ }
@@ -0,0 +1,26 @@
1
+ // Freedom 前端入口。
2
+ // window.freedom 由壳层注入,无需 import。
3
+ // call(method, ...args) -> Promise 调用后端方法
4
+ // on(event, cb) / off(event, cb) 订阅 / 取消订阅后端事件
5
+ // window.minimize() / maximize() / close() 等窗口控制(无边框模式自绘按钮用)
6
+
7
+ // 无边框模式:显示自绘标题栏并绑定按钮。
8
+ if (window.freedom && window.freedom.window) {
9
+ window.freedom.window.isFrameless().then((frameless) => {
10
+ if (frameless) {
11
+ document.body.classList.add('freedom-frameless');
12
+ window.freedom.window.bindButtons({ min: '#tbMin', max: '#tbMax', close: '#tbClose' });
13
+ }
14
+ }).catch(() => {});
15
+ }
16
+
17
+ // 桥接自检。
18
+ document.getElementById('pingBtn').addEventListener('click', async () => {
19
+ const el = document.getElementById('result');
20
+ try {
21
+ const r = await window.freedom.call('__freedom__ping');
22
+ el.textContent = '桥接正常:' + r;
23
+ } catch (e) {
24
+ el.textContent = '桥接异常:' + e.message;
25
+ }
26
+ });
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vite';
2
+ import { viteSingleFile } from 'vite-plugin-singlefile';
3
+
4
+ // 单文件打包:把所有 JS/CSS 内联进一个 index.html,
5
+ // 便于 freedom CLI 嵌入 Go 壳层(内存加载,无本地端口)。
6
+ // outDir 指向临时构建区 .freedom/vite-dist,最终产物由 freedom build 统一输出到
7
+ // freedom.config.js 的 outDir(默认 dist/,可设为项目根目录)。
8
+ export default defineConfig({
9
+ plugins: [viteSingleFile()],
10
+ build: {
11
+ target: 'esnext',
12
+ outDir: '.freedom/vite-dist',
13
+ assetsInlineLimit: 100000000,
14
+ chunkSizeWarningLimit: 100000000,
15
+ cssCodeSplit: false,
16
+ reportCompressedSize: false,
17
+ },
18
+ });