diff --git a/.github/workflows/blank.yml b/.github/workflows/blank.yml index 01502b1..5128759 100644 --- a/.github/workflows/blank.yml +++ b/.github/workflows/blank.yml @@ -1,4 +1,4 @@ -# This is a basic workflow to help you get started with Actions +# CI workflow for BotDesk name: CI @@ -25,12 +25,18 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 - # Runs a single command using the runners shell - - name: Run a one-line script - run: echo Hello, world! + # Set up Python + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' - # Runs a set of commands using the runners shell - - name: Run a multi-line script + # Install dependencies + - name: Install dependencies run: | - echo Add other actions to build, - echo test, and deploy your project. + python -m pip install --upgrade pip + pip install -r requirements.txt + + # Run tests + - name: Run tests + run: python -m unittest discover tests diff --git a/.gitignore b/.gitignore index 15201ac..4e40054 100644 --- a/.gitignore +++ b/.gitignore @@ -165,7 +165,13 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ + +# Visual Studio +.vs/ + +# Error logs +error_log.txt # PyPI configuration file .pypirc diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8139c50 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Use an official Python runtime as the base image +FROM python:3.10-slim + +# Set environment variables to prevent Python from buffering output +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Set the working directory in the container +WORKDIR /app + +# Copy only the requirements file initially to take advantage of Docker's layer caching +COPY requirements.txt /app/requirements.txt + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the entire project to the entire container +COPY . /app + +# Export port 5000 (optional, in case you use Flask or other servers in the future) +EXPOSE 5000 + +# Set the entry point to run the main.py file +CMD ["python", "main.py"] + +#FROM ubuntu:latest +#LABEL authors="irfan" + +#ENTRYPOINT ["top", "-b"] \ No newline at end of file diff --git a/README.md b/README.md index d773203..2b077ac 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,154 @@ -# BotDesk -A desktop automation application to automate tasks on your computer. +# Botdesk + +**Botdesk** is a powerful desktop automation tool designed to simplify repetitive tasks on your computer. It provides a user-friendly +dashboard for automating common tasks like organizing files, finding duplicate files and analyzing folder structures. Built with Python +and PyQt5, BotDesk is ideal for professionals and everyday users seeking efficiency and time savings. + +--- + +## Features + +- **File Organizer**: Automatically organizes files into subfolders based on their extensions. +- **Duplicate Finder**: Detects and lists duplicate files in a selected folder. +- **Folder Analyzer**: Provides insights into folder sizes, file counts, and storage usage. +- **Batch File Renamer**: Rename multiple files in a folder with customizable patterns or by appending timestamps for better organization. +- **File Search and Filter**: Quickly search for specific files based on extensions sizes or modification dates includes advanced filtering options to locate files faster. +- **Scheduled Automation**: Set up automation tasks to run at specific times, such as organizing files or clearing duplicates daily, weekly, or monthly. +- **Drag-and-Drop Support**: Simplifies file selection by allowing users to drag folders or files directly into the app interface. +- **Custom Rules for File Organization**: Set up custom rules to sort files not just by extensions but also by file size, date, or even keywords in filenames. +- **Multi-Folder Support**: Automate actions across multiple folders simultaneously, saving you time for large-scale file operations. +- **Backup & Restore**: Automatically create backups of files before applying any changes, with a one-click restore option for added safety. + +--- + +## Upcoming Features + +- **Cloud Integration**: Automate file organization and synchronization with popular cloud storage services like Google Drive, Dropbox, and OneDrive. +- **Advanced File Filters**: Filter files by metadata such as creation date, size range, and custom tags for precise automation. +- **Email Automation**: Automatically download, sort, and organize email attachments into specified folders. +- **File Conversion Tools**: Convert files between popular formats (e.g., PDF to Word, image formats) directly within the app. +- **Text Extraction and OCR**: Extract text from images or scanned documents using Optical Character Recognition (OCR) for document processing tasks. +- **Task Scheduler Enhancements**: Introduce an intuitive task calendar to manage recurring tasks +- **Duplicate Detection Improvements**: Add visual previews and advanced duplicate comparison options, such as by resolution for images or embedded metadata. +- **System Cleanup Tools**: Automate cleaning temporary files, caches, and unused programs to optimize computer performance. +- **Zip and Archive Management**: Automate compression and extraction of files, with options to organize extracted content into subfolders. +- **Integration with External APIs**: Enable automations that interact with external APIs for custom workflows, such as downloading reports or scraping web content. +- **Customizable Themes**: Personalize your BotDesk experience with light, dark, and high-contrast themes. +- **Multi-Language Support**: Expand accessibility with support for multiple languages, starting with French, Spanish and German. +- **Ai-Powered Suggestions**: Leverage AI to analyze usage patterns and recommend automation tasks tailored to your workflow. + +--- + +## Installation +### Prerequisites + +1. Install 3.8 or higher +2. Install pip (Python's package manager) +3. Install the required dependencies using the command +`` +pip install -r requirements.txt +`` + +### Steps to Run +1. Clone this repository: +``` +git clone https://github.com/C4bbage64/BotDesk +``` +2. Navigate to the project directory: +``` +cd BotDesk +``` +3. Run the application: +``` +python main.py +``` + +--- + +## Usage +### Dashboard Overview + +When you launch BotDesk, you'll be greeted with a dashboard featuring multiple automation options. Each option provides a dedicated +interface to execute the desired automation. + +1. File Organizer: + - Enter a folder path. + - Specify file extensions (comma-separated). + - Click "Organize Files" to sort files into subfolders by extension. +2. Duplicate Finder: + - Enter a folder path + - Click "Find Duplicates" to detect duplicate files in the selected folder. + - View duplicate results in the log area. +3. Folder Analyzer: + - Enter a folder path. + - Click "Analyzer" to get detailed statistics about the folder and its contents + +--- + +## Project Structure + +``` +BotDesk/ +├── automations/ +│ ├── file_organizer.py # Core logic for file organization +│ ├── duplicate_finder.py # Core logic for finding duplicate files +│ ├── folder_analyzer.py # Core logic for analyzing folders +├── ui/ +│ ├── dashboard.py # Main dashboard UI +│ ├── file_organizer.py # File Organizer UI +│ ├── duplicate_finder.py # Duplicate Finder UI +│ ├── folder_analyzer.py # Folder Analyzer UI +├── main.py # Entry point of the application +├── requirements.txt # Python dependencies +└── README.md # Project documentation +``` + +## Dependencies + +The following Python libraries are required: +- PyQt5 +- os +- shutil +- hashlib + +Install all dependencies with: +``` +pip install -r requirements.txt +``` + +--- + +# Future Enhancements + +Planned features for upcoming versions +- Add more automation tools, like scheduled tasks or batch renaming. +- Introduce drag-and-drop functionality for file selection. +- Save user preferences for faster operations +- Support for advanced file (e.g., by size or date) + +--- + +# Contributing +Contributions are welcome! If you'd like to improve this project: +1. Fork the repository. +2. Create a feature branch: +``` +git checkout -b feature-name +``` +3. Commit your changes and push the branch: +``` +git push origin feature-name +``` +4. Create a pull request + +--- + +## License + +This project is licensed under the MIT License + +--- + +## Acknowledgements + +Special thanks to the open-source community for their tools and inspirations \ No newline at end of file diff --git a/automations/__init__.py b/automations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/automations/batch_renamer.py b/automations/batch_renamer.py new file mode 100644 index 0000000..a538b9c --- /dev/null +++ b/automations/batch_renamer.py @@ -0,0 +1,97 @@ +import os + +def get_rename_preview(folder_path, replace_text="", with_text="", add_prefix="", add_suffix=""): + """ + Generates a preview of file renames based on the provided criteria. + + Args: + folder_path (str): Path to the folder containing files. + replace_text (str): Text to replace in the filename. + with_text (str): Text to replace with. + add_prefix (str): Text to add to the beginning of the filename. + add_suffix (str): Text to add to the end of the filename (before extension). + + Returns: + list: A list of tuples (original_name, new_name) for files that will be changed. + """ + preview = [] + if not os.path.exists(folder_path): + return [] + + try: + files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))] + except Exception: + return [] + + for filename in files: + new_name = filename + name, ext = os.path.splitext(filename) + + # Replace text + if replace_text: + new_name = new_name.replace(replace_text, with_text) + # Re-split in case replacement affected extension (though usually we operate on full name or just base?) + # Standard behavior: replace in the whole name including extension? + # Or just base? Let's do whole name for flexibility, but usually people want base. + # Let's stick to simple string replacement on the whole name for now, + # but prefix/suffix usually apply to base. + + # Actually, let's refine: Replace usually applies to the whole string. + # Prefix/Suffix apply to the base name. + + # Let's re-calculate name/ext after replacement if we want to be safe, + # but if we replace in 'new_name', we are modifying the whole thing. + pass + + # Apply prefix/suffix to the current state of the name (excluding extension for suffix) + # To do this correctly, we need to split the potentially modified new_name + current_base, current_ext = os.path.splitext(new_name) + + if add_prefix: + current_base = f"{add_prefix}{current_base}" + + if add_suffix: + current_base = f"{current_base}{add_suffix}" + + final_name = f"{current_base}{current_ext}" + + if final_name != filename: + preview.append((filename, final_name)) + + return preview + +def execute_renames(folder_path, rename_map): + """ + Executes the renaming of files based on a map of {old_name: new_name}. + + Args: + folder_path (str): Path to the folder. + rename_map (dict): Dictionary mapping original filenames to new filenames. + + Returns: + tuple: (success_message, error_list) + """ + renamed_count = 0 + errors = [] + + if not os.path.exists(folder_path): + return "Folder not found.", ["Folder does not exist."] + + for old_name, new_name in rename_map.items(): + if old_name == new_name: + continue + + try: + old_path = os.path.join(folder_path, old_name) + new_path = os.path.join(folder_path, new_name) + + if os.path.exists(new_path): + errors.append(f"Skipped {old_name}: Destination {new_name} already exists.") + continue + + os.rename(old_path, new_path) + renamed_count += 1 + except Exception as e: + errors.append(f"Error renaming {old_name}: {str(e)}") + + return f"Successfully renamed {renamed_count} files.", errors diff --git a/automations/duplicate_finder.py b/automations/duplicate_finder.py new file mode 100644 index 0000000..aef672a --- /dev/null +++ b/automations/duplicate_finder.py @@ -0,0 +1,48 @@ +import os +import hashlib + +class DuplicateFinder: + @staticmethod + def calculate_file_hash(file_path, chunk_size=1024): + """Calculate the hash of a file for comparison.""" + hasher = hashlib.md5() + try: + with open(file_path, "rb") as file: + while chunk := file.read(chunk_size): + hasher.update(chunk) + except Exception as e: + return None + return hasher.hexdigest() + + @staticmethod + def find_duplicates(folder_path): + """Find and return duplicate files in the folder.""" + if not os.path.exists(folder_path): + return {"error": "Folder does not exist."} + + file_hashes = {} + duplicates = [] + + for root, _, files in os.walk(folder_path): + for file in files: + file_path = os.path.join(root, file) + file_hash = DuplicateFinder.calculate_file_hash(file_path) + if file_hash: + if file_hash in file_hashes: + duplicates.append((file_path, file_hashes[file_hash])) + else: + file_hashes[file_hash] = file_path + + return {"duplicates": duplicates} + + @staticmethod + def delete_duplicates(duplicates): + """Delete the duplicate files provided in the list.""" + deleted_files = [] + for duplicate, _ in duplicates: + try: + os.remove(duplicate) + deleted_files.append(duplicate) + except Exception as e: + pass # Handle specific errors if needed + return deleted_files diff --git a/automations/file_organizer.py b/automations/file_organizer.py new file mode 100644 index 0000000..88cd70d --- /dev/null +++ b/automations/file_organizer.py @@ -0,0 +1,30 @@ +import os +import shutil + +def organize_files(folder_path, extensions): + """ + Organizes files in the given folder into subfolders based on their extensions. + + :param folder_path: Path to the folder to organize + :param extensions: List of file extensions to organize + :return: Success or error message + """ + if not os.path.isdir(folder_path): + return "Error: Folder path does not exist." + + try: + for ext in extensions: + ext_folder = os.path.join(folder_path, ext) + os.makedirs(ext_folder, exist_ok=True) + + for filename in os.listdir(folder_path): + file_path = os.path.join(folder_path, filename) + if os.path.isfile(file_path): + file_ext = filename.split('.')[-1].lower() + if file_ext in extensions: + dest_folder = os.path.join(folder_path, file_ext) + shutil.move(file_path, os.path.join(dest_folder, filename)) + return "Files organized successfully." + except Exception as e: + return f"Error: {str(e)}" + diff --git a/automations/folder_analyzer.py b/automations/folder_analyzer.py new file mode 100644 index 0000000..39c7a49 --- /dev/null +++ b/automations/folder_analyzer.py @@ -0,0 +1,27 @@ +import os + +def analyze_folder(folder_path): + """Analyze the contents of a folder and returns a summary""" + if not os.path.exists(folder_path): + return "Error: The folder does not exist." + + file_count = 0 + folder_count = 0 + total_size = 0 + + try: + for root, dirs, files in os.walk(folder_path): + folder_count += len(dirs) + file_count += len(files) + total_size += sum(os.path.getsize(os.path.join(root, f)) for f in files) + + result = ( + f"Folder Analysis:\n" + f"Total Files: {file_count}\n" + f"Total Folder: {folder_count}\n" + f"Total Size: {total_size / (1024 * 1024):.2f} MB" + ) + return result + + except Exception as e: + return f"Error analyzing folder: {str(e)}" diff --git a/automations/pdf_tools.py b/automations/pdf_tools.py new file mode 100644 index 0000000..2b73452 --- /dev/null +++ b/automations/pdf_tools.py @@ -0,0 +1,30 @@ +from PyPDF2 import PdfReader, PdfWriter + +def merge_pdfs(file_list, output_file): + pdf_writer = PdfWriter() + for file in file_list: + pdf_reader = PdfReader(file) + for page in pdf_reader.pages: + pdf_writer.add_page(page) + with open(output_file, 'wb') as output_pdf: + pdf_writer.write(output_pdf) + return f"Merged PDFs into: {output_file}" + +def split_pdfs(input_file): + pdf_reader = PdfReader(input_file) + split_files = [] + for i, page in enumerate(pdf_reader.pages): + pdf_writer = PdfWriter() + pdf_writer.add_page(page) + output_file = f"page_{i + 1}.pdf" + with open(output_file, 'wb') as output_pdf: + pdf_writer.write(output_pdf) + split_files.append(output_file) + return f"Split PDF into: {split_files}" + +def extract_text_from_pdf(input_file): + pdf_reader = PdfReader(input_file) + text = "" + for page in pdf_reader.pages: + text += page.extract_text() + return text diff --git a/automations/system_cleaner.py b/automations/system_cleaner.py new file mode 100644 index 0000000..be08968 --- /dev/null +++ b/automations/system_cleaner.py @@ -0,0 +1,41 @@ +import os +import shutil +import tempfile + +def clean_temp_files(): + """Cleans temporary files from the system.""" + temp_dir = tempfile.gettempdir() + removed_files = [] + for root, dirs, files in os.walk(temp_dir): + for file in files: + try: + file_path = os.path.join(root, file) + os.remove(file_path) + removed_files.append(file_path) + except Exception as e: + pass # ignore errors for system-locked files + return f"Cleaned {len(removed_files)} temporary files." + +def clean_cache(cache_dirs=None): + """ + Cleans cache from the specified directories. + + :param cache_dirs: List of cache directories to clean. Default is None. + :return: Summary string. + """ + if cache_dirs is None: + # Placeholder for common browser cache paths or other temp locations + # For safety, we won't guess user paths without explicit configuration + return "No cache directories specified. Skipping cache cleanup." + + cleaned_count = 0 + for directory in cache_dirs: + if os.path.exists(directory): + for root, dirs, files in os.walk(directory): + for file in files: + try: + os.remove(os.path.join(root, file)) + cleaned_count += 1 + except Exception: + pass + return f"Cleaned {cleaned_count} files from cache directories." \ No newline at end of file diff --git a/automations/zip_manager.py b/automations/zip_manager.py new file mode 100644 index 0000000..8ccb7cc --- /dev/null +++ b/automations/zip_manager.py @@ -0,0 +1,65 @@ +import os +import shutil +import zipfile + +def compress_files(source_paths, output_path): + """ + Compresses selected files and folders into a zip archive. + + Args: + source_paths (list): List of absolute paths to files/folders to compress. + output_path (str): Destination path for the .zip file. + + Returns: + str: Success message or error message. + """ + if not source_paths: + return "No files selected for compression." + + try: + # Ensure output path ends with .zip + if not output_path.lower().endswith('.zip'): + output_path += '.zip' + + with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for path in source_paths: + if os.path.isfile(path): + # Add file, using its basename as arcname + zipf.write(path, os.path.basename(path)) + elif os.path.isdir(path): + # Add folder and its contents + base_folder = os.path.basename(path) + for root, dirs, files in os.walk(path): + for file in files: + file_path = os.path.join(root, file) + # Calculate relative path for arcname + rel_path = os.path.relpath(file_path, os.path.dirname(path)) + zipf.write(file_path, rel_path) + + return f"Successfully created archive: {output_path}" + except Exception as e: + return f"Error creating archive: {str(e)}" + +def extract_archive(archive_path, output_dir): + """ + Extracts an archive to the specified directory. + + Args: + archive_path (str): Path to the archive file. + output_dir (str): Destination directory. + + Returns: + str: Success message or error message. + """ + if not os.path.exists(archive_path): + return "Archive file not found." + + try: + # Create output directory if it doesn't exist + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + shutil.unpack_archive(archive_path, output_dir) + return f"Successfully extracted to: {output_dir}" + except Exception as e: + return f"Error extracting archive: {str(e)}" diff --git a/main.py b/main.py new file mode 100644 index 0000000..6ff2258 --- /dev/null +++ b/main.py @@ -0,0 +1,12 @@ +import sys +from PyQt5.QtWidgets import QApplication +from ui.dashboard import Dashboard + +def main(): + app = QApplication(sys.argv) + main_window = Dashboard() + main_window.show() + sys.exit(app.exec_()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b4171ff --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyQt5~=5.15.11 +PyPDF2 \ No newline at end of file diff --git a/tests/test_automations.py b/tests/test_automations.py new file mode 100644 index 0000000..c8d6896 --- /dev/null +++ b/tests/test_automations.py @@ -0,0 +1,43 @@ +import unittest +import os +import tempfile +from automations.system_cleaner import clean_temp_files, clean_cache +from automations.pdf_tools import merge_pdfs, split_pdfs, extract_text_from_pdf + +class TestAutomations(unittest.TestCase): + + def test_clean_temp_files(self): + # Create a dummy temp file + temp_dir = tempfile.gettempdir() + fd, path = tempfile.mkstemp(dir=temp_dir) + os.close(fd) + + # Run cleaner + result = clean_temp_files() + + # Verify file is gone (or at least the function ran without error) + # Note: clean_temp_files might not delete *all* files due to locks, + # but it should try to delete our dummy file if it's not locked. + # Since we just closed it, it should be deletable. + self.assertFalse(os.path.exists(path), "Temp file should have been deleted") + self.assertIn("Cleaned", result) + + def test_clean_cache_no_args(self): + result = clean_cache() + self.assertIn("No cache directories specified", result) + + def test_clean_cache_with_dir(self): + # Create a dummy cache dir + with tempfile.TemporaryDirectory() as tmpdirname: + # Create a file inside + file_path = os.path.join(tmpdirname, "cache_file.txt") + with open(file_path, "w") as f: + f.write("data") + + result = clean_cache([tmpdirname]) + + self.assertFalse(os.path.exists(file_path), "Cache file should be deleted") + self.assertIn("Cleaned 1 files", result) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/verify_new_features.py b/tests/verify_new_features.py new file mode 100644 index 0000000..b1d6548 --- /dev/null +++ b/tests/verify_new_features.py @@ -0,0 +1,64 @@ +import os +import shutil +import unittest +import sys +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from automations.batch_renamer import get_rename_preview, execute_renames +from automations.zip_manager import compress_files, extract_archive + +class TestNewFeatures(unittest.TestCase): + def setUp(self): + self.test_dir = "test_env" + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + os.makedirs(self.test_dir) + + # Create dummy files + self.files = ["file1.txt", "file2.txt", "image.png"] + for f in self.files: + with open(os.path.join(self.test_dir, f), 'w') as fh: + fh.write("content") + + def tearDown(self): + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + + def test_batch_renamer(self): + print("\nTesting Batch Renamer...") + # Test Preview + preview = get_rename_preview(self.test_dir, add_prefix="test_") + self.assertEqual(len(preview), 3) + self.assertEqual(preview[0][1], "test_file1.txt") + + # Test Execution + rename_map = {old: new for old, new in preview} + msg, errors = execute_renames(self.test_dir, rename_map) + self.assertTrue("Successfully renamed 3 files" in msg) + self.assertEqual(len(errors), 0) + + # Verify files exist + self.assertTrue(os.path.exists(os.path.join(self.test_dir, "test_file1.txt"))) + self.assertFalse(os.path.exists(os.path.join(self.test_dir, "file1.txt"))) + print("Batch Renamer Passed!") + + def test_zip_manager(self): + print("\nTesting Zip Manager...") + # Test Compression + files_to_zip = [os.path.join(self.test_dir, f) for f in os.listdir(self.test_dir)] + zip_path = os.path.join(self.test_dir, "archive.zip") + + msg = compress_files(files_to_zip, zip_path) + self.assertTrue("Successfully created archive" in msg) + self.assertTrue(os.path.exists(zip_path)) + + # Test Extraction + extract_dir = os.path.join(self.test_dir, "extracted") + msg = extract_archive(zip_path, extract_dir) + self.assertTrue("Successfully extracted" in msg) + + # Verify extracted content + self.assertTrue(os.path.exists(os.path.join(extract_dir, "file1.txt"))) + print("Zip Manager Passed!") + +if __name__ == '__main__': + unittest.main() diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/batch_renamer.py b/ui/batch_renamer.py new file mode 100644 index 0000000..d3fa1d6 --- /dev/null +++ b/ui/batch_renamer.py @@ -0,0 +1,163 @@ +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, + QLineEdit, QFileDialog, QTableWidget, QTableWidgetItem, + QHeaderView, QGroupBox, QGridLayout, QMessageBox +) +from PyQt5.QtCore import Qt +from automations.batch_renamer import get_rename_preview, execute_renames + +class BatchRenamerUI(QWidget): + def __init__(self): + super().__init__() + self.init_ui() + self.preview_map = {} # Stores {old_name: new_name} + + def init_ui(self): + layout = QVBoxLayout() + + # Title + title = QLabel("Batch File Renamer") + title.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;") + title.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + + # Folder Selection + folder_layout = QHBoxLayout() + self.folder_input = QLineEdit() + self.folder_input.setPlaceholderText("Select a folder to rename files...") + browse_btn = QPushButton("Browse") + browse_btn.clicked.connect(self.browse_folder) + folder_layout.addWidget(self.folder_input) + folder_layout.addWidget(browse_btn) + layout.addLayout(folder_layout) + + # Options Group + options_group = QGroupBox("Renaming Options") + options_group.setStyleSheet("QGroupBox { font-weight: bold; border: 1px solid #555; border-radius: 5px; margin-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; }") + options_layout = QGridLayout() + options_layout.setSpacing(10) + + self.prefix_input = QLineEdit() + self.prefix_input.setPlaceholderText("e.g. img_") + self.prefix_input.setToolTip("Text to add to the beginning of the filename") + + self.suffix_input = QLineEdit() + self.suffix_input.setPlaceholderText("e.g. _v1") + self.suffix_input.setToolTip("Text to add to the end of the filename (before extension)") + + self.replace_input = QLineEdit() + self.replace_input.setPlaceholderText("Text to find") + self.replace_input.setToolTip("The text you want to remove or replace") + + self.with_input = QLineEdit() + self.with_input.setPlaceholderText("Replacement text") + self.with_input.setToolTip("The text to insert instead") + + options_layout.addWidget(QLabel("Add Prefix:"), 0, 0) + options_layout.addWidget(self.prefix_input, 0, 1) + options_layout.addWidget(QLabel("Add Suffix:"), 0, 2) + options_layout.addWidget(self.suffix_input, 0, 3) + + options_layout.addWidget(QLabel("Replace Text:"), 1, 0) + options_layout.addWidget(self.replace_input, 1, 1) + options_layout.addWidget(QLabel("With Text:"), 1, 2) + options_layout.addWidget(self.with_input, 1, 3) + + # Clear Button + clear_btn = QPushButton("Clear Options") + clear_btn.setCursor(Qt.PointingHandCursor) + clear_btn.clicked.connect(self.clear_options) + clear_btn.setStyleSheet("background-color: #444; color: white; padding: 5px; border-radius: 3px;") + options_layout.addWidget(clear_btn, 2, 3) + + options_group.setLayout(options_layout) + layout.addWidget(options_group) + + # Action Buttons (Preview) + preview_btn = QPushButton("Preview Changes") + preview_btn.clicked.connect(self.update_preview) + preview_btn.setStyleSheet("background-color: #0e639c; color: white; padding: 8px;") + layout.addWidget(preview_btn) + + # Preview Table + self.table = QTableWidget() + self.table.setColumnCount(2) + self.table.setHorizontalHeaderLabels(["Original Name", "New Name"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + layout.addWidget(self.table) + + # Rename Button + self.rename_btn = QPushButton("Rename Files") + self.rename_btn.clicked.connect(self.run_rename) + self.rename_btn.setStyleSheet("background-color: #28a745; color: white; padding: 10px; font-weight: bold;") + self.rename_btn.setEnabled(False) + layout.addWidget(self.rename_btn) + + self.setLayout(layout) + + def browse_folder(self): + folder = QFileDialog.getExistingDirectory(self, "Select Folder") + if folder: + self.folder_input.setText(folder) + self.update_preview() + + def update_preview(self): + folder = self.folder_input.text() + if not folder: + return + + prefix = self.prefix_input.text() + suffix = self.suffix_input.text() + replace = self.replace_input.text() + with_text = self.with_input.text() + + # Get preview data + preview_list = get_rename_preview(folder, replace, with_text, prefix, suffix) + + # Update Table + self.table.setRowCount(len(preview_list)) + self.preview_map = {} + + for i, (old, new) in enumerate(preview_list): + self.table.setItem(i, 0, QTableWidgetItem(old)) + self.table.setItem(i, 1, QTableWidgetItem(new)) + self.preview_map[old] = new + + if preview_list: + self.rename_btn.setEnabled(True) + self.rename_btn.setText(f"Rename {len(preview_list)} Files") + else: + self.rename_btn.setEnabled(False) + self.rename_btn.setText("Rename Files") + + def run_rename(self): + folder = self.folder_input.text() + if not folder or not self.preview_map: + return + + confirm = QMessageBox.question( + self, "Confirm Rename", + f"Are you sure you want to rename {len(self.preview_map)} files?", + QMessageBox.Yes | QMessageBox.No + ) + + if confirm == QMessageBox.Yes: + msg, errors = execute_renames(folder, self.preview_map) + + if errors: + error_text = "\n".join(errors[:10]) + if len(errors) > 10: + error_text += f"\n...and {len(errors)-10} more errors." + QMessageBox.warning(self, "Completed with Errors", f"{msg}\n\nErrors:\n{error_text}") + else: + QMessageBox.information(self, "Success", msg) + + # Refresh + self.update_preview() + + def clear_options(self): + self.prefix_input.clear() + self.suffix_input.clear() + self.replace_input.clear() + self.with_input.clear() + self.update_preview() diff --git a/ui/dashboard.py b/ui/dashboard.py new file mode 100644 index 0000000..539bfa7 --- /dev/null +++ b/ui/dashboard.py @@ -0,0 +1,184 @@ +from PyQt5.QtWidgets import ( + QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QLabel, QStackedWidget, QFrame +) +from PyQt5.QtCore import Qt +from ui.file_organizer import FileOrganizerUI +from ui.duplicate_finder import DuplicateFinderUI +from ui.folder_analyzer import FolderAnalyzerUI +from ui.pdf_tools import PDFToolsUI +from ui.system_cleaner import SystemCleanerUI +from ui.batch_renamer import BatchRenamerUI +from ui.zip_manager import ZipManagerUI + +class Dashboard(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("BotDesk Automation") + self.setGeometry(100, 100, 1000, 700) + + # Main Layout + self.central_widget = QWidget() + self.setCentralWidget(self.central_widget) + self.main_layout = QHBoxLayout(self.central_widget) + self.main_layout.setContentsMargins(0, 0, 0, 0) + self.main_layout.setSpacing(0) + + # Sidebar + self.sidebar = QFrame() + self.sidebar.setObjectName("Sidebar") + self.sidebar_layout = QVBoxLayout(self.sidebar) + self.sidebar_layout.setContentsMargins(0, 20, 0, 20) + self.sidebar_layout.setSpacing(10) + self.sidebar.setFixedWidth(250) + + # Sidebar Title + self.title_label = QLabel("BotDesk") + self.title_label.setObjectName("SidebarTitle") + self.title_label.setAlignment(Qt.AlignCenter) + self.sidebar_layout.addWidget(self.title_label) + + self.sidebar_layout.addSpacing(20) + + # Navigation Buttons + self.nav_buttons = [] + self.add_nav_button("File Organizer", 0) + self.add_nav_button("Duplicate Finder", 1) + self.add_nav_button("Folder Analyzer", 2) + self.add_nav_button("PDF Tools", 3) + self.add_nav_button("System Cleaner", 4) + self.add_nav_button("Batch Renamer", 5) + self.add_nav_button("Zip Manager", 6) + + self.sidebar_layout.addStretch() + + # Version/Footer + self.version_label = QLabel("v1.0.0") + self.version_label.setObjectName("VersionLabel") + self.version_label.setAlignment(Qt.AlignCenter) + self.sidebar_layout.addWidget(self.version_label) + + # Content Area + self.content_area = QStackedWidget() + + # Initialize Tools + self.file_organizer = FileOrganizerUI() + self.duplicate_finder = DuplicateFinderUI() + self.folder_analyzer = FolderAnalyzerUI() + self.pdf_tools = PDFToolsUI() + self.system_cleaner = SystemCleanerUI() + self.batch_renamer = BatchRenamerUI() + self.zip_manager = ZipManagerUI() + + # Add Tools to Stack + self.content_area.addWidget(self.file_organizer) + self.content_area.addWidget(self.duplicate_finder) + self.content_area.addWidget(self.folder_analyzer) + self.content_area.addWidget(self.pdf_tools) + self.content_area.addWidget(self.system_cleaner) + self.content_area.addWidget(self.batch_renamer) + self.content_area.addWidget(self.zip_manager) + + # Add widgets to main layout + self.main_layout.addWidget(self.sidebar) + self.main_layout.addWidget(self.content_area) + + # Apply Styles + self.apply_styles() + + def add_nav_button(self, text, index): + btn = QPushButton(text) + btn.setCheckable(True) + btn.clicked.connect(lambda: self.switch_page(index, btn)) + self.sidebar_layout.addWidget(btn) + self.nav_buttons.append(btn) + + # Select first button by default + if index == 0: + btn.setChecked(True) + + def switch_page(self, index, sender_btn): + self.content_area.setCurrentIndex(index) + + # Update button states + for btn in self.nav_buttons: + btn.setChecked(False) + sender_btn.setChecked(True) + + def apply_styles(self): + self.setStyleSheet(""" + QMainWindow { + background-color: #1e1e1e; + } + QFrame#Sidebar { + background-color: #252526; + border-right: 1px solid #333; + } + QLabel#SidebarTitle { + color: #ffffff; + font-size: 24px; + font-weight: bold; + padding: 10px; + } + QLabel#VersionLabel { + color: #888888; + font-size: 12px; + } + QPushButton { + background-color: transparent; + color: #cccccc; + border: none; + padding: 15px 20px; + text-align: left; + font-size: 14px; + border-left: 4px solid transparent; + } + QPushButton:hover { + background-color: #2a2d2e; + color: #ffffff; + } + QPushButton:checked { + background-color: #37373d; + color: #ffffff; + border-left: 4px solid #007acc; + } + QWidget { + color: #ffffff; + font-family: 'Segoe UI', sans-serif; + } + QLineEdit { + background-color: #3c3c3c; + color: #ffffff; + border: 1px solid #555; + padding: 8px; + border-radius: 4px; + } + QTextEdit { + background-color: #1e1e1e; + color: #cccccc; + border: 1px solid #333; + border-radius: 4px; + } + /* Specific styles for tool buttons inside pages */ + QPushButton[text="Browse"], QPushButton[text="Organize Files"], + QPushButton[text="Find Duplicates"], QPushButton[text="Delete Duplicates"], + QPushButton[text="Analyze Folder"], QPushButton[text="Merge PDFs"], + QPushButton[text="Split PDF"], QPushButton[text="Extract Text"], + QPushButton[text="Clean Temporary Files"] { + background-color: #0e639c; + color: white; + border-radius: 4px; + padding: 8px 16px; + border: none; + } + QPushButton[text="Browse"]:hover, QPushButton[text="Organize Files"]:hover, + QPushButton[text="Find Duplicates"]:hover, QPushButton[text="Delete Duplicates"]:hover, + QPushButton[text="Analyze Folder"]:hover, QPushButton[text="Merge PDFs"]:hover, + QPushButton[text="Split PDF"]:hover, QPushButton[text="Extract Text"]:hover, + QPushButton[text="Clean Temporary Files"]:hover { + background-color: #1177bb; + } + QPushButton:disabled { + background-color: #3a3d41; + color: #888888; + } + """) diff --git a/ui/duplicate_finder.py b/ui/duplicate_finder.py new file mode 100644 index 0000000..6a87c4f --- /dev/null +++ b/ui/duplicate_finder.py @@ -0,0 +1,85 @@ +from PyQt5.QtWidgets import ( + QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QFileDialog, QTextEdit +) +from automations.duplicate_finder import DuplicateFinder + +class DuplicateFinderUI(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("Duplicate Finder") + self.setGeometry(300, 300, 600, 400) + + # Layout + self.layout = QVBoxLayout() + + # Folder input + self.folder_label = QLabel("Folder Path:") + self.folder_input = QLineEdit() + self.browse_button = QPushButton("Browse") + self.browse_button.clicked.connect(self.browse_folder) + + # Find duplicates button + self.find_button = QPushButton("Find Duplicates") + self.find_button.clicked.connect(self.find_duplicates) + + # Delete duplicates button + self.delete_button = QPushButton("Delete Duplicates") + self.delete_button.setEnabled(False) # Disabled until duplicates are found + self.delete_button.clicked.connect(self.delete_duplicates) + + # Log area + self.log_area = QTextEdit() + self.log_area.setReadOnly(True) + + # Adding widgets to layout + self.layout.addWidget(self.folder_label) + self.layout.addWidget(self.folder_input) + self.layout.addWidget(self.browse_button) + self.layout.addWidget(self.find_button) + self.layout.addWidget(self.delete_button) + self.layout.addWidget(self.log_area) + + self.setLayout(self.layout) + + # State + self.duplicates = [] + + def browse_folder(self): + folder = QFileDialog.getExistingDirectory(self, "Select Folder") + if folder: + self.folder_input.setText(folder) + + def find_duplicates(self): + folder_path = self.folder_input.text().strip() + if not folder_path: + self.log_area.append("Error: Please specify a folder path.") + return + + result = DuplicateFinder.find_duplicates(folder_path) + + if "error" in result: + self.log_area.append(f"Error: {result['error']}") + return + + self.duplicates = result["duplicates"] + + if self.duplicates: + self.log_area.append(f"Found {len(self.duplicates)} duplicate files:") + for duplicate, original in self.duplicates: + self.log_area.append(f"- Duplicate: {duplicate}\n Original: {original}") + self.delete_button.setEnabled(True) + else: + self.log_area.append("No duplicate files found.") + + def delete_duplicates(self): + if not self.duplicates: + self.log_area.append("Error: No duplicates to delete.") + return + + deleted_files = DuplicateFinder.delete_duplicates(self.duplicates) + self.log_area.append(f"Deleted {len(deleted_files)} duplicate files:") + for file in deleted_files: + self.log_area.append(f"- {file}") + + self.duplicates = [] # Reset duplicates + self.delete_button.setEnabled(False) diff --git a/ui/file_organizer.py b/ui/file_organizer.py new file mode 100644 index 0000000..226c27d --- /dev/null +++ b/ui/file_organizer.py @@ -0,0 +1,66 @@ +from PyQt5.QtWidgets import ( + QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QFileDialog, QTextEdit +) +from automations.file_organizer import organize_files + +class FileOrganizerUI(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("File Organizer") + self.setGeometry(300, 300, 600, 400) + + # Layout + self.layout = QVBoxLayout() + + # Folder input + self.folder_label = QLabel("Folder Path:") + self.folder_input = QLineEdit() + self.browse_button = QPushButton("Browse") + self.browse_button.clicked.connect(self.browse_folder) + + # Extensions input + self.ext_label = QLabel("File Extensions (comma-separated):") + self.ext_input = QLineEdit() + + # Organize button + self.organize_button = QPushButton("Organize Files") + self.organize_button.clicked.connect(self.organize_files) + + # Log area + self.log_area = QTextEdit() + self.log_area.setReadOnly(True) + + # Adding widgets to layout + self.layout.addWidget(self.folder_label) + self.layout.addWidget(self.folder_input) + self.layout.addWidget(self.browse_button) + self.layout.addWidget(self.ext_label) + self.layout.addWidget(self.ext_input) + self.layout.addWidget(self.organize_button) + self.layout.addWidget(self.log_area) + + self.setLayout(self.layout) + + def browse_folder(self): + folder = QFileDialog.getExistingDirectory(self, "Select Folder") + if folder: + self.folder_input.setText(folder) + + def organize_files(self): + folder_path = self.folder_input.text().strip() + extensions = self.ext_input.text().strip().split(',') + extensions = [ext.strip() for ext in extensions if ext.strip()] + + if not folder_path: + self.log_area.append("Error: Please specify a folder path.") + return + + if not extensions: + self.log_area.append("Error: Please specify at least one file extension.") + return + + try: + result = organize_files(folder_path, extensions) + self.log_area.append(result) + except Exception as e: + self.log_area.append(f"An error occurred: {str(e)}") diff --git a/ui/folder_analyzer.py b/ui/folder_analyzer.py new file mode 100644 index 0000000..8a62aaf --- /dev/null +++ b/ui/folder_analyzer.py @@ -0,0 +1,55 @@ +from PyQt5.QtWidgets import ( + QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QFileDialog, QTextEdit +) +from automations.folder_analyzer import analyze_folder # Import the folder analyzer logic + +class FolderAnalyzerUI(QWidget): + def __init__(self, folder_path=None): + super().__init__() + self.setWindowTitle("Folder Analyzer") + self.setGeometry(300, 300, 250, 150) + + # Layout + self.layout = QVBoxLayout() + + # Folder input + self.folder_label = QLabel("Folder Path:") + self.folder_input = QLineEdit() + self.browse_button = QPushButton("Browse") + self.browse_button.clicked.connect(self.browse_folder) + + # Analyze button + self.analyze_button = QPushButton("Analyze Folder") + self.analyze_button.clicked.connect(self.analyze_folder) + + # Log area + self.log_area = QTextEdit() + self.log_area.setReadOnly(True) + + # Adding widgets to layout + self.layout.addWidget(self.folder_label) + self.layout.addWidget(self.folder_input) + self.layout.addWidget(self.browse_button) + self.layout.addWidget(self.analyze_button) + self.layout.addWidget(self.log_area) + + self.setLayout(self.layout) + + def browse_folder(self): + folder = QFileDialog.getExistingDirectory(self, "Select Folder") + if folder: + self.folder_input.setText(folder) + + def analyze_folder(self): + folder_path = self.folder_input.text().strip() + + if not folder_path: + self.log_area.append("Error: Please specify a folder path.") + return + + # Perform analysis and log the result + try: + result = analyze_folder(folder_path) + self.log_area.append(result) + except Exception as e: + self.log_area.append(f"Error: {str(e)}") diff --git a/ui/pdf_tools.py b/ui/pdf_tools.py new file mode 100644 index 0000000..79a879e --- /dev/null +++ b/ui/pdf_tools.py @@ -0,0 +1,66 @@ +from PyQt5.QtWidgets import ( + QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QFileDialog, QTextEdit +) +from automations.pdf_tools import merge_pdfs, split_pdfs, extract_text_from_pdf + +class PDFToolsUI(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("PDF Tools") + self.setGeometry(300, 300, 600, 400) + + self.layout = QVBoxLayout() + + # File input + self.file_label = QLabel("PDF File:") + self.file_input = QLineEdit() + self.browse_button = QPushButton("Browse") + self.browse_button.clicked.connect(self.browse_file) + + # Merge PDFs + self.merge_button = QPushButton("Merge PDFs (Coming Soon)") + self.merge_button.setEnabled(False) # Placeholder + + # Split PDFs + self.split_button = QPushButton("Split PDF") + self.split_button.clicked.connect(self.split_pdf) + + # Extract text from PDF + self.extract_text_button = QPushButton("Extract Text") + self.extract_text_button.clicked.connect(self.extract_text) + + # Log area + self.log_area = QTextEdit() + self.log_area.setReadOnly(True) + + # Add widgets to layout + self.layout.addWidget(self.file_label) + self.layout.addWidget(self.file_input) + self.layout.addWidget(self.browse_button) + self.layout.addWidget(self.merge_button) + self.layout.addWidget(self.split_button) + self.layout.addWidget(self.extract_text_button) + self.layout.addWidget(self.log_area) + + self.setLayout(self.layout) + + def browse_file(self): + file, _ = QFileDialog.getOpenFileName(self, "Select PDF File", "", "PDF Files (*.pdf)") + if file: + self.file_input.setText(file) + + def split_pdf(self): + input_file = self.file_input.text().strip() + if not input_file: + self.log_area.append("Error: Please specify a PDF file.") + return + result = split_pdfs(input_file) + self.log_area.append(result) + + def extract_text(self): + input_file = self.file_input.text().strip() + if not input_file: + self.log_area.append("Error: Please specify a PDF file.") + return + result = extract_text_from_pdf(input_file) + self.log_area.append("Extracted Text:\n" + result) diff --git a/ui/system_cleaner.py b/ui/system_cleaner.py new file mode 100644 index 0000000..038dfb7 --- /dev/null +++ b/ui/system_cleaner.py @@ -0,0 +1,58 @@ +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QPushButton, QLabel, QTextEdit, QMessageBox +) +from PyQt5.QtCore import Qt +from automations.system_cleaner import clean_temp_files, clean_cache + +class SystemCleanerUI(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("System Cleaner") + self.setGeometry(400, 300, 600, 400) + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + + # Title + title = QLabel("System Cleaner") + title.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;") + title.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + + # Description + description = QLabel("Clean temporary files and caches to free up space.") + description.setAlignment(Qt.AlignCenter) + layout.addWidget(description) + + # Clean Temp Files Button + self.clean_temp_btn = QPushButton("Clean Temporary Files") + self.clean_temp_btn.clicked.connect(self.run_clean_temp) + layout.addWidget(self.clean_temp_btn) + + # Clean Cache Button (Placeholder for now as it requires paths) + self.clean_cache_btn = QPushButton("Clean Cache (Requires Configuration)") + self.clean_cache_btn.setEnabled(False) # Disabled until we have a config UI + self.clean_cache_btn.clicked.connect(self.run_clean_cache) + layout.addWidget(self.clean_cache_btn) + + # Log Area + self.log_area = QTextEdit() + self.log_area.setReadOnly(True) + layout.addWidget(self.log_area) + + self.setLayout(layout) + + def run_clean_temp(self): + self.log_area.append("Cleaning temporary files...") + try: + result = clean_temp_files() + self.log_area.append(result) + QMessageBox.information(self, "Success", result) + except Exception as e: + self.log_area.append(f"Error: {str(e)}") + QMessageBox.critical(self, "Error", f"Failed to clean temp files: {str(e)}") + + def run_clean_cache(self): + # This would need a way to select directories + pass diff --git a/ui/zip_manager.py b/ui/zip_manager.py new file mode 100644 index 0000000..764a495 --- /dev/null +++ b/ui/zip_manager.py @@ -0,0 +1,186 @@ +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, + QLineEdit, QFileDialog, QListWidget, QTabWidget, QMessageBox +) +from PyQt5.QtCore import Qt +from automations.zip_manager import compress_files, extract_archive +import os + +class ZipManagerUI(QWidget): + def __init__(self): + super().__init__() + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + + # Title + title = QLabel("Zip & Archive Manager") + title.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;") + title.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + + # Tabs + self.tabs = QTabWidget() + self.tabs.addTab(self.create_compress_tab(), "Compress Files") + self.tabs.addTab(self.create_extract_tab(), "Extract Archive") + layout.addWidget(self.tabs) + + self.setLayout(layout) + + def create_compress_tab(self): + tab = QWidget() + layout = QVBoxLayout() + + # File List + layout.addWidget(QLabel("Selected Files/Folders:")) + self.file_list = QListWidget() + layout.addWidget(self.file_list) + + # Buttons to Add/Remove + btn_layout = QHBoxLayout() + add_file_btn = QPushButton("Add Files") + add_file_btn.setToolTip("Select individual files to compress") + add_file_btn.clicked.connect(self.add_files) + + add_folder_btn = QPushButton("Add Folder") + add_folder_btn.setToolTip("Select a whole folder to compress") + add_folder_btn.clicked.connect(self.add_folder) + + remove_btn = QPushButton("Remove Selected") + remove_btn.setToolTip("Remove selected items from the list") + remove_btn.clicked.connect(self.remove_selected) + + clear_list_btn = QPushButton("Clear List") + clear_list_btn.setToolTip("Remove all items") + clear_list_btn.clicked.connect(self.file_list.clear) + + btn_layout.addWidget(add_file_btn) + btn_layout.addWidget(add_folder_btn) + btn_layout.addWidget(remove_btn) + btn_layout.addWidget(clear_list_btn) + layout.addLayout(btn_layout) + + # Output Path + layout.addWidget(QLabel("Output Archive Path:")) + path_layout = QHBoxLayout() + self.output_zip_input = QLineEdit() + self.output_zip_input.setPlaceholderText("Save as...") + browse_btn = QPushButton("Browse") + browse_btn.clicked.connect(self.browse_save_zip) + path_layout.addWidget(self.output_zip_input) + path_layout.addWidget(browse_btn) + layout.addLayout(path_layout) + + # Compress Button + self.compress_btn = QPushButton("Create Archive") + self.compress_btn.setCursor(Qt.PointingHandCursor) + self.compress_btn.clicked.connect(self.run_compression) + self.compress_btn.setStyleSheet("background-color: #0e639c; color: white; padding: 12px; font-weight: bold; font-size: 14px; border-radius: 6px;") + layout.addWidget(self.compress_btn) + + tab.setLayout(layout) + return tab + + def create_extract_tab(self): + tab = QWidget() + layout = QVBoxLayout() + + # Archive Selection + layout.addWidget(QLabel("Select Archive to Extract:")) + archive_layout = QHBoxLayout() + self.archive_input = QLineEdit() + browse_archive_btn = QPushButton("Browse") + browse_archive_btn.clicked.connect(self.browse_archive) + archive_layout.addWidget(self.archive_input) + archive_layout.addWidget(browse_archive_btn) + layout.addLayout(archive_layout) + + # Destination Selection + layout.addWidget(QLabel("Extraction Destination:")) + dest_layout = QHBoxLayout() + self.extract_dest_input = QLineEdit() + browse_dest_btn = QPushButton("Browse") + browse_dest_btn.clicked.connect(self.browse_extract_dest) + dest_layout.addWidget(self.extract_dest_input) + dest_layout.addWidget(browse_dest_btn) + layout.addLayout(dest_layout) + + layout.addStretch() + + # Extract Button + self.extract_btn = QPushButton("Extract Here") + self.extract_btn.setCursor(Qt.PointingHandCursor) + self.extract_btn.clicked.connect(self.run_extraction) + self.extract_btn.setStyleSheet("background-color: #28a745; color: white; padding: 12px; font-weight: bold; font-size: 14px; border-radius: 6px;") + layout.addWidget(self.extract_btn) + + tab.setLayout(layout) + return tab + + # --- Compression Logic --- + def add_files(self): + files, _ = QFileDialog.getOpenFileNames(self, "Select Files") + if files: + self.file_list.addItems(files) + + def add_folder(self): + folder = QFileDialog.getExistingDirectory(self, "Select Folder") + if folder: + self.file_list.addItem(folder) + + def remove_selected(self): + for item in self.file_list.selectedItems(): + self.file_list.takeItem(self.file_list.row(item)) + + def browse_save_zip(self): + path, _ = QFileDialog.getSaveFileName(self, "Save Archive", "", "Zip Files (*.zip)") + if path: + self.output_zip_input.setText(path) + + def run_compression(self): + items = [self.file_list.item(i).text() for i in range(self.file_list.count())] + output = self.output_zip_input.text() + + if not items: + QMessageBox.warning(self, "Warning", "Please select files to compress.") + return + if not output: + QMessageBox.warning(self, "Warning", "Please specify an output path.") + return + + msg = compress_files(items, output) + if "Error" in msg: + QMessageBox.critical(self, "Error", msg) + else: + QMessageBox.information(self, "Success", msg) + self.file_list.clear() + self.output_zip_input.clear() + + # --- Extraction Logic --- + def browse_archive(self): + path, _ = QFileDialog.getOpenFileName(self, "Select Archive", "", "Archives (*.zip *.tar *.gztar *.bztar *.xztar)") + if path: + self.archive_input.setText(path) + + def browse_extract_dest(self): + folder = QFileDialog.getExistingDirectory(self, "Select Destination") + if folder: + self.extract_dest_input.setText(folder) + + def run_extraction(self): + archive = self.archive_input.text() + dest = self.extract_dest_input.text() + + if not archive: + QMessageBox.warning(self, "Warning", "Please select an archive.") + return + if not dest: + QMessageBox.warning(self, "Warning", "Please select a destination.") + return + + msg = extract_archive(archive, dest) + if "Error" in msg: + QMessageBox.critical(self, "Error", msg) + else: + QMessageBox.information(self, "Success", msg)