Created
February 8, 2026 17:31
-
-
Save NotNahid/597ba29f82e30a4a6d3b9f6c92e95dcc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ======================================== | |
| # FFmpeg Smart Studio v9 - FULL FEATURED | |
| # ======================================== | |
| Add-Type -AssemblyName System.Windows.Forms | |
| Add-Type -AssemblyName System.Drawing | |
| # For async/background work | |
| Add-Type -AssemblyName WindowsBase | |
| # --------------------------- | |
| # Config Path | |
| # --------------------------- | |
| $script:configPath = Join-Path $env:APPDATA "FFmpegStudio" | |
| $script:configFile = Join-Path $script:configPath "settings.json" | |
| $script:presetsFile = Join-Path $script:configPath "custom_presets.json" | |
| $script:thumbCachePath = Join-Path $script:configPath "thumbs" | |
| if (-not (Test-Path $script:configPath)) { New-Item -ItemType Directory -Path $script:configPath -Force | Out-Null } | |
| if (-not (Test-Path $script:thumbCachePath)) { New-Item -ItemType Directory -Path $script:thumbCachePath -Force | Out-Null } | |
| # --------------------------- | |
| # Color Theme | |
| # --------------------------- | |
| $theme = @{ | |
| Background = [System.Drawing.Color]::FromArgb(30, 30, 30) | |
| Panel = [System.Drawing.Color]::FromArgb(45, 45, 45) | |
| Control = [System.Drawing.Color]::FromArgb(60, 60, 60) | |
| Text = [System.Drawing.Color]::FromArgb(220, 220, 220) | |
| Accent = [System.Drawing.Color]::FromArgb(0, 122, 204) | |
| Success = [System.Drawing.Color]::FromArgb(76, 175, 80) | |
| Error = [System.Drawing.Color]::FromArgb(244, 67, 54) | |
| Warning = [System.Drawing.Color]::FromArgb(255, 193, 7) | |
| ButtonBg = [System.Drawing.Color]::FromArgb(70, 70, 70) | |
| ButtonHover = [System.Drawing.Color]::FromArgb(90, 90, 90) | |
| GridBg = [System.Drawing.Color]::FromArgb(35, 35, 35) | |
| GridAlt = [System.Drawing.Color]::FromArgb(42, 42, 42) | |
| LogBg = [System.Drawing.Color]::FromArgb(20, 20, 20) | |
| PresetBg = [System.Drawing.Color]::FromArgb(50, 50, 55) | |
| PresetHover = [System.Drawing.Color]::FromArgb(60, 65, 75) | |
| ThumbBg = [System.Drawing.Color]::FromArgb(25, 25, 25) | |
| } | |
| $fontNormal = New-Object System.Drawing.Font("Segoe UI", 9) | |
| $fontBold = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold) | |
| $fontTitle = New-Object System.Drawing.Font("Segoe UI", 11, [System.Drawing.FontStyle]::Bold) | |
| $fontMono = New-Object System.Drawing.Font("Cascadia Code,Consolas", 9) | |
| $fontSmall = New-Object System.Drawing.Font("Segoe UI", 8) | |
| $fontLarge = New-Object System.Drawing.Font("Segoe UI", 13, [System.Drawing.FontStyle]::Bold) | |
| # --------------------------- | |
| # Global State | |
| # --------------------------- | |
| $script:currentProcess = $null | |
| $script:cancelRequested = $false | |
| $script:isProcessing = $false | |
| $script:fileDataStore = [System.Collections.ArrayList]::new() | |
| $script:recentOutputs = [System.Collections.ArrayList]::new() | |
| # --------------------------- | |
| # Config Save / Load | |
| # --------------------------- | |
| function Save-Config { | |
| $config = @{ | |
| OutputFolder = $txtOut.Text | |
| Format = $cbFmt.SelectedIndex | |
| Resolution = $cbRes.SelectedIndex | |
| CustomResW = $txtCustomW.Text | |
| CustomResH = $txtCustomH.Text | |
| UseCustomRes = $chkCustomRes.Checked | |
| CRF = $trackCRF.Value | |
| Volume = $trackVol.Value | |
| Speed = $txtSpeed.Text | |
| Prefix = $txtPrefix.Text | |
| Suffix = $txtSuffix.Text | |
| Mute = $chkMute.Checked | |
| Grayscale = $chkGray.Checked | |
| Invert = $chkInvert.Checked | |
| Blur = $chkBlur.Checked | |
| AudioCodec = $cbACodec.SelectedIndex | |
| VideoCodec = $cbVCodec.SelectedIndex | |
| HWAccel = $cbHWAccel.SelectedIndex | |
| Bitrate = $txtBitrate.Text | |
| FPS = $txtFPS.Text | |
| TrimStart = $txtStart.Text | |
| TrimEnd = $txtEnd.Text | |
| AutoOpen = $chkAutoOpen.Checked | |
| Overwrite = $chkOverwrite.Checked | |
| ShutdownAfter = $chkShutdown.Checked | |
| WindowW = $form.Width | |
| WindowH = $form.Height | |
| RecentOutputs = @($script:recentOutputs) | |
| } | |
| $config | ConvertTo-Json -Depth 3 | Set-Content $script:configFile -Force | |
| } | |
| function Load-Config { | |
| if (-not (Test-Path $script:configFile)) { return } | |
| try { | |
| $config = Get-Content $script:configFile -Raw | ConvertFrom-Json | |
| if ($config.OutputFolder) { $txtOut.Text = $config.OutputFolder } | |
| if ($null -ne $config.Format) { $cbFmt.SelectedIndex = [Math]::Min($config.Format, $cbFmt.Items.Count - 1) } | |
| if ($null -ne $config.Resolution) { $cbRes.SelectedIndex = [Math]::Min($config.Resolution, $cbRes.Items.Count - 1) } | |
| if ($config.CustomResW) { $txtCustomW.Text = $config.CustomResW } | |
| if ($config.CustomResH) { $txtCustomH.Text = $config.CustomResH } | |
| if ($null -ne $config.UseCustomRes) { $chkCustomRes.Checked = $config.UseCustomRes } | |
| if ($null -ne $config.CRF) { $trackCRF.Value = [Math]::Max(0, [Math]::Min(51, $config.CRF)) } | |
| if ($null -ne $config.Volume) { $trackVol.Value = [Math]::Max(0, [Math]::Min(300, $config.Volume)) } | |
| if ($config.Speed) { $txtSpeed.Text = $config.Speed } | |
| if ($null -ne $config.Prefix) { $txtPrefix.Text = $config.Prefix } | |
| if ($null -ne $config.Suffix) { $txtSuffix.Text = $config.Suffix } | |
| if ($null -ne $config.Mute) { $chkMute.Checked = $config.Mute } | |
| if ($null -ne $config.Grayscale) { $chkGray.Checked = $config.Grayscale } | |
| if ($null -ne $config.Invert) { $chkInvert.Checked = $config.Invert } | |
| if ($null -ne $config.Blur) { $chkBlur.Checked = $config.Blur } | |
| if ($null -ne $config.AudioCodec) { $cbACodec.SelectedIndex = [Math]::Min($config.AudioCodec, $cbACodec.Items.Count - 1) } | |
| if ($null -ne $config.VideoCodec) { $cbVCodec.SelectedIndex = [Math]::Min($config.VideoCodec, $cbVCodec.Items.Count - 1) } | |
| if ($null -ne $config.HWAccel) { $cbHWAccel.SelectedIndex = [Math]::Min($config.HWAccel, $cbHWAccel.Items.Count - 1) } | |
| if ($config.Bitrate) { $txtBitrate.Text = $config.Bitrate } | |
| if ($config.FPS) { $txtFPS.Text = $config.FPS } | |
| if ($config.TrimStart) { $txtStart.Text = $config.TrimStart } | |
| if ($config.TrimEnd) { $txtEnd.Text = $config.TrimEnd } | |
| if ($null -ne $config.AutoOpen) { $chkAutoOpen.Checked = $config.AutoOpen } | |
| if ($null -ne $config.Overwrite) { $chkOverwrite.Checked = $config.Overwrite } | |
| if ($null -ne $config.ShutdownAfter) { $chkShutdown.Checked = $config.ShutdownAfter } | |
| if ($config.WindowW -gt 0 -and $config.WindowH -gt 0) { | |
| $form.Width = $config.WindowW | |
| $form.Height = $config.WindowH | |
| } | |
| if ($config.RecentOutputs) { | |
| foreach ($r in $config.RecentOutputs) { $script:recentOutputs.Add($r) | Out-Null } | |
| } | |
| } catch { | |
| # Silently ignore corrupt config | |
| } | |
| } | |
| # --------------------------- | |
| # Thumbnail Generator | |
| # --------------------------- | |
| function Get-Thumbnail { | |
| param([string]$FilePath, [int]$Width = 160, [int]$Height = 90) | |
| $hash = [System.IO.Path]::GetFileName($FilePath).GetHashCode().ToString("X8") | |
| $thumbFile = Join-Path $script:thumbCachePath "$hash.jpg" | |
| if (Test-Path $thumbFile) { | |
| try { return [System.Drawing.Image]::FromFile($thumbFile) } catch { } | |
| } | |
| try { | |
| $result = & ffmpeg -y -i "$FilePath" -ss 00:00:01 -vframes 1 -s "${Width}x${Height}" -q:v 5 "$thumbFile" 2>&1 | |
| if (Test-Path $thumbFile) { | |
| return [System.Drawing.Image]::FromFile($thumbFile) | |
| } | |
| } catch { } | |
| # Return placeholder | |
| $bmp = New-Object System.Drawing.Bitmap($Width, $Height) | |
| $g = [System.Drawing.Graphics]::FromImage($bmp) | |
| $g.Clear($theme.ThumbBg) | |
| $g.DrawString("No Preview", $fontSmall, [System.Drawing.Brushes]::Gray, | |
| [System.Drawing.RectangleF]::new(0, 0, $Width, $Height), | |
| (New-Object System.Drawing.StringFormat -Property @{ Alignment = 'Center'; LineAlignment = 'Center' })) | |
| $g.Dispose() | |
| return $bmp | |
| } | |
| # --------------------------- | |
| # Helpers | |
| # --------------------------- | |
| function New-StyledButton { | |
| param( | |
| [string]$Text, | |
| [int]$Width = 130, | |
| [int]$Height = 36, | |
| [System.Drawing.Color]$BgColor = $theme.ButtonBg, | |
| [System.Drawing.Color]$FgColor = $theme.Text | |
| ) | |
| $btn = New-Object System.Windows.Forms.Button | |
| $btn.Text = $Text | |
| $btn.Size = New-Object System.Drawing.Size($Width, $Height) | |
| $btn.FlatStyle = 'Flat' | |
| $btn.FlatAppearance.BorderColor = $theme.Accent | |
| $btn.FlatAppearance.BorderSize = 1 | |
| $btn.FlatAppearance.MouseOverBackColor = $theme.ButtonHover | |
| $btn.BackColor = $BgColor | |
| $btn.ForeColor = $FgColor | |
| $btn.Font = $fontBold | |
| $btn.Cursor = [System.Windows.Forms.Cursors]::Hand | |
| return $btn | |
| } | |
| function New-StyledLabel { | |
| param([string]$Text, [int]$X, [int]$Y, [switch]$Title) | |
| $lbl = New-Object System.Windows.Forms.Label | |
| $lbl.Text = $Text | |
| $lbl.Location = New-Object System.Drawing.Point($X, $Y) | |
| $lbl.AutoSize = $true | |
| $lbl.ForeColor = $theme.Text | |
| $lbl.Font = if ($Title) { $fontTitle } else { $fontNormal } | |
| $lbl.BackColor = [System.Drawing.Color]::Transparent | |
| return $lbl | |
| } | |
| function New-StyledTextBox { | |
| param([int]$X, [int]$Y, [int]$W = 200, [string]$Default = "") | |
| $tb = New-Object System.Windows.Forms.TextBox | |
| $tb.Location = New-Object System.Drawing.Point($X, $Y) | |
| $tb.Width = $W | |
| $tb.BackColor = $theme.Control | |
| $tb.ForeColor = $theme.Text | |
| $tb.Font = $fontNormal | |
| $tb.BorderStyle = 'FixedSingle' | |
| $tb.Text = $Default | |
| return $tb | |
| } | |
| function New-StyledCombo { | |
| param([int]$X, [int]$Y, [int]$W = 120, [string[]]$Items, [int]$Default = 0) | |
| $cb = New-Object System.Windows.Forms.ComboBox | |
| $cb.Location = New-Object System.Drawing.Point($X, $Y) | |
| $cb.Width = $W | |
| $cb.BackColor = $theme.Control | |
| $cb.ForeColor = $theme.Text | |
| $cb.Font = $fontNormal | |
| $cb.FlatStyle = 'Flat' | |
| $cb.DropDownStyle = 'DropDownList' | |
| if ($Items) { $cb.Items.AddRange($Items) } | |
| if ($cb.Items.Count -gt $Default) { $cb.SelectedIndex = $Default } | |
| return $cb | |
| } | |
| function New-StyledCheckBox { | |
| param([string]$Text, [int]$X, [int]$Y) | |
| $chk = New-Object System.Windows.Forms.CheckBox | |
| $chk.Text = $Text | |
| $chk.Location = New-Object System.Drawing.Point($X, $Y) | |
| $chk.ForeColor = $theme.Text | |
| $chk.Font = $fontNormal | |
| $chk.AutoSize = $true | |
| $chk.BackColor = [System.Drawing.Color]::Transparent | |
| return $chk | |
| } | |
| # --------------------------- | |
| # Main Form | |
| # --------------------------- | |
| $form = New-Object System.Windows.Forms.Form | |
| $form.Text = "FFmpeg Smart Studio v9" | |
| $form.Size = New-Object System.Drawing.Size(1400, 950) | |
| $form.MinimumSize = New-Object System.Drawing.Size(1000, 700) | |
| $form.StartPosition = "CenterScreen" | |
| $form.BackColor = $theme.Background | |
| $form.Font = $fontNormal | |
| $form.FormBorderStyle = 'Sizable' | |
| $form.MaximizeBox = $true | |
| $form.Icon = [System.Drawing.SystemIcons]::Application | |
| # Status Bar | |
| $statusBar = New-Object System.Windows.Forms.StatusStrip | |
| $statusBar.BackColor = $theme.Panel | |
| $statusBar.SizingGrip = $true | |
| $statusLabel = New-Object System.Windows.Forms.ToolStripStatusLabel | |
| $statusLabel.Text = "Ready" | |
| $statusLabel.ForeColor = $theme.Text | |
| $statusLabel.Spring = $true | |
| $statusLabel.TextAlign = 'MiddleLeft' | |
| $statusFFmpeg = New-Object System.Windows.Forms.ToolStripStatusLabel | |
| $statusFFmpeg.Text = "Checking FFmpeg..." | |
| $statusFFmpeg.ForeColor = $theme.Warning | |
| $statusBar.Items.Add($statusLabel) | Out-Null | |
| $statusBar.Items.Add($statusFFmpeg) | Out-Null | |
| $form.Controls.Add($statusBar) | |
| # Check FFmpeg availability | |
| $ffmpegOk = $false | |
| try { | |
| $ffVer = & ffmpeg -version 2>&1 | Select-Object -First 1 | |
| if ($ffVer -match "ffmpeg version") { | |
| $statusFFmpeg.Text = "FFmpeg: OK" | |
| $statusFFmpeg.ForeColor = $theme.Success | |
| $ffmpegOk = $true | |
| } | |
| } catch { } | |
| if (-not $ffmpegOk) { | |
| $statusFFmpeg.Text = "FFmpeg: NOT FOUND" | |
| $statusFFmpeg.ForeColor = $theme.Error | |
| } | |
| # --------------------------- | |
| # Tab Control | |
| # --------------------------- | |
| $tabs = New-Object System.Windows.Forms.TabControl | |
| $tabs.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $tabs.Font = $fontBold | |
| $tabs.Padding = New-Object System.Drawing.Point(14, 6) | |
| $tabConvert = New-Object System.Windows.Forms.TabPage | |
| $tabConvert.Text = " Convert / Edit " | |
| $tabConvert.BackColor = $theme.Background | |
| $tabConvert.Padding = New-Object System.Windows.Forms.Padding(6) | |
| $tabPresets = New-Object System.Windows.Forms.TabPage | |
| $tabPresets.Text = " Quick Presets " | |
| $tabPresets.BackColor = $theme.Background | |
| $tabPresets.Padding = New-Object System.Windows.Forms.Padding(6) | |
| $tabPreview = New-Object System.Windows.Forms.TabPage | |
| $tabPreview.Text = " Preview / Info " | |
| $tabPreview.BackColor = $theme.Background | |
| $tabPreview.Padding = New-Object System.Windows.Forms.Padding(6) | |
| $tabSettings = New-Object System.Windows.Forms.TabPage | |
| $tabSettings.Text = " Settings " | |
| $tabSettings.BackColor = $theme.Background | |
| $tabSettings.Padding = New-Object System.Windows.Forms.Padding(6) | |
| $tabs.TabPages.AddRange(@($tabConvert, $tabPresets, $tabPreview, $tabSettings)) | |
| $form.Controls.Add($tabs) | |
| # ========================================== | |
| # TAB 1: CONVERT / EDIT | |
| # ========================================== | |
| $mainLayout = New-Object System.Windows.Forms.TableLayoutPanel | |
| $mainLayout.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $mainLayout.ColumnCount = 1 | |
| $mainLayout.RowCount = 6 | |
| $mainLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 100))) | Out-Null | |
| $mainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 28))) | Out-Null | |
| $mainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 55))) | Out-Null | |
| $mainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 165))) | Out-Null | |
| $mainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 78))) | Out-Null | |
| $mainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 72))) | Out-Null | |
| $mainLayout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 50))) | Out-Null | |
| $tabConvert.Controls.Add($mainLayout) | |
| # --------------------------- | |
| # Row 0: File Queue with Thumbnails | |
| # --------------------------- | |
| $queuePanel = New-Object System.Windows.Forms.Panel | |
| $queuePanel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $queuePanel.BackColor = $theme.Panel | |
| $queuePanel.Padding = New-Object System.Windows.Forms.Padding(5) | |
| $lblQueueTitle = New-StyledLabel "File Queue" 8 4 -Title | |
| $lblQueueTitle.ForeColor = $theme.Accent | |
| $queuePanel.Controls.Add($lblQueueTitle) | |
| $lblQueueCount = New-StyledLabel "(0 files)" 110 7 | |
| $lblQueueCount.ForeColor = [System.Drawing.Color]::Gray | |
| $queuePanel.Controls.Add($lblQueueCount) | |
| # DataGridView with image column | |
| $grid = New-Object System.Windows.Forms.DataGridView | |
| $grid.Location = New-Object System.Drawing.Point(5, 26) | |
| $grid.AllowUserToAddRows = $false | |
| $grid.RowHeadersVisible = $false | |
| $grid.AutoSizeColumnsMode = "Fill" | |
| $grid.SelectionMode = 'FullRowSelect' | |
| $grid.MultiSelect = $true | |
| $grid.BackgroundColor = $theme.GridBg | |
| $grid.GridColor = $theme.Control | |
| $grid.DefaultCellStyle.BackColor = $theme.GridBg | |
| $grid.DefaultCellStyle.ForeColor = $theme.Text | |
| $grid.DefaultCellStyle.SelectionBackColor = $theme.Accent | |
| $grid.DefaultCellStyle.Font = $fontNormal | |
| $grid.AlternatingRowsDefaultCellStyle.BackColor = $theme.GridAlt | |
| $grid.ColumnHeadersDefaultCellStyle.BackColor = $theme.Panel | |
| $grid.ColumnHeadersDefaultCellStyle.ForeColor = $theme.Accent | |
| $grid.ColumnHeadersDefaultCellStyle.Font = $fontBold | |
| $grid.EnableHeadersVisualStyles = $false | |
| $grid.BorderStyle = 'None' | |
| $grid.CellBorderStyle = 'SingleHorizontal' | |
| $grid.RowTemplate.Height = 50 | |
| # Thumbnail column | |
| $imgCol = New-Object System.Windows.Forms.DataGridViewImageColumn | |
| $imgCol.Name = "Thumb" | |
| $imgCol.HeaderText = "Preview" | |
| $imgCol.ImageLayout = 'Zoom' | |
| $imgCol.FillWeight = 10 | |
| $grid.Columns.Add($imgCol) | Out-Null | |
| $grid.Columns.Add("FileName", "File Path") | Out-Null | |
| $grid.Columns.Add("Duration", "Duration") | Out-Null | |
| $grid.Columns.Add("Resolution", "Resolution") | Out-Null | |
| $grid.Columns.Add("Codec", "Codec") | Out-Null | |
| $grid.Columns.Add("Size", "Size") | Out-Null | |
| $grid.Columns.Add("Status", "Status") | Out-Null | |
| $grid.Columns["FileName"].FillWeight = 35 | |
| $grid.Columns["Duration"].FillWeight = 10 | |
| $grid.Columns["Resolution"].FillWeight = 10 | |
| $grid.Columns["Codec"].FillWeight = 10 | |
| $grid.Columns["Size"].FillWeight = 10 | |
| $grid.Columns["Status"].FillWeight = 10 | |
| $queuePanel.Controls.Add($grid) | |
| # Queue side buttons | |
| $queueBtnPanel = New-Object System.Windows.Forms.FlowLayoutPanel | |
| $queueBtnPanel.Size = New-Object System.Drawing.Size(90, 200) | |
| $queueBtnPanel.FlowDirection = 'TopDown' | |
| $queueBtnPanel.BackColor = $theme.Panel | |
| $queueBtnPanel.WrapContents = $false | |
| $btnMoveUp = New-StyledButton "Move Up" 86 30 | |
| $btnMoveDown = New-StyledButton "Move Down" 86 30 | |
| $btnRemove = New-StyledButton "Remove" 86 30 $theme.Error ([System.Drawing.Color]::White) | |
| $btnPreviewFile = New-StyledButton "Preview" 86 30 | |
| $queueBtnPanel.Controls.AddRange(@($btnMoveUp, $btnMoveDown, $btnRemove, $btnPreviewFile)) | |
| $queuePanel.Controls.Add($queueBtnPanel) | |
| $queuePanel.Add_Resize({ | |
| $grid.Size = New-Object System.Drawing.Size(($queuePanel.Width - 105), ($queuePanel.Height - 32)) | |
| $queueBtnPanel.Location = New-Object System.Drawing.Point(($queuePanel.Width - 96), 26) | |
| $queueBtnPanel.Height = $queuePanel.Height - 32 | |
| }) | |
| $mainLayout.Controls.Add($queuePanel, 0, 0) | |
| # Drag & Drop | |
| $grid.AllowDrop = $true | |
| $grid.Add_DragEnter({ param($s, $e) | |
| if ($e.Data.GetDataPresent([Windows.Forms.DataFormats]::FileDrop)) { $e.Effect = "Copy" } | |
| }) | |
| # --------------------------- | |
| # FFprobe / Media Info | |
| # --------------------------- | |
| function Get-MediaInfo { | |
| param([string]$FilePath) | |
| $info = @{ Duration = "N/A"; Resolution = "N/A"; Size = "N/A"; Codec = "N/A"; | |
| Width = 0; Height = 0; DurationSec = 0; Bitrate = "N/A"; AudioCodec = "N/A"; | |
| FrameRate = "N/A" } | |
| try { | |
| $sizeBytes = (Get-Item $FilePath).Length | |
| if ($sizeBytes -gt 1GB) { $info.Size = "{0:N2} GB" -f ($sizeBytes / 1GB) } | |
| elseif ($sizeBytes -gt 1MB) { $info.Size = "{0:N1} MB" -f ($sizeBytes / 1MB) } | |
| else { $info.Size = "{0:N0} KB" -f ($sizeBytes / 1KB) } | |
| } catch {} | |
| try { | |
| $probe = & ffprobe -v quiet -print_format json -show_format -show_streams "$FilePath" 2>&1 | ConvertFrom-Json | |
| if ($probe.format.duration) { | |
| $info.DurationSec = [double]$probe.format.duration | |
| $ts = [TimeSpan]::FromSeconds($info.DurationSec) | |
| $info.Duration = "{0:D2}:{1:D2}:{2:D2}" -f $ts.Hours, $ts.Minutes, $ts.Seconds | |
| } | |
| if ($probe.format.bit_rate) { | |
| $br = [double]$probe.format.bit_rate | |
| $info.Bitrate = if ($br -gt 1000000) { "{0:N1} Mbps" -f ($br / 1000000) } else { "{0:N0} kbps" -f ($br / 1000) } | |
| } | |
| $vidStream = $probe.streams | Where-Object { $_.codec_type -eq "video" } | Select-Object -First 1 | |
| if ($vidStream) { | |
| $info.Resolution = "$($vidStream.width)x$($vidStream.height)" | |
| $info.Width = [int]$vidStream.width | |
| $info.Height = [int]$vidStream.height | |
| $info.Codec = $vidStream.codec_name | |
| if ($vidStream.r_frame_rate -match "(\d+)/(\d+)") { | |
| $fps = [math]::Round([double]$Matches[1] / [double]$Matches[2], 2) | |
| $info.FrameRate = "$fps fps" | |
| } | |
| } | |
| $audStream = $probe.streams | Where-Object { $_.codec_type -eq "audio" } | Select-Object -First 1 | |
| if ($audStream) { $info.AudioCodec = $audStream.codec_name } | |
| } catch {} | |
| return $info | |
| } | |
| function Add-FileToQueue { | |
| param([string]$FilePath) | |
| foreach ($row in $grid.Rows) { | |
| if ($row.Cells["FileName"].Value -eq $FilePath) { return } | |
| } | |
| $info = Get-MediaInfo $FilePath | |
| $script:fileDataStore.Add(@{ Path = $FilePath; Info = $info }) | Out-Null | |
| $rowIdx = $grid.Rows.Add() | |
| $grid.Rows[$rowIdx].Cells["FileName"].Value = $FilePath | |
| $grid.Rows[$rowIdx].Cells["Duration"].Value = $info.Duration | |
| $grid.Rows[$rowIdx].Cells["Resolution"].Value = $info.Resolution | |
| $grid.Rows[$rowIdx].Cells["Codec"].Value = $info.Codec | |
| $grid.Rows[$rowIdx].Cells["Size"].Value = $info.Size | |
| $grid.Rows[$rowIdx].Cells["Status"].Value = "Pending" | |
| # Generate thumbnail in background | |
| $thumb = Get-Thumbnail $FilePath 80 45 | |
| if ($thumb) { $grid.Rows[$rowIdx].Cells["Thumb"].Value = $thumb } | |
| $lblQueueCount.Text = "($($grid.Rows.Count) files)" | |
| } | |
| $grid.Add_DragDrop({ param($s, $e) | |
| $files = $e.Data.GetData([Windows.Forms.DataFormats]::FileDrop) | |
| foreach ($f in $files) { | |
| if (Test-Path $f -PathType Container) { | |
| Get-ChildItem $f -Recurse -File -Include *.mp4,*.mkv,*.avi,*.mov,*.wmv,*.flv,*.webm,*.mp3,*.wav,*.flac | ForEach-Object { | |
| Add-FileToQueue $_.FullName | |
| } | |
| } else { | |
| Add-FileToQueue $f | |
| } | |
| } | |
| Log "Added files via drag and drop" "success" | |
| }) | |
| # Click on row to show preview | |
| $grid.Add_SelectionChanged({ | |
| if ($grid.SelectedRows.Count -eq 1) { | |
| $filePath = $grid.SelectedRows[0].Cells["FileName"].Value | |
| if ($filePath) { Update-PreviewPanel $filePath } | |
| } | |
| }) | |
| # --------------------------- | |
| # Row 1: Output Settings (responsive) | |
| # --------------------------- | |
| $outputRow = New-Object System.Windows.Forms.Panel | |
| $outputRow.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $outputRow.BackColor = $theme.Panel | |
| $lblOut = New-StyledLabel "Output:" 8 8 | |
| $lblOut.ForeColor = $theme.Text | |
| $outputRow.Controls.Add($lblOut) | |
| $txtOut = New-StyledTextBox 65 5 300 | |
| $txtOut.Anchor = 'Top,Left,Right' | |
| $outputRow.Controls.Add($txtOut) | |
| $btnOut = New-StyledButton "Browse" 70 26 | |
| $btnOut.Anchor = 'Top,Right' | |
| $btnOut.Add_Click({ | |
| $fbd = New-Object System.Windows.Forms.FolderBrowserDialog | |
| if ($fbd.ShowDialog() -eq 'OK') { $txtOut.Text = $fbd.SelectedPath } | |
| }) | |
| $outputRow.Controls.Add($btnOut) | |
| $lblFmt = New-StyledLabel "Format:" 0 8 | |
| $lblFmt.ForeColor = $theme.Text | |
| $lblFmt.Anchor = 'Top,Right' | |
| $outputRow.Controls.Add($lblFmt) | |
| $cbFmt = New-StyledCombo 0 5 70 @("mp4","mkv","avi","mov","webm","mp3","wav","flac","gif","ts") | |
| $cbFmt.Anchor = 'Top,Right' | |
| $outputRow.Controls.Add($cbFmt) | |
| $lblPrefix = New-StyledLabel "Prefix:" 0 8 | |
| $lblPrefix.ForeColor = $theme.Text | |
| $lblPrefix.Anchor = 'Top,Right' | |
| $outputRow.Controls.Add($lblPrefix) | |
| $txtPrefix = New-StyledTextBox 0 5 70 | |
| $txtPrefix.Anchor = 'Top,Right' | |
| $outputRow.Controls.Add($txtPrefix) | |
| $lblSuffix = New-StyledLabel "Suffix:" 0 8 | |
| $lblSuffix.ForeColor = $theme.Text | |
| $lblSuffix.Anchor = 'Top,Right' | |
| $outputRow.Controls.Add($lblSuffix) | |
| $txtSuffix = New-StyledTextBox 0 5 85 "_out" | |
| $txtSuffix.Anchor = 'Top,Right' | |
| $outputRow.Controls.Add($txtSuffix) | |
| $outputRow.Add_Resize({ | |
| $w = $outputRow.Width | |
| $txtOut.Width = [Math]::Max(120, $w - 640) | |
| $btnOut.Location = New-Object System.Drawing.Point(($txtOut.Right + 5), 3) | |
| $lblFmt.Location = New-Object System.Drawing.Point(($btnOut.Right + 12), 8) | |
| $cbFmt.Location = New-Object System.Drawing.Point(($lblFmt.Right + 3), 5) | |
| $lblPrefix.Location = New-Object System.Drawing.Point(($cbFmt.Right + 12), 8) | |
| $txtPrefix.Location = New-Object System.Drawing.Point(($lblPrefix.Right + 3), 5) | |
| $lblSuffix.Location = New-Object System.Drawing.Point(($txtPrefix.Right + 8), 8) | |
| $txtSuffix.Location = New-Object System.Drawing.Point(($lblSuffix.Right + 3), 5) | |
| }) | |
| $mainLayout.Controls.Add($outputRow, 0, 1) | |
| # --------------------------- | |
| # Row 2: Settings (Video + Audio side by side) | |
| # --------------------------- | |
| $settingsRow = New-Object System.Windows.Forms.TableLayoutPanel | |
| $settingsRow.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $settingsRow.ColumnCount = 2 | |
| $settingsRow.RowCount = 1 | |
| $settingsRow.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 55))) | Out-Null | |
| $settingsRow.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 45))) | Out-Null | |
| # --- Video Settings --- | |
| $pnlVideo = New-Object System.Windows.Forms.Panel | |
| $pnlVideo.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $pnlVideo.BackColor = $theme.Panel | |
| $pnlVideo.Margin = New-Object System.Windows.Forms.Padding(0, 0, 3, 0) | |
| $lblVT = New-StyledLabel "Video" 8 3 -Title | |
| $lblVT.ForeColor = $theme.Accent | |
| $pnlVideo.Controls.Add($lblVT) | |
| # Resolution preset | |
| $lblRes = New-StyledLabel "Resolution:" 10 30 | |
| $lblRes.ForeColor = $theme.Text | |
| $pnlVideo.Controls.Add($lblRes) | |
| $cbRes = New-StyledCombo 90 27 155 @("Original","3840x2160 (4K)","2560x1440 (1440p)","1920x1080 (1080p)","1280x720 (720p)","854x480 (480p)","640x360 (360p)") | |
| $pnlVideo.Controls.Add($cbRes) | |
| # Custom resolution | |
| $chkCustomRes = New-StyledCheckBox "Custom:" 260 30 | |
| $pnlVideo.Controls.Add($chkCustomRes) | |
| $txtCustomW = New-StyledTextBox 340 27 55 "" | |
| $txtCustomW.Enabled = $false | |
| $pnlVideo.Controls.Add($txtCustomW) | |
| $lblResX = New-StyledLabel "x" 400 30 | |
| $lblResX.ForeColor = $theme.Text | |
| $pnlVideo.Controls.Add($lblResX) | |
| $txtCustomH = New-StyledTextBox 415 27 55 "" | |
| $txtCustomH.Enabled = $false | |
| $pnlVideo.Controls.Add($txtCustomH) | |
| $lblResPx = New-StyledLabel "px" 475 30 | |
| $lblResPx.ForeColor = [System.Drawing.Color]::Gray | |
| $pnlVideo.Controls.Add($lblResPx) | |
| $chkCustomRes.Add_CheckedChanged({ | |
| $txtCustomW.Enabled = $chkCustomRes.Checked | |
| $txtCustomH.Enabled = $chkCustomRes.Checked | |
| $cbRes.Enabled = -not $chkCustomRes.Checked | |
| }) | |
| # Quality CRF | |
| $lblCRF = New-StyledLabel "Quality (CRF):" 10 58 | |
| $lblCRF.ForeColor = $theme.Text | |
| $pnlVideo.Controls.Add($lblCRF) | |
| $trackCRF = New-Object System.Windows.Forms.TrackBar | |
| $trackCRF.Location = New-Object System.Drawing.Point(115, 52) | |
| $trackCRF.Size = New-Object System.Drawing.Size(180, 28) | |
| $trackCRF.Minimum = 0 | |
| $trackCRF.Maximum = 51 | |
| $trackCRF.Value = 23 | |
| $trackCRF.TickFrequency = 5 | |
| $trackCRF.BackColor = $theme.Panel | |
| $pnlVideo.Controls.Add($trackCRF) | |
| $lblCRFVal = New-StyledLabel "23 (Good)" 300 58 | |
| $lblCRFVal.ForeColor = $theme.Warning | |
| $pnlVideo.Controls.Add($lblCRFVal) | |
| $trackCRF.Add_ValueChanged({ | |
| $val = $trackCRF.Value | |
| $q = switch ($true) { | |
| ($val -le 12) { "Lossless-like"; break } | |
| ($val -le 18) { "Very High"; break } | |
| ($val -le 23) { "Good"; break } | |
| ($val -le 28) { "Medium"; break } | |
| ($val -le 35) { "Low"; break } | |
| default { "Very Low" } | |
| } | |
| $lblCRFVal.Text = "$val ($q)" | |
| }) | |
| # Video Codec | |
| $lblVCodec = New-StyledLabel "V.Codec:" 400 58 | |
| $lblVCodec.ForeColor = $theme.Text | |
| $pnlVideo.Controls.Add($lblVCodec) | |
| $cbVCodec = New-StyledCombo 470 55 110 @("Auto (libx264)","libx265 (HEVC)","libvpx-vp9","copy","mpeg4") | |
| $pnlVideo.Controls.Add($cbVCodec) | |
| # FPS | |
| $lblFPSLbl = New-StyledLabel "FPS:" 10 87 | |
| $lblFPSLbl.ForeColor = $theme.Text | |
| $pnlVideo.Controls.Add($lblFPSLbl) | |
| $txtFPS = New-StyledTextBox 45 84 45 "" | |
| $pnlVideo.Controls.Add($txtFPS) | |
| $lblFPSHint = New-StyledLabel "(blank=original)" 95 87 | |
| $lblFPSHint.ForeColor = [System.Drawing.Color]::Gray | |
| $lblFPSHint.Font = $fontSmall | |
| $pnlVideo.Controls.Add($lblFPSHint) | |
| # Bitrate | |
| $lblBitrate = New-StyledLabel "Bitrate:" 210 87 | |
| $lblBitrate.ForeColor = $theme.Text | |
| $pnlVideo.Controls.Add($lblBitrate) | |
| $txtBitrate = New-StyledTextBox 270 84 60 "" | |
| $pnlVideo.Controls.Add($txtBitrate) | |
| $lblBitrateHint = New-StyledLabel "(e.g. 5M, blank=auto)" 335 87 | |
| $lblBitrateHint.ForeColor = [System.Drawing.Color]::Gray | |
| $lblBitrateHint.Font = $fontSmall | |
| $pnlVideo.Controls.Add($lblBitrateHint) | |
| # HW Accel | |
| $lblHW = New-StyledLabel "HW Accel:" 10 115 | |
| $lblHW.ForeColor = $theme.Text | |
| $pnlVideo.Controls.Add($lblHW) | |
| $cbHWAccel = New-StyledCombo 85 112 120 @("None","NVIDIA (NVENC)","AMD (AMF)","Intel (QSV)","CUDA Decode") | |
| $pnlVideo.Controls.Add($cbHWAccel) | |
| # Filters | |
| $chkGray = New-StyledCheckBox "Grayscale" 230 115 | |
| $pnlVideo.Controls.Add($chkGray) | |
| $chkInvert = New-StyledCheckBox "Invert" 330 115 | |
| $pnlVideo.Controls.Add($chkInvert) | |
| $chkBlur = New-StyledCheckBox "Blur" 410 115 | |
| $pnlVideo.Controls.Add($chkBlur) | |
| # Subtitle | |
| $lblSub = New-StyledLabel "Sub:" 10 143 | |
| $lblSub.ForeColor = $theme.Text | |
| $pnlVideo.Controls.Add($lblSub) | |
| $txtSub = New-StyledTextBox 45 140 380 | |
| $txtSub.Anchor = 'Top,Left,Right' | |
| $pnlVideo.Controls.Add($txtSub) | |
| $btnSub = New-StyledButton "..." 30 24 | |
| $btnSub.Anchor = 'Top,Right' | |
| $btnSub.Add_Click({ | |
| $ofd = New-Object System.Windows.Forms.OpenFileDialog | |
| $ofd.Filter = "Subtitle Files|*.srt;*.ass;*.ssa;*.sub;*.vtt" | |
| if ($ofd.ShowDialog() -eq 'OK') { $txtSub.Text = $ofd.FileName } | |
| }) | |
| $pnlVideo.Controls.Add($btnSub) | |
| $pnlVideo.Add_Resize({ | |
| $txtSub.Width = [Math]::Max(100, $pnlVideo.Width - 95) | |
| $btnSub.Location = New-Object System.Drawing.Point(($pnlVideo.Width - 40), 138) | |
| }) | |
| $settingsRow.Controls.Add($pnlVideo, 0, 0) | |
| # --- Audio & Trim --- | |
| $pnlAudio = New-Object System.Windows.Forms.Panel | |
| $pnlAudio.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $pnlAudio.BackColor = $theme.Panel | |
| $pnlAudio.Margin = New-Object System.Windows.Forms.Padding(3, 0, 0, 0) | |
| $lblAT = New-StyledLabel "Audio & Trim" 8 3 -Title | |
| $lblAT.ForeColor = $theme.Accent | |
| $pnlAudio.Controls.Add($lblAT) | |
| # Volume | |
| $lblVol = New-StyledLabel "Volume:" 10 30 | |
| $lblVol.ForeColor = $theme.Text | |
| $pnlAudio.Controls.Add($lblVol) | |
| $trackVol = New-Object System.Windows.Forms.TrackBar | |
| $trackVol.Location = New-Object System.Drawing.Point(70, 24) | |
| $trackVol.Size = New-Object System.Drawing.Size(150, 28) | |
| $trackVol.Minimum = 0 | |
| $trackVol.Maximum = 300 | |
| $trackVol.Value = 100 | |
| $trackVol.TickFrequency = 50 | |
| $trackVol.BackColor = $theme.Panel | |
| $pnlAudio.Controls.Add($trackVol) | |
| $lblVolVal = New-StyledLabel "100%" 225 30 | |
| $lblVolVal.ForeColor = $theme.Warning | |
| $pnlAudio.Controls.Add($lblVolVal) | |
| $trackVol.Add_ValueChanged({ $lblVolVal.Text = "$($trackVol.Value)%" }) | |
| # Audio Codec | |
| $lblACodec = New-StyledLabel "A.Codec:" 280 30 | |
| $lblACodec.ForeColor = $theme.Text | |
| $pnlAudio.Controls.Add($lblACodec) | |
| $cbACodec = New-StyledCombo 345 27 90 @("Auto","aac","mp3","opus","flac","copy") | |
| $pnlAudio.Controls.Add($cbACodec) | |
| # Speed | |
| $lblSpeed = New-StyledLabel "Speed:" 10 60 | |
| $lblSpeed.ForeColor = $theme.Text | |
| $pnlAudio.Controls.Add($lblSpeed) | |
| $txtSpeed = New-StyledTextBox 65 57 50 "1.0" | |
| $pnlAudio.Controls.Add($txtSpeed) | |
| $lblSpeedHint = New-StyledLabel "x (0.25 - 4.0)" 120 60 | |
| $lblSpeedHint.ForeColor = [System.Drawing.Color]::Gray | |
| $lblSpeedHint.Font = $fontSmall | |
| $pnlAudio.Controls.Add($lblSpeedHint) | |
| # Mute | |
| $chkMute = New-StyledCheckBox "Strip Audio" 260 60 | |
| $pnlAudio.Controls.Add($chkMute) | |
| # Trim | |
| $lblStart = New-StyledLabel "Trim Start:" 10 90 | |
| $lblStart.ForeColor = $theme.Text | |
| $pnlAudio.Controls.Add($lblStart) | |
| $txtStart = New-StyledTextBox 85 87 90 "00:00:00" | |
| $pnlAudio.Controls.Add($txtStart) | |
| $lblEnd = New-StyledLabel "Trim End:" 190 90 | |
| $lblEnd.ForeColor = $theme.Text | |
| $pnlAudio.Controls.Add($lblEnd) | |
| $txtEnd = New-StyledTextBox 260 87 90 "00:00:00" | |
| $pnlAudio.Controls.Add($txtEnd) | |
| # Quick trim buttons | |
| $btnTrimReset = New-StyledButton "Reset Trim" 86 24 | |
| $btnTrimReset.Location = New-Object System.Drawing.Point(365, 87) | |
| $btnTrimReset.Font = $fontSmall | |
| $btnTrimReset.Add_Click({ $txtStart.Text = "00:00:00"; $txtEnd.Text = "00:00:00" }) | |
| $pnlAudio.Controls.Add($btnTrimReset) | |
| # Extra options | |
| $chkFade = New-StyledCheckBox "Fade In/Out" 10 120 | |
| $pnlAudio.Controls.Add($chkFade) | |
| $chkNormalize = New-StyledCheckBox "Normalize Audio" 130 120 | |
| $pnlAudio.Controls.Add($chkNormalize) | |
| $settingsRow.Controls.Add($pnlAudio, 1, 0) | |
| $mainLayout.Controls.Add($settingsRow, 0, 2) | |
| # --------------------------- | |
| # Row 3: Progress | |
| # --------------------------- | |
| $progressPanel = New-Object System.Windows.Forms.Panel | |
| $progressPanel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $progressPanel.BackColor = $theme.Panel | |
| $lblCur = New-StyledLabel "Ready" 10 5 | |
| $lblCur.ForeColor = $theme.Text | |
| $lblCur.MaximumSize = New-Object System.Drawing.Size(500, 0) | |
| $progressPanel.Controls.Add($lblCur) | |
| $lblETA = New-StyledLabel "" 520 5 | |
| $lblETA.ForeColor = $theme.Warning | |
| $lblETA.Anchor = 'Top,Right' | |
| $progressPanel.Controls.Add($lblETA) | |
| $lblFileProgress = New-StyledLabel "File: 0%" 10 32 | |
| $lblFileProgress.ForeColor = $theme.Accent | |
| $progressPanel.Controls.Add($lblFileProgress) | |
| $progressFile = New-Object System.Windows.Forms.ProgressBar | |
| $progressFile.Location = New-Object System.Drawing.Point(80, 30) | |
| $progressFile.Size = New-Object System.Drawing.Size(350, 16) | |
| $progressFile.Style = 'Continuous' | |
| $progressFile.Anchor = 'Top,Left,Right' | |
| $progressPanel.Controls.Add($progressFile) | |
| $lblTotalProgress = New-StyledLabel "Total: 0%" 10 55 | |
| $lblTotalProgress.ForeColor = $theme.Accent | |
| $progressPanel.Controls.Add($lblTotalProgress) | |
| $progressTotal = New-Object System.Windows.Forms.ProgressBar | |
| $progressTotal.Location = New-Object System.Drawing.Point(80, 53) | |
| $progressTotal.Size = New-Object System.Drawing.Size(350, 16) | |
| $progressTotal.Style = 'Continuous' | |
| $progressTotal.Anchor = 'Top,Left,Right' | |
| $progressPanel.Controls.Add($progressTotal) | |
| $progressPanel.Add_Resize({ | |
| $half = [int](($progressPanel.Width - 100) / 2) | |
| $progressFile.Width = $half | |
| $progressTotal.Location = New-Object System.Drawing.Point(($progressFile.Right + 80), 53) | |
| $progressTotal.Width = [Math]::Max(80, $progressPanel.Width - $progressTotal.Left - 10) | |
| $lblTotalProgress.Location = New-Object System.Drawing.Point(($progressFile.Right + 15), 55) | |
| $lblETA.Location = New-Object System.Drawing.Point(($progressFile.Right + 15), 32) | |
| }) | |
| $mainLayout.Controls.Add($progressPanel, 0, 3) | |
| # --------------------------- | |
| # Row 4: Log | |
| # --------------------------- | |
| $logPanel = New-Object System.Windows.Forms.Panel | |
| $logPanel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $logPanel.BackColor = $theme.Panel | |
| $logPanel.Padding = New-Object System.Windows.Forms.Padding(5) | |
| $lblLogT = New-StyledLabel "Log" 8 3 | |
| $lblLogT.ForeColor = $theme.Accent | |
| $lblLogT.Font = $fontBold | |
| $logPanel.Controls.Add($lblLogT) | |
| $btnClearLog = New-StyledButton "Clear Log" 70 22 | |
| $btnClearLog.Font = $fontSmall | |
| $btnClearLog.Anchor = 'Top,Right' | |
| $btnClearLog.Add_Click({ $txtLog.Clear() }) | |
| $logPanel.Controls.Add($btnClearLog) | |
| $btnSaveLog = New-StyledButton "Save Log" 70 22 | |
| $btnSaveLog.Font = $fontSmall | |
| $btnSaveLog.Anchor = 'Top,Right' | |
| $btnSaveLog.Add_Click({ | |
| $sfd = New-Object System.Windows.Forms.SaveFileDialog | |
| $sfd.Filter = "Text Files|*.txt|All|*.*" | |
| $sfd.FileName = "ffmpeg_log_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" | |
| if ($sfd.ShowDialog() -eq 'OK') { | |
| $txtLog.Text | Set-Content $sfd.FileName | |
| Log "Log saved to: $($sfd.FileName)" "success" | |
| } | |
| }) | |
| $logPanel.Controls.Add($btnSaveLog) | |
| $txtLog = New-Object System.Windows.Forms.TextBox | |
| $txtLog.Location = New-Object System.Drawing.Point(5, 28) | |
| $txtLog.Multiline = $true | |
| $txtLog.ScrollBars = 'Both' | |
| $txtLog.ReadOnly = $true | |
| $txtLog.BackColor = $theme.LogBg | |
| $txtLog.ForeColor = [System.Drawing.Color]::FromArgb(0, 255, 0) | |
| $txtLog.Font = $fontMono | |
| $txtLog.BorderStyle = 'None' | |
| $txtLog.WordWrap = $false | |
| $txtLog.Anchor = 'Top,Bottom,Left,Right' | |
| $logPanel.Controls.Add($txtLog) | |
| $logPanel.Add_Resize({ | |
| $txtLog.Size = New-Object System.Drawing.Size(($logPanel.Width - 12), ($logPanel.Height - 34)) | |
| $btnClearLog.Location = New-Object System.Drawing.Point(($logPanel.Width - 155), 2) | |
| $btnSaveLog.Location = New-Object System.Drawing.Point(($logPanel.Width - 80), 2) | |
| }) | |
| $mainLayout.Controls.Add($logPanel, 0, 4) | |
| # --------------------------- | |
| # Row 5: Action Buttons | |
| # --------------------------- | |
| $actionPanel = New-Object System.Windows.Forms.FlowLayoutPanel | |
| $actionPanel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $actionPanel.BackColor = $theme.Background | |
| $actionPanel.Padding = New-Object System.Windows.Forms.Padding(0, 5, 0, 0) | |
| $actionPanel.WrapContents = $true | |
| $btnAdd = New-StyledButton "Add Files" 110 38 | |
| $btnAddFolder = New-StyledButton "Add Folder" 110 38 | |
| $btnClear = New-StyledButton "Clear Queue" 110 38 | |
| $btnStart = New-StyledButton "START" 160 38 $theme.Success ([System.Drawing.Color]::White) | |
| $btnCancel = New-StyledButton "CANCEL" 110 38 $theme.Error ([System.Drawing.Color]::White) | |
| $btnCancel.Enabled = $false | |
| $btnOpenFolder = New-StyledButton "Open Output" 110 38 | |
| $actionPanel.Controls.AddRange(@($btnAdd, $btnAddFolder, $btnClear, $btnStart, $btnCancel, $btnOpenFolder)) | |
| $mainLayout.Controls.Add($actionPanel, 0, 5) | |
| # ========================================== | |
| # TAB 2: QUICK PRESETS | |
| # ========================================== | |
| $presetScroll = New-Object System.Windows.Forms.FlowLayoutPanel | |
| $presetScroll.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $presetScroll.FlowDirection = 'LeftToRight' | |
| $presetScroll.WrapContents = $true | |
| $presetScroll.AutoScroll = $true | |
| $presetScroll.BackColor = $theme.Background | |
| $presetScroll.Padding = New-Object System.Windows.Forms.Padding(10) | |
| $tabPresets.Controls.Add($presetScroll) | |
| $presets = @( | |
| @{ Name="YouTube 1080p"; Desc="H.264 AAC optimized for YouTube"; Cat="Social"; Fmt="mp4"; Res="1920x1080 (1080p)"; CRF=20; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="YouTube 4K"; Desc="4K upload, high quality"; Cat="Social"; Fmt="mp4"; Res="3840x2160 (4K)"; CRF=18; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Discord <25MB"; Desc="Compressed 720p for Discord"; Cat="Social"; Fmt="mp4"; Res="1280x720 (720p)"; CRF=30; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Discord <8MB"; Desc="Max compress for free Discord"; Cat="Social"; Fmt="mp4"; Res="854x480 (480p)"; CRF=36; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Twitter/X"; Desc="720p fast encode"; Cat="Social"; Fmt="mp4"; Res="1280x720 (720p)"; CRF=25; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Instagram Reel"; Desc="1080x1920 vertical"; Cat="Social"; Fmt="mp4"; Res="Original"; CRF=22; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0; CustomW="1080"; CustomH="1920" }, | |
| @{ Name="TikTok"; Desc="1080x1920 vertical short"; Cat="Social"; Fmt="mp4"; Res="Original"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0; CustomW="1080"; CustomH="1920" }, | |
| @{ Name="WhatsApp"; Desc="480p small file"; Cat="Social"; Fmt="mp4"; Res="854x480 (480p)"; CRF=32; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Telegram"; Desc="720p good quality"; Cat="Social"; Fmt="mp4"; Res="1280x720 (720p)"; CRF=24; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Mute Video"; Desc="Strip all audio, keep video"; Cat="Audio"; Fmt="mp4"; Res="Original"; CRF=18; Mute=$true; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Extract MP3"; Desc="Audio only, MP3 format"; Cat="Audio"; Fmt="mp3"; Res="Original"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=2 }, | |
| @{ Name="Extract WAV"; Desc="Lossless WAV audio"; Cat="Audio"; Fmt="wav"; Res="Original"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Extract FLAC"; Desc="Lossless FLAC audio"; Cat="Audio"; Fmt="flac"; Res="Original"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=4 }, | |
| @{ Name="Boost Vol 200%"; Desc="Double audio volume"; Cat="Audio"; Fmt="mp4"; Res="Original"; CRF=18; Mute=$false; Vol=200; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Reduce Vol 50%"; Desc="Halve audio volume"; Cat="Audio"; Fmt="mp4"; Res="Original"; CRF=18; Mute=$false; Vol=50; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Boost Vol 300%"; Desc="Triple audio volume"; Cat="Audio"; Fmt="mp4"; Res="Original"; CRF=18; Mute=$false; Vol=300; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="GIF (480p)"; Desc="Animated GIF 480p"; Cat="Convert"; Fmt="gif"; Res="854x480 (480p)"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="GIF (360p)"; Desc="Animated GIF 360p smaller"; Cat="Convert"; Fmt="gif"; Res="640x360 (360p)"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="To MKV"; Desc="Remux to MKV container"; Cat="Convert"; Fmt="mkv"; Res="Original"; CRF=18; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="To WebM"; Desc="WebM for web"; Cat="Convert"; Fmt="webm"; Res="Original"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=2; ACodec=3 }, | |
| @{ Name="To AVI"; Desc="AVI legacy format"; Cat="Convert"; Fmt="avi"; Res="Original"; CRF=18; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="To MOV"; Desc="MOV for Apple"; Cat="Convert"; Fmt="mov"; Res="Original"; CRF=18; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="HEVC/H.265"; Desc="Modern codec, smaller files"; Cat="Convert"; Fmt="mp4"; Res="Original"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=1; ACodec=0 }, | |
| @{ Name="720p"; Desc="Downscale to 720p"; Cat="Resize"; Fmt="mp4"; Res="1280x720 (720p)"; CRF=20; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="480p"; Desc="Downscale to 480p"; Cat="Resize"; Fmt="mp4"; Res="854x480 (480p)"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="360p"; Desc="Downscale to 360p"; Cat="Resize"; Fmt="mp4"; Res="640x360 (360p)"; CRF=25; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="1080p"; Desc="Scale to 1080p"; Cat="Resize"; Fmt="mp4"; Res="1920x1080 (1080p)"; CRF=18; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="1440p"; Desc="Scale to 1440p"; Cat="Resize"; Fmt="mp4"; Res="2560x1440 (1440p)"; CRF=18; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Square 1080"; Desc="1080x1080 square crop"; Cat="Resize"; Fmt="mp4"; Res="Original"; CRF=20; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0; CustomW="1080"; CustomH="1080" }, | |
| @{ Name="2x Speed"; Desc="Double playback speed"; Cat="Speed"; Fmt="mp4"; Res="Original"; CRF=20; Mute=$false; Vol=100; Speed="2.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="1.5x Speed"; Desc="50% faster playback"; Cat="Speed"; Fmt="mp4"; Res="Original"; CRF=20; Mute=$false; Vol=100; Speed="1.5"; VCodec=0; ACodec=0 }, | |
| @{ Name="0.5x Slow"; Desc="Half speed slow-mo"; Cat="Speed"; Fmt="mp4"; Res="Original"; CRF=20; Mute=$false; Vol=100; Speed="0.5"; VCodec=0; ACodec=0 }, | |
| @{ Name="0.25x Ultra Slow"; Desc="Quarter speed ultra slo-mo"; Cat="Speed"; Fmt="mp4"; Res="Original"; CRF=18; Mute=$false; Vol=100; Speed="0.25"; VCodec=0; ACodec=0 }, | |
| @{ Name="3x Fast"; Desc="Triple speed timelapse"; Cat="Speed"; Fmt="mp4"; Res="Original"; CRF=20; Mute=$false; Vol=100; Speed="3.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Archive Lossless"; Desc="MKV near-lossless CRF 8"; Cat="Quality"; Fmt="mkv"; Res="Original"; CRF=8; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=4 }, | |
| @{ Name="4K Preserve"; Desc="4K high quality"; Cat="Quality"; Fmt="mp4"; Res="3840x2160 (4K)"; CRF=16; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Max Compress"; Desc="Smallest possible file"; Cat="Quality"; Fmt="mp4"; Res="640x360 (360p)"; CRF=42; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="Balanced"; Desc="Good quality, reasonable size"; Cat="Quality"; Fmt="mp4"; Res="Original"; CRF=23; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 }, | |
| @{ Name="High Quality"; Desc="Very high quality, larger"; Cat="Quality"; Fmt="mp4"; Res="Original"; CRF=15; Mute=$false; Vol=100; Speed="1.0"; VCodec=0; ACodec=0 } | |
| ) | |
| $catColors = @{ | |
| "Social" = [System.Drawing.Color]::FromArgb(0, 122, 204) | |
| "Audio" = [System.Drawing.Color]::FromArgb(156, 39, 176) | |
| "Convert" = [System.Drawing.Color]::FromArgb(255, 152, 0) | |
| "Resize" = [System.Drawing.Color]::FromArgb(0, 150, 136) | |
| "Speed" = [System.Drawing.Color]::FromArgb(233, 30, 99) | |
| "Quality" = [System.Drawing.Color]::FromArgb(76, 175, 80) | |
| } | |
| $currentCat = "" | |
| foreach ($preset in ($presets | Sort-Object { $_.Cat })) { | |
| if ($preset.Cat -ne $currentCat) { | |
| $currentCat = $preset.Cat | |
| $catHeader = New-Object System.Windows.Forms.Label | |
| $catHeader.Text = " $currentCat" | |
| $catHeader.Size = New-Object System.Drawing.Size(1300, 28) | |
| $catHeader.Font = $fontTitle | |
| $catHeader.ForeColor = if ($catColors.ContainsKey($currentCat)) { $catColors[$currentCat] } else { $theme.Accent } | |
| $catHeader.BackColor = [System.Drawing.Color]::Transparent | |
| $catHeader.TextAlign = 'MiddleLeft' | |
| $catHeader.Margin = New-Object System.Windows.Forms.Padding(0, 10, 0, 2) | |
| $presetScroll.Controls.Add($catHeader) | |
| $sep = New-Object System.Windows.Forms.Label | |
| $sep.Size = New-Object System.Drawing.Size(1300, 1) | |
| $sep.BackColor = $theme.Control | |
| $sep.Margin = New-Object System.Windows.Forms.Padding(5, 0, 5, 5) | |
| $presetScroll.Controls.Add($sep) | |
| } | |
| $presetBox = New-Object System.Windows.Forms.Panel | |
| $presetBox.Size = New-Object System.Drawing.Size(230, 75) | |
| $presetBox.BackColor = $theme.PresetBg | |
| $presetBox.Margin = New-Object System.Windows.Forms.Padding(6, 3, 6, 3) | |
| $presetBox.Cursor = [System.Windows.Forms.Cursors]::Hand | |
| $strip = New-Object System.Windows.Forms.Panel | |
| $strip.Size = New-Object System.Drawing.Size(4, 75) | |
| $strip.Location = New-Object System.Drawing.Point(0, 0) | |
| $strip.BackColor = if ($catColors.ContainsKey($preset.Cat)) { $catColors[$preset.Cat] } else { $theme.Accent } | |
| $presetBox.Controls.Add($strip) | |
| $pName = New-StyledLabel $preset.Name 12 5 | |
| $pName.ForeColor = [System.Drawing.Color]::White | |
| $pName.Font = $fontBold | |
| $presetBox.Controls.Add($pName) | |
| $pDesc = New-StyledLabel $preset.Desc 12 25 | |
| $pDesc.ForeColor = [System.Drawing.Color]::FromArgb(170, 170, 170) | |
| $pDesc.Font = $fontSmall | |
| $pDesc.MaximumSize = New-Object System.Drawing.Size(210, 0) | |
| $presetBox.Controls.Add($pDesc) | |
| $tagText = "$($preset.Fmt.ToUpper())" | |
| if ($preset.CustomW) { $tagText += " | $($preset.CustomW)x$($preset.CustomH)" } | |
| elseif ($preset.Res -ne "Original") { $tagText += " | $($preset.Res -replace ' \(.*\)','')" } | |
| if ($preset.Mute) { $tagText += " | MUTE" } | |
| if ($preset.Speed -ne "1.0") { $tagText += " | $($preset.Speed)x" } | |
| $pInfo = New-StyledLabel $tagText 12 55 | |
| $pInfo.ForeColor = [System.Drawing.Color]::FromArgb(110, 110, 110) | |
| $pInfo.Font = $fontSmall | |
| $presetBox.Controls.Add($pInfo) | |
| $cap = $preset | |
| foreach ($ctrl in @($presetBox, $strip, $pName, $pDesc, $pInfo)) { | |
| $ctrl.Add_MouseEnter({ $presetBox.BackColor = $theme.PresetHover }.GetNewClosure()) | |
| $ctrl.Add_MouseLeave({ $presetBox.BackColor = $theme.PresetBg }.GetNewClosure()) | |
| $ctrl.Add_Click({ | |
| $tabs.SelectedTab = $tabConvert | |
| # Format | |
| for ($i = 0; $i -lt $cbFmt.Items.Count; $i++) { | |
| if ($cbFmt.Items[$i] -eq $cap.Fmt) { $cbFmt.SelectedIndex = $i; break } | |
| } | |
| # Resolution | |
| if ($cap.CustomW) { | |
| $chkCustomRes.Checked = $true | |
| $txtCustomW.Text = $cap.CustomW | |
| $txtCustomH.Text = $cap.CustomH | |
| } else { | |
| $chkCustomRes.Checked = $false | |
| for ($i = 0; $i -lt $cbRes.Items.Count; $i++) { | |
| $ri = $cbRes.Items[$i].ToString() | |
| if ($cap.Res -eq "Original" -and $i -eq 0) { $cbRes.SelectedIndex = $i; break } | |
| if ($ri -eq $cap.Res -or $ri -match ($cap.Res -replace ' \(.*\)','')) { $cbRes.SelectedIndex = $i; break } | |
| } | |
| } | |
| $trackCRF.Value = $cap.CRF | |
| $chkMute.Checked = $cap.Mute | |
| $trackVol.Value = $cap.Vol | |
| $txtSpeed.Text = $cap.Speed | |
| if ($null -ne $cap.VCodec) { $cbVCodec.SelectedIndex = [Math]::Min($cap.VCodec, $cbVCodec.Items.Count - 1) } | |
| if ($null -ne $cap.ACodec) { $cbACodec.SelectedIndex = [Math]::Min($cap.ACodec, $cbACodec.Items.Count - 1) } | |
| $chkGray.Checked = $false | |
| $chkInvert.Checked = $false | |
| $chkBlur.Checked = $false | |
| Log "Preset: $($cap.Name) - $($cap.Desc)" "success" | |
| }.GetNewClosure()) | |
| } | |
| $presetScroll.Controls.Add($presetBox) | |
| } | |
| # ========================================== | |
| # TAB 3: PREVIEW / INFO | |
| # ========================================== | |
| $previewLayout = New-Object System.Windows.Forms.TableLayoutPanel | |
| $previewLayout.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $previewLayout.ColumnCount = 2 | |
| $previewLayout.RowCount = 1 | |
| $previewLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 45))) | Out-Null | |
| $previewLayout.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle('Percent', 55))) | Out-Null | |
| $tabPreview.Controls.Add($previewLayout) | |
| # Left: Thumbnail | |
| $previewImgPanel = New-Object System.Windows.Forms.Panel | |
| $previewImgPanel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $previewImgPanel.BackColor = $theme.ThumbBg | |
| $previewImgPanel.Padding = New-Object System.Windows.Forms.Padding(10) | |
| $picPreview = New-Object System.Windows.Forms.PictureBox | |
| $picPreview.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $picPreview.SizeMode = 'Zoom' | |
| $picPreview.BackColor = $theme.ThumbBg | |
| $previewImgPanel.Controls.Add($picPreview) | |
| $btnPlayPreview = New-StyledButton "Play in Default Player" 200 36 | |
| $btnPlayPreview.Dock = [System.Windows.Forms.DockStyle]::Bottom | |
| $btnPlayPreview.Add_Click({ | |
| if ($grid.SelectedRows.Count -eq 1) { | |
| $f = $grid.SelectedRows[0].Cells["FileName"].Value | |
| if ($f -and (Test-Path $f)) { Start-Process $f } | |
| } | |
| }) | |
| $previewImgPanel.Controls.Add($btnPlayPreview) | |
| $previewLayout.Controls.Add($previewImgPanel, 0, 0) | |
| # Right: Media Info | |
| $previewInfoPanel = New-Object System.Windows.Forms.Panel | |
| $previewInfoPanel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $previewInfoPanel.BackColor = $theme.Panel | |
| $previewInfoPanel.Padding = New-Object System.Windows.Forms.Padding(15) | |
| $lblPreviewTitle = New-StyledLabel "Media Information" 10 10 -Title | |
| $lblPreviewTitle.ForeColor = $theme.Accent | |
| $previewInfoPanel.Controls.Add($lblPreviewTitle) | |
| $txtMediaInfo = New-Object System.Windows.Forms.TextBox | |
| $txtMediaInfo.Location = New-Object System.Drawing.Point(10, 40) | |
| $txtMediaInfo.Multiline = $true | |
| $txtMediaInfo.ReadOnly = $true | |
| $txtMediaInfo.ScrollBars = 'Vertical' | |
| $txtMediaInfo.BackColor = $theme.LogBg | |
| $txtMediaInfo.ForeColor = $theme.Text | |
| $txtMediaInfo.Font = $fontMono | |
| $txtMediaInfo.BorderStyle = 'None' | |
| $txtMediaInfo.WordWrap = $true | |
| $txtMediaInfo.Anchor = 'Top,Bottom,Left,Right' | |
| $txtMediaInfo.Text = "Select a file from the queue to see details..." | |
| $previewInfoPanel.Controls.Add($txtMediaInfo) | |
| $previewInfoPanel.Add_Resize({ | |
| $txtMediaInfo.Size = New-Object System.Drawing.Size(($previewInfoPanel.Width - 25), ($previewInfoPanel.Height - 55)) | |
| }) | |
| $previewLayout.Controls.Add($previewInfoPanel, 1, 0) | |
| function Update-PreviewPanel { | |
| param([string]$FilePath) | |
| if (-not $FilePath -or -not (Test-Path $FilePath)) { return } | |
| # Thumbnail | |
| try { | |
| $hash = [System.IO.Path]::GetFileName($FilePath).GetHashCode().ToString("X8") | |
| $thumbFile = Join-Path $script:thumbCachePath "${hash}_large.jpg" | |
| if (-not (Test-Path $thumbFile)) { | |
| & ffmpeg -y -i "$FilePath" -ss 00:00:02 -vframes 1 -s "640x360" -q:v 3 "$thumbFile" 2>&1 | Out-Null | |
| } | |
| if (Test-Path $thumbFile) { | |
| $picPreview.Image = [System.Drawing.Image]::FromFile($thumbFile) | |
| } | |
| } catch {} | |
| # Media info text | |
| $info = Get-MediaInfo $FilePath | |
| $infoText = @" | |
| File: $(Split-Path $FilePath -Leaf) | |
| Path: $FilePath | |
| Size: $($info.Size) | |
| Duration: $($info.Duration) | |
| Resolution: $($info.Resolution) | |
| V.Codec: $($info.Codec) | |
| A.Codec: $($info.AudioCodec) | |
| Bitrate: $($info.Bitrate) | |
| Frame Rate: $($info.FrameRate) | |
| --- Full FFprobe Output --- | |
| "@ | |
| try { | |
| $fullProbe = & ffprobe -v quiet -print_format json -show_format -show_streams "$FilePath" 2>&1 | |
| $infoText += "`r`n$fullProbe" | |
| } catch { | |
| $infoText += "`r`n(FFprobe not available)" | |
| } | |
| $txtMediaInfo.Text = $infoText | |
| } | |
| # ========================================== | |
| # TAB 4: SETTINGS | |
| # ========================================== | |
| $settingsScroll = New-Object System.Windows.Forms.Panel | |
| $settingsScroll.Dock = [System.Windows.Forms.DockStyle]::Fill | |
| $settingsScroll.AutoScroll = $true | |
| $settingsScroll.BackColor = $theme.Background | |
| $settingsScroll.Padding = New-Object System.Windows.Forms.Padding(20) | |
| $tabSettings.Controls.Add($settingsScroll) | |
| $lblSettingsTitle = New-StyledLabel "Settings & Preferences" 10 10 -Title | |
| $lblSettingsTitle.ForeColor = $theme.Accent | |
| $settingsScroll.Controls.Add($lblSettingsTitle) | |
| # General Options | |
| $gbGeneral = New-Object System.Windows.Forms.GroupBox | |
| $gbGeneral.Text = "General" | |
| $gbGeneral.ForeColor = $theme.Accent | |
| $gbGeneral.Font = $fontBold | |
| $gbGeneral.BackColor = $theme.Panel | |
| $gbGeneral.Location = New-Object System.Drawing.Point(10, 45) | |
| $gbGeneral.Size = New-Object System.Drawing.Size(500, 160) | |
| $settingsScroll.Controls.Add($gbGeneral) | |
| $chkAutoOpen = New-StyledCheckBox "Auto-open output folder when done" 15 25 | |
| $gbGeneral.Controls.Add($chkAutoOpen) | |
| $chkOverwrite = New-StyledCheckBox "Overwrite files without asking" 15 50 | |
| $gbGeneral.Controls.Add($chkOverwrite) | |
| $chkShutdown = New-StyledCheckBox "Shutdown PC after batch completes" 15 75 | |
| $chkShutdown.ForeColor = $theme.Error | |
| $gbGeneral.Controls.Add($chkShutdown) | |
| $chkSaveConfig = New-StyledCheckBox "Auto-save settings on exit" 15 100 | |
| $chkSaveConfig.Checked = $true | |
| $gbGeneral.Controls.Add($chkSaveConfig) | |
| $chkGenThumb = New-StyledCheckBox "Generate thumbnail previews (slower add)" 15 125 | |
| $chkGenThumb.Checked = $true | |
| $gbGeneral.Controls.Add($chkGenThumb) | |
| # Config buttons | |
| $gbConfigBtns = New-Object System.Windows.Forms.GroupBox | |
| $gbConfigBtns.Text = "Configuration" | |
| $gbConfigBtns.ForeColor = $theme.Accent | |
| $gbConfigBtns.Font = $fontBold | |
| $gbConfigBtns.BackColor = $theme.Panel | |
| $gbConfigBtns.Location = New-Object System.Drawing.Point(10, 215) | |
| $gbConfigBtns.Size = New-Object System.Drawing.Size(500, 80) | |
| $settingsScroll.Controls.Add($gbConfigBtns) | |
| $btnSaveSettings = New-StyledButton "Save Settings" 130 34 $theme.Success ([System.Drawing.Color]::White) | |
| $btnSaveSettings.Location = New-Object System.Drawing.Point(15, 30) | |
| $btnSaveSettings.Add_Click({ | |
| Save-Config | |
| Log "Settings saved to: $($script:configFile)" "success" | |
| [System.Windows.Forms.MessageBox]::Show("Settings saved!", "Saved", 'OK', 'Information') | |
| }) | |
| $gbConfigBtns.Controls.Add($btnSaveSettings) | |
| $btnLoadSettings = New-StyledButton "Load Settings" 130 34 | |
| $btnLoadSettings.Location = New-Object System.Drawing.Point(155, 30) | |
| $btnLoadSettings.Add_Click({ | |
| Load-Config | |
| Log "Settings loaded" "success" | |
| }) | |
| $gbConfigBtns.Controls.Add($btnLoadSettings) | |
| $btnResetSettings = New-StyledButton "Reset Defaults" 130 34 $theme.Error ([System.Drawing.Color]::White) | |
| $btnResetSettings.Location = New-Object System.Drawing.Point(295, 30) | |
| $btnResetSettings.Add_Click({ | |
| $confirm = [System.Windows.Forms.MessageBox]::Show("Reset all settings to defaults?", "Reset", 'YesNo', 'Warning') | |
| if ($confirm -eq 'Yes') { | |
| if (Test-Path $script:configFile) { Remove-Item $script:configFile -Force } | |
| $cbFmt.SelectedIndex = 0; $cbRes.SelectedIndex = 0; $trackCRF.Value = 23; $trackVol.Value = 100 | |
| $txtSpeed.Text = "1.0"; $txtPrefix.Text = ""; $txtSuffix.Text = "_out" | |
| $chkMute.Checked = $false; $chkGray.Checked = $false; $chkInvert.Checked = $false; $chkBlur.Checked = $false | |
| $cbVCodec.SelectedIndex = 0; $cbACodec.SelectedIndex = 0; $cbHWAccel.SelectedIndex = 0 | |
| $txtBitrate.Text = ""; $txtFPS.Text = ""; $txtStart.Text = "00:00:00"; $txtEnd.Text = "00:00:00" | |
| $chkCustomRes.Checked = $false; $txtCustomW.Text = ""; $txtCustomH.Text = "" | |
| $chkAutoOpen.Checked = $false; $chkOverwrite.Checked = $false; $chkShutdown.Checked = $false | |
| Log "Settings reset to defaults" "warn" | |
| } | |
| }) | |
| $gbConfigBtns.Controls.Add($btnResetSettings) | |
| # FFmpeg Path | |
| $gbFFmpeg = New-Object System.Windows.Forms.GroupBox | |
| $gbFFmpeg.Text = "FFmpeg" | |
| $gbFFmpeg.ForeColor = $theme.Accent | |
| $gbFFmpeg.Font = $fontBold | |
| $gbFFmpeg.BackColor = $theme.Panel | |
| $gbFFmpeg.Location = New-Object System.Drawing.Point(10, 305) | |
| $gbFFmpeg.Size = New-Object System.Drawing.Size(500, 70) | |
| $settingsScroll.Controls.Add($gbFFmpeg) | |
| $lblFFPath = New-StyledLabel "FFmpeg Path:" 15 30 | |
| $lblFFPath.ForeColor = $theme.Text | |
| $gbFFmpeg.Controls.Add($lblFFPath) | |
| $txtFFPath = New-StyledTextBox 110 27 250 "ffmpeg.exe" | |
| $gbFFmpeg.Controls.Add($txtFFPath) | |
| $btnFFBrowse = New-StyledButton "Browse" 70 26 | |
| $btnFFBrowse.Location = New-Object System.Drawing.Point(370, 26) | |
| $btnFFBrowse.Add_Click({ | |
| $ofd = New-Object System.Windows.Forms.OpenFileDialog | |
| $ofd.Filter = "FFmpeg|ffmpeg.exe|All|*.*" | |
| if ($ofd.ShowDialog() -eq 'OK') { $txtFFPath.Text = $ofd.FileName } | |
| }) | |
| $gbFFmpeg.Controls.Add($btnFFBrowse) | |
| # Cache management | |
| $gbCache = New-Object System.Windows.Forms.GroupBox | |
| $gbCache.Text = "Cache" | |
| $gbCache.ForeColor = $theme.Accent | |
| $gbCache.Font = $fontBold | |
| $gbCache.BackColor = $theme.Panel | |
| $gbCache.Location = New-Object System.Drawing.Point(10, 385) | |
| $gbCache.Size = New-Object System.Drawing.Size(500, 70) | |
| $settingsScroll.Controls.Add($gbCache) | |
| $btnClearThumbCache = New-StyledButton "Clear Thumbnail Cache" 180 32 | |
| $btnClearThumbCache.Location = New-Object System.Drawing.Point(15, 28) | |
| $btnClearThumbCache.Add_Click({ | |
| Get-ChildItem $script:thumbCachePath -File | Remove-Item -Force | |
| Log "Thumbnail cache cleared" "success" | |
| [System.Windows.Forms.MessageBox]::Show("Cache cleared!", "Done", 'OK', 'Information') | |
| }) | |
| $gbCache.Controls.Add($btnClearThumbCache) | |
| $lblCacheSize = New-StyledLabel "" 210 34 | |
| $lblCacheSize.ForeColor = [System.Drawing.Color]::Gray | |
| $gbCache.Controls.Add($lblCacheSize) | |
| try { | |
| $cacheSize = (Get-ChildItem $script:thumbCachePath -File -Recurse | Measure-Object -Property Length -Sum).Sum | |
| $lblCacheSize.Text = "Cache: {0:N1} MB" -f ($cacheSize / 1MB) | |
| } catch { $lblCacheSize.Text = "Cache: 0 MB" } | |
| # About | |
| $gbAbout = New-Object System.Windows.Forms.GroupBox | |
| $gbAbout.Text = "About" | |
| $gbAbout.ForeColor = $theme.Accent | |
| $gbAbout.Font = $fontBold | |
| $gbAbout.BackColor = $theme.Panel | |
| $gbAbout.Location = New-Object System.Drawing.Point(10, 465) | |
| $gbAbout.Size = New-Object System.Drawing.Size(500, 80) | |
| $settingsScroll.Controls.Add($gbAbout) | |
| $lblAbout = New-StyledLabel "FFmpeg Smart Studio v9`nPowered by FFmpeg. Built with PowerShell." 15 25 | |
| $lblAbout.ForeColor = $theme.Text | |
| $lblAbout.MaximumSize = New-Object System.Drawing.Size(470, 0) | |
| $gbAbout.Controls.Add($lblAbout) | |
| # --------------------------- | |
| # Core Logic | |
| # --------------------------- | |
| function Log { | |
| param([string]$msg, [string]$type = "info") | |
| $stamp = Get-Date -Format "HH:mm:ss" | |
| $prefix = switch ($type) { | |
| "error" { "[ERROR]" } | |
| "success" { "[OK]" } | |
| "warn" { "[WARN]" } | |
| default { "[INFO]" } | |
| } | |
| $txtLog.AppendText("[$stamp] $prefix $msg`r`n") | |
| $txtLog.SelectionStart = $txtLog.Text.Length | |
| $txtLog.ScrollToCaret() | |
| $statusLabel.Text = $msg | |
| } | |
| function Build-FFmpegArgs { | |
| param([string]$file, [string]$outfile) | |
| $vf = @() | |
| $af = @() | |
| $extraArgs = @() | |
| # Video filters | |
| if ($chkGray.Checked) { $vf += "hue=s=0" } | |
| if ($chkInvert.Checked) { $vf += "negate" } | |
| if ($chkBlur.Checked) { $vf += "boxblur=2:1" } | |
| # Resolution (custom or preset) | |
| if ($chkCustomRes.Checked -and $txtCustomW.Text -match '^\d+$' -and $txtCustomH.Text -match '^\d+$') { | |
| $vf += "scale=$($txtCustomW.Text):$($txtCustomH.Text)" | |
| } else { | |
| $resText = $cbRes.SelectedItem.ToString() | |
| if ($resText -ne "Original") { | |
| $resMatch = [regex]::Match($resText, "(\d+)x(\d+)") | |
| if ($resMatch.Success) { $vf += "scale=$($resMatch.Groups[1].Value):$($resMatch.Groups[2].Value)" } | |
| } | |
| } | |
| # FPS | |
| if ($txtFPS.Text -match '^\d+(\.\d+)?$') { | |
| $vf += "fps=$($txtFPS.Text)" | |
| } | |
| # Subtitles | |
| if ($txtSub.Text -ne "" -and (Test-Path $txtSub.Text)) { | |
| $cleanSub = $txtSub.Text -replace "\\", "/" -replace "'", "'\''" -replace ":", "\\:" | |
| $vf += "subtitles='$cleanSub'" | |
| } | |
| # Volume | |
| if ($trackVol.Value -ne 100) { | |
| $volFactor = [math]::Round($trackVol.Value / 100, 2) | |
| $af += "volume=$volFactor" | |
| } | |
| # Normalize audio | |
| if ($chkNormalize.Checked) { $af += "loudnorm" } | |
| # Fade | |
| if ($chkFade.Checked) { | |
| $vf += "fade=t=in:st=0:d=1,fade=t=out:st=0:d=1" | |
| $af += "afade=t=in:st=0:d=1,afade=t=out:st=0:d=1" | |
| } | |
| $vfArg = if ($vf.Count -gt 0) { "-vf `"$($vf -join ',')`"" } else { "" } | |
| $afArg = if ($af.Count -gt 0) { "-af `"$($af -join ',')`"" } else { "" } | |
| # Trim | |
| $trimArg = "" | |
| if ($txtStart.Text -ne "00:00:00") { $trimArg += " -ss $($txtStart.Text)" } | |
| if ($txtEnd.Text -ne "00:00:00") { $trimArg += " -to $($txtEnd.Text)" } | |
| # CRF | |
| $crfArg = "-crf $($trackCRF.Value)" | |
| # Video codec | |
| $vCodecArg = "" | |
| $vCodecText = $cbVCodec.SelectedItem.ToString() | |
| if ($vCodecText -match "libx265") { $vCodecArg = "-c:v libx265 -tag:v hvc1" } | |
| elseif ($vCodecText -match "libvpx") { $vCodecArg = "-c:v libvpx-vp9" } | |
| elseif ($vCodecText -match "copy") { $vCodecArg = "-c:v copy"; $crfArg = "" } | |
| elseif ($vCodecText -match "mpeg4") { $vCodecArg = "-c:v mpeg4" } | |
| # HW Accel | |
| $hwArg = "" | |
| $hwText = $cbHWAccel.SelectedItem.ToString() | |
| if ($hwText -match "NVIDIA") { $hwArg = "-hwaccel cuda"; if ($vCodecArg -eq "") { $vCodecArg = "-c:v h264_nvenc"; $crfArg = "-cq $($trackCRF.Value)" } } | |
| elseif ($hwText -match "AMD") { $hwArg = "-hwaccel auto"; if ($vCodecArg -eq "") { $vCodecArg = "-c:v h264_amf" } } | |
| elseif ($hwText -match "Intel") { $hwArg = "-hwaccel qsv"; if ($vCodecArg -eq "") { $vCodecArg = "-c:v h264_qsv" } } | |
| elseif ($hwText -match "CUDA") { $hwArg = "-hwaccel cuda" } | |
| # Audio codec | |
| $aCodecArg = "" | |
| if ($cbACodec.SelectedItem -ne "Auto" -and $null -ne $cbACodec.SelectedItem) { | |
| if ($cbACodec.SelectedItem -eq "copy") { $aCodecArg = "-c:a copy" } | |
| else { $aCodecArg = "-c:a $($cbACodec.SelectedItem)" } | |
| } | |
| # Bitrate | |
| $bitrateArg = "" | |
| if ($txtBitrate.Text -match '\S') { $bitrateArg = "-b:v $($txtBitrate.Text)" } | |
| # Mute | |
| $muteArg = if ($chkMute.Checked) { "-an" } else { "" } | |
| # Speed | |
| $speed = 1.0 | |
| [double]::TryParse($txtSpeed.Text, [ref]$speed) | Out-Null | |
| $speedArg = "" | |
| if ($speed -ne 1.0 -and $speed -gt 0) { | |
| $vSpeed = [math]::Round(1 / $speed, 4) | |
| $allVideoFilters = @("setpts=$vSpeed*PTS") | |
| $allVideoFilters += $vf | |
| $filterComplex = "[0:v]$($allVideoFilters -join ',')[v]" | |
| if (-not $chkMute.Checked -and $cbFmt.SelectedItem -notin @("mp3","wav","flac","gif")) { | |
| $atempoChain = @() | |
| $remaining = $speed | |
| while ($remaining -gt 2.0) { $atempoChain += "atempo=2.0"; $remaining /= 2.0 } | |
| while ($remaining -lt 0.5) { $atempoChain += "atempo=0.5"; $remaining /= 0.5 } | |
| $atempoChain += "atempo=$([math]::Round($remaining, 4))" | |
| if ($af.Count -gt 0) { $atempoChain += $af } | |
| $filterComplex += ";[0:a]$($atempoChain -join ',')[a]" | |
| $speedArg = "-filter_complex `"$filterComplex`" -map `"[v]`" -map `"[a]`"" | |
| } else { | |
| $speedArg = "-filter_complex `"$filterComplex`" -map `"[v]`"" | |
| } | |
| $vfArg = "" | |
| $afArg = "" | |
| } | |
| # Audio-only | |
| if ($cbFmt.SelectedItem -in @("mp3","wav","flac")) { | |
| return "-y$trimArg -i `"$file`" $afArg -vn `"$outfile`"" | |
| } | |
| # Build | |
| $parts = @("-y") | |
| if ($hwArg -ne "") { $parts += $hwArg } | |
| if ($trimArg.Trim() -ne "") { $parts += $trimArg.Trim() } | |
| $parts += "-i `"$file`"" | |
| if ($speedArg -ne "") { $parts += $speedArg } | |
| elseif ($vfArg -ne "") { $parts += $vfArg } | |
| if ($afArg -ne "" -and $speedArg -eq "") { $parts += $afArg } | |
| if ($vCodecArg -ne "") { $parts += $vCodecArg } | |
| if ($muteArg -ne "") { $parts += $muteArg } | |
| if ($crfArg -ne "") { $parts += $crfArg } | |
| if ($bitrateArg -ne "") { $parts += $bitrateArg } | |
| if ($aCodecArg -ne "" -and -not $chkMute.Checked) { $parts += $aCodecArg } | |
| $parts += "`"$outfile`"" | |
| return ($parts | Where-Object { $_ -ne "" }) -join " " | |
| } | |
| function Get-OutputFileName { | |
| param([string]$inputFile) | |
| $baseName = [System.IO.Path]::GetFileNameWithoutExtension($inputFile) | |
| return "$($txtPrefix.Text)${baseName}$($txtSuffix.Text).$($cbFmt.SelectedItem)" | |
| } | |
| function Update-RowStatus { | |
| param($row, [string]$status) | |
| $row.Cells["Status"].Value = $status | |
| switch -Wildcard ($status) { | |
| "*Done*" { $row.Cells["Status"].Style.ForeColor = $theme.Success } | |
| "*Error*" { $row.Cells["Status"].Style.ForeColor = $theme.Error } | |
| "*Processing*" { $row.Cells["Status"].Style.ForeColor = $theme.Warning } | |
| "*Pending*" { $row.Cells["Status"].Style.ForeColor = $theme.Text } | |
| "*Cancelled*" { $row.Cells["Status"].Style.ForeColor = $theme.Error } | |
| "*Skipped*" { $row.Cells["Status"].Style.ForeColor = $theme.Warning } | |
| } | |
| } | |
| function Parse-FFmpegProgress { | |
| param([string]$line, [double]$totalSeconds) | |
| if ($totalSeconds -le 0) { return -1 } | |
| if ($line -match "time=(\d{2}):(\d{2}):(\d{2})\.(\d{2})") { | |
| $cur = [int]$Matches[1] * 3600 + [int]$Matches[2] * 60 + [int]$Matches[3] | |
| return [math]::Min(100, [int](($cur / $totalSeconds) * 100)) | |
| } | |
| return -1 | |
| } | |
| function Get-DurationSeconds { | |
| param([string]$s) | |
| if ($s -match "(\d{2}):(\d{2}):(\d{2})") { | |
| return [int]$Matches[1] * 3600 + [int]$Matches[2] * 60 + [int]$Matches[3] | |
| } | |
| return 0 | |
| } | |
| # --------------------------- | |
| # Button Events | |
| # --------------------------- | |
| $btnAdd.Add_Click({ | |
| $ofd = New-Object System.Windows.Forms.OpenFileDialog | |
| $ofd.Multiselect = $true | |
| $ofd.Filter = "Media|*.mp4;*.mkv;*.avi;*.mov;*.wmv;*.flv;*.webm;*.ts;*.m2ts;*.mp3;*.wav;*.flac;*.aac;*.ogg;*.m4a;*.wma|All|*.*" | |
| if ($ofd.ShowDialog() -eq "OK") { | |
| foreach ($f in $ofd.FileNames) { Add-FileToQueue $f } | |
| Log "Added $($ofd.FileNames.Count) file(s)" "success" | |
| } | |
| }) | |
| $btnAddFolder.Add_Click({ | |
| $fbd = New-Object System.Windows.Forms.FolderBrowserDialog | |
| $fbd.Description = "Select folder to add all media files" | |
| if ($fbd.ShowDialog() -eq 'OK') { | |
| $count = 0 | |
| Get-ChildItem $fbd.SelectedPath -File -Include *.mp4,*.mkv,*.avi,*.mov,*.wmv,*.flv,*.webm,*.mp3,*.wav,*.flac,*.ts -Recurse | ForEach-Object { | |
| Add-FileToQueue $_.FullName | |
| $count++ | |
| } | |
| Log "Added $count file(s) from folder" "success" | |
| } | |
| }) | |
| $btnClear.Add_Click({ | |
| $grid.Rows.Clear() | |
| $script:fileDataStore.Clear() | |
| $progressTotal.Value = 0; $progressFile.Value = 0 | |
| $lblCur.Text = "Ready"; $lblETA.Text = "" | |
| $lblFileProgress.Text = "File: 0%"; $lblTotalProgress.Text = "Total: 0%" | |
| $lblQueueCount.Text = "(0 files)" | |
| Log "Queue cleared" "info" | |
| }) | |
| $btnMoveUp.Add_Click({ | |
| if ($grid.SelectedRows.Count -eq 1) { | |
| $idx = $grid.SelectedRows[0].Index | |
| if ($idx -gt 0) { | |
| $v = @(); foreach ($c in $grid.Rows[$idx].Cells) { $v += $c.Value } | |
| $grid.Rows.RemoveAt($idx); $grid.Rows.Insert($idx - 1, $v) | |
| $grid.Rows[$idx - 1].Selected = $true | |
| } | |
| } | |
| }) | |
| $btnMoveDown.Add_Click({ | |
| if ($grid.SelectedRows.Count -eq 1) { | |
| $idx = $grid.SelectedRows[0].Index | |
| if ($idx -lt $grid.Rows.Count - 1) { | |
| $v = @(); foreach ($c in $grid.Rows[$idx].Cells) { $v += $c.Value } | |
| $grid.Rows.RemoveAt($idx); $grid.Rows.Insert($idx + 1, $v) | |
| $grid.Rows[$idx + 1].Selected = $true | |
| } | |
| } | |
| }) | |
| $btnRemove.Add_Click({ | |
| $toRemove = @(); foreach ($r in $grid.SelectedRows) { $toRemove += $r.Index } | |
| $toRemove | Sort-Object -Descending | ForEach-Object { $grid.Rows.RemoveAt($_) } | |
| $lblQueueCount.Text = "($($grid.Rows.Count) files)" | |
| if ($toRemove.Count -gt 0) { Log "Removed $($toRemove.Count) file(s)" "info" } | |
| }) | |
| $btnPreviewFile.Add_Click({ | |
| if ($grid.SelectedRows.Count -eq 1) { | |
| $f = $grid.SelectedRows[0].Cells["FileName"].Value | |
| if ($f) { | |
| Update-PreviewPanel $f | |
| $tabs.SelectedTab = $tabPreview | |
| } | |
| } | |
| }) | |
| $btnOpenFolder.Add_Click({ | |
| if ($txtOut.Text -ne "" -and (Test-Path $txtOut.Text)) { Start-Process explorer.exe $txtOut.Text } | |
| else { [System.Windows.Forms.MessageBox]::Show("Output folder not set.", "Info", 'OK', 'Information') } | |
| }) | |
| $btnCancel.Add_Click({ | |
| $script:cancelRequested = $true | |
| if ($script:currentProcess -and -not $script:currentProcess.HasExited) { | |
| $script:currentProcess.Kill() | |
| Log "Cancel requested, killing process..." "warn" | |
| } | |
| }) | |
| # --------------------------- | |
| # START (Async via Timer-based approach) | |
| # --------------------------- | |
| $script:processQueue = [System.Collections.Queue]::new() | |
| $script:batchStartTime = $null | |
| $script:batchTotal = 0 | |
| $script:batchDone = 0 | |
| $script:batchSuccess = 0 | |
| $script:batchFail = 0 | |
| $processTimer = New-Object System.Windows.Forms.Timer | |
| $processTimer.Interval = 100 | |
| $processTimer.Add_Tick({ | |
| # Check if current process is still running | |
| if ($script:currentProcess -and -not $script:currentProcess.HasExited) { | |
| # Read available stderr | |
| try { | |
| while (-not $script:currentProcess.StandardError.EndOfStream) { | |
| $line = $script:currentProcess.StandardError.ReadLine() | |
| if ($null -eq $line) { break } | |
| $totalSec = Get-DurationSeconds $script:currentRow.Cells["Duration"].Value | |
| $pct = Parse-FFmpegProgress $line $totalSec | |
| if ($pct -ge 0) { | |
| $progressFile.Value = $pct | |
| $lblFileProgress.Text = "File: $pct%" | |
| } | |
| if ($line -notmatch "^frame=|^size=|^\s*$") { Log $line } | |
| break # Process one line per tick to keep UI responsive | |
| } | |
| } catch {} | |
| return | |
| } | |
| # Process finished or no process - handle completion | |
| if ($script:currentProcess) { | |
| $row = $script:currentRow | |
| $inFile = $row.Cells["FileName"].Value | |
| $fileName = [System.IO.Path]::GetFileName($inFile) | |
| if ($script:cancelRequested) { | |
| Update-RowStatus $row "Cancelled" | |
| Log "Cancelled: $fileName" "warn" | |
| } elseif ($script:currentProcess.ExitCode -eq 0) { | |
| Update-RowStatus $row "Done" | |
| $script:batchSuccess++ | |
| $outFile = $script:currentOutFile | |
| if (Test-Path $outFile) { | |
| $sz = (Get-Item $outFile).Length | |
| $szStr = if ($sz -gt 1MB) { "{0:N1} MB" -f ($sz / 1MB) } else { "{0:N0} KB" -f ($sz / 1KB) } | |
| Log "Done: $fileName -> $szStr" "success" | |
| } | |
| } else { | |
| Update-RowStatus $row "Error" | |
| $script:batchFail++ | |
| Log "Failed (exit $($script:currentProcess.ExitCode)): $fileName" "error" | |
| } | |
| $script:batchDone++ | |
| $totalPct = [math]::Min(100, [int](($script:batchDone / $script:batchTotal) * 100)) | |
| $progressTotal.Value = $totalPct | |
| $lblTotalProgress.Text = "Total: $totalPct% ($($script:batchDone)/$($script:batchTotal))" | |
| $progressFile.Value = 0 | |
| $lblFileProgress.Text = "File: 0%" | |
| # ETA | |
| $elapsed = (Get-Date) - $script:batchStartTime | |
| if ($script:batchDone -lt $script:batchTotal -and $script:batchDone -gt 0) { | |
| $avg = $elapsed.TotalSeconds / $script:batchDone | |
| $rem = ($script:batchTotal - $script:batchDone) * $avg | |
| $eta = [TimeSpan]::FromSeconds($rem) | |
| $lblETA.Text = "ETA: $("{0:D2}:{1:D2}:{2:D2}" -f $eta.Hours, $eta.Minutes, $eta.Seconds)" | |
| } | |
| $script:currentProcess = $null | |
| } | |
| # Check cancellation | |
| if ($script:cancelRequested) { | |
| # Mark remaining as cancelled | |
| while ($script:processQueue.Count -gt 0) { | |
| $item = $script:processQueue.Dequeue() | |
| Update-RowStatus $item.Row "Cancelled" | |
| $script:batchDone++ | |
| } | |
| } | |
| # Start next or finish | |
| if ($script:processQueue.Count -eq 0 -or $script:cancelRequested) { | |
| $processTimer.Stop() | |
| $script:isProcessing = $false | |
| $totalElapsed = (Get-Date) - $script:batchStartTime | |
| $timeStr = "{0:D2}:{1:D2}:{2:D2}" -f $totalElapsed.Hours, $totalElapsed.Minutes, $totalElapsed.Seconds | |
| $lblCur.Text = "Done! $($script:batchSuccess) ok, $($script:batchFail) failed" | |
| $lblETA.Text = "Time: $timeStr" | |
| $btnStart.Enabled = $true; $btnCancel.Enabled = $false | |
| $btnAdd.Enabled = $true; $btnAddFolder.Enabled = $true; $btnClear.Enabled = $true | |
| Log "Batch complete: $($script:batchSuccess) success, $($script:batchFail) failed, $timeStr" "success" | |
| if ($chkAutoOpen.Checked -and $txtOut.Text -ne "" -and (Test-Path $txtOut.Text)) { | |
| Start-Process explorer.exe $txtOut.Text | |
| } | |
| if ($script:cancelRequested) { | |
| [System.Windows.Forms.MessageBox]::Show("Cancelled.", "Cancelled", 'OK', 'Warning') | |
| } else { | |
| [System.Windows.Forms.MessageBox]::Show( | |
| "Done!`n$($script:batchSuccess) success, $($script:batchFail) failed`nTime: $timeStr", | |
| "Complete", 'OK', 'Information') | |
| if ($chkShutdown.Checked) { | |
| $confirmShut = [System.Windows.Forms.MessageBox]::Show( | |
| "Shutdown PC now?", "Shutdown", 'YesNo', 'Warning') | |
| if ($confirmShut -eq 'Yes') { Stop-Computer -Force } | |
| } | |
| } | |
| return | |
| } | |
| # Start next file | |
| $item = $script:processQueue.Dequeue() | |
| $row = $item.Row | |
| $inFile = $row.Cells["FileName"].Value | |
| $fileName = [System.IO.Path]::GetFileName($inFile) | |
| Update-RowStatus $row "Processing..." | |
| $lblCur.Text = "($($script:batchDone + 1)/$($script:batchTotal)) $fileName" | |
| $outName = Get-OutputFileName $inFile | |
| $outFile = Join-Path $txtOut.Text $outName | |
| $script:currentOutFile = $outFile | |
| $script:currentRow = $row | |
| # Overwrite check | |
| if ((Test-Path $outFile) -and -not $chkOverwrite.Checked) { | |
| $ow = [System.Windows.Forms.MessageBox]::Show("'$outName' exists. Overwrite?", "Exists", 'YesNo', 'Question') | |
| if ($ow -eq 'No') { | |
| Update-RowStatus $row "Skipped" | |
| $script:batchDone++ | |
| return | |
| } | |
| } | |
| $ffArgs = Build-FFmpegArgs $inFile $outFile | |
| Log "ffmpeg $ffArgs" | |
| $pinfo = New-Object System.Diagnostics.ProcessStartInfo | |
| $pinfo.FileName = if ($txtFFPath.Text -ne "") { $txtFFPath.Text } else { "ffmpeg.exe" } | |
| $pinfo.Arguments = $ffArgs | |
| $pinfo.CreateNoWindow = $true | |
| $pinfo.UseShellExecute = $false | |
| $pinfo.RedirectStandardOutput = $true | |
| $pinfo.RedirectStandardError = $true | |
| try { | |
| $script:currentProcess = [System.Diagnostics.Process]::Start($pinfo) | |
| } catch { | |
| Update-RowStatus $row "Error" | |
| $script:batchFail++ | |
| $script:batchDone++ | |
| Log "Failed to start: $($_.Exception.Message)" "error" | |
| $script:currentProcess = $null | |
| } | |
| }) | |
| $btnStart.Add_Click({ | |
| if ($grid.Rows.Count -eq 0) { | |
| [System.Windows.Forms.MessageBox]::Show("Add files first!", "Empty", 'OK', 'Warning'); return | |
| } | |
| if ($txtOut.Text -eq "") { | |
| [System.Windows.Forms.MessageBox]::Show("Select output folder!", "No Output", 'OK', 'Warning'); return | |
| } | |
| if (-not (Test-Path $txtOut.Text)) { | |
| New-Item -ItemType Directory -Path $txtOut.Text -Force | Out-Null | |
| Log "Created: $($txtOut.Text)" "info" | |
| } | |
| $script:cancelRequested = $false | |
| $script:isProcessing = $true | |
| $script:processQueue.Clear() | |
| $script:batchStartTime = Get-Date | |
| $script:batchTotal = $grid.Rows.Count | |
| $script:batchDone = 0 | |
| $script:batchSuccess = 0 | |
| $script:batchFail = 0 | |
| $script:currentProcess = $null | |
| $btnStart.Enabled = $false; $btnCancel.Enabled = $true | |
| $btnAdd.Enabled = $false; $btnAddFolder.Enabled = $false; $btnClear.Enabled = $false | |
| $progressTotal.Value = 0; $progressFile.Value = 0 | |
| foreach ($row in $grid.Rows) { | |
| $script:processQueue.Enqueue(@{ Row = $row }) | Out-Null | |
| } | |
| Log "Starting batch: $($script:batchTotal) files" "info" | |
| $processTimer.Start() | |
| }) | |
| # --------------------------- | |
| # Form Events | |
| # --------------------------- | |
| $form.Add_Shown({ Load-Config }) | |
| $form.Add_FormClosing({ | |
| if ($chkSaveConfig.Checked) { Save-Config } | |
| # Cleanup thumbnails from PictureBox | |
| if ($picPreview.Image) { $picPreview.Image.Dispose() } | |
| $processTimer.Stop() | |
| if ($script:currentProcess -and -not $script:currentProcess.HasExited) { | |
| $script:currentProcess.Kill() | |
| } | |
| }) | |
| # --------------------------- | |
| # Keyboard Shortcuts | |
| # --------------------------- | |
| $form.KeyPreview = $true | |
| $form.Add_KeyDown({ | |
| param($s, $e) | |
| if ($e.Control) { | |
| switch ($e.KeyCode) { | |
| 'O' { $btnAdd.PerformClick(); $e.Handled = $true } | |
| 'S' { Save-Config; Log "Settings saved (Ctrl+S)" "success"; $e.Handled = $true } | |
| 'R' { $btnStart.PerformClick(); $e.Handled = $true } | |
| } | |
| } | |
| if ($e.KeyCode -eq 'Delete' -and -not $script:isProcessing) { | |
| $btnRemove.PerformClick(); $e.Handled = $true | |
| } | |
| if ($e.KeyCode -eq 'Escape' -and $script:isProcessing) { | |
| $btnCancel.PerformClick(); $e.Handled = $true | |
| } | |
| }) | |
| # --------------------------- | |
| # Show | |
| # --------------------------- | |
| [void]$form.ShowDialog() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment