diff --git a/fileservice.go b/fileservice.go new file mode 100644 index 0000000..61f14d8 --- /dev/null +++ b/fileservice.go @@ -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) +} diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts new file mode 100644 index 0000000..1ea1058 --- /dev/null +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts @@ -0,0 +1,9 @@ +//@ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +Object.freeze($Create.Events); diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts new file mode 100644 index 0000000..3dd1807 --- /dev/null +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -0,0 +1,2 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT diff --git a/frontend/bindings/sfxkeeb/fileservice.ts b/frontend/bindings/sfxkeeb/fileservice.ts new file mode 100644 index 0000000..3a02264 --- /dev/null +++ b/frontend/bindings/sfxkeeb/fileservice.ts @@ -0,0 +1,36 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * FileService exposes native file dialogs and streams the current video. + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +export function OpenProject(): $CancellablePromise { + return $Call.ByID(2194602340); +} + +export function OpenVideo(): $CancellablePromise<$models.VideoResult> { + return $Call.ByID(392322154).then(($result: any) => { + return $$createType0($result); + }); +} + +export function SaveAudio(data: string, defaultName: string): $CancellablePromise { + return $Call.ByID(1785178400, data, defaultName); +} + +export function SaveProject(content: string, defaultName: string): $CancellablePromise { + return $Call.ByID(2685219959, content, defaultName); +} + +// Private type creation functions +const $$createType0 = $models.VideoResult.createFrom; diff --git a/frontend/bindings/sfxkeeb/index.ts b/frontend/bindings/sfxkeeb/index.ts new file mode 100644 index 0000000..6a55141 --- /dev/null +++ b/frontend/bindings/sfxkeeb/index.ts @@ -0,0 +1,11 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as FileService from "./fileservice.js"; +export { + FileService +}; + +export { + VideoResult +} from "./models.js"; diff --git a/frontend/bindings/sfxkeeb/models.ts b/frontend/bindings/sfxkeeb/models.ts new file mode 100644 index 0000000..5fac6ab --- /dev/null +++ b/frontend/bindings/sfxkeeb/models.ts @@ -0,0 +1,34 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +/** + * VideoResult is returned when the user opens a video file. + */ +export class VideoResult { + "url": string; + "name": string; + + /** Creates a new VideoResult instance. */ + constructor($$source: Partial = {}) { + if (!("url" in $$source)) { + this["url"] = ""; + } + if (!("name" in $$source)) { + this["name"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new VideoResult instance from a string or object. + */ + static createFrom($$source: any = {}): VideoResult { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new VideoResult($$parsedSource as Partial); + } +} diff --git a/main.go b/main.go index ca8cdac..1412ae4 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "embed" "log" + "net/http" "github.com/wailsapp/wails/v3/pkg/application" ) @@ -11,21 +12,35 @@ import ( var assets embed.FS func main() { + var fileService *FileService + app := application.New(application.Options{ Name: "sfxkeeb", Description: "Annotate keyboard key presses on a video timeline", Assets: application.AssetOptions{ Handler: application.AssetFileServerFS(assets), + Middleware: func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == localMediaVideoPath && fileService != nil { + fileService.serveVideo(w, r) + return + } + next.ServeHTTP(w, r) + }) + }, }, Mac: application.MacOptions{ ApplicationShouldTerminateAfterLastWindowClosed: true, }, }) + fileService = NewFileService(app) + app.RegisterService(application.NewService(fileService)) + app.Window.NewWithOptions(application.WebviewWindowOptions{ - Title: "sfxkeeb", - Width: 1280, - Height: 900, + Title: "sfxkeeb", + Width: 1280, + Height: 900, MinWidth: 960, MinHeight: 600, Mac: application.MacWindow{