package services
|
|
import (
|
"fmt"
|
"os"
|
"path/filepath"
|
"sync"
|
"time"
|
"voicesnap/internal/correctioncsv"
|
"voicesnap/internal/history"
|
"voicesnap/internal/userdict"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
)
|
|
// CorrectionCSVService handles history correction CSV export and import preview.
|
type CorrectionCSVService struct {
|
history *history.Store
|
userdict *userdict.Store
|
app *application.App
|
win application.Window
|
|
mu sync.Mutex
|
sequence int64
|
previews map[string]pendingCorrectionPreview
|
}
|
|
type pendingCorrectionPreview struct {
|
rules []userdict.Replacement
|
}
|
|
// CorrectionCSVPreviewResponse is returned to the frontend after CSV preview parsing.
|
type CorrectionCSVPreviewResponse struct {
|
PreviewID string `json:"previewId"`
|
FileName string `json:"fileName"`
|
Canceled bool `json:"canceled"`
|
Summary correctioncsv.Summary `json:"summary"`
|
Items []correctioncsv.PreviewItem `json:"items"`
|
TotalSlots int `json:"totalSlots"`
|
VisibleLimit int `json:"visibleLimit"`
|
DetailsTruncated bool `json:"detailsTruncated"`
|
}
|
|
// CorrectionCSVExportResult describes a correction CSV export attempt.
|
type CorrectionCSVExportResult struct {
|
Path string `json:"path"`
|
Canceled bool `json:"canceled"`
|
NoEntries bool `json:"noEntries"`
|
}
|
|
// CorrectionCSVImportResult describes a confirmed import write.
|
type CorrectionCSVImportResult struct {
|
Added int `json:"added"`
|
}
|
|
func NewCorrectionCSVService(historyStore *history.Store, userDictStore *userdict.Store) *CorrectionCSVService {
|
return &CorrectionCSVService{
|
history: historyStore,
|
userdict: userDictStore,
|
previews: map[string]pendingCorrectionPreview{},
|
}
|
}
|
|
// SetApp sets the Wails app and parent window used for native dialogs.
|
func (s *CorrectionCSVService) SetApp(app *application.App, win application.Window) {
|
s.app = app
|
s.win = win
|
}
|
|
// ExportHistoryCorrectionCSVToFile asks the user where to save a correction CSV and writes unexported entries.
|
func (s *CorrectionCSVService) ExportHistoryCorrectionCSVToFile(scopeValue string) (CorrectionCSVExportResult, error) {
|
if s.app == nil {
|
return CorrectionCSVExportResult{}, os.ErrInvalid
|
}
|
|
scope, err := history.ParseExportScope(scopeValue)
|
if err != nil {
|
return CorrectionCSVExportResult{}, err
|
}
|
|
entries := s.history.GetUnexported(scope, time.Now(), time.Local)
|
if len(entries) == 0 {
|
return CorrectionCSVExportResult{NoEntries: true}, nil
|
}
|
|
home, _ := os.UserHomeDir()
|
dialog := s.app.Dialog.SaveFile().
|
SetMessage("导出勘误 CSV").
|
SetButtonText("导出").
|
SetDirectory(filepath.Join(home, "Downloads")).
|
SetFilename("privatevoice-correction-history-"+time.Now().Format("20060102-1504")+".csv").
|
AddFilter("CSV Files", "*.csv").
|
CanCreateDirectories(true)
|
|
if s.win != nil {
|
dialog.AttachToWindow(s.win)
|
}
|
|
path, err := dialog.PromptForSingleSelection()
|
if err != nil {
|
return CorrectionCSVExportResult{}, err
|
}
|
if path == "" {
|
return CorrectionCSVExportResult{Canceled: true}, nil
|
}
|
if filepath.Ext(path) == "" {
|
path += ".csv"
|
}
|
|
data, err := correctioncsv.BuildHistoryCSV(entries, time.Local)
|
if err != nil {
|
return CorrectionCSVExportResult{}, err
|
}
|
if err := os.WriteFile(path, data, 0600); err != nil {
|
return CorrectionCSVExportResult{}, err
|
}
|
|
timestamps := make([]int64, 0, len(entries))
|
for _, entry := range entries {
|
timestamps = append(timestamps, entry.Timestamp)
|
}
|
if _, err := s.history.MarkExported(timestamps, time.Now().UnixMilli()); err != nil {
|
return CorrectionCSVExportResult{}, err
|
}
|
|
return CorrectionCSVExportResult{Path: path}, nil
|
}
|
|
// PreviewCorrectionCSVFromFile asks the user to pick a CSV and returns a non-writing preview.
|
func (s *CorrectionCSVService) PreviewCorrectionCSVFromFile() (CorrectionCSVPreviewResponse, error) {
|
if s.app == nil {
|
return CorrectionCSVPreviewResponse{}, os.ErrInvalid
|
}
|
|
home, _ := os.UserHomeDir()
|
dialog := s.app.Dialog.OpenFile().
|
SetMessage("导入勘误表").
|
SetButtonText("选择").
|
SetDirectory(filepath.Join(home, "Downloads")).
|
AddFilter("CSV Files", "*.csv")
|
|
if s.win != nil {
|
dialog.AttachToWindow(s.win)
|
}
|
|
path, err := dialog.PromptForSingleSelection()
|
if err != nil {
|
return CorrectionCSVPreviewResponse{}, err
|
}
|
if path == "" {
|
return CorrectionCSVPreviewResponse{Canceled: true}, nil
|
}
|
|
data, err := os.ReadFile(path)
|
if err != nil {
|
return CorrectionCSVPreviewResponse{}, err
|
}
|
|
preview, err := correctioncsv.ParseAndPreviewCSV(data, s.userdict.GetAll(), correctioncsv.Options{})
|
if err != nil {
|
return CorrectionCSVPreviewResponse{}, err
|
}
|
|
previewID := s.storePreview(preview.NewRules)
|
return CorrectionCSVPreviewResponse{
|
PreviewID: previewID,
|
FileName: filepath.Base(path),
|
Summary: preview.Summary,
|
Items: preview.Items,
|
TotalSlots: preview.TotalSlots,
|
VisibleLimit: preview.VisibleLimit,
|
DetailsTruncated: preview.DetailsTruncated,
|
}, nil
|
}
|
|
// ConfirmCorrectionCSVImport appends only the previously previewed new rules.
|
func (s *CorrectionCSVService) ConfirmCorrectionCSVImport(previewID string) (CorrectionCSVImportResult, error) {
|
rules, ok := s.takePreview(previewID)
|
if !ok {
|
return CorrectionCSVImportResult{}, fmt.Errorf("correction CSV preview expired")
|
}
|
if len(rules) == 0 {
|
return CorrectionCSVImportResult{Added: 0}, nil
|
}
|
|
added, err := s.userdict.AppendNewReplacements(rules)
|
if err != nil {
|
return CorrectionCSVImportResult{}, err
|
}
|
return CorrectionCSVImportResult{Added: len(added)}, nil
|
}
|
|
func (s *CorrectionCSVService) storePreview(rules []correctioncsv.Rule) string {
|
s.mu.Lock()
|
defer s.mu.Unlock()
|
|
s.sequence++
|
id := fmt.Sprintf("%d-%d", time.Now().UnixNano(), s.sequence)
|
replacements := make([]userdict.Replacement, 0, len(rules))
|
for _, rule := range rules {
|
replacements = append(replacements, userdict.Replacement{
|
From: rule.From,
|
To: rule.To,
|
})
|
}
|
s.previews[id] = pendingCorrectionPreview{
|
rules: replacements,
|
}
|
return id
|
}
|
|
func (s *CorrectionCSVService) takePreview(id string) ([]userdict.Replacement, bool) {
|
s.mu.Lock()
|
defer s.mu.Unlock()
|
|
preview, ok := s.previews[id]
|
if !ok {
|
return nil, false
|
}
|
delete(s.previews, id)
|
return preview.rules, true
|
}
|