package app import ( "fmt" "io/fs" "log" "os" "github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/ttf" ) const messageDisplayTime = uint64(3000) type Viewer struct { FileCollection *FileCollection Image *ViewerImage Input *InputHandler currentIndex int currentFrame int currentIndexTime uint64 lastFrameUpdate uint64 messageSurface *sdl.Surface messageTick uint64 error error activeGroup int groupIndexes map[int]int } func (v *Viewer) FilteredFiles() []*FileInfo { if v.activeGroup == 0 { return v.FileCollection.Files } var filtered []*FileInfo for _, f := range v.FileCollection.Files { if f.Group == v.activeGroup { filtered = append(filtered, f) } } return filtered } func (v *Viewer) File() *FileInfo { files := v.FilteredFiles() if len(files) == 0 { return nil } return files[v.currentIndex] } func (v *Viewer) saveGroupIndex() { v.groupIndexes[v.activeGroup] = v.currentIndex } func (v *Viewer) loadGroupIndex(group int) int { if idx, ok := v.groupIndexes[group]; ok { files := v.getGroupFiles(group) if idx < 0 || idx >= len(files) { return 0 } return idx } return 0 } func (v *Viewer) getGroupFiles(group int) []*FileInfo { if group == 0 { return v.FileCollection.Files } var filtered []*FileInfo for _, f := range v.FileCollection.Files { if f.Group == group { filtered = append(filtered, f) } } return filtered } func (v *Viewer) switchImage() { v.SetError(nil) v.currentIndexTime = sdl.GetTicks64() v.lastFrameUpdate = 0 if v.Image != nil { v.Image.Free() v.Image = nil } if v.messageSurface != nil { v.messageSurface.Free() v.messageSurface = nil } } func (v *Viewer) Goto(index int) { v.saveGroupIndex() v.currentIndex = index v.switchImage() } func (v *Viewer) Next() { files := v.FilteredFiles() if len(files) == 0 { return } v.Goto((v.currentIndex + 1) % len(files)) } func (v *Viewer) Previous() { files := v.FilteredFiles() if len(files) == 0 { return } if v.currentIndex-1 < 0 { v.Goto(len(files) - 1) return } v.Goto((v.currentIndex - 1) % len(files)) } func (v *Viewer) SetError(err error) { v.error = err if err != nil { log.Println(" - ERROR:", err) v.RenderMessage(fmt.Sprintf("ERROR: %s", err.Error())) } } func (v *Viewer) setActiveGroup(group int) { if group == v.activeGroup { group = 0 } v.saveGroupIndex() v.activeGroup = group idx := v.loadGroupIndex(group) v.currentIndex = idx v.switchImage() if group == 0 { v.RenderMessage("All files") } else { files := v.FilteredFiles() msg := fmt.Sprintf("%s (%d)", GroupName(group), len(files)) v.RenderMessage(msg) } } func (v *Viewer) handleGroupHold(group int) { fileInfo := v.File() if fileInfo == nil { return } if fileInfo.Group == group { if err := v.FileCollection.RemoveGroup(fileInfo); err != nil { v.SetError(err) return } v.RenderMessage(fmt.Sprintf("Removed from %s", GroupName(group))) } else { if err := v.FileCollection.SaveGroup(fileInfo, group); err != nil { v.SetError(err) return } v.RenderMessage(fmt.Sprintf("Added to %s", GroupName(group))) } files := v.FilteredFiles() if v.currentIndex < 0 || v.currentIndex >= len(files) { if len(files) > 0 { v.currentIndex = len(files) - 1 v.switchImage() } } } func (v *Viewer) Destroy() { if v.Image != nil { v.Image.Free() } if v.messageSurface != nil { v.messageSurface.Free() } } func Run(directory string) error { fsys := os.DirFS(directory) dbPath := "" return RunViewer(fsys, dbPath) } func RunViewer(fsys fs.FS, dbPath string) error { config, err := LoadConfig() if err != nil { return err } fileCollection, err := LoadFileCollection(fsys, dbPath) if err != nil { return err } defer fileCollection.Close() if err := sdl.Init(sdl.INIT_VIDEO | sdl.INIT_EVENTS); err != nil { return err } defer sdl.Quit() if err := ttf.Init(); err != nil { return err } defer ttf.Quit() displayMode, err := sdl.GetCurrentDisplayMode(0) if err != nil { return err } window, err := sdl.CreateWindow("NView", 0, 0, displayMode.W, displayMode.H, sdl.WINDOW_BORDERLESS|sdl.WINDOW_FULLSCREEN_DESKTOP) if err != nil { return err } defer window.Destroy() surface, err := window.GetSurface() if err != nil { return err } defer surface.Free() viewer := Viewer{ FileCollection: fileCollection, Input: NewInputHandler(config.Keybinds), groupIndexes: make(map[int]int), } viewer.Input.AttachCallback(InputNextFile, viewer.Next) viewer.Input.AttachCallback(InputPreviousFile, viewer.Previous) viewer.Input.AttachCallback(InputMinimize, window.Minimize) viewer.Input.AttachCallback(InputClearFilter, func() { viewer.setActiveGroup(0) }) for g := 1; g <= 9; g++ { group := g action := InputAction(fmt.Sprintf("group_%d", g)) viewer.Input.AttachCallback(action, func() { viewer.setActiveGroup(group) }) viewer.Input.AttachHoldCallback(action, func() { viewer.handleGroupHold(group) }) } running := true for running { for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { switch event.(type) { case *sdl.QuitEvent: running = false } viewer.Input.HandleEvent(event) } viewer.Input.Check() viewer.Blit(surface) window.UpdateSurface() sdl.Delay(1000 / 60) } return nil } func (v *Viewer) Blit(surface *sdl.Surface) { currentTick := sdl.GetTicks64() if v.messageSurface != nil && v.messageTick > 0 && currentTick-v.messageTick >= messageDisplayTime { v.messageSurface.Free() v.messageSurface = nil v.messageTick = 0 } if v.error != nil { if v.messageSurface != nil { if err := surface.FillRect(nil, 0); err != nil { v.SetError(err) return } textRect := getCenteredRect(v.messageSurface.W, v.messageSurface.H, surface.W, surface.H) v.messageSurface.Blit(nil, surface, &textRect) } return } files := v.FilteredFiles() if len(files) == 0 { v.SetError(errNoFiles) return } if v.currentIndex < 0 || v.currentIndex >= len(files) { v.currentIndex = 0 v.switchImage() } if v.Image == nil { v.RenderViewerImage() v.currentFrame = 0 if err := surface.FillRect(nil, 0); err != nil { v.SetError(err) return } } if v.Image != nil { currentTick := sdl.GetTicks64() frame := v.Image.Frames[v.currentFrame] if v.lastFrameUpdate == 0 || v.lastFrameUpdate+uint64(frame.Duration) < currentTick { v.lastFrameUpdate = sdl.GetTicks64() v.currentFrame = (v.currentFrame + 1) % len(v.Image.Frames) imageRect := getObjectScaledRect(v.Image.Width, v.Image.Height, surface.W, surface.H) if err := frame.Surface.BlitScaled(nil, surface, &imageRect); err != nil { v.SetError(err) return } } } if v.error == nil && v.messageSurface != nil { if err := v.messageSurface.Blit(nil, surface, &sdl.Rect{X: 15, Y: 15}); err != nil { v.SetError(err) } } }