class Albums(QWidget): def __init__(self): super().__init__() self.clear_selected = QPushButton('Clear Selected Items') self.import_files = QPushButton('Import 0 Selected Items') self.edit_album_button = QPushButton('Edit Album') self.path = QLineEdit() self.import_flow = FlowLayout(self, 1, 1) self.scroll_widget = MouseFlowWidget(self.import_flow) self.sort_container = QVBoxLayout() self.album_desc = QLabel('No Album Selected') self.sort_container.addWidget(self.album_desc) # Albums self.loaded_albums = [] self.selected_album = None # Mirror array for indexing files/folders selected using import_flow/flow without casting issues self.loaded_images = [] self.selected_album_mirror = [] # Stores selected files and directories self.selected_files = [] self.selected_album_files = [] # Album management layout self.container = QVBoxLayout() self.flow = FlowLayout(self.container.parent(), 1, 1) # GUI self.album_list = QListWidget() self.remove_album_button = QPushButton('Remove Album') main_layout = QVBoxLayout() main_layout.addLayout(self.init_gui()) # File group file = QGroupBox('File') file_controls = QHBoxLayout() file.setLayout(file_controls) reload_photos = QPushButton('Refresh Photos') reload_photos.setToolTip('Refreshes all photos to full resolution') reload_photos.clicked.connect(self.refresh_photos) file_controls.addWidget(reload_photos) save = QPushButton('Save') save.setToolTip('Saves all albums') save.clicked.connect(self.save_albums) file_controls.addWidget(save) rescan = QPushButton('Rescan for Albums') rescan.setToolTip( 'Scans for albums in the data directory (' + appdirs.user_data_dir('PhotoUtilities', 'JackHogan') + ')') rescan.clicked.connect(self.rescan_albums) file_controls.addWidget(rescan) import_album = QPushButton('Import') import_album.clicked.connect(self.import_fat) import_album.setToolTip( 'Imports an album from a selected .jfatalbum file to a selected location' ) file_controls.addWidget(import_album) self.export_album = QPushButton('Export') self.export_album.setEnabled(False) self.export_album.clicked.connect(self.export_fat) self.export_album.setToolTip('Exports an album to a .jfatalbum') file_controls.addWidget(self.export_album) self.recover_album = QPushButton('Recover Album') self.recover_album.setToolTip( 'Attempts rebuild of selected album using a selected album') self.recover_album.clicked.connect(self.recover_current_album) self.recover_album.setEnabled(False) file_controls.addWidget(self.recover_album) main_layout.addWidget(file) # Remove all selected items from album self.remove_from_album = QPushButton('Remove Selected From Album') self.remove_from_album.setEnabled(False) self.remove_from_album.clicked.connect( self.remove_selected_album_items) # Only can be used when 1 item is selected self.open_path = QPushButton('Open Path') self.open_path.clicked.connect(self.open_selected_path) self.open_path.setEnabled(False) # Photo viewer album_widget = MouseFlowWidget(self.flow) album_widget.mouse_down.connect(self.album_flow_mouse_down) album_widget.setLayout(self.flow) album_scroll = QScrollArea() album_scroll.setWidget(album_widget) album_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) album_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) album_scroll.setWidgetResizable(True) self.container.addWidget(album_scroll) self.container.addWidget(self.open_path) self.container.addWidget(self.remove_from_album) self.setLayout(main_layout) # Check data save_data = check_save_data() if save_data: self.loaded_albums = save_data self.refresh_list() self.sort_container.addLayout(self.update_album_layout()) self.show() def init_gui(self): main_layout = QHBoxLayout() album_group = QGroupBox('Albums') album_group.setMaximumWidth(250) self.album_list.setMaximumWidth(250) main_layout.addWidget(album_group) sort_group = QGroupBox('Album Contents') sort_group.setLayout(self.sort_container) main_layout.addWidget(sort_group) import_group = QGroupBox('Other Photos') main_layout.addWidget(import_group) list_layout = QVBoxLayout() list_layout.addWidget(self.album_list) self.album_list.clicked.connect(self.get_selected_item) add_album = QPushButton('Add Album') add_album.clicked.connect(self.add_new_album) list_layout.addWidget(add_album) self.edit_album_button.setEnabled(False) self.edit_album_button.clicked.connect(self.edit_selected_album) list_layout.addWidget(self.edit_album_button) list_layout.addWidget(self.remove_album_button) self.remove_album_button.setEnabled(False) self.remove_album_button.clicked.connect(self.remove_album) album_group.setLayout(list_layout) import_layout = QVBoxLayout() path_finder = QHBoxLayout() go_up = QPushButton('↑') go_up.clicked.connect(self.go_up_dir) go_up.setMaximumWidth(25) path_finder.addWidget(go_up) self.path.setText(str(Path.home())) self.update_path() self.path.textChanged.connect(self.update_path) path_finder.addWidget(self.path) dialog = QPushButton('Choose...') dialog.clicked.connect(self.choose_path) path_finder.addWidget(dialog) import_layout.addLayout(path_finder) # Contains the whole thing scroll_container = QVBoxLayout() # Contains FlowLayout self.scroll_widget.mouse_down.connect(self.import_flow_mouse_down) self.scroll_widget.double_click.connect(self.import_flow_double_click) self.scroll_widget.resize.connect(self.import_resize) # Actual scroll area and configuration scroll_area = QScrollArea() scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll_area.setWidgetResizable(True) scroll_area.setWidget(self.scroll_widget) # Add scroll widget to parent container scroll_container.addWidget(scroll_area) import_layout.addLayout(scroll_container) self.import_files.setEnabled(False) self.import_files.clicked.connect(self.import_selected_items) import_layout.addWidget(self.import_files) self.clear_selected.setEnabled(False) self.clear_selected.clicked.connect(self.clear_selected_items) import_layout.addWidget(self.clear_selected) import_group.setLayout(import_layout) return main_layout # Refreshes photos to full resolution def refresh_photos(self): self.update_album_layout() self.update_path() self.import_resize(self.scroll_widget.size()) # Sets album viewing area to correct layout and text def update_album_layout(self): self.clear_layout(self.flow) self.selected_album_mirror.clear() self.selected_album_files.clear() if self.selected_album is not None: self.album_desc.setText('Description: ' + self.selected_album.get_description()) for entry in self.selected_album.get_paths(): if isfile(entry): img = CaptionedImage('PHOTO', entry, entry, basename(entry), 100) else: img = CaptionedImage('PHOTO', 'assets/errorFindingFile.png', 'NULL', basename(entry), 100, None, True, True) self.flow.addWidget(img) self.selected_album_mirror.append(img) return self.container # Updates the list with new albums and makes sure buttons are set correctly def refresh_list(self): self.album_desc.setText('No Album Selected') self.album_list.clear() self.remove_album_button.setEnabled(False) self.edit_album_button.setEnabled(False) for album in self.loaded_albums: self.album_list.addItem(album.get_title()) self.selected_album = None self.export_album.setEnabled(False) self.recover_album.setEnabled(False) self.update_album_layout() # Creates a new album, can be configured with a prefilled title and description def add_new_album(self, title='', description='', override_dialog=False): if not title or title == '' or override_dialog: dialog = AlbumCreator(self, self.loaded_albums, False, None, title if title else '', description) title = dialog.get_title().text() description = dialog.get_description().text() if not title or len(title) == 0: return self.loaded_albums.append(AlbumData(title, description)) self.refresh_list() return title, description # Gets the currently selected album from the list of albums def get_selected_item(self): list_sel = self.album_list.currentItem().text() for album in self.loaded_albums: if album.get_title() == list_sel: self.selected_album = album self.export_album.setEnabled(True) self.recover_album.setEnabled(True) break self.remove_album_button.setEnabled(True) self.edit_album_button.setEnabled(True) self.update_album_layout() self.update_import_button() # Saves all albums def save_albums(self, rescan=False): main_data = appdirs.user_data_dir('PhotoUtilities', 'JackHogan') album_dir = join(main_data, 'albums') for file_path in listdir(album_dir): os.remove(join(album_dir, file_path)) for album in self.loaded_albums: pickle.dump( album, open(join(album_dir, album.get_title() + '.jalbum'), 'wb'), 4) if not rescan: QMessageBox.information(self, 'Albums', 'All albums saved successfully.') # Scans for more albums def rescan_albums(self): self.save_albums(True) save_data = check_save_data() if save_data: self.loaded_albums = save_data self.refresh_list() self.update_album_layout() def import_fat(self): album_loc = QFileDialog.getOpenFileName( self, 'Open Fat Album File', '/home', 'Fat Album Files (*.jfatalbum)', 'Fat Album Files (*.jfatalbum)') if not album_loc: return # Specifies where new photos will be located photo_loc = QFileDialog.getExistingDirectory( self, 'Select Extracted Photo Location (will be placed ' 'under file with album name)', '/home') if not photo_loc: return # Load album into memory album = pickle.load(open(album_loc[0], 'rb')) # Add album to list results = self.add_new_album(album.get_title(), album.get_description(), True) if not results: return album.set_title(results[0]) album.set_description(results[1]) self.selected_album = [ f for f in self.loaded_albums if f.get_title() == album.get_title() ][0] FatContentImporter(self, album, photo_loc, self.selected_album) QMessageBox.information(self, 'Import', 'Import completed successfully.') def export_fat(self): save_location = QFileDialog.getExistingDirectory( self, 'Choose an Export Directory', '/home') if not save_location: return FatContentExporter(self, save_location, self.selected_album, self.selected_album_mirror) QMessageBox.information(self, 'Export', 'Export completed successfully.') # Removes an album from the list and its file def remove_album(self): # Remove file main_data = appdirs.user_data_dir('PhotoUtilities', 'JackHogan') album_dir = join(main_data, 'albums') # Searches for pickled album data files album_files = [ f for f in listdir(album_dir) if isfile(join(album_dir, f)) and join(album_dir, f).lower().endswith('.jalbum') ] if self.selected_album.get_title() + '.jalbum' in album_files: os.remove( join(album_dir, self.selected_album.get_title() + '.jalbum')) # Remove from list self.loaded_albums.remove(self.selected_album) self.remove_album_button.setEnabled(False) self.refresh_list() def clear_layout(self, layout): if layout is not None: while layout.count(): item = layout.takeAt(0) widget = item.widget() if widget is not None: widget.deleteLater() else: self.clear_layout(item.layout()) def import_resize(self, new_size): size = calculate_flow_size(new_size) widgets = [] for wid_item in self.import_flow.get_widgets(): widgets.append(wid_item.widget()) for w in widgets: w.setFixedWidth(size / 6) widgets.clear() for wid_item in self.flow.get_widgets(): widgets.append(wid_item.widget()) for w in widgets: w.setFixedWidth(size / 6) # Fills the import_flow FlowLayout with the current directory def fill_import(self, directory: str, layout: FlowLayout): self.clear_layout(layout) self.loaded_images.clear() try: files = listdir(directory) except PermissionError: QMessageBox.critical( self, 'File Error', 'Couldn\'t access directory: Permission Denied') return dirs = [] photos = [] other_files = [] width = self.scroll_widget.width() / 5 for file in files: f = join(directory, file) if isdir(f): caption = CaptionedImage('FOLDER', 'assets/folder.png', str(f), str(file), width) dirs.append(caption) elif isfile(f) and f.lower().endswith(('.png', '.jpg', '.jpeg')): caption = CaptionedImage('PHOTO', str(f), str(f), str(file), width) photos.append(caption) else: caption = CaptionedImage('UNKNOWN', 'assets/unknownFile.png', str(f), str(file), width) other_files.append(caption) for direct in dirs: layout.addWidget(direct) self.loaded_images.append(direct) for photo in photos: layout.addWidget(photo) self.loaded_images.append(photo) for other in other_files: layout.addWidget(other) self.loaded_images.append(other) # Highlight selected items for index in range(len(self.loaded_images)): widget = layout.get_widgets()[index].widget() if test_names(self.selected_files, widget): widget.setStyleSheet('background-color: #93b6ed') else: widget.setStyleSheet('') def update_path(self): # Turns text red if path is invalid if isdir(self.path.text()): self.path.setStyleSheet("color: #000000") self.fill_import(self.path.text(), self.import_flow) else: self.path.setStyleSheet("color: #FF0000") # Open file dialog for choosing import path def choose_path(self): dialog = QFileDialog.getExistingDirectory(self, 'Open Directory', '/home') if dialog: self.path.setText(dialog) def go_up_dir(self): self.path.setText(str(Path(self.path.text()).parent)) # param: e[0]: QMouseEvent, e[1]: index of clicked widget def import_flow_mouse_down(self, e: tuple): index = e[1] try: widget = self.import_flow.get_widgets()[index].widget() except TypeError: return if widget.styleSheet() is '': if self.loaded_images[index] not in self.selected_files: self.selected_files.append(self.loaded_images[index]) widget.setStyleSheet('background-color: #93b6ed') else: if get_index_from_name(self.selected_files, self.loaded_images[index]) is not None: self.selected_files.pop( get_index_from_name(self.selected_files, self.loaded_images[index])) widget.setStyleSheet('') self.update_import_button() # Tracks clicks for album layout def album_flow_mouse_down(self, e: tuple): index = e[1] try: widget = self.flow.get_widgets()[index].widget() except TypeError: return if widget.styleSheet() is '': if self.selected_album_mirror[ index] not in self.selected_album_files: self.selected_album_files.append( self.selected_album_mirror[index]) widget.setStyleSheet('background-color: #93b6ed') else: index_name = get_index_from_name(self.selected_album_files, self.selected_album_mirror[index]) if index_name is not None: self.selected_album_files.pop(index_name) widget.setStyleSheet('') self.update_album_view_buttons() def import_flow_double_click(self, e: tuple): index = e[1] try: widget = self.loaded_images[index] except TypeError: return try: listdir(join(self.path.text(), widget.get_name())) except PermissionError: QMessageBox.critical( self, 'File Error', 'Couldn\'t access directory: Permission Denied') return except NotADirectoryError: return if widget.get_file_type() is 'FOLDER': self.path.setText(join(self.path.text(), widget.get_name())) self.update_path() if widget in self.selected_files: self.selected_files.pop(self.selected_files.index(widget)) self.update_import_button() def update_import_button(self): self.import_files.setText( 'Import ' + str(len(self.selected_files)) + ' Selected ' + ('Item' if len(self.selected_files) == 1 else 'Items')) self.clear_selected.setEnabled(len(self.selected_files) > 0) self.import_files.setEnabled( len(self.selected_files) > 0 and self.selected_album is not None) def update_album_view_buttons(self): self.remove_from_album.setEnabled( len([ f for f in self.flow.get_widgets() if not f.widget().styleSheet() is '' ]) > 0) # Only enable when 1 item is selected self.open_path.setEnabled( len([ f for f in self.flow.get_widgets() if not f.widget().styleSheet() is '' ]) == 1) def clear_selected_items(self): self.selected_files.clear() self.update_import_button() for wid in self.import_flow.get_widgets(): wid.widget().setStyleSheet('') def edit_selected_album(self): original_file_name = self.selected_album.get_title() # Remove file main_data = appdirs.user_data_dir('PhotoUtilities', 'JackHogan') album_dir = join(main_data, 'albums') # Searches for pickled album data files album_files = [ f for f in listdir(album_dir) if isfile(join(album_dir, f)) and join(album_dir, f).lower().endswith('.jalbum') ] if self.selected_album.get_title() + '.jalbum' in album_files: os.remove(join(album_dir, original_file_name + '.jalbum')) edited = AlbumCreator(self, self.loaded_albums, True, self.selected_album) self.selected_album.set_title(edited.get_title().text()) self.selected_album.set_description(edited.get_description().text()) self.refresh_list() self.save_albums(True) def import_selected_items(self): NewContentImporter(self, self.selected_files, self.selected_album) self.clear_selected_items() self.update_album_layout() def remove_selected_album_items(self): for image in self.selected_album_files: self.selected_album.remove_path(image.get_image()) self.selected_album_files.clear() self.update_album_view_buttons() self.update_album_layout() for wid in self.flow.get_widgets(): wid.widget().setStyleSheet('') self.remove_from_album.setEnabled(False) def open_selected_path(self): if len(self.selected_album_files) > 1: return # Opens parent directory in system's file explorer webbrowser.open( 'file://' + str(Path(self.selected_album_files[0].get_file_path()).parent)) def recover_current_album(self): search_dir = QFileDialog.getExistingDirectory( self, 'Select Directory to Search', '/home') if not search_dir: return AlbumRecovery(self, self.selected_album, search_dir) self.rescan_albums() QMessageBox.information( self, 'Album Recovery', 'Recovery completed successfully. ' 'However, not all files may have been found.')