diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..ed5b977 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,129 @@ +# version: 2.1 + +# executors: +# docker-executor: +# docker: +# - image: docker:20.10.7 + +# jobs: +# build_and_push: +# executor: docker-executor +# steps: +# # Set up Docker environment for building images +# - setup_remote_docker: +# version: 20.10.7 + +# # Check out the code from the repository +# - checkout + +# # Build the Docker image +# - run: +# name: Build Docker image +# #command: docker build -t mgallai/vision_ai:${CIRCLE_SHA1} . +# command: docker build -t $DOCKER_USER/vision_ai:${CIRCLE_SHA1} . + + +# # Log in to Docker Hub using credentials stored in CircleCI environment variables +# - run: +# name: Log in to Docker Hub +# command: echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin + +# # Push Docker image +# - run: +# name: Push Docker image +# command: docker push $DOCKER_USER/vision_ai:${CIRCLE_SHA1} + +# workflows: +# version: 2 +# build_and_push: +# jobs: +# - build_and_push: +# filters: +# branches: +# only: +# - main + +version: 2.1 + +executors: + python-executor: + docker: + - image: circleci/python:3.10 + environment: + DISPLAY: :99 + + docker-executor: + docker: + - image: docker:20.10.7 + +jobs: + + build-and-test: + executor: python-executor + steps: + - checkout + - run: + name: Install dependencies + command: | + sudo apt-get update + sudo apt-get install -y xvfb libxi-dev libxtst-dev xdotool + sudo apt-get install -y xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils + python -m venv venv + . venv/bin/activate + pip install --upgrade pip + pip install -r requirements.txt + pip install pytest-qt + pip install pytest pytest-qt PyQt5 + pip install pytest pytest-xvfb pyautogui + - run: + name: Setup PYTHONPATH + command: | + export PYTHONPATH="$PYTHONPATH:/home/circleci/project" + - run: + name: Run tests + command: | + . venv/bin/activate + xvfb-run -a pytest tests/test_AI_NumberOfOutputs.py -v + xvfb-run -a pytest tests/test_AI_PredictedCity.py -v + xvfb-run -a pytest tests/test_AI_Preprocessing.py -v + xvfb-run -a pytest tests/test_ButtonPanel.py -v + xvfb-run -a pytest tests/test_IconsExist.py -v + xvfb-run -a pytest tests/test_InformationDialog.py -v + xvfb-run -a pytest tests/test_ModelExist.py -v + xvfb-run -a pytest tests/test_SelectMethod.py -v + xvfb-run -a pytest tests/test_sort_button.py -v + - persist_to_workspace: + root: . + paths: + - ./* + + build-and-push: + executor: docker-executor + steps: + - checkout + - setup_remote_docker: + version: 20.10.7 + - attach_workspace: + at: . + - run: + name: Build Docker image + command: docker build -t $DOCKER_USER/vision_ai:${CIRCLE_SHA1} . + - run: + name: Log in to Docker Hub + command: echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin + - run: + name: Push Docker image + command: docker push $DOCKER_USER/vision_ai:${CIRCLE_SHA1} + +workflows: + version: 2 + build-test-and-push: + jobs: + - build-and-test + - build-and-push: + requires: + - build-and-test + filters: + branches: + only: + - main diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c8d5252 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,64 @@ +# Use a specific version of Miniconda as the base image +FROM continuumio/miniconda3:4.9.2 + +# Set the working directory in the container +WORKDIR /app + +# Update repository information and install necessary system libraries and Xvfb +RUN apt-get update --allow-releaseinfo-change && \ + apt-get install -y --no-install-recommends apt-utils && \ + sed -i 's|http://deb.debian.org/debian buster|http://deb.debian.org/debian oldoldstable|g' /etc/apt/sources.list && \ + sed -i 's|http://security.debian.org/debian-security buster/updates|http://security.debian.org/debian-security oldoldstable/updates|g' /etc/apt/sources.list && \ + apt-get update --allow-releaseinfo-change && \ + apt-get install -y \ + libgl1-mesa-glx \ + libglib2.0-0 \ + libxkbcommon-x11-0 \ + libxcb-glx0 \ + libxcb-keysyms1 \ + libx11-xcb1 \ + libxrender1 \ + libfontconfig1 \ + libxcomposite1 \ + libxcursor1 \ + libxdamage1 \ + libxtst6 \ + libxi6 \ + libxrandr2 \ + libxss1 \ + libxshmfence1 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-shm0 \ + libxcb-util0 \ + libxcb-xfixes0 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-xinerama0 \ + xvfb && \ + rm -rf /var/lib/apt/lists/* + +# Set environment variables for Qt +ENV QT_XCB_GL_INTEGRATION=none +ENV QT_DEBUG_PLUGINS=1 + +# Copy the environment configuration file +COPY environment.yml . + +# Create the Conda environment specified in environment.yml +RUN conda env create -f environment.yml + +# Install onnxruntime via pip +RUN conda run -n myenv pip install onnxruntime==1.8.0 + +# Make sure the environment is activated by default +SHELL ["conda", "run", "-n", "myenv", "/bin/bash", "-c"] + +# Copy the rest of the application code into the container +COPY . . + +# Define environment variable +ENV NAME World + +# Start Xvfb and run the application when the container launches +CMD ["bash", "-c", "xvfb-run -a python app.py"] diff --git a/README.md b/README.md index a787dad..55edf22 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,185 @@ # VisionAI -Image Classification Software + +![VisionAI Logo](https://i.ibb.co/sq5J35B/Screenshot-2024-06-12-094922.png) + +VisionAI is a PyQt5-based desktop application for organizing image collections using Vision AI and manual tools. Users can select folders, navigate history, sort folders, view individual images, open subfolders, and delete unwanted folders. VisionAI leverages AI to analyze and categorize images, enhancing the manual organization process and making it efficient to manage large image collections. + +## Table of Contents +- [Features](#features) +- [Installation](#installation) +- [Usage](#usage) +- [Testing](#testing) +- [Datasets](#datasets) +- [Project Structure](#project-structure) +- [Demo](#demo) +- [Burndown Chart](#burndown-chart) +- [Technologies Used](#technologies-used) +- [Contributing](#contributing) +- [License](#license) +- [Acknowledgements](#acknowledgements) + +## Features + +- **Select Folder:** Choose a directory to display its contents. +- **Navigate History:** Use back and forward buttons to navigate through folder history. +- **Sort Folders:** Sort folders by the number of images they contain. +- **View Images:** Double-click to view single images. +- **Open Folder:** Double-click to open any displayed folder. +- **Vision AI:** Automatically create albums for folders using Vision AI. +- **Manual Classification:** Manually create albums for selected images. +- **Web Demo:** View a web demo hosted on Hugging Face. +- **Rename Folder:** Rename an album folder with confirmation. +- **Delete Folder:** Delete an album folder with confirmation. +- **Information Dialog:** View instructions and information about the application. + +## Installation + +1. **Clone the repository:** + ``` + git clone https://github.com/mmgallai/VisionAI.git + cd VisionAI + ``` + +2. **Create and activate a virtual environment:** + ``` + python -m venv venv + source venv/bin/activate # On Windows, use `venv\Scripts\activate` + ``` + +3. **Install dependencies:** + ``` + pip install -r requirements.txt + ``` + +4. **Install PyQt5:** + ``` + pip install PyQt5 + ``` + +5. **Install pytest and pytest-qt for testing:** + ``` + pip install pytest pytest-qt + ``` + +## Usage + +1. **Run the application:** + ``` + python app.py + ``` + +2. **Interact with the UI:** + - Use the buttons to navigate, sort, and select folders. + - Double-click on images to view them. + - Use the "Vision AI" and "Manual" buttons to organize images. + - Use the "Delete" button to remove folders. + +## Testing + +1. **Run the tests:** + ``` + pytest tests/ + ``` + +## Datasets +1. **Link to Dataset :** [Subset of parent](https://drive.google.com/drive/folders/1Drk4mrMexkMgB0lk4JvJPYrQYq1OvzFL) +2. **Link to Parent Dataset :** [Parent dataset](https://www.kaggle.com/datasets/amaralibey/gsv-cities/data) + +## Project Structure + +``` +VisionAI/ +├── controller/ +│ ├── AI.py +│ └── Manual.py +├── view/ +│ ├── ButtonPanel.py +│ ├── ButtonStyle.py +│ ├── CloseConfirmationDialog.py +│ ├── DemoButton.py +│ ├── FolderList.py +│ ├── FrameSettings.py +│ ├── HistoryManager.py +│ ├── ImageDisplay.py +│ ├── InformationDialog.py +│ ├── InitialFolderSelection.py +│ ├── MainView.py +│ ├── UploadButton.py +│ └── SelectMethod.py +├── tests/ +│ ├── __init__.py +│ ├── test_AI_NumberOfOutputs.py +│ ├── test_AI_PredictedCity.py +│ ├── test_AI_Preprocessing.py +│ ├── test_ButtonPanel.py +│ ├── test_Delete_FolderList.py +│ ├── test_IconsExist.py +│ ├── test_ImageCount.py +│ ├── test_InformationDialog.py +│ ├── test_Manual.py +│ ├── test_ModelExist.py +│ ├── test_SelectMethod.py +│ └── test_WebDemo.py +├── test images/ +│ ├── image1.jpg +│ ├── image2.jpg +│ ├── image3.jpg +│ ├── image4.jpg +│ ├── image5.jpg +│ ├── image6.jpg +│ ├── image7.jpg +│ ├── image8.jpg +│ ├── image9.jpg +│ ├── image10.jpg +│ └── names.txt +├── icons/ +│ ├── back_icon.png +│ ├── delete_icon.png +│ ├── directories_icon.png +│ ├── folder_icon.png +│ ├── forward_icon.png +│ ├── image_icon.png +│ ├── info_icon.png +│ ├── method_icon.png +│ └── sort_icon.png +├── model/ +│ └── best.onnx +├── app.py +├── requirements.txt +└── README.md + +``` +- app.py: The main entry point for the application. + +## Demo +![Demo](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZHAydWU3MnE5dnZ4Njg4eXdzYnBkZDgwMzJnemw0Z3Z0azl1MmN0MCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/Y5rMvrTf8JABxYHAPh/giphy.gif) + +## Burndown Chart +**Sprint 2:** +![Burndown Chart 2](https://i.ibb.co/9TLgm7g/Sprint-burndown.png) +**Sprint 3:** +![Burndown Chart 3](https://i.ibb.co/gJ35qW9/Screenshot-2024-06-18-114417.png) + +## Technologies Used +- Python **Python**: The primary programming language used for this project. +- PyQt5 **PyQt5**: Used for creating the graphical user interface. +- ONNX **ONNX**: Used for the machine learning models. +- pytest **pytest**: Used for testing the application. +- GitHub **GitHub**: For version control and collaboration. + + +## Contributing + +Contributions are welcome! Please create an issue or submit a pull request for any improvements or bug fixes. + +## License + +This project is licensed under the MIT License. + +## Acknowledgements + +- This project uses PyQt5 for the graphical user interface. +- This project uses [YOLOv8](https://github.com/ultralytics/ultralytics) to organize images. +- Special thanks to user "[amaralibey](https://www.kaggle.com/amaralibey)" in kaggle for the datatset. +- Special thanks to the contributors and the open-source community for their support. + diff --git a/app.py b/app.py index 522e6bd..5e4a6c7 100644 --- a/app.py +++ b/app.py @@ -4,13 +4,19 @@ from view.InitialFolderSelection import InitialFolderSelection if __name__ == "__main__": + print("Starting application...") app = QApplication(sys.argv) + + print("Showing initial folder selection dialog...") initial_folder_dialog = InitialFolderSelection() if initial_folder_dialog.exec_() == QDialog.Accepted: + print("Dialog accepted") selected_folder = initial_folder_dialog.selected_folder window = MainWindow(initial_folder=selected_folder) window.show() + print("Showing Main Window...") sys.exit(app.exec_()) else: + print("Dialog canceled") sys.exit(0) diff --git a/controller/AI.py b/controller/AI.py index 14d2596..e9bb246 100644 --- a/controller/AI.py +++ b/controller/AI.py @@ -11,37 +11,57 @@ class AI: def __init__(self, parent, initial_directory): self.parent = parent self.initial_directory = initial_directory - self.model_path = os.path.join('model', 'best.onnx') + + # Construct model path relative to the current script's location + self.model_path = os.path.join(os.path.dirname(__file__), '..', 'model', 'best.onnx') + self.session = ort.InferenceSession(self.model_path) self.input_name = self.session.get_inputs()[0].name + self.input_shape = self.session.get_inputs()[0].shape self.class_names = ["Boston", "Chicago", "LosAngeles", "Phoenix", "WashingtonDC"] print("AI class instantiated") - def classify_image(self, image_path): - img = Image.open(image_path).resize((224, 224)) + def preprocess_image(self, image_path): + img = Image.open(image_path).convert('RGB').resize((224, 224)) img_array = np.array(img).astype(np.float32) / 255.0 # Normalize - img_array = img_array.transpose(2, 0, 1) # Convert to (C, H, W) - img_array = np.expand_dims(img_array, axis=0) # Add batch dimension + print(f"Original image array shape: {img_array.shape}") + + img_array = img_array.transpose(2, 0, 1) + print(f"Transposed image array shape: {img_array.shape}") + + img_array = np.expand_dims(img_array, axis=0) + print(f"Final image array shape (with batch): {img_array.shape}") + + return img_array + + def classify_image(self, image_path): + img_array = self.preprocess_image(image_path) + + if img_array.shape != tuple(self.input_shape): + raise ValueError(f"Input shape mismatch. Expected: {self.input_shape}, Got: {img_array.shape}") outputs = self.session.run(None, {self.input_name: img_array}) prediction = outputs[0][0] - print("outputs ", outputs) - print("prediction ", prediction) + print("Outputs: ", outputs) + print("Prediction: ", prediction) class_id = np.argmax(prediction) return self.class_names[class_id] def classify_files(self, image_paths): for image_path in image_paths: - class_name = self.classify_image(image_path) - print(class_name) - class_folder = os.path.join(self.initial_directory, class_name) - Path(class_folder).mkdir(parents=True, exist_ok=True) - new_image_path = os.path.join(class_folder, os.path.basename(image_path)) - if not os.path.exists(new_image_path): - os.rename(image_path, new_image_path) - print(f"Moved {image_path} to {new_image_path}") - else: - print(f"File {new_image_path} already exists, skipping.") + try: + class_name = self.classify_image(image_path) + print(class_name) + class_folder = os.path.join(self.initial_directory, class_name) + Path(class_folder).mkdir(parents=True, exist_ok=True) + new_image_path = os.path.join(class_folder, os.path.basename(image_path)) + if not os.path.exists(new_image_path): + os.rename(image_path, new_image_path) + print(f"Moved {image_path} to {new_image_path}") + else: + print(f"File {new_image_path} already exists, skipping.") + except Exception as e: + print(f"Error processing {image_path}: {e}") self.show_success_message("Success", "Images have been classified and moved to their respective folders.") def show_success_message(self, title, message): diff --git a/controller/__pycache__/AI.cpython-311.pyc b/controller/__pycache__/AI.cpython-311.pyc new file mode 100644 index 0000000..43d0ffd Binary files /dev/null and b/controller/__pycache__/AI.cpython-311.pyc differ diff --git a/controller/__pycache__/AI.cpython-312.pyc b/controller/__pycache__/AI.cpython-312.pyc new file mode 100644 index 0000000..a33d70e Binary files /dev/null and b/controller/__pycache__/AI.cpython-312.pyc differ diff --git a/controller/__pycache__/Manual.cpython-311.pyc b/controller/__pycache__/Manual.cpython-311.pyc new file mode 100644 index 0000000..20d2323 Binary files /dev/null and b/controller/__pycache__/Manual.cpython-311.pyc differ diff --git a/controller/__pycache__/Manual.cpython-312.pyc b/controller/__pycache__/Manual.cpython-312.pyc new file mode 100644 index 0000000..29b37e7 Binary files /dev/null and b/controller/__pycache__/Manual.cpython-312.pyc differ diff --git a/controller/__pycache__/__init__.cpython-311.pyc b/controller/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f0365a6 Binary files /dev/null and b/controller/__pycache__/__init__.cpython-311.pyc differ diff --git a/controller/__pycache__/__init__.cpython-312.pyc b/controller/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1fabdc9 Binary files /dev/null and b/controller/__pycache__/__init__.cpython-312.pyc differ diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..f4f8151 --- /dev/null +++ b/environment.yml @@ -0,0 +1,15 @@ +name: myenv +channels: + - defaults + - conda-forge +dependencies: + - python=3.9 + - numpy=1.26.4 + - pillow=10.3.0 + - pyqt=5.15.10 + - pytest=6.2.5 + - pytest-qt=3.3.0 + - libxkbcommon + - libxcb + - xorg-libx11 + - pip diff --git a/icons/Original/back_icon.png b/icons/Original/back_icon.png deleted file mode 100644 index f6a471c..0000000 Binary files a/icons/Original/back_icon.png and /dev/null differ diff --git a/icons/Original/demo_icon.png b/icons/Original/demo_icon.png deleted file mode 100644 index 3d2e680..0000000 Binary files a/icons/Original/demo_icon.png and /dev/null differ diff --git a/icons/Original/directories_icon.png b/icons/Original/directories_icon.png deleted file mode 100644 index 16da0a6..0000000 Binary files a/icons/Original/directories_icon.png and /dev/null differ diff --git a/icons/Original/folder_icon.png b/icons/Original/folder_icon.png deleted file mode 100644 index f7b40e3..0000000 Binary files a/icons/Original/folder_icon.png and /dev/null differ diff --git a/icons/Original/forward_icon.png b/icons/Original/forward_icon.png deleted file mode 100644 index 7a9893c..0000000 Binary files a/icons/Original/forward_icon.png and /dev/null differ diff --git a/icons/Original/info_icon.png b/icons/Original/info_icon.png deleted file mode 100644 index 5d2b82c..0000000 Binary files a/icons/Original/info_icon.png and /dev/null differ diff --git a/icons/Original/noun-gear-45180.svg b/icons/Original/noun-gear-45180.svg deleted file mode 100644 index 6ce59b1..0000000 --- a/icons/Original/noun-gear-45180.svg +++ /dev/null @@ -1 +0,0 @@ -Created by Vasil Enchevfrom the Noun Project \ No newline at end of file diff --git a/icons/Original/noun-image-6802419.svg b/icons/Original/noun-image-6802419.svg deleted file mode 100644 index 4f6ef77..0000000 --- a/icons/Original/noun-image-6802419.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - -Created by Riyan Resdianfrom Noun Project \ No newline at end of file diff --git a/icons/Original/noun-web-6834408.svg b/icons/Original/noun-web-6834408.svg deleted file mode 100644 index 1ee3d29..0000000 --- a/icons/Original/noun-web-6834408.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -Created by Aswell Studiofrom Noun Project \ No newline at end of file diff --git a/main b/main new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2dfd430 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +numpy==1.26.4 +Pillow==10.3.0 +pyqt5==5.15.10 +pyqt5-qt5==5.15.2 +pyqt5-sip==12.13.0 +pytest==8.2.2 +pytest-qt==4.4.0 + +# Windows-specific dependency +pywin32==306; sys_platform == 'win32' + +onnxruntime==1.18.0 diff --git a/tests/test_AI_NumberOfOutputs.py b/tests/test_AI_NumberOfOutputs.py index c326d9c..810c236 100644 --- a/tests/test_AI_NumberOfOutputs.py +++ b/tests/test_AI_NumberOfOutputs.py @@ -21,7 +21,9 @@ def setUp(self): self.select_method = SelectMethod(self.window) def test_AI_Method_NumberOfOutputs(self): + # Ensure the model path is correct and the file exists ai_instance = AI(self.window, self.window.initial_directory) + self.assertTrue(os.path.isfile(ai_instance.model_path), f"Model file does not exist: {ai_instance.model_path}") # Construct the path to the test image dynamically script_dir = os.path.dirname(__file__) @@ -36,8 +38,8 @@ def test_AI_Method_NumberOfOutputs(self): # Print prediction for debugging print(f"Prediction: {prediction}") - # Verify the prediction has 5 elements - self.assertEqual(len(prediction), 6, "The prediction should have 5 elements") ## one is dtype=float32 + # Verify the prediction length + self.assertEqual(len(prediction), 6, "The prediction should have 6 elements") # Adjust if needed @classmethod def tearDownClass(cls): diff --git a/tests/test_AI_PredictedCity.py b/tests/test_AI_PredictedCity.py index 3f4c466..905093d 100644 --- a/tests/test_AI_PredictedCity.py +++ b/tests/test_AI_PredictedCity.py @@ -44,4 +44,4 @@ def tearDownClass(cls): del cls.app if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/tests/test_AI_Preprocessing.py b/tests/test_AI_Preprocessing.py index 9dd935a..62958b4 100644 --- a/tests/test_AI_Preprocessing.py +++ b/tests/test_AI_Preprocessing.py @@ -47,5 +47,9 @@ def test_preprocessing(self, mock_open, mock_run): # Check if preprocessing is done correctly np.testing.assert_array_almost_equal(expected_img_array, actual_img_array, decimal=5, err_msg="Preprocessing not done correctly") + @classmethod + def tearDownClass(cls): + del cls.ai + if __name__ == '__main__': unittest.main() diff --git a/tests/test_ButtonPanel.py b/tests/test_ButtonPanel.py index 47fb85a..8a5aeb5 100644 --- a/tests/test_ButtonPanel.py +++ b/tests/test_ButtonPanel.py @@ -32,4 +32,4 @@ def find_buttons_in_layout(self, layout): return buttons if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/tests/test_IconsExist.py b/tests/test_IconsExist.py index ca21404..bd6df1b 100644 --- a/tests/test_IconsExist.py +++ b/tests/test_IconsExist.py @@ -24,4 +24,4 @@ def test_icons_exist(self): self.assertTrue(os.path.isfile(icon_path), f"Icon file {icon} does not exist") if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/tests/test_ImageCount.py b/tests/test_ImageCount.py deleted file mode 100644 index c374345..0000000 --- a/tests/test_ImageCount.py +++ /dev/null @@ -1,47 +0,0 @@ -import unittest -import os -import sys -from PyQt5.QtWidgets import QApplication - -# Ensure the root directory is in the PYTHONPATH -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from view.MainView import MainWindow - -class TestImageCount(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.app = QApplication([]) - - @classmethod - def tearDownClass(cls): - cls.app.quit() - - def setUp(self): - self.main_window = MainWindow(initial_folder='test images') - self.main_window.show() - - # Override closeEvent to avoid asking for confirmation - self.main_window.closeEvent = lambda event: event.accept() - - def tearDown(self): - self.main_window.close() - - def test_image_count(self): - # Update the view with the test_images folder path - test_folder = 'test images' - self.main_window.update_view(test_folder) - - # Get the displayed image count from the label - actual_image_count_label = self.main_window.button_panel.image_count_label.text() - actual_image_count = int(actual_image_count_label.split(': ')[1]) - - # Get the expected image count by counting the image files in the test_images folder - image_files = [f for f in os.listdir(test_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp'))] - expected_image_count = len(image_files) - - # Verify the image count is correct - self.assertEqual(actual_image_count, expected_image_count) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_InformationDialog.py b/tests/test_InformationDialog.py index b8b0a7c..ef16b9c 100644 --- a/tests/test_InformationDialog.py +++ b/tests/test_InformationDialog.py @@ -78,4 +78,4 @@ def test_ok_button_functionality(self): self.assertFalse(self.dialog.isVisible()) if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/tests/test_WebDemo.py b/tests/test_WebDemo.py deleted file mode 100644 index 89364ab..0000000 --- a/tests/test_WebDemo.py +++ /dev/null @@ -1,38 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock -from PyQt5.QtWidgets import QApplication -from PyQt5.QtCore import Qt -import sys -import os - -# Ensure the root directory is in the PYTHONPATH -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from view.MainView import MainWindow - -class TestMainWindow(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.app = QApplication([]) - - def setUp(self): - self.main_window = MainWindow(initial_folder='test images') - - # Override closeEvent to avoid asking for confirmation during tests - self.main_window.closeEvent = lambda event: event.accept() - - self.main_window.show() - - def tearDown(self): - self.main_window.close() - - @patch('webbrowser.open') - def test_open_demo(self, mock_open): - # Call the open_demo method - self.main_window.open_demo() - - # Check if webbrowser.open was called with the correct URL - mock_open.assert_called_once_with("https://huggingface.co/spaces/jagruthh/cities_small") - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_folder_list.py b/tests/test_folder_list.py deleted file mode 100644 index 6845fbb..0000000 --- a/tests/test_folder_list.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import sys -import unittest -from unittest.mock import MagicMock, patch - -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox # Import QMessageBox directly -from PyQt5.QtWidgets import QWidget - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from view.FolderList import FolderList - - -class TestFolderList(unittest.TestCase): - - def setUp(self): - self.folder_list = FolderList(MockParent()) - - @patch('PyQt5.QtWidgets.QMessageBox.question', return_value=QMessageBox.Yes) - @patch('PyQt5.QtWidgets.QMessageBox.information') - @patch('shutil.rmtree') - def test_delete_folder_confirmation_yes(self, mock_rmtree, mock_information, mock_question): - # Mock folder_path and parent - folder_path = '/path/to/folder' - self.folder_list.parent = MagicMock() - - # Call delete_folder method - self.folder_list.delete_folder(folder_path) - - # Assertions - mock_question.assert_called_once() - mock_rmtree.assert_called_once_with(folder_path) - mock_information.assert_called_once_with(self.folder_list, 'Success', 'Folder deleted successfully!') - - @patch('PyQt5.QtWidgets.QMessageBox.question', return_value=QMessageBox.No) - @patch('PyQt5.QtWidgets.QMessageBox.information') - @patch('shutil.rmtree') - def test_delete_folder_confirmation_no(self, mock_rmtree, mock_information, mock_question): - # Mock folder_path and parent - folder_path = '/path/to/folder' - self.folder_list.parent = MagicMock() - - # Call delete_folder method - self.folder_list.delete_folder(folder_path) - - # Assertions - mock_question.assert_called_once() - mock_rmtree.assert_not_called() - mock_information.assert_not_called() - - @patch('PyQt5.QtWidgets.QMessageBox.question', return_value=QMessageBox.Yes) - @patch('PyQt5.QtWidgets.QMessageBox.warning') - @patch('shutil.rmtree', side_effect=Exception('Test error')) - - def test_delete_folder_error(self, mock_rmtree, mock_warning, mock_question): - # Mock folder_path and parent - folder_path = '/path/to/folder' - self.folder_list.parent = MagicMock() - - # Call delete_folder method - self.folder_list.delete_folder(folder_path) - - # Assertions - mock_question.assert_called_once() - mock_rmtree.assert_called_once_with(folder_path) - mock_warning.assert_called_once() - - -class MockParent(QWidget): - def __init__(self): - super().__init__() - self.central_widget = MagicMock() - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/test_manual.py b/tests/test_manual.py deleted file mode 100644 index 184c644..0000000 --- a/tests/test_manual.py +++ /dev/null @@ -1,50 +0,0 @@ -import os -import sys -from pathlib import Path - -import pytest -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QDialog, QLineEdit, QPushButton -from pytestqt import qtbot - -# Add the project root to the sys.path -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from controller.Manual import Manual - - -def test_handle_new_album_creation_existing_file(qtbot, tmpdir): - initial_directory = Path(tmpdir.mkdir("images")) - - image_paths = [initial_directory / "image1.jpg", initial_directory / "image2.png"] - for image_path in image_paths: - image_path.write_text("dummy data", encoding='utf-8') - - album_name = "New Album" - album_path = initial_directory / album_name - album_path.mkdir() - existing_file_path = album_path / "image1.jpg" - existing_file_path.write_text("existing file", encoding='utf-8') - - parent = QDialog() - parent.update_view = lambda x: None # Mock the update_view method - album_dialog = QDialog() - - qtbot.addWidget(parent) - qtbot.addWidget(album_dialog) - - album_input = QLineEdit(album_dialog) - album_input.setText(album_name) - - manual_instance = Manual(parent, str(initial_directory)) - - manual_instance.handle_new_album_creation(album_input, [str(p) for p in image_paths], album_dialog) - - assert existing_file_path.read_text(encoding='utf-8') == "existing file" - - new_file_path = album_path / "image2.png" - assert new_file_path.exists() - - qtbot.mouseClick(album_dialog.findChild(QPushButton, "Create"), Qt.LeftButton) - -if __name__ == '__main__': - pytest.main(["-v", __file__]) diff --git a/tests/test_sort_button.py b/tests/test_sort_button.py new file mode 100644 index 0000000..deb2168 --- /dev/null +++ b/tests/test_sort_button.py @@ -0,0 +1,95 @@ +import sys +import unittest +from unittest.mock import MagicMock + +from PyQt5.QtWidgets import QApplication, QMainWindow + +# Mock classes to represent the application structure +class FolderList: + def __init__(self): + self.folders = [] + self.images = [] + + def add_folder(self, folder_name): + self.folders.append(folder_name) + + def add_image(self, image_name): + self.images.append(image_name) + + def sort_folders(self, order="ascending", case_sensitive=True): + if not case_sensitive: + self.folders.sort(key=lambda x: x.lower()) + self.images.sort(key=lambda x: x.lower()) + else: + self.folders.sort() + self.images.sort() + + if order == "descending": + self.folders.reverse() + self.images.reverse() + +class ButtonPanel: + def __init__(self, parent): + self.sort_button = MagicMock() + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.folder_list = FolderList() + self.button_panel = ButtonPanel(self) + +class TestSortButton(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.app = QApplication(sys.argv) + + def setUp(self): + self.main_window = MainWindow() + self.button_panel = self.main_window.button_panel + self.folder_list = self.main_window.folder_list + + # Adding mock folders and images + self.folder_list.add_folder('FolderC') + self.folder_list.add_folder('FolderA') + self.folder_list.add_folder('FolderB') + self.folder_list.add_image('ImageC.jpg') + self.folder_list.add_image('ImageA.jpg') + self.folder_list.add_image('ImageB.jpg') + + def test_sort_button(self): + # Simulate a click on the sort button + self.folder_list.sort_folders() + + # Check if folders and images are sorted + expected_folders = ['FolderA', 'FolderB', 'FolderC'] + expected_images = ['ImageA.jpg', 'ImageB.jpg', 'ImageC.jpg'] + + self.assertEqual(self.folder_list.folders, expected_folders) + self.assertEqual(self.folder_list.images, expected_images) + + def test_sort_ascending(self): + # Additional test case for ascending order sorting + self.folder_list.add_folder('') + self.folder_list.add_image('') + self.folder_list.sort_folders(order="ascending") + + expected_folders = ['', 'FolderA', 'FolderB', 'FolderC'] + expected_images = ['', 'ImageA.jpg', 'ImageB.jpg', 'ImageC.jpg'] + + self.assertEqual(self.folder_list.folders, expected_folders) + self.assertEqual(self.folder_list.images, expected_images) + + def test_sort_empty_name(self): + # Additional test case for handling empty folder/image names + self.folder_list.add_folder('') + self.folder_list.add_image('') + self.folder_list.sort_folders(order="ascending") + + expected_folders = ['', 'FolderA', 'FolderB', 'FolderC'] + expected_images = ['', 'ImageA.jpg', 'ImageB.jpg', 'ImageC.jpg'] + + self.assertEqual(self.folder_list.folders, expected_folders) + self.assertEqual(self.folder_list.images, expected_images) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/view/ButtonPanel.py b/view/ButtonPanel.py index 23fe5d8..bb818f6 100644 --- a/view/ButtonPanel.py +++ b/view/ButtonPanel.py @@ -37,8 +37,8 @@ def create_buttons(self): self.add_button_with_label("Select Method", "icons/method_icon.png", self.parent.open_select_method) self.add_button_with_label("Web Demo", "icons/demo_icon.png", self.parent.open_demo) self.add_button_with_label("Sort", "icons/sort_icon.png", self.parent.folder_list.sort_albums) - self.add_button_with_label("Information", "icons/info_icon.png", self.parent.show_information) self.add_button_with_label("Delete", "icons/delete_icon.png", self.parent.confirm_delete) + self.add_button_with_label("Information", "icons/info_icon.png", self.parent.show_information) self.layout.addStretch() def add_button_with_label(self, text, icon_path, callback): @@ -75,20 +75,6 @@ def add_button_with_label(self, text, icon_path, callback): button_widget.setLayout(button_layout) self.layout.addWidget(button_widget) return button - - def confirm_delete(self): - folder_path = self.parent.folder_list.selected_folder_path - if folder_path: - reply = QMessageBox.question(self.parent, 'Confirmation', 'Are you sure you want to delete the album folder?', - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) - if reply == QMessageBox.Yes: - try: - shutil.rmtree(folder_path) - QMessageBox.information(self.parent, 'Success', 'Folder deleted successfully!') - except Exception as e: - QMessageBox.warning(self.parent, 'Error', f'An error occurred: {str(e)}') - else: - QMessageBox.warning(self.parent, 'Warning', 'Please select a folder to delete.') def update_navigation_buttons(self, can_go_back, can_go_forward): diff --git a/view/DeleteConfirmationDialog.py b/view/DeleteConfirmationDialog.py new file mode 100644 index 0000000..3c26caa --- /dev/null +++ b/view/DeleteConfirmationDialog.py @@ -0,0 +1,32 @@ +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout +from PyQt5.QtCore import Qt +from view.ButtonStyle import ButtonStyle + +class DeleteConfirmationDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Delete Confirmation") + self.setStyleSheet("background-color: #2b2b2b; color: white; font-family: Consolas; font-size: 18px;") + self.setFixedSize(400, 200) + + layout = QVBoxLayout(self) + + label = QLabel("Are you sure you want to delete the album folder?", self) + label.setAlignment(Qt.AlignCenter) + label.setWordWrap(True) + label.setStyleSheet("font-size: 20px; font-family: Consolas;") + layout.addWidget(label) + + button_layout = QHBoxLayout() + + yes_button = QPushButton("Yes", self) + yes_button.setStyleSheet(ButtonStyle.get_default_style()) + yes_button.clicked.connect(self.accept) + button_layout.addWidget(yes_button) + + no_button = QPushButton("No", self) + no_button.setStyleSheet(ButtonStyle.get_default_style()) + no_button.clicked.connect(self.reject) + button_layout.addWidget(no_button) + + layout.addLayout(button_layout) \ No newline at end of file diff --git a/view/DemoButton.py b/view/DemoButton.py new file mode 100644 index 0000000..c1e7208 --- /dev/null +++ b/view/DemoButton.py @@ -0,0 +1,34 @@ +from PyQt5.QtWidgets import QPushButton, QWidget, QVBoxLayout +from PyQt5.QtCore import Qt +import webbrowser + +class DemoButton(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.button = QPushButton("Try Web Demo", self) + self.button.setStyleSheet( + """ + QPushButton { + border: 4px solid #3EB489; /* Mint color */ + color: white; + font-family: 'shanti'; + font-size: 36px; + border-radius: 25px; + padding: 15px 30px; /* Increased padding for a larger button */ + background-color: transparent; /* Transparent background */ + } + QPushButton:hover { + background-color: #3EB489; /* Mint color on hover */ + } + """ + ) + + self.layout = QVBoxLayout(self) + self.layout.addWidget(self.button, alignment=Qt.AlignCenter) + self.setLayout(self.layout) + + self.button.clicked.connect(self.try_demo) + + def try_demo(self): + webbrowser.open("https://huggingface.co/spaces/jagruthh/cities_small") + print("Web demo initiated") diff --git a/view/FolderList.py b/view/FolderList.py index 1052cc2..01c457f 100644 --- a/view/FolderList.py +++ b/view/FolderList.py @@ -4,9 +4,9 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import (QFileDialog, QListWidget, QListWidgetItem, - QMessageBox) - + QMessageBox, QDialog, QInputDialog, QMenu, QAction) +from view.DeleteConfirmationDialog import DeleteConfirmationDialog class FolderList(QListWidget): def __init__(self, parent): super().__init__(parent.central_widget) @@ -58,14 +58,13 @@ def select_folder(self): self.parent.update_image_count_label(folder_path) def delete_folder(self, folder_path): - reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to delete the album folder?', - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) - if reply == QMessageBox.Yes: + dialog = DeleteConfirmationDialog(self) + if dialog.exec_() == QDialog.Accepted: if folder_path: try: shutil.rmtree(folder_path) QMessageBox.information(self, 'Success', 'Folder deleted successfully!') - # Optionally, you can update the view after deletion + # Update the view after deletion self.load_folders_and_images(os.path.dirname(folder_path)) except Exception as e: QMessageBox.warning(self, 'Error', f'An error occurred while deleting the folder: {str(e)}') @@ -75,3 +74,51 @@ def delete_folder(self, folder_path): def sort_albums(self): current_directory = self.parent.history_manager.current_directory() self.load_folders_and_images(current_directory, sort=True) + + def contextMenuEvent(self, event): + item = self.itemAt(event.pos()) + if item: + context_menu = QMenu(self) + + rename_action = QAction("Rename Folder", self) + rename_action.triggered.connect(lambda: self.rename_folder(item)) + + delete_action = QAction("Delete Folder", self) + delete_action.triggered.connect(lambda: self.delete_folder_from_context_menu(item)) + + context_menu.addAction(rename_action) + context_menu.addAction(delete_action) + + context_menu.exec_(event.globalPos()) + + def delete_folder_from_context_menu(self, item): + folder_path = item.data(Qt.UserRole) + if os.path.isdir(folder_path): + self.parent.confirm_delete(folder_path) + else: + QMessageBox.warning(self, 'Error', 'Selected item is not a folder.') + + def rename_folder(self, item): + folder_path = item.data(Qt.UserRole) + if os.path.isdir(folder_path): + new_name, ok = QInputDialog.getText(self, 'Rename Folder', 'Enter new folder name:') + if ok and new_name: + new_folder_path = os.path.join(os.path.dirname(folder_path), new_name) + try: + os.rename(folder_path, new_folder_path) + QMessageBox.information(self, 'Success', 'Folder renamed successfully!') + + # Update the folder list to reflect the new name + self.load_folders_and_images(os.path.dirname(new_folder_path)) + except Exception as e: + QMessageBox.warning(self, 'Error', f'An error occurred while renaming the folder: {str(e)}') + else: + QMessageBox.warning(self, 'Error', 'Selected item is not a folder.') + + def mousePressEvent(self, event): + super().mousePressEvent(event) + item = self.itemAt(event.pos()) + if item: + self.selected_folder_path = item.data(Qt.UserRole) + + \ No newline at end of file diff --git a/view/MainView.py b/view/MainView.py index 117ebf1..1fbd741 100644 --- a/view/MainView.py +++ b/view/MainView.py @@ -14,6 +14,7 @@ from view.InformationDialog import \ InformationDialog # Import InformationDialog from view.SelectMethod import SelectMethod +from view.DeleteConfirmationDialog import DeleteConfirmationDialog class MainWindow(QMainWindow): @@ -69,20 +70,27 @@ def open_select_method(self): popup = SelectMethod(self) # Updated class name popup.exec_() - def confirm_delete(self): - reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to delete the album folder?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) - if reply == QMessageBox.Yes: + def confirm_delete(self, folder_path=None): + if not folder_path: folder_path = self.folder_list.selected_folder_path - if folder_path: - try: - shutil.rmtree(folder_path) - QMessageBox.information(self, 'Success', 'Folder deleted successfully!') - # Optionally, you can update the view after deletion - self.folder_list.load_folders_and_images(self.initial_directory) - except Exception as e: - QMessageBox.warning(self, 'Error', f'An error occurred while deleting the folder: {str(e)}') + + if folder_path: + dialog = DeleteConfirmationDialog(self) + if dialog.exec_() == QDialog.Accepted: + if os.path.isdir(folder_path): + try: + shutil.rmtree(folder_path) + QMessageBox.information(self, 'Success', 'Folder deleted successfully!') + # Update the view after deletion + parent_folder = os.path.dirname(folder_path) + self.folder_list.load_folders_and_images(parent_folder) + self.update_view(parent_folder) + except Exception as e: + QMessageBox.warning(self, 'Error', f'An error occurred while deleting the folder: {str(e)}') + else: + QMessageBox.warning(self, 'Error', 'Selected item is not a folder.') else: - return + QMessageBox.warning(self, 'Error', 'No folder selected.') def open_demo(self): webbrowser.open("https://huggingface.co/spaces/jagruthh/cities_small") diff --git a/view/__pycache__/ButtonPanel.cpython-311.pyc b/view/__pycache__/ButtonPanel.cpython-311.pyc new file mode 100644 index 0000000..26f2f9c Binary files /dev/null and b/view/__pycache__/ButtonPanel.cpython-311.pyc differ diff --git a/view/__pycache__/ButtonPanel.cpython-312.pyc b/view/__pycache__/ButtonPanel.cpython-312.pyc new file mode 100644 index 0000000..362371a Binary files /dev/null and b/view/__pycache__/ButtonPanel.cpython-312.pyc differ diff --git a/view/__pycache__/ButtonStyle.cpython-311.pyc b/view/__pycache__/ButtonStyle.cpython-311.pyc new file mode 100644 index 0000000..b07be1f Binary files /dev/null and b/view/__pycache__/ButtonStyle.cpython-311.pyc differ diff --git a/view/__pycache__/ButtonStyle.cpython-312.pyc b/view/__pycache__/ButtonStyle.cpython-312.pyc new file mode 100644 index 0000000..f711a68 Binary files /dev/null and b/view/__pycache__/ButtonStyle.cpython-312.pyc differ diff --git a/view/__pycache__/CloseConfirmationDialog.cpython-311.pyc b/view/__pycache__/CloseConfirmationDialog.cpython-311.pyc new file mode 100644 index 0000000..dfb0c3a Binary files /dev/null and b/view/__pycache__/CloseConfirmationDialog.cpython-311.pyc differ diff --git a/view/__pycache__/CloseConfirmationDialog.cpython-312.pyc b/view/__pycache__/CloseConfirmationDialog.cpython-312.pyc new file mode 100644 index 0000000..b512984 Binary files /dev/null and b/view/__pycache__/CloseConfirmationDialog.cpython-312.pyc differ diff --git a/view/__pycache__/DeleteConfirmationDialog.cpython-311.pyc b/view/__pycache__/DeleteConfirmationDialog.cpython-311.pyc new file mode 100644 index 0000000..4782e8c Binary files /dev/null and b/view/__pycache__/DeleteConfirmationDialog.cpython-311.pyc differ diff --git a/view/__pycache__/DeleteConfirmationDialog.cpython-312.pyc b/view/__pycache__/DeleteConfirmationDialog.cpython-312.pyc new file mode 100644 index 0000000..28b3032 Binary files /dev/null and b/view/__pycache__/DeleteConfirmationDialog.cpython-312.pyc differ diff --git a/view/__pycache__/FolderList.cpython-311.pyc b/view/__pycache__/FolderList.cpython-311.pyc new file mode 100644 index 0000000..ca37572 Binary files /dev/null and b/view/__pycache__/FolderList.cpython-311.pyc differ diff --git a/view/__pycache__/FolderList.cpython-312.pyc b/view/__pycache__/FolderList.cpython-312.pyc new file mode 100644 index 0000000..7336594 Binary files /dev/null and b/view/__pycache__/FolderList.cpython-312.pyc differ diff --git a/view/__pycache__/FrameSettings.cpython-311.pyc b/view/__pycache__/FrameSettings.cpython-311.pyc new file mode 100644 index 0000000..6bb99c5 Binary files /dev/null and b/view/__pycache__/FrameSettings.cpython-311.pyc differ diff --git a/view/__pycache__/FrameSettings.cpython-312.pyc b/view/__pycache__/FrameSettings.cpython-312.pyc new file mode 100644 index 0000000..5bbb1db Binary files /dev/null and b/view/__pycache__/FrameSettings.cpython-312.pyc differ diff --git a/view/__pycache__/HistoryManager.cpython-311.pyc b/view/__pycache__/HistoryManager.cpython-311.pyc new file mode 100644 index 0000000..68ee212 Binary files /dev/null and b/view/__pycache__/HistoryManager.cpython-311.pyc differ diff --git a/view/__pycache__/HistoryManager.cpython-312.pyc b/view/__pycache__/HistoryManager.cpython-312.pyc new file mode 100644 index 0000000..05d1e14 Binary files /dev/null and b/view/__pycache__/HistoryManager.cpython-312.pyc differ diff --git a/view/__pycache__/ImageDisplay.cpython-311.pyc b/view/__pycache__/ImageDisplay.cpython-311.pyc new file mode 100644 index 0000000..6ed7a61 Binary files /dev/null and b/view/__pycache__/ImageDisplay.cpython-311.pyc differ diff --git a/view/__pycache__/ImageDisplay.cpython-312.pyc b/view/__pycache__/ImageDisplay.cpython-312.pyc new file mode 100644 index 0000000..8c46062 Binary files /dev/null and b/view/__pycache__/ImageDisplay.cpython-312.pyc differ diff --git a/view/__pycache__/InformationDialog.cpython-311.pyc b/view/__pycache__/InformationDialog.cpython-311.pyc new file mode 100644 index 0000000..e813e7b Binary files /dev/null and b/view/__pycache__/InformationDialog.cpython-311.pyc differ diff --git a/view/__pycache__/InformationDialog.cpython-312.pyc b/view/__pycache__/InformationDialog.cpython-312.pyc new file mode 100644 index 0000000..79bc54a Binary files /dev/null and b/view/__pycache__/InformationDialog.cpython-312.pyc differ diff --git a/view/__pycache__/InitialFolderSelection.cpython-311.pyc b/view/__pycache__/InitialFolderSelection.cpython-311.pyc new file mode 100644 index 0000000..aca0747 Binary files /dev/null and b/view/__pycache__/InitialFolderSelection.cpython-311.pyc differ diff --git a/view/__pycache__/InitialFolderSelection.cpython-312.pyc b/view/__pycache__/InitialFolderSelection.cpython-312.pyc new file mode 100644 index 0000000..89bd4e7 Binary files /dev/null and b/view/__pycache__/InitialFolderSelection.cpython-312.pyc differ diff --git a/view/__pycache__/MainView.cpython-311.pyc b/view/__pycache__/MainView.cpython-311.pyc new file mode 100644 index 0000000..3579ae6 Binary files /dev/null and b/view/__pycache__/MainView.cpython-311.pyc differ diff --git a/view/__pycache__/MainView.cpython-312.pyc b/view/__pycache__/MainView.cpython-312.pyc new file mode 100644 index 0000000..d4f546a Binary files /dev/null and b/view/__pycache__/MainView.cpython-312.pyc differ diff --git a/view/__pycache__/SelectMethod.cpython-311.pyc b/view/__pycache__/SelectMethod.cpython-311.pyc new file mode 100644 index 0000000..bf76213 Binary files /dev/null and b/view/__pycache__/SelectMethod.cpython-311.pyc differ diff --git a/view/__pycache__/SelectMethod.cpython-312.pyc b/view/__pycache__/SelectMethod.cpython-312.pyc new file mode 100644 index 0000000..b4ebd3a Binary files /dev/null and b/view/__pycache__/SelectMethod.cpython-312.pyc differ diff --git a/view/__pycache__/__init__.cpython-311.pyc b/view/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..e845e91 Binary files /dev/null and b/view/__pycache__/__init__.cpython-311.pyc differ diff --git a/view/__pycache__/__init__.cpython-312.pyc b/view/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..705d1f8 Binary files /dev/null and b/view/__pycache__/__init__.cpython-312.pyc differ