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 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
+
+
+## Burndown Chart
+**Sprint 2:**
+
+**Sprint 3:**
+
+
+## Technologies Used
+-
**Python**: The primary programming language used for this project.
+-
**PyQt5**: Used for creating the graphical user interface.
+-
**ONNX**: Used for the machine learning models.
+-
**pytest**: Used for testing the application.
+-
**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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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