diff --git a/.gitignore b/.gitignore
index 4c8a4bd..c619310 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,10 @@
+<<<<<<< HEAD
build/bin
node_modules
frontend/dist
TODO.md
+
+=======
+fyne-cross
+TODO.md
+>>>>>>> 5c9da659bc26ab9faf682fb549dc7e0b599a7169
diff --git a/Predator/.gitignore b/Predator/.gitignore
new file mode 100644
index 0000000..129d522
--- /dev/null
+++ b/Predator/.gitignore
@@ -0,0 +1,3 @@
+build/bin
+node_modules
+frontend/dist
diff --git a/Predator/LICENSE.txt b/Predator/LICENSE.txt
new file mode 100644
index 0000000..8edb07f
--- /dev/null
+++ b/Predator/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Aswanidev-vs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Predator/app.go b/Predator/app.go
new file mode 100644
index 0000000..3b0aa31
--- /dev/null
+++ b/Predator/app.go
@@ -0,0 +1,790 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/lrstanley/go-ytdlp"
+ wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+}
+
+/* -------------------- Constants -------------------- */
+
+const (
+ maxConcurrentDownloads = 3
+ progressUpdateInterval = 200 * time.Millisecond
+ speedSmoothingAlpha = 0.2
+ taskQueueSize = 100
+ cleanupDelay = 2 * time.Second
+ fetchTimeout = 30 * time.Second
+ fetchDebounceDelay = 600 * time.Millisecond
+ maxRetries = 3
+ retryBaseDelay = 2 * time.Second
+ prefOutputDir = "output_dir"
+)
+
+/* -------------------- Types -------------------- */
+
+type DownloadTask struct {
+ URL string `json:"url"`
+ Title string `json:"title"`
+ Type string `json:"type"`
+ Resolution string `json:"resolution"`
+ CleanRes string `json:"cleanRes"`
+ AudioFormat string `json:"audioFormat"`
+ AudioQuality string `json:"audioQuality"`
+ VideoCodec string `json:"videoCodec"`
+}
+
+type VideoInfo struct {
+ Title string `json:"title"`
+ Duration int `json:"duration"`
+ Resolutions []string `json:"resolutions"`
+}
+
+type ProgressUpdate struct {
+ TaskID string `json:"taskId"`
+ Percent float64 `json:"percent"`
+ Status string `json:"status"`
+ Speed string `json:"speed"`
+ ETA string `json:"eta"`
+ Error string `json:"error,omitempty"`
+}
+
+type DownloadHistory struct {
+ ID string `json:"id"`
+ URL string `json:"url"`
+ Title string `json:"title"`
+ Type string `json:"type"`
+ Resolution string `json:"resolution"`
+ AudioFormat string `json:"audioFormat"`
+ FilePath string `json:"filePath"`
+ FileSize int64 `json:"fileSize"`
+ DownloadedAt time.Time `json:"downloadedAt"`
+ Status string `json:"status"`
+}
+
+/* -------------------- URL Validation -------------------- */
+
+var youtubeURLPatterns = []*regexp.Regexp{
+ regexp.MustCompile(`^(https?://)?(www\.)?(youtube\.com|youtu\.be)/.+`),
+ regexp.MustCompile(`^(https?://)?(www\.)?youtube\.com/watch\?v=[\w-]+`),
+ regexp.MustCompile(`^(https?://)?(www\.)?youtu\.be/[\w-]+`),
+ regexp.MustCompile(`^(https?://)?(www\.)?youtube\.com/shorts/[\w-]+`),
+ regexp.MustCompile(`^(https?://)?(www\.)?youtube\.com/live/[\w-]+`),
+}
+
+func (a *App) IsValidYouTubeURL(url string) bool {
+ for _, pattern := range youtubeURLPatterns {
+ if pattern.MatchString(url) {
+ return true
+ }
+ }
+ return false
+}
+
+/* -------------------- Helpers -------------------- */
+
+func formatBytes(b float64) string {
+ const unit = 1024
+ if b < unit {
+ return fmt.Sprintf("%.0f B", b)
+ }
+ div, exp := float64(unit), 0
+ for n := b / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+ return fmt.Sprintf("%.1f %ciB", b/div, "KMGT"[exp])
+}
+
+func formatETA(d time.Duration) string {
+ s := int64(d.Seconds())
+ h := s / 3600
+ m := (s % 3600) / 60
+ sec := s % 60
+ if h > 0 {
+ return fmt.Sprintf("%02d:%02d:%02d", h, m, sec)
+ }
+ return fmt.Sprintf("%02d:%02d", m, sec)
+}
+
+func formatSpeed(speed float64) string {
+ return formatBytes(speed) + "/s"
+}
+
+func parseSizeString(sizeStr string) int64 {
+ if sizeStr == "" || sizeStr == "Unknown" {
+ return 0
+ }
+ sizeStr = strings.TrimPrefix(sizeStr, "~")
+ re := regexp.MustCompile(`([\d.]+)\s*([KMGT]?)i?B`)
+ matches := re.FindStringSubmatch(sizeStr)
+ if len(matches) < 3 {
+ return 0
+ }
+ val, err := strconv.ParseFloat(matches[1], 64)
+ if err != nil {
+ return 0
+ }
+ multiplier := float64(1024)
+ switch matches[2] {
+ case "K":
+ multiplier = 1024
+ case "M":
+ multiplier = 1024 * 1024
+ case "G":
+ multiplier = 1024 * 1024 * 1024
+ case "T":
+ multiplier = 1024 * 1024 * 1024 * 1024
+ default:
+ multiplier = 1
+ }
+ return int64(val * multiplier)
+}
+
+func truncateError(err string, maxLen int) string {
+ if len(err) <= maxLen {
+ return err
+ }
+ return err[:maxLen] + "..."
+}
+
+func extractResolution(resolution string) string {
+ re := regexp.MustCompile(`^(\d+)p`)
+ matches := re.FindStringSubmatch(resolution)
+ if len(matches) > 1 {
+ return matches[1]
+ }
+ if strings.HasPrefix(resolution, "best") {
+ return "best"
+ }
+ return ""
+}
+
+/* -------------------- Format Builders -------------------- */
+
+func buildVideoFormatString(cleanRes string, preferH264 bool) string {
+ // Build format string that handles all codecs (h264, vp9, av1)
+ // ffmpeg will transcode to h264/aac for mp4 compatibility if needed
+ if cleanRes == "best" {
+ // Try h264 first, then vp9, then av1, then any best
+ return "bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/" +
+ "bestvideo[vcodec^=avc1]+bestaudio/" +
+ "bestvideo[vcodec^=vp9]+bestaudio[acodec^=mp4a]/" +
+ "bestvideo[vcodec^=vp9]+bestaudio/" +
+ "bestvideo[vcodec^=av01]+bestaudio[acodec^=mp4a]/" +
+ "bestvideo[vcodec^=av01]+bestaudio/" +
+ "bestvideo+bestaudio/best"
+ }
+
+ resNum, _ := strconv.Atoi(cleanRes)
+ if resNum >= 1440 {
+ // High resolutions - try exact height first, then fall back to <=
+ // This ensures 4K videos actually download at 4K, not 1080p
+ return fmt.Sprintf(
+ // Try exact height match first (e.g., exactly 2160p)
+ "bestvideo[vcodec^=avc1][height=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=avc1][height=%s]+bestaudio/"+
+ "bestvideo[vcodec^=vp9][height=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=vp9][height=%s]+bestaudio/"+
+ "bestvideo[vcodec^=av01][height=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=av01][height=%s]+bestaudio/"+
+ // Fall back to <= if exact match not available
+ "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=avc1][height<=%s]+bestaudio/"+
+ "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=vp9][height<=%s]+bestaudio/"+
+ "bestvideo[vcodec^=av01][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=av01][height<=%s]+bestaudio/"+
+ "bestvideo[height<=%s]+bestaudio/"+
+ "best[height<=%s]",
+ cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes,
+ cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes,
+ )
+ }
+
+ // Standard resolutions - try exact height first, then fall back
+ return fmt.Sprintf(
+ // Try exact height match first
+ "bestvideo[vcodec^=avc1][height=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=avc1][height=%s]+bestaudio/"+
+ "bestvideo[vcodec^=vp9][height=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=vp9][height=%s]+bestaudio/"+
+ "bestvideo[vcodec^=av01][height=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=av01][height=%s]+bestaudio/"+
+ // Fall back to <= if exact match not available
+ "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=avc1][height<=%s]+bestaudio/"+
+ "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=vp9][height<=%s]+bestaudio/"+
+ "bestvideo[vcodec^=av01][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=av01][height<=%s]+bestaudio/"+
+ "bestvideo[height<=%s]+bestaudio/"+
+ "best[height<=%s]",
+ cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes,
+ cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes,
+ )
+}
+
+func buildAudioFormatString(format string) string {
+ switch format {
+ case "m4a":
+ return "bestaudio[ext=m4a]/bestaudio[acodec^=mp4a]/bestaudio/best"
+ case "opus":
+ return "bestaudio[ext=opus]/bestaudio[acodec^=opus]/bestaudio/best"
+ case "mp3":
+ return "bestaudio[ext=mp3]/bestaudio/best"
+ case "wav":
+ return "bestaudio[ext=wav]/bestaudio/best"
+ default:
+ return "bestaudio/best"
+ }
+}
+
+/* -------------------- Queue System -------------------- */
+
+var (
+ taskQueue = make(chan DownloadTask, taskQueueSize)
+ sem chan struct{}
+ semOnce sync.Once
+ activeTasks = make(map[string]context.CancelFunc)
+ tasksMu sync.RWMutex
+ taskCounter uint64
+ historyMu sync.RWMutex
+)
+
+/* -------------------- History Management -------------------- */
+
+func (a *App) getHistoryFilePath() string {
+ homeDir, _ := os.UserHomeDir()
+ historyDir := filepath.Join(homeDir, ".predator")
+ os.MkdirAll(historyDir, 0755)
+ return filepath.Join(historyDir, "history.json")
+}
+
+// GetDownloadHistory returns all download history
+func (a *App) GetDownloadHistory() ([]DownloadHistory, error) {
+ historyMu.RLock()
+ defer historyMu.RUnlock()
+
+ historyFile := a.getHistoryFilePath()
+ data, err := os.ReadFile(historyFile)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return []DownloadHistory{}, nil
+ }
+ return nil, err
+ }
+
+ var history []DownloadHistory
+ if err := json.Unmarshal(data, &history); err != nil {
+ return nil, err
+ }
+
+ // Sort by date descending (newest first)
+ for i, j := 0, len(history)-1; i < j; i, j = i+1, j-1 {
+ history[i], history[j] = history[j], history[i]
+ }
+
+ return history, nil
+}
+
+// SaveToHistory adds a completed download to history
+func (a *App) SaveToHistory(item DownloadHistory) error {
+ historyMu.Lock()
+ defer historyMu.Unlock()
+
+ historyFile := a.getHistoryFilePath()
+
+ // Read existing history
+ var history []DownloadHistory
+ data, err := os.ReadFile(historyFile)
+ if err == nil {
+ json.Unmarshal(data, &history)
+ }
+
+ // Add new item at the beginning
+ history = append([]DownloadHistory{item}, history...)
+
+ // Keep only last 100 items
+ if len(history) > 100 {
+ history = history[:100]
+ }
+
+ // Save back to file
+ data, err = json.MarshalIndent(history, "", " ")
+ if err != nil {
+ return err
+ }
+
+ return os.WriteFile(historyFile, data, 0644)
+}
+
+// OpenFolder opens the file explorer at the specified path
+func (a *App) OpenFolder(path string) error {
+ log.Printf("OpenFolder called with path: %s", path)
+
+ // Clean the path
+ path = filepath.Clean(path)
+ log.Printf("Cleaned path: %s", path)
+
+ // Check if path exists
+ info, err := os.Stat(path)
+ if err != nil {
+ log.Printf("Path does not exist: %v", err)
+ return fmt.Errorf("path does not exist: %s", path)
+ }
+
+ log.Printf("Path exists, isDir: %v", info.IsDir())
+
+ var cmd *exec.Cmd
+
+ switch runtime.GOOS {
+ case "windows":
+ // Use full path to explorer.exe to avoid PATH issues
+ explorerPath := `C:\Windows\explorer.exe`
+ // If path is a file, use /select, to highlight it
+ // If path is a directory, just open it
+ if info.IsDir() {
+ log.Printf("Opening directory: %s", path)
+ cmd = exec.Command(explorerPath, path)
+ } else {
+ // /select, opens the folder and highlights the file
+ log.Printf("Opening file with /select,: %s", path)
+ cmd = exec.Command(explorerPath, "/select,", path)
+ }
+ case "darwin":
+ cmd = exec.Command("open", path)
+ default: // linux
+ cmd = exec.Command("xdg-open", filepath.Dir(path))
+ }
+
+ log.Printf("Executing command: %v", cmd)
+ return cmd.Start()
+}
+
+// ClearHistory clears all download history
+func (a *App) ClearHistory() error {
+ historyMu.Lock()
+ defer historyMu.Unlock()
+
+ historyFile := a.getHistoryFilePath()
+ return os.Remove(historyFile)
+}
+
+func initSemaphore() {
+ semOnce.Do(func() {
+ sem = make(chan struct{}, maxConcurrentDownloads)
+ for i := 0; i < maxConcurrentDownloads; i++ {
+ sem <- struct{}{}
+ }
+ })
+}
+
+func (a *App) generateTaskID() string {
+ return fmt.Sprintf("task-%d", atomic.AddUint64(&taskCounter, 1))
+}
+
+/* -------------------- Exposed Methods -------------------- */
+
+// CheckAndInstallDeps checks and installs required dependencies
+func (a *App) CheckAndInstallDeps() error {
+ if _, err := exec.LookPath("ffmpeg"); err == nil {
+ if _, err := exec.LookPath("ffprobe"); err == nil {
+ ytdlp.MustInstall(context.Background(), nil)
+ return nil
+ }
+ }
+
+ // Ask user for confirmation
+ result, err := wailsRuntime.MessageDialog(a.ctx, wailsRuntime.MessageDialogOptions{
+ Type: wailsRuntime.QuestionDialog,
+ Title: "Install Required Tools",
+ Message: "Predator requires ffmpeg and ffprobe for merging video+audio and extracting audio.\n\nThey are not detected on your system.\n\nWe can automatically download open-source bundled versions (yt-dlp + ffmpeg + ffprobe) and cache them locally.\n\nDo you want to continue? (Recommended)",
+ Buttons: []string{"Yes, Install", "No"},
+ DefaultButton: "Yes, Install",
+ CancelButton: "No",
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if result == "No" {
+ return fmt.Errorf("user declined bundled dependency installation")
+ }
+
+ // Show progress dialog
+ wailsRuntime.EventsEmit(a.ctx, "installing-deps", true)
+
+ _, err = ytdlp.Install(context.Background(), nil)
+ wailsRuntime.EventsEmit(a.ctx, "installing-deps", false)
+
+ return err
+}
+
+// GetOutputDir returns the current output directory from environment or default
+func (a *App) GetOutputDir() string {
+ outDir := os.Getenv("PREDATOR_OUTPUT_DIR")
+ if outDir == "" {
+ outDir, _ := os.Getwd()
+ return outDir
+ }
+ return outDir
+}
+
+// SelectOutputDir opens a dialog to select output directory
+func (a *App) SelectOutputDir() (string, error) {
+ dir, err := wailsRuntime.OpenDirectoryDialog(a.ctx, wailsRuntime.OpenDialogOptions{
+ Title: "Select Download Location",
+ })
+ if err != nil {
+ return "", err
+ }
+ // Set environment variable for the session
+ if dir != "" {
+ os.Setenv("PREDATOR_OUTPUT_DIR", dir)
+ }
+ return dir, nil
+}
+
+// FetchVideoInfo fetches video information from URL
+func (a *App) FetchVideoInfo(url string) (*VideoInfo, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), fetchTimeout)
+ defer cancel()
+
+ result, err := ytdlp.New().DumpJSON().Run(ctx, url)
+ if err != nil {
+ return nil, err
+ }
+
+ var info struct {
+ Title string `json:"title"`
+ Duration int `json:"duration"`
+ Formats []struct {
+ Height *int `json:"height"`
+ Width *int `json:"width"`
+ Filesize *int64 `json:"filesize"`
+ FilesizeApprox *int64 `json:"filesize_approx"`
+ Vcodec string `json:"vcodec"`
+ Acodec string `json:"acodec"`
+ Ext string `json:"ext"`
+ } `json:"formats"`
+ }
+
+ if err := json.Unmarshal([]byte(result.Stdout), &info); err != nil {
+ return nil, err
+ }
+
+ if info.Title == "" {
+ return nil, fmt.Errorf("no video found at URL")
+ }
+
+ // Build resolution map
+ resolutions := []string{"144p", "240p", "360p", "480p", "720p", "1080p", "1440p", "2160p", "best"}
+ resMap := make(map[string]string)
+
+ for _, f := range info.Formats {
+ if f.Vcodec != "none" && f.Height != nil && *f.Height > 0 {
+ res := fmt.Sprintf("%dp", *f.Height)
+ var size int64 = 0
+ if f.Filesize != nil && *f.Filesize > 0 {
+ size = *f.Filesize
+ } else if f.FilesizeApprox != nil && *f.FilesizeApprox > 0 {
+ size = *f.FilesizeApprox
+ }
+ if size > 0 {
+ existingSize := parseSizeString(resMap[res])
+ if size > existingSize {
+ resMap[res] = formatBytes(float64(size))
+ }
+ }
+ }
+ }
+
+ // Build options
+ opts := []string{}
+ for _, r := range resolutions {
+ size := "Unknown"
+ if s, ok := resMap[r]; ok {
+ size = s
+ }
+ opts = append(opts, fmt.Sprintf("%s (%s)", r, size))
+ }
+
+ return &VideoInfo{
+ Title: info.Title,
+ Duration: info.Duration,
+ Resolutions: opts,
+ }, nil
+}
+
+// AddToQueue adds a download task to the queue
+func (a *App) AddToQueue(task DownloadTask) (string, error) {
+ taskID := a.generateTaskID()
+ taskQueue <- task
+
+ // Start worker if not already running
+ go a.worker(task, taskID)
+
+ return taskID, nil
+}
+
+// CancelTask cancels a running download task
+func (a *App) CancelTask(taskID string) {
+ tasksMu.Lock()
+ if cancel, ok := activeTasks[taskID]; ok {
+ cancel()
+ delete(activeTasks, taskID)
+ }
+ tasksMu.Unlock()
+}
+
+// UpdateYtDlp updates yt-dlp to the latest version
+func (a *App) UpdateYtDlp() error {
+ log.Println("Updating yt-dlp to latest version...")
+ _, err := ytdlp.Install(context.Background(), nil)
+ if err != nil {
+ ytdlp.MustInstall(context.Background(), nil)
+ return err
+ }
+ return nil
+}
+
+/* -------------------- Worker -------------------- */
+
+func (a *App) worker(task DownloadTask, taskID string) {
+ initSemaphore()
+
+ <-sem
+
+ ctx, cancel := context.WithCancel(context.Background())
+ tasksMu.Lock()
+ activeTasks[taskID] = cancel
+ tasksMu.Unlock()
+
+ defer func() {
+ sem <- struct{}{}
+ tasksMu.Lock()
+ delete(activeTasks, taskID)
+ tasksMu.Unlock()
+ time.Sleep(cleanupDelay)
+ }()
+
+ // Emit task started event
+ wailsRuntime.EventsEmit(a.ctx, "task-started", taskID, task)
+
+ var lastDownloaded int
+ var lastTime = time.Now()
+ var smoothedSpeed float64
+ var mu sync.Mutex
+
+ updateProgress := func(p ytdlp.ProgressUpdate) {
+ if p.Status != "downloading" {
+ return
+ }
+
+ mu.Lock()
+ now := time.Now()
+ elapsed := now.Sub(lastTime).Seconds()
+ var speed float64
+ if elapsed > 0 {
+ speed = float64(p.DownloadedBytes-lastDownloaded) / elapsed
+ }
+ if smoothedSpeed == 0 {
+ smoothedSpeed = speed
+ } else {
+ smoothedSpeed = speedSmoothingAlpha*speed + (1-speedSmoothingAlpha)*smoothedSpeed
+ }
+ lastDownloaded = p.DownloadedBytes
+ lastTime = now
+ currentSpeed := smoothedSpeed
+ mu.Unlock()
+
+ update := ProgressUpdate{
+ TaskID: taskID,
+ Percent: p.Percent(),
+ Status: "downloading",
+ Speed: formatSpeed(currentSpeed),
+ ETA: formatETA(p.ETA()),
+ }
+
+ if p.Percent() >= 100 {
+ update.Status = "processing"
+ update.Percent = 100
+ }
+
+ wailsRuntime.EventsEmit(a.ctx, "task-progress", update)
+ }
+
+ // Update yt-dlp before starting
+ a.UpdateYtDlp()
+
+ outDir := os.Getenv("PREDATOR_OUTPUT_DIR")
+ if outDir == "" {
+ outDir = "."
+ }
+
+ var err error
+
+ // Retry logic
+ for attempt := 0; attempt < maxRetries; attempt++ {
+ if attempt > 0 {
+ wailsRuntime.EventsEmit(a.ctx, "task-retry", taskID, attempt+1, maxRetries)
+ time.Sleep(time.Duration(attempt) * retryBaseDelay)
+ }
+
+ if ctx.Err() == context.Canceled {
+ break
+ }
+
+ if task.Type == "Video" {
+ format := buildVideoFormatString(task.CleanRes, true)
+ dl := ytdlp.New().
+ Format(format).
+ MergeOutputFormat("mp4").
+ RemuxVideo("mp4").
+ AudioFormat("m4a").
+ AudioQuality("0").
+ // Use ffmpeg to transcode any codec (vp9, av1, h264) to h264/aac
+ // This ensures single mp4 output regardless of source codec
+ PostProcessorArgs("FFmpegVideoConvertor:-c:v libx264 -preset fast -crf 23 -c:a aac -b:a 192k").
+ Output(filepath.Join(outDir, "%(title)s [%(id)s] (%(resolution)s).%(ext)s")).
+ ProgressFunc(progressUpdateInterval, updateProgress)
+
+ _, err = dl.Run(ctx, task.URL)
+ } else {
+ format := buildAudioFormatString(task.AudioFormat)
+ _, err = ytdlp.New().
+ ExtractAudio().
+ AudioFormat(task.AudioFormat).
+ AudioQuality("0").
+ Format(format).
+ Output(filepath.Join(outDir, "%(title)s [%(id)s].%(ext)s")).
+ ProgressFunc(progressUpdateInterval, updateProgress).
+ Run(ctx, task.URL)
+ }
+
+ if err == nil {
+ break
+ }
+
+ if ctx.Err() == context.Canceled {
+ break
+ }
+ }
+
+ // Emit completion event
+ if err != nil {
+ if ctx.Err() == context.Canceled {
+ wailsRuntime.EventsEmit(a.ctx, "task-cancelled", taskID)
+ } else {
+ errStr := err.Error()
+ if strings.Contains(errStr, "codec") ||
+ strings.Contains(errStr, "merge") ||
+ strings.Contains(errStr, "ffmpeg") ||
+ strings.Contains(errStr, "postprocessor") {
+ wailsRuntime.EventsEmit(a.ctx, "task-error", taskID, "Failed: codec/merge error. Try lower resolution.")
+ } else {
+ wailsRuntime.EventsEmit(a.ctx, "task-error", taskID, truncateError(err.Error(), 50))
+ }
+ }
+ } else {
+ wailsRuntime.EventsEmit(a.ctx, "task-completed", taskID, task)
+
+ // Find the actual downloaded file
+ var actualFilePath string
+ var fileSize int64
+
+ // Try to find the file with the title pattern
+ pattern := filepath.Join(outDir, "*"+strings.ReplaceAll(task.Title, " ", "*")+"*")
+ if files, err := filepath.Glob(pattern); err == nil && len(files) > 0 {
+ // Find the largest file (in case there are multiple matches)
+ var largestFile string
+ var largestSize int64
+ for _, f := range files {
+ if info, err := os.Stat(f); err == nil && !info.IsDir() {
+ if info.Size() > largestSize {
+ largestSize = info.Size()
+ largestFile = f
+ }
+ }
+ }
+ if largestFile != "" {
+ actualFilePath = largestFile
+ fileSize = largestSize
+ }
+ }
+
+ // If still not found, try a broader search
+ if actualFilePath == "" {
+ // List all files in directory and find the most recent one
+ if entries, err := os.ReadDir(outDir); err == nil {
+ var mostRecentFile string
+ var mostRecentTime time.Time
+ for _, entry := range entries {
+ if !entry.IsDir() {
+ info, err := entry.Info()
+ if err == nil {
+ if info.ModTime().After(mostRecentTime) {
+ mostRecentTime = info.ModTime()
+ mostRecentFile = filepath.Join(outDir, entry.Name())
+ }
+ }
+ }
+ }
+ if mostRecentFile != "" {
+ actualFilePath = mostRecentFile
+ if info, err := os.Stat(mostRecentFile); err == nil {
+ fileSize = info.Size()
+ }
+ }
+ }
+ }
+
+ // Save to history with actual file path
+ historyItem := DownloadHistory{
+ ID: taskID,
+ URL: task.URL,
+ Title: task.Title,
+ Type: task.Type,
+ Resolution: task.Resolution,
+ AudioFormat: task.AudioFormat,
+ FilePath: actualFilePath,
+ FileSize: fileSize,
+ DownloadedAt: time.Now(),
+ Status: "completed",
+ }
+
+ a.SaveToHistory(historyItem)
+ }
+}
diff --git a/Predator/build/README.md b/Predator/build/README.md
new file mode 100644
index 0000000..1ae2f67
--- /dev/null
+++ b/Predator/build/README.md
@@ -0,0 +1,35 @@
+# Build Directory
+
+The build directory is used to house all the build files and assets for your application.
+
+The structure is:
+
+* bin - Output directory
+* darwin - macOS specific files
+* windows - Windows specific files
+
+## Mac
+
+The `darwin` directory holds files specific to Mac builds.
+These may be customised and used as part of the build. To return these files to the default state, simply delete them
+and
+build with `wails build`.
+
+The directory contains the following files:
+
+- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
+- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
+
+## Windows
+
+The `windows` directory contains the manifest and rc files used when building with `wails build`.
+These may be customised for your application. To return these files to the default state, simply delete them and
+build with `wails build`.
+
+- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
+ use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
+ will be created using the `appicon.png` file in the build directory.
+- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
+- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
+ as well as the application itself (right click the exe -> properties -> details)
+- `wails.exe.manifest` - The main application manifest file.
\ No newline at end of file
diff --git a/Predator/build/appicon.png b/Predator/build/appicon.png
new file mode 100644
index 0000000..63617fe
Binary files /dev/null and b/Predator/build/appicon.png differ
diff --git a/Predator/build/darwin/Info.dev.plist b/Predator/build/darwin/Info.dev.plist
new file mode 100644
index 0000000..14121ef
--- /dev/null
+++ b/Predator/build/darwin/Info.dev.plist
@@ -0,0 +1,68 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.OutputFilename}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
diff --git a/Predator/build/darwin/Info.plist b/Predator/build/darwin/Info.plist
new file mode 100644
index 0000000..d17a747
--- /dev/null
+++ b/Predator/build/darwin/Info.plist
@@ -0,0 +1,63 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.OutputFilename}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+
+
diff --git a/Predator/build/windows/icon.ico b/Predator/build/windows/icon.ico
new file mode 100644
index 0000000..a749bf3
Binary files /dev/null and b/Predator/build/windows/icon.ico differ
diff --git a/Predator/build/windows/info.json b/Predator/build/windows/info.json
new file mode 100644
index 0000000..9727946
--- /dev/null
+++ b/Predator/build/windows/info.json
@@ -0,0 +1,15 @@
+{
+ "fixed": {
+ "file_version": "{{.Info.ProductVersion}}"
+ },
+ "info": {
+ "0000": {
+ "ProductVersion": "{{.Info.ProductVersion}}",
+ "CompanyName": "{{.Info.CompanyName}}",
+ "FileDescription": "{{.Info.ProductName}}",
+ "LegalCopyright": "{{.Info.Copyright}}",
+ "ProductName": "{{.Info.ProductName}}",
+ "Comments": "{{.Info.Comments}}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Predator/build/windows/installer/project.nsi b/Predator/build/windows/installer/project.nsi
new file mode 100644
index 0000000..654ae2e
--- /dev/null
+++ b/Predator/build/windows/installer/project.nsi
@@ -0,0 +1,114 @@
+Unicode true
+
+####
+## Please note: Template replacements don't work in this file. They are provided with default defines like
+## mentioned underneath.
+## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
+## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
+## from outside of Wails for debugging and development of the installer.
+##
+## For development first make a wails nsis build to populate the "wails_tools.nsh":
+## > wails build --target windows/amd64 --nsis
+## Then you can call makensis on this file with specifying the path to your binary:
+## For a AMD64 only installer:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
+## For a ARM64 only installer:
+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
+## For a installer with both architectures:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
+####
+## The following information is taken from the ProjectInfo file, but they can be overwritten here.
+####
+## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
+## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
+## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
+## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
+## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
+###
+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+####
+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
+####
+## Include the wails tools
+####
+!include "wails_tools.nsh"
+
+# The version information for this two must consist of 4 parts
+VIProductVersion "${INFO_PRODUCTVERSION}.0"
+VIFileVersion "${INFO_PRODUCTVERSION}.0"
+
+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
+
+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
+ManifestDPIAware true
+
+!include "MUI.nsh"
+
+!define MUI_ICON "..\icon.ico"
+!define MUI_UNICON "..\icon.ico"
+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
+
+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
+!insertmacro MUI_PAGE_INSTFILES # Installing page.
+!insertmacro MUI_PAGE_FINISH # Finished installation page.
+
+!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
+
+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
+
+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
+#!uninstfinalize 'signtool --file "%1"'
+#!finalize 'signtool --file "%1"'
+
+Name "${INFO_PRODUCTNAME}"
+OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
+InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
+ShowInstDetails show # This will always show the installation details.
+
+Function .onInit
+ !insertmacro wails.checkArchitecture
+FunctionEnd
+
+Section
+ !insertmacro wails.setShellContext
+
+ !insertmacro wails.webview2runtime
+
+ SetOutPath $INSTDIR
+
+ !insertmacro wails.files
+
+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+
+ !insertmacro wails.associateFiles
+ !insertmacro wails.associateCustomProtocols
+
+ !insertmacro wails.writeUninstaller
+SectionEnd
+
+Section "uninstall"
+ !insertmacro wails.setShellContext
+
+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
+
+ RMDir /r $INSTDIR
+
+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
+
+ !insertmacro wails.unassociateFiles
+ !insertmacro wails.unassociateCustomProtocols
+
+ !insertmacro wails.deleteUninstaller
+SectionEnd
diff --git a/Predator/build/windows/installer/wails_tools.nsh b/Predator/build/windows/installer/wails_tools.nsh
new file mode 100644
index 0000000..2f6d321
--- /dev/null
+++ b/Predator/build/windows/installer/wails_tools.nsh
@@ -0,0 +1,249 @@
+# DO NOT EDIT - Generated automatically by `wails build`
+
+!include "x64.nsh"
+!include "WinVer.nsh"
+!include "FileFunc.nsh"
+
+!ifndef INFO_PROJECTNAME
+ !define INFO_PROJECTNAME "{{.Name}}"
+!endif
+!ifndef INFO_COMPANYNAME
+ !define INFO_COMPANYNAME "{{.Info.CompanyName}}"
+!endif
+!ifndef INFO_PRODUCTNAME
+ !define INFO_PRODUCTNAME "{{.Info.ProductName}}"
+!endif
+!ifndef INFO_PRODUCTVERSION
+ !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
+!endif
+!ifndef INFO_COPYRIGHT
+ !define INFO_COPYRIGHT "{{.Info.Copyright}}"
+!endif
+!ifndef PRODUCT_EXECUTABLE
+ !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
+!endif
+!ifndef UNINST_KEY_NAME
+ !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+!endif
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
+
+!ifndef REQUEST_EXECUTION_LEVEL
+ !define REQUEST_EXECUTION_LEVEL "admin"
+!endif
+
+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
+
+!ifdef ARG_WAILS_AMD64_BINARY
+ !define SUPPORTS_AMD64
+!endif
+
+!ifdef ARG_WAILS_ARM64_BINARY
+ !define SUPPORTS_ARM64
+!endif
+
+!ifdef SUPPORTS_AMD64
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "amd64_arm64"
+ !else
+ !define ARCH "amd64"
+ !endif
+!else
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "arm64"
+ !else
+ !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
+ !endif
+!endif
+
+!macro wails.checkArchitecture
+ !ifndef WAILS_WIN10_REQUIRED
+ !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
+ !endif
+
+ !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
+ !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
+ !endif
+
+ ${If} ${AtLeastWin10}
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ IfSilent silentArch notSilentArch
+ silentArch:
+ SetErrorLevel 65
+ Abort
+ notSilentArch:
+ MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
+ Quit
+ ${else}
+ IfSilent silentWin notSilentWin
+ silentWin:
+ SetErrorLevel 64
+ Abort
+ notSilentWin:
+ MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
+ Quit
+ ${EndIf}
+
+ ok:
+!macroend
+
+!macro wails.files
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
+ ${EndIf}
+ !endif
+!macroend
+
+!macro wails.writeUninstaller
+ WriteUninstaller "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+ WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+
+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
+ IntFmt $0 "0x%08X" $0
+ WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
+!macroend
+
+!macro wails.deleteUninstaller
+ Delete "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ DeleteRegKey HKLM "${UNINST_KEY}"
+!macroend
+
+!macro wails.setShellContext
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
+ SetShellVarContext all
+ ${else}
+ SetShellVarContext current
+ ${EndIf}
+!macroend
+
+# Install webview2 by launching the bootstrapper
+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
+!macro wails.webview2runtime
+ !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
+ !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
+ !endif
+
+ SetRegView 64
+ # If the admin key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
+ # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+ ${EndIf}
+
+ SetDetailsPrint both
+ DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
+ SetDetailsPrint listonly
+
+ InitPluginsDir
+ CreateDirectory "$pluginsdir\webview2bootstrapper"
+ SetOutPath "$pluginsdir\webview2bootstrapper"
+ File "tmp\MicrosoftEdgeWebview2Setup.exe"
+ ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
+
+ SetDetailsPrint both
+ ok:
+!macroend
+
+# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_UNASSOCIATE EXT FILECLASS
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
+
+ DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
+!macroend
+
+!macro wails.associateFiles
+ ; Create file associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ File "..\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro wails.unassociateFiles
+ ; Delete app associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
+
+ Delete "$INSTDIR\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
+!macroend
+
+!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+!macroend
+
+!macro wails.associateCustomProtocols
+ ; Create custom protocols associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ {{end}}
+!macroend
+
+!macro wails.unassociateCustomProtocols
+ ; Delete app custom protocol associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
+ {{end}}
+!macroend
diff --git a/Predator/build/windows/wails.exe.manifest b/Predator/build/windows/wails.exe.manifest
new file mode 100644
index 0000000..17e1a23
--- /dev/null
+++ b/Predator/build/windows/wails.exe.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
\ No newline at end of file
diff --git a/Predator/convert-icon.js b/Predator/convert-icon.js
new file mode 100644
index 0000000..afc1d04
--- /dev/null
+++ b/Predator/convert-icon.js
@@ -0,0 +1,44 @@
+const fs = require('fs');
+const path = require('path');
+
+// Simple ICO file creation
+// ICO format: header (6 bytes) + directory entries (16 bytes each) + image data
+
+function createICO(inputPath, outputPath) {
+ const pngBuffer = fs.readFileSync(inputPath);
+
+ // ICO header
+ const header = Buffer.alloc(6);
+ header.writeUInt16LE(0, 0); // Reserved
+ header.writeUInt16LE(1, 2); // Type: 1 = ICO
+ header.writeUInt16LE(1, 4); // Count: 1 image
+
+ // Directory entry
+ const entry = Buffer.alloc(16);
+ entry.writeUInt8(32, 0); // Width (0 means 256)
+ entry.writeUInt8(32, 1); // Height (0 means 256)
+ entry.writeUInt8(0, 2); // Color palette
+ entry.writeUInt8(0, 3); // Reserved
+ entry.writeUInt16LE(1, 4); // Color planes
+ entry.writeUInt16LE(32, 6); // Bits per pixel
+ entry.writeUInt32LE(pngBuffer.length, 8); // Size of image data
+ entry.writeUInt32LE(22, 12); // Offset to image data (6 + 16 = 22)
+
+ // Combine all parts
+ const icoBuffer = Buffer.concat([header, entry, pngBuffer]);
+
+ fs.writeFileSync(outputPath, icoBuffer);
+ console.log(`Created ICO: ${outputPath} (${icoBuffer.length} bytes)`);
+}
+
+// Use logov4.png from parent directory
+const inputFile = process.argv[2] || '../logov4.png';
+const outputFile = process.argv[3] || './build/windows/icon.ico';
+
+try {
+ createICO(inputFile, outputFile);
+ console.log('Icon conversion successful!');
+} catch (err) {
+ console.error('Error:', err.message);
+ process.exit(1);
+}
diff --git a/Predator/frontend/index.html b/Predator/frontend/index.html
new file mode 100644
index 0000000..cb0e338
--- /dev/null
+++ b/Predator/frontend/index.html
@@ -0,0 +1,155 @@
+
+
+
+
+
+ Predator
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Download location not set
+
+
+
+
+
+
+
+
Ready
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Completed Downloads
+
+
No download history yet
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to clear all download history?
+
This action cannot be undone.
+
+
+
+
+
+
+
+
+
+
+
The downloaded file could not be found.
+
It may have been moved or deleted.
+
+
+
+
+
+
+
+
diff --git a/Predator/frontend/package-lock.json b/Predator/frontend/package-lock.json
new file mode 100644
index 0000000..66f18a7
--- /dev/null
+++ b/Predator/frontend/package-lock.json
@@ -0,0 +1,1104 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "devDependencies": {
+ "vite": "^7.3.1"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.3",
+ "@esbuild/android-arm": "0.27.3",
+ "@esbuild/android-arm64": "0.27.3",
+ "@esbuild/android-x64": "0.27.3",
+ "@esbuild/darwin-arm64": "0.27.3",
+ "@esbuild/darwin-x64": "0.27.3",
+ "@esbuild/freebsd-arm64": "0.27.3",
+ "@esbuild/freebsd-x64": "0.27.3",
+ "@esbuild/linux-arm": "0.27.3",
+ "@esbuild/linux-arm64": "0.27.3",
+ "@esbuild/linux-ia32": "0.27.3",
+ "@esbuild/linux-loong64": "0.27.3",
+ "@esbuild/linux-mips64el": "0.27.3",
+ "@esbuild/linux-ppc64": "0.27.3",
+ "@esbuild/linux-riscv64": "0.27.3",
+ "@esbuild/linux-s390x": "0.27.3",
+ "@esbuild/linux-x64": "0.27.3",
+ "@esbuild/netbsd-arm64": "0.27.3",
+ "@esbuild/netbsd-x64": "0.27.3",
+ "@esbuild/openbsd-arm64": "0.27.3",
+ "@esbuild/openbsd-x64": "0.27.3",
+ "@esbuild/openharmony-arm64": "0.27.3",
+ "@esbuild/sunos-x64": "0.27.3",
+ "@esbuild/win32-arm64": "0.27.3",
+ "@esbuild/win32-ia32": "0.27.3",
+ "@esbuild/win32-x64": "0.27.3"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
+ "@rollup/rollup-android-arm64": "4.57.1",
+ "@rollup/rollup-darwin-arm64": "4.57.1",
+ "@rollup/rollup-darwin-x64": "4.57.1",
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
+ "@rollup/rollup-freebsd-x64": "4.57.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
+ "@rollup/rollup-openbsd-x64": "4.57.1",
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/Predator/frontend/package.json b/Predator/frontend/package.json
new file mode 100644
index 0000000..4022432
--- /dev/null
+++ b/Predator/frontend/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^7.3.1"
+ }
+}
\ No newline at end of file
diff --git a/Predator/frontend/package.json.md5 b/Predator/frontend/package.json.md5
new file mode 100644
index 0000000..d4671a1
--- /dev/null
+++ b/Predator/frontend/package.json.md5
@@ -0,0 +1 @@
+5fbf12469d224a93954efecb5886e8a6
\ No newline at end of file
diff --git a/Predator/frontend/src/app.css b/Predator/frontend/src/app.css
new file mode 100644
index 0000000..27eeefe
--- /dev/null
+++ b/Predator/frontend/src/app.css
@@ -0,0 +1,843 @@
+/* Header - Clean Minimal */
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 20px;
+ background-color: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.title {
+ font-size: 1.5rem;
+ font-weight: 800;
+ color: var(--accent-primary);
+ letter-spacing: -0.03em;
+}
+
+.theme-btn {
+ width: 36px;
+ height: 36px;
+ border-radius: var(--radius-md);
+ border: 2px solid var(--accent-primary);
+ background-color: var(--bg-tertiary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ box-shadow: 0 0 10px var(--accent-glow);
+}
+
+.theme-btn:hover {
+ border-color: var(--accent-primary);
+ background-color: var(--bg-card);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+.theme-btn img {
+ width: 18px;
+ height: 18px;
+ opacity: 1;
+ transition: transform 0.3s ease;
+ filter: invert(1) brightness(1.2);
+}
+
+.theme-btn:hover img {
+ transform: rotate(15deg);
+}
+
+/* Main Content */
+.main-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+/* Tabs - Underline Style */
+.tabs {
+ display: flex;
+ gap: 4px;
+ padding: 8px 20px 0;
+ background-color: var(--bg-primary);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.tab-btn {
+ padding: 10px 16px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 0.85rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ transition: all 0.2s ease;
+ margin-bottom: -1px;
+}
+
+.tab-btn:hover {
+ color: var(--text-primary);
+ background-color: var(--bg-tertiary);
+}
+
+.tab-btn.active {
+ color: var(--accent-primary);
+ border-bottom-color: var(--accent-primary);
+ background-color: var(--bg-tertiary);
+}
+
+/* Tab Content */
+.tab-content {
+ display: none;
+ flex: 1;
+ flex-direction: column;
+ padding: 16px 20px;
+ overflow-y: auto;
+ gap: 12px;
+ animation: fadeIn 0.3s ease;
+}
+
+.tab-content.active {
+ display: flex;
+}
+
+/* Input Group */
+.input-group {
+ width: 100%;
+}
+
+.url-input-wrapper {
+ display: flex;
+ align-items: center;
+ background-color: var(--bg-input);
+ border: 2px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 4px;
+ transition: all 0.2s ease;
+}
+
+.url-input-wrapper:focus-within {
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 3px var(--accent-glow);
+}
+
+#url-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ padding: 10px 14px;
+ color: var(--text-primary);
+ font-size: 0.95rem;
+ outline: none;
+}
+
+#url-input::placeholder {
+ color: var(--text-muted);
+}
+
+.clear-btn {
+ background: transparent;
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ padding: 8px 12px;
+ font-size: 1.1rem;
+ border-radius: var(--radius-sm);
+ transition: all 0.2s ease;
+ margin-right: 4px;
+}
+
+.clear-btn:hover {
+ color: var(--error);
+ background-color: rgba(255, 51, 102, 0.1);
+}
+
+/* Title Display */
+.title-display {
+ background-color: var(--bg-card);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 10px 14px;
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ line-height: 1.4;
+}
+
+/* Download Type - Inline Compact */
+.download-type {
+ display: flex;
+ gap: 6px;
+ justify-content: flex-start;
+ flex-wrap: nowrap;
+}
+
+.radio-label {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ cursor: pointer;
+ padding: 8px 12px;
+ background-color: var(--bg-card);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ font-weight: 600;
+ font-size: 0.8rem;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+ min-width: 50px;
+}
+
+.radio-label:hover {
+ border-color: var(--accent-primary);
+ color: var(--text-primary);
+}
+
+.radio-label:has(input[type="radio"]:checked) {
+ background-color: var(--bg-tertiary);
+ border-color: var(--accent-primary);
+ color: var(--accent-primary);
+ box-shadow: 0 0 10px var(--accent-glow);
+}
+
+.radio-label input[type="radio"] {
+ display: none;
+}
+
+/* Format Selection - Flexbox for same row */
+.format-selection {
+ display: flex;
+ flex-direction: row;
+ gap: 12px;
+ align-items: flex-end;
+}
+
+.select-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ flex: 1;
+}
+
+.select-wrapper:first-child {
+ flex: 0 0 auto;
+ min-width: 120px;
+}
+
+.select-wrapper label {
+ font-size: 0.7rem;
+ color: var(--text-muted);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.select-wrapper select {
+ background-color: var(--bg-input);
+ border: 2px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 10px 12px;
+ color: var(--text-primary);
+ font-size: 0.9rem;
+ cursor: pointer;
+ outline: none;
+ transition: all 0.2s ease;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23666666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 10px center;
+ background-size: 16px;
+ padding-right: 32px;
+}
+
+.select-wrapper select:focus {
+ border-color: var(--accent-primary);
+}
+
+.select-wrapper select:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Output Section */
+.output-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.output-dir {
+ background-color: var(--bg-input);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 10px 14px;
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ font-family: 'SF Mono', Monaco, monospace;
+ word-break: break-all;
+}
+
+/* Buttons */
+.primary-btn {
+ background-color: var(--accent-primary);
+ color: var(--bg-primary);
+ border: none;
+ border-radius: var(--radius-md);
+ padding: 12px 24px;
+ font-size: 0.95rem;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: var(--shadow-sm);
+ margin-top: 4px;
+}
+
+.primary-btn:hover:not(:disabled) {
+ background-color: var(--accent-secondary);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-glow);
+}
+
+.primary-btn:active:not(:disabled) {
+ transform: translateY(0);
+}
+
+.primary-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.secondary-btn {
+ background-color: var(--bg-card);
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 10px 14px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.secondary-btn:hover {
+ background-color: var(--bg-tertiary);
+ border-color: var(--accent-primary);
+ color: var(--accent-primary);
+}
+
+/* Status */
+.status {
+ text-align: center;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ padding: 8px;
+ min-height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ margin-top: 4px;
+}
+
+.status:empty::before {
+ content: 'Ready to download';
+ color: var(--text-muted);
+}
+
+/* Downloads Container */
+.downloads-container {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.empty-state {
+ text-align: center;
+ color: var(--text-muted);
+ padding: 20px;
+ font-size: 0.9rem;
+ background-color: var(--bg-card);
+ border: 2px dashed var(--border-color);
+ border-radius: var(--radius-md);
+}
+
+/* Download Task Card */
+.download-task {
+ background-color: var(--bg-card);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ animation: slideInRight 0.3s ease;
+ box-shadow: var(--shadow-sm);
+}
+
+.download-task:hover {
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 15px var(--accent-glow);
+}
+
+.task-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 10px;
+}
+
+.task-title {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ line-height: 1.4;
+ flex: 1;
+}
+
+.task-cancel-btn {
+ background-color: transparent;
+ color: var(--error);
+ border: 1px solid var(--error);
+ border-radius: var(--radius-sm);
+ padding: 4px 8px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+}
+
+.task-cancel-btn:hover {
+ background-color: var(--error);
+ color: white;
+}
+
+.task-cancel-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+/* Progress Bar */
+.progress-container {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 5px;
+ background-color: var(--bg-tertiary);
+ border-radius: var(--radius-full);
+ overflow: hidden;
+}
+
+.progress-fill {
+ height: 100%;
+ background: var(--accent-gradient);
+ border-radius: var(--radius-full);
+ transition: width 0.3s ease;
+ position: relative;
+}
+
+.progress-fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
+ animation: shimmer 2s infinite;
+}
+
+.progress-info {
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.task-status {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.task-status::before {
+ content: '';
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background-color: var(--text-muted);
+}
+
+.task-status.completed {
+ color: var(--success);
+}
+
+.task-status.completed::before {
+ background-color: var(--success);
+}
+
+.task-status.error {
+ color: var(--error);
+}
+
+.task-status.error::before {
+ background-color: var(--error);
+}
+
+.task-status.downloading {
+ color: var(--accent-primary);
+}
+
+.task-status.downloading::before {
+ background-color: var(--accent-primary);
+ animation: pulse 1.5s infinite;
+}
+
+/* Loading Spinner */
+.spinner {
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--border-color);
+ border-top-color: var(--accent-primary);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+/* History Section */
+.history-filters {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.filter-btn {
+ background-color: var(--bg-card);
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 6px 14px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.filter-btn:hover {
+ border-color: var(--accent-primary);
+ color: var(--text-primary);
+}
+
+.filter-btn.active {
+ background-color: var(--accent-primary);
+ color: var(--bg-primary);
+ border-color: var(--accent-primary);
+}
+
+.clear-history-btn {
+ background-color: transparent;
+ color: var(--error);
+ border: 1px solid var(--error);
+ border-radius: var(--radius-md);
+ padding: 6px 14px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ margin-left: auto;
+}
+
+.clear-history-btn:hover {
+ background-color: var(--error);
+ color: white;
+}
+
+.history-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.history-section-title {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin: 0;
+}
+
+.history-container {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+/* History Item */
+.history-item {
+ background-color: var(--bg-card);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 10px 12px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ transition: all 0.2s ease;
+}
+
+.history-item:hover {
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 10px var(--accent-glow);
+}
+
+.history-item-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-sm);
+ background-color: var(--bg-tertiary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.9rem;
+ flex-shrink: 0;
+}
+
+.history-item-icon.video {
+ color: var(--accent-primary);
+}
+
+.history-item-icon.audio {
+ color: #ff6b9d;
+}
+
+.history-item-info {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.history-item-title {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.history-item-meta {
+ font-size: 0.7rem;
+ color: var(--text-muted);
+ display: flex;
+ gap: 8px;
+}
+
+.history-item-size {
+ color: var(--accent-primary);
+ font-weight: 500;
+}
+
+.history-item-folder-btn {
+ width: 28px;
+ height: 28px;
+ border-radius: var(--radius-sm);
+ background-color: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.9rem;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+}
+
+.history-item-folder-btn:hover {
+ background-color: var(--accent-primary);
+ color: var(--bg-primary);
+ border-color: var(--accent-primary);
+}
+
+/* Modal Dialog */
+.modal-overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.7);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(4px);
+}
+
+.modal-overlay.active {
+ display: flex;
+}
+
+.modal-dialog {
+ background-color: var(--bg-card);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-lg);
+ width: 90%;
+ max-width: 400px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+ animation: modalSlideIn 0.3s ease;
+}
+
+@keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.modal-title {
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.modal-close {
+ background: transparent;
+ border: none;
+ color: var(--text-muted);
+ font-size: 1.5rem;
+ cursor: pointer;
+ padding: 0;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-sm);
+ transition: all 0.2s ease;
+}
+
+.modal-close:hover {
+ color: var(--text-primary);
+ background-color: var(--bg-tertiary);
+}
+
+.modal-body {
+ padding: 20px;
+}
+
+.modal-body p {
+ margin: 0 0 8px 0;
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ line-height: 1.5;
+}
+
+.modal-warning {
+ color: var(--error) !important;
+ font-size: 0.85rem !important;
+ font-weight: 500;
+}
+
+.modal-footer {
+ display: flex;
+ gap: 10px;
+ padding: 0 20px 20px;
+ justify-content: flex-end;
+}
+
+.modal-btn {
+ padding: 10px 20px;
+ border-radius: var(--radius-md);
+ font-size: 0.9rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border: none;
+}
+
+.modal-btn-secondary {
+ background-color: var(--bg-tertiary);
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+}
+
+.modal-btn-secondary:hover {
+ background-color: var(--bg-input);
+ color: var(--text-primary);
+}
+
+.modal-btn-danger {
+ background-color: transparent;
+ color: var(--error);
+ border: 1px solid var(--error);
+}
+
+.modal-btn-danger:hover {
+ background-color: var(--error);
+ color: white;
+}
+
+/* Responsive */
+@media (max-width: 520px) {
+ .format-selection {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .select-wrapper:first-child {
+ min-width: auto;
+ }
+
+ .download-type {
+ width: 100%;
+ }
+
+ .radio-label {
+ flex: 1;
+ }
+
+ .header {
+ padding: 10px 16px;
+ }
+
+ .title {
+ font-size: 1.3rem;
+ }
+
+ .tab-content {
+ padding: 12px 16px;
+ }
+}
diff --git a/Predator/frontend/src/assets/download.svg b/Predator/frontend/src/assets/download.svg
new file mode 100644
index 0000000..0e8248d
--- /dev/null
+++ b/Predator/frontend/src/assets/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Predator/frontend/src/assets/fonts/OFL.txt b/Predator/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 0000000..9cac04c
--- /dev/null
+++ b/Predator/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/Predator/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/Predator/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 0000000..2f9cc59
Binary files /dev/null and b/Predator/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/Predator/frontend/src/assets/history.svg b/Predator/frontend/src/assets/history.svg
new file mode 100644
index 0000000..906fae8
--- /dev/null
+++ b/Predator/frontend/src/assets/history.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Predator/frontend/src/assets/images/Predator.iss b/Predator/frontend/src/assets/images/Predator.iss
new file mode 100644
index 0000000..448a97c
--- /dev/null
+++ b/Predator/frontend/src/assets/images/Predator.iss
@@ -0,0 +1,69 @@
+; Script generated by the Inno Setup Script Wizard.
+; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
+; Non-commercial use only
+
+#define MyAppName "Predator"
+#define MyAppVersion "1.0.0"
+#define MyAppPublisher "Aswanidev"
+#define MyAppURL "https://www.example.com/"
+#define MyAppExeName "Predator.exe"
+#define MyAppAssocName MyAppName + " File"
+#define MyAppAssocExt ".myp"
+#define MyAppAssocKey StringChange(MyAppAssocName, " ", "") + MyAppAssocExt
+
+[Setup]
+; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
+; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
+AppId={{B59BB20F-0A98-429D-9F2D-27475E87DAA6}
+AppName={#MyAppName}
+AppVersion={#MyAppVersion}
+;AppVerName={#MyAppName} {#MyAppVersion}
+AppPublisher={#MyAppPublisher}
+AppPublisherURL={#MyAppURL}
+AppSupportURL={#MyAppURL}
+AppUpdatesURL={#MyAppURL}
+DefaultDirName=G:\Predator\Predator\build\bin\{#MyAppName}
+UninstallDisplayIcon={app}\{#MyAppExeName}
+; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
+; on anything but x64 and Windows 11 on Arm.
+ArchitecturesAllowed=x64compatible
+; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the
+; install be done in "64-bit mode" on x64 or Windows 11 on Arm,
+; meaning it should use the native 64-bit Program Files directory and
+; the 64-bit view of the registry.
+ArchitecturesInstallIn64BitMode=x64compatible
+ChangesAssociations=yes
+DisableProgramGroupPage=yes
+; Uncomment the following line to run in non administrative install mode (install for current user only).
+;PrivilegesRequired=lowest
+PrivilegesRequiredOverridesAllowed=dialog
+OutputDir=C:\Users\LENOVO\Downloads
+OutputBaseFilename=Predator_base-File
+SetupIconFile=G:\Predator\Predator\frontend\src\assets\images\icon.ico
+SolidCompression=yes
+WizardStyle=modern dynamic
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Tasks]
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+
+[Files]
+Source: "G:\Predator\Predator\build\bin\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
+Source: "G:\Predator\Predator\build\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+; NOTE: Don't use "Flags: ignoreversion" on any shared system files
+
+[Registry]
+Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue
+Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey
+Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"
+Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
+
+[Icons]
+Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
+Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
+
+[Run]
+Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
+
diff --git a/Predator/frontend/src/assets/images/dark-mode.png b/Predator/frontend/src/assets/images/dark-mode.png
new file mode 100644
index 0000000..a41726b
Binary files /dev/null and b/Predator/frontend/src/assets/images/dark-mode.png differ
diff --git a/Predator/frontend/src/assets/images/icon.ico b/Predator/frontend/src/assets/images/icon.ico
new file mode 100644
index 0000000..67e85a4
Binary files /dev/null and b/Predator/frontend/src/assets/images/icon.ico differ
diff --git a/Predator/frontend/src/assets/images/logo-universal.png b/Predator/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 0000000..d63303b
Binary files /dev/null and b/Predator/frontend/src/assets/images/logo-universal.png differ
diff --git a/Predator/frontend/src/assets/images/logov4.png b/Predator/frontend/src/assets/images/logov4.png
new file mode 100644
index 0000000..67e85a4
Binary files /dev/null and b/Predator/frontend/src/assets/images/logov4.png differ
diff --git a/Predator/frontend/src/assets/images/summer.png b/Predator/frontend/src/assets/images/summer.png
new file mode 100644
index 0000000..7f701ee
Binary files /dev/null and b/Predator/frontend/src/assets/images/summer.png differ
diff --git a/Predator/frontend/src/main.js b/Predator/frontend/src/main.js
new file mode 100644
index 0000000..faf8582
--- /dev/null
+++ b/Predator/frontend/src/main.js
@@ -0,0 +1,703 @@
+// Wails Frontend - Predator YouTube Downloader
+import {
+ IsValidYouTubeURL,
+ FetchVideoInfo,
+ AddToQueue,
+ CancelTask,
+ SelectOutputDir,
+ GetOutputDir,
+ CheckAndInstallDeps,
+ GetDownloadHistory,
+ OpenFolder,
+ ClearHistory
+} from '../wailsjs/go/main/App';
+import {EventsOn, EventsEmit} from '../wailsjs/runtime/runtime';
+import lightIcon from './assets/images/summer.png';
+import darkIcon from './assets/images/dark-mode.png';
+
+// DOM Elements
+const urlInput = document.getElementById('url-input');
+const clearBtn = document.getElementById('clear-btn');
+const titleDisplay = document.getElementById('title-display');
+const downloadTypeRadios = document.querySelectorAll('input[name="download-type"]');
+const resolutionSelect = document.getElementById('resolution-select');
+const audioFormatSelect = document.getElementById('audio-format-select');
+const resolutionWrapper = document.getElementById('resolution-wrapper');
+const audioFormatWrapper = document.getElementById('audio-format-wrapper');
+const addBtn = document.getElementById('add-btn');
+const statusLabel = document.getElementById('status-label');
+const outputDirDisplay = document.getElementById('output-dir-display');
+const changeDirBtn = document.getElementById('change-dir-btn');
+const themeToggle = document.getElementById('theme-toggle');
+const themeIcon = document.getElementById('theme-icon');
+const tabBtns = document.querySelectorAll('.tab-btn');
+const tabContents = document.querySelectorAll('.tab-content');
+const downloadsContainer = document.getElementById('downloads-container');
+const historyContainer = document.getElementById('history-container');
+const filterBtns = document.querySelectorAll('.filter-btn');
+const clearHistoryModal = document.getElementById('clear-history-modal');
+const modalCloseBtn = document.getElementById('modal-close-btn');
+const modalCancelBtn = document.getElementById('modal-cancel-btn');
+const modalConfirmBtn = document.getElementById('modal-confirm-btn');
+
+// File Not Found Modal Elements
+const fileNotFoundModal = document.getElementById('file-not-found-modal');
+const fileNotFoundCloseBtn = document.getElementById('file-not-found-close-btn');
+const fileNotFoundOkBtn = document.getElementById('file-not-found-ok-btn');
+const fileNotFoundMessage = document.getElementById('file-not-found-message');
+
+// State
+let currentVideoInfo = null;
+let fetchTimer = null;
+let isFetching = false;
+let isDark = true;
+let activeDownloads = new Map();
+let downloadHistory = [];
+let currentFilter = 'all';
+
+// Constants
+const FETCH_DEBOUNCE = 600;
+const MAX_RETRIES = 3;
+
+// Initialize
+document.addEventListener('DOMContentLoaded', async () => {
+ // Check dependencies on startup
+ try {
+ await CheckAndInstallDeps();
+ } catch (err) {
+ console.log('Dependency check:', err);
+ }
+
+ // Get initial output directory
+ updateOutputDir();
+
+ // Setup event listeners
+ setupEventListeners();
+
+ // Setup Wails event handlers
+ setupWailsEvents();
+});
+
+// Event Listeners
+function setupEventListeners() {
+ // URL input with debounce
+ urlInput.addEventListener('input', handleUrlInput);
+
+ // Enable right-click context menu for paste
+ urlInput.addEventListener('contextmenu', (e) => {
+ // Allow default browser context menu for paste functionality
+ e.stopPropagation();
+ });
+
+ // Clear button
+ clearBtn.addEventListener('click', () => {
+ urlInput.value = '';
+ resetUI();
+ });
+
+ // Download type change
+ downloadTypeRadios.forEach(radio => {
+ radio.addEventListener('change', handleDownloadTypeChange);
+ });
+
+ // Add to queue
+ addBtn.addEventListener('click', addToQueue);
+
+ // Change output directory
+ changeDirBtn.addEventListener('click', async () => {
+ try {
+ const dir = await SelectOutputDir();
+ if (dir) {
+ updateOutputDir();
+ }
+ } catch (err) {
+ console.error('Failed to select directory:', err);
+ }
+ });
+
+ // Theme toggle
+ themeToggle.addEventListener('click', toggleTheme);
+
+ // Tab switching
+ tabBtns.forEach(btn => {
+ btn.addEventListener('click', () => switchTab(btn.dataset.tab));
+ });
+
+ // Filter buttons
+ filterBtns.forEach(btn => {
+ btn.addEventListener('click', () => setFilter(btn.dataset.filter));
+ });
+
+ // Clear history button
+ const clearHistoryBtn = document.getElementById('clear-history-btn');
+ if (clearHistoryBtn) {
+ clearHistoryBtn.addEventListener('click', showClearHistoryModal);
+ }
+
+ // Modal event listeners
+ if (modalCloseBtn) {
+ modalCloseBtn.addEventListener('click', hideClearHistoryModal);
+ }
+ if (modalCancelBtn) {
+ modalCancelBtn.addEventListener('click', hideClearHistoryModal);
+ }
+ if (modalConfirmBtn) {
+ modalConfirmBtn.addEventListener('click', confirmClearHistory);
+ }
+ if (clearHistoryModal) {
+ clearHistoryModal.addEventListener('click', (e) => {
+ if (e.target === clearHistoryModal) {
+ hideClearHistoryModal();
+ }
+ });
+ }
+
+ // File Not Found Modal event listeners
+ if (fileNotFoundCloseBtn) {
+ fileNotFoundCloseBtn.addEventListener('click', hideFileNotFoundModal);
+ }
+ if (fileNotFoundOkBtn) {
+ fileNotFoundOkBtn.addEventListener('click', hideFileNotFoundModal);
+ }
+ if (fileNotFoundModal) {
+ fileNotFoundModal.addEventListener('click', (e) => {
+ if (e.target === fileNotFoundModal) {
+ hideFileNotFoundModal();
+ }
+ });
+ }
+}
+
+// Wails Event Handlers
+function setupWailsEvents() {
+ // Task started
+ EventsOn('task-started', (taskId, task) => {
+ createDownloadTask(taskId, task);
+ });
+
+ // Task progress
+ EventsOn('task-progress', (update) => {
+ updateDownloadProgress(update);
+ });
+
+ // Task retry
+ EventsOn('task-retry', (taskId, attempt, maxAttempts) => {
+ updateTaskStatus(taskId, `Retrying... (${attempt}/${maxAttempts})`);
+ });
+
+ // Task completed
+ EventsOn('task-completed', (taskId, task) => {
+ completeDownloadTask(taskId);
+ // Refresh history after download completes
+ setTimeout(() => loadHistory(), 1000);
+ });
+
+ // Task error
+ EventsOn('task-error', (taskId, error) => {
+ errorDownloadTask(taskId, error);
+ });
+
+ // Task cancelled
+ EventsOn('task-cancelled', (taskId) => {
+ cancelDownloadTask(taskId);
+ });
+
+ // Installing deps
+ EventsOn('installing-deps', (installing) => {
+ if (installing) {
+ statusLabel.textContent = 'Installing dependencies...';
+ } else {
+ statusLabel.textContent = 'Ready';
+ }
+ });
+}
+
+// URL Input Handler with Debounce
+function handleUrlInput() {
+ const url = urlInput.value.trim();
+
+ // Clear existing timer
+ if (fetchTimer) {
+ clearTimeout(fetchTimer);
+ }
+
+ // Reset UI if empty
+ if (!url) {
+ resetUI();
+ return;
+ }
+
+ // Validate URL
+ if (!IsValidYouTubeURL(url)) {
+ statusLabel.textContent = 'Invalid YouTube URL';
+ titleDisplay.classList.add('hidden');
+ resolutionSelect.disabled = true;
+ addBtn.disabled = true;
+ return;
+ }
+
+ // Debounced fetch
+ fetchTimer = setTimeout(async () => {
+ if (isFetching) return;
+ isFetching = true;
+
+ statusLabel.textContent = 'Fetching video info...';
+ resolutionSelect.disabled = true;
+ addBtn.disabled = true;
+
+ try {
+ const info = await FetchVideoInfo(url);
+ currentVideoInfo = info;
+ displayVideoInfo(info);
+ statusLabel.textContent = 'Ready to download';
+ } catch (err) {
+ console.error('Fetch error:', err);
+ statusLabel.textContent = 'Failed to fetch info';
+ titleDisplay.classList.add('hidden');
+ } finally {
+ isFetching = false;
+ }
+ }, FETCH_DEBOUNCE);
+}
+
+// Display Video Info
+function displayVideoInfo(info) {
+ titleDisplay.textContent = `Title: ${info.title}`;
+ titleDisplay.classList.remove('hidden');
+
+ // Populate resolution select
+ resolutionSelect.innerHTML = '';
+ info.resolutions.forEach(res => {
+ const option = document.createElement('option');
+ option.value = res;
+ option.textContent = res;
+ resolutionSelect.appendChild(option);
+ });
+
+ // Auto-select best quality (prefer 1080p or 720p)
+ const bestIndex = info.resolutions.findIndex(r =>
+ r.includes('1080p') || r.includes('720p')
+ );
+ if (bestIndex >= 0) {
+ resolutionSelect.selectedIndex = bestIndex;
+ }
+
+ resolutionSelect.disabled = false;
+ addBtn.disabled = false;
+}
+
+// Handle Download Type Change
+function handleDownloadTypeChange(e) {
+ const type = e.target.value;
+
+ if (type === 'Video') {
+ resolutionWrapper.classList.remove('hidden');
+ audioFormatWrapper.classList.add('hidden');
+ resolutionSelect.disabled = !currentVideoInfo;
+ } else {
+ resolutionWrapper.classList.add('hidden');
+ audioFormatWrapper.classList.remove('hidden');
+ }
+}
+
+// Add to Queue
+async function addToQueue() {
+ const url = urlInput.value.trim();
+ const type = document.querySelector('input[name="download-type"]:checked').value;
+
+ if (!url || !await IsValidYouTubeURL(url)) {
+ statusLabel.textContent = 'Please enter a valid YouTube URL';
+ return;
+ }
+
+ const task = {
+ url: url,
+ title: currentVideoInfo?.title || 'Unknown',
+ type: type,
+ resolution: type === 'Video' ? resolutionSelect.value : '',
+ cleanRes: type === 'Video' ? extractResolution(resolutionSelect.value) : '',
+ audioFormat: type === 'Audio' ? audioFormatSelect.value : '',
+ audioQuality: '0',
+ videoCodec: ''
+ };
+
+ try {
+ const taskId = await AddToQueue(task);
+ console.log('Added to queue:', taskId);
+
+ // Reset UI
+ urlInput.value = '';
+ resetUI();
+
+ // Switch to history tab
+ switchTab('history');
+
+ statusLabel.textContent = 'Added to queue';
+ } catch (err) {
+ console.error('Failed to add to queue:', err);
+ statusLabel.textContent = 'Failed to add to queue';
+ }
+}
+
+// Create Download Task UI
+function createDownloadTask(taskId, task) {
+ const taskElement = document.createElement('div');
+ taskElement.className = 'download-task';
+ taskElement.id = `task-${taskId}`;
+ taskElement.innerHTML = `
+
+
+ Starting...
+ `;
+
+ // Remove empty state if present
+ const emptyState = downloadsContainer.querySelector('.empty-state');
+ if (emptyState) {
+ emptyState.remove();
+ }
+
+ downloadsContainer.appendChild(taskElement);
+ activeDownloads.set(taskId, taskElement);
+}
+
+// Update Download Progress
+function updateDownloadProgress(update) {
+ const taskElement = activeDownloads.get(update.taskId);
+ if (!taskElement) return;
+
+ const progressFill = taskElement.querySelector('.progress-fill');
+ const progressPercent = taskElement.querySelector('.progress-percent');
+ const progressSpeed = taskElement.querySelector('.progress-speed');
+ const taskStatus = taskElement.querySelector('.task-status');
+
+ const percent = Math.min(update.percent, 100);
+ progressFill.style.width = `${percent}%`;
+ progressPercent.textContent = `${percent.toFixed(1)}%`;
+
+ if (update.status === 'downloading') {
+ progressSpeed.textContent = `${update.speed} | ETA: ${update.eta}`;
+ taskStatus.textContent = `Downloading... ${percent.toFixed(1)}%`;
+ taskStatus.className = 'task-status';
+ } else if (update.status === 'processing') {
+ progressSpeed.textContent = '';
+ taskStatus.textContent = 'Processing... (merging)';
+ }
+}
+
+// Update Task Status
+function updateTaskStatus(taskId, status) {
+ const taskElement = activeDownloads.get(taskId);
+ if (!taskElement) return;
+
+ const taskStatus = taskElement.querySelector('.task-status');
+ taskStatus.textContent = status;
+}
+
+// Complete Download Task
+function completeDownloadTask(taskId) {
+ const taskElement = activeDownloads.get(taskId);
+ if (!taskElement) return;
+
+ const progressFill = taskElement.querySelector('.progress-fill');
+ const progressPercent = taskElement.querySelector('.progress-percent');
+ const progressSpeed = taskElement.querySelector('.progress-speed');
+ const taskStatus = taskElement.querySelector('.task-status');
+ const cancelBtn = taskElement.querySelector('.task-cancel-btn');
+
+ progressFill.style.width = '100%';
+ progressPercent.textContent = '100%';
+ progressSpeed.textContent = '';
+ taskStatus.textContent = 'Completed ✓';
+ taskStatus.className = 'task-status completed';
+ cancelBtn.disabled = true;
+
+ // Remove after delay
+ setTimeout(() => {
+ taskElement.remove();
+ activeDownloads.delete(taskId);
+ checkEmptyState();
+ }, 3000);
+}
+
+// Load Download History
+async function loadHistory() {
+ try {
+ downloadHistory = await GetDownloadHistory();
+ renderHistory();
+ } catch (err) {
+ console.error('Failed to load history:', err);
+ }
+}
+
+// Set Filter
+function setFilter(filter) {
+ currentFilter = filter;
+
+ // Update button states
+ filterBtns.forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.filter === filter);
+ });
+
+ renderHistory();
+}
+
+// Render History
+function renderHistory() {
+ // Filter history
+ let filtered = downloadHistory;
+ if (currentFilter !== 'all') {
+ filtered = downloadHistory.filter(item => item.type === currentFilter);
+ }
+
+ // Clear container
+ historyContainer.innerHTML = '';
+
+ if (filtered.length === 0) {
+ historyContainer.innerHTML = 'No download history
';
+ return;
+ }
+
+ // Render items
+ filtered.forEach(item => {
+ const historyItem = createHistoryItem(item);
+ historyContainer.appendChild(historyItem);
+ });
+}
+
+// Create History Item Element
+function createHistoryItem(item) {
+ const div = document.createElement('div');
+ div.className = 'history-item';
+
+ const isVideo = item.type === 'Video';
+ const icon = isVideo ? '🎬' : '🎵';
+ const typeClass = isVideo ? 'video' : 'audio';
+ const format = isVideo ? item.resolution : item.audioFormat;
+ const size = item.fileSize ? formatBytes(item.fileSize) : 'Unknown size';
+ const date = new Date(item.downloadedAt).toLocaleDateString();
+
+ div.innerHTML = `
+ ${icon}
+
+
${escapeHtml(item.title)}
+
+ ${format}
+ ${size}
+ ${date}
+
+
+
+ `;
+
+ const folderButton = div.querySelector('.history-item-folder-btn');
+ if (folderButton) {
+ folderButton.addEventListener('click', () => {
+ // Use the original, unescaped file path when opening the folder
+ openHistoryFolder(item.filePath);
+ });
+ }
+
+ return div;
+}
+
+// Open History Folder
+window.openHistoryFolder = async function(path) {
+ try {
+ console.log('Opening folder:', path);
+ await OpenFolder(path);
+ } catch (err) {
+ console.error('Failed to open folder:', err);
+ showFileNotFoundModal('The file or folder could not be found. It may have been moved or deleted.');
+ }
+};
+
+// Show File Not Found Modal
+function showFileNotFoundModal(message) {
+ if (fileNotFoundMessage) {
+ fileNotFoundMessage.textContent = message || 'The downloaded file could not be found.';
+ }
+ if (fileNotFoundModal) {
+ fileNotFoundModal.classList.add('active');
+ }
+}
+
+// Hide File Not Found Modal
+function hideFileNotFoundModal() {
+ if (fileNotFoundModal) {
+ fileNotFoundModal.classList.remove('active');
+ }
+}
+
+// Format bytes helper
+function formatBytes(bytes) {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
+}
+
+// Show Clear History Modal
+function showClearHistoryModal() {
+ if (clearHistoryModal) {
+ clearHistoryModal.classList.add('active');
+ }
+}
+
+// Hide Clear History Modal
+function hideClearHistoryModal() {
+ if (clearHistoryModal) {
+ clearHistoryModal.classList.remove('active');
+ }
+}
+
+// Confirm Clear History
+async function confirmClearHistory() {
+ hideClearHistoryModal();
+
+ try {
+ await ClearHistory();
+ downloadHistory = [];
+ renderHistory();
+ console.log('History cleared');
+ } catch (err) {
+ console.error('Failed to clear history:', err);
+ }
+}
+
+// Error Download Task
+function errorDownloadTask(taskId, error) {
+ const taskElement = activeDownloads.get(taskId);
+ if (!taskElement) return;
+
+ const progressSpeed = taskElement.querySelector('.progress-speed');
+ const taskStatus = taskElement.querySelector('.task-status');
+ const cancelBtn = taskElement.querySelector('.task-cancel-btn');
+
+ progressSpeed.textContent = '';
+ taskStatus.textContent = error;
+ taskStatus.className = 'task-status error';
+ cancelBtn.disabled = true;
+
+ // Remove after delay
+ setTimeout(() => {
+ taskElement.remove();
+ activeDownloads.delete(taskId);
+ checkEmptyState();
+ }, 5000);
+}
+
+// Cancel Download Task
+function cancelDownloadTask(taskId) {
+ const taskElement = activeDownloads.get(taskId);
+ if (!taskElement) return;
+
+ const progressSpeed = taskElement.querySelector('.progress-speed');
+ const taskStatus = taskElement.querySelector('.task-status');
+ const cancelBtn = taskElement.querySelector('.task-cancel-btn');
+
+ progressSpeed.textContent = '';
+ taskStatus.textContent = 'Cancelled';
+ taskStatus.className = 'task-status cancelled';
+ cancelBtn.disabled = true;
+
+ // Remove after delay
+ setTimeout(() => {
+ taskElement.remove();
+ activeDownloads.delete(taskId);
+ checkEmptyState();
+ }, 2000);
+}
+
+// Cancel Task (called from UI)
+window.cancelTask = async function(taskId) {
+ try {
+ await CancelTask(taskId);
+ } catch (err) {
+ console.error('Failed to cancel task:', err);
+ }
+};
+
+// Check Empty State
+function checkEmptyState() {
+ if (activeDownloads.size === 0 && !downloadsContainer.querySelector('.empty-state')) {
+ downloadsContainer.innerHTML = 'No active downloads
';
+ }
+}
+
+// Reset UI
+function resetUI() {
+ currentVideoInfo = null;
+ titleDisplay.textContent = '';
+ titleDisplay.classList.add('hidden');
+ resolutionSelect.innerHTML = '';
+ resolutionSelect.disabled = true;
+ addBtn.disabled = true;
+ statusLabel.textContent = 'Ready';
+}
+
+// Switch Tab
+function switchTab(tabName) {
+ tabBtns.forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.tab === tabName);
+ });
+
+ tabContents.forEach(content => {
+ content.classList.toggle('active', content.id === tabName);
+ });
+
+ // Load history when switching to history tab
+ if (tabName === 'history') {
+ loadHistory();
+ }
+}
+
+// Toggle Theme
+function toggleTheme() {
+ isDark = !isDark;
+ document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
+ themeIcon.src = isDark ? lightIcon : darkIcon;
+}
+
+// Update Output Directory Display
+async function updateOutputDir() {
+ try {
+ const dir = await GetOutputDir();
+ if (dir) {
+ outputDirDisplay.textContent = `Download Location: ${dir}`;
+ } else {
+ outputDirDisplay.textContent = 'Download location not set';
+ }
+ } catch (err) {
+ console.error('Failed to get output dir:', err);
+ outputDirDisplay.textContent = 'Download location not set';
+ }
+}
+
+// Extract Resolution
+function extractResolution(resolution) {
+ const match = resolution.match(/^(\d+)p/);
+ if (match) return match[1];
+ if (resolution.startsWith('best')) return 'best';
+ return '';
+}
+
+// Escape HTML
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
diff --git a/Predator/frontend/src/style.css b/Predator/frontend/src/style.css
new file mode 100644
index 0000000..3e3c6ea
--- /dev/null
+++ b/Predator/frontend/src/style.css
@@ -0,0 +1,209 @@
+/* Predator UI - Cyber-Minimalist Design System */
+/* Fresh Color Scheme: Electric Dark Theme */
+
+:root {
+ /* Core Palette - Electric Dark */
+ --bg-primary: #0d0d0d;
+ --bg-secondary: #141414;
+ --bg-tertiary: #1a1a1a;
+ --bg-card: #111111;
+ --bg-input: #0a0a0a;
+
+ /* Text Colors */
+ --text-primary: #ffffff;
+ --text-secondary: #a0a0a0;
+ --text-muted: #666666;
+ --text-disabled: #444444;
+
+ /* Accent Colors - Electric Cyan */
+ --accent-primary: #00d4ff;
+ --accent-secondary: #00a8cc;
+ --accent-tertiary: #0088a8;
+ --accent-gradient: linear-gradient(135deg, #00d4ff 0%, #00a8cc 100%);
+ --accent-glow: rgba(0, 212, 255, 0.4);
+
+ /* Status Colors */
+ --success: #00ff88;
+ --error: #ff3366;
+ --warning: #ffaa00;
+
+ /* Borders & Shadows */
+ --border-color: #2a2a2a;
+ --border-accent: #00d4ff;
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.5);
+ --shadow-md: 0 4px 20px rgba(0, 0, 0, 0.6);
+ --shadow-glow: 0 0 30px rgba(0, 212, 255, 0.15);
+
+ /* Spacing Scale */
+ --space-xs: 4px;
+ --space-sm: 8px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+
+ /* Border Radius */
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 16px;
+ --radius-full: 9999px;
+}
+
+[data-theme="light"] {
+ /* Light Theme - Clean Minimal */
+ --bg-primary: #fafafa;
+ --bg-secondary: #ffffff;
+ --bg-tertiary: #f5f5f5;
+ --bg-card: #ffffff;
+ --bg-input: #f0f0f0;
+
+ --text-primary: #0a0a0a;
+ --text-secondary: #666666;
+ --text-muted: #999999;
+ --text-disabled: #cccccc;
+
+ --accent-primary: #0088cc;
+ --accent-secondary: #0066aa;
+ --accent-tertiary: #005588;
+ --accent-gradient: linear-gradient(135deg, #0088cc 0%, #0066aa 100%);
+ --accent-glow: rgba(0, 136, 204, 0.3);
+
+ --success: #00cc66;
+ --error: #ff0066;
+ --warning: #ff9900;
+
+ --border-color: #e0e0e0;
+ --border-accent: #0066ff;
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
+ --shadow-md: 0 4px 20px rgba(0, 0, 0, 0.12);
+ --shadow-glow: 0 0 30px rgba(0, 102, 255, 0.2);
+}
+
+/* Base Reset */
+*, *::before, *::after {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 15px;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ min-height: 100vh;
+ overflow-x: hidden;
+}
+
+/* App Container */
+#app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Typography Scale */
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 700;
+ line-height: 1.2;
+ letter-spacing: -0.02em;
+}
+
+/* Scrollbar - Minimal Dark */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-color);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-muted);
+}
+
+/* Selection */
+::selection {
+ background: var(--accent-primary);
+ color: var(--bg-primary);
+}
+
+/* Utility Classes */
+.hidden { display: none !important; }
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* Animation Keyframes */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes slideInRight {
+ from { opacity: 0; transform: translateX(20px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.6; }
+}
+
+@keyframes glow {
+ 0%, 100% { box-shadow: 0 0 20px var(--accent-glow); }
+ 50% { box-shadow: 0 0 40px var(--accent-glow), 0 0 60px var(--accent-glow); }
+}
+
+@keyframes shimmer {
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Font Import */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Inter Regular'), local('Inter-Regular'),
+ url('assets/fonts/nunito-v16-latin-regular.woff2') format('woff2');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ src: local('Inter SemiBold'), local('Inter-SemiBold'),
+ url('assets/fonts/nunito-v16-latin-regular.woff2') format('woff2');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Inter Bold'), local('Inter-Bold'),
+ url('assets/fonts/nunito-v16-latin-regular.woff2') format('woff2');
+}
diff --git a/Predator/frontend/wailsjs/go/main/App.d.ts b/Predator/frontend/wailsjs/go/main/App.d.ts
new file mode 100644
index 0000000..1550ef8
--- /dev/null
+++ b/Predator/frontend/wailsjs/go/main/App.d.ts
@@ -0,0 +1,27 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+import {main} from '../models';
+
+export function AddToQueue(arg1:main.DownloadTask):Promise;
+
+export function CancelTask(arg1:string):Promise;
+
+export function CheckAndInstallDeps():Promise;
+
+export function ClearHistory():Promise;
+
+export function FetchVideoInfo(arg1:string):Promise;
+
+export function GetDownloadHistory():Promise>;
+
+export function GetOutputDir():Promise;
+
+export function IsValidYouTubeURL(arg1:string):Promise;
+
+export function OpenFolder(arg1:string):Promise;
+
+export function SaveToHistory(arg1:main.DownloadHistory):Promise;
+
+export function SelectOutputDir():Promise;
+
+export function UpdateYtDlp():Promise;
diff --git a/Predator/frontend/wailsjs/go/main/App.js b/Predator/frontend/wailsjs/go/main/App.js
new file mode 100644
index 0000000..92e5b14
--- /dev/null
+++ b/Predator/frontend/wailsjs/go/main/App.js
@@ -0,0 +1,51 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function AddToQueue(arg1) {
+ return window['go']['main']['App']['AddToQueue'](arg1);
+}
+
+export function CancelTask(arg1) {
+ return window['go']['main']['App']['CancelTask'](arg1);
+}
+
+export function CheckAndInstallDeps() {
+ return window['go']['main']['App']['CheckAndInstallDeps']();
+}
+
+export function ClearHistory() {
+ return window['go']['main']['App']['ClearHistory']();
+}
+
+export function FetchVideoInfo(arg1) {
+ return window['go']['main']['App']['FetchVideoInfo'](arg1);
+}
+
+export function GetDownloadHistory() {
+ return window['go']['main']['App']['GetDownloadHistory']();
+}
+
+export function GetOutputDir() {
+ return window['go']['main']['App']['GetOutputDir']();
+}
+
+export function IsValidYouTubeURL(arg1) {
+ return window['go']['main']['App']['IsValidYouTubeURL'](arg1);
+}
+
+export function OpenFolder(arg1) {
+ return window['go']['main']['App']['OpenFolder'](arg1);
+}
+
+export function SaveToHistory(arg1) {
+ return window['go']['main']['App']['SaveToHistory'](arg1);
+}
+
+export function SelectOutputDir() {
+ return window['go']['main']['App']['SelectOutputDir']();
+}
+
+export function UpdateYtDlp() {
+ return window['go']['main']['App']['UpdateYtDlp']();
+}
diff --git a/Predator/frontend/wailsjs/go/models.ts b/Predator/frontend/wailsjs/go/models.ts
new file mode 100644
index 0000000..b6abb57
--- /dev/null
+++ b/Predator/frontend/wailsjs/go/models.ts
@@ -0,0 +1,96 @@
+export namespace main {
+
+ export class DownloadHistory {
+ id: string;
+ url: string;
+ title: string;
+ type: string;
+ resolution: string;
+ audioFormat: string;
+ filePath: string;
+ fileSize: number;
+ // Go type: time
+ downloadedAt: any;
+ status: string;
+
+ static createFrom(source: any = {}) {
+ return new DownloadHistory(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.id = source["id"];
+ this.url = source["url"];
+ this.title = source["title"];
+ this.type = source["type"];
+ this.resolution = source["resolution"];
+ this.audioFormat = source["audioFormat"];
+ this.filePath = source["filePath"];
+ this.fileSize = source["fileSize"];
+ this.downloadedAt = this.convertValues(source["downloadedAt"], null);
+ this.status = source["status"];
+ }
+
+ convertValues(a: any, classs: any, asMap: boolean = false): any {
+ if (!a) {
+ return a;
+ }
+ if (a.slice && a.map) {
+ return (a as any[]).map(elem => this.convertValues(elem, classs));
+ } else if ("object" === typeof a) {
+ if (asMap) {
+ for (const key of Object.keys(a)) {
+ a[key] = new classs(a[key]);
+ }
+ return a;
+ }
+ return new classs(a);
+ }
+ return a;
+ }
+ }
+ export class DownloadTask {
+ url: string;
+ title: string;
+ type: string;
+ resolution: string;
+ cleanRes: string;
+ audioFormat: string;
+ audioQuality: string;
+ videoCodec: string;
+
+ static createFrom(source: any = {}) {
+ return new DownloadTask(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.url = source["url"];
+ this.title = source["title"];
+ this.type = source["type"];
+ this.resolution = source["resolution"];
+ this.cleanRes = source["cleanRes"];
+ this.audioFormat = source["audioFormat"];
+ this.audioQuality = source["audioQuality"];
+ this.videoCodec = source["videoCodec"];
+ }
+ }
+ export class VideoInfo {
+ title: string;
+ duration: number;
+ resolutions: string[];
+
+ static createFrom(source: any = {}) {
+ return new VideoInfo(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.title = source["title"];
+ this.duration = source["duration"];
+ this.resolutions = source["resolutions"];
+ }
+ }
+
+}
+
diff --git a/Predator/frontend/wailsjs/runtime/package.json b/Predator/frontend/wailsjs/runtime/package.json
new file mode 100644
index 0000000..1e7c8a5
--- /dev/null
+++ b/Predator/frontend/wailsjs/runtime/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@wailsapp/runtime",
+ "version": "2.0.0",
+ "description": "Wails Javascript runtime library",
+ "main": "runtime.js",
+ "types": "runtime.d.ts",
+ "scripts": {
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/wailsapp/wails.git"
+ },
+ "keywords": [
+ "Wails",
+ "Javascript",
+ "Go"
+ ],
+ "author": "Lea Anthony ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wailsapp/wails/issues"
+ },
+ "homepage": "https://github.com/wailsapp/wails#readme"
+}
diff --git a/Predator/frontend/wailsjs/runtime/runtime.d.ts b/Predator/frontend/wailsjs/runtime/runtime.d.ts
new file mode 100644
index 0000000..4445dac
--- /dev/null
+++ b/Predator/frontend/wailsjs/runtime/runtime.d.ts
@@ -0,0 +1,249 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Size {
+ w: number;
+ h: number;
+}
+
+export interface Screen {
+ isCurrent: boolean;
+ isPrimary: boolean;
+ width : number
+ height : number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+ buildType: string;
+ platform: string;
+ arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
+// Returns the state of the window, i.e. whether the window is in full screen mode or not.
+export function WindowIsFullscreen(): Promise;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
+// Returns the state of the window, i.e. whether the window is maximised or not.
+export function WindowIsMaximised(): Promise;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
+// Returns the state of the window, i.e. whether the window is minimised or not.
+export function WindowIsMinimised(): Promise;
+
+// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
+// Returns the state of the window, i.e. whether the window is normal or not.
+export function WindowIsNormal(): Promise;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
+
+// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
+// Returns the current text stored on clipboard
+export function ClipboardGetText(): Promise;
+
+// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
+// Sets a text on the clipboard
+export function ClipboardSetText(text: string): Promise;
+
+// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
+// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
+
+// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
+// OnFileDropOff removes the drag and drop listeners and handlers.
+export function OnFileDropOff() :void
+
+// Check if the file path resolver is available
+export function CanResolveFilePaths(): boolean;
+
+// Resolves file paths for an array of files
+export function ResolveFilePaths(files: File[]): void
\ No newline at end of file
diff --git a/Predator/frontend/wailsjs/runtime/runtime.js b/Predator/frontend/wailsjs/runtime/runtime.js
new file mode 100644
index 0000000..7cb89d7
--- /dev/null
+++ b/Predator/frontend/wailsjs/runtime/runtime.js
@@ -0,0 +1,242 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+ window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+ window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+ window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+ window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+ window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+ window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+ window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName, ...additionalEventNames) {
+ return window.runtime.EventsOff(eventName, ...additionalEventNames);
+}
+
+export function EventsOffAll() {
+ return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+ let args = [eventName].slice.call(arguments);
+ return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+ window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+ window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+ window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+ window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+ window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+ window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+ window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+ window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+ window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+ window.runtime.WindowUnfullscreen();
+}
+
+export function WindowIsFullscreen() {
+ return window.runtime.WindowIsFullscreen();
+}
+
+export function WindowGetSize() {
+ return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+ window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+ window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+ window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+ window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+ return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+ window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+ window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+ window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+ window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+ window.runtime.WindowUnmaximise();
+}
+
+export function WindowIsMaximised() {
+ return window.runtime.WindowIsMaximised();
+}
+
+export function WindowMinimise() {
+ window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+ window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+ window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+ return window.runtime.ScreenGetAll();
+}
+
+export function WindowIsMinimised() {
+ return window.runtime.WindowIsMinimised();
+}
+
+export function WindowIsNormal() {
+ return window.runtime.WindowIsNormal();
+}
+
+export function BrowserOpenURL(url) {
+ window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+ return window.runtime.Environment();
+}
+
+export function Quit() {
+ window.runtime.Quit();
+}
+
+export function Hide() {
+ window.runtime.Hide();
+}
+
+export function Show() {
+ window.runtime.Show();
+}
+
+export function ClipboardGetText() {
+ return window.runtime.ClipboardGetText();
+}
+
+export function ClipboardSetText(text) {
+ return window.runtime.ClipboardSetText(text);
+}
+
+/**
+ * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ *
+ * @export
+ * @callback OnFileDropCallback
+ * @param {number} x - x coordinate of the drop
+ * @param {number} y - y coordinate of the drop
+ * @param {string[]} paths - A list of file paths.
+ */
+
+/**
+ * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+ *
+ * @export
+ * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
+ */
+export function OnFileDrop(callback, useDropTarget) {
+ return window.runtime.OnFileDrop(callback, useDropTarget);
+}
+
+/**
+ * OnFileDropOff removes the drag and drop listeners and handlers.
+ */
+export function OnFileDropOff() {
+ return window.runtime.OnFileDropOff();
+}
+
+export function CanResolveFilePaths() {
+ return window.runtime.CanResolveFilePaths();
+}
+
+export function ResolveFilePaths(files) {
+ return window.runtime.ResolveFilePaths(files);
+}
\ No newline at end of file
diff --git a/Predator/go.mod b/Predator/go.mod
new file mode 100644
index 0000000..9e025b5
--- /dev/null
+++ b/Predator/go.mod
@@ -0,0 +1,43 @@
+module Predator
+
+go 1.25.0
+
+require (
+ github.com/lrstanley/go-ytdlp v1.3.0
+ github.com/wailsapp/wails/v2 v2.11.0
+)
+
+require (
+ github.com/ProtonMail/go-crypto v1.3.0 // indirect
+ github.com/bep/debounce v1.2.1 // indirect
+ github.com/cloudflare/circl v1.6.3 // indirect
+ github.com/go-ole/go-ole v1.3.0 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/websocket v1.5.3 // indirect
+ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
+ github.com/labstack/echo/v4 v4.13.3 // indirect
+ github.com/labstack/gommon v0.4.2 // indirect
+ github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
+ github.com/leaanthony/gosod v1.0.4 // indirect
+ github.com/leaanthony/slicer v1.6.0 // indirect
+ github.com/leaanthony/u v1.1.1 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/samber/lo v1.49.1 // indirect
+ github.com/tkrajina/go-reflector v0.5.8 // indirect
+ github.com/ulikunitz/xz v0.5.15 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasttemplate v1.2.2 // indirect
+ github.com/wailsapp/go-webview2 v1.0.22 // indirect
+ github.com/wailsapp/mimetype v1.4.1 // indirect
+ golang.org/x/crypto v0.48.0 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/text v0.34.0 // indirect
+)
+
+// replace github.com/wailsapp/wails/v2 v2.11.0 => C:\Users\LENOVO\go\pkg\mod
diff --git a/Predator/go.sum b/Predator/go.sum
new file mode 100644
index 0000000..61b7913
--- /dev/null
+++ b/Predator/go.sum
@@ -0,0 +1,89 @@
+github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
+github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
+github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
+github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
+github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
+github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
+github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
+github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
+github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/lrstanley/go-ytdlp v1.3.0 h1:SZcyuMklsy+qj8Xy05uthydW4C0EYCEyOMl6bJrEr3g=
+github.com/lrstanley/go-ytdlp v1.3.0/go.mod h1:VgjnTrvkTf+23JuySjyPq1iQ8ijSovBtTPpXH5XrLtI=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
+github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
+github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
+github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
+github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
+github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
+github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
+github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
+golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/Predator/main.go b/Predator/main.go
new file mode 100644
index 0000000..cec2bff
--- /dev/null
+++ b/Predator/main.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "embed"
+ "os"
+
+ "github.com/wailsapp/wails/v2"
+
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+//go:embed all:frontend/dist
+var assets embed.FS
+
+func main() {
+ // Create an instance of the app structure
+ app := NewApp()
+
+ // Create application with options
+ err := wails.Run(&options.App{
+ Title: "Predator",
+ Width: 520,
+ Height: 600,
+ AssetServer: &assetserver.Options{
+ Assets: assets,
+ },
+ BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+ OnStartup: app.startup,
+ Bind: []interface{}{
+ app,
+ },
+ })
+
+ if err != nil {
+ println("Error:", err.Error())
+ os.Exit(1)
+ }
+}
diff --git a/Predator/wails.json b/Predator/wails.json
new file mode 100644
index 0000000..c3e0db7
--- /dev/null
+++ b/Predator/wails.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://wails.io/schemas/config.v2.json",
+ "name": "Predator",
+ "outputfilename": "Predator",
+ "frontend:install": "npm install",
+ "frontend:build": "npm run build",
+ "frontend:dev:watcher": "npm run dev",
+ "frontend:dev:serverUrl": "auto",
+ "author": {
+ "name": "Aswanidev",
+ "email": "workwithsakuta@protonmail.com"
+ }
+}
diff --git a/README.md b/README.md
index cc071ab..447fbd4 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+<<<<<<< HEAD
# Predator
@@ -58,6 +59,120 @@ That's it. The app shows progress in real-time and you can cancel anytime.
- Videos get merged into a single MP4 file with H.264 codec for compatibility.
- Audio gets extracted in whatever format you chose.
- Download history is saved locally so you can find your files later.
+=======
+# Predator - YouTube Downloader
+
+
+
+A modern, cross-platform desktop application for downloading videos and audio from YouTube .
+It is built using Go and the Fyne GUI framework, with yt-dlp as the download engine.
+
+The project focuses on simplicity, transparency, and responsive user experience while keeping the implementation clean and maintainable.
+
+## Features
+
+- **Video Download**: Download YouTube videos in various resolutions (144p to 2160p)
+- **Audio Extraction**: Extract audio from videos in multiple formats (MP3, M4A, Opus, WAV)
+- **Real-time Metadata**: Fetches video information dynamically, showing available resolutions with file sizes
+- **Progress Tracking**: Live download progress ETA information
+- **Cancellable Downloads**: Stop downloads at any time
+- **Custom Output Directory**: Select and manage your download location
+- **Cross-platform**: Works on Windows, macOS, and Linux
+
+## Requirements
+
+- **Go**: 1.24.0 or higher
+- **FFmpeg**: Required for audio extraction and video merging (automatically handled by yt-dlp)
+- **yt-dlp**: Automatically installed on first run
+
+## Installation
+
+### Clone the Repository
+```bash
+git clone https://github.com/Aswanidev-vs/Predator.git
+cd Predator
+```
+
+### Install Dependencies
+```bash
+go mod download
+```
+
+### Build and Run
+```bash
+go run main.go
+```
+
+### Build for Distribution
+The project includes cross-platform build configuration in the `fyne-cross` directory:
+
+```bash
+# For Windows
+go install fyne.io/tools/cmd/fyne-cross@latest
+fyne-cross windows
+
+# For macOS
+fyne-cross darwin
+
+# For Linux
+fyne-cross linux
+```
+
+## Usage
+
+1. **Launch the Application**: Run the executable or `go run main.go`
+2. **Set Download Location**: Click "Change Download Location" to select where videos will be saved
+3. **Paste YouTube URL**: Paste a YouTube video link into the URL field
+4. **Select Download Type**:
+ - **Video**: Choose a resolution and video will be downloaded with best audio merged
+ - **Audio**: Choose an audio format (MP3, M4A, Opus, WAV)
+5. **Click Download**: Start the download process
+6. **Monitor Progress**: Watch real-time progress, speed, and ETA
+7. **Cancel if Needed**: Click Cancel button to stop an ongoing download
+
+## Architecture
+
+### Main Components
+
+- **UI Layer**: Built with [Fyne](https://fyne.io/) - a cross-platform GUI framework
+- **Download Engine**: Powered by [go-ytdlp](https://github.com/lrstanley/go-ytdlp) - a Go wrapper for yt-dlp
+- **Async Operations**: Uses goroutines for non-blocking UI interactions
+- **Progress Tracking**: Real-time progress callbacks with ETA calculations
+
+### Key Functions
+
+- `main()`: Initializes the UI and event handlers
+- `fetchVideoInfo()`: Dynamically fetches video metadata and available resolutions
+- `formatBytes()`: Converts byte sizes to human-readable format
+- `formatETA()`: Converts duration to HH:MM:SS format
+
+## Project Structure
+
+```
+Predator/
+├── main.go # Main application code
+├── go.mod # Go module definition
+├── go.sum # Go module checksums
+├── logov4.png # Application icon
+├── asset/ # Icon and logo assets
+├── fyne-cross/ # Cross-platform build configuration
+├── sample.txt # Code sample reference
+└── samplev*.txt # Additional code samples
+```
+
+## Dependencies
+
+- **fyne.io/fyne/v2**: Cross-platform GUI framework
+- **github.com/lrstanley/go-ytdlp**: Go wrapper for yt-dlp
+
+
+## Performance
+
+- **Lazy Metadata Loading**: Video info is only fetched when a valid URL is entered
+- **Debounced Input**: 600ms debounce on URL input to prevent excessive API calls
+- **Atomic Operations**: Thread-safe state management for concurrent operations
+- **Streaming Download**: Supports live progress updates during download
+>>>>>>> 5c9da659bc26ab9faf682fb549dc7e0b599a7169
## Disclaimer
@@ -92,3 +207,37 @@ This application is provided "as-is" without warranty of any kind, express or im
The authors assume no responsibility for any illegal activities or misuse of this tool. By using Predator, you accept all risks and responsibilities associated with your downloads.
+<<<<<<< HEAD
+=======
+## License
+
+MIT License
+
+Copyright (c) 2025 Aswanidev-vs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Contributing
+
+Contributions are welcome! Feel free to submit issues and pull requests.
+
+
+
+**Note**: This application requires an active internet connection to download from YouTube and relies on the yt-dlp project to function.
+>>>>>>> 5c9da659bc26ab9faf682fb549dc7e0b599a7169
diff --git a/dark-mode.png b/dark-mode.png
new file mode 100644
index 0000000..a41726b
Binary files /dev/null and b/dark-mode.png differ
diff --git a/go.mod b/go.mod
index 9e025b5..5f7a8ef 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,13 @@
+<<<<<<< HEAD
module Predator
+=======
+module predator
+>>>>>>> 5c9da659bc26ab9faf682fb549dc7e0b599a7169
go 1.25.0
require (
+<<<<<<< HEAD
github.com/lrstanley/go-ytdlp v1.3.0
github.com/wailsapp/wails/v2 v2.11.0
)
@@ -41,3 +46,50 @@ require (
)
// replace github.com/wailsapp/wails/v2 v2.11.0 => C:\Users\LENOVO\go\pkg\mod
+=======
+ fyne.io/fyne/v2 v2.7.2
+ github.com/lrstanley/go-ytdlp v1.3.0
+)
+
+require (
+ fyne.io/systray v1.12.0 // indirect
+ github.com/BurntSushi/toml v1.5.0 // indirect
+ github.com/ProtonMail/go-crypto v1.3.0 // indirect
+ github.com/cloudflare/circl v1.6.3 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/fredbi/uri v1.1.1 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/fyne-io/gl-js v0.2.0 // indirect
+ github.com/fyne-io/glfw-js v0.3.0 // indirect
+ github.com/fyne-io/image v0.1.1 // indirect
+ github.com/fyne-io/oksvg v0.2.0 // indirect
+ github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
+ github.com/go-text/render v0.2.0 // indirect
+ github.com/go-text/typesetting v0.2.1 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/hack-pad/go-indexeddb v0.3.2 // indirect
+ github.com/hack-pad/safejs v0.1.0 // indirect
+ github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
+ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
+ github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/rymdport/portal v0.4.2 // indirect
+ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
+ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
+ github.com/stretchr/testify v1.11.1 // indirect
+ github.com/ulikunitz/xz v0.5.15 // indirect
+ github.com/yuin/goldmark v1.7.8 // indirect
+ golang.org/x/crypto v0.48.0 // indirect
+ github.com/ulikunitz/xz v0.5.14 // indirect
+ github.com/yuin/goldmark v1.7.8 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
+ golang.org/x/image v0.24.0 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/text v0.34.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+>>>>>>> 5c9da659bc26ab9faf682fb549dc7e0b599a7169
diff --git a/go.sum b/go.sum
index 61b7913..e8907d5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+<<<<<<< HEAD
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
@@ -85,5 +86,101 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+=======
+fyne.io/fyne/v2 v2.7.2 h1:XiNpWkn0PzX43ZCjbb0QYGg1RCxVbugwfVgikWZBCMw=
+fyne.io/fyne/v2 v2.7.2/go.mod h1:PXbqY3mQmJV3J1NRUR2VbVgUUx3vgvhuFJxyjRK/4Ug=
+fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
+fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
+github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
+github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
+github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
+github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
+github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
+github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
+github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
+github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
+github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
+github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
+github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
+github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
+github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
+github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
+github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
+github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
+github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
+github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
+github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
+github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
+github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
+github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
+github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
+github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
+github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
+github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lrstanley/go-ytdlp v1.3.0 h1:SZcyuMklsy+qj8Xy05uthydW4C0EYCEyOMl6bJrEr3g=
+github.com/lrstanley/go-ytdlp v1.3.0/go.mod h1:VgjnTrvkTf+23JuySjyPq1iQ8ijSovBtTPpXH5XrLtI=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
+github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
+github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
+github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
+github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
+github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
+github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
+golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
+golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+>>>>>>> 5c9da659bc26ab9faf682fb549dc7e0b599a7169
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
index cec2bff..8a9d1e8 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
package main
import (
+<<<<<<< HEAD
"embed"
"os"
@@ -37,3 +38,867 @@ func main() {
os.Exit(1)
}
}
+=======
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/app"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/dialog"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+ "github.com/lrstanley/go-ytdlp"
+)
+
+//go:embed summer.png
+var summerBytes []byte
+
+//go:embed dark-mode.png
+var darkBytes []byte
+
+func checkAndInstallDeps(w fyne.Window) error {
+
+ // Quick check: do we have system ffmpeg and ffprobe in PATH?
+ if _, err := exec.LookPath("ffmpeg"); err == nil {
+ if _, err := exec.LookPath("ffprobe"); err == nil {
+ // System ffmpeg/ffprobe found → ensure yt-dlp is cached (fast if already there)
+ ytdlp.MustInstall(context.Background(), nil)
+ return nil
+ }
+ }
+ // First time without system ffmpeg → ask user
+ done := make(chan error, 1)
+
+ confirm := dialog.NewConfirm(
+ "Install Required Tools",
+ "Predator requires ffmpeg and ffprobe for merging video+audio and extracting audio.\n\n"+
+ "They are not detected on your system.\n\n"+
+ "We can automatically download open-source bundled versions (yt-dlp + ffmpeg + ffprobe) and cache them locally.\n\n"+
+ "Do you want to continue? (Recommended)",
+ func(ok bool) {
+ if !ok {
+ done <- fmt.Errorf("user declined bundled dependency installation")
+ return
+ }
+
+ // Show progress
+ bar := widget.NewProgressBarInfinite()
+ label := widget.NewLabel("Downloading yt-dlp, ffmpeg & ffprobe…\nThis may take a moment on first run.")
+ content := container.NewVBox(label, bar)
+
+ progressDialog := dialog.NewCustomWithoutButtons("Installing Dependencies", content, w)
+ progressDialog.Show()
+ go func() {
+ defer progressDialog.Hide()
+
+ defer func() {
+ if r := recover(); r != nil {
+ done <- fmt.Errorf("installation panicked: %v", r)
+ }
+ }()
+
+ // Fixed: handle two return values
+ _, err := ytdlp.Install(context.Background(), nil)
+ if err != nil {
+ done <- fmt.Errorf("failed to install dependencies: %w", err)
+ return
+ }
+
+ done <- nil
+ }()
+
+ },
+ w,
+ )
+
+ confirm.SetDismissText("No")
+ confirm.SetConfirmText("Yes, Install")
+ confirm.Show()
+
+ return <-done
+}
+
+const prefOutputDir = "output_dir"
+
+/* -------------------- Constants -------------------- */
+
+const (
+ maxConcurrentDownloads = 3
+ progressUpdateInterval = 200 * time.Millisecond
+ speedSmoothingAlpha = 0.2
+ taskQueueSize = 100
+ cleanupDelay = 2 * time.Second
+ fetchTimeout = 30 * time.Second
+ fetchDebounceDelay = 600 * time.Millisecond
+ maxRetries = 3
+ retryBaseDelay = 2 * time.Second
+)
+
+/* -------------------- URL Validation -------------------- */
+
+var youtubeURLPatterns = []*regexp.Regexp{
+ regexp.MustCompile(`^(https?://)?(www\.)?(youtube\.com|youtu\.be)/.+`),
+ regexp.MustCompile(`^(https?://)?(www\.)?youtube\.com/watch\?v=[\w-]+`),
+ regexp.MustCompile(`^(https?://)?(www\.)?youtu\.be/[\w-]+`),
+ regexp.MustCompile(`^(https?://)?(www\.)?youtube\.com/shorts/[\w-]+`),
+ regexp.MustCompile(`^(https?://)?(www\.)?youtube\.com/live/[\w-]+`),
+}
+
+func isValidYouTubeURL(url string) bool {
+ for _, pattern := range youtubeURLPatterns {
+ if pattern.MatchString(url) {
+ return true
+ }
+ }
+ return false
+}
+
+/* -------------------- Helpers -------------------- */
+
+// updateYtDlp explicitly updates yt-dlp to the latest version
+func updateYtDlp() error {
+ log.Println("Updating yt-dlp to latest version...")
+ // Call Install which should update to latest version
+ // The second parameter can be used to specify options
+ _, err := ytdlp.Install(context.Background(), nil)
+ if err != nil {
+ // Try with MustInstall as fallback
+ log.Println("yt-dlp.Install failed, trying MustInstall:", err)
+ ytdlp.MustInstall(context.Background(), nil)
+ return err
+ }
+ log.Println("yt-dlp updated successfully")
+ return nil
+}
+
+func formatBytes(b float64) string {
+ const unit = 1024
+ if b < unit {
+ return fmt.Sprintf("%.0f B", b)
+ }
+ div, exp := float64(unit), 0
+ for n := b / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+ return fmt.Sprintf("%.1f %ciB", b/div, "KMGT"[exp])
+}
+
+func formatETA(d time.Duration) string {
+ s := int64(d.Seconds())
+ h := s / 3600
+ m := (s % 3600) / 60
+ sec := s % 60
+ if h > 0 {
+ return fmt.Sprintf("%02d:%02d:%02d", h, m, sec)
+ }
+ return fmt.Sprintf("%02d:%02d", m, sec)
+}
+
+func formatSpeed(speed float64) string {
+ return formatBytes(speed) + "/s"
+}
+
+func parseSizeString(sizeStr string) int64 {
+ if sizeStr == "" || sizeStr == "Unknown" {
+ return 0
+ }
+ // Remove ~ prefix if present
+ sizeStr = strings.TrimPrefix(sizeStr, "~")
+
+ // Parse format like "15.2 MiB" or "1.5 GiB"
+ re := regexp.MustCompile(`([\d.]+)\s*([KMGT]?)i?B`)
+ matches := re.FindStringSubmatch(sizeStr)
+ if len(matches) < 3 {
+ return 0
+ }
+
+ val, err := strconv.ParseFloat(matches[1], 64)
+ if err != nil {
+ return 0
+ }
+
+ multiplier := float64(1024)
+ switch matches[2] {
+ case "K":
+ multiplier = 1024
+ case "M":
+ multiplier = 1024 * 1024
+ case "G":
+ multiplier = 1024 * 1024 * 1024
+ case "T":
+ multiplier = 1024 * 1024 * 1024 * 1024
+ default:
+ multiplier = 1
+ }
+
+ return int64(val * multiplier)
+}
+
+func truncateError(err string, maxLen int) string {
+ if len(err) <= maxLen {
+ return err
+ }
+ return err[:maxLen] + "..."
+}
+
+/* -------------------- Queue System -------------------- */
+
+type DownloadTask struct {
+ URL string
+ Title string
+ Type string
+ Resolution string // Display text like "720p (15.2 MiB)"
+ CleanRes string // Clean resolution like "720" or "best"
+ AudioFormat string
+ AudioQuality string
+ VideoCodec string // Preferred video codec
+}
+
+var (
+ taskQueue = make(chan DownloadTask, taskQueueSize)
+ sem chan struct{}
+ semOnce sync.Once
+)
+
+func initSemaphore() {
+ semOnce.Do(func() {
+ sem = make(chan struct{}, maxConcurrentDownloads)
+ for i := 0; i < maxConcurrentDownloads; i++ {
+ sem <- struct{}{}
+ }
+ })
+}
+
+func extractResolution(resolution string) string {
+ // Extract just the resolution number from strings like "720p (15.2 MiB)"
+ re := regexp.MustCompile(`^(\d+)p`)
+ matches := re.FindStringSubmatch(resolution)
+ if len(matches) > 1 {
+ return matches[1]
+ }
+ if strings.HasPrefix(resolution, "best") {
+ return "best"
+ }
+ return ""
+}
+
+// buildVideoFormatString creates format string with Code 2's superior codec handling
+func buildVideoFormatString(cleanRes string, preferH264 bool) string {
+ if cleanRes == "best" {
+ // For best quality with comprehensive codec fallbacks
+ // H.264 + AAC (best compatibility), then VP9, then AV1
+ return "bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/" + // H.264 + AAC (best compatibility)
+ "bestvideo[vcodec^=avc1]+bestaudio/" + // H.264 + any audio
+ "bestvideo[vcodec^=vp9]+bestaudio[acodec^=mp4a]/" + // VP9 + AAC (reencode audio)
+ "bestvideo[vcodec^=vp9]+bestaudio/" + // VP9 + any audio
+ "bestvideo[vcodec^=av01]+bestaudio[acodec^=mp4a]/" + // AV1 + AAC
+ "bestvideo[vcodec^=av01]+bestaudio/" + // AV1 + any audio
+ "bestvideo+bestaudio[acodec^=mp4a]/" + // Any video + AAC
+ "bestvideo+bestaudio/best" // Ultimate fallback
+ }
+
+ // For high resolutions (1440p, 2160p), H.264 is rarely available
+ // Prioritize VP9 and AV1 which are commonly used for high-res YouTube videos
+ resNum, _ := strconv.Atoi(cleanRes)
+ if resNum >= 1440 {
+ // High resolution: prioritize VP9/AV1 since H.264 usually not available
+ return fmt.Sprintf(
+ "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=vp9][height<=%s]+bestaudio/"+
+ "bestvideo[vcodec^=av01][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=av01][height<=%s]+bestaudio/"+
+ "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=avc1][height<=%s]+bestaudio/"+
+ "bestvideo[height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[height<=%s]+bestaudio/"+
+ "best[height<=%s]",
+ cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes,
+ )
+ }
+
+ // Standard resolution (1080p and below) with comprehensive codec fallback chains
+ // 1. H.264 + AAC (most reliable, always works in MP4)
+ // 2. H.264 + any audio (with audio conversion)
+ // 3. VP9 + AAC (reencode audio to AAC)
+ // 4. VP9 + Opus (may need remux)
+ // 5. Any video + AAC audio
+ // 6. Final fallback to best available
+ return fmt.Sprintf(
+ "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=avc1][height<=%s]+bestaudio/"+
+ "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[vcodec^=vp9][height<=%s]+bestaudio/"+
+ "bestvideo[height<=%s]+bestaudio[acodec^=mp4a]/"+
+ "bestvideo[height<=%s]+bestaudio/"+
+ "best[height<=%s]",
+ cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes,
+ )
+}
+
+func buildAudioFormatString(format string) string {
+ // Audio codec priority: aac (m4a) > opus > mp3 > best
+ switch format {
+ case "m4a":
+ return "bestaudio[ext=m4a]/bestaudio[acodec^=mp4a]/bestaudio/best"
+ case "opus":
+ return "bestaudio[ext=opus]/bestaudio[acodec^=opus]/bestaudio/best"
+ case "mp3":
+ return "bestaudio[ext=mp3]/bestaudio/best"
+ case "wav":
+ return "bestaudio[ext=wav]/bestaudio/best"
+ default:
+ return "bestaudio/best"
+ }
+}
+
+func worker(downloadsContainer *fyne.Container, outputDir *string) {
+ initSemaphore()
+
+ for task := range taskQueue {
+ <-sem
+
+ titleLbl := widget.NewLabel(task.Title)
+ titleLbl.Wrapping = fyne.TextWrapWord
+ titleLbl.TextStyle = fyne.TextStyle{Bold: true}
+
+ progBar := widget.NewProgressBar()
+ statLbl := widget.NewLabel("Starting...")
+ spdLbl := widget.NewLabel("")
+ cancelBtn := widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), nil)
+
+ taskCont := container.NewVBox(
+ widget.NewSeparator(),
+ titleLbl,
+ container.NewBorder(nil, nil, nil, cancelBtn, progBar),
+ container.NewHBox(statLbl, spdLbl),
+ )
+
+ fyne.Do(func() {
+ downloadsContainer.Add(taskCont)
+ downloadsContainer.Refresh()
+ })
+
+ ctx, cancel := context.WithCancel(context.Background())
+ cancelBtn.OnTapped = func() {
+ cancel()
+ cancelBtn.Disable()
+ statLbl.SetText("Cancelling...")
+ }
+
+ go func(t DownloadTask, c *fyne.Container, ctx context.Context, cancel context.CancelFunc) {
+ defer func() {
+ sem <- struct{}{}
+ time.Sleep(cleanupDelay)
+ fyne.Do(func() {
+ downloadsContainer.Remove(c)
+ downloadsContainer.Refresh()
+ })
+ }()
+
+ var lastDownloaded int
+ var lastTime = time.Now()
+ var smoothedSpeed float64
+ var mu sync.Mutex // Protect progress variables
+
+ updateProgress := func(p ytdlp.ProgressUpdate) {
+ if p.Status != "downloading" {
+ return
+ }
+
+ mu.Lock()
+ now := time.Now()
+ elapsed := now.Sub(lastTime).Seconds()
+ var speed float64
+ if elapsed > 0 {
+ speed = float64(p.DownloadedBytes-lastDownloaded) / elapsed
+ }
+ if smoothedSpeed == 0 {
+ smoothedSpeed = speed
+ } else {
+ smoothedSpeed = speedSmoothingAlpha*speed + (1-speedSmoothingAlpha)*smoothedSpeed
+ }
+ lastDownloaded = p.DownloadedBytes
+ lastTime = now
+ currentSpeed := smoothedSpeed
+ mu.Unlock()
+
+ fyne.Do(func() {
+ if p.Percent() >= 100 {
+ progBar.SetValue(1)
+ statLbl.SetText("Processing… (merging)")
+ spdLbl.SetText("")
+ return
+ }
+ progBar.SetValue(p.Percent() / 100)
+ statLbl.SetText(fmt.Sprintf("Downloading… %.1f%%", p.Percent()))
+ spdLbl.SetText(fmt.Sprintf("Speed: %s | ETA: %s", formatSpeed(currentSpeed), formatETA(p.ETA())))
+ })
+ }
+
+ var err error
+ outPath := *outputDir
+
+ // Update yt-dlp before starting download
+ updateYtDlp()
+
+ // Retry logic with exponential backoff
+ for attempt := 0; attempt < maxRetries; attempt++ {
+ if attempt > 0 {
+ fyne.Do(func() {
+ statLbl.SetText(fmt.Sprintf("Retrying... (attempt %d/%d)", attempt+1, maxRetries))
+ })
+ time.Sleep(time.Duration(attempt) * retryBaseDelay)
+ }
+
+ if ctx.Err() == context.Canceled {
+ break
+ }
+
+ if t.Type == "Video" {
+ format := buildVideoFormatString(t.CleanRes, true) // Prefer h264 for compatibility
+ dl := ytdlp.New().
+ Format(format).
+ MergeOutputFormat("mp4").
+ NoKeepVideo().
+ NoKeepFragments().
+ RemuxVideo("mp4").
+ // Convert audio to m4a (AAC) for guaranteed MP4 compatibility
+ AudioFormat("m4a").
+ AudioQuality("0"). // Best quality
+ // Postprocessor options to ensure successful merge
+ PostProcessorArgs("FFmpegMerger:-c:v copy -c:a aac -b:a 192k").
+ Output(filepath.Join(outPath, "%(title)s [%(id)s] (%(resolution)s).%(ext)s")).
+ ProgressFunc(progressUpdateInterval, updateProgress)
+
+ _, err = dl.Run(ctx, t.URL)
+ } else {
+ format := buildAudioFormatString(t.AudioFormat)
+ _, err = ytdlp.New().
+ ExtractAudio().
+ AudioFormat(t.AudioFormat).
+ AudioQuality("0"). // Best quality
+ Format(format).
+ Output(filepath.Join(outPath, "%(title)s [%(id)s].%(ext)s")).
+ ProgressFunc(progressUpdateInterval, updateProgress).
+ Run(ctx, t.URL)
+ }
+
+ if err == nil {
+ break // Success
+ }
+
+ // Check if error is retryable
+ if ctx.Err() == context.Canceled {
+ break
+ }
+ }
+
+ fyne.Do(func() {
+ cancelBtn.Disable()
+ if err != nil {
+ if ctx.Err() == context.Canceled {
+ statLbl.SetText("Cancelled")
+ } else {
+ // Check if it's a codec/merge related error
+ errStr := err.Error()
+ if strings.Contains(errStr, "codec") ||
+ strings.Contains(errStr, "merge") ||
+ strings.Contains(errStr, "ffmpeg") ||
+ strings.Contains(errStr, "postprocessor") {
+ statLbl.SetText("Failed: codec/merge error. Try lower resolution.")
+ } else {
+ statLbl.SetText("Failed: " + truncateError(err.Error(), 50))
+ }
+ }
+ progBar.SetValue(0)
+ } else {
+ progBar.SetValue(1)
+ statLbl.SetText("Completed ✓")
+ }
+ spdLbl.SetText("")
+ })
+ }(task, taskCont, ctx, cancel)
+ }
+}
+
+/* -------------------- Main -------------------- */
+
+func main() {
+
+ // ytdlp.MustInstall(context.Background(), nil)
+ a := app.NewWithID("Predator")
+ w := a.NewWindow("Predator")
+ w.Resize(fyne.NewSize(520, 480))
+ prefs := a.Preferences()
+ logo, err := os.ReadFile("logov4.png")
+ if err != nil {
+ log.Println("Error reading icon file ", err)
+ } else {
+ appIcon := fyne.NewStaticResource("logov4.png", logo)
+ w.SetIcon(appIcon)
+ }
+
+ // Load icons
+ lightIcon := fyne.NewStaticResource("summer.png", summerBytes)
+ darkIcon := fyne.NewStaticResource("dark-mode.png", darkBytes)
+
+ isDark := true
+
+ var themeBtn *widget.Button
+ themeBtn = widget.NewButtonWithIcon("", lightIcon, func() {
+ if isDark {
+ a.Settings().SetTheme(theme.LightTheme())
+ themeBtn.SetIcon(darkIcon) // show moon icon
+ } else {
+ a.Settings().SetTheme(theme.DarkTheme())
+ themeBtn.SetIcon(lightIcon) // show sun icon
+ }
+ isDark = !isDark
+ })
+
+ headerLabel := widget.NewLabel("Predator")
+ headerContainer := container.NewBorder(nil, nil, headerLabel, themeBtn)
+
+ /* -------------------- UI -------------------- */
+ go func() {
+ err := checkAndInstallDeps(w)
+ if err != nil {
+ fyne.Do(func() {
+ var msg string
+ if strings.Contains(err.Error(), "user declined") {
+ msg = "Dependency installation was declined.\n\nVideo downloads and audio extraction will not work properly."
+ } else {
+ msg = fmt.Sprintf("Failed to install required dependencies (yt-dlp/ffmpeg):\n%s\n\nSome features will be limited.", err)
+ }
+ dialog.ShowError(fmt.Errorf(msg), w)
+ })
+ }
+ }()
+
+ urlEntry := widget.NewEntry()
+ urlEntry.SetPlaceHolder("Paste YouTube URL here")
+
+ // Create clear button with icon
+ clearBtn := widget.NewButtonWithIcon("", theme.ContentClearIcon(), func() {
+ urlEntry.SetText("")
+ })
+
+ // Create a Border container: Entry in Center (expands), Button on Right
+ urlInputContainer := container.NewBorder(nil, nil, nil, clearBtn, urlEntry)
+
+ downloadType := widget.NewRadioGroup([]string{"Video", "Audio"}, nil)
+ downloadType.SetSelected("Video")
+ downloadType.Horizontal = true
+
+ resolutions := []string{"144p", "240p", "360p", "480p", "720p", "1080p", "1440p", "2160p", "best"}
+ resSelect := widget.NewSelect(nil, nil)
+ resSelect.Disable()
+
+ audioFormats := []string{"mp3", "m4a", "opus", "wav"}
+ audioSelect := widget.NewSelect(audioFormats, nil)
+ audioSelect.SetSelected("mp3")
+ audioSelect.Disable()
+
+ downloadType.OnChanged = func(s string) {
+ if s == "Video" {
+ resSelect.Enable()
+ audioSelect.Disable()
+ } else {
+ resSelect.Disable()
+ audioSelect.Enable()
+ }
+ }
+
+ addBtn := widget.NewButton("Add to Queue", nil)
+ addBtn.Disable()
+
+ var tabs *container.AppTabs
+
+ titleLabel := widget.NewLabel("")
+ titleLabel.Wrapping = fyne.TextWrapWord
+ statusLabel := widget.NewLabel("Ready")
+ /* -------------------- Output Dir -------------------- */
+
+ outputDir := prefs.String(prefOutputDir)
+ outputDirLabel := widget.NewLabel("")
+
+ updateOutputUI := func() {
+ if outputDir == "" {
+ outputDirLabel.SetText("Download location not set")
+ } else {
+ outputDirLabel.SetText("Download Location: " + outputDir)
+ }
+ }
+
+ selectDirectory := func() {
+ dialog.NewFolderOpen(func(uri fyne.ListableURI, err error) {
+ if err != nil || uri == nil {
+ return
+ }
+ outputDir = uri.Path()
+ prefs.SetString(prefOutputDir, outputDir)
+ updateOutputUI()
+ }, w).Show()
+ }
+
+ if outputDir == "" {
+ dialog.ShowInformation("Select Download Location", "Please select a download folder.", w)
+ selectDirectory()
+ }
+ updateOutputUI()
+
+ changeDirBtn := widget.NewButton("Change Download Location", selectDirectory)
+
+ /* -------------------- Dynamic Fetch -------------------- */
+
+ var fetchTimer *time.Timer
+ var fetching int32
+
+ urlEntry.OnChanged = func(text string) {
+ text = strings.TrimSpace(text)
+ if fetchTimer != nil {
+ fetchTimer.Stop()
+ }
+ if text == "" {
+ fyne.Do(func() {
+ statusLabel.SetText("Ready")
+ titleLabel.SetText("")
+ resSelect.Disable()
+ addBtn.Disable()
+ })
+ return
+ }
+
+ // Validate URL before fetching
+ if !isValidYouTubeURL(text) {
+ fyne.Do(func() {
+ statusLabel.SetText("Invalid YouTube URL")
+ titleLabel.SetText("")
+ resSelect.Disable()
+ addBtn.Disable()
+ })
+ return
+ }
+
+ fetchTimer = time.AfterFunc(fetchDebounceDelay, func() {
+ if atomic.LoadInt32(&fetching) == 1 {
+ return
+ }
+ atomic.StoreInt32(&fetching, 1)
+
+ fyne.Do(func() {
+ statusLabel.SetText("Fetching video info...")
+ resSelect.Disable()
+ addBtn.Disable()
+ })
+
+ go fetchVideoInfo(text, resolutions, resSelect, statusLabel, titleLabel, addBtn, &fetching)
+ })
+ }
+
+ /* -------------------- Download -------------------- */
+
+ addBtn.OnTapped = func() {
+ url := strings.TrimSpace(urlEntry.Text)
+ if url == "" || !isValidYouTubeURL(url) {
+ dialog.ShowError(fmt.Errorf("Please enter a valid YouTube URL"), w)
+ return
+ }
+
+ cleanRes := extractResolution(resSelect.Selected)
+ task := DownloadTask{
+ URL: url,
+ Title: strings.TrimPrefix(titleLabel.Text, "Title : "),
+ Type: downloadType.Selected,
+ Resolution: resSelect.Selected,
+ CleanRes: cleanRes,
+ AudioFormat: audioSelect.Selected,
+ }
+ taskQueue <- task
+
+ urlEntry.SetText("")
+ titleLabel.SetText("")
+ resSelect.Disable()
+ addBtn.Disable()
+ statusLabel.SetText("Added to queue")
+ if tabs != nil {
+ tabs.SelectIndex(1)
+ }
+ }
+
+ downloadsContainer := container.NewVBox()
+ downloadsScroll := container.NewVScroll(downloadsContainer)
+
+ go worker(downloadsContainer, &outputDir)
+
+ /* -------------------- Layout -------------------- */
+
+ content := container.NewVBox(
+ urlInputContainer,
+ titleLabel,
+ downloadType,
+ container.NewGridWithColumns(2, resSelect, audioSelect),
+ widget.NewSeparator(),
+ outputDirLabel,
+ changeDirBtn,
+ addBtn,
+ statusLabel,
+ )
+
+ tabs = container.NewAppTabs(
+ container.NewTabItemWithIcon("New Download", theme.DownloadIcon(), container.NewVScroll(content)),
+ container.NewTabItemWithIcon("Queue", theme.ListIcon(), downloadsScroll),
+ )
+
+ mainLayout := container.NewBorder(headerContainer, nil, nil, nil, tabs)
+ w.SetContent(mainLayout)
+ w.ShowAndRun()
+}
+
+/* -------------------- Fetch Function -------------------- */
+
+func fetchVideoInfo(
+ url string,
+ resolutions []string,
+ resSelect *widget.Select,
+ statusLabel *widget.Label,
+ titleLabel *widget.Label,
+ downloadBtn *widget.Button,
+ fetching *int32,
+) {
+ ctx, cancel := context.WithTimeout(context.Background(), fetchTimeout)
+ defer cancel()
+
+ // Check context before starting
+ if ctx.Err() != nil {
+ fyne.Do(func() {
+ atomic.StoreInt32(fetching, 0)
+ statusLabel.SetText("Request timeout")
+ })
+ return
+ }
+
+ result, err := ytdlp.New().DumpJSON().Run(ctx, url)
+
+ fyne.Do(func() {
+ defer atomic.StoreInt32(fetching, 0)
+
+ if err != nil {
+ if ctx.Err() == context.DeadlineExceeded {
+ statusLabel.SetText("Request timeout - try again")
+ } else {
+ statusLabel.SetText("Failed to fetch info")
+ }
+ return
+ }
+
+ var info struct {
+ Title string `json:"title"`
+ Duration int `json:"duration"`
+ Formats []struct {
+ Height *int `json:"height"`
+ Width *int `json:"width"`
+ Filesize *int64 `json:"filesize"`
+ FilesizeApprox *int64 `json:"filesize_approx"`
+ Vcodec string `json:"vcodec"`
+ Acodec string `json:"acodec"`
+ Ext string `json:"ext"`
+ } `json:"formats"`
+ }
+
+ if err := json.Unmarshal([]byte(result.Stdout), &info); err != nil {
+ statusLabel.SetText("Failed to parse info")
+ return
+ }
+
+ if info.Title == "" {
+ statusLabel.SetText("No video found at URL")
+ return
+ }
+
+ titleLabel.SetText("Title : " + info.Title)
+
+ // Build resolution map with better size detection
+ resMap := make(map[string]string)
+ for _, f := range info.Formats {
+ // Only include video formats (has video codec and height)
+ if f.Vcodec != "none" && f.Height != nil && *f.Height > 0 {
+ res := fmt.Sprintf("%dp", *f.Height)
+
+ // Get the best size estimate available
+ var size int64 = 0
+ if f.Filesize != nil && *f.Filesize > 0 {
+ size = *f.Filesize
+ } else if f.FilesizeApprox != nil && *f.FilesizeApprox > 0 {
+ size = *f.FilesizeApprox
+ }
+
+ // Keep the largest size for each resolution (worst case estimate)
+ if size > 0 {
+ existingSize := parseSizeString(resMap[res])
+ if size > existingSize {
+ resMap[res] = formatBytes(float64(size))
+ }
+ }
+
+ }
+ }
+
+ // Build options with available resolutions
+ opts := []string{}
+ for _, r := range resolutions {
+ size := "Unknown"
+ if s, ok := resMap[r]; ok {
+ size = s
+ }
+ opts = append(opts, fmt.Sprintf("%s (%s)", r, size))
+ }
+
+ resSelect.Options = opts
+ if len(opts) > 0 {
+ // Check if current selection is still valid in new options
+ currentSelection := resSelect.Selected
+ selectionValid := false
+ for _, opt := range opts {
+ if opt == currentSelection {
+ selectionValid = true
+ break
+ }
+ }
+
+ // Only auto-select if current selection is empty or invalid
+ if !selectionValid || currentSelection == "" {
+ // Select best available quality by default (prefer 1080p, then 720p, then first)
+ selectedIdx := 0
+ for i, opt := range opts {
+ if strings.Contains(opt, "1080p") || strings.Contains(opt, "720p") {
+ selectedIdx = i
+ break
+ }
+ }
+ resSelect.SetSelected(opts[selectedIdx])
+ }
+ resSelect.Enable()
+ downloadBtn.Enable()
+ statusLabel.SetText("Ready to download")
+ } else {
+ statusLabel.SetText("No video formats found")
+ }
+
+ })
+}
+>>>>>>> 5c9da659bc26ab9faf682fb549dc7e0b599a7169