mirror of
https://github.com/IllusionMan1212/ffui.git
synced 2026-10-24 18:12:35 +00:00
Compare commits
5
Commits
2951ea391f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
067f2155b8
|
||
|
|
424319bf95
|
||
|
|
3ef9017031
|
||
|
|
0fb852e034
|
||
|
|
36f648fd86
|
@@ -1,6 +1,9 @@
|
||||
- [x] query ffmpeg binary for available encoders and list the ones we know
|
||||
- [-] when passing in a directory, list all video files in that directory and have the user choose which ones they want encoded
|
||||
- [ ] Scrollable viewport for the files. They currently get cut if there's too many.
|
||||
- [x] Scrollable viewport for the files. They currently get cut if there's too many.
|
||||
- [ ] CLI Option/Flag to choose the output directory for the encoded video(s)
|
||||
- [ ] The 'Encoding "XXXXX" ETA: XHXXMXXS' text gets cut instead of wrapping when the text is too long/terminal width too small. Fix
|
||||
- [ ] Show FPS, bitrate, and current file size when encoding.
|
||||
- Has to be parsed from the ffmpeg output
|
||||
- [ ] calculate total progress based on file sizes instead of counts of files for a more accurate progress percentage.
|
||||
might not do this.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/charmbracelet/bubbles/progress"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
@@ -35,8 +36,10 @@ type Model struct {
|
||||
IsDirectory bool
|
||||
Path string
|
||||
CurrentFileName string
|
||||
ViewportFocused bool
|
||||
FileCount int
|
||||
Files []File
|
||||
Viewport viewport.Model
|
||||
Spinner spinner.Model
|
||||
SingleFileProgressBar progress.Model
|
||||
SingleFileProgress float64
|
||||
@@ -92,6 +95,7 @@ func initialModel(fileInfo os.FileInfo, absolutePath string) *Model {
|
||||
Path: absolutePath,
|
||||
FileCount: 0,
|
||||
Files: make([]File, 0),
|
||||
Viewport: viewport.New(0, 0),
|
||||
Screen: Cfg,
|
||||
Spinner: s,
|
||||
SingleFileProgressBar: progress.New(progress.WithGradient("#1010ff", "#00ff00")),
|
||||
@@ -133,6 +137,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.Files = msg.files
|
||||
|
||||
return m, nil
|
||||
case tea.WindowSizeMsg:
|
||||
m.Viewport.Width = msg.Width
|
||||
m.Viewport.Height = msg.Height - lipgloss.Height(FilesScreenViewHeader(m))
|
||||
}
|
||||
|
||||
switch m.Screen {
|
||||
@@ -144,7 +151,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "ctrl+c", "esc":
|
||||
return m, tea.Quit
|
||||
case "enter", " ":
|
||||
if m.FocusIndex == 0 && key == "enter" {
|
||||
if !m.ViewportFocused && key == "enter" {
|
||||
selectAll := !every(m.Files, func(e File) bool { return e.Selected })
|
||||
|
||||
if m.ChoiceIndex == 0 {
|
||||
@@ -161,20 +168,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
return m, tea.Batch(m.Spinner.Tick, encodeVideo)
|
||||
}
|
||||
} else if m.FocusIndex != 0 {
|
||||
m.Files[m.FocusIndex-1].Selected = !m.Files[m.FocusIndex-1].Selected
|
||||
} else if m.ViewportFocused {
|
||||
m.Files[m.FocusIndex].Selected = !m.Files[m.FocusIndex].Selected
|
||||
|
||||
// Reset the choice to "Select All" button when no files are selected
|
||||
if !anyOf(m.Files, func(f File) bool { return f.Selected }) {
|
||||
m.ChoiceIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
m.SetViewportContent()
|
||||
case "g":
|
||||
m.FocusIndex = 0
|
||||
if m.ViewportFocused {
|
||||
m.FocusIndex = 0
|
||||
m.SetViewportContent()
|
||||
m.Viewport.GotoTop()
|
||||
}
|
||||
case "G":
|
||||
m.FocusIndex = len(m.Files)
|
||||
if m.ViewportFocused {
|
||||
m.FocusIndex = len(m.Files) - 1
|
||||
m.SetViewportContent()
|
||||
m.Viewport.GotoBottom()
|
||||
}
|
||||
case "left", "right", "h", "l":
|
||||
if m.FocusIndex == 0 && anyOf(m.Files, func(f File) bool { return f.Selected }) {
|
||||
if !m.ViewportFocused && anyOf(m.Files, func(f File) bool { return f.Selected }) {
|
||||
if key == "right" || key == "l" {
|
||||
m.ChoiceIndex++
|
||||
} else {
|
||||
@@ -187,17 +204,34 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.ChoiceIndex = 1
|
||||
}
|
||||
}
|
||||
case "tab", "shift+tab", "up", "down", "j", "k":
|
||||
if key == "up" || key == "shift+tab" || key == "k" {
|
||||
m.FocusIndex--
|
||||
} else {
|
||||
m.FocusIndex++
|
||||
}
|
||||
case "tab", "shift+tab":
|
||||
m.ViewportFocused = !m.ViewportFocused
|
||||
m.SetViewportContent()
|
||||
case "up", "down", "j", "k":
|
||||
if m.ViewportFocused {
|
||||
if key == "up" || key == "k" {
|
||||
m.FocusIndex--
|
||||
} else {
|
||||
m.FocusIndex++
|
||||
}
|
||||
|
||||
if m.FocusIndex >= len(m.Files)+1 {
|
||||
m.FocusIndex = 0
|
||||
} else if m.FocusIndex < 0 {
|
||||
m.FocusIndex = len(m.Files)
|
||||
if (key == "down" || key == "j") && m.FocusIndex >= m.Viewport.Height+m.Viewport.YOffset {
|
||||
m.Viewport.LineDown(1)
|
||||
}
|
||||
|
||||
if (key == "up" || key == "k") && m.FocusIndex < m.Viewport.YOffset {
|
||||
m.Viewport.LineUp(1)
|
||||
}
|
||||
|
||||
if m.FocusIndex >= len(m.Files) {
|
||||
m.Viewport.GotoTop()
|
||||
m.FocusIndex = 0
|
||||
} else if m.FocusIndex < 0 {
|
||||
m.Viewport.GotoBottom()
|
||||
m.FocusIndex = len(m.Files) - 1
|
||||
}
|
||||
|
||||
m.SetViewportContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,6 +313,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.Screen = Files
|
||||
m.FocusIndex = 0
|
||||
m.ChoiceIndex = 0
|
||||
|
||||
m.SetViewportContent()
|
||||
} else {
|
||||
m.Screen = Main
|
||||
|
||||
@@ -354,6 +390,26 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) SetViewportContent() {
|
||||
var files string
|
||||
|
||||
for i, file := range m.Files {
|
||||
selection := " "
|
||||
if file.Selected {
|
||||
selection = "x"
|
||||
}
|
||||
if m.ViewportFocused && m.FocusIndex == i {
|
||||
files += fmt.Sprintf(FocusedConfig.UnsetMarginTop().Render("[%s] %s"), selection, filepath.Base(file.Path))
|
||||
} else {
|
||||
files += fmt.Sprintf(BlurredConfig.UnsetMarginTop().Render("[%s] %s"), selection, filepath.Base(file.Path))
|
||||
}
|
||||
|
||||
files += "\n"
|
||||
}
|
||||
|
||||
m.Viewport.SetContent(files)
|
||||
}
|
||||
|
||||
func parseConfig(cfg []Config) ParsedConfig {
|
||||
vEncoder := find(cfg, "Video Encoder")
|
||||
aEncoder := find(cfg, "Audio Encoder")
|
||||
@@ -370,9 +426,8 @@ func parseConfig(cfg []Config) ParsedConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func FilesScreenView(m Model) string {
|
||||
func FilesScreenViewHeader(m Model) string {
|
||||
view := lipgloss.NewStyle().Margin(1, 0).Render("Select the files you wish to encode.")
|
||||
var files string
|
||||
var buttons string
|
||||
var selectAllBtnText string
|
||||
|
||||
@@ -393,7 +448,7 @@ func FilesScreenView(m Model) string {
|
||||
focusedStartButton = DisabledStartButton
|
||||
}
|
||||
|
||||
if m.FocusIndex == 0 {
|
||||
if !m.ViewportFocused {
|
||||
if m.ChoiceIndex == 0 {
|
||||
buttons = lipgloss.JoinHorizontal(0, FocusedSelectAllButton.Render(selectAllBtnText), blurredStartButton)
|
||||
} else {
|
||||
@@ -403,24 +458,17 @@ func FilesScreenView(m Model) string {
|
||||
buttons = lipgloss.JoinHorizontal(0, BlurredSelectAllButton.Render(selectAllBtnText), blurredStartButton)
|
||||
}
|
||||
|
||||
// TODO: files should be a scrollable grid
|
||||
for i, file := range m.Files {
|
||||
selection := " "
|
||||
if file.Selected {
|
||||
selection = "x"
|
||||
}
|
||||
if m.FocusIndex == i+1 {
|
||||
files += fmt.Sprintf(FocusedConfig.Render("[%s] %s"), selection, filepath.Base(file.Path))
|
||||
} else {
|
||||
files += fmt.Sprintf(BlurredConfig.Render("[%s] %s"), selection, filepath.Base(file.Path))
|
||||
}
|
||||
}
|
||||
|
||||
view += lipgloss.JoinVertical(0, buttons, files)
|
||||
view += buttons
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func FilesScreenView(m Model) string {
|
||||
// TODO: files should be a grid (?)
|
||||
|
||||
return lipgloss.JoinVertical(0, FilesScreenViewHeader(m), m.Viewport.View())
|
||||
}
|
||||
|
||||
func CfgScreenView(m Model) string {
|
||||
view := ""
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ require (
|
||||
github.com/charmbracelet/bubbles v0.10.3
|
||||
github.com/charmbracelet/bubbletea v0.23.2
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.8
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -22,9 +22,9 @@ require (
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.14.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
)
|
||||
|
||||
@@ -14,8 +14,8 @@ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0
|
||||
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
|
||||
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
@@ -46,25 +46,26 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -125,7 +125,7 @@ func encode(file File, fileName string, teaP *tea.Program, cfg ParsedConfig) {
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(mType.String(), "video/") {
|
||||
log.Fatalf("%s is not a valid video file\n", fileName)
|
||||
log.Fatalf("%s is not a valid video file. It is %v\n", fileName, mType)
|
||||
}
|
||||
|
||||
parentDir := filepath.Dir(file.Path)
|
||||
|
||||
@@ -42,6 +42,7 @@ func TempSock(totalDuration float64, teaP *tea.Program) string {
|
||||
go func() {
|
||||
re := regexp.MustCompile(`out_time_ms=(\d+)`)
|
||||
speedRe := regexp.MustCompile(`speed=(\d+\.\d+x)`)
|
||||
totalSizeRe := regexp.MustCompile(`total_size=(\d+)`)
|
||||
|
||||
fd, err := l.Accept()
|
||||
if err != nil {
|
||||
@@ -65,6 +66,12 @@ func TempSock(totalDuration float64, teaP *tea.Program) string {
|
||||
cp = fmt.Sprintf("%.2f", float64(c)/totalDuration/1000000)
|
||||
}
|
||||
if strings.Contains(data, "progress=end") {
|
||||
totalSize := totalSizeRe.FindString(data)
|
||||
if len(totalSize) > 0 && strings.Split(totalSize, "=")[1] == "0" {
|
||||
// Command failed. Don't send a finishedEncodingVideo message.
|
||||
// encode() will take care of reporting the error.
|
||||
continue
|
||||
}
|
||||
cp = "done"
|
||||
teaP.Send(finishedEncodingVideo{})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user