chore: migrate and structure cmbone project

This commit is contained in:
QiuSW
2026-07-08 15:13:26 +08:00
parent 519a532038
commit e5d10577c0
123 changed files with 9076 additions and 25 deletions
+52
View File
@@ -0,0 +1,52 @@
package services
type AppConfigService struct {
store *SuiStore
}
// 创建新的AppConfigService实例
func NewAppConfigService(store *SuiStore) *AppConfigService {
return &AppConfigService{
store: store,
}
}
// 根据key获取配置项
func (r *AppConfigService) GetAppConfig(key string) (string, error) {
row := r.store.DB.QueryRow(`
SELECT value
FROM appconfig
WHERE key = ?
`, key)
var value string
err := row.Scan(&value)
if err != nil {
return "en", err
}
return value, nil
}
// 设置或更新配置项
func (r *AppConfigService) SetAppConfig(key, value string) error {
_, err := r.store.DB.Exec(`
INSERT INTO appconfig (key, type, value, description)
VALUES (?, 'user', ?, '')
ON CONFLICT(key) DO UPDATE SET value=excluded.value
`, key, value)
return err
}
// 获取应用语言
func (s *AppConfigService) GetLanguage() string {
lang, err := s.GetAppConfig("language")
if err != nil || lang == "" {
return "en"
}
return lang
}
// 设置应用语言
func (s *AppConfigService) SetLanguage(lang string) error {
return s.SetAppConfig("language", lang)
}
+164
View File
@@ -0,0 +1,164 @@
package services
import (
"cmbone/internal/models"
"embed"
"encoding/json"
"fmt"
"log"
"runtime"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
type AppService struct {
App *application.App
SecondWindow *application.WebviewWindow
MainWindow *application.WebviewWindow
Config *AppConfigService
Tray *application.SystemTray
I18nFS embed.FS
Menu *application.Menu
MenuQuit *application.MenuItem
MenuSetting *application.MenuItem
MenuSecondBar *application.MenuItem
MenuVersion *application.MenuItem
}
const HotkeyShowSecondWindow = 1
func ConfigureAppService(a *AppService, app *application.App, i18nFS embed.FS, suidb *SuiStore) {
a.App = app
a.I18nFS = i18nFS
a.Config = NewAppConfigService(suidb)
isWindows := runtime.GOOS == "windows"
mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "main",
Frameless: isWindows,
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInset,
},
Windows: application.WindowsWindow{
DisableIcon: true,
},
URL: "/",
})
secondWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "second",
Name: "second",
Width: 340,
Height: 320,
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
TitleBar: application.MacTitleBarHidden,
Backdrop: application.MacBackdropTransparent,
},
BackgroundColour: application.NewRGB(27, 38, 54),
URL: "/#/second",
})
secondWindow.Hide()
a.SecondWindow = secondWindow
a.MainWindow = mainWindow
RegisterWindowAndCallback(HotkeyShowSecondWindow, a.SecondWindow, func() {
if a.SecondWindow == nil {
return
}
a.SecondWindow.Show()
a.SecondWindow.Focus()
})
a.MainWindow.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
a.MainWindow.Hide()
e.Cancel()
})
a.SecondWindow.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
a.SecondWindow.Hide()
e.Cancel()
})
initTray(a, NewAppConfigService(suidb).GetLanguage())
}
func (a *AppService) OpenSecondWindow() {
if a.SecondWindow != nil {
a.SecondWindow.Show()
}
}
func initTray(a *AppService, langValue string) {
a.Tray = a.App.NewSystemTray()
a.Tray.SetLabel("cmbone")
loadAppMenu(a, langValue)
}
func loadLang(i18nemb embed.FS, lang string) (*models.Lang, error) {
file := fmt.Sprintf("frontend/src/locales/%s.json", lang)
data, err := i18nemb.ReadFile(file)
if err != nil {
return nil, err
}
var i18nva models.Lang
err = json.Unmarshal(data, &i18nva)
return &i18nva, err
}
func loadAppMenu(a *AppService, langValue string) {
lang, langerr := loadLang(a.I18nFS, langValue)
if langerr != nil {
log.Printf("load language %q failed: %v", langValue, langerr)
return
}
a.Menu = application.NewMenu()
a.MenuSecondBar = a.Menu.Add(lang.AppMenu.Second).OnClick(func(ctx *application.Context) {
a.SecondWindow.Show()
a.SecondWindow.Focus()
})
a.MenuSetting = a.Menu.Add(lang.AppMenu.Preferences).OnClick(func(ctx *application.Context) {
a.MainWindow.Show()
a.MainWindow.Focus()
})
a.Menu.AddSeparator()
item := a.Menu.Add(lang.AppMenu.Version)
item.SetEnabled(false)
a.MenuVersion = item
a.Menu.AddSeparator()
a.MenuQuit = a.Menu.Add(lang.AppMenu.Quit).OnClick(func(ctx *application.Context) {
a.App.Quit()
})
a.Tray.SetMenu(a.Menu)
}
func updateAppMenu(a *AppService, langValue string) {
lang, langerr := loadLang(a.I18nFS, langValue)
if langerr != nil {
log.Printf("load language %q failed: %v", langValue, langerr)
return
}
a.MenuSecondBar.SetLabel(lang.AppMenu.Second)
a.MenuSetting.SetLabel(lang.AppMenu.Preferences)
a.MenuVersion.SetLabel(lang.AppMenu.Version)
a.MenuQuit.SetLabel(lang.AppMenu.Quit)
if runtime.GOOS == "darwin" {
a.Menu.Update()
} else {
a.Tray.SetMenu(a.Menu)
}
}
func (s *AppService) SetLanguage(lang string) error {
current := s.Config.GetLanguage()
if current == lang {
return nil
}
updateAppMenu(s, lang)
return s.Config.SetLanguage(lang)
}
+122
View File
@@ -0,0 +1,122 @@
package services
import (
"cmbone/internal/models"
hotkey "cmbone/platform"
"log"
"sync"
"github.com/wailsapp/wails/v3/pkg/application"
)
type HotkeyService struct {
store *SuiStore
}
func NewHotkeyService(store *SuiStore) *HotkeyService {
return &HotkeyService{
store: store,
}
}
func (cs *HotkeyService) UpHotkey(id int, key int, modifier int) error {
var oldKey, oldModifier uint32
if err := cs.store.DB.QueryRow("SELECT keycode, modifiers FROM hotkeys WHERE id = ?", id).Scan(&oldKey, &oldModifier); err != nil {
return err
}
_, err := cs.store.DB.Exec(`
UPDATE hotkeys
SET keycode = ?, modifiers = ?
WHERE id = ?
`, key, modifier, id)
if err == nil {
LoadAndRegisterHotkeysFrom(cs.store, id)
hotkey.UnregisterHotKey(oldKey, oldModifier)
}
return err
}
func (cs *HotkeyService) GetHotkeys() ([]models.Hotkey, error) {
rows, err := cs.store.DB.Query("SELECT id, keycode, modifiers FROM hotkeys")
if err != nil {
return nil, err
}
defer rows.Close()
var hotkeys []models.Hotkey
for rows.Next() {
var hk models.Hotkey
if err := rows.Scan(&hk.ID, &hk.KeyCode, &hk.Modifiers); err != nil {
return nil, err
}
hotkeys = append(hotkeys, hk)
}
if err := rows.Err(); err != nil {
return nil, err
}
return hotkeys, nil
}
type hotkeyEntry struct {
ID int
Hotkey models.Hotkey
Window application.Window
Callback func()
}
var (
hotkeys = make(map[int]*hotkeyEntry)
mutex sync.RWMutex
)
func RegisterHotkeyEntry(id int, keycode, modifiers uint32, win application.Window, cb func()) {
mutex.Lock()
defer mutex.Unlock()
hotkeys[id] = &hotkeyEntry{
ID: id,
Hotkey: models.Hotkey{
KeyCode: keycode,
Modifiers: modifiers,
},
Window: win,
Callback: cb,
}
hotkey.RegisterHotKeyWithCallback(keycode, modifiers, cb)
}
func LoadAndRegisterHotkeysFrom(m *SuiStore, id int) {
var hk models.Hotkey
err := m.DB.QueryRow("SELECT keycode, modifiers FROM hotkeys where id = ?", id).Scan(&hk.KeyCode, &hk.Modifiers)
if err != nil {
log.Printf("load hotkey %d failed: %v", id, err)
return
}
mutex.RLock()
entry, ok := hotkeys[id]
mutex.RUnlock()
if !ok {
log.Printf("hotkey %d has no registered window callback", id)
return
}
hotkey.UnregisterHotKey(hk.KeyCode, hk.Modifiers)
RegisterHotkeyEntry(id, hk.KeyCode, hk.Modifiers, entry.Window, entry.Callback)
}
func RegisterWindowAndCallback(id int, win application.Window, cb hotkey.HotKeyCallback) {
mutex.Lock()
hotkeys[id] = &hotkeyEntry{
ID: id,
Hotkey: models.Hotkey{
KeyCode: 0,
Modifiers: 0,
},
Window: win,
Callback: cb,
}
mutex.Unlock()
}
+12
View File
@@ -0,0 +1,12 @@
package services
import (
"cmbone/platform"
)
type OCRService struct{}
// 识别Base64图片(data:image/png;base64,...)
func (o *OCRService) RecognizeImageBase64(base64str string) (string, error) {
return platform.RecognizeImageBase64(base64str)
}
+99
View File
@@ -0,0 +1,99 @@
package services
import (
"database/sql"
"os"
"path/filepath"
_ "modernc.org/sqlite"
)
const (
appConfigDirName = "cmbone"
databaseFileName = "cmbone.db"
)
type SuiStore struct {
DB *sql.DB
}
func getSafeDBPath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
appDir := filepath.Join(configDir, appConfigDirName)
err = os.MkdirAll(appDir, 0755)
if err != nil {
return "", err
}
return filepath.Join(appDir, databaseFileName), nil
}
func NewSuiStore() (*SuiStore, error) {
dbPath, err := getSafeDBPath()
if err != nil {
return nil, err
}
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, err
}
err = Migrate(db)
if err != nil {
_ = db.Close()
return nil, err
}
return &SuiStore{DB: db}, nil
}
func (cs *SuiStore) Close() {
if cs != nil && cs.DB != nil {
_ = cs.DB.Close()
}
}
func (cs *SuiStore) Start() error {
// 这里可以初始化数据库或其它启动逻辑
return nil
}
func (cs *SuiStore) Stop() error {
cs.Close()
return nil
}
func Migrate(db *sql.DB) error {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS hotkeys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keycode INTEGER NOT NULL,
modifiers INTEGER NOT NULL,
description TEXT,
target TEXT
);
CREATE TABLE IF NOT EXISTS appconfig (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE,
type TEXT,
value TEXT,
description TEXT,
state INTEGER DEFAULT 0
);
`)
if err != nil {
return err
}
return initDefaultData(db)
}
func initDefaultData(db *sql.DB) error {
if _, err := db.Exec(defaultAppConfigSQL()); err != nil {
return err
}
if _, err := db.Exec(defaultHotkeysSQL()); err != nil {
return err
}
return nil
}
+64
View File
@@ -0,0 +1,64 @@
package services
import (
"database/sql"
"testing"
)
func newTestStore(t *testing.T) *SuiStore {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() {
_ = db.Close()
})
if err := Migrate(db); err != nil {
t.Fatalf("migrate: %v", err)
}
return &SuiStore{DB: db}
}
func TestMigrateCreatesDefaultDataOnce(t *testing.T) {
store := newTestStore(t)
if err := Migrate(store.DB); err != nil {
t.Fatalf("second migrate: %v", err)
}
var hotkeyCount int
if err := store.DB.QueryRow("SELECT COUNT(*) FROM hotkeys").Scan(&hotkeyCount); err != nil {
t.Fatalf("count hotkeys: %v", err)
}
if hotkeyCount != 2 {
t.Fatalf("hotkey count = %d, want 2", hotkeyCount)
}
var lang string
if err := store.DB.QueryRow("SELECT value FROM appconfig WHERE key = 'language'").Scan(&lang); err != nil {
t.Fatalf("query language: %v", err)
}
if lang != "zh" {
t.Fatalf("language = %q, want zh", lang)
}
}
func TestAppConfigServiceLanguage(t *testing.T) {
store := newTestStore(t)
service := NewAppConfigService(store)
if got := service.GetLanguage(); got != "zh" {
t.Fatalf("default language = %q, want zh", got)
}
if err := service.SetLanguage("en"); err != nil {
t.Fatalf("set language: %v", err)
}
if got := service.GetLanguage(); got != "en" {
t.Fatalf("updated language = %q, want en", got)
}
}
+44
View File
@@ -0,0 +1,44 @@
package services
import (
"runtime"
)
type SystemInfo struct{}
func NewSystemInfo() *SystemInfo {
return &SystemInfo{}
}
func (s *SystemInfo) GetOS() string {
return runtime.GOOS // 返回 "darwin" / "windows" / "linux"
}
func defaultHotkeysSQL() string {
var sqlhotkeys = `
INSERT INTO hotkeys (keycode, modifiers, description, target)
SELECT 80, 1, '剪贴板窗口快捷键', '1'
WHERE NOT EXISTS (SELECT 1 FROM hotkeys WHERE target = '1');
INSERT INTO hotkeys (keycode, modifiers, description, target)
SELECT 77, 1, '偏好设置快捷键 ', '2'
WHERE NOT EXISTS (SELECT 1 FROM hotkeys WHERE target = '2'); `
if runtime.GOOS == "darwin" {
sqlhotkeys = `
INSERT INTO hotkeys (keycode, modifiers, description, target)
SELECT 34, 768, '剪贴板窗口快捷键', '1'
WHERE NOT EXISTS (SELECT 1 FROM hotkeys WHERE target = '1');
INSERT INTO hotkeys (keycode, modifiers, description, target)
SELECT 46, 768, '偏好设置快捷键 ', '2'
WHERE NOT EXISTS (SELECT 1 FROM hotkeys WHERE target = '2'); `
}
return sqlhotkeys
}
func defaultAppConfigSQL() string {
return `
INSERT OR IGNORE INTO appconfig (key, type, value, description)
VALUES ('language', 'system', 'zh', '应用语言,支持 zh/en');`
}