Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions frontend/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,31 @@ export class UnifiedClient {
throw new Error("Use uploadFileList for web folder uploads");
}

async selectFolders(): Promise<string[]> {
await this.initialize();

if (this._environment === "wails") {
const client = await getWailsClient();
const paths = await client.App.SelectFolders();
return paths ?? [];
}

// In web mode, folder selection is handled via HTML input with webkitdirectory
return [];
}

async uploadFolders(folderPaths: string[]): Promise<void> {
await this.initialize();

if (this._environment === "wails") {
const client = await getWailsClient();
return client.App.UploadFolders(folderPaths);
}

// In web mode, folder upload is handled via uploadFileList with webkitdirectory files
throw new Error("Use uploadFileList for web folder uploads");
}

// Logs
async getLogs(): Promise<string> {
await this.initialize();
Expand Down
24 changes: 15 additions & 9 deletions frontend/src/lib/components/dashboard/DashboardHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,27 @@ function closeFileExplorer() {
async function handleFolderUpload() {
try {
if (apiClient.environment === "wails") {
// Desktop mode: use native folder picker
const folderPath = await apiClient.selectFolder();
if (folderPath) {
await apiClient.uploadFolder(folderPath);
// Desktop mode: use native multi-folder picker
const folderPaths = await apiClient.selectFolders();
if (folderPaths.length === 0) return;

await apiClient.uploadFolders(folderPaths);

if (folderPaths.length === 1) {
toastStore.success(
$t("dashboard.header.folder_added"),
$t("dashboard.header.folder_added_description")
);
} else {
toastStore.success(
$t("dashboard.header.folders_added"),
$t("dashboard.header.folders_added_description", { count: folderPaths.length })
);
}
} else {
// Web mode: use hidden input with webkitdirectory attribute
// Web mode already supports selecting one folder at a time via the browser's native picker;
// multiple folder uploads require repeated clicks (browser limitation).
const input = document.createElement("input");
input.type = "file";
// @ts-ignore - webkitdirectory is not in the type definitions
Expand All @@ -145,11 +155,7 @@ async function handleFolderUpload() {

input.onchange = async () => {
if (input.files && input.files.length > 0) {
// In web mode, we need to send the files to the server
const fileList = input.files;

// Use the existing file upload mechanism
uploadActions.startUpload(fileList);
uploadActions.startUpload(input.files);

try {
await apiClient.uploadFileList(
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/lib/locales/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
"description": "Manage your file uploads and monitor progress",
"add_files": "Add Files",
"add_folder": "Add Folder",
"add_folder_tooltip": "Upload an entire folder as a single NZB",
"add_folder_tooltip": "Upload one or more folders, each as a single NZB",
"folder_added": "Folder Added",
"folder_added_description": "Folder has been added to the upload queue",
"folders_added": "Folders Added",
"folders_added_description": "{{count}} folders have been added to the upload queue",
"import_files": "Import Files",
"import_files_tooltip": "Browse and import files from the remote server",
"clear_completed": "Clear Completed"
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/lib/locales/es/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
"description": "Gestiona tus cargas de archivos y monitorea el progreso",
"add_files": "Agregar Archivos",
"add_folder": "Agregar Carpeta",
"add_folder_tooltip": "Subir una carpeta entera como un solo NZB",
"add_folder_tooltip": "Subir una o más carpetas, cada una como un solo NZB",
"folder_added": "Carpeta Agregada",
"folder_added_description": "La carpeta ha sido agregada a la cola de carga",
"folders_added": "Carpetas Agregadas",
"folders_added_description": "{{count}} carpetas han sido agregadas a la cola de carga",
"import_files": "Importar Archivos",
"import_files_tooltip": "Explorar e importar archivos desde el servidor remoto",
"clear_completed": "Limpiar Completados"
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/lib/locales/fr/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
"description": "Gérez vos téléchargements de fichiers et surveillez le progrès",
"add_files": "Ajouter des Fichiers",
"add_folder": "Ajouter un Dossier",
"add_folder_tooltip": "Télécharger un dossier entier comme un seul NZB",
"add_folder_tooltip": "Télécharger un ou plusieurs dossiers, chacun comme un seul NZB",
"folder_added": "Dossier Ajouté",
"folder_added_description": "Le dossier a été ajouté à la file d'attente de téléchargement",
"folders_added": "Dossiers Ajoutés",
"folders_added_description": "{{count}} dossiers ont été ajoutés à la file d'attente de téléchargement",
"import_files": "Importer des Fichiers",
"import_files_tooltip": "Explorer et importer des fichiers depuis le serveur distant",
"clear_completed": "Vider les Terminés"
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/lib/locales/tr/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
"description": "Dosya yüklemelerinizi yönetin ve ilerlemeyi izleyin",
"add_files": "Dosya Ekle",
"add_folder": "Klasör Ekle",
"add_folder_tooltip": "Tüm klasörü tek bir NZB olarak yükle",
"add_folder_tooltip": "Bir veya daha fazla klasörü her biri tek bir NZB olarak yükle",
"folder_added": "Klasör Eklendi",
"folder_added_description": "Klasör yükleme kuyruğuna eklendi",
"folders_added": "Klasörler Eklendi",
"folders_added_description": "{{count}} klasör yükleme kuyruğuna eklendi",
"import_files": "Dosyaları İçe Aktar",
"import_files_tooltip": "Uzak sunucudan dosyalara göz atın ve içe aktarın",
"clear_completed": "Tamamlananları Temizle"
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/lib/wailsjs/go/backend/App.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export function SelectConfigFile():Promise<string>;

export function SelectFolder():Promise<string>;

export function SelectFolders():Promise<Array<string>>;

export function SelectOutputDirectory():Promise<string>;

export function SelectTempDirectory():Promise<string>;
Expand All @@ -132,4 +134,6 @@ export function UploadFiles():Promise<void>;

export function UploadFolder(arg1:string):Promise<void>;

export function UploadFolders(arg1:Array<string>):Promise<void>;

export function ValidateNNTPServer(arg1:backend.ServerData):Promise<backend.ValidationResult>;
8 changes: 8 additions & 0 deletions frontend/src/lib/wailsjs/go/backend/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ export function SelectFolder() {
return window['go']['backend']['App']['SelectFolder']();
}

export function SelectFolders() {
return window['go']['backend']['App']['SelectFolders']();
}

export function SelectOutputDirectory() {
return window['go']['backend']['App']['SelectOutputDirectory']();
}
Expand Down Expand Up @@ -254,6 +258,10 @@ export function UploadFolder(arg1) {
return window['go']['backend']['App']['UploadFolder'](arg1);
}

export function UploadFolders(arg1) {
return window['go']['backend']['App']['UploadFolders'](arg1);
}

export function ValidateNNTPServer(arg1) {
return window['go']['backend']['App']['ValidateNNTPServer'](arg1);
}
105 changes: 105 additions & 0 deletions internal/backend/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,34 @@ func (a *App) SelectFolder() (string, error) {
return folderPath, nil
}

// SelectFolders opens a native multi-select dialog and returns paths of all selected folders.
// Returns nil slice (not an error) when the user cancels.
func (a *App) SelectFolders() ([]string, error) {
defer a.recoverPanic("SelectFolders")

selected, err := runtime.OpenMultipleFilesDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select folders to upload",
})
if err != nil {
return nil, fmt.Errorf("error opening folder dialog: %w", err)
}

// Filter to directories only so files accidentally selected are ignored
var dirs []string
for _, p := range selected {
info, err := os.Stat(p)
if err != nil {
slog.Warn("Could not stat selected path, skipping", "path", p, "error", err)
continue
}
if info.IsDir() {
dirs = append(dirs, p)
}
}

return dirs, nil
}

// UploadFolder uploads all files from a folder as a single NZB
// The folder structure will be preserved in the article subjects
func (a *App) UploadFolder(folderPath string) error {
Expand Down Expand Up @@ -217,3 +245,80 @@ func (a *App) UploadFolder(folderPath string) error {

return nil
}

// UploadFolders queues multiple folders for upload as separate NZBs.
func (a *App) UploadFolders(folderPaths []string) error {
defer a.recoverPanic("UploadFolders")

if len(folderPaths) == 0 {
return fmt.Errorf("no folder paths provided")
}

// Check configuration once before processing all folders
status := a.GetAppStatus()
if status.NeedsConfiguration {
return fmt.Errorf("configuration required: Please configure at least one server in the Settings page before uploading files")
}

if a.queue == nil {
return fmt.Errorf("queue not initialized")
}

addedCount := 0
for _, folderPath := range folderPaths {
info, err := os.Stat(folderPath)
if err != nil {
slog.Warn("Could not access folder, skipping", "path", folderPath, "error", err)
continue
}
if !info.IsDir() {
slog.Warn("Path is not a folder, skipping", "path", folderPath)
continue
}

filesByFolder, sizeByFolder, err := processDirectoryRecursively(folderPath)
if err != nil {
slog.Error("Error processing directory, skipping", "path", folderPath, "error", err)
continue
}

var totalFiles int
var totalSize int64
for _, files := range filesByFolder {
totalFiles += len(files)
}
for _, size := range sizeByFolder {
totalSize += size
}

if totalFiles == 0 {
slog.Warn("Folder contains no files, skipping", "path", folderPath)
continue
}

folderName := filepath.Base(folderPath)
folderQueuePath := "FOLDER:" + folderPath
if err := a.queue.AddFile(context.Background(), folderQueuePath, totalSize); err != nil {
slog.Warn("Could not add folder to queue, skipping", "folder", folderName, "error", err)
continue
}

addedCount++
slog.Info("Folder added to queue", "folder", folderName, "files", totalFiles, "size", totalSize)
}

if addedCount == 0 {
return fmt.Errorf("no valid folders could be added to queue")
}

slog.Info("Added folders to queue", "added", addedCount, "total", len(folderPaths))

// Emit a single event after all folders are queued
if !a.isWebMode {
runtime.EventsEmit(a.ctx, "queue-updated")
} else if a.webEventEmitter != nil {
a.webEventEmitter("queue-updated", nil)
}

return nil
}
Loading