feat(author): Implement real-time progress, add to queue, clear title
This commit introduces several enhancements to the Author module: - **Real-time Progress Reporting:** Implemented granular, real-time progress updates for FFmpeg encoding steps during DVD authoring. The progress bar now updates smoothly, reflecting the actual video processing. Progress calculation is weighted by video durations for accuracy. - **Add to Queue Functionality:** Added an 'Add to Queue' button to the Author module, allowing users to queue authoring jobs for later execution without immediate start. The authoring workflow was refactored to accept a 'startNow' parameter for this purpose. - **Clear Output Title:** Modified the 'Clear All' functionality to also reset the DVD Output Title, preventing accidental naming conflicts for new projects. Additionally, this commit includes a UI enhancement: - **Main Menu Categorization:** Relocated 'Author', 'Rip', and 'Blu-Ray' modules to a new 'Disc' category on the main menu, improving logical grouping. Fixes: - Corrected a missing argument error in a call to . - Added missing import in . Updates: - and have been updated to reflect these changes.
This commit is contained in:
parent
0193886676
commit
c8f4eec0d1
40
DONE.md
40
DONE.md
|
|
@ -2,6 +2,27 @@
|
|||
|
||||
This file tracks completed features, fixes, and milestones.
|
||||
|
||||
## Version 0.1.0-dev20+ (2025-12-26) - Author Module & UI Enhancements
|
||||
|
||||
### Features
|
||||
- ✅ **Author Module - Real-time Progress Reporting**
|
||||
- Implemented granular progress updates for FFmpeg encoding steps in the Author module.
|
||||
- Progress bar now updates smoothly during video processing, providing better feedback.
|
||||
- Weighted progress calculation based on video durations for accurate overall progress.
|
||||
|
||||
- ✅ **Author Module - "Add to Queue" & Output Title Clear**
|
||||
- Added an "Add to Queue" button to the Author module for non-immediate job execution.
|
||||
- Refactored authoring workflow to support queuing jobs via a `startNow` parameter.
|
||||
- Modified "Clear All" functionality to also clear the DVD Output Title, preventing naming conflicts.
|
||||
|
||||
- ✅ **Main Menu - "Disc" Category for Author, Rip, and Blu-Ray**
|
||||
- Relocated "Author", "Rip", and "Blu-Ray" buttons to a new "Disc" category on the main menu.
|
||||
- Improved logical grouping of disc-related functionalities.
|
||||
|
||||
- ✅ **Subtitles Module - Video File Path Population**
|
||||
- Fixed an issue where dragging and dropping a video file onto the Subtitles module would not populate the "Video File Path" section.
|
||||
- Ensured the video entry widget correctly reflects the dropped video's path.
|
||||
|
||||
## Version 0.1.0-dev20+ (2025-12-23) - Player UX & Installer Polish
|
||||
|
||||
### Features (2025-12-23 Session)
|
||||
|
|
@ -424,13 +445,11 @@ This file tracks completed features, fixes, and milestones.
|
|||
- Filter chain combination support
|
||||
|
||||
### Bug Fixes
|
||||
- ✅ Fixed snippet duration issues with dual-mode approach
|
||||
- Default Format: Uses stream copy (keyframe-level precision)
|
||||
- Output Format: Re-encodes for frame-perfect duration
|
||||
- ✅ Fixed container/codec mismatch in snippet generation
|
||||
- Now properly matches container to codec (MP4 for h264, source format for stream copy)
|
||||
- ✅ Fixed missing audio bitrate in thumbnail metadata
|
||||
- ✅ Fixed contact sheet dimensions not accounting for padding
|
||||
- ✅ Fixed incorrect thumbnail count in contact sheets (was generating 34 instead of 40 for 5x8 grid)
|
||||
- ✅ Fixed frame selection FPS assumption (hardcoded 30fps removed)
|
||||
- ✅ Fixed module visibility (added thumb module to enabled check)
|
||||
- ✅ Fixed undefined function call (openFileManager → openFolder)
|
||||
- ✅ Fixed dynamic total count not updating when changing grid dimensions
|
||||
- ✅ Added missing `strings` import to thumbnail/generator.go
|
||||
- ✅ Updated snippet UI labels for clarity (Default Format vs Output Format)
|
||||
|
||||
|
|
@ -709,7 +728,7 @@ This file tracks completed features, fixes, and milestones.
|
|||
- Braille character animations
|
||||
- Shows current task during build and install
|
||||
- Interactive path selection (system-wide or user-local)
|
||||
- ✅ Added error dialogs with "Copy Error" button
|
||||
- Added error dialogs with "Copy Error" button
|
||||
- One-click error message copying for debugging
|
||||
- Applied to all major error scenarios
|
||||
- Better user experience when reporting issues
|
||||
|
|
@ -871,7 +890,6 @@ This file tracks completed features, fixes, and milestones.
|
|||
- ✅ Category-based logging (SYS, UI, MODULE, etc.)
|
||||
- ✅ Timestamp formatting
|
||||
- ✅ Debug output toggle via environment variable
|
||||
- ✅ Comprehensive debug messages throughout application
|
||||
- ✅ Log file output (videotools.log)
|
||||
|
||||
### Error Handling
|
||||
|
|
@ -907,6 +925,10 @@ This file tracks completed features, fixes, and milestones.
|
|||
- ✅ Audio decoding and playback
|
||||
- ✅ Synchronization between audio and video
|
||||
- ✅ Embedded playback within application window
|
||||
- ✅ Seek functionality with progress bar
|
||||
- ✅ Player window sizing based on video aspect ratio
|
||||
- ✅ Frame pump system for smooth playback
|
||||
- ✅ Audio/video synchronization
|
||||
- ✅ Checkpoint system for playback position
|
||||
|
||||
### UI/UX
|
||||
|
|
|
|||
29
TODO.md
29
TODO.md
|
|
@ -41,6 +41,8 @@ This file tracks upcoming features, improvements, and known issues.
|
|||
- Lossless option only for H.265/AV1
|
||||
- Dynamic dropdown based on codec
|
||||
- Lossless + Target Size mode support
|
||||
- Dynamic dropdown based on codec
|
||||
- Lossless + Target Size mode support
|
||||
- Audio bitrate estimation when metadata is missing
|
||||
- Target size unit selector and numeric entry
|
||||
- Snippet history updates in sidebar
|
||||
|
|
@ -70,7 +72,7 @@ This file tracks upcoming features, improvements, and known issues.
|
|||
- Frame interpolation presets in Filters with Upscale linkage
|
||||
- Real-ESRGAN AI upscale controls with ncnn pipeline (models, presets, tiles, TTA)
|
||||
|
||||
*Last Updated: 2025-12-21*
|
||||
*Last Updated: 2025-12-26*
|
||||
|
||||
## Priority Features for dev20+
|
||||
|
||||
|
|
@ -112,7 +114,30 @@ This file tracks upcoming features, improvements, and known issues.
|
|||
- Creative effects (grayscale, vignette)
|
||||
- Real-time preview system
|
||||
|
||||
- [ ] **DVD Authoring module**
|
||||
- [ ] **Upscale module implementation**
|
||||
- Design UI for upscaling
|
||||
- Implement traditional scaling (Lanczos, Bicubic)
|
||||
- Integrate Waifu2x (if feasible)
|
||||
- Integrate Real-ESRGAN (if feasible)
|
||||
- Add resolution presets
|
||||
- Quality vs. speed slider
|
||||
- Before/after comparison
|
||||
- Batch upscaling
|
||||
|
||||
- [ ] **Audio module implementation**
|
||||
- Design audio extraction UI
|
||||
- Implement audio track extraction
|
||||
- Audio track replacement/addition
|
||||
- Multi-track management
|
||||
- Volume normalization
|
||||
- Audio delay correction
|
||||
- Format conversion
|
||||
- Channel mapping
|
||||
- Audio-only operations
|
||||
|
||||
- [x] **DVD Authoring module**
|
||||
- [x] **Real-time progress reporting for FFmpeg encoding**
|
||||
- [x] **"Add to Queue" and "Clear Output Title" functionality**
|
||||
- Output VIDEO_TS folder + burn-ready ISO
|
||||
- Auto-detect NTSC/PAL with manual override
|
||||
- Preserve all audio tracks
|
||||
|
|
|
|||
188
author_module.go
188
author_module.go
|
|
@ -13,6 +13,7 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -250,17 +251,27 @@ func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
|||
state.authorChapters = nil
|
||||
state.authorChapterSource = ""
|
||||
state.authorVideoTSPath = ""
|
||||
state.authorTitle = ""
|
||||
rebuildList()
|
||||
state.updateAuthorSummary()
|
||||
})
|
||||
clearBtn.Importance = widget.MediumImportance
|
||||
|
||||
addQueueBtn := widget.NewButton("Add to Queue", func() {
|
||||
if len(state.authorClips) == 0 {
|
||||
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
||||
return
|
||||
}
|
||||
state.startAuthorGeneration(false)
|
||||
})
|
||||
addQueueBtn.Importance = widget.MediumImportance
|
||||
|
||||
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
||||
if len(state.authorClips) == 0 {
|
||||
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
||||
return
|
||||
}
|
||||
state.startAuthorGeneration()
|
||||
state.startAuthorGeneration(true)
|
||||
})
|
||||
compileBtn.Importance = widget.HighImportance
|
||||
|
||||
|
|
@ -302,7 +313,7 @@ func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
|||
|
||||
controls := container.NewBorder(
|
||||
widget.NewLabel("Videos:"),
|
||||
container.NewVBox(chapterToggle, container.NewHBox(addBtn, clearBtn, compileBtn)),
|
||||
container.NewVBox(chapterToggle, container.NewHBox(addBtn, clearBtn, addQueueBtn, compileBtn)),
|
||||
nil,
|
||||
nil,
|
||||
listArea,
|
||||
|
|
@ -730,7 +741,7 @@ func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
|||
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
||||
return
|
||||
}
|
||||
state.startAuthorGeneration()
|
||||
state.startAuthorGeneration(true)
|
||||
})
|
||||
generateBtn.Importance = widget.HighImportance
|
||||
|
||||
|
|
@ -1258,7 +1269,7 @@ func (s *appState) setAuthorProgress(percent float64) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *appState) startAuthorGeneration() {
|
||||
func (s *appState) startAuthorGeneration(startNow bool) {
|
||||
if s.authorVideoTSPath != "" {
|
||||
title := authorOutputTitle(s)
|
||||
outputPath := authorDefaultOutputPath("iso", title, []string{s.authorVideoTSPath})
|
||||
|
|
@ -1266,7 +1277,7 @@ func (s *appState) startAuthorGeneration() {
|
|||
dialog.ShowError(fmt.Errorf("failed to resolve output path"), s.window)
|
||||
return
|
||||
}
|
||||
if err := s.addAuthorVideoTSToQueue(s.authorVideoTSPath, title, outputPath, true); err != nil {
|
||||
if err := s.addAuthorVideoTSToQueue(s.authorVideoTSPath, title, outputPath, startNow); err != nil {
|
||||
dialog.ShowError(err, s.window)
|
||||
}
|
||||
return
|
||||
|
|
@ -1296,7 +1307,7 @@ func (s *appState) startAuthorGeneration() {
|
|||
}
|
||||
continuePrompt := func() {
|
||||
uiCall(func() {
|
||||
s.promptAuthorOutput(paths, region, aspect, title)
|
||||
s.promptAuthorOutput(paths, region, aspect, title, startNow)
|
||||
})
|
||||
}
|
||||
if len(warnings) > 0 {
|
||||
|
|
@ -1313,7 +1324,7 @@ func (s *appState) startAuthorGeneration() {
|
|||
continuePrompt()
|
||||
}
|
||||
|
||||
func (s *appState) promptAuthorOutput(paths []string, region, aspect, title string) {
|
||||
func (s *appState) promptAuthorOutput(paths []string, region, aspect, title string, startNow bool) {
|
||||
outputType := strings.ToLower(strings.TrimSpace(s.authorOutputType))
|
||||
if outputType == "" {
|
||||
outputType = "dvd"
|
||||
|
|
@ -1321,10 +1332,10 @@ func (s *appState) promptAuthorOutput(paths []string, region, aspect, title stri
|
|||
|
||||
outputPath := authorDefaultOutputPath(outputType, title, paths)
|
||||
if outputType == "iso" {
|
||||
s.generateAuthoring(paths, region, aspect, title, outputPath, true)
|
||||
s.generateAuthoring(paths, region, aspect, title, outputPath, true, startNow)
|
||||
return
|
||||
}
|
||||
s.generateAuthoring(paths, region, aspect, title, outputPath, false)
|
||||
s.generateAuthoring(paths, region, aspect, title, outputPath, false, startNow)
|
||||
}
|
||||
|
||||
func authorWarnings(state *appState) []string {
|
||||
|
|
@ -1502,8 +1513,8 @@ func uniqueFilePath(path string) string {
|
|||
return fmt.Sprintf("%s-%d%s", base, time.Now().Unix(), ext)
|
||||
}
|
||||
|
||||
func (s *appState) generateAuthoring(paths []string, region, aspect, title, outputPath string, makeISO bool) {
|
||||
if err := s.addAuthorToQueue(paths, region, aspect, title, outputPath, makeISO, true); err != nil {
|
||||
func (s *appState) generateAuthoring(paths []string, region, aspect, title, outputPath string, makeISO, startNow bool) {
|
||||
if err := s.addAuthorToQueue(paths, region, aspect, title, outputPath, makeISO, startNow); err != nil {
|
||||
dialog.ShowError(err, s.window)
|
||||
}
|
||||
}
|
||||
|
|
@ -1636,20 +1647,22 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
|||
return err
|
||||
}
|
||||
|
||||
totalSteps := len(paths) + 2
|
||||
var totalDuration float64
|
||||
for _, path := range paths {
|
||||
src, err := probeVideo(path)
|
||||
if err == nil {
|
||||
totalDuration += src.Duration
|
||||
}
|
||||
}
|
||||
|
||||
encodingProgressShare := 80.0
|
||||
otherStepsProgressShare := 20.0
|
||||
otherStepsCount := 2.0
|
||||
if makeISO {
|
||||
totalSteps++
|
||||
}
|
||||
step := 0
|
||||
advance := func(message string) {
|
||||
step++
|
||||
if logFn != nil && message != "" {
|
||||
logFn(message)
|
||||
}
|
||||
if progressFn != nil && totalSteps > 0 {
|
||||
progressFn(float64(step) / float64(totalSteps) * 100.0)
|
||||
}
|
||||
otherStepsCount++
|
||||
}
|
||||
progressForOtherStep := otherStepsProgressShare / otherStepsCount
|
||||
var accumulatedProgress float64
|
||||
|
||||
var mpgPaths []string
|
||||
for i, path := range paths {
|
||||
|
|
@ -1661,36 +1674,43 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to probe %s: %w", filepath.Base(path), err)
|
||||
}
|
||||
|
||||
clipProgressShare := 0.0
|
||||
if totalDuration > 0 {
|
||||
clipProgressShare = (src.Duration / totalDuration) * encodingProgressShare
|
||||
}
|
||||
|
||||
ffmpegProgressFn := func(stepPct float64) {
|
||||
overallPct := accumulatedProgress + (stepPct / 100.0 * clipProgressShare)
|
||||
if progressFn != nil {
|
||||
progressFn(overallPct)
|
||||
}
|
||||
}
|
||||
|
||||
args := buildAuthorFFmpegArgs(path, outPath, region, aspect, src.IsProgressive())
|
||||
if logFn != nil {
|
||||
logFn(fmt.Sprintf(">> ffmpeg %s", strings.Join(args, " ")))
|
||||
}
|
||||
if err := runCommandWithLogger(ctx, platformConfig.FFmpegPath, args, logFn); err != nil {
|
||||
|
||||
if err := runAuthorFFmpeg(ctx, args, src.Duration, logFn, ffmpegProgressFn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remultiplex the MPEG to fix timestamps for DVD compliance
|
||||
// This resolves "SCR moves backwards" errors from dvdauthor
|
||||
remuxPath := filepath.Join(workDir, fmt.Sprintf("title_%02d_remux.mpg", i+1))
|
||||
remuxArgs := []string{
|
||||
"-fflags", "+genpts",
|
||||
"-i", outPath,
|
||||
"-c", "copy",
|
||||
"-f", "dvd",
|
||||
"-y",
|
||||
remuxPath,
|
||||
accumulatedProgress += clipProgressShare
|
||||
if progressFn != nil {
|
||||
progressFn(accumulatedProgress)
|
||||
}
|
||||
|
||||
remuxPath := filepath.Join(workDir, fmt.Sprintf("title_%02d_remux.mpg", i+1))
|
||||
remuxArgs := []string{"-fflags", "+genpts", "-i", outPath, "-c", "copy", "-f", "dvd", "-y", remuxPath}
|
||||
if logFn != nil {
|
||||
logFn(fmt.Sprintf(">> ffmpeg %s (remuxing for DVD compliance)", strings.Join(remuxArgs, " ")))
|
||||
}
|
||||
if err := runCommandWithLogger(ctx, platformConfig.FFmpegPath, remuxArgs, logFn); err != nil {
|
||||
return fmt.Errorf("remux failed: %w", err)
|
||||
}
|
||||
|
||||
// Remove original encode, use remuxed version
|
||||
os.Remove(outPath)
|
||||
mpgPaths = append(mpgPaths, remuxPath)
|
||||
advance("")
|
||||
}
|
||||
|
||||
if len(chapters) == 0 && treatAsChapters && len(clips) > 1 {
|
||||
|
|
@ -1701,7 +1721,6 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
|||
chapters = embed
|
||||
}
|
||||
}
|
||||
|
||||
if treatAsChapters && len(mpgPaths) > 1 {
|
||||
concatPath := filepath.Join(workDir, "titles_joined.mpg")
|
||||
if logFn != nil {
|
||||
|
|
@ -1712,7 +1731,6 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
|||
}
|
||||
mpgPaths = []string{concatPath}
|
||||
}
|
||||
|
||||
if len(mpgPaths) > 1 {
|
||||
chapters = nil
|
||||
}
|
||||
|
|
@ -1722,23 +1740,21 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
|||
return err
|
||||
}
|
||||
|
||||
if logFn != nil {
|
||||
logFn("Authoring DVD structure...")
|
||||
logFn(fmt.Sprintf(">> dvdauthor -o %s -x %s", discRoot, xmlPath))
|
||||
}
|
||||
logFn("Authoring DVD structure...")
|
||||
logFn(fmt.Sprintf(">> dvdauthor -o %s -x %s", discRoot, xmlPath))
|
||||
if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-x", xmlPath}, logFn); err != nil {
|
||||
return err
|
||||
}
|
||||
advance("")
|
||||
accumulatedProgress += progressForOtherStep
|
||||
progressFn(accumulatedProgress)
|
||||
|
||||
if logFn != nil {
|
||||
logFn("Building DVD tables...")
|
||||
logFn(fmt.Sprintf(">> dvdauthor -o %s -T", discRoot))
|
||||
}
|
||||
logFn("Building DVD tables...")
|
||||
logFn(fmt.Sprintf(">> dvdauthor -o %s -T", discRoot))
|
||||
if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-T"}, logFn); err != nil {
|
||||
return err
|
||||
}
|
||||
advance("")
|
||||
accumulatedProgress += progressForOtherStep
|
||||
progressFn(accumulatedProgress)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(discRoot, "AUDIO_TS"), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create AUDIO_TS: %w", err)
|
||||
|
|
@ -1749,19 +1765,83 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if logFn != nil {
|
||||
logFn("Creating ISO image...")
|
||||
logFn(fmt.Sprintf(">> %s %s", tool, strings.Join(args, " ")))
|
||||
}
|
||||
logFn("Creating ISO image...")
|
||||
logFn(fmt.Sprintf(">> %s %s", tool, strings.Join(args, " ")))
|
||||
if err := runCommandWithLogger(ctx, tool, args, logFn); err != nil {
|
||||
return err
|
||||
}
|
||||
advance("")
|
||||
accumulatedProgress += progressForOtherStep
|
||||
progressFn(accumulatedProgress)
|
||||
}
|
||||
|
||||
progressFn(100.0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAuthorFFmpeg(ctx context.Context, args []string, duration float64, logFn func(string), progressFn func(float64)) error {
|
||||
finalArgs := append([]string{"-progress", "pipe:1", "-nostats"}, args...)
|
||||
cmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, finalArgs...)
|
||||
utils.ApplyNoWindow(cmd)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ffmpeg stdout pipe: %w", err)
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ffmpeg stderr pipe: %w", err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("ffmpeg start failed: %w", err)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
if logFn != nil {
|
||||
logFn(scanner.Text())
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
key, val := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||
if key == "out_time_ms" {
|
||||
if ms, err := strconv.ParseInt(val, 10, 64); err == nil && ms > 0 {
|
||||
currentSec := float64(ms) / 1000000.0
|
||||
if duration > 0 {
|
||||
stepPct := (currentSec / duration) * 100.0
|
||||
if stepPct > 100 {
|
||||
stepPct = 100
|
||||
}
|
||||
if progressFn != nil {
|
||||
progressFn(stepPct)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if logFn != nil {
|
||||
logFn(line)
|
||||
}
|
||||
}
|
||||
}()
|
||||
err = cmd.Wait()
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ffmpeg failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
||||
cfg := job.Config
|
||||
if cfg == nil {
|
||||
|
|
|
|||
6
main.go
6
main.go
|
|
@ -86,9 +86,9 @@ var (
|
|||
{"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green
|
||||
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
|
||||
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
|
||||
{"author", "Author", utils.MustHex("#FFAA44"), "DVD", modules.HandleAuthor}, // Orange
|
||||
{"rip", "Rip", utils.MustHex("#FF9944"), "DVD", modules.HandleRip}, // Orange
|
||||
{"bluray", "Blu-Ray", utils.MustHex("#4D7CFE"), "Blu-Ray", modules.HandleBluRay}, // Blue
|
||||
{"author", "Author", utils.MustHex("#FFAA44"), "Disc", modules.HandleAuthor}, // Orange
|
||||
{"rip", "Rip", utils.MustHex("#FF9944"), "Disc", modules.HandleRip}, // Orange
|
||||
{"bluray", "Blu-Ray", utils.MustHex("#4D7CFE"), "Disc", modules.HandleBluRay}, // Blue
|
||||
{"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure
|
||||
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
|
||||
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user