feat(wails): add native file dialogs and video streaming
FileService exposes open/save dialogs for video, project JSON, and WAV export. User-selected videos stream from disk at /localmedia/video with range request support for seeking in the webview. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"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(data []byte, defaultName string) error {
|
||||
if defaultName == "" {
|
||||
defaultName = "keyboard_track.wav"
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user