Files
sfxkeeb/fileservice.go
end 45075b3328 feat(frontend): use Wails bindings for file I/O
Replace hidden file inputs and anchor downloads with native open/save
dialogs via a thin platform layer. WAV export sends base64 audio data to
the Go backend for writing to the user-chosen path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 22:23:35 -07:00

168 lines
3.3 KiB
Go

package main
import (
"encoding/base64"
"net/http"
"os"
"path/filepath"
"sync"
"github.com/wailsapp/wails/v3/pkg/application"
)
const localMediaVideoPath = "/localmedia/video"
// VideoResult is returned when the user opens a video file.
type VideoResult struct {
URL string `json:"url"`
Name string `json:"name"`
}
// FileService exposes native file dialogs and streams the current video.
type FileService struct {
app *application.App
mu sync.RWMutex
videoPath string
videoMod os.FileInfo
}
func NewFileService(app *application.App) *FileService {
return &FileService{app: app}
}
func (s *FileService) OpenVideo() (VideoResult, error) {
path, err := s.app.Dialog.OpenFile().
SetTitle("Open Video").
CanChooseFiles(true).
AddFilter("Video", "*.mp4;*.mov;*.webm;*.mkv").
AddFilter("All Files", "*").
PromptForSingleSelection()
if err != nil {
return VideoResult{}, err
}
if path == "" {
return VideoResult{}, nil
}
info, err := os.Stat(path)
if err != nil {
return VideoResult{}, err
}
s.mu.Lock()
s.videoPath = path
s.videoMod = info
s.mu.Unlock()
return VideoResult{
URL: localMediaVideoPath,
Name: filepath.Base(path),
}, nil
}
func (s *FileService) OpenProject() (string, error) {
path, err := s.app.Dialog.OpenFile().
SetTitle("Open Project").
CanChooseFiles(true).
AddFilter("JSON", "*.json").
AddFilter("All Files", "*").
PromptForSingleSelection()
if err != nil {
return "", err
}
if path == "" {
return "", nil
}
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
}
func (s *FileService) SaveProject(content string, defaultName string) error {
if defaultName == "" {
defaultName = "project.json"
}
path, err := s.app.Dialog.SaveFile().
SetMessage("Save Project").
SetFilename(defaultName).
AddFilter("JSON", "*.json").
AddFilter("All Files", "*").
PromptForSingleSelection()
if err != nil {
return err
}
if path == "" {
return nil
}
return os.WriteFile(path, []byte(content), 0o644)
}
func (s *FileService) SaveAudio(dataBase64 string, defaultName string) error {
if defaultName == "" {
defaultName = "keyboard_track.wav"
}
data, err := base64.StdEncoding.DecodeString(dataBase64)
if err != nil {
return err
}
path, err := s.app.Dialog.SaveFile().
SetMessage("Export Audio").
SetFilename(defaultName).
AddFilter("WAV", "*.wav").
AddFilter("All Files", "*").
PromptForSingleSelection()
if err != nil {
return err
}
if path == "" {
return nil
}
return os.WriteFile(path, data, 0o644)
}
func (s *FileService) serveVideo(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
s.mu.RLock()
path := s.videoPath
modTime := s.videoMod
s.mu.RUnlock()
if path == "" {
http.NotFound(w, r)
return
}
file, err := os.Open(path)
if err != nil {
http.Error(w, "video unavailable", http.StatusNotFound)
return
}
defer file.Close()
info, err := file.Stat()
if err != nil {
http.Error(w, "video unavailable", http.StatusNotFound)
return
}
if modTime != nil && !info.ModTime().Equal(modTime.ModTime()) {
s.mu.Lock()
s.videoMod = info
s.mu.Unlock()
}
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
}