forked from snsogbl/clip-save
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.go
More file actions
1125 lines (972 loc) · 30.7 KB
/
app.go
File metadata and controls
1125 lines (972 loc) · 30.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"image"
"image/png"
"io"
"log"
"net/http"
"net/url"
"os"
"os/exec"
gRuntime "runtime"
"sync"
"time"
"goWeb3/common"
"github.com/makiuchi-d/gozxing"
"github.com/makiuchi-d/gozxing/qrcode"
qrcodegen "github.com/skip2/go-qrcode"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.design/x/clipboard"
)
// App struct
type App struct {
ctx context.Context
isWindowHidden bool
isUserSetAlwaysOnTop bool // 用户是否设置了置顶
sayProcess *os.Process // 保存 say 进程对象
sayProcessMutex sync.Mutex // 保护并发访问
}
// ShowAbout 显示关于对话框
func (a *App) ShowAbout() {
if a.ctx == nil {
return
}
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.InfoDialog,
Title: common.T("app.name"),
Message: common.T("app.description") + "\n" + common.T("app.version"),
})
}
// ShowSetting 显示设置对话框
func (a *App) ShowSetting() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "nav.setting")
}
}
// RunScript 显示脚本选择器
func (a *App) RunScript() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "nav.runScript")
}
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
// 添加 panic 恢复机制
defer func() {
if r := recover(); r != nil {
log.Printf("startup 函数崩溃恢复: %v", r)
}
}()
a.ctx = ctx
log.Println("Wails 应用启动成功")
// 延迟初始化应用切换监听器,确保 NSApplication 已完全初始化
go func() {
time.Sleep(500 * time.Millisecond)
defer func() {
if r := recover(); r != nil {
log.Printf("初始化应用切换监听器失败: %v", r)
}
}()
common.InitAppSwitchListener()
}()
// 初始化统计模块 - 添加错误处理
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("初始化统计模块崩溃: %v", r)
}
}()
if err := common.InitAnalytics(); err != nil {
log.Printf("初始化统计模块失败: %v", err)
}
}()
// 延迟注册 Dock 点击激活时的自动恢复与强退标记(仅 macOS 生效,其他平台为 no-op)
// 确保 NSApplication 已完全初始化后再注册
go func() {
time.Sleep(500 * time.Millisecond)
defer func() {
if r := recover(); r != nil {
log.Printf("注册 Dock 重新打开监听失败: %v", r)
}
}()
common.InitDockReopen(func() {
a.ShowWindow()
})
}()
common.SetForceQuitCallback(func() { common.SetForceQuit() })
// 根据设置调整 Dock 图标可见性(仅 macOS 生效)
if gRuntime.GOOS == "darwin" {
go func() {
// 延迟执行,确保应用已完全启动
time.Sleep(300 * time.Millisecond)
settingsJSON, err := common.GetSetting("app_settings")
if err == nil && settingsJSON != "" {
var settings map[string]interface{}
if err := json.Unmarshal([]byte(settingsJSON), &settings); err == nil {
if backgroundMode, ok := settings["backgroundMode"].(bool); ok && backgroundMode {
// 开启后台模式:隐藏 Dock 图标
common.SetDockIconVisibility(2)
log.Println("已根据设置启用后台模式(隐藏 Dock 图标)")
}
}
}
}()
}
// 延迟调整窗口控制按钮位置,确保窗口已创建(仅 macOS 生效)
go func() {
time.Sleep(200 * time.Millisecond)
common.AdjustWindowButtons()
log.Println("已调整窗口控制按钮位置")
}()
// 设置脚本事件回调函数,用于发送事件到前端
common.SetScriptEventCallback(func(eventName string, data interface{}) {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, eventName, data)
}
})
}
// shutdown is called when the app is closing
func (a *App) shutdown(ctx context.Context) {
log.Println("Wails 应用关闭")
// 停止脚本 HTTP 服务器
if err := common.StopScriptHTTPServer(); err != nil {
log.Printf("停止脚本 HTTP 服务器失败: %v", err)
}
if err := common.CloseDB(); err != nil {
log.Printf("关闭数据库失败: %v", err)
}
// 清理窗口按钮观察者(仅 macOS 生效)
common.CleanupWindowButtonsObserver()
}
// SearchClipboardItems 搜索剪贴板项目(供前端调用)
// loadImageData: 是否加载图片数据(极简模式下需要显示图片缩略图)
func (a *App) SearchClipboardItems(isFavorite bool, keyword string, filterType string, limit int, loadImageData bool) ([]common.ClipboardItem, error) {
items, err := common.SearchClipboardItems(isFavorite, keyword, filterType, limit, loadImageData)
if err != nil {
log.Printf("搜索剪贴板项目失败: %v", err)
return []common.ClipboardItem{}, err
}
return items, nil
}
// ToggleFavorite 切换收藏状态(供前端调用)
func (a *App) ToggleFavorite(id string) (int, error) {
newVal, err := common.ToggleFavorite(id)
if err != nil {
log.Printf("切换收藏失败: %v", err)
return 0, err
}
return newVal, nil
}
// GetClipboardItems 获取剪贴板项目列表(供前端调用)
func (a *App) GetClipboardItems(limit int) ([]common.ClipboardItem, error) {
items, err := common.GetClipboardItems(limit)
if err != nil {
log.Printf("获取剪贴板项目失败: %v", err)
return []common.ClipboardItem{}, err
}
return items, nil
}
// GetClipboardItemByID 根据ID获取剪贴板项目(供前端调用)
func (a *App) GetClipboardItemByID(id string) (*common.ClipboardItem, error) {
item, err := common.GetClipboardItemByID(id)
if err != nil {
log.Printf("获取剪贴板项目失败: %v", err)
return nil, err
}
return item, nil
}
// DeleteClipboardItem 删除剪贴板项目(供前端调用)
func (a *App) DeleteClipboardItem(id string) error {
err := common.DeleteClipboardItem(id)
if err != nil {
log.Printf("删除剪贴板项目失败: %v", err)
return err
}
return nil
}
// CopyTextToClipboard 复制文本到剪贴板(供前端调用)
func (a *App) CopyTextToClipboard(text string) error {
clipboard.Write(clipboard.FmtText, []byte(text))
log.Printf("已复制文本到剪贴板: %s", text)
return nil
}
// CopyToClipboard 复制项目到剪贴板(供前端调用)
func (a *App) CopyToClipboard(id string) error {
item, err := common.GetClipboardItemByID(id)
if err != nil {
return fmt.Errorf("获取项目失败: %v", err)
}
// 根据类型复制到剪贴板
if item.ContentType == "Image" && len(item.ImageData) > 0 {
// 复制图片
clipboard.Write(clipboard.FmtImage, []byte(item.ImageData))
log.Printf("已复制图片到剪贴板: %s", id)
} else if item.ContentType == "File" && item.FilePaths != "" {
// 复制文件(不是文本,而是真实的文件 URL)
err := common.WriteFileURLs(item.FilePaths)
if err != nil {
log.Printf("复制文件失败: %v", err)
return fmt.Errorf("复制文件失败: %v", err)
}
log.Printf("已复制文件到剪贴板: %s", id)
} else {
// 复制文本
clipboard.Write(clipboard.FmtText, []byte(item.Content))
log.Printf("已复制文本到剪贴板: %s", id)
}
return nil
}
// GetStatistics 获取统计信息(供前端调用)
func (a *App) GetStatistics() (map[string]interface{}, error) {
stats, err := common.GetStatistics()
if err != nil {
log.Printf("获取统计信息失败: %v", err)
return nil, err
}
return stats, nil
}
// ClearItemsOlderThanDays 清除超过指定天数的项目(供前端调用)
func (a *App) ClearItemsOlderThanDays(days int) error {
err := common.ClearItemsOlderThanDays(days)
if err != nil {
log.Printf("清除超过 %d 天的项目失败: %v", days, err)
return err
}
return nil
}
// ClearAllItems 清除所有剪贴板项目(供前端调用)
func (a *App) ClearAllItems() error {
err := common.ClearAllItems()
if err != nil {
log.Printf("清除所有项目失败: %v", err)
return err
}
return nil
}
// SaveAppSettings 保存应用设置(供前端调用)
func (a *App) SaveAppSettings(settingsJSON string) error {
err := common.SaveSetting("app_settings", settingsJSON)
if err != nil {
log.Printf("保存应用设置失败: %v", err)
return err
}
log.Printf("已保存应用设置")
return nil
}
// GetAppSettings 获取应用设置(供前端调用)
func (a *App) GetAppSettings() (string, error) {
settings, err := common.GetSetting("app_settings")
if err != nil {
log.Printf("获取应用设置失败: %v", err)
return "", err
}
return settings, nil
}
// GetCurrentLanguage 获取当前语言(供前端调用)
func (a *App) GetCurrentLanguage() (string, error) {
return common.GetCurrentLanguage(), nil
}
// SetLanguage 设置语言(供前端调用)
func (a *App) SetLanguage(lang string) error {
err := common.SetLanguage(lang)
if err != nil {
log.Printf("设置语言失败: %v", err)
return err
}
log.Printf("语言已设置为: %s", lang)
return nil
}
// SetDockIconVisibility 设置 Dock 图标可见性(供前端调用,仅 macOS 生效)
func (a *App) SetDockIconVisibility(visible int) error {
common.SetDockIconVisibility(visible)
log.Printf("Dock 图标可见性已设置为: %d", visible)
// 设置后台模式后,确保窗口仍然显示(不自动隐藏)
if visible == 2 && a.ctx != nil {
// 延迟确保 Activation Policy 设置完成后再显示窗口
go func() {
time.Sleep(10 * time.Millisecond)
runtime.WindowShow(a.ctx)
runtime.WindowUnminimise(a.ctx)
log.Println("✅ 后台模式设置后,窗口已保持显示")
}()
}
return nil
}
// GetSupportedLanguages 获取支持的语言列表(供前端调用)
func (a *App) GetSupportedLanguages() ([]string, error) {
return common.GetSupportedLanguages(), nil
}
// VerifyPassword 验证密码(供前端调用)
func (a *App) VerifyPassword(password string) (bool, error) {
// 获取设置
settingsJSON, err := common.GetSetting("app_settings")
if err != nil {
log.Printf("获取设置失败: %v", err)
return false, err
}
if settingsJSON == "" {
// 没有设置,密码验证失败
return false, nil
}
// 解析设置
var settings map[string]interface{}
if err := json.Unmarshal([]byte(settingsJSON), &settings); err != nil {
log.Printf("解析设置失败: %v", err)
return false, err
}
// 获取存储的密码hash
storedPassword, ok := settings["password"].(string)
if !ok || storedPassword == "" {
// 没有设置密码
return false, nil
}
// 计算输入密码的hash
inputHash := hashPassword(password)
// 比较hash
isValid := inputHash == storedPassword
if isValid {
log.Println("✅ 密码验证成功")
} else {
log.Println("❌ 密码验证失败")
}
return isValid, nil
}
// hashPassword 计算密码的SHA-256哈希
func hashPassword(password string) string {
hash := sha256.Sum256([]byte(password))
return hex.EncodeToString(hash[:])
}
// OpenFileInFinder 在系统文件管理器中显示/打开文件(供前端调用)
func (a *App) OpenFileInFinder(filePath string) error {
switch gRuntime.GOOS {
case "darwin":
// macOS: Finder
cmd := exec.Command("open", "-R", filePath)
if err := cmd.Run(); err != nil {
log.Printf("在 Finder 中打开文件失败: %v", err)
return fmt.Errorf("打开文件失败: %v", err)
}
log.Printf("已在 Finder 中打开文件: %s", filePath)
return nil
case "windows":
// Windows: Explorer,/select, 展示并选中文件
// 如果是目录,则直接打开目录
if fi, err := os.Stat(filePath); err == nil && fi.IsDir() {
cmd := exec.Command("explorer", filePath)
if err := cmd.Start(); err != nil {
log.Printf("在资源管理器中打开目录失败: %v", err)
return fmt.Errorf("打开目录失败: %v", err)
}
log.Printf("已在资源管理器中打开目录: %s", filePath)
return nil
}
cmd := exec.Command("explorer", "/select,", filePath)
// 使用 Start 避免捕获 explorer 的非零退出码导致误报
if err := cmd.Start(); err != nil {
log.Printf("在资源管理器中显示文件失败: %v", err)
return fmt.Errorf("打开文件失败: %v", err)
}
log.Printf("已在资源管理器中显示文件: %s", filePath)
return nil
default:
// Linux: xdg-open 直接打开路径
cmd := exec.Command("xdg-open", filePath)
if err := cmd.Run(); err != nil {
log.Printf("在文件管理器中打开失败: %v", err)
return fmt.Errorf("打开文件失败: %v", err)
}
log.Printf("已在文件管理器中打开: %s", filePath)
return nil
}
}
// GetFileInfo 获取文件详细信息(供前端调用)
func (a *App) GetFileInfo(id string) ([]common.FileInfo, error) {
item, err := common.GetClipboardItemByID(id)
if err != nil {
return nil, fmt.Errorf("获取项目失败: %v", err)
}
if item.ContentType != "File" || item.FileInfo == "" {
return nil, fmt.Errorf("不是文件类型")
}
var fileInfos []common.FileInfo
if err := json.Unmarshal([]byte(item.FileInfo), &fileInfos); err != nil {
return nil, fmt.Errorf("解析文件信息失败: %v", err)
}
return fileInfos, nil
}
// OpenURL 在默认浏览器中打开 URL(供前端调用)
func (a *App) OpenURL(urlStr string) error {
// 尝试解码 URL(如果已经被编码)
decodedURL, err := url.QueryUnescape(urlStr)
if err != nil {
// 如果解码失败,使用原始 URL
log.Printf("URL 解码失败,使用原始 URL: %v", err)
decodedURL = urlStr
}
switch gRuntime.GOOS {
case "darwin":
cmd := exec.Command("open", decodedURL)
if err := cmd.Run(); err != nil {
log.Printf("打开 URL 失败: %v (原始: %s, 解码后: %s)", err, urlStr, decodedURL)
return fmt.Errorf("打开 URL 失败: %v", err)
}
log.Printf("已在浏览器中打开 URL: %s (原始: %s)", decodedURL, urlStr)
return nil
case "windows":
// 使用 rundll32 调起默认浏览器;用 Start 避免非零退出码误报
cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", decodedURL)
if err := cmd.Start(); err != nil {
log.Printf("在 Windows 打开 URL 失败: %v (原始: %s, 解码后: %s)", err, urlStr, decodedURL)
return fmt.Errorf("打开 URL 失败: %v", err)
}
log.Printf("已在浏览器中打开 URL: %s (原始: %s)", decodedURL, urlStr)
return nil
default:
// Linux: xdg-open
cmd := exec.Command("xdg-open", decodedURL)
if err := cmd.Run(); err != nil {
log.Printf("在 Linux 打开 URL 失败: %v (原始: %s, 解码后: %s)", err, urlStr, decodedURL)
return fmt.Errorf("打开 URL 失败: %v", err)
}
log.Printf("已在浏览器中打开 URL: %s (原始: %s)", decodedURL, urlStr)
return nil
}
}
// SayText 使用 macOS 的 say 命令播放文字(仅 macOS)
func (a *App) SayText(text string) error {
if gRuntime.GOOS != "darwin" {
return fmt.Errorf("say 命令仅在 macOS 系统上可用")
}
if text == "" {
return fmt.Errorf("文本不能为空")
}
// 先停止之前的播放(如果有)
a.StopSay()
// 启动 say 命令
cmd := exec.Command("say", text)
if err := cmd.Start(); err != nil {
log.Printf("启动播放失败: %v", err)
return fmt.Errorf("启动播放失败: %v", err)
}
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "say.started")
}
// 保存进程对象
a.sayProcessMutex.Lock()
a.sayProcess = cmd.Process
a.sayProcessMutex.Unlock()
// 在 goroutine 中等待进程结束,清除进程对象并发送事件
go func() {
cmd.Wait()
a.sayProcessMutex.Lock()
a.sayProcess = nil
a.sayProcessMutex.Unlock()
// 播放完成,发送事件通知前端
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "say.finished")
}
}()
log.Printf("已启动播放,进程 ID: %d", cmd.Process.Pid)
return nil
}
// StopSay 停止当前正在播放的 say 命令(仅 macOS)
func (a *App) StopSay() error {
if gRuntime.GOOS != "darwin" {
return fmt.Errorf("stop say 命令仅在 macOS 系统上可用")
}
a.sayProcessMutex.Lock()
process := a.sayProcess
a.sayProcess = nil // 先清除,避免重复调用
a.sayProcessMutex.Unlock()
if process == nil {
// 没有正在播放的进程
return nil
}
// 使用 Process.Kill() 终止进程(沙盒环境下可以终止自己启动的进程)
if err := process.Kill(); err != nil {
log.Printf("停止播放失败(进程可能已结束): %v", err)
return nil // 不返回错误,因为进程可能已经结束
}
// 停止成功,发送事件通知前端
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "say.stopped")
}
log.Printf("已停止播放(进程 ID: %d)", process.Pid)
return nil
}
func (a *App) IsSayPlaying() bool {
a.sayProcessMutex.Lock()
defer a.sayProcessMutex.Unlock()
return a.sayProcess != nil
}
// ShowWindow 显示并聚焦窗口(供快捷键调用)
func (a *App) ShowWindow() {
if a.ctx != nil {
// 在激活本应用之前,记录当前前台应用
common.RecordPreviousAppPID()
// 如果窗口之前是隐藏状态,需要移动到当前活动的桌面空间
runtime.WindowShow(a.ctx)
common.EnsureWindowOnCurrentScreen(a.ctx)
runtime.WindowUnminimise(a.ctx)
// 通知前端选中第一个列表项
// 使用 goroutine 异步发送事件,避免在 CGO 回调中直接调用导致信号错误
if a.isWindowHidden {
go func() {
// 短暂延迟确保窗口操作已完成
time.Sleep(50 * time.Millisecond)
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "window.show")
}
}()
}
// 清除隐藏标记
a.isWindowHidden = false
// 临时设置置顶,确保窗口获得焦点
runtime.WindowSetAlwaysOnTop(a.ctx, true)
// 如果用户没有设置置顶,延迟取消置顶
if !a.isUserSetAlwaysOnTop {
go func() {
time.Sleep(100 * time.Millisecond)
if a.ctx != nil {
runtime.WindowSetAlwaysOnTop(a.ctx, false)
}
}()
}
}
}
// SetWindowAlwaysOnTop 设置窗口置顶状态(供前端调用,同时更新全局变量)
func (a *App) SetWindowAlwaysOnTop(alwaysOnTop bool) error {
if a.ctx != nil {
a.isUserSetAlwaysOnTop = alwaysOnTop
runtime.WindowSetAlwaysOnTop(a.ctx, alwaysOnTop)
return nil
}
return fmt.Errorf("窗口上下文不存在")
}
// PrevItem 菜单:上一条
func (a *App) PrevItem() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "nav.prev")
}
}
// NextItem 菜单:下一条
func (a *App) NextItem() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "nav.next")
}
}
// ForceQuit 标记强制退出并真正退出应用
func (a *App) ForceQuit() {
if a.ctx == nil {
return
}
common.SetForceQuit()
runtime.Quit(a.ctx)
}
// SwitchLeftTab 菜单:切换列表
func (a *App) SwitchLeftTab(tab string) {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "nav.switch", tab)
}
}
// CopyCurrentItem 菜单:复制当前项
func (a *App) CopyCurrentItem() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "copy.current")
}
}
// DeleteCurrentItem 菜单:删除当前项
func (a *App) DeleteCurrentItem() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "delete.current")
}
}
// CollectCurrentItem 菜单:收藏当前项
func (a *App) CollectCurrentItem() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "collect.current")
}
}
// SearchItem 菜单:查找
func (a *App) SearchItem() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "search.item")
}
}
// EnterItem
func (a *App) EnterItem() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "enter.item")
}
}
// HideWindow 隐藏窗口
func (a *App) HideWindow() {
if a.ctx != nil {
// Windows: 最小化而不是隐藏,确保任务栏图标可见
if gRuntime.GOOS == "windows" {
// runtime.WindowMinimise(a.ctx)
} else {
a.isWindowHidden = true
// 其他平台:保持原有隐藏行为
runtime.WindowHide(a.ctx)
}
}
}
func (a *App) HideWindowAndQuit() {
if a.ctx != nil {
// Windows: 最小化而不是隐藏,确保任务栏图标可见
if gRuntime.GOOS == "windows" {
// runtime.WindowMinimise(a.ctx)
} else {
a.isWindowHidden = true
// 其他平台:保持原有隐藏行为
runtime.Hide(a.ctx)
}
}
}
func (a *App) AutoPasteCurrentItem() {
if a.ctx != nil {
go common.PasteCmdV()
}
}
// 激活应用
func (a *App) ActivatePreviousApp() {
if a.ctx != nil {
go common.ActivatePreviousApp()
}
}
// AutoPasteCurrentItemToPreviousApp 自动粘贴到之前的前台应用(直接发送到进程)
func (a *App) AutoPasteCurrentItemToPreviousApp() {
if a.ctx != nil {
go common.PasteCmdVToPreviousApp()
}
}
// SaveImagePNG 通过系统对话框将 Base64 PNG 保存到本地(供前端调用)
func (a *App) SaveImagePNG(base64Data string, suggestedName string) (string, error) {
if a.ctx == nil {
return "", fmt.Errorf("应用上下文未初始化")
}
// 生成默认文件名
now := time.Now()
pad := func(n int) string { return fmt.Sprintf("%02d", n) }
defaultName := fmt.Sprintf(
"clipboard-%d%s%s-%s%s%s.png",
now.Year(), pad(int(now.Month())), pad(now.Day()),
pad(now.Hour()), pad(now.Minute()), pad(now.Second()),
)
if suggestedName != "" {
defaultName = suggestedName
}
// 弹出保存对话框
path, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
DefaultFilename: defaultName,
Filters: []runtime.FileFilter{
{DisplayName: "PNG 图片", Pattern: "*.png"},
},
})
if err != nil {
return "", fmt.Errorf("选择保存路径失败: %v", err)
}
if path == "" {
// 用户取消
return "", nil
}
// 解码 Base64 数据
data, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "", fmt.Errorf("解码图片失败: %v", err)
}
// 写入文件
if err := os.WriteFile(path, data, 0644); err != nil {
return "", fmt.Errorf("写入文件失败: %v", err)
}
log.Printf("图片已保存到: %s", path)
return path, nil
}
// DetectQRCode 检测图片中是否包含二维码(供前端调用)
func (a *App) DetectQRCode(base64Data string) (bool, error) {
// 解码 Base64 数据
data, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return false, fmt.Errorf("解码图片失败: %v", err)
}
// 解码图片
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return false, fmt.Errorf("解码图片失败: %v", err)
}
// 将图片转换为灰度图
bmp, err := gozxing.NewBinaryBitmapFromImage(img)
if err != nil {
return false, fmt.Errorf("转换图片失败: %v", err)
}
// 创建二维码读取器
reader := qrcode.NewQRCodeReader()
// 尝试识别二维码
_, err = reader.Decode(bmp, nil)
if err != nil {
// 如果没有找到二维码,返回false
return false, nil
}
// 找到二维码
return true, nil
}
// RecognizeQRCode 识别图片中的二维码内容(供前端调用)
func (a *App) RecognizeQRCode(base64Data string) (string, error) {
// 解码 Base64 数据
data, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "", fmt.Errorf("解码图片失败: %v", err)
}
// 解码图片
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return "", fmt.Errorf("解码图片失败: %v", err)
}
// 将图片转换为灰度图
bmp, err := gozxing.NewBinaryBitmapFromImage(img)
if err != nil {
return "", fmt.Errorf("转换图片失败: %v", err)
}
// 创建二维码读取器
reader := qrcode.NewQRCodeReader()
// 尝试识别二维码
result, err := reader.Decode(bmp, nil)
if err != nil {
return "", fmt.Errorf("识别二维码失败: %v", err)
}
// 返回二维码内容
return result.GetText(), nil
}
// GenerateQRCode 生成二维码并返回Base64编码的PNG图片(供前端调用)
func (a *App) GenerateQRCode(content string, size int) (string, error) {
if content == "" {
return "", fmt.Errorf("内容不能为空")
}
// 设置默认尺寸
if size <= 0 {
size = 256
}
// 生成二维码
qr, err := qrcodegen.New(content, qrcodegen.Medium)
if err != nil {
return "", fmt.Errorf("生成二维码失败: %v", err)
}
// 转换为PNG
img := qr.Image(size)
// 编码为PNG
var buf bytes.Buffer
err = png.Encode(&buf, img)
if err != nil {
return "", fmt.Errorf("编码PNG失败: %v", err)
}
// 转换为Base64
base64Str := base64.StdEncoding.EncodeToString(buf.Bytes())
return base64Str, nil
}
// CopyImageToClipboard 将Base64编码的图片复制到剪贴板(供前端调用)
func (a *App) CopyImageToClipboard(base64Data string) error {
if base64Data == "" {
return fmt.Errorf("图片数据不能为空")
}
// 解码Base64数据
data, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return fmt.Errorf("解码图片数据失败: %v", err)
}
// 写入剪贴板
done := clipboard.Write(clipboard.FmtImage, data)
<-done // 等待写入完成
log.Printf("图片已复制到剪贴板,大小: %d bytes", len(data))
return nil
}
// TranslateCurrentItem 翻译当前项(供前端调用)
func (a *App) TranslateCurrentItem() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "translate.current")
}
}
// PlayCurrentItem 播放当前项(供前端调用)
func (a *App) PlayCurrentItem() {
if a.ctx != nil {
runtime.EventsEmit(a.ctx, "play.current")
}
}
// 添加互斥锁防止重复调用
var hotkeyRestartMutex sync.Mutex
func (a *App) RestartRegisterHotkey() error {
// 使用互斥锁防止重复调用
hotkeyRestartMutex.Lock()
defer hotkeyRestartMutex.Unlock()
log.Println("🔄 重启注册快捷键")
// 先取消当前注册的快捷键
common.UnregisterHotkey()
// 等待一小段时间确保旧快捷键完全清理
time.Sleep(100 * time.Millisecond)
// 获取设置
settingsJSON, err := common.GetSetting("app_settings")
if err != nil {
log.Printf("获取应用设置失败: %v", err)
}
// 解析设置
var settings map[string]interface{}
if err := json.Unmarshal([]byte(settingsJSON), &settings); err != nil {
log.Printf("解析应用设置失败: %v", err)
}
// 获取快捷键设置
hotkey := "Command+Option+c" // 默认快捷键
if hotkeyVal, ok := settings["hotkey"].(string); ok && hotkeyVal != "" {
hotkey = hotkeyVal
}
// 注册快捷键
if err := common.RegisterHotkey(hotkey, func() {
a.ShowWindow()
}); err != nil {
log.Printf("⚠️ 注册快捷键失败: %v", err)
return fmt.Errorf("注册快捷键失败: %v", err)
}
log.Printf("✅ 快捷键注册成功: %s", hotkey)
return nil
}
// GetAllUserScripts 获取所有用户脚本
func (a *App) GetAllUserScripts() ([]common.UserScript, error) {
return common.GetAllUserScripts()
}
// GetEnabledUserScriptsByTrigger 根据触发类型获取启用的脚本
func (a *App) GetEnabledUserScriptsByTrigger(trigger string) ([]common.UserScript, error) {
return common.GetEnabledUserScripts(trigger)
}
// UpdateUserScriptOrder 更新单个脚本顺序
func (a *App) UpdateUserScriptOrder(scriptID string, sortOrder int) error {
return common.UpdateUserScriptOrder(scriptID, sortOrder)
}
// GetUserScriptByID 根据 ID 获取脚本
func (a *App) GetUserScriptByID(id string) (*common.UserScript, error) {
return common.GetUserScriptByID(id)
}
// GetUserScriptsByIDs 根据 ID 列表批量获取脚本
func (a *App) GetUserScriptsByIDs(ids []string) ([]common.UserScript, error) {
return common.GetUserScriptsByIDs(ids)
}
// SaveUserScript 保存用户脚本
func (a *App) SaveUserScript(scriptJSON string) error {
var script common.UserScript