def __init__(self, signals, directory: str): super().__init__() self.setupUi(self) # init attributes self.signals = signals self.directory = directory self.db = database.DBHandler() self.books = [] self.selected = None self.details_panel = DetailsPanel(self.db, self.signals) self.search_panel = SearchPanel(self.db, self.signals) self.metadata_panel = MetadataPanel(self.db, self.signals) self.setup_panels() self.connect_events() self.populate_gallery()
def __init__(self): appdata = str( QDesktopServices.storageLocation(QDesktopServices.DataLocation)) MusicGuruBase.__init__(self, appdata) ApplicationBase.__init__(self) if not op.exists(appdata): os.makedirs(appdata) logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING) self.prefs = Preferences() self.prefs.load() self.selectedBoardItems = [] self.selectedLocation = None self.mainWindow = MainWindow(app=self) self.locationsPanel = LocationsPanel(app=self) self.detailsPanel = DetailsPanel(app=self) self.ignoreBox = IgnoreBox(app=self) self.progress = Progress(self.mainWindow) self.aboutBox = AboutBox(self.mainWindow, self) self.connect(self.progress, SIGNAL('finished(QString)'), self.jobFinished) self.connect(self, SIGNAL('applicationFinishedLaunching()'), self.applicationFinishedLaunching)
def __init__(self): appdata = str(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) MusicGuruBase.__init__(self, appdata) ApplicationBase.__init__(self) if not op.exists(appdata): os.makedirs(appdata) logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING) self.prefs = Preferences() self.prefs.load() self.selectedBoardItems = [] self.selectedLocation = None self.mainWindow = MainWindow(app=self) self.locationsPanel = LocationsPanel(app=self) self.detailsPanel = DetailsPanel(app=self) self.ignoreBox = IgnoreBox(app=self) self.progress = Progress(self.mainWindow) self.aboutBox = AboutBox(self.mainWindow, self) self.connect(self.progress, SIGNAL('finished(QString)'), self.jobFinished) self.connect(self, SIGNAL('applicationFinishedLaunching()'), self.applicationFinishedLaunching)
class Home(QMainWindow, Ui_MainWindow): """The main screen Args: signals (signals.Signals) directory (str) Attributes: search_bar (QLineEdit) search_button (QPushButton): same function as pressing entet in search bar details_button (QPushButton) advanced_search_button (QPushButton): opens a new window with all searchable options metadata_button (QPushButton): sort_by (QComboBox): contains "Alphabetically", "Rating", and "Randomly" random_button (QPushButton): randomly selects from the list main_area (QHBoxLayout) bookshelf (QGridLayout): the layout where all the books will be displayed bookshelf_area (QScrollArea): the whole area where books will be displayed books ([QFrame]): holds a list of all book frames selected (QFrame): holds the currently selected book details_panel (DetailsPanel) search_panel (SearchPanel) metadata_panel (MetadataPanel) """ def __init__(self, signals, directory: str): super().__init__() self.setupUi(self) # init attributes self.signals = signals self.directory = directory self.db = database.DBHandler() self.books = [] self.selected = None self.details_panel = DetailsPanel(self.db, self.signals) self.search_panel = SearchPanel(self.db, self.signals) self.metadata_panel = MetadataPanel(self.db, self.signals) self.setup_panels() self.connect_events() self.populate_gallery() def setup_panels(self): """Adds all panels to the main layout and makes only the details panel visible """ self.main_area.addWidget(self.details_panel) self.main_area.addWidget(self.search_panel) self.main_area.addWidget(self.metadata_panel) self.search_panel.setVisible(False) self.metadata_panel.setVisible(False) def connect_events(self): """Connects each signal to their respective functions """ # dropdowns self.sort_by.textActivated.connect(self.sort_gallery) # buttons self.details_button.clicked.connect(lambda: [ self.details_panel.setVisible(True), self.search_panel.setVisible(False), self.metadata_panel.setVisible(False) ]) self.advanced_search_button.clicked.connect(lambda: [ self.details_panel.setVisible(False), self.search_panel.setVisible(True), self.metadata_panel.setVisible(False) ]) self.metadata_button.clicked.connect(lambda: [ self.details_panel.setVisible(False), self.search_panel.setVisible(False), self.metadata_panel.setVisible(True) ]) self.random_button.clicked.connect(self.random_select) # bookshelf self.bookshelf_area.contextMenuEvent = self.context_menu self.bookshelf_area.mousePressEvent = self.reset_selected # signals self.signals.update_spines.connect(self.update_gallery) self.signals.search_advanced.connect(self.populate_gallery) def random_select(self): """Randomly selects a book from the list """ self.select(random.choice(self.books)) def generate_books(self, filters, sort): """Creates objects for the books based on certain search parameters Args: filters (dict, list, optional): filters to filter by (either a dict from search panel or a list from sort_gallery) sort (int): obtained from self.sort_by.currentIndex() """ self.books = [] for book in self.db.get_books(filters, sort): # set up frame spine = spines.BookSpine(book['id'], book['name'], book['directory']) # add the new frames to the list self.books.append(spine) # connect events spine.mousePressEvent = partial(self.select, self.books[-1]) spine.mouseDoubleClickEvent = partial(self.open_book, self.books[-1]) spine.enterEvent = partial(self.highlight, self.books[-1]) spine.leaveEvent = partial(self.unhighlight, self.books[-1]) def populate_gallery(self, filters=None): """Populates the gallery with books that match a search filter Books should be generated by self.generate_books() Args: filters (dict, list, optional): filters to filter by (either a dict from search panel or a list from sort_gallery) """ self.clear_gallery() self.generate_books(filters, self.sort_by.currentIndex()) row_pos = 0 col_pos = 0 for book in self.books: self.bookshelf.addWidget(book, col_pos, row_pos) # calculate next position row_pos += 1 if row_pos > 4: # max 5 columns. aka 5 slots per row row_pos = 0 col_pos += 1 while col_pos <= 1: # if there's only a few books, place invisible frames to shove things into the top left corner # this avoids books showing up in the middle and messing up the look of the gallery blank = spines.BlankSpine() self.bookshelf.addWidget(blank, col_pos, row_pos) # calculate next position row_pos += 1 if row_pos > 4: # max 5 columns. aka 5 slots per row row_pos = 0 col_pos += 1 if (new_select := next((x for x in self.books if x == self.selected), None)): self.select(new_select) else:
class MusicGuru(MusicGuruBase, ApplicationBase): LOGO_NAME = 'mg_logo' def __init__(self): appdata = str( QDesktopServices.storageLocation(QDesktopServices.DataLocation)) MusicGuruBase.__init__(self, appdata) ApplicationBase.__init__(self) if not op.exists(appdata): os.makedirs(appdata) logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING) self.prefs = Preferences() self.prefs.load() self.selectedBoardItems = [] self.selectedLocation = None self.mainWindow = MainWindow(app=self) self.locationsPanel = LocationsPanel(app=self) self.detailsPanel = DetailsPanel(app=self) self.ignoreBox = IgnoreBox(app=self) self.progress = Progress(self.mainWindow) self.aboutBox = AboutBox(self.mainWindow, self) self.connect(self.progress, SIGNAL('finished(QString)'), self.jobFinished) self.connect(self, SIGNAL('applicationFinishedLaunching()'), self.applicationFinishedLaunching) #--- Private def _placeDetailsPanel(self): # locations panel must be placed first if self.detailsPanel.isVisible(): return desktop = QApplication.desktop() w = self.locationsPanel.width() h = self.detailsPanel.height() x = self.locationsPanel.x() windowBottom = self.locationsPanel.frameGeometry().y( ) + self.locationsPanel.frameGeometry().height() y = windowBottom self.detailsPanel.move(x, y) self.detailsPanel.resize(w, h) def _placeIgnoreBox(self): if self.ignoreBox.isVisible(): return desktop = QApplication.desktop() windowWidth = self.mainWindow.frameGeometry().width() frameWidth = self.ignoreBox.frameGeometry().width( ) - self.ignoreBox.width() w = windowWidth - frameWidth h = self.ignoreBox.height() x = self.mainWindow.x() windowBottom = self.mainWindow.frameGeometry().y( ) + self.mainWindow.frameGeometry().height() y = min(windowBottom, desktop.height() - h) self.ignoreBox.move(x, y) self.ignoreBox.resize(w, h) def _placeLocationsPanel(self): if self.locationsPanel.isVisible(): return desktop = QApplication.desktop() w = self.locationsPanel.width() windowHeight = self.mainWindow.frameGeometry().height() frameHeight = self.locationsPanel.frameGeometry().height( ) - self.locationsPanel.height() h = windowHeight - frameHeight - self.detailsPanel.frameGeometry( ).height() windowRight = self.mainWindow.frameGeometry().x( ) + self.mainWindow.frameGeometry().width() x = min(windowRight, desktop.width() - w) y = self.mainWindow.y() self.locationsPanel.move(x, y) self.locationsPanel.resize(w, h) def _setup_as_registered(self): self.prefs.registration_code = self.registration_code self.prefs.registration_email = self.registration_email self.prefs.save() self.mainWindow.actionRegister.setVisible(False) self.aboutBox.registerButton.hide() self.aboutBox.registeredEmailLabel.setText( self.prefs.registration_email) def _startJob(self, jobid, func): title = JOBID2TITLE[jobid] try: j = self.progress.create_job() self.progress.run(jobid, title, func, args=(j, )) except job.JobInProgressError: msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." QMessageBox.information(self.mainWindow, "Action in progress", msg) #--- Public def addLocation(self, path, name, removeable): def do(j): MusicGuruBase.AddLocation(self, path, name, removeable, j) error_msg = self.CanAddLocation(path, name) if error_msg: QMessageBox.warning(self.mainWindow, "Add Location", error_msg) return self._startJob(JOB_ADD, do) def addLocationPrompt(self): dialog = AddLocationDialog(self) result = dialog.exec_() if result == QDialog.Accepted: self.addLocation(dialog.locationPath, dialog.locationName, dialog.isLocationRemovable) def askForRegCode(self): if self.reg.ask_for_code(): self._setup_as_registered() def copyOrMove(self, copy): def onNeedCd(location): # We can't do anything GUI related in a separate thread with Qt. Since copy/move # operations are performed asynchronously, the calls made to needCdDialog (created in # the main thread) must also be made asynchronously. return needCdDialog.askForDiskAsync(location.name) def do(j): MusicGuruBase.CopyOrMove(self, copy, dirpath, j, onNeedCd) needCdDialog = DiskNeededDialog() title = "Choose a destination" flags = QFileDialog.ShowDirsOnly dirpath = str( QFileDialog.getExistingDirectory(self.mainWindow, title, '', flags)) if dirpath: jobid = JOB_MATERIALIZE_COPY if copy else JOB_MATERIALIZE_MOVE self._startJob(jobid, do) def massRename(self, model, whitespace): def do(j): self.board.MassRename(model, whitespace, j) self._startJob(JOB_MASS_RENAME, do) def moveConflicts(self, with_original=False): if self.board.MoveConflicts(with_original=with_original) > 0: self.emit(SIGNAL('boardChanged()')) self.emit(SIGNAL('ignoreBoxChanged()')) def moveSelectedToIgnoreBox(self): smart_move(self.selectedBoardItems, self.board.ignore_box, allow_merge=True) self.emit(SIGNAL('boardChanged()')) self.emit(SIGNAL('ignoreBoxChanged()')) def removeEmptyFolders(self): MusicGuruBase.RemoveEmptyDirs(self) self.emit(SIGNAL('boardChanged()')) def removeLocation(self, location): self.board.RemoveLocation(location) location.delete() self.emit(SIGNAL('locationsChanged()')) self.emit(SIGNAL('boardChanged()')) def removeLocationPrompt(self): location = self.selectedLocation if location is None: return title = "Remove location" msg = "Do you really want to remove location {0}?".format( location.name) buttons = QMessageBox.Yes | QMessageBox.No answer = QMessageBox.question(self.mainWindow, title, msg, buttons, QMessageBox.Yes) if answer != QMessageBox.Yes: return self.removeLocation(location) def renameInRespectiveLocations(self): def do(j): MusicGuruBase.RenameInRespectiveLocations(self, j) self._startJob(JOB_MATERIALIZE_RENAME, do) def selectBoardItems(self, items): self.selectedBoardItems = items self.emit(SIGNAL('boardSelectionChanged()')) def selectLocation(self, location): self.selectedLocation = location def showAboutBox(self): self.aboutBox.show() def showDetailsPanel(self): self._placeLocationsPanel() self._placeDetailsPanel() self.detailsPanel.show() self.detailsPanel.activateWindow() def showHelp(self): url = QUrl.fromLocalFile(op.join(op.abspath(HELP_PATH), 'intro.htm')) QDesktopServices.openUrl(url) def showIgnoreBox(self): self._placeIgnoreBox() self.ignoreBox.show() self.ignoreBox.activateWindow() def showLocationPanel(self): self._placeLocationsPanel() self.locationsPanel.show() self.locationsPanel.activateWindow() def split(self, model, capacity, grouping_level): def do(j): self.board.Split(model, capacity, grouping_level, j) self._startJob(JOB_SPLIT, do) def toggleLocation(self, location): self.board.ToggleLocation(location) self.emit(SIGNAL('locationsChanged()')) self.emit(SIGNAL('boardChanged()')) def undoSplit(self): self.board.Unsplit() self.emit(SIGNAL('boardChanged()')) def updateCollection(self): def do(j): self.collection.update_volumes(j) self._startJob(JOB_UPDATE, do) def updateLocation(self, location): def do(j): location.update(None, j) self._startJob(JOB_UPDATE, do) #--- Events def applicationFinishedLaunching(self): self.reg = Registration(self) self.set_registration(self.prefs.registration_code, self.prefs.registration_email) if not self.registered and self.unpaid_hours >= 1: self.reg.show_nag() self.mainWindow.show() self.showLocationPanel() self.showDetailsPanel() self.updateCollection() def jobFinished(self, jobid): if jobid in (JOB_UPDATE, JOB_ADD): self.emit(SIGNAL('locationsChanged()')) if jobid in (JOB_MASS_RENAME, JOB_SPLIT): self.emit(SIGNAL('boardChanged()')) if jobid in (JOB_MATERIALIZE_RENAME, JOB_MATERIALIZE_MOVE): self.board.Empty() self.emit(SIGNAL('locationsChanged()')) self.emit(SIGNAL('boardChanged()')) self.emit(SIGNAL('ignoreBoxChanged()'))
class MusicGuru(MusicGuruBase, ApplicationBase): LOGO_NAME = 'mg_logo' def __init__(self): appdata = str(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) MusicGuruBase.__init__(self, appdata) ApplicationBase.__init__(self) if not op.exists(appdata): os.makedirs(appdata) logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING) self.prefs = Preferences() self.prefs.load() self.selectedBoardItems = [] self.selectedLocation = None self.mainWindow = MainWindow(app=self) self.locationsPanel = LocationsPanel(app=self) self.detailsPanel = DetailsPanel(app=self) self.ignoreBox = IgnoreBox(app=self) self.progress = Progress(self.mainWindow) self.aboutBox = AboutBox(self.mainWindow, self) self.connect(self.progress, SIGNAL('finished(QString)'), self.jobFinished) self.connect(self, SIGNAL('applicationFinishedLaunching()'), self.applicationFinishedLaunching) #--- Private def _placeDetailsPanel(self): # locations panel must be placed first if self.detailsPanel.isVisible(): return desktop = QApplication.desktop() w = self.locationsPanel.width() h = self.detailsPanel.height() x = self.locationsPanel.x() windowBottom = self.locationsPanel.frameGeometry().y() + self.locationsPanel.frameGeometry().height() y = windowBottom self.detailsPanel.move(x, y) self.detailsPanel.resize(w, h) def _placeIgnoreBox(self): if self.ignoreBox.isVisible(): return desktop = QApplication.desktop() windowWidth = self.mainWindow.frameGeometry().width() frameWidth = self.ignoreBox.frameGeometry().width() - self.ignoreBox.width() w = windowWidth - frameWidth h = self.ignoreBox.height() x = self.mainWindow.x() windowBottom = self.mainWindow.frameGeometry().y() + self.mainWindow.frameGeometry().height() y = min(windowBottom, desktop.height() - h) self.ignoreBox.move(x, y) self.ignoreBox.resize(w, h) def _placeLocationsPanel(self): if self.locationsPanel.isVisible(): return desktop = QApplication.desktop() w = self.locationsPanel.width() windowHeight = self.mainWindow.frameGeometry().height() frameHeight = self.locationsPanel.frameGeometry().height() - self.locationsPanel.height() h = windowHeight - frameHeight - self.detailsPanel.frameGeometry().height() windowRight = self.mainWindow.frameGeometry().x() + self.mainWindow.frameGeometry().width() x = min(windowRight, desktop.width() - w) y = self.mainWindow.y() self.locationsPanel.move(x, y) self.locationsPanel.resize(w, h) def _setup_as_registered(self): self.prefs.registration_code = self.registration_code self.prefs.registration_email = self.registration_email self.prefs.save() self.mainWindow.actionRegister.setVisible(False) self.aboutBox.registerButton.hide() self.aboutBox.registeredEmailLabel.setText(self.prefs.registration_email) def _startJob(self, jobid, func): title = JOBID2TITLE[jobid] try: j = self.progress.create_job() self.progress.run(jobid, title, func, args=(j, )) except job.JobInProgressError: msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." QMessageBox.information(self.mainWindow, "Action in progress", msg) #--- Public def addLocation(self, path, name, removeable): def do(j): MusicGuruBase.AddLocation(self, path, name, removeable, j) error_msg = self.CanAddLocation(path, name) if error_msg: QMessageBox.warning(self.mainWindow, "Add Location", error_msg) return self._startJob(JOB_ADD, do) def addLocationPrompt(self): dialog = AddLocationDialog(self) result = dialog.exec_() if result == QDialog.Accepted: self.addLocation(dialog.locationPath, dialog.locationName, dialog.isLocationRemovable) def askForRegCode(self): if self.reg.ask_for_code(): self._setup_as_registered() def copyOrMove(self, copy): def onNeedCd(location): # We can't do anything GUI related in a separate thread with Qt. Since copy/move # operations are performed asynchronously, the calls made to needCdDialog (created in # the main thread) must also be made asynchronously. return needCdDialog.askForDiskAsync(location.name) def do(j): MusicGuruBase.CopyOrMove(self, copy, dirpath, j, onNeedCd) needCdDialog = DiskNeededDialog() title = "Choose a destination" flags = QFileDialog.ShowDirsOnly dirpath = str(QFileDialog.getExistingDirectory(self.mainWindow, title, '', flags)) if dirpath: jobid = JOB_MATERIALIZE_COPY if copy else JOB_MATERIALIZE_MOVE self._startJob(jobid, do) def massRename(self, model, whitespace): def do(j): self.board.MassRename(model, whitespace, j) self._startJob(JOB_MASS_RENAME, do) def moveConflicts(self, with_original=False): if self.board.MoveConflicts(with_original=with_original) > 0: self.emit(SIGNAL('boardChanged()')) self.emit(SIGNAL('ignoreBoxChanged()')) def moveSelectedToIgnoreBox(self): smart_move(self.selectedBoardItems, self.board.ignore_box, allow_merge=True) self.emit(SIGNAL('boardChanged()')) self.emit(SIGNAL('ignoreBoxChanged()')) def removeEmptyFolders(self): MusicGuruBase.RemoveEmptyDirs(self) self.emit(SIGNAL('boardChanged()')) def removeLocation(self, location): self.board.RemoveLocation(location) location.delete() self.emit(SIGNAL('locationsChanged()')) self.emit(SIGNAL('boardChanged()')) def removeLocationPrompt(self): location = self.selectedLocation if location is None: return title = "Remove location" msg = "Do you really want to remove location {0}?".format(location.name) buttons = QMessageBox.Yes | QMessageBox.No answer = QMessageBox.question(self.mainWindow, title, msg, buttons, QMessageBox.Yes) if answer != QMessageBox.Yes: return self.removeLocation(location) def renameInRespectiveLocations(self): def do(j): MusicGuruBase.RenameInRespectiveLocations(self, j) self._startJob(JOB_MATERIALIZE_RENAME, do) def selectBoardItems(self, items): self.selectedBoardItems = items self.emit(SIGNAL('boardSelectionChanged()')) def selectLocation(self, location): self.selectedLocation = location def showAboutBox(self): self.aboutBox.show() def showDetailsPanel(self): self._placeLocationsPanel() self._placeDetailsPanel() self.detailsPanel.show() self.detailsPanel.activateWindow() def showHelp(self): url = QUrl.fromLocalFile(op.join(op.abspath(HELP_PATH), 'intro.htm')) QDesktopServices.openUrl(url) def showIgnoreBox(self): self._placeIgnoreBox() self.ignoreBox.show() self.ignoreBox.activateWindow() def showLocationPanel(self): self._placeLocationsPanel() self.locationsPanel.show() self.locationsPanel.activateWindow() def split(self, model, capacity, grouping_level): def do(j): self.board.Split(model, capacity, grouping_level, j) self._startJob(JOB_SPLIT, do) def toggleLocation(self, location): self.board.ToggleLocation(location) self.emit(SIGNAL('locationsChanged()')) self.emit(SIGNAL('boardChanged()')) def undoSplit(self): self.board.Unsplit() self.emit(SIGNAL('boardChanged()')) def updateCollection(self): def do(j): self.collection.update_volumes(j) self._startJob(JOB_UPDATE, do) def updateLocation(self, location): def do(j): location.update(None, j) self._startJob(JOB_UPDATE, do) #--- Events def applicationFinishedLaunching(self): self.reg = Registration(self) self.set_registration(self.prefs.registration_code, self.prefs.registration_email) if not self.registered and self.unpaid_hours >= 1: self.reg.show_nag() self.mainWindow.show() self.showLocationPanel() self.showDetailsPanel() self.updateCollection() def jobFinished(self, jobid): if jobid in (JOB_UPDATE, JOB_ADD): self.emit(SIGNAL('locationsChanged()')) if jobid in (JOB_MASS_RENAME, JOB_SPLIT): self.emit(SIGNAL('boardChanged()')) if jobid in (JOB_MATERIALIZE_RENAME, JOB_MATERIALIZE_MOVE): self.board.Empty() self.emit(SIGNAL('locationsChanged()')) self.emit(SIGNAL('boardChanged()')) self.emit(SIGNAL('ignoreBoxChanged()'))