diff --git a/res/style.css b/res/style.css index a43ab78..2cef9ed 100644 --- a/res/style.css +++ b/res/style.css @@ -6,7 +6,7 @@ QLabel#author { } AppWindow > QToolBar, QStatusBar, SideBarWidget { - background-color: #2b303a; + background-color: #2A2D31; border: none; } @@ -41,13 +41,13 @@ QScrollBar:horizontal { } QScrollBar:vertical, QScrollBar:horizontal { - border: 0px solid #2b303a; + border: 0px solid #2A2D31; background:none; margin: 0px 0px 0px 0px; } QScrollBar::handle { - background: #2b303a; + background: #2A2D31; } QScrollBar::handle:vertical, QScrollBar::handle:horizontal { @@ -55,28 +55,28 @@ QScrollBar::handle:vertical, QScrollBar::handle:horizontal { } QScrollBar::add-line:vertical { - background: #2b303a; + background: #2A2D31; height: 0px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::add-line:horizontal { - background: #2b303a; + background: #2A2D31; width: 0px; subcontrol-position: right; subcontrol-origin: margin; } QScrollBar::sub-line:vertical { - background: #2b303a; + background: #2A2D31; height: 0px; subcontrol-position: top; subcontrol-origin: margin; } QScrollBar::sub-line:horizontal { - background: #2b303a; + background: #2A2D31; width: 0px; subcontrol-position: left; subcontrol-origin: margin; @@ -87,7 +87,7 @@ QScrollBar::add-page, QScrollBar::sub-page { } QMenu { - background-color: #2b303a; + background-color: #2A2D31; color: white; } @@ -104,13 +104,13 @@ QLabel { } QPushButton { - border: 1px solid #555555; - border-radius: 2px; + border: 1px solid #3E4249; + border-radius: 1px; padding: 5px; } QPushButton, QToolButton { - background-color: #2b303a; + background-color: #3E4249; color: white; } @@ -124,7 +124,7 @@ QPushButton:pressed, QPushButton:checked { QToolTip { border-style: none; - background-color: #2b303a; + background-color: #2A2D31; color: white; } @@ -139,30 +139,30 @@ TagText { padding-top: 0.1em; border-radius: 0.45em; border: 0.1em solid #d64933; - background-color: rgba(25,100,126,0.50) !important; + background-color: rgba(62, 66, 73,0.50) !important; } ArrowWindow { - background-color: #2b303a; + background-color: #2A2D31; color: #d64933; border: 1px solid #d64933; border-radius: 5px; } GalleryMetaWindow QScrollArea QWidget, QHeaderView::section, QHeaderView::section::checked { - background-color: #2b303a; + background-color: #2A2D31; } SettingsDialog, SettingsDialog QScrollArea QWidget, GalleryDialog, GalleryDialog QScrollArea QWidget { - color: #2b303a; + color: #2A2D31; } GalleryDialog QScrollArea QWidget QPushButton, SettingsDialog QScrollArea QWidget QPushButton{ color: white; } -QGroupBox::title, QHeaderView::section { - color: #ff3516; +QHeaderView::section { + color: white; } QHeaderView::section { @@ -170,7 +170,7 @@ QHeaderView::section { padding: 5px; } -QHeaderView::down-arrow, QHeaderView::up-arrow { +QGroupBox::title, QHeaderView::down-arrow, QHeaderView::up-arrow { color: #d64933; } @@ -179,7 +179,7 @@ QListView, QTableView { } QProgressBar { - border: 2px solid 2b303a; + border: 2px solid #3E4249; border-radius: 5px; } diff --git a/version/app.py b/version/app.py index bf8eb30..fd05042 100644 --- a/version/app.py +++ b/version/app.py @@ -23,19 +23,19 @@ import traceback from PyQt5.QtCore import (Qt, QSize, pyqtSignal, QThread, QEvent, QTimer, - QObject, QPoint, QPropertyAnimation) + QObject, QPoint, QPropertyAnimation) from PyQt5.QtGui import (QPixmap, QIcon, QMoveEvent, QCursor, - QKeySequence) + QKeySequence) from PyQt5.QtWidgets import (QMainWindow, QListView, - QHBoxLayout, QFrame, QWidget, QVBoxLayout, - QLabel, QStackedLayout, QToolBar, QMenuBar, - QSizePolicy, QMenu, QAction, QLineEdit, - QSplitter, QMessageBox, QFileDialog, - QDesktopWidget, QPushButton, QCompleter, - QListWidget, QListWidgetItem, QToolTip, - QProgressBar, QToolButton, QSystemTrayIcon, - QShortcut, QGraphicsBlurEffect, QTableWidget, - QTableWidgetItem, QActionGroup) + QHBoxLayout, QFrame, QWidget, QVBoxLayout, + QLabel, QStackedLayout, QToolBar, QMenuBar, + QSizePolicy, QMenu, QAction, QLineEdit, + QSplitter, QMessageBox, QFileDialog, + QDesktopWidget, QPushButton, QCompleter, + QListWidget, QListWidgetItem, QToolTip, + QProgressBar, QToolButton, QSystemTrayIcon, + QShortcut, QGraphicsBlurEffect, QTableWidget, + QTableWidgetItem, QActionGroup) from executors import Executors @@ -61,1127 +61,1128 @@ log_c = log.critical class AppWindow(QMainWindow): - "The application's main window" - - move_listener = pyqtSignal() - db_startup_invoker = pyqtSignal(list) - duplicate_check_invoker = pyqtSignal(gallery.GalleryModel) - admin_db_method_invoker = pyqtSignal(object) - db_activity_checker = pyqtSignal() - graphics_blur = QGraphicsBlurEffect() - - def __init__(self, disable_excepthook=False): - super().__init__() - if not disable_excepthook: - sys.excepthook = self.excepthook - app_constants.GENERAL_THREAD = QThread(self) - app_constants.GENERAL_THREAD.finished.connect(app_constants.GENERAL_THREAD.deleteLater) - app_constants.GENERAL_THREAD.start() - self._db_startup_thread = QThread(self) - self._db_startup_thread.finished.connect(self._db_startup_thread.deleteLater) - self.db_startup = gallerydb.DatabaseStartup() - self._db_startup_thread.start() - self.db_startup.moveToThread(self._db_startup_thread) - self.db_startup.DONE.connect(lambda: self.scan_for_new_galleries() if app_constants.LOOK_NEW_GALLERY_STARTUP else None) - self.db_startup_invoker.connect(self.db_startup.startup) - self.setAcceptDrops(True) - self.initUI() - self.startup() - QTimer.singleShot(3000, self._check_update) - self.setFocusPolicy(Qt.NoFocus) - self.set_shortcuts() - self.graphics_blur.setParent(self) - - def set_shortcuts(self): - quit = QShortcut(QKeySequence('Ctrl+Q'), self, self.close) - search_focus = QShortcut(QKeySequence(QKeySequence.Find), self, lambda:self.search_bar.setFocus(Qt.ShortcutFocusReason)) - prev_view = QShortcut(QKeySequence(QKeySequence.PreviousChild), self, self.switch_display) - next_view = QShortcut(QKeySequence(QKeySequence.NextChild), self, self.switch_display) - help = QShortcut(QKeySequence(QKeySequence.HelpContents), self, lambda:utils.open_web_link("https://github.com/Pewpews/happypanda/wiki")) - - def init_watchers(self): - - def remove_gallery(g): - index = gallery.CommonView.find_index(self.get_current_view(), g.id, True) - if index: - gallery.CommonView.remove_gallery(self.get_current_view(), [index]) - else: - log_e('Could not find gallery to remove from watcher') - - def create_gallery(path): - g_dia = gallerydialog.GalleryDialog(self, path) - g_dia.SERIES.connect(self.default_manga_view.add_gallery) - g_dia.show() - - def update_gallery(g): - index = gallery.CommonView.find_index(self.get_current_view(), g.id) - if index: - gal = index.data(gallery.GalleryModel.GALLERY_ROLE) - gal.path = g.path - gal.chapters = g.chapters - else: - log_e('Could not find gallery to update from watcher') - self.default_manga_view.replace_gallery(g, False) - - def created(path): - c_popup = io_misc.CreatedPopup(path, self) - c_popup.ADD_SIGNAL.connect(create_gallery) - def modified(path, gallery): - mod_popup = io_misc.ModifiedPopup(path, gallery, self) - def deleted(path, gallery): - d_popup = io_misc.DeletedPopup(path, gallery, self) - d_popup.UPDATE_SIGNAL.connect(update_gallery) - d_popup.REMOVE_SIGNAL.connect(remove_gallery) - def moved(new_path, gallery): - mov_popup = io_misc.MovedPopup(new_path, gallery, self) - mov_popup.UPDATE_SIGNAL.connect(update_gallery) - - self.watchers = io_misc.Watchers() - self.watchers.gallery_handler.CREATE_SIGNAL.connect(created) - self.watchers.gallery_handler.MODIFIED_SIGNAL.connect(modified) - self.watchers.gallery_handler.MOVED_SIGNAL.connect(moved) - self.watchers.gallery_handler.DELETED_SIGNAL.connect(deleted) - - def startup(self): - def normalize_first_time(): - settings.set(app_constants.INTERNAL_LEVEL, 'Application', 'first time level') - settings.save() - - def done(status=True): - self.db_startup_invoker.emit(gallery.MangaViews.manga_views) - #self.db_startup.startup() - if app_constants.FIRST_TIME_LEVEL != app_constants.INTERNAL_LEVEL: - normalize_first_time() - else: - settings.set(app_constants.vs, 'Application', 'version') - if app_constants.UPDATE_VERSION != app_constants.vs: - self.notif_bubble.update_text( - "Happypanda has been updated!", - "Don't forget to check out what's new in this version by clicking here!") - else: - hello = ["Hello!", "Hi!", "Onii-chan!", "Senpai!", "Hisashiburi!", "Welcome!", "Okaerinasai!", "Welcome back!", "Hajimemashite!"] - self.notif_bubble.update_text("{}".format(hello[random.randint(0, len(hello) - 1)]), "Please don't hesitate to report any bugs you find.", 10) - - if app_constants.ENABLE_MONITOR and \ - app_constants.MONITOR_PATHS and all(app_constants.MONITOR_PATHS): - self.init_watchers() - self.download_manager = pewnet.Downloader() - app_constants.DOWNLOAD_MANAGER = self.download_manager - self.download_manager.start_manager(4) - - if app_constants.FIRST_TIME_LEVEL < 4: - log_i('Invoking first time level {}'.format(4)) - app_constants.INTERNAL_LEVEL = 4 - settings.set([], 'Application', 'monitor paths') - settings.set([], 'Application', 'ignore paths') - app_constants.MONITOR_PATHS = [] - app_constants.IGNORE_PATHS = [] - settings.save() - done() - elif app_constants.FIRST_TIME_LEVEL < 5: - log_i('Invoking first time level {}'.format(5)) - app_constants.INTERNAL_LEVEL = 5 - app_widget = misc.AppDialog(self) - app_widget.note_info.setText("IMPORTANT: Application restart is required when done") - app_widget.restart_info.hide() - self.admin_db = gallerydb.AdminDB() - self.admin_db.moveToThread(app_constants.GENERAL_THREAD) - self.admin_db.DONE.connect(done) - self.admin_db.DONE.connect(lambda: app_constants.NOTIF_BAR.add_text("Application requires a restart")) - self.admin_db.DONE.connect(self.admin_db.deleteLater) - self.admin_db.DATA_COUNT.connect(app_widget.prog.setMaximum) - self.admin_db.PROGRESS.connect(app_widget.prog.setValue) - self.admin_db_method_invoker.connect(self.admin_db.from_v021_to_v022) - self.admin_db_method_invoker.connect(app_widget.show) - app_widget.adjustSize() - db_p = os.path.join(os.path.split(database.db_constants.DB_PATH)[0], 'sadpanda.db') - self.admin_db_method_invoker.emit(db_p) - elif app_constants.FIRST_TIME_LEVEL < 7: - log_i('Invoking first time level {}'.format(7)) - app_constants.INTERNAL_LEVEL = 7 - if app_constants.EXTERNAL_VIEWER_ARGS == '{file}': - app_constants.EXTERNAL_VIEWER_ARGS = '{$file}' - settings.set('{$file}','Advanced', 'external viewer args') - settings.save() - elif app_constants.FIRST_TIME_LEVEL < 8: - log_i('Invoking first time level {}'.format(8)) - app_constants.INTERNAL_LEVEL = 8 - app_widget = misc.AppDialog(self) - app_widget.note_info.setText("IMPORTANT: Application restart is required when done") - app_widget.restart_info.hide() - self.admin_db = gallerydb.AdminDB() - self.admin_db.moveToThread(app_constants.GENERAL_THREAD) - self.admin_db.DONE.connect(done) - self.admin_db.DONE.connect(lambda: app_constants.NOTIF_BAR.add_text("Application requires a restart")) - self.admin_db.DONE.connect(self.admin_db.deleteLater) - self.admin_db.DATA_COUNT.connect(app_widget.prog.setMaximum) - self.admin_db.PROGRESS.connect(app_widget.prog.setValue) - self.admin_db_method_invoker.connect(self.admin_db.rebuild_database) - self.admin_db_method_invoker.connect(app_widget.show) - app_widget.adjustSize() - self.admin_db_method_invoker.emit(True) - - else: - done() - - def initUI(self): - self.center = QWidget() - self._main_layout = QHBoxLayout(self.center) - self._main_layout.setSpacing(0) - self._main_layout.setContentsMargins(0,0,0,0) - - self.init_stat_bar() - self.manga_views = {} - self._current_manga_view = None - self.default_manga_view = gallery.MangaViews(app_constants.ViewType.Default, self, True) - def refresh_view(): - self.current_manga_view.sort_model.refresh() - self.db_startup.DONE.connect(refresh_view) - self.manga_list_view = self.default_manga_view.list_view - self.manga_table_view = self.default_manga_view.table_view - self.manga_list_view.gallery_model.STATUSBAR_MSG.connect(self.stat_temp_msg) - self.manga_list_view.STATUS_BAR_MSG.connect(self.stat_temp_msg) - self.manga_table_view.STATUS_BAR_MSG.connect(self.stat_temp_msg) - - self.sidebar_list = misc_db.SideBarWidget(self) - self.db_startup.DONE.connect(self.sidebar_list.tags_tree.setup_tags) - self._main_layout.addWidget(self.sidebar_list) - self.current_manga_view = self.default_manga_view - - #self.display_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.download_window = io_misc.GalleryDownloader(self) - self.download_window.hide() - # init toolbar - self.init_toolbar() - - log_d('Create statusbar: OK') - - self.system_tray = misc.SystemTray(QIcon(app_constants.APP_ICO_PATH), self) - app_constants.SYSTEM_TRAY = self.system_tray - tray_menu = QMenu(self) - self.system_tray.setContextMenu(tray_menu) - self.system_tray.setToolTip('Happypanda {}'.format(app_constants.vs)) - tray_quit = QAction('Quit', tray_menu) - tray_update = tray_menu.addAction('Check for update') - tray_update.triggered.connect(self._check_update) - tray_menu.addAction(tray_quit) - tray_quit.triggered.connect(self.close) - self.system_tray.show() - def tray_activate(r=None): - if not r or r == QSystemTrayIcon.Trigger: - self.showNormal() - self.activateWindow() - self.system_tray.messageClicked.connect(tray_activate) - self.system_tray.activated.connect(tray_activate) - log_d('Create system tray: OK') - #self.display.addWidget(self.chapter_main) - - self.setCentralWidget(self.center) - self.setWindowIcon(QIcon(app_constants.APP_ICO_PATH)) - - props = settings.win_read(self, 'AppWindow') - if props.resize: - x, y = props.resize - self.resize(x, y) - else: - self.resize(app_constants.MAIN_W, app_constants.MAIN_H) - self.setMinimumWidth(600) - self.setMinimumHeight(400) - misc.centerWidget(self) - self.init_spinners() - self.show() - log_d('Show window: OK') - - self.notification_bar = misc.NotificationOverlay(self) - p = self.toolbar.pos() - self.notification_bar.move(p.x(), p.y() + self.toolbar.height()) - self.notification_bar.resize(self.width()) - self.notif_bubble = misc.AppBubble(self) - app_constants.NOTIF_BAR = self.notification_bar - app_constants.NOTIF_BUBBLE = self.notif_bubble - - log_d('Create notificationbar: OK') - - log_d('Window Create: OK') - - def _check_update(self): - class upd_chk(QObject): - UPDATE_CHECK = pyqtSignal(str) - def __init__(self, **kwargs): - super().__init__(**kwargs) - def fetch_vs(self): - import requests - import time - log_d('Checking Update') - time.sleep(1.5) - try: - r = requests.get("https://raw.githubusercontent.com/Pewpews/happypanda/master/VS.txt") - a = r.text - vs = a.strip() - self.UPDATE_CHECK.emit(vs) - except: - log.exception('Checking Update: FAIL') - self.UPDATE_CHECK.emit('this is a very long text which is sure to be over limit') - - def check_update(vs): - log_i('Received version: {}\nCurrent version: {}'.format(vs, app_constants.vs)) - if vs != app_constants.vs: - if len(vs) < 10: - self.notification_bar.begin_show() - self.notification_bar.add_text("Version {} of Happypanda is".format(vs) + " available. Click here to update!", False) - self.notification_bar.clicked.connect(lambda: utils.open_web_link('https://github.com/Pewpews/happypanda/releases')) - self.notification_bar.set_clickable(True) - else: - self.notification_bar.add_text("An error occurred while checking for new version") - - self.update_instance = upd_chk() - thread = QThread(self) - self.update_instance.moveToThread(thread) - thread.started.connect(self.update_instance.fetch_vs) - self.update_instance.UPDATE_CHECK.connect(check_update) - self.update_instance.UPDATE_CHECK.connect(self.update_instance.deleteLater) - thread.finished.connect(thread.deleteLater) - thread.start() - - def _web_metadata_picker(self, gallery, title_url_list, queue, parent=None): - if not parent: - parent = self - text = "Which gallery do you want to extract metadata from?" - s_gallery_popup = misc.SingleGalleryChoices(gallery, title_url_list, - text, parent) - s_gallery_popup.USER_CHOICE.connect(queue.put) - - def get_metadata(self, gal=None): - if not app_constants.GLOBAL_EHEN_LOCK: - metadata_spinner = misc.Spinner(self) - metadata_spinner.set_text("Metadata") - metadata_spinner.set_size(55) - thread = QThread(self) - thread.setObjectName('App.get_metadata') - fetch_instance = fetch.Fetch() - if gal: - if not isinstance(gal, list): - galleries = [gal] - else: - galleries = gal - else: - if app_constants.CONTINUE_AUTO_METADATA_FETCHER: - galleries = [g for g in self.current_manga_view.gallery_model._data if not g.exed] - else: - galleries = self.current_manga_view.gallery_model._data - if not galleries: - self.notification_bar.add_text('Looks like we\'ve already gone through all galleries!') - return None - fetch_instance.galleries = galleries - - self.notification_bar.begin_show() - fetch_instance.moveToThread(thread) - - def done(status): - self.notification_bar.end_show() - gallerydb.execute(database.db.DBBase.end, True) - try: - fetch_instance.deleteLater() - except RuntimeError: - pass - if not isinstance(status, bool): - galleries = [] - for tup in status: - galleries.append(tup[0]) - - class GalleryContextMenu(QMenu): - app_instance = self - def __init__(self, parent=None): - super().__init__(parent) - show_in_library_act = self.addAction('Show in library') - show_in_library_act.triggered.connect(self.show_in_library) - - def show_in_library(self): - index = gallery.CommonView.find_index(self.app_instance.get_current_view(), self.gallery_widget.gallery.id, True) - if index: - gallery.CommonView.scroll_to_index(self.app_instance.get_current_view(), index) - - g_popup = io_misc.GalleryPopup(('Fecthing metadata for these galleries failed.' + ' Check happypanda.log for details.', galleries), self, menu=GalleryContextMenu) - errors = {g[0].id: g[1] for g in status} - for g_item in g_popup.get_all_items(): - g_item.extra_text.setText("{}".format(errors[g_item.gallery.id])) - g_item.extra_text.show() - g_popup.graphics_blur.setEnabled(False) - close_button = g_popup.add_buttons('Close')[0] - close_button.clicked.connect(g_popup.close) - - database.db.DBBase.begin() - fetch_instance.GALLERY_PICKER.connect(self._web_metadata_picker) - fetch_instance.GALLERY_EMITTER.connect(self.default_manga_view.replace_gallery) - fetch_instance.AUTO_METADATA_PROGRESS.connect(self.notification_bar.add_text) - thread.started.connect(fetch_instance.auto_web_metadata) - fetch_instance.FINISHED.connect(done) - fetch_instance.FINISHED.connect(metadata_spinner.before_hide) - thread.finished.connect(thread.deleteLater) - thread.start() - #fetch_instance.auto_web_metadata() - metadata_spinner.show() - else: - self.notif_bubble.update_text("Oops!", "Auto metadata fetcher is already running...") - - def init_stat_bar(self): - self.status_bar = self.statusBar() - self.status_bar.setSizeGripEnabled(False) - self.stat_info = QLabel() - self.stat_info.setIndent(5) - self.sort_main = QAction("Asc", self) - sort_menu = QMenu() - self.sort_main.setMenu(sort_menu) - s_by_title = QAction("Title", sort_menu) - s_by_artist = QAction("Artist", sort_menu) - sort_menu.addAction(s_by_title) - sort_menu.addAction(s_by_artist) - self.status_bar.addPermanentWidget(self.stat_info) - #self.status_bar.addAction(self.sort_main) - self.temp_msg = QLabel() - self.temp_timer = QTimer() - - app_constants.STAT_MSG_METHOD = self.stat_temp_msg - - def stat_temp_msg(self, msg): - self.temp_timer.stop() - self.temp_msg.setText(msg) - self.status_bar.addWidget(self.temp_msg) - self.temp_timer.timeout.connect(self.temp_msg.clear) - self.temp_timer.setSingleShot(True) - self.temp_timer.start(5000) - - def stat_row_info(self): - r = self.current_manga_view.get_current_view().sort_model.rowCount() - t = self.current_manga_view.get_current_view().gallery_model.rowCount() - g_l = self.get_current_view().sort_model.current_gallery_list - if g_l: - self.stat_info.setText( - "{} | Showing {} of {} ".format(g_l.name, r, t)) - else: - self.stat_info.setText("Showing {} of {} ".format(r, t)) - - def set_current_manga_view(self, v): - self.current_manga_view = v - - @property - def current_manga_view(self): - return self._current_manga_view - - @current_manga_view.setter - def current_manga_view(self, new_view): - if self._current_manga_view: - self._main_layout.takeAt(1) - self._current_manga_view = new_view - self._main_layout.insertLayout(1, new_view.view_layout, 1) - self.stat_row_info() - - def init_spinners(self): - # fetching spinner - self.data_fetch_spinner = misc.Spinner(self, "center") - self.data_fetch_spinner.set_size(80) - - self.manga_list_view.gallery_model.ADD_MORE.connect(self.data_fetch_spinner.show) - self.db_startup.START.connect(self.data_fetch_spinner.show) - self.db_startup.PROGRESS.connect(self.data_fetch_spinner.set_text) - self.manga_list_view.gallery_model.ADDED_ROWS.connect(self.data_fetch_spinner.before_hide) - self.db_startup.DONE.connect(self.data_fetch_spinner.before_hide) - - ## deleting spinner - #self.gallery_delete_spinner = misc.Spinner(self) - #self.gallery_delete_spinner.set_size(40,40) - ##self.gallery_delete_spinner.set_text('Removing...') - #self.manga_list_view.gallery_model.rowsAboutToBeRemoved.connect(self.gallery_delete_spinner.show) - #self.manga_list_view.gallery_model.rowsRemoved.connect(self.gallery_delete_spinner.before_hide) - - - def search(self, srch_string): - "Args should be Search Enums" - self.search_bar.setText(srch_string) - self.search_backward.setVisible(True) - args = [] - if app_constants.GALLERY_SEARCH_REGEX: - args.append(app_constants.Search.Regex) - if app_constants.GALLERY_SEARCH_CASE: - args.append(app_constants.Search.Case) - if app_constants.GALLERY_SEARCH_STRICT: - args.append(app_constants.Search.Strict) - self.current_manga_view.get_current_view().sort_model.init_search(srch_string, args) - old_cursor_pos = self._search_cursor_pos[0] - self.search_bar.end(False) - if self.search_bar.cursorPosition() != old_cursor_pos + 1: - self.search_bar.setCursorPosition(old_cursor_pos) - - def switch_display(self): - "Switches between fav and catalog display" - if self.current_manga_view.fav_is_current(): - self.tab_manager.library_btn.click() - else: - self.tab_manager.favorite_btn.click() - - def settings(self): - sett = settingsdialog.SettingsDialog(self) - sett.scroll_speed_changed.connect(self.manga_list_view.updateGeometries) - #sett.show() - - def init_toolbar(self): - self.toolbar = QToolBar() - self.toolbar.adjustSize() - #self.toolbar.setFixedHeight() - self.toolbar.setWindowTitle("Show") # text for the contextmenu - #self.toolbar.setStyleSheet("QToolBar {border:0px}") # make it user defined? - self.toolbar.setMovable(False) - self.toolbar.setFloatable(False) - #self.toolbar.setIconSize(QSize(20,20)) - self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) - self.toolbar.setIconSize(QSize(20,20)) - - spacer_start = QWidget() # aligns the first actions properly - spacer_start.setFixedSize(QSize(10, 1)) - self.toolbar.addWidget(spacer_start) - - def switch_view(fav): - if fav: - self.default_manga_view.get_current_view().sort_model.fav_view() - else: - self.default_manga_view.get_current_view().sort_model.catalog_view() - - self.tab_manager = misc_db.ToolbarTabManager(self.toolbar, self) - self.tab_manager.favorite_btn.clicked.connect(lambda: switch_view(True)) - self.tab_manager.library_btn.click() - self.tab_manager.library_btn.clicked.connect(lambda: switch_view(False)) - - self.addition_tab = self.tab_manager.addTab("Inbox", app_constants.ViewType.Addition) - - gallery_k = QKeySequence('Alt+G') - new_gallery_k = QKeySequence('Ctrl+N') - new_galleries_k = QKeySequence('Ctrl+Shift+N') - new_populate_k = QKeySequence('Ctrl+Alt+N') - scan_galleries_k = QKeySequence('Ctrl+Alt+S') - open_random_k = QKeySequence(QKeySequence.Open) - get_all_metadata_k = QKeySequence('Ctrl+Alt+M') - gallery_downloader_k = QKeySequence('Ctrl+Alt+D') - - gallery_menu = QMenu() - gallery_action = QToolButton() - gallery_action.setShortcut(gallery_k) - gallery_action.setText('Gallery ') - gallery_action.setPopupMode(QToolButton.InstantPopup) - gallery_action.setToolTip('Contains various gallery related features') - gallery_action.setMenu(gallery_menu) - add_gallery_icon = QIcon(app_constants.PLUS_PATH) - gallery_action_add = QAction(add_gallery_icon, "Add single gallery...", self) - gallery_action_add.triggered.connect(lambda: gallery.CommonView.spawn_dialog(self)) - gallery_action_add.setToolTip('Add a single gallery thoroughly') - gallery_action_add.setShortcut(new_gallery_k) - gallery_menu.addAction(gallery_action_add) - add_more_action = QAction(add_gallery_icon, "Add galleries...", self) - add_more_action.setStatusTip('Add galleries from different folders') - add_more_action.setShortcut(new_galleries_k) - add_more_action.triggered.connect(lambda: self.populate(True)) - gallery_menu.addAction(add_more_action) - populate_action = QAction(add_gallery_icon, "Populate from directory/archive...", self) - populate_action.setStatusTip('Populates the DB with galleries from a single folder or archive') - populate_action.triggered.connect(self.populate) - populate_action.setShortcut(new_populate_k) - gallery_menu.addAction(populate_action) - gallery_menu.addSeparator() - metadata_action = QAction('Get metadata for all galleries', self) - metadata_action.triggered.connect(self.get_metadata) - metadata_action.setShortcut(get_all_metadata_k) - gallery_menu.addAction(metadata_action) - scan_galleries_action = QAction('Scan for new galleries', self) - scan_galleries_action.triggered.connect(self.scan_for_new_galleries) - scan_galleries_action.setStatusTip('Scan monitored folders for new galleries') - scan_galleries_action.setShortcut(scan_galleries_k) - gallery_menu.addAction(scan_galleries_action) - - gallery_action_random = gallery_menu.addAction("Open random gallery") - gallery_action_random.triggered.connect(lambda: gallery.CommonView.open_random_gallery(self.get_current_view())) - gallery_action_random.setShortcut(open_random_k) - self.toolbar.addWidget(gallery_action) - - tools_k = QKeySequence('Alt+T') - misc_action = QToolButton() - misc_action.setText('Tools ') - misc_action.setShortcut(tools_k) - misc_action_menu = QMenu() - misc_action.setMenu(misc_action_menu) - misc_action.setPopupMode(QToolButton.InstantPopup) - misc_action.setToolTip("Contains misc. features") - gallery_downloader = QAction("Gallery Downloader", misc_action_menu) - gallery_downloader.triggered.connect(self.download_window.show) - gallery_downloader.setShortcut(gallery_downloader_k) - misc_action_menu.addAction(gallery_downloader) - duplicate_check_simple = QAction("Simple Duplicate Finder", misc_action_menu) - duplicate_check_simple.triggered.connect(lambda: self.duplicate_check()) # triggered emits False - misc_action_menu.addAction(duplicate_check_simple) - self.toolbar.addWidget(misc_action) - - # debug specfic code - if app_constants.DEBUG: - def debug_func(): - print(self.current_manga_view.gallery_model.rowCount()) - print(self.current_manga_view.sort_model.rowCount()) - - debug_btn = QToolButton() - debug_btn.setText("DEBUG BUTTON") - self.toolbar.addWidget(debug_btn) - debug_btn.clicked.connect(debug_func) - - spacer_middle = QWidget() # aligns buttons to the right - spacer_middle.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.toolbar.addWidget(spacer_middle) - - sort_k = QKeySequence('Alt+S') - - def set_new_sort(s): - self.current_manga_view.list_view.sort(s) - - sort_action = QToolButton() - sort_action.setShortcut(sort_k) - sort_action.setIcon(QIcon(app_constants.SORT_PATH)) - sort_menu = misc.SortMenu(self, self.toolbar) - sort_menu.new_sort.connect(set_new_sort) - sort_action.setMenu(sort_menu) - sort_action.setPopupMode(QToolButton.InstantPopup) - self.toolbar.addWidget(sort_action) - - togle_view_k = QKeySequence('Alt+Space') - - self.grid_toggle_g_icon = QIcon(app_constants.GRID_PATH) - self.grid_toggle_l_icon = QIcon(app_constants.LIST_PATH) - self.grid_toggle = QToolButton() - self.grid_toggle.setShortcut(togle_view_k) - if self.current_manga_view.current_view == gallery.MangaViews.View.List: - self.grid_toggle.setIcon(self.grid_toggle_l_icon) - else: - self.grid_toggle.setIcon(self.grid_toggle_g_icon) - self.grid_toggle.setObjectName('gridtoggle') - self.grid_toggle.clicked.connect(self.toggle_view) - self.toolbar.addWidget(self.grid_toggle) - - spacer_mid2 = QWidget() - spacer_mid2.setFixedSize(QSize(5, 1)) - self.toolbar.addWidget(spacer_mid2) - - - self.search_bar = misc.LineEdit() - search_options = self.search_bar.addAction(QIcon(app_constants.SEARCH_OPTIONS_PATH), QLineEdit.TrailingPosition) - search_options_menu = QMenu(self) - search_options.triggered.connect(lambda: search_options_menu.popup(QCursor.pos())) - search_options.setMenu(search_options_menu) - case_search_option = search_options_menu.addAction('Case Sensitive') - case_search_option.setCheckable(True) - case_search_option.setChecked(app_constants.GALLERY_SEARCH_CASE) - - def set_search_case(b): - app_constants.GALLERY_SEARCH_CASE = b - settings.set(b, 'Application', 'gallery search case') - settings.save() - - case_search_option.toggled.connect(set_search_case) - - strict_search_option = search_options_menu.addAction('Match whole terms') - strict_search_option.setCheckable(True) - strict_search_option.setChecked(app_constants.GALLERY_SEARCH_STRICT) - - - regex_search_option = search_options_menu.addAction('Regex') - regex_search_option.setCheckable(True) - regex_search_option.setChecked(app_constants.GALLERY_SEARCH_REGEX) - - def set_search_strict(b): - if b: - if regex_search_option.isChecked(): - regex_search_option.toggle() - app_constants.GALLERY_SEARCH_STRICT = b - settings.set(b, 'Application', 'gallery search strict') - settings.save() - - strict_search_option.toggled.connect(set_search_strict) - - def set_search_regex(b): - if b: - if strict_search_option.isChecked(): - strict_search_option.toggle() - app_constants.GALLERY_SEARCH_REGEX = b - settings.set(b, 'Application', 'allow search regex') - settings.save() - - regex_search_option.toggled.connect(set_search_regex) - - self.search_bar.setObjectName('search_bar') - self.search_timer = QTimer(self) - self.search_timer.setSingleShot(True) - self.search_timer.timeout.connect(lambda: self.search(self.search_bar.text())) - self._search_cursor_pos = [0, 0] - def set_cursor_pos(old, new): - self._search_cursor_pos[0] = old - self._search_cursor_pos[1] = new - self.search_bar.cursorPositionChanged.connect(set_cursor_pos) - - if app_constants.SEARCH_AUTOCOMPLETE: - completer = QCompleter(self) - completer_view = misc.CompleterPopupView() - completer.setPopup(completer_view) - completer_view._setup() - completer.setModel(self.manga_list_view.gallery_model) - completer.setCaseSensitivity(Qt.CaseInsensitive) - completer.setCompletionMode(QCompleter.PopupCompletion) - completer.setCompletionRole(Qt.DisplayRole) - completer.setCompletionColumn(app_constants.TITLE) - completer.setFilterMode(Qt.MatchContains) - self.search_bar.setCompleter(completer) - self.search_bar.returnPressed.connect(lambda: self.search(self.search_bar.text())) - if not app_constants.SEARCH_ON_ENTER: - self.search_bar.textEdited.connect(lambda: self.search_timer.start(800)) - self.search_bar.setPlaceholderText("Search title, artist, namespace & tags") - self.search_bar.setMinimumWidth(150) - #self.search_bar.setMaximumWidth(500) - self.search_bar.setFixedHeight(self.toolbar.height() * 2) - self.manga_list_view.sort_model.HISTORY_SEARCH_TERM.connect(lambda a: self.search_bar.setText(a)) - self.toolbar.addWidget(self.search_bar) - - def search_history(_, back=True): # clicked signal passes a bool - sort_model = self.manga_list_view.sort_model - nav = sort_model.PREV if back else sort_model.NEXT - history_term = sort_model.navigate_history(nav) - if back: - self.search_forward.setVisible(True) - - back_k = QKeySequence(QKeySequence.Back) - forward_k = QKeySequence(QKeySequence.Forward) - - search_backbutton = QToolButton(self.toolbar) - search_backbutton.setText(u'\u25C0') - search_backbutton.setFixedWidth(15) - search_backbutton.clicked.connect(search_history) - search_backbutton.setShortcut(back_k) - self.search_backward = self.toolbar.addWidget(search_backbutton) - self.search_backward.setVisible(False) - search_forwardbutton = QToolButton(self.toolbar) - search_forwardbutton.setText(u'\u25B6') - search_forwardbutton.setFixedWidth(15) - search_forwardbutton.clicked.connect(lambda: search_history(None, False)) - search_forwardbutton.setShortcut(forward_k) - self.search_forward = self.toolbar.addWidget(search_forwardbutton) - self.search_forward.setVisible(False) - - spacer_end = QWidget() # aligns settings action properly - spacer_end.setFixedSize(QSize(10, 1)) - self.toolbar.addWidget(spacer_end) - - settings_k = QKeySequence("Ctrl+P") - - settings_act = QToolButton(self.toolbar) - settings_act.setShortcut(settings_k) - settings_act.setIcon(QIcon(app_constants.SETTINGS_PATH)) - settings_act.clicked.connect(self.settings) - self.toolbar.addWidget(settings_act) - - spacer_end2 = QWidget() # aligns About action properly - spacer_end2.setFixedSize(QSize(5, 1)) - self.toolbar.addWidget(spacer_end2) - self.addToolBar(self.toolbar) - - def get_current_view(self): - return self.current_manga_view.get_current_view() - - def toggle_view(self): - """ - Toggles the current display view - """ - if self.current_manga_view.current_view == gallery.MangaViews.View.Table: - self.current_manga_view.changeTo(self.current_manga_view.m_l_view_index) - self.grid_toggle.setIcon(self.grid_toggle_l_icon) - else: - self.current_manga_view.changeTo(self.current_manga_view.m_t_view_index) - self.grid_toggle.setIcon(self.grid_toggle_g_icon) - - # TODO: Improve this so that it adds to the gallery dialog, - # so user can edit data before inserting (make it a choice) - def populate(self, mixed=None): - "Populates the database with gallery from local drive'" - - if mixed: - gallery_view = misc.GalleryListView(self, True) - gallery_view.SERIES.connect(self.gallery_populate) - gallery_view.show() - else: - msg_box = misc.BasePopup(self) - l = QVBoxLayout() - msg_box.main_widget.setLayout(l) - l.addWidget(QLabel('Directory or Archive?')) - l.addLayout(msg_box.buttons_layout) - - def from_dir(): - path = QFileDialog.getExistingDirectory(self, "Choose a directory containing your galleries") - if not path: - return - msg_box.close() - app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY = True - self.gallery_populate(path, True) - def from_arch(): - path = QFileDialog.getOpenFileName(self, 'Choose an archive containing your galleries', - filter=utils.FILE_FILTER) - path = [path[0]] - if not all(path) or not path: - return - msg_box.close() - app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY = True - self.gallery_populate(path, True) - - buttons = msg_box.add_buttons('Directory', 'Archive', 'Close') - buttons[2].clicked.connect(msg_box.close) - buttons[0].clicked.connect(from_dir) - buttons[1].clicked.connect(from_arch) - msg_box.adjustSize() - msg_box.show() - - def gallery_populate(self, path, validate=False): - "Scans the given path for gallery to add into the DB" - if len(path) is not 0: - data_thread = QThread(self) - data_thread.setObjectName('General gallery populate') - self.addition_tab.click() - self.g_populate_inst = fetch.Fetch() - self.g_populate_inst.series_path = path - self._g_populate_count = 0 - - fetch_spinner = misc.Spinner(self) - fetch_spinner.set_size(60) - fetch_spinner.set_text("Populating") - fetch_spinner.show() - - def finished(status): - fetch_spinner.hide() - if not status: - log_e('Populating DB from gallery folder: Nothing was added!') - self.notif_bubble.update_text("Gallery Populate", - "Nothing was added. Check happypanda_log for details..") - - def skipped_gs(s_list): - "Skipped galleries" - msg_box = QMessageBox(self) - msg_box.setIcon(QMessageBox.Question) - msg_box.setText('Do you want to view skipped paths?') - msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - msg_box.setDefaultButton(QMessageBox.No) - if msg_box.exec() == QMessageBox.Yes: - list_wid = QTableWidget(self) - list_wid.setAttribute(Qt.WA_DeleteOnClose) - list_wid.setRowCount(len(s_list)) - list_wid.setColumnCount(2) - list_wid.setAlternatingRowColors(True) - list_wid.setEditTriggers(list_wid.NoEditTriggers) - list_wid.setHorizontalHeaderLabels(['Reason', 'Path']) - list_wid.setSelectionBehavior(list_wid.SelectRows) - list_wid.setSelectionMode(list_wid.SingleSelection) - list_wid.setSortingEnabled(True) - list_wid.verticalHeader().hide() - list_wid.setAutoScroll(False) - for x, g in enumerate(s_list): - list_wid.setItem(x, 0, QTableWidgetItem(g[1])) - list_wid.setItem(x, 1, QTableWidgetItem(g[0])) - list_wid.resizeColumnsToContents() - list_wid.setWindowTitle('{} skipped paths'.format(len(s_list))) - list_wid.setWindowFlags(Qt.Window) - list_wid.resize(900,400) - - list_wid.doubleClicked.connect(lambda i: utils.open_path(list_wid.item(i.row(), 1).text(), list_wid.item(i.row(), 1).text())) - - list_wid.show() - - def a_progress(prog): - fetch_spinner.set_text("Populating... {}/{}".format(prog, self._g_populate_count)) - - def add_to_model(gallery): - self.addition_tab.view.add_gallery(gallery, app_constants.KEEP_ADDED_GALLERIES) - - def set_count(c): - self._g_populate_count = c - - self.g_populate_inst.moveToThread(data_thread) - self.g_populate_inst.PROGRESS.connect(a_progress) - self.g_populate_inst.DATA_COUNT.connect(set_count) - self.g_populate_inst.LOCAL_EMITTER.connect(add_to_model) - self.g_populate_inst.FINISHED.connect(finished) - self.g_populate_inst.FINISHED.connect(self.g_populate_inst.deleteLater) - self.g_populate_inst.SKIPPED.connect(skipped_gs) - data_thread.finished.connect(data_thread.deleteLater) - data_thread.started.connect(self.g_populate_inst.local) - data_thread.start() - #self.g_populate_inst.local() - log_i('Populating DB from directory/archive') - - def scan_for_new_galleries(self): - available_folders = app_constants.ENABLE_MONITOR and \ - app_constants.MONITOR_PATHS and all(app_constants.MONITOR_PATHS) - if available_folders and not app_constants.SCANNING_FOR_GALLERIES: - app_constants.SCANNING_FOR_GALLERIES = True - self.notification_bar.add_text("Scanning for new galleries...") - log_i('Scanning for new galleries...') - try: - class ScanDir(QObject): - finished = pyqtSignal() - fetch_inst = fetch.Fetch(self) - def __init__(self, addition_view, addition_tab, parent=None): - super().__init__(parent) - self.addition_view = addition_view - self.addition_tab = addition_tab - self._switched = False - - def switch_tab(self): - if not self._switched: - self.addition_tab.click() - self._switched = True - - def scan_dirs(self): - paths = [] - for p in app_constants.MONITOR_PATHS: - if os.path.exists(p): - dir_content = scandir.scandir(p) - for d in dir_content: - paths.append(d.path) - else: - log_e("Monitored path does not exists: {}".format(p.encode(errors='ignore'))) - - self.fetch_inst.series_path = paths - self.fetch_inst.LOCAL_EMITTER.connect(lambda g:self.addition_view.add_gallery(g, app_constants.KEEP_ADDED_GALLERIES)) - self.fetch_inst.LOCAL_EMITTER.connect(self.switch_tab) - self.fetch_inst.local() - #contents = [] - #for g in self.scanned_data: - # contents.append(g) - - #paths = sorted(paths) - #new_galleries = [] - #for x in contents: - # y = utils.b_search(paths, os.path.normcase(x.path)) - # if not y: - # new_galleries.append(x) - self.finished.emit() - self.deleteLater() - #if app_constants.LOOK_NEW_GALLERY_AUTOADD: - # QTimer.singleShot(10000, self.gallery_populate(final_paths)) - # return - - - def finished(): app_constants.SCANNING_FOR_GALLERIES = False; - - new_gall_spinner = misc.Spinner(self) - new_gall_spinner.set_text("Gallery Scan") - new_gall_spinner.show() - - thread = QThread(self) - self.scan_inst = ScanDir(self.addition_tab.view, self.addition_tab) - self.scan_inst.moveToThread(thread) - self.scan_inst.finished.connect(finished) - self.scan_inst.finished.connect(new_gall_spinner.before_hide) - thread.started.connect(self.scan_inst.scan_dirs) - #self.scan_inst.scan_dirs() - thread.finished.connect(thread.deleteLater) - thread.start() - except: - self.notification_bar.add_text('An error occured while attempting to scan for new galleries. Check happypanda.log.') - log.exception('An error occured while attempting to scan for new galleries.') - app_constants.SCANNING_FOR_GALLERIES = False - else: - self.notification_bar.add_text("Please specify directory in settings to scan for new galleries!") - - def dragEnterEvent(self, event): - if event.mimeData().hasUrls(): - event.acceptProposedAction() - else: - super().dragEnterEvent(event) - - def dropEvent(self, event): - acceptable = [] - unaccept = [] - for u in event.mimeData().urls(): - path = u.toLocalFile() - if os.path.isdir(path) or path.endswith(utils.ARCHIVE_FILES): - acceptable.append(path) - else: - unaccept.append(path) - log_i('Acceptable dropped items: {}'.format(len(acceptable))) - log_i('Unacceptable dropped items: {}'.format(len(unaccept))) - log_d('Dropped items: {}\n{}'.format(acceptable, unaccept).encode(errors='ignore')) - if acceptable: - self.notification_bar.add_text('Adding dropped items...') - log_i('Adding dropped items') - l = len(acceptable) == 1 - f_item = acceptable[0] - if f_item.endswith(utils.ARCHIVE_FILES): - f_item = utils.check_archive(f_item) - else: - f_item = utils.recursive_gallery_check(f_item) - f_item_l = len(f_item) < 2 - subfolder_as_c = not app_constants.SUBFOLDER_AS_GALLERY - if l and subfolder_as_c or l and f_item_l: - g_d = gallerydialog.GalleryDialog(self, acceptable[0]) - g_d.show() - else: - self.gallery_populate(acceptable, True) - event.accept() - else: - text = 'File not supported' if len(unaccept) < 2 else 'Files not supported' - self.notification_bar.add_text(text) - - if unaccept: - self.notification_bar.add_text('Some unsupported files did not get added') - super().dropEvent(event) - - def resizeEvent(self, event): - try: - self.notification_bar.resize(event.size().width()) - except AttributeError: - pass - self.move_listener.emit() - return super().resizeEvent(event) - - def moveEvent(self, event): - self.move_listener.emit() - return super().moveEvent(event) - - def showEvent(self, event): - return super().showEvent(event) - - def cleanup_exit(self): - self.system_tray.hide() - # watchers - try: - self.watchers.stop_all() - except AttributeError: - pass - - # settings - settings.set(self.manga_list_view.current_sort, 'General', 'current sort') - settings.set(app_constants.IGNORE_PATHS, 'Application', 'ignore paths') - if not self.isMaximized(): - settings.win_save(self, 'AppWindow') - - # temp dir - try: - for root, dirs, files in scandir.walk('temp', topdown=False): - for name in files: - os.remove(os.path.join(root, name)) - for name in dirs: - os.rmdir(os.path.join(root, name)) - log_d('Flush temp on exit: OK') - except: - log.exception('Flush temp on exit: FAIL') - - # DB - try: - log_i("Analyzing database...") - gallerydb.GalleryDB.analyze() - log_i("Closing database...") - gallerydb.GalleryDB.close() - except: - pass - self.download_window.close() - - # check if there is db activity - if not gallerydb.method_queue.empty(): - class DBActivityChecker(QObject): - FINISHED = pyqtSignal() - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def check(self): - gallerydb.method_queue.join() - self.FINISHED.emit() - self.deleteLater() - - db_activity = DBActivityChecker() - db_spinner = misc.Spinner(self) - self.db_activity_checker.connect(db_activity.check) - db_activity.moveToThread(app_constants.GENERAL_THREAD) - db_activity.FINISHED.connect(db_spinner.close) - db_spinner.set_text('DB Activity') - db_spinner.show() - self.db_activity_checker.emit() - msg_box = QMessageBox(self) - msg_box.setText('Database activity detected!') - msg_box.setInformativeText("Closing now might result in data loss." + " Do you still want to close?\n(Wait for the activity spinner to hide before closing)") - msg_box.setIcon(QMessageBox.Critical) - msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - msg_box.setDefaultButton(QMessageBox.No) - if msg_box.exec() == QMessageBox.Yes: - return 1 - else: - return 2 - else: - return 0 - - def duplicate_check(self, simple=True): - try: - self.duplicate_check_invoker.disconnect() - except TypeError: - pass - mode = 'simple' if simple else 'advanced' - log_i('Checking for duplicates in mode: {}'.format(mode)) - notifbar = app_constants.NOTIF_BAR - notifbar.add_text('Checking for duplicates...') - duplicate_spinner = misc.Spinner(self) - duplicate_spinner.set_text("Duplicate Check") - duplicate_spinner.show() - dup_tab = self.tab_manager.addTab("Duplicate", app_constants.ViewType.Duplicate) - dup_tab.view.set_delete_proxy(self.default_manga_view.gallery_model) - - class DuplicateCheck(QObject): - found_duplicates = pyqtSignal(tuple) - finished = pyqtSignal() - def __init__(self): - super().__init__() - - def checkSimple(self, model): - galleries = model._data - - duplicates = [] - for n, g in enumerate(galleries, 1): - notifbar.add_text('Checking gallery {}'.format(n)) - log_d('Checking gallery {}'.format(g.title.encode(errors="ignore"))) - for y in galleries: - title = g.title.strip().lower() == y.title.strip().lower() - path = os.path.normcase(g.path) == os.path.normcase(y.path) - if g.id != y.id and (title or path): - if g not in duplicates: - duplicates.append(y) - duplicates.append(g) - self.found_duplicates.emit((g, y)) - self.finished.emit() - - self._d_checker = DuplicateCheck() - self._d_checker.moveToThread(app_constants.GENERAL_THREAD) - self._d_checker.found_duplicates.connect(lambda t: dup_tab.view.add_gallery(t, record_time=True)) - self._d_checker.finished.connect(dup_tab.click) - self._d_checker.finished.connect(self._d_checker.deleteLater) - self._d_checker.finished.connect(duplicate_spinner.before_hide) - if simple: - self.duplicate_check_invoker.connect(self._d_checker.checkSimple) - self.duplicate_check_invoker.emit(self.default_manga_view.gallery_model) - - def excepthook(self, ex_type, ex, tb): - w = misc.AppDialog(self, misc.AppDialog.MESSAGE) - w.show() - log_c(''.join(traceback.format_tb(tb))) - log_c('{}: {}'.format(ex_type, ex)) - traceback.print_exception(ex_type, ex, tb) - - def closeEvent(self, event): - r_code = self.cleanup_exit() - if r_code == 1: - log_d('Force Exit App: OK') - super().closeEvent(event) - elif r_code == 2: - log_d('Ignore Exit App') - event.ignore() - else: - log_d('Normal Exit App: OK') - super().closeEvent(event) + "The application's main window" + + move_listener = pyqtSignal() + db_startup_invoker = pyqtSignal(list) + duplicate_check_invoker = pyqtSignal(gallery.GalleryModel) + admin_db_method_invoker = pyqtSignal(object) + db_activity_checker = pyqtSignal() + graphics_blur = QGraphicsBlurEffect() + + def __init__(self, disable_excepthook=False): + super().__init__() + if not disable_excepthook: + sys.excepthook = self.excepthook + app_constants.GENERAL_THREAD = QThread(self) + app_constants.GENERAL_THREAD.finished.connect(app_constants.GENERAL_THREAD.deleteLater) + app_constants.GENERAL_THREAD.start() + self._db_startup_thread = QThread(self) + self._db_startup_thread.finished.connect(self._db_startup_thread.deleteLater) + self.db_startup = gallerydb.DatabaseStartup() + self._db_startup_thread.start() + self.db_startup.moveToThread(self._db_startup_thread) + self.db_startup.DONE.connect(lambda: self.scan_for_new_galleries() if app_constants.LOOK_NEW_GALLERY_STARTUP else None) + self.db_startup_invoker.connect(self.db_startup.startup) + self.setAcceptDrops(True) + self.initUI() + self.startup() + QTimer.singleShot(3000, self._check_update) + self.setFocusPolicy(Qt.NoFocus) + self.set_shortcuts() + self.graphics_blur.setParent(self) + + def set_shortcuts(self): + quit = QShortcut(QKeySequence('Ctrl+Q'), self, self.close) + search_focus = QShortcut(QKeySequence(QKeySequence.Find), self, lambda:self.search_bar.setFocus(Qt.ShortcutFocusReason)) + prev_view = QShortcut(QKeySequence(QKeySequence.PreviousChild), self, self.switch_display) + next_view = QShortcut(QKeySequence(QKeySequence.NextChild), self, self.switch_display) + help = QShortcut(QKeySequence(QKeySequence.HelpContents), self, lambda:utils.open_web_link("https://github.com/Pewpews/happypanda/wiki")) + + def init_watchers(self): + + def remove_gallery(g): + index = gallery.CommonView.find_index(self.get_current_view(), g.id, True) + if index: + gallery.CommonView.remove_gallery(self.get_current_view(), [index]) + else: + log_e('Could not find gallery to remove from watcher') + + def create_gallery(path): + g_dia = gallerydialog.GalleryDialog(self, path) + g_dia.SERIES.connect(self.default_manga_view.add_gallery) + g_dia.show() + + def update_gallery(g): + index = gallery.CommonView.find_index(self.get_current_view(), g.id) + if index: + gal = index.data(gallery.GalleryModel.GALLERY_ROLE) + gal.path = g.path + gal.chapters = g.chapters + else: + log_e('Could not find gallery to update from watcher') + self.default_manga_view.replace_gallery(g, False) + + def created(path): + c_popup = io_misc.CreatedPopup(path, self) + c_popup.ADD_SIGNAL.connect(create_gallery) + def modified(path, gallery): + mod_popup = io_misc.ModifiedPopup(path, gallery, self) + def deleted(path, gallery): + d_popup = io_misc.DeletedPopup(path, gallery, self) + d_popup.UPDATE_SIGNAL.connect(update_gallery) + d_popup.REMOVE_SIGNAL.connect(remove_gallery) + def moved(new_path, gallery): + mov_popup = io_misc.MovedPopup(new_path, gallery, self) + mov_popup.UPDATE_SIGNAL.connect(update_gallery) + + self.watchers = io_misc.Watchers() + self.watchers.gallery_handler.CREATE_SIGNAL.connect(created) + self.watchers.gallery_handler.MODIFIED_SIGNAL.connect(modified) + self.watchers.gallery_handler.MOVED_SIGNAL.connect(moved) + self.watchers.gallery_handler.DELETED_SIGNAL.connect(deleted) + + def startup(self): + def normalize_first_time(): + settings.set(app_constants.INTERNAL_LEVEL, 'Application', 'first time level') + settings.save() + + def done(status=True): + self.db_startup_invoker.emit(gallery.MangaViews.manga_views) + #self.db_startup.startup() + if app_constants.FIRST_TIME_LEVEL != app_constants.INTERNAL_LEVEL: + normalize_first_time() + else: + settings.set(app_constants.vs, 'Application', 'version') + if app_constants.UPDATE_VERSION != app_constants.vs: + self.notif_bubble.update_text("Happypanda has been updated!", + "Don't forget to check out what's new in this version by clicking here!") + else: + hello = ["Hello!", "Hi!", "Onii-chan!", "Senpai!", "Hisashiburi!", "Welcome!", "Okaerinasai!", "Welcome back!", "Hajimemashite!"] + self.notif_bubble.update_text("{}".format(hello[random.randint(0, len(hello) - 1)]), "Please don't hesitate to report any bugs you find.", 10) + + if app_constants.ENABLE_MONITOR and \ + app_constants.MONITOR_PATHS and all(app_constants.MONITOR_PATHS): + self.init_watchers() + self.download_manager = pewnet.Downloader() + app_constants.DOWNLOAD_MANAGER = self.download_manager + self.download_manager.start_manager(4) + + if app_constants.FIRST_TIME_LEVEL < 4: + log_i('Invoking first time level {}'.format(4)) + app_constants.INTERNAL_LEVEL = 4 + settings.set([], 'Application', 'monitor paths') + settings.set([], 'Application', 'ignore paths') + app_constants.MONITOR_PATHS = [] + app_constants.IGNORE_PATHS = [] + settings.save() + done() + elif app_constants.FIRST_TIME_LEVEL < 5: + log_i('Invoking first time level {}'.format(5)) + app_constants.INTERNAL_LEVEL = 5 + app_widget = misc.AppDialog(self) + app_widget.note_info.setText("IMPORTANT: Application restart is required when done") + app_widget.restart_info.hide() + self.admin_db = gallerydb.AdminDB() + self.admin_db.moveToThread(app_constants.GENERAL_THREAD) + self.admin_db.DONE.connect(done) + self.admin_db.DONE.connect(lambda: app_constants.NOTIF_BAR.add_text("Application requires a restart")) + self.admin_db.DONE.connect(self.admin_db.deleteLater) + self.admin_db.DATA_COUNT.connect(app_widget.prog.setMaximum) + self.admin_db.PROGRESS.connect(app_widget.prog.setValue) + self.admin_db_method_invoker.connect(self.admin_db.from_v021_to_v022) + self.admin_db_method_invoker.connect(app_widget.show) + app_widget.adjustSize() + db_p = os.path.join(os.path.split(database.db_constants.DB_PATH)[0], 'sadpanda.db') + self.admin_db_method_invoker.emit(db_p) + elif app_constants.FIRST_TIME_LEVEL < 7: + log_i('Invoking first time level {}'.format(7)) + app_constants.INTERNAL_LEVEL = 7 + if app_constants.EXTERNAL_VIEWER_ARGS == '{file}': + app_constants.EXTERNAL_VIEWER_ARGS = '{$file}' + settings.set('{$file}','Advanced', 'external viewer args') + settings.save() + elif app_constants.FIRST_TIME_LEVEL < 8: + log_i('Invoking first time level {}'.format(8)) + app_constants.INTERNAL_LEVEL = 8 + app_widget = misc.AppDialog(self) + app_widget.note_info.setText("IMPORTANT: Application restart is required when done") + app_widget.restart_info.hide() + self.admin_db = gallerydb.AdminDB() + self.admin_db.moveToThread(app_constants.GENERAL_THREAD) + self.admin_db.DONE.connect(done) + self.admin_db.DONE.connect(lambda: app_constants.NOTIF_BAR.add_text("Application requires a restart")) + self.admin_db.DONE.connect(self.admin_db.deleteLater) + self.admin_db.DATA_COUNT.connect(app_widget.prog.setMaximum) + self.admin_db.PROGRESS.connect(app_widget.prog.setValue) + self.admin_db_method_invoker.connect(self.admin_db.rebuild_database) + self.admin_db_method_invoker.connect(app_widget.show) + app_widget.adjustSize() + self.admin_db_method_invoker.emit(True) + + else: + done() + + def initUI(self): + self.center = QWidget() + self._main_layout = QHBoxLayout(self.center) + self._main_layout.setSpacing(0) + self._main_layout.setContentsMargins(0,0,0,0) + + self.init_stat_bar() + self.manga_views = {} + self._current_manga_view = None + self.default_manga_view = gallery.MangaViews(app_constants.ViewType.Default, self, True) + def refresh_view(): + self.current_manga_view.sort_model.refresh() + self.db_startup.DONE.connect(refresh_view) + self.manga_list_view = self.default_manga_view.list_view + self.manga_table_view = self.default_manga_view.table_view + self.manga_list_view.gallery_model.STATUSBAR_MSG.connect(self.stat_temp_msg) + self.manga_list_view.STATUS_BAR_MSG.connect(self.stat_temp_msg) + self.manga_table_view.STATUS_BAR_MSG.connect(self.stat_temp_msg) + + self.sidebar_list = misc_db.SideBarWidget(self) + self.db_startup.DONE.connect(self.sidebar_list.tags_tree.setup_tags) + self._main_layout.addWidget(self.sidebar_list) + self.current_manga_view = self.default_manga_view + + #self.display_widget.setSizePolicy(QSizePolicy.Expanding, + #QSizePolicy.Preferred) + self.download_window = io_misc.GalleryDownloader(self) + self.download_window.hide() + # init toolbar + self.init_toolbar() + + log_d('Create statusbar: OK') + + self.system_tray = misc.SystemTray(QIcon(app_constants.APP_ICO_PATH), self) + app_constants.SYSTEM_TRAY = self.system_tray + tray_menu = QMenu(self) + self.system_tray.setContextMenu(tray_menu) + self.system_tray.setToolTip('Happypanda {}'.format(app_constants.vs)) + tray_quit = QAction('Quit', tray_menu) + tray_update = tray_menu.addAction('Check for update') + tray_update.triggered.connect(self._check_update) + tray_menu.addAction(tray_quit) + tray_quit.triggered.connect(self.close) + self.system_tray.show() + def tray_activate(r=None): + if not r or r == QSystemTrayIcon.Trigger: + self.showNormal() + self.activateWindow() + self.system_tray.messageClicked.connect(tray_activate) + self.system_tray.activated.connect(tray_activate) + log_d('Create system tray: OK') + #self.display.addWidget(self.chapter_main) + + self.setCentralWidget(self.center) + self.setWindowIcon(QIcon(app_constants.APP_ICO_PATH)) + + props = settings.win_read(self, 'AppWindow') + if props.resize: + x, y = props.resize + self.resize(x, y) + else: + self.resize(app_constants.MAIN_W, app_constants.MAIN_H) + self.setMinimumWidth(600) + self.setMinimumHeight(400) + misc.centerWidget(self) + self.init_spinners() + self.show() + log_d('Show window: OK') + + self.notification_bar = misc.NotificationOverlay(self) + p = self.toolbar.pos() + self.notification_bar.move(p.x(), p.y() + self.toolbar.height()) + self.notification_bar.resize(self.width()) + self.notif_bubble = misc.AppBubble(self) + app_constants.NOTIF_BAR = self.notification_bar + app_constants.NOTIF_BUBBLE = self.notif_bubble + + log_d('Create notificationbar: OK') + + log_d('Window Create: OK') + + def _check_update(self): + class upd_chk(QObject): + UPDATE_CHECK = pyqtSignal(str) + def __init__(self, **kwargs): + super().__init__(**kwargs) + def fetch_vs(self): + import requests + import time + log_d('Checking Update') + time.sleep(1.5) + try: + r = requests.get("https://raw.githubusercontent.com/Pewpews/happypanda/master/VS.txt") + a = r.text + vs = a.strip() + self.UPDATE_CHECK.emit(vs) + except: + log.exception('Checking Update: FAIL') + self.UPDATE_CHECK.emit('this is a very long text which is sure to be over limit') + + def check_update(vs): + log_i('Received version: {}\nCurrent version: {}'.format(vs, app_constants.vs)) + if vs != app_constants.vs: + if len(vs) < 10: + self.notification_bar.begin_show() + self.notification_bar.add_text("Version {} of Happypanda is".format(vs) + " available. Click here to update!", False) + self.notification_bar.clicked.connect(lambda: utils.open_web_link('https://github.com/Pewpews/happypanda/releases')) + self.notification_bar.set_clickable(True) + else: + self.notification_bar.add_text("An error occurred while checking for new version") + + self.update_instance = upd_chk() + thread = QThread(self) + self.update_instance.moveToThread(thread) + thread.started.connect(self.update_instance.fetch_vs) + self.update_instance.UPDATE_CHECK.connect(check_update) + self.update_instance.UPDATE_CHECK.connect(self.update_instance.deleteLater) + thread.finished.connect(thread.deleteLater) + thread.start() + + def _web_metadata_picker(self, gallery, title_url_list, queue, parent=None): + if not parent: + parent = self + text = "Which gallery do you want to extract metadata from?" + s_gallery_popup = misc.SingleGalleryChoices(gallery, title_url_list, + text, parent) + s_gallery_popup.USER_CHOICE.connect(queue.put) + + def get_metadata(self, gal=None): + if not app_constants.GLOBAL_EHEN_LOCK: + metadata_spinner = misc.Spinner(self) + metadata_spinner.set_text("Metadata") + metadata_spinner.set_size(55) + thread = QThread(self) + thread.setObjectName('App.get_metadata') + fetch_instance = fetch.Fetch() + if gal: + if not isinstance(gal, list): + galleries = [gal] + else: + galleries = gal + else: + if app_constants.CONTINUE_AUTO_METADATA_FETCHER: + galleries = [g for g in self.current_manga_view.gallery_model._data if not g.exed] + else: + galleries = self.current_manga_view.gallery_model._data + if not galleries: + self.notification_bar.add_text('Looks like we\'ve already gone through all galleries!') + return None + fetch_instance.galleries = galleries + + self.notification_bar.begin_show() + fetch_instance.moveToThread(thread) + + def done(status): + self.notification_bar.end_show() + gallerydb.execute(database.db.DBBase.end, True) + try: + fetch_instance.deleteLater() + except RuntimeError: + pass + if not isinstance(status, bool): + galleries = [] + for tup in status: + galleries.append(tup[0]) + + class GalleryContextMenu(QMenu): + app_instance = self + def __init__(self, parent=None): + super().__init__(parent) + show_in_library_act = self.addAction('Show in library') + show_in_library_act.triggered.connect(self.show_in_library) + + def show_in_library(self): + index = gallery.CommonView.find_index(self.app_instance.get_current_view(), self.gallery_widget.gallery.id, True) + if index: + gallery.CommonView.scroll_to_index(self.app_instance.get_current_view(), index) + + g_popup = io_misc.GalleryPopup(('Fecthing metadata for these galleries failed.' + ' Check happypanda.log for details.', galleries), self, menu=GalleryContextMenu) + errors = {g[0].id: g[1] for g in status} + for g_item in g_popup.get_all_items(): + g_item.extra_text.setText("{}".format(errors[g_item.gallery.id])) + g_item.extra_text.show() + g_popup.graphics_blur.setEnabled(False) + close_button = g_popup.add_buttons('Close')[0] + close_button.clicked.connect(g_popup.close) + + database.db.DBBase.begin() + fetch_instance.GALLERY_PICKER.connect(self._web_metadata_picker) + fetch_instance.GALLERY_EMITTER.connect(self.default_manga_view.replace_gallery) + fetch_instance.AUTO_METADATA_PROGRESS.connect(self.notification_bar.add_text) + thread.started.connect(fetch_instance.auto_web_metadata) + fetch_instance.FINISHED.connect(done) + fetch_instance.FINISHED.connect(metadata_spinner.before_hide) + thread.finished.connect(thread.deleteLater) + thread.start() + #fetch_instance.auto_web_metadata() + metadata_spinner.show() + else: + self.notif_bubble.update_text("Oops!", "Auto metadata fetcher is already running...") + + def init_stat_bar(self): + self.status_bar = self.statusBar() + self.status_bar.setSizeGripEnabled(False) + self.stat_info = QLabel() + self.stat_info.setIndent(5) + self.sort_main = QAction("Asc", self) + sort_menu = QMenu() + self.sort_main.setMenu(sort_menu) + s_by_title = QAction("Title", sort_menu) + s_by_artist = QAction("Artist", sort_menu) + sort_menu.addAction(s_by_title) + sort_menu.addAction(s_by_artist) + self.status_bar.addPermanentWidget(self.stat_info) + #self.status_bar.addAction(self.sort_main) + self.temp_msg = QLabel() + self.temp_timer = QTimer() + + app_constants.STAT_MSG_METHOD = self.stat_temp_msg + + def stat_temp_msg(self, msg): + self.temp_timer.stop() + self.temp_msg.setText(msg) + self.status_bar.addWidget(self.temp_msg) + self.temp_timer.timeout.connect(self.temp_msg.clear) + self.temp_timer.setSingleShot(True) + self.temp_timer.start(5000) + + def stat_row_info(self): + r = self.current_manga_view.get_current_view().sort_model.rowCount() + t = self.current_manga_view.get_current_view().gallery_model.rowCount() + g_l = self.get_current_view().sort_model.current_gallery_list + if g_l: + self.stat_info.setText("{} | Showing {} of {} ".format(g_l.name, r, t)) + else: + self.stat_info.setText("Showing {} of {} ".format(r, t)) + + def set_current_manga_view(self, v): + self.current_manga_view = v + + @property + def current_manga_view(self): + return self._current_manga_view + + @current_manga_view.setter + def current_manga_view(self, new_view): + if self._current_manga_view: + self._main_layout.takeAt(1) + self._current_manga_view = new_view + self._main_layout.insertLayout(1, new_view.view_layout, 1) + self.stat_row_info() + + def init_spinners(self): + # fetching spinner + self.data_fetch_spinner = misc.Spinner(self, "center") + self.data_fetch_spinner.set_size(80) + + self.manga_list_view.gallery_model.ADD_MORE.connect(self.data_fetch_spinner.show) + self.db_startup.START.connect(self.data_fetch_spinner.show) + self.db_startup.PROGRESS.connect(self.data_fetch_spinner.set_text) + self.manga_list_view.gallery_model.ADDED_ROWS.connect(self.data_fetch_spinner.before_hide) + self.db_startup.DONE.connect(self.data_fetch_spinner.before_hide) + + ## deleting spinner + #self.gallery_delete_spinner = misc.Spinner(self) + #self.gallery_delete_spinner.set_size(40,40) + ##self.gallery_delete_spinner.set_text('Removing...') + #self.manga_list_view.gallery_model.rowsAboutToBeRemoved.connect(self.gallery_delete_spinner.show) + #self.manga_list_view.gallery_model.rowsRemoved.connect(self.gallery_delete_spinner.before_hide) + + + def search(self, srch_string): + "Args should be Search Enums" + self.search_bar.setText(srch_string) + self.search_backward.setVisible(True) + args = [] + if app_constants.GALLERY_SEARCH_REGEX: + args.append(app_constants.Search.Regex) + if app_constants.GALLERY_SEARCH_CASE: + args.append(app_constants.Search.Case) + if app_constants.GALLERY_SEARCH_STRICT: + args.append(app_constants.Search.Strict) + self.current_manga_view.get_current_view().sort_model.init_search(srch_string, args) + old_cursor_pos = self._search_cursor_pos[0] + self.search_bar.end(False) + if self.search_bar.cursorPosition() != old_cursor_pos + 1: + self.search_bar.setCursorPosition(old_cursor_pos) + + def switch_display(self): + "Switches between fav and catalog display" + if self.current_manga_view.fav_is_current(): + self.tab_manager.library_btn.click() + else: + self.tab_manager.favorite_btn.click() + + def settings(self): + sett = settingsdialog.SettingsDialog(self) + sett.scroll_speed_changed.connect(self.manga_list_view.updateGeometries) + #sett.show() + + def init_toolbar(self): + self.toolbar = QToolBar() + self.toolbar.adjustSize() + #self.toolbar.setFixedHeight() + self.toolbar.setWindowTitle("Show") # text for the contextmenu + #self.toolbar.setStyleSheet("QToolBar {border:0px}") # make it user + #defined? + self.toolbar.setMovable(False) + self.toolbar.setFloatable(False) + #self.toolbar.setIconSize(QSize(20,20)) + self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + self.toolbar.setIconSize(QSize(20,20)) + + spacer_start = QWidget() # aligns the first actions properly + spacer_start.setFixedSize(QSize(10, 1)) + self.toolbar.addWidget(spacer_start) + + def switch_view(fav): + if fav: + self.default_manga_view.get_current_view().sort_model.fav_view() + else: + self.default_manga_view.get_current_view().sort_model.catalog_view() + + self.tab_manager = misc_db.ToolbarTabManager(self.toolbar, self) + self.tab_manager.favorite_btn.clicked.connect(lambda: switch_view(True)) + self.tab_manager.library_btn.click() + self.tab_manager.library_btn.clicked.connect(lambda: switch_view(False)) + + self.addition_tab = self.tab_manager.addTab("Inbox", app_constants.ViewType.Addition) + + gallery_k = QKeySequence('Alt+G') + new_gallery_k = QKeySequence('Ctrl+N') + new_galleries_k = QKeySequence('Ctrl+Shift+N') + new_populate_k = QKeySequence('Ctrl+Alt+N') + scan_galleries_k = QKeySequence('Ctrl+Alt+S') + open_random_k = QKeySequence(QKeySequence.Open) + get_all_metadata_k = QKeySequence('Ctrl+Alt+M') + gallery_downloader_k = QKeySequence('Ctrl+Alt+D') + + gallery_menu = QMenu() + gallery_action = QToolButton() + gallery_action.setShortcut(gallery_k) + gallery_action.setText('Gallery ') + gallery_action.setPopupMode(QToolButton.InstantPopup) + gallery_action.setToolTip('Contains various gallery related features') + gallery_action.setMenu(gallery_menu) + add_gallery_icon = QIcon(app_constants.PLUS_PATH) + gallery_action_add = QAction(add_gallery_icon, "Add single gallery...", self) + gallery_action_add.triggered.connect(lambda: gallery.CommonView.spawn_dialog(self)) + gallery_action_add.setToolTip('Add a single gallery thoroughly') + gallery_action_add.setShortcut(new_gallery_k) + gallery_menu.addAction(gallery_action_add) + add_more_action = QAction(add_gallery_icon, "Add galleries...", self) + add_more_action.setStatusTip('Add galleries from different folders') + add_more_action.setShortcut(new_galleries_k) + add_more_action.triggered.connect(lambda: self.populate(True)) + gallery_menu.addAction(add_more_action) + populate_action = QAction(add_gallery_icon, "Populate from directory/archive...", self) + populate_action.setStatusTip('Populates the DB with galleries from a single folder or archive') + populate_action.triggered.connect(self.populate) + populate_action.setShortcut(new_populate_k) + gallery_menu.addAction(populate_action) + gallery_menu.addSeparator() + metadata_action = QAction('Get metadata for all galleries', self) + metadata_action.triggered.connect(self.get_metadata) + metadata_action.setShortcut(get_all_metadata_k) + gallery_menu.addAction(metadata_action) + scan_galleries_action = QAction('Scan for new galleries', self) + scan_galleries_action.triggered.connect(self.scan_for_new_galleries) + scan_galleries_action.setStatusTip('Scan monitored folders for new galleries') + scan_galleries_action.setShortcut(scan_galleries_k) + gallery_menu.addAction(scan_galleries_action) + + gallery_action_random = gallery_menu.addAction("Open random gallery") + gallery_action_random.triggered.connect(lambda: gallery.CommonView.open_random_gallery(self.get_current_view())) + gallery_action_random.setShortcut(open_random_k) + self.toolbar.addWidget(gallery_action) + + tools_k = QKeySequence('Alt+T') + misc_action = QToolButton() + misc_action.setText('Tools ') + misc_action.setShortcut(tools_k) + misc_action_menu = QMenu() + misc_action.setMenu(misc_action_menu) + misc_action.setPopupMode(QToolButton.InstantPopup) + misc_action.setToolTip("Contains misc. features") + gallery_downloader = QAction("Gallery Downloader", misc_action_menu) + gallery_downloader.triggered.connect(self.download_window.show) + gallery_downloader.setShortcut(gallery_downloader_k) + misc_action_menu.addAction(gallery_downloader) + duplicate_check_simple = QAction("Simple Duplicate Finder", misc_action_menu) + duplicate_check_simple.triggered.connect(lambda: self.duplicate_check()) # triggered emits False + misc_action_menu.addAction(duplicate_check_simple) + self.toolbar.addWidget(misc_action) + + # debug specfic code + if app_constants.DEBUG: + def debug_func(): + print(self.current_manga_view.gallery_model.rowCount()) + print(self.current_manga_view.sort_model.rowCount()) + + debug_btn = QToolButton() + debug_btn.setText("DEBUG BUTTON") + self.toolbar.addWidget(debug_btn) + debug_btn.clicked.connect(debug_func) + + spacer_middle = QWidget() # aligns buttons to the right + spacer_middle.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.toolbar.addWidget(spacer_middle) + + sort_k = QKeySequence('Alt+S') + + def set_new_sort(s): + self.current_manga_view.list_view.sort(s) + + sort_action = QToolButton() + sort_action.setShortcut(sort_k) + sort_action.setIcon(QIcon(app_constants.SORT_PATH)) + sort_menu = misc.SortMenu(self, self.toolbar) + sort_menu.new_sort.connect(set_new_sort) + sort_action.setMenu(sort_menu) + sort_action.setPopupMode(QToolButton.InstantPopup) + self.toolbar.addWidget(sort_action) + + togle_view_k = QKeySequence('Alt+Space') + + self.grid_toggle_g_icon = QIcon(app_constants.GRID_PATH) + self.grid_toggle_l_icon = QIcon(app_constants.LIST_PATH) + self.grid_toggle = QToolButton() + self.grid_toggle.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + self.grid_toggle.setShortcut(togle_view_k) + if self.current_manga_view.current_view == gallery.MangaViews.View.List: + self.grid_toggle.setIcon(self.grid_toggle_l_icon) + else: + self.grid_toggle.setIcon(self.grid_toggle_g_icon) + self.grid_toggle.setObjectName('gridtoggle') + self.grid_toggle.clicked.connect(self.toggle_view) + self.toolbar.addWidget(self.grid_toggle) + + spacer_mid2 = QWidget() + spacer_mid2.setFixedSize(QSize(5, 1)) + self.toolbar.addWidget(spacer_mid2) + + + self.search_bar = misc.LineEdit() + search_options = self.search_bar.addAction(QIcon(app_constants.SEARCH_OPTIONS_PATH), QLineEdit.TrailingPosition) + search_options_menu = QMenu(self) + search_options.triggered.connect(lambda: search_options_menu.popup(QCursor.pos())) + search_options.setMenu(search_options_menu) + case_search_option = search_options_menu.addAction('Case Sensitive') + case_search_option.setCheckable(True) + case_search_option.setChecked(app_constants.GALLERY_SEARCH_CASE) + + def set_search_case(b): + app_constants.GALLERY_SEARCH_CASE = b + settings.set(b, 'Application', 'gallery search case') + settings.save() + + case_search_option.toggled.connect(set_search_case) + + strict_search_option = search_options_menu.addAction('Match whole terms') + strict_search_option.setCheckable(True) + strict_search_option.setChecked(app_constants.GALLERY_SEARCH_STRICT) + + + regex_search_option = search_options_menu.addAction('Regex') + regex_search_option.setCheckable(True) + regex_search_option.setChecked(app_constants.GALLERY_SEARCH_REGEX) + + def set_search_strict(b): + if b: + if regex_search_option.isChecked(): + regex_search_option.toggle() + app_constants.GALLERY_SEARCH_STRICT = b + settings.set(b, 'Application', 'gallery search strict') + settings.save() + + strict_search_option.toggled.connect(set_search_strict) + + def set_search_regex(b): + if b: + if strict_search_option.isChecked(): + strict_search_option.toggle() + app_constants.GALLERY_SEARCH_REGEX = b + settings.set(b, 'Application', 'allow search regex') + settings.save() + + regex_search_option.toggled.connect(set_search_regex) + + self.search_bar.setObjectName('search_bar') + self.search_timer = QTimer(self) + self.search_timer.setSingleShot(True) + self.search_timer.timeout.connect(lambda: self.search(self.search_bar.text())) + self._search_cursor_pos = [0, 0] + def set_cursor_pos(old, new): + self._search_cursor_pos[0] = old + self._search_cursor_pos[1] = new + self.search_bar.cursorPositionChanged.connect(set_cursor_pos) + + if app_constants.SEARCH_AUTOCOMPLETE: + completer = QCompleter(self) + completer_view = misc.CompleterPopupView() + completer.setPopup(completer_view) + completer_view._setup() + completer.setModel(self.manga_list_view.gallery_model) + completer.setCaseSensitivity(Qt.CaseInsensitive) + completer.setCompletionMode(QCompleter.PopupCompletion) + completer.setCompletionRole(Qt.DisplayRole) + completer.setCompletionColumn(app_constants.TITLE) + completer.setFilterMode(Qt.MatchContains) + self.search_bar.setCompleter(completer) + self.search_bar.returnPressed.connect(lambda: self.search(self.search_bar.text())) + if not app_constants.SEARCH_ON_ENTER: + self.search_bar.textEdited.connect(lambda: self.search_timer.start(800)) + self.search_bar.setPlaceholderText("Search title, artist, namespace & tags") + self.search_bar.setMinimumWidth(400) + self.search_bar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + self.manga_list_view.sort_model.HISTORY_SEARCH_TERM.connect(lambda a: self.search_bar.setText(a)) + self.toolbar.addWidget(self.search_bar) + + def search_history(_, back=True): # clicked signal passes a bool + sort_model = self.manga_list_view.sort_model + nav = sort_model.PREV if back else sort_model.NEXT + history_term = sort_model.navigate_history(nav) + if back: + self.search_forward.setVisible(True) + + back_k = QKeySequence(QKeySequence.Back) + forward_k = QKeySequence(QKeySequence.Forward) + + search_backbutton = QToolButton(self.toolbar) + search_backbutton.setText(u'\u25C0') + search_backbutton.setFixedWidth(15) + search_backbutton.clicked.connect(search_history) + search_backbutton.setShortcut(back_k) + self.search_backward = self.toolbar.addWidget(search_backbutton) + self.search_backward.setVisible(False) + search_forwardbutton = QToolButton(self.toolbar) + search_forwardbutton.setText(u'\u25B6') + search_forwardbutton.setFixedWidth(15) + search_forwardbutton.clicked.connect(lambda: search_history(None, False)) + search_forwardbutton.setShortcut(forward_k) + self.search_forward = self.toolbar.addWidget(search_forwardbutton) + self.search_forward.setVisible(False) + + spacer_end = QWidget() # aligns settings action properly + spacer_end.setFixedSize(QSize(10, 1)) + self.toolbar.addWidget(spacer_end) + + settings_k = QKeySequence("Ctrl+P") + + settings_act = QToolButton(self.toolbar) + settings_act.setShortcut(settings_k) + settings_act.setIcon(QIcon(app_constants.SETTINGS_PATH)) + settings_act.clicked.connect(self.settings) + self.toolbar.addWidget(settings_act) + + spacer_end2 = QWidget() # aligns About action properly + spacer_end2.setFixedSize(QSize(5, 1)) + self.toolbar.addWidget(spacer_end2) + self.addToolBar(self.toolbar) + + def get_current_view(self): + return self.current_manga_view.get_current_view() + + def toggle_view(self): + """ + Toggles the current display view + """ + if self.current_manga_view.current_view == gallery.MangaViews.View.Table: + self.current_manga_view.changeTo(self.current_manga_view.m_l_view_index) + self.grid_toggle.setIcon(self.grid_toggle_l_icon) + else: + self.current_manga_view.changeTo(self.current_manga_view.m_t_view_index) + self.grid_toggle.setIcon(self.grid_toggle_g_icon) + + # TODO: Improve this so that it adds to the gallery dialog, + # so user can edit data before inserting (make it a choice) + def populate(self, mixed=None): + "Populates the database with gallery from local drive'" + + if mixed: + gallery_view = misc.GalleryListView(self, True) + gallery_view.SERIES.connect(self.gallery_populate) + gallery_view.show() + else: + msg_box = misc.BasePopup(self) + l = QVBoxLayout() + msg_box.main_widget.setLayout(l) + l.addWidget(QLabel('Directory or Archive?')) + l.addLayout(msg_box.buttons_layout) + + def from_dir(): + path = QFileDialog.getExistingDirectory(self, "Choose a directory containing your galleries") + if not path: + return + msg_box.close() + app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY = True + self.gallery_populate(path, True) + def from_arch(): + path = QFileDialog.getOpenFileName(self, 'Choose an archive containing your galleries', + filter=utils.FILE_FILTER) + path = [path[0]] + if not all(path) or not path: + return + msg_box.close() + app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY = True + self.gallery_populate(path, True) + + buttons = msg_box.add_buttons('Directory', 'Archive', 'Close') + buttons[2].clicked.connect(msg_box.close) + buttons[0].clicked.connect(from_dir) + buttons[1].clicked.connect(from_arch) + msg_box.adjustSize() + msg_box.show() + + def gallery_populate(self, path, validate=False): + "Scans the given path for gallery to add into the DB" + if len(path) is not 0: + data_thread = QThread(self) + data_thread.setObjectName('General gallery populate') + self.addition_tab.click() + self.g_populate_inst = fetch.Fetch() + self.g_populate_inst.series_path = path + self._g_populate_count = 0 + + fetch_spinner = misc.Spinner(self) + fetch_spinner.set_size(60) + fetch_spinner.set_text("Populating") + fetch_spinner.show() + + def finished(status): + fetch_spinner.hide() + if not status: + log_e('Populating DB from gallery folder: Nothing was added!') + self.notif_bubble.update_text("Gallery Populate", + "Nothing was added. Check happypanda_log for details..") + + def skipped_gs(s_list): + "Skipped galleries" + msg_box = QMessageBox(self) + msg_box.setIcon(QMessageBox.Question) + msg_box.setText('Do you want to view skipped paths?') + msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msg_box.setDefaultButton(QMessageBox.No) + if msg_box.exec() == QMessageBox.Yes: + list_wid = QTableWidget(self) + list_wid.setAttribute(Qt.WA_DeleteOnClose) + list_wid.setRowCount(len(s_list)) + list_wid.setColumnCount(2) + list_wid.setAlternatingRowColors(True) + list_wid.setEditTriggers(list_wid.NoEditTriggers) + list_wid.setHorizontalHeaderLabels(['Reason', 'Path']) + list_wid.setSelectionBehavior(list_wid.SelectRows) + list_wid.setSelectionMode(list_wid.SingleSelection) + list_wid.setSortingEnabled(True) + list_wid.verticalHeader().hide() + list_wid.setAutoScroll(False) + for x, g in enumerate(s_list): + list_wid.setItem(x, 0, QTableWidgetItem(g[1])) + list_wid.setItem(x, 1, QTableWidgetItem(g[0])) + list_wid.resizeColumnsToContents() + list_wid.setWindowTitle('{} skipped paths'.format(len(s_list))) + list_wid.setWindowFlags(Qt.Window) + list_wid.resize(900,400) + + list_wid.doubleClicked.connect(lambda i: utils.open_path(list_wid.item(i.row(), 1).text(), list_wid.item(i.row(), 1).text())) + + list_wid.show() + + def a_progress(prog): + fetch_spinner.set_text("Populating... {}/{}".format(prog, self._g_populate_count)) + + def add_to_model(gallery): + self.addition_tab.view.add_gallery(gallery, app_constants.KEEP_ADDED_GALLERIES) + + def set_count(c): + self._g_populate_count = c + + self.g_populate_inst.moveToThread(data_thread) + self.g_populate_inst.PROGRESS.connect(a_progress) + self.g_populate_inst.DATA_COUNT.connect(set_count) + self.g_populate_inst.LOCAL_EMITTER.connect(add_to_model) + self.g_populate_inst.FINISHED.connect(finished) + self.g_populate_inst.FINISHED.connect(self.g_populate_inst.deleteLater) + self.g_populate_inst.SKIPPED.connect(skipped_gs) + data_thread.finished.connect(data_thread.deleteLater) + data_thread.started.connect(self.g_populate_inst.local) + data_thread.start() + #self.g_populate_inst.local() + log_i('Populating DB from directory/archive') + + def scan_for_new_galleries(self): + available_folders = app_constants.ENABLE_MONITOR and \ + app_constants.MONITOR_PATHS and all(app_constants.MONITOR_PATHS) + if available_folders and not app_constants.SCANNING_FOR_GALLERIES: + app_constants.SCANNING_FOR_GALLERIES = True + self.notification_bar.add_text("Scanning for new galleries...") + log_i('Scanning for new galleries...') + try: + class ScanDir(QObject): + finished = pyqtSignal() + fetch_inst = fetch.Fetch(self) + def __init__(self, addition_view, addition_tab, parent=None): + super().__init__(parent) + self.addition_view = addition_view + self.addition_tab = addition_tab + self._switched = False + + def switch_tab(self): + if not self._switched: + self.addition_tab.click() + self._switched = True + + def scan_dirs(self): + paths = [] + for p in app_constants.MONITOR_PATHS: + if os.path.exists(p): + dir_content = scandir.scandir(p) + for d in dir_content: + paths.append(d.path) + else: + log_e("Monitored path does not exists: {}".format(p.encode(errors='ignore'))) + + self.fetch_inst.series_path = paths + self.fetch_inst.LOCAL_EMITTER.connect(lambda g:self.addition_view.add_gallery(g, app_constants.KEEP_ADDED_GALLERIES)) + self.fetch_inst.LOCAL_EMITTER.connect(self.switch_tab) + self.fetch_inst.local() + #contents = [] + #for g in self.scanned_data: + # contents.append(g) + + #paths = sorted(paths) + #new_galleries = [] + #for x in contents: + # y = utils.b_search(paths, os.path.normcase(x.path)) + # if not y: + # new_galleries.append(x) + self.finished.emit() + self.deleteLater() + #if app_constants.LOOK_NEW_GALLERY_AUTOADD: + # QTimer.singleShot(10000, + # self.gallery_populate(final_paths)) + # return + + + def finished(): app_constants.SCANNING_FOR_GALLERIES = False + + new_gall_spinner = misc.Spinner(self) + new_gall_spinner.set_text("Gallery Scan") + new_gall_spinner.show() + + thread = QThread(self) + self.scan_inst = ScanDir(self.addition_tab.view, self.addition_tab) + self.scan_inst.moveToThread(thread) + self.scan_inst.finished.connect(finished) + self.scan_inst.finished.connect(new_gall_spinner.before_hide) + thread.started.connect(self.scan_inst.scan_dirs) + #self.scan_inst.scan_dirs() + thread.finished.connect(thread.deleteLater) + thread.start() + except: + self.notification_bar.add_text('An error occured while attempting to scan for new galleries. Check happypanda.log.') + log.exception('An error occured while attempting to scan for new galleries.') + app_constants.SCANNING_FOR_GALLERIES = False + else: + self.notification_bar.add_text("Please specify directory in settings to scan for new galleries!") + + def dragEnterEvent(self, event): + if event.mimeData().hasUrls(): + event.acceptProposedAction() + else: + super().dragEnterEvent(event) + + def dropEvent(self, event): + acceptable = [] + unaccept = [] + for u in event.mimeData().urls(): + path = u.toLocalFile() + if os.path.isdir(path) or path.endswith(utils.ARCHIVE_FILES): + acceptable.append(path) + else: + unaccept.append(path) + log_i('Acceptable dropped items: {}'.format(len(acceptable))) + log_i('Unacceptable dropped items: {}'.format(len(unaccept))) + log_d('Dropped items: {}\n{}'.format(acceptable, unaccept).encode(errors='ignore')) + if acceptable: + self.notification_bar.add_text('Adding dropped items...') + log_i('Adding dropped items') + l = len(acceptable) == 1 + f_item = acceptable[0] + if f_item.endswith(utils.ARCHIVE_FILES): + f_item = utils.check_archive(f_item) + else: + f_item = utils.recursive_gallery_check(f_item) + f_item_l = len(f_item) < 2 + subfolder_as_c = not app_constants.SUBFOLDER_AS_GALLERY + if l and subfolder_as_c or l and f_item_l: + g_d = gallerydialog.GalleryDialog(self, acceptable[0]) + g_d.show() + else: + self.gallery_populate(acceptable, True) + event.accept() + else: + text = 'File not supported' if len(unaccept) < 2 else 'Files not supported' + self.notification_bar.add_text(text) + + if unaccept: + self.notification_bar.add_text('Some unsupported files did not get added') + super().dropEvent(event) + + def resizeEvent(self, event): + try: + self.notification_bar.resize(event.size().width()) + except AttributeError: + pass + self.move_listener.emit() + return super().resizeEvent(event) + + def moveEvent(self, event): + self.move_listener.emit() + return super().moveEvent(event) + + def showEvent(self, event): + return super().showEvent(event) + + def cleanup_exit(self): + self.system_tray.hide() + # watchers + try: + self.watchers.stop_all() + except AttributeError: + pass + + # settings + settings.set(self.manga_list_view.current_sort, 'General', 'current sort') + settings.set(app_constants.IGNORE_PATHS, 'Application', 'ignore paths') + if not self.isMaximized(): + settings.win_save(self, 'AppWindow') + + # temp dir + try: + for root, dirs, files in scandir.walk('temp', topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + log_d('Flush temp on exit: OK') + except: + log.exception('Flush temp on exit: FAIL') + + # DB + try: + log_i("Analyzing database...") + gallerydb.GalleryDB.analyze() + log_i("Closing database...") + gallerydb.GalleryDB.close() + except: + pass + self.download_window.close() + + # check if there is db activity + if not gallerydb.method_queue.empty(): + class DBActivityChecker(QObject): + FINISHED = pyqtSignal() + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def check(self): + gallerydb.method_queue.join() + self.FINISHED.emit() + self.deleteLater() + + db_activity = DBActivityChecker() + db_spinner = misc.Spinner(self) + self.db_activity_checker.connect(db_activity.check) + db_activity.moveToThread(app_constants.GENERAL_THREAD) + db_activity.FINISHED.connect(db_spinner.close) + db_spinner.set_text('DB Activity') + db_spinner.show() + self.db_activity_checker.emit() + msg_box = QMessageBox(self) + msg_box.setText('Database activity detected!') + msg_box.setInformativeText("Closing now might result in data loss." + " Do you still want to close?\n(Wait for the activity spinner to hide before closing)") + msg_box.setIcon(QMessageBox.Critical) + msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msg_box.setDefaultButton(QMessageBox.No) + if msg_box.exec() == QMessageBox.Yes: + return 1 + else: + return 2 + else: + return 0 + + def duplicate_check(self, simple=True): + try: + self.duplicate_check_invoker.disconnect() + except TypeError: + pass + mode = 'simple' if simple else 'advanced' + log_i('Checking for duplicates in mode: {}'.format(mode)) + notifbar = app_constants.NOTIF_BAR + notifbar.add_text('Checking for duplicates...') + duplicate_spinner = misc.Spinner(self) + duplicate_spinner.set_text("Duplicate Check") + duplicate_spinner.show() + dup_tab = self.tab_manager.addTab("Duplicate", app_constants.ViewType.Duplicate) + dup_tab.view.set_delete_proxy(self.default_manga_view.gallery_model) + + class DuplicateCheck(QObject): + found_duplicates = pyqtSignal(tuple) + finished = pyqtSignal() + def __init__(self): + super().__init__() + + def checkSimple(self, model): + galleries = model._data + + duplicates = [] + for n, g in enumerate(galleries, 1): + notifbar.add_text('Checking gallery {}'.format(n)) + log_d('Checking gallery {}'.format(g.title.encode(errors="ignore"))) + for y in galleries: + title = g.title.strip().lower() == y.title.strip().lower() + path = os.path.normcase(g.path) == os.path.normcase(y.path) + if g.id != y.id and (title or path): + if g not in duplicates: + duplicates.append(y) + duplicates.append(g) + self.found_duplicates.emit((g, y)) + self.finished.emit() + + self._d_checker = DuplicateCheck() + self._d_checker.moveToThread(app_constants.GENERAL_THREAD) + self._d_checker.found_duplicates.connect(lambda t: dup_tab.view.add_gallery(t, record_time=True)) + self._d_checker.finished.connect(dup_tab.click) + self._d_checker.finished.connect(self._d_checker.deleteLater) + self._d_checker.finished.connect(duplicate_spinner.before_hide) + if simple: + self.duplicate_check_invoker.connect(self._d_checker.checkSimple) + self.duplicate_check_invoker.emit(self.default_manga_view.gallery_model) + + def excepthook(self, ex_type, ex, tb): + w = misc.AppDialog(self, misc.AppDialog.MESSAGE) + w.show() + log_c(''.join(traceback.format_tb(tb))) + log_c('{}: {}'.format(ex_type, ex)) + traceback.print_exception(ex_type, ex, tb) + + def closeEvent(self, event): + r_code = self.cleanup_exit() + if r_code == 1: + log_d('Force Exit App: OK') + super().closeEvent(event) + elif r_code == 2: + log_d('Ignore Exit App') + event.ignore() + else: + log_d('Normal Exit App: OK') + super().closeEvent(event) if __name__ == '__main__': - raise NotImplementedError("Unit testing not implemented yet!") \ No newline at end of file + raise NotImplementedError("Unit testing not implemented yet!") \ No newline at end of file diff --git a/version/app_constants.py b/version/app_constants.py index e53ef22..76e55fc 100644 --- a/version/app_constants.py +++ b/version/app_constants.py @@ -707,6 +707,12 @@ class WrongLogin(Exception): pass +path:none, path:null +* +Galleries that has been moved/deleted from the filesystem + + + descr:none, descr:null, description:none, description:null * Galleries with no description set diff --git a/version/gallerydb.py b/version/gallerydb.py index 379a8ab..8499ef6 100644 --- a/version/gallerydb.py +++ b/version/gallerydb.py @@ -12,14 +12,23 @@ #along with Happypanda. If not, see . #""" -import datetime, os, enum, scandir, threading, logging, queue, io, uuid, functools +import datetime +import os +import enum +import scandir +import threading +import logging +import queue +import io +import uuid +import functools import re as regex from dateutil import parser as dateparser from PyQt5.QtCore import QObject, pyqtSignal, QTime from utils import (today, ArchiveFile, generate_img_hash, delete_path, - ARCHIVE_FILES, get_gallery_img, IMG_FILES) + ARCHIVE_FILES, get_gallery_img, IMG_FILES) from database import db_constants from database import db from database.db import DBBase @@ -42,2200 +51,2203 @@ db_constants.METHOD_RETURN = method_return class PriorityObject: - def __init__(self, priority, data): - self.p = priority - self.data = data + def __init__(self, priority, data): + self.p = priority + self.data = data - def __lt__(self, other): - return self.p < other.p + def __lt__(self, other): + return self.p < other.p def process_methods(): - """ - Methods are objects. - Put a list in the method queue where first index is the - method. Named arguments are put in a dict. - """ - while True: - l = method_queue.get().data - log_d('Processing a method from queue...') - method = l.pop(0) - log_d(method) - args = [] - kwargs = {} - get_args = 1 - no_return = False - while get_args: - try: - a = l.pop(0) - if a == 'no return': - no_return = True - continue - if isinstance(a, dict): - kwargs = a - else: - args.append(a) - except IndexError: - get_args = 0 - args = tuple(args) - if args and kwargs: - r = method(*args, **kwargs) - elif args: - r = method(*args) - elif kwargs: - r = method(**kwargs) - else: - r = method() - if not no_return: - method_return.put(r) - method_queue.task_done() + """ + Methods are objects. + Put a list in the method queue where first index is the + method. Named arguments are put in a dict. + """ + while True: + l = method_queue.get().data + log_d('Processing a method from queue...') + method = l.pop(0) + log_d(method) + args = [] + kwargs = {} + get_args = 1 + no_return = False + while get_args: + try: + a = l.pop(0) + if a == 'no return': + no_return = True + continue + if isinstance(a, dict): + kwargs = a + else: + args.append(a) + except IndexError: + get_args = 0 + args = tuple(args) + if args and kwargs: + r = method(*args, **kwargs) + elif args: + r = method(*args) + elif kwargs: + r = method(**kwargs) + else: + r = method() + if not no_return: + method_return.put(r) + method_queue.task_done() method_queue_thread = threading.Thread(name='Method Queue Thread', target=process_methods, - daemon=True) + daemon=True) method_queue_thread.start() def execute(method, no_return, *args, **kwargs): - log_d('Added method to queue') - log_d('Method name: {}'.format(method.__name__)) - arg_list = [method] - priority = kwargs.pop("priority", 999) - if no_return: - arg_list.append('no return') - if args: - for x in args: - arg_list.append(x) - if kwargs: - arg_list.append(kwargs) - method_queue.put(PriorityObject(priority, arg_list)) - if not no_return: - return method_return.get() + log_d('Added method to queue') + log_d('Method name: {}'.format(method.__name__)) + arg_list = [method] + priority = kwargs.pop("priority", 999) + if no_return: + arg_list.append('no return') + if args: + for x in args: + arg_list.append(x) + if kwargs: + arg_list.append(kwargs) + method_queue.put(PriorityObject(priority, arg_list)) + if not no_return: + return method_return.get() def chapter_map(row, chapter): - assert isinstance(chapter, Chapter) - chapter.title = row['chapter_title'] - chapter.path = bytes.decode(row['chapter_path']) - chapter.in_archive = row['in_archive'] - chapter.pages = row['pages'] - return chapter + assert isinstance(chapter, Chapter) + chapter.title = row['chapter_title'] + chapter.path = bytes.decode(row['chapter_path']) + chapter.in_archive = row['in_archive'] + chapter.pages = row['pages'] + return chapter def gallery_map(row, gallery, chapters=True, tags=True, hashes=True): - gallery.title = row['title'] - gallery.artist = row['artist'] - gallery.profile = bytes.decode(row['profile']) - gallery.path = bytes.decode(row['series_path']) - gallery.is_archive = row['is_archive'] - try: - gallery.path_in_archive = bytes.decode(row['path_in_archive']) - except TypeError: - pass - gallery.info = row['info'] - gallery.language = row['language'] - gallery.status = row['status'] - gallery.type = row['type'] - gallery.fav = row['fav'] - - def convert_date(date_str): - #2015-10-25 21:44:38 - if date_str and date_str != 'None': - return datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") - - gallery.pub_date = convert_date(row['pub_date']) - gallery.last_read = convert_date(row['last_read']) - gallery.date_added = convert_date(row['date_added']) - gallery.times_read = row['times_read'] - gallery._db_v = row['db_v'] - gallery.exed = row['exed'] - gallery.view = row['view'] - try: - gallery.link = bytes.decode(row['link']) - except TypeError: - gallery.link = row['link'] - - if chapters: - gallery.chapters = ChapterDB.get_chapters_for_gallery(gallery.id) - - if tags: - gallery.tags = TagDB.get_gallery_tags(gallery.id) - - if hashes: - gallery.hashes = HashDB.get_gallery_hashes(gallery.id) - - gallery.set_defaults() - return gallery + gallery.title = row['title'] + gallery.artist = row['artist'] + gallery.profile = bytes.decode(row['profile']) + gallery.path = bytes.decode(row['series_path']) + gallery.is_archive = row['is_archive'] + try: + gallery.path_in_archive = bytes.decode(row['path_in_archive']) + except TypeError: + pass + gallery.info = row['info'] + gallery.language = row['language'] + gallery.status = row['status'] + gallery.type = row['type'] + gallery.fav = row['fav'] + + def convert_date(date_str): + #2015-10-25 21:44:38 + if date_str and date_str != 'None': + return datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") + + gallery.pub_date = convert_date(row['pub_date']) + gallery.last_read = convert_date(row['last_read']) + gallery.date_added = convert_date(row['date_added']) + gallery.times_read = row['times_read'] + gallery._db_v = row['db_v'] + gallery.exed = row['exed'] + gallery.view = row['view'] + try: + gallery.link = bytes.decode(row['link']) + except TypeError: + gallery.link = row['link'] + + if chapters: + gallery.chapters = ChapterDB.get_chapters_for_gallery(gallery.id) + + if tags: + gallery.tags = TagDB.get_gallery_tags(gallery.id) + + if hashes: + gallery.hashes = HashDB.get_gallery_hashes(gallery.id) + + gallery.set_defaults() + return gallery def default_chap_exec(gallery_or_id, chap, only_values=False): - "Pass a Gallery object or gallery id and a Chapter object" - if isinstance(gallery_or_id, Gallery): - gid = gallery_or_id.id - in_archive = gallery_or_id.is_archive - else: - gid = gallery_or_id - in_archive = chap.in_archive - - if only_values: - execute = (gid, chap.title, chap.number, str.encode(chap.path), chap.pages, in_archive) - else: - execute = (""" - INSERT INTO chapters(series_id, chapter_title, chapter_number, chapter_path, pages, in_archive) - VALUES(:series_id, :chapter_title, :chapter_number, :chapter_path, :pages, :in_archive)""", - {'series_id':gid, - 'chapter_title':chap.title, - 'chapter_number':chap.number, - 'chapter_path':str.encode(chap.path), - 'pages':chap.pages, - 'in_archive':in_archive}) - return execute + "Pass a Gallery object or gallery id and a Chapter object" + if isinstance(gallery_or_id, Gallery): + gid = gallery_or_id.id + in_archive = gallery_or_id.is_archive + else: + gid = gallery_or_id + in_archive = chap.in_archive + + if only_values: + execute = (gid, chap.title, chap.number, str.encode(chap.path), chap.pages, in_archive) + else: + execute = (""" + INSERT INTO chapters(series_id, chapter_title, chapter_number, chapter_path, pages, in_archive) + VALUES(:series_id, :chapter_title, :chapter_number, :chapter_path, :pages, :in_archive)""", + {'series_id':gid, + 'chapter_title':chap.title, + 'chapter_number':chap.number, + 'chapter_path':str.encode(chap.path), + 'pages':chap.pages, + 'in_archive':in_archive}) + return execute def default_exec(object): - object.set_defaults() - def check(obj): - if obj == "None": - return None - else: - return obj - executing = ["""INSERT INTO series(title, artist, profile, series_path, is_archive, path_in_archive, - info, type, fav, language, status, pub_date, date_added, last_read, link, - times_read, db_v, exed, view) - VALUES(:title, :artist, :profile, :series_path, :is_archive, :path_in_archive, :info, :type, :fav, :language, - :status, :pub_date, :date_added, :last_read, :link, :times_read, :db_v, :exed, :view)""", - { - 'title':check(object.title), - 'artist':check(object.artist), - 'profile':str.encode(object.profile), - 'series_path':str.encode(object.path), - 'is_archive':check(object.is_archive), - 'path_in_archive':str.encode(object.path_in_archive), - 'info':check(object.info), - 'fav':check(object.fav), - 'type':check(object.type), - 'language':check(object.language), - 'status':check(object.status), - 'pub_date':check(object.pub_date), - 'date_added':check(object.date_added), - 'last_read':check(object.last_read), - 'link':str.encode(object.link), - 'times_read':check(object.times_read), - 'db_v':check(db_constants.REAL_DB_VERSION), - 'exed':check(object.exed), - 'view':check(object.view) - }] - return executing + object.set_defaults() + def check(obj): + if obj == "None": + return None + else: + return obj + executing = ["""INSERT INTO series(title, artist, profile, series_path, is_archive, path_in_archive, + info, type, fav, language, status, pub_date, date_added, last_read, link, + times_read, db_v, exed, view) + VALUES(:title, :artist, :profile, :series_path, :is_archive, :path_in_archive, :info, :type, :fav, :language, + :status, :pub_date, :date_added, :last_read, :link, :times_read, :db_v, :exed, :view)""", + { + 'title':check(object.title), + 'artist':check(object.artist), + 'profile':str.encode(object.profile), + 'series_path':str.encode(object.path), + 'is_archive':check(object.is_archive), + 'path_in_archive':str.encode(object.path_in_archive), + 'info':check(object.info), + 'fav':check(object.fav), + 'type':check(object.type), + 'language':check(object.language), + 'status':check(object.status), + 'pub_date':check(object.pub_date), + 'date_added':check(object.date_added), + 'last_read':check(object.last_read), + 'link':str.encode(object.link), + 'times_read':check(object.times_read), + 'db_v':check(db_constants.REAL_DB_VERSION), + 'exed':check(object.exed), + 'view':check(object.view) + }] + return executing class GalleryDB(DBBase): - """ - Provides the following s methods: - rebuild_thumb -> Rebuilds gallery thumbnail - rebuild_galleries -> Rebuilds the galleries in DB - modify_gallery -> Modifies gallery with given gallery id - get_all_gallery -> returns a list of all gallery ( class) currently in DB - get_gallery_by_path -> Returns gallery with given path - get_gallery_by_id -> Returns gallery with given id - add_gallery -> adds gallery into db - set_gallery_title -> changes gallery title - gallery_count -> returns amount of gallery (can be used for indexing) - del_gallery -> deletes the gallery with the given id recursively - check_exists -> Checks if provided string exists - clear_thumb -> Deletes a thumbnail - clear_thumb_dir -> Dletes everything in the thumbnail directory - """ - def __init__(self): - raise Exception("GalleryDB should not be instantiated") - - @staticmethod - def rebuild_thumb(gallery): - "Rebuilds gallery thumbnail" - try: - log_i('Recreating thumb {}'.format(gallery.title.encode(errors='ignore'))) - if gallery.profile: - GalleryDB.clear_thumb(gallery.profile) - gallery.profile = Executors.generate_thumbnail(gallery, blocking=True) - GalleryDB.modify_gallery(gallery.id, - profile=gallery.profile) - except: - log.exception("Failed rebuilding thumbnail") - return False - return True - - @staticmethod - def clear_thumb(path): - "Deletes a thumbnail" - try: - if os.path.samefile(path, app_constants.NO_IMAGE_PATH): - return - except FileNotFoundError: - pass - - try: - os.unlink(path) - except FileNotFoundError: - pass - except: - log.exception('Failed to delete thumb {}'.format(os.path.split(path)[1].encode(errors='ignore'))) - - @staticmethod - def clear_thumb_dir(): - "Deletes everything in the thumbnail directory" - if os.path.exists(db_constants.THUMBNAIL_PATH): - for thumbfile in scandir.scandir(db_constants.THUMBNAIL_PATH): - GalleryDB.clear_thumb(thumbfile.path) - - @staticmethod - def rebuild_gallery(gallery, thumb=False): - "Rebuilds the galleries in DB" - try: - log_i('Rebuilding {}'.format(gallery.title.encode(errors='ignore'))) - log_i("Rebuilding gallery {}".format(gallery.id)) - HashDB.del_gallery_hashes(gallery.id) - GalleryDB.modify_gallery(gallery.id, - title=gallery.title, - artist=gallery.artist, - info=gallery.info, - type=gallery.type, - fav=gallery.fav, - tags=gallery.tags, - language=gallery.language, - status=gallery.status, - pub_date=gallery.pub_date, - link=gallery.link, - times_read=gallery.times_read, - last_read=gallery.last_read, - _db_v=db_constants.CURRENT_DB_VERSION, - exed=gallery.exed, - is_archive=gallery.is_archive, - path_in_archive=gallery.path_in_archive, - view=gallery.view) - if thumb: - GalleryDB.rebuild_thumb(gallery) - except: - log.exception('Failed rebuilding') - return False - return True - - @classmethod - def modify_gallery(cls, series_id, title=None, profile=None, artist=None, info=None, type=None, fav=None, - tags=None, language=None, status=None, pub_date=None, link=None, - times_read=None, last_read=None, series_path=None, chapters=None, _db_v=None, - hashes=None, exed=None, is_archive=None, path_in_archive=None, view=None): - "Modifies gallery with given gallery id" - assert isinstance(series_id, int) - assert not isinstance(series_id, bool) - executing = [] - if title != None: - assert isinstance(title, str) - executing.append(["UPDATE series SET title=? WHERE series_id=?", (title, series_id)]) - if profile != None: - assert isinstance(profile, str) - executing.append(["UPDATE series SET profile=? WHERE series_id=?", (str.encode(profile), series_id)]) - if artist != None: - assert isinstance(artist, str) - executing.append(["UPDATE series SET artist=? WHERE series_id=?", (artist, series_id)]) - if info != None: - assert isinstance(info, str) - executing.append(["UPDATE series SET info=? WHERE series_id=?", (info, series_id)]) - if type != None: - assert isinstance(type, str) - executing.append(["UPDATE series SET type=? WHERE series_id=?", (type, series_id)]) - if fav != None: - assert isinstance(fav, int) - executing.append(["UPDATE series SET fav=? WHERE series_id=?", (fav, series_id)]) - if language != None: - assert isinstance(language, str) - executing.append(["UPDATE series SET language=? WHERE series_id=?", (language, series_id)]) - if status != None: - assert isinstance(status, str) - executing.append(["UPDATE series SET status=? WHERE series_id=?", (status, series_id)]) - if pub_date != None: - executing.append(["UPDATE series SET pub_date=? WHERE series_id=?", (pub_date, series_id)]) - if link != None: - executing.append(["UPDATE series SET link=? WHERE series_id=?", (link, series_id)]) - if times_read != None: - executing.append(["UPDATE series SET times_read=? WHERE series_id=?", (times_read, series_id)]) - if last_read != None: - executing.append(["UPDATE series SET last_read=? WHERE series_id=?", (last_read, series_id)]) - if series_path != None: - executing.append(["UPDATE series SET series_path=? WHERE series_id=?", (str.encode(series_path), series_id)]) - if _db_v != None: - executing.append(["UPDATE series SET db_v=? WHERE series_id=?", (_db_v, series_id)]) - if exed != None: - executing.append(["UPDATE series SET exed=? WHERE series_id=?", (exed, series_id)]) - if is_archive != None: - executing.append(["UPDATE series SET is_archive=? WHERE series_id=?", (is_archive, series_id)]) - if path_in_archive != None: - executing.append(["UPDATE series SET path_in_archive=? WHERE series_id=?", (path_in_archive, series_id)]) - if view != None: - executing.append(["UPDATE series SET view=? WHERE series_id=?", (view, series_id)]) - - if tags != None: - assert isinstance(tags, dict) - TagDB.modify_tags(series_id, tags) - if chapters != None: - assert isinstance(chapters, ChaptersContainer) - ChapterDB.update_chapter(chapters) - - if hashes != None: - assert isinstance(hashes, Gallery) - HashDB.rebuild_gallery_hashes(hashes) - - for query in executing: - cls.execute(cls, *query) - - @classmethod - def get_all_gallery(cls, chapters=True, tags=True, hashes=True): - """ - Careful, might crash with very large libraries i think... - Returns a list of all galleries ( class) currently in DB - """ - cursor = cls.execute(cls, 'SELECT * FROM series') - all_gallery = cursor.fetchall() - return GalleryDB.gen_galleries(all_gallery, chapters, tags, hashes) - - @staticmethod - def gen_galleries(gallery_dict, chapters=True, tags=True, hashes=True): - """ - Map galleries fetched from DB - """ - gallery_list = [] - for gallery_row in gallery_dict: - gallery = Gallery() - gallery.id = gallery_row['series_id'] - gallery = gallery_map(gallery_row, gallery, chapters, tags, hashes) - if not os.path.exists(gallery.path): - gallery.dead_link = True - ListDB.query_gallery(gallery) - gallery_list.append(gallery) - - return gallery_list - - @classmethod - def get_gallery_by_path(cls, path): - "Returns gallery with given path" - assert isinstance(path, str), "Provided path is invalid" - cursor = cls.execute(cls, 'SELECT * FROM series WHERE series_path=?', (str.encode(path),)) - row = cursor.fetchone() - try: - gallery = Gallery() - gallery.id = row['series_id'] - gallery = gallery_map(row, gallery) - return gallery - except TypeError: - return None - - @classmethod - def get_gallery_by_id(cls, id): - "Returns gallery with given id" - assert isinstance(id, int), "Provided ID is invalid" - cursor = cls.execute(cls, 'SELECT * FROM series WHERE series_id=?', (id,)) - row = cursor.fetchone() - gallery = Gallery() - try: - gallery.id = row['series_id'] - gallery = gallery_map(row, gallery) - return gallery - except TypeError: - return None - - @classmethod - def add_gallery(cls, object, test_mode=False): - "Receives an object of class gallery, and appends it to DB" - "Adds gallery of class into database" - assert isinstance(object, Gallery), "add_gallery method only accepts gallery items" - log_i('Recevied gallery: {}'.format(object.path.encode(errors='ignore'))) - - #TODO: implement mass gallery adding! User execute_many method for effeciency! - - cursor = cls.execute(cls, *default_exec(object)) - series_id = cursor.lastrowid - object.id = series_id - if not object.profile: - Executors.generate_thumbnail(object, on_method=object.set_profile) - if object.tags: - TagDB.add_tags(object) - ChapterDB.add_chapters(object) - - @classmethod - def gallery_count(cls): - """ - Returns the amount of galleries in db. - """ - cursor = cls.execute(cls, "SELECT count(*) AS 'size' FROM series") - return cursor.fetchone()['size'] - - @classmethod - def del_gallery(cls, list_of_gallery, local=False): - "Deletes all galleries in the list recursively." - assert isinstance(list_of_gallery, list), "Please provide a valid list of galleries to delete" - for gallery in list_of_gallery: - if local: - if gallery.is_archive: - s = delete_path(gallery.path) - else: - for chap in gallery.chapters: - path = chap.path - s = delete_path(path) - if not s: - log_e('Failed to delete chapter {}:{}, {}'.format(chap, - gallery.id, gallery.title.encode('utf-8', 'ignore'))) - continue - s = delete_path(gallery.path) - - if not s: - log_e('Failed to delete gallery:{}, {}'.format(gallery.id, - gallery.title.encode('utf-8', 'ignore'))) - continue - - GalleryDB.clear_thumb(gallery.profile) - cls.execute(cls, 'DELETE FROM series WHERE series_id=?', (gallery.id,)) - gallery.id = None - log_i('Successfully deleted: {}'.format(gallery.title.encode('utf-8', 'ignore'))) - app_constants.NOTIF_BAR.add_text('Successfully deleted: {}'.format(gallery.title)) - - @staticmethod - def check_exists(name, galleries=None, filter=True): - """ - Checks if provided string exists in provided sorted - list based on path name. - Note: key will be normcased - """ - #pdb.set_trace() - if galleries is None: - galleries = app_constants.GALLERY_DATA + app_constants.GALLERY_ADDITION_DATA - filter = True - - if filter: - filter_list = [] - for gallery in galleries: - filter_list.append(os.path.normcase(gallery.path)) - filter_list = sorted(filter_list) - else: - filter_list = galleries - - def binary_search(key): - low = 0 - high = len(filter_list) - 1 - while high >= low: - mid = low + (high - low) // 2 - if filter_list[mid] < key: - low = mid + 1 - elif filter_list[mid] > key: - high = mid - 1 - else: - return True - return False - - return binary_search(os.path.normcase(name)) + """ + Provides the following s methods: + rebuild_thumb -> Rebuilds gallery thumbnail + rebuild_galleries -> Rebuilds the galleries in DB + modify_gallery -> Modifies gallery with given gallery id + get_all_gallery -> returns a list of all gallery ( class) currently in DB + get_gallery_by_path -> Returns gallery with given path + get_gallery_by_id -> Returns gallery with given id + add_gallery -> adds gallery into db + set_gallery_title -> changes gallery title + gallery_count -> returns amount of gallery (can be used for indexing) + del_gallery -> deletes the gallery with the given id recursively + check_exists -> Checks if provided string exists + clear_thumb -> Deletes a thumbnail + clear_thumb_dir -> Dletes everything in the thumbnail directory + """ + def __init__(self): + raise Exception("GalleryDB should not be instantiated") + + @staticmethod + def rebuild_thumb(gallery): + "Rebuilds gallery thumbnail" + try: + log_i('Recreating thumb {}'.format(gallery.title.encode(errors='ignore'))) + if gallery.profile: + GalleryDB.clear_thumb(gallery.profile) + gallery.profile = Executors.generate_thumbnail(gallery, blocking=True) + GalleryDB.modify_gallery(gallery.id, + profile=gallery.profile) + except: + log.exception("Failed rebuilding thumbnail") + return False + return True + + @staticmethod + def clear_thumb(path): + "Deletes a thumbnail" + try: + if os.path.samefile(path, app_constants.NO_IMAGE_PATH): + return + except FileNotFoundError: + pass + + try: + os.unlink(path) + except FileNotFoundError: + pass + except: + log.exception('Failed to delete thumb {}'.format(os.path.split(path)[1].encode(errors='ignore'))) + + @staticmethod + def clear_thumb_dir(): + "Deletes everything in the thumbnail directory" + if os.path.exists(db_constants.THUMBNAIL_PATH): + for thumbfile in scandir.scandir(db_constants.THUMBNAIL_PATH): + GalleryDB.clear_thumb(thumbfile.path) + + @staticmethod + def rebuild_gallery(gallery, thumb=False): + "Rebuilds the galleries in DB" + try: + log_i('Rebuilding {}'.format(gallery.title.encode(errors='ignore'))) + log_i("Rebuilding gallery {}".format(gallery.id)) + HashDB.del_gallery_hashes(gallery.id) + GalleryDB.modify_gallery(gallery.id, + title=gallery.title, + artist=gallery.artist, + info=gallery.info, + type=gallery.type, + fav=gallery.fav, + tags=gallery.tags, + language=gallery.language, + status=gallery.status, + pub_date=gallery.pub_date, + link=gallery.link, + times_read=gallery.times_read, + last_read=gallery.last_read, + _db_v=db_constants.CURRENT_DB_VERSION, + exed=gallery.exed, + is_archive=gallery.is_archive, + path_in_archive=gallery.path_in_archive, + view=gallery.view) + if thumb: + GalleryDB.rebuild_thumb(gallery) + except: + log.exception('Failed rebuilding') + return False + return True + + @classmethod + def modify_gallery(cls, series_id, title=None, profile=None, artist=None, info=None, type=None, fav=None, + tags=None, language=None, status=None, pub_date=None, link=None, + times_read=None, last_read=None, series_path=None, chapters=None, _db_v=None, + hashes=None, exed=None, is_archive=None, path_in_archive=None, view=None): + "Modifies gallery with given gallery id" + assert isinstance(series_id, int) + assert not isinstance(series_id, bool) + executing = [] + if title != None: + assert isinstance(title, str) + executing.append(["UPDATE series SET title=? WHERE series_id=?", (title, series_id)]) + if profile != None: + assert isinstance(profile, str) + executing.append(["UPDATE series SET profile=? WHERE series_id=?", (str.encode(profile), series_id)]) + if artist != None: + assert isinstance(artist, str) + executing.append(["UPDATE series SET artist=? WHERE series_id=?", (artist, series_id)]) + if info != None: + assert isinstance(info, str) + executing.append(["UPDATE series SET info=? WHERE series_id=?", (info, series_id)]) + if type != None: + assert isinstance(type, str) + executing.append(["UPDATE series SET type=? WHERE series_id=?", (type, series_id)]) + if fav != None: + assert isinstance(fav, int) + executing.append(["UPDATE series SET fav=? WHERE series_id=?", (fav, series_id)]) + if language != None: + assert isinstance(language, str) + executing.append(["UPDATE series SET language=? WHERE series_id=?", (language, series_id)]) + if status != None: + assert isinstance(status, str) + executing.append(["UPDATE series SET status=? WHERE series_id=?", (status, series_id)]) + if pub_date != None: + executing.append(["UPDATE series SET pub_date=? WHERE series_id=?", (pub_date, series_id)]) + if link != None: + executing.append(["UPDATE series SET link=? WHERE series_id=?", (link, series_id)]) + if times_read != None: + executing.append(["UPDATE series SET times_read=? WHERE series_id=?", (times_read, series_id)]) + if last_read != None: + executing.append(["UPDATE series SET last_read=? WHERE series_id=?", (last_read, series_id)]) + if series_path != None: + executing.append(["UPDATE series SET series_path=? WHERE series_id=?", (str.encode(series_path), series_id)]) + if _db_v != None: + executing.append(["UPDATE series SET db_v=? WHERE series_id=?", (_db_v, series_id)]) + if exed != None: + executing.append(["UPDATE series SET exed=? WHERE series_id=?", (exed, series_id)]) + if is_archive != None: + executing.append(["UPDATE series SET is_archive=? WHERE series_id=?", (is_archive, series_id)]) + if path_in_archive != None: + executing.append(["UPDATE series SET path_in_archive=? WHERE series_id=?", (path_in_archive, series_id)]) + if view != None: + executing.append(["UPDATE series SET view=? WHERE series_id=?", (view, series_id)]) + + if tags != None: + assert isinstance(tags, dict) + TagDB.modify_tags(series_id, tags) + if chapters != None: + assert isinstance(chapters, ChaptersContainer) + ChapterDB.update_chapter(chapters) + + if hashes != None: + assert isinstance(hashes, Gallery) + HashDB.rebuild_gallery_hashes(hashes) + + for query in executing: + cls.execute(cls, *query) + + @classmethod + def get_all_gallery(cls, chapters=True, tags=True, hashes=True): + """ + Careful, might crash with very large libraries i think... + Returns a list of all galleries ( class) currently in DB + """ + cursor = cls.execute(cls, 'SELECT * FROM series') + all_gallery = cursor.fetchall() + return GalleryDB.gen_galleries(all_gallery, chapters, tags, hashes) + + @staticmethod + def gen_galleries(gallery_dict, chapters=True, tags=True, hashes=True): + """ + Map galleries fetched from DB + """ + gallery_list = [] + for gallery_row in gallery_dict: + gallery = Gallery() + gallery.id = gallery_row['series_id'] + gallery = gallery_map(gallery_row, gallery, chapters, tags, hashes) + if not os.path.exists(gallery.path): + gallery.dead_link = True + ListDB.query_gallery(gallery) + gallery_list.append(gallery) + + return gallery_list + + @classmethod + def get_gallery_by_path(cls, path): + "Returns gallery with given path" + assert isinstance(path, str), "Provided path is invalid" + cursor = cls.execute(cls, 'SELECT * FROM series WHERE series_path=?', (str.encode(path),)) + row = cursor.fetchone() + try: + gallery = Gallery() + gallery.id = row['series_id'] + gallery = gallery_map(row, gallery) + return gallery + except TypeError: + return None + + @classmethod + def get_gallery_by_id(cls, id): + "Returns gallery with given id" + assert isinstance(id, int), "Provided ID is invalid" + cursor = cls.execute(cls, 'SELECT * FROM series WHERE series_id=?', (id,)) + row = cursor.fetchone() + gallery = Gallery() + try: + gallery.id = row['series_id'] + gallery = gallery_map(row, gallery) + return gallery + except TypeError: + return None + + @classmethod + def add_gallery(cls, object, test_mode=False): + "Receives an object of class gallery, and appends it to DB" + "Adds gallery of class into database" + assert isinstance(object, Gallery), "add_gallery method only accepts gallery items" + log_i('Recevied gallery: {}'.format(object.path.encode(errors='ignore'))) + + #TODO: implement mass gallery adding! User execute_many method for + #effeciency! + + cursor = cls.execute(cls, *default_exec(object)) + series_id = cursor.lastrowid + object.id = series_id + if not object.profile: + Executors.generate_thumbnail(object, on_method=object.set_profile) + if object.tags: + TagDB.add_tags(object) + ChapterDB.add_chapters(object) + + @classmethod + def gallery_count(cls): + """ + Returns the amount of galleries in db. + """ + cursor = cls.execute(cls, "SELECT count(*) AS 'size' FROM series") + return cursor.fetchone()['size'] + + @classmethod + def del_gallery(cls, list_of_gallery, local=False): + "Deletes all galleries in the list recursively." + assert isinstance(list_of_gallery, list), "Please provide a valid list of galleries to delete" + for gallery in list_of_gallery: + if local: + if gallery.is_archive: + s = delete_path(gallery.path) + else: + for chap in gallery.chapters: + path = chap.path + s = delete_path(path) + if not s: + log_e('Failed to delete chapter {}:{}, {}'.format(chap, + gallery.id, gallery.title.encode('utf-8', 'ignore'))) + continue + s = delete_path(gallery.path) + + if not s: + log_e('Failed to delete gallery:{}, {}'.format(gallery.id, + gallery.title.encode('utf-8', 'ignore'))) + continue + + GalleryDB.clear_thumb(gallery.profile) + cls.execute(cls, 'DELETE FROM series WHERE series_id=?', (gallery.id,)) + gallery.id = None + log_i('Successfully deleted: {}'.format(gallery.title.encode('utf-8', 'ignore'))) + app_constants.NOTIF_BAR.add_text('Successfully deleted: {}'.format(gallery.title)) + + @staticmethod + def check_exists(name, galleries=None, filter=True): + """ + Checks if provided string exists in provided sorted + list based on path name. + Note: key will be normcased + """ + #pdb.set_trace() + if galleries is None: + galleries = app_constants.GALLERY_DATA + app_constants.GALLERY_ADDITION_DATA + filter = True + + if filter: + filter_list = [] + for gallery in galleries: + filter_list.append(os.path.normcase(gallery.path)) + filter_list = sorted(filter_list) + else: + filter_list = galleries + + def binary_search(key): + low = 0 + high = len(filter_list) - 1 + while high >= low: + mid = low + (high - low) // 2 + if filter_list[mid] < key: + low = mid + 1 + elif filter_list[mid] > key: + high = mid - 1 + else: + return True + return False + + return binary_search(os.path.normcase(name)) class ChapterDB(DBBase): - """ - Provides the following database methods: - update_chapter -> Updates an existing chapter in DB - add_chapter -> adds chapter into db - add_chapter_raw -> links chapter to the given seires id, and adds into db - get_chapters_for_gallery -> returns a dict with chapters linked to the given series_id - get_chapter-> returns a dict with chapter matching the given chapter_number - get_chapter_id -> returns id of the chapter number - chapter_size -> returns amount of manga (can be used for indexing) - del_all_chapters <- Deletes all chapters with the given series_id - del_chapter <- Deletes chapter with the given number from gallery - """ - - def __init__(self): - raise Exception("ChapterDB should not be instantiated") - - @classmethod - def update_chapter(cls, chapter_container, numbers=[]): - """ - Updates an existing chapter in DB. - Pass a gallery's ChapterContainer, specify number with a list of ints - leave empty to update all chapters. - """ - assert isinstance(chapter_container, ChaptersContainer) and isinstance(numbers, (list, tuple)) - if numbers: - chapters = [] - for n in numbers: - chapters.append(chapter_container[n]) - else: - chapters = chapter_container.get_all_chapters() - - executing = [] - for chap in chapters: - new_path = chap.path - executing.append((chap.title, str.encode(new_path), chap.pages, chap.in_archive, chap.gallery.id, chap.number,)) - - cls.executemany(cls, "UPDATE chapters SET chapter_title=?, chapter_path=?, pages=?, in_archive=? WHERE series_id=? AND chapter_number=?", - executing) - - @classmethod - def add_chapters(cls, gallery_object): - "Adds chapters linked to gallery into database" - assert isinstance(gallery_object, Gallery), "Parent gallery need to be of class Gallery" - series_id = gallery_object.id - executing = [] - for chap in gallery_object.chapters: - executing.append(default_chap_exec(gallery_object, chap, True)) - if not executing: - raise Exception - cls.executemany(cls, 'INSERT INTO chapters VALUES(NULL, ?, ?, ?, ?, ?, ?)', executing) - - @classmethod - def add_chapters_raw(cls, series_id, chapters_container): - "Adds chapter(s) to a gallery with the received series_id" - assert isinstance(chapters_container, ChaptersContainer), "chapters_container must be of class ChaptersContainer" - executing = [] - for chap in chapters_container: - if not ChapterDB.get_chapter(series_id, chap.number): - executing.append(default_chap_exec(series_id, chap, True)) - else: - ChapterDB.update_chapter(chapters_container, [chap.number]) - - cls.executemany(cls, 'INSERT INTO chapters VALUES(NULL, ?, ?, ?, ?, ?, ?)', executing) - - - @classmethod - def get_chapters_for_gallery(cls, series_id): - """ - Returns a ChaptersContainer of chapters matching the received series_id - """ - assert isinstance(series_id, int), "Please provide a valid gallery ID" - cursor = cls.execute(cls, 'SELECT * FROM chapters WHERE series_id=?', (series_id,)) - rows = cursor.fetchall() - chapters = ChaptersContainer() - - for row in rows: - chap = chapters.create_chapter(row['chapter_number']) - chapter_map(row, chap) - return chapters - - - @classmethod - def get_chapter(cls, series_id, chap_numb): - """Returns a ChaptersContainer of chapters matching the recieved chapter_number - return None for no match - """ - assert isinstance(chap_numb, int), "Please provide a valid chapter number" - cursor = cls.execute(cls, 'SELECT * FROM chapters WHERE series_id=? AND chapter_number=?', (series_id, chap_numb,)) - try: - rows = cursor.fetchall() - chapters = ChaptersContainer() - for row in rows: - chap = chapters.create_chapter(row['chapter_number']) - chapter_map(row, chap) - except TypeError: - return None - return chapters - - @classmethod - def get_chapter_id(cls, series_id, chapter_number): - "Returns id of the chapter number" - assert isinstance(series_id, int) and isinstance(chapter_number, int),\ - "Passed args must be of int not {} and {}".format(type(series_id), type(chapter_number)) - cursor = cls.execute(cls, 'SELECT chapter_id FROM chapters WHERE series_id=? AND chapter_number=?', - (series_id, chapter_number,)) - try: - row = cursor.fetchone() - chp_id = row['chapter_id'] - return chp_id - except KeyError: - return None - except TypeError: - return None - - @staticmethod - def chapter_size(gallery_id): - """Returns the amount of chapters for the given - gallery id - """ - pass - - @classmethod - def del_all_chapters(cls, series_id): - "Deletes all chapters with the given series_id" - assert isinstance(series_id, int), "Please provide a valid gallery ID" - cls.execute(cls, 'DELETE FROM chapters WHERE series_id=?', (series_id,)) - - @classmethod - def del_chapter(cls, series_id, chap_number): - "Deletes chapter with the given number from gallery" - assert isinstance(series_id, int), "Please provide a valid gallery ID" - assert isinstance(chap_number, int), "Please provide a valid chapter number" - cls.execute(cls, 'DELETE FROM chapters WHERE series_id=? AND chapter_number=?', - (series_id, chap_number,)) + """ + Provides the following database methods: + update_chapter -> Updates an existing chapter in DB + add_chapter -> adds chapter into db + add_chapter_raw -> links chapter to the given seires id, and adds into db + get_chapters_for_gallery -> returns a dict with chapters linked to the given series_id + get_chapter-> returns a dict with chapter matching the given chapter_number + get_chapter_id -> returns id of the chapter number + chapter_size -> returns amount of manga (can be used for indexing) + del_all_chapters <- Deletes all chapters with the given series_id + del_chapter <- Deletes chapter with the given number from gallery + """ + + def __init__(self): + raise Exception("ChapterDB should not be instantiated") + + @classmethod + def update_chapter(cls, chapter_container, numbers=[]): + """ + Updates an existing chapter in DB. + Pass a gallery's ChapterContainer, specify number with a list of ints + leave empty to update all chapters. + """ + assert isinstance(chapter_container, ChaptersContainer) and isinstance(numbers, (list, tuple)) + if numbers: + chapters = [] + for n in numbers: + chapters.append(chapter_container[n]) + else: + chapters = chapter_container.get_all_chapters() + + executing = [] + for chap in chapters: + new_path = chap.path + executing.append((chap.title, str.encode(new_path), chap.pages, chap.in_archive, chap.gallery.id, chap.number,)) + + cls.executemany(cls, "UPDATE chapters SET chapter_title=?, chapter_path=?, pages=?, in_archive=? WHERE series_id=? AND chapter_number=?", + executing) + + @classmethod + def add_chapters(cls, gallery_object): + "Adds chapters linked to gallery into database" + assert isinstance(gallery_object, Gallery), "Parent gallery need to be of class Gallery" + series_id = gallery_object.id + executing = [] + for chap in gallery_object.chapters: + executing.append(default_chap_exec(gallery_object, chap, True)) + if not executing: + raise Exception + cls.executemany(cls, 'INSERT INTO chapters VALUES(NULL, ?, ?, ?, ?, ?, ?)', executing) + + @classmethod + def add_chapters_raw(cls, series_id, chapters_container): + "Adds chapter(s) to a gallery with the received series_id" + assert isinstance(chapters_container, ChaptersContainer), "chapters_container must be of class ChaptersContainer" + executing = [] + for chap in chapters_container: + if not ChapterDB.get_chapter(series_id, chap.number): + executing.append(default_chap_exec(series_id, chap, True)) + else: + ChapterDB.update_chapter(chapters_container, [chap.number]) + + cls.executemany(cls, 'INSERT INTO chapters VALUES(NULL, ?, ?, ?, ?, ?, ?)', executing) + + + @classmethod + def get_chapters_for_gallery(cls, series_id): + """ + Returns a ChaptersContainer of chapters matching the received series_id + """ + assert isinstance(series_id, int), "Please provide a valid gallery ID" + cursor = cls.execute(cls, 'SELECT * FROM chapters WHERE series_id=?', (series_id,)) + rows = cursor.fetchall() + chapters = ChaptersContainer() + + for row in rows: + chap = chapters.create_chapter(row['chapter_number']) + chapter_map(row, chap) + return chapters + + + @classmethod + def get_chapter(cls, series_id, chap_numb): + """Returns a ChaptersContainer of chapters matching the recieved chapter_number + return None for no match + """ + assert isinstance(chap_numb, int), "Please provide a valid chapter number" + cursor = cls.execute(cls, 'SELECT * FROM chapters WHERE series_id=? AND chapter_number=?', (series_id, chap_numb,)) + try: + rows = cursor.fetchall() + chapters = ChaptersContainer() + for row in rows: + chap = chapters.create_chapter(row['chapter_number']) + chapter_map(row, chap) + except TypeError: + return None + return chapters + + @classmethod + def get_chapter_id(cls, series_id, chapter_number): + "Returns id of the chapter number" + assert isinstance(series_id, int) and isinstance(chapter_number, int),\ + "Passed args must be of int not {} and {}".format(type(series_id), type(chapter_number)) + cursor = cls.execute(cls, 'SELECT chapter_id FROM chapters WHERE series_id=? AND chapter_number=?', + (series_id, chapter_number,)) + try: + row = cursor.fetchone() + chp_id = row['chapter_id'] + return chp_id + except KeyError: + return None + except TypeError: + return None + + @staticmethod + def chapter_size(gallery_id): + """Returns the amount of chapters for the given + gallery id + """ + pass + + @classmethod + def del_all_chapters(cls, series_id): + "Deletes all chapters with the given series_id" + assert isinstance(series_id, int), "Please provide a valid gallery ID" + cls.execute(cls, 'DELETE FROM chapters WHERE series_id=?', (series_id,)) + + @classmethod + def del_chapter(cls, series_id, chap_number): + "Deletes chapter with the given number from gallery" + assert isinstance(series_id, int), "Please provide a valid gallery ID" + assert isinstance(chap_number, int), "Please provide a valid chapter number" + cls.execute(cls, 'DELETE FROM chapters WHERE series_id=? AND chapter_number=?', + (series_id, chap_number,)) class TagDB(DBBase): - """ - Tags are returned in a dict where {"namespace":["tag1","tag2"]} - The namespace "default" will be used for tags without namespaces. - - Provides the following methods: - del_tags <- Deletes the tags with corresponding tag_ids from DB - del_gallery_tags_mapping <- Deletes the tags and gallery mappings with corresponding series_ids from DB - get_gallery_tags -> Returns all tags and namespaces found for the given series_id; - get_tag_gallery -> Returns all galleries with the given tag - get_ns_tags -> "Returns a dict with namespace as key and list of tags as value" - get_ns_tags_to_gallery -> Returns all galleries linked to the namespace tags. Receives a dict like this: {"namespace":["tag1","tag2"]} - get_tags_from_namespace -> Returns all galleries linked to the namespace - add_tags <- Adds the given dict_of_tags to the given series_id - modify_tags <- Modifies the given tags - get_all_tags -> Returns all tags in database - get_all_ns -> Returns all namespaces in database - """ - - def __init__(self): - raise Exception("TagsDB should not be instantiated") - - @staticmethod - def del_tags(list_of_tags_id): - "Deletes the tags with corresponding tag_ids from DB" - pass - - @classmethod - def del_gallery_mapping(cls, series_id): - "Deletes the tags and gallery mappings with corresponding series_ids from DB" - assert isinstance(series_id, int), "Please provide a valid gallery id" - - # delete all mappings related to the given series_id - cls.execute(cls, 'DELETE FROM series_tags_map WHERE series_id=?', [series_id]) - - @classmethod - def get_gallery_tags(cls, series_id): - "Returns all tags and namespaces found for the given series_id" - if not isinstance(series_id, int): - return {} - cursor = cls.execute(cls, 'SELECT tags_mappings_id FROM series_tags_map WHERE series_id=?', - (series_id,)) - tags = {} - result = cursor.fetchall() - for tag_map_row in result: # iterate all tag_mappings_ids - try: - if not tag_map_row: - continue - # get tag and namespace - c = cls.execute(cls, 'SELECT namespace_id, tag_id FROM tags_mappings WHERE tags_mappings_id=?', - (tag_map_row['tags_mappings_id'],)) - for row in c.fetchall(): # iterate all rows - # get namespace - c = cls.execute(cls, 'SELECT namespace FROM namespaces WHERE namespace_id=?', - (row['namespace_id'],)) - try: - namespace = c.fetchone()['namespace'] - except TypeError: - continue - - # get tag - c = cls.execute(cls, 'SELECT tag FROM tags WHERE tag_id=?', (row['tag_id'],)) - try: - tag = c.fetchone()['tag'] - except TypeError: - continue - - # add them to dict - if not namespace in tags: - tags[namespace] = [tag] - else: - # namespace already exists in dict - tags[namespace].append(tag) - except IndexError: - continue - return tags - - @classmethod - def add_tags(cls, object): - "Adds the given dict_of_tags to the given series_id" - assert isinstance(object, Gallery), "Please provide a valid gallery of class gallery" - - series_id = object.id - dict_of_tags = object.tags - - def look_exists(tag_or_ns, what): - """check if tag or namespace already exists in base - returns id, else returns None""" - c = cls.execute(cls, 'SELECT {}_id FROM {}s WHERE {} = ?'.format(what, what, what), - (tag_or_ns,)) - try: # exists - return c.fetchone()['{}_id'.format(what)] - except TypeError: # doesnt exist - return None - except IndexError: - return None - - tags_mappings_id_list = [] - # first let's add the tags and namespaces to db - for namespace in dict_of_tags: - tags_list = dict_of_tags[namespace] - # don't add if it already exists - try: - namespace_id = look_exists(namespace, "namespace") - if not namespace_id: - raise ValueError - except ValueError: - c = cls.execute(cls, 'INSERT INTO namespaces(namespace) VALUES(?)', (namespace,)) - namespace_id = c.lastrowid - - tags_id_list = [] - for tag in tags_list: - try: - tag_id = look_exists(tag, "tag") - if not tag_id: - raise ValueError - except ValueError: - c = cls.execute(cls, 'INSERT INTO tags(tag) VALUES(?)', (tag,)) - tag_id = c.lastrowid - - tags_id_list.append(tag_id) - - - def look_exist_tag_map(tag_id): - "Checks DB if the tag_id already exists with the namespace_id, returns id else None" - c = cls.execute(cls, 'SELECT tags_mappings_id FROM tags_mappings WHERE namespace_id=? AND tag_id=?', - (namespace_id, tag_id,)) - try: # exists - return c.fetchone()['tags_mappings_id'] - except TypeError: # doesnt exist - return None - except IndexError: - return None - - # time to map the tags to the namespace now - for tag_id in tags_id_list: - # First check if tags mappings exists - try: - t_map_id = look_exist_tag_map(tag_id) - if t_map_id: - tags_mappings_id_list.append(t_map_id) - else: - raise TypeError - except TypeError: - c = cls.execute(cls, 'INSERT INTO tags_mappings(namespace_id, tag_id) VALUES(?, ?)', - (namespace_id, tag_id,)) - # add the tags_mappings_id to our list - tags_mappings_id_list.append(c.lastrowid) - - # Lastly we map the series_id to the tags_mappings - executing = [] - for tags_map in tags_mappings_id_list: - executing.append((series_id, tags_map,)) - #cls.execute(cls, 'INSERT INTO series_tags_map(series_id, tags_mappings_id) - #VALUES(?, ?)', (series_id, tags_map,)) - cls.executemany(cls, 'INSERT OR IGNORE INTO series_tags_map(series_id, tags_mappings_id) VALUES(?, ?)', executing) - - @staticmethod - def modify_tags(series_id, dict_of_tags): - "Modifies the given tags" - - # We first delete all mappings - TagDB.del_gallery_mapping(series_id) - - # Now we add the new tags to DB - weak_gallery = Gallery() - weak_gallery.id = series_id - weak_gallery.tags = dict_of_tags - - TagDB.add_tags(weak_gallery) - - - @staticmethod - def get_tag_gallery(tag): - "Returns all galleries with the given tag" - pass - - @classmethod - def get_ns_tags(cls): - "Returns a dict of all tags with namespace as key and list of tags as value" - cursor = cls.execute(cls, 'SELECT namespace_id, tag_id FROM tags_mappings') - ns_tags = {} - ns_id_history = {} # to avoid unesseccary DB fetching - for t in cursor.fetchall(): - try: - # get namespace - if not t['namespace_id'] in ns_id_history: - c = cls.execute(cls, 'SELECT namespace FROM namespaces WHERE namespace_id=?', (t['namespace_id'],)) - ns = c.fetchone()['namespace'] - ns_id_history[t['namespace_id']] = ns - else: - ns = ns_id_history[t['namespace_id']] - # get tag - c = cls.execute(cls, 'SELECT tag FROM tags WHERE tag_id=?', (t['tag_id'],)) - tag = c.fetchone()['tag'] - # put in dict - if ns in ns_tags: - ns_tags[ns].append(tag) - else: - ns_tags[ns] = [tag] - except: - continue - return ns_tags - - @staticmethod - def get_tags_from_namespace(namespace): - "Returns a dict with namespace as key and list of tags as value" - pass - - @staticmethod - def get_ns_tags_to_gallery(ns_tags): - """ - Returns all galleries linked to the namespace tags. - Receives a dict like this: {"namespace":["tag1","tag2"]} - """ - pass - - @classmethod - def get_all_tags(cls): - """ - Returns all tags in database in a list - """ - cursor = cls.execute(cls, 'SELECT tag FROM tags') - tags = [t['tag'] for t in cursor.fetchall()] - return tags - - @classmethod - def get_all_ns(cls): - """ - Returns all namespaces in database in a list - """ - cursor = cls.execute(cls, 'SELECT namespace FROM namespaces') - ns = [n['namespace'] for n in cursor.fetchall()] - return ns + """ + Tags are returned in a dict where {"namespace":["tag1","tag2"]} + The namespace "default" will be used for tags without namespaces. + + Provides the following methods: + del_tags <- Deletes the tags with corresponding tag_ids from DB + del_gallery_tags_mapping <- Deletes the tags and gallery mappings with corresponding series_ids from DB + get_gallery_tags -> Returns all tags and namespaces found for the given series_id; + get_tag_gallery -> Returns all galleries with the given tag + get_ns_tags -> "Returns a dict with namespace as key and list of tags as value" + get_ns_tags_to_gallery -> Returns all galleries linked to the namespace tags. Receives a dict like this: {"namespace":["tag1","tag2"]} + get_tags_from_namespace -> Returns all galleries linked to the namespace + add_tags <- Adds the given dict_of_tags to the given series_id + modify_tags <- Modifies the given tags + get_all_tags -> Returns all tags in database + get_all_ns -> Returns all namespaces in database + """ + + def __init__(self): + raise Exception("TagsDB should not be instantiated") + + @staticmethod + def del_tags(list_of_tags_id): + "Deletes the tags with corresponding tag_ids from DB" + pass + + @classmethod + def del_gallery_mapping(cls, series_id): + "Deletes the tags and gallery mappings with corresponding series_ids from DB" + assert isinstance(series_id, int), "Please provide a valid gallery id" + + # delete all mappings related to the given series_id + cls.execute(cls, 'DELETE FROM series_tags_map WHERE series_id=?', [series_id]) + + @classmethod + def get_gallery_tags(cls, series_id): + "Returns all tags and namespaces found for the given series_id" + if not isinstance(series_id, int): + return {} + cursor = cls.execute(cls, 'SELECT tags_mappings_id FROM series_tags_map WHERE series_id=?', + (series_id,)) + tags = {} + result = cursor.fetchall() + for tag_map_row in result: # iterate all tag_mappings_ids + try: + if not tag_map_row: + continue + # get tag and namespace + c = cls.execute(cls, 'SELECT namespace_id, tag_id FROM tags_mappings WHERE tags_mappings_id=?', + (tag_map_row['tags_mappings_id'],)) + for row in c.fetchall(): # iterate all rows + # get namespace + c = cls.execute(cls, 'SELECT namespace FROM namespaces WHERE namespace_id=?', + (row['namespace_id'],)) + try: + namespace = c.fetchone()['namespace'] + except TypeError: + continue + + # get tag + c = cls.execute(cls, 'SELECT tag FROM tags WHERE tag_id=?', (row['tag_id'],)) + try: + tag = c.fetchone()['tag'] + except TypeError: + continue + + # add them to dict + if not namespace in tags: + tags[namespace] = [tag] + else: + # namespace already exists in dict + tags[namespace].append(tag) + except IndexError: + continue + return tags + + @classmethod + def add_tags(cls, object): + "Adds the given dict_of_tags to the given series_id" + assert isinstance(object, Gallery), "Please provide a valid gallery of class gallery" + + series_id = object.id + dict_of_tags = object.tags + + def look_exists(tag_or_ns, what): + """check if tag or namespace already exists in base + returns id, else returns None""" + c = cls.execute(cls, 'SELECT {}_id FROM {}s WHERE {} = ?'.format(what, what, what), + (tag_or_ns,)) + try: # exists + return c.fetchone()['{}_id'.format(what)] + except TypeError: # doesnt exist + return None + except IndexError: + return None + + tags_mappings_id_list = [] + # first let's add the tags and namespaces to db + for namespace in dict_of_tags: + tags_list = dict_of_tags[namespace] + # don't add if it already exists + try: + namespace_id = look_exists(namespace, "namespace") + if not namespace_id: + raise ValueError + except ValueError: + c = cls.execute(cls, 'INSERT INTO namespaces(namespace) VALUES(?)', (namespace,)) + namespace_id = c.lastrowid + + tags_id_list = [] + for tag in tags_list: + try: + tag_id = look_exists(tag, "tag") + if not tag_id: + raise ValueError + except ValueError: + c = cls.execute(cls, 'INSERT INTO tags(tag) VALUES(?)', (tag,)) + tag_id = c.lastrowid + + tags_id_list.append(tag_id) + + + def look_exist_tag_map(tag_id): + "Checks DB if the tag_id already exists with the namespace_id, returns id else None" + c = cls.execute(cls, 'SELECT tags_mappings_id FROM tags_mappings WHERE namespace_id=? AND tag_id=?', + (namespace_id, tag_id,)) + try: # exists + return c.fetchone()['tags_mappings_id'] + except TypeError: # doesnt exist + return None + except IndexError: + return None + + # time to map the tags to the namespace now + for tag_id in tags_id_list: + # First check if tags mappings exists + try: + t_map_id = look_exist_tag_map(tag_id) + if t_map_id: + tags_mappings_id_list.append(t_map_id) + else: + raise TypeError + except TypeError: + c = cls.execute(cls, 'INSERT INTO tags_mappings(namespace_id, tag_id) VALUES(?, ?)', + (namespace_id, tag_id,)) + # add the tags_mappings_id to our list + tags_mappings_id_list.append(c.lastrowid) + + # Lastly we map the series_id to the tags_mappings + executing = [] + for tags_map in tags_mappings_id_list: + executing.append((series_id, tags_map,)) + #cls.execute(cls, 'INSERT INTO series_tags_map(series_id, tags_mappings_id) + #VALUES(?, ?)', (series_id, tags_map,)) + cls.executemany(cls, 'INSERT OR IGNORE INTO series_tags_map(series_id, tags_mappings_id) VALUES(?, ?)', executing) + + @staticmethod + def modify_tags(series_id, dict_of_tags): + "Modifies the given tags" + + # We first delete all mappings + TagDB.del_gallery_mapping(series_id) + + # Now we add the new tags to DB + weak_gallery = Gallery() + weak_gallery.id = series_id + weak_gallery.tags = dict_of_tags + + TagDB.add_tags(weak_gallery) + + + @staticmethod + def get_tag_gallery(tag): + "Returns all galleries with the given tag" + pass + + @classmethod + def get_ns_tags(cls): + "Returns a dict of all tags with namespace as key and list of tags as value" + cursor = cls.execute(cls, 'SELECT namespace_id, tag_id FROM tags_mappings') + ns_tags = {} + ns_id_history = {} # to avoid unesseccary DB fetching + for t in cursor.fetchall(): + try: + # get namespace + if not t['namespace_id'] in ns_id_history: + c = cls.execute(cls, 'SELECT namespace FROM namespaces WHERE namespace_id=?', (t['namespace_id'],)) + ns = c.fetchone()['namespace'] + ns_id_history[t['namespace_id']] = ns + else: + ns = ns_id_history[t['namespace_id']] + # get tag + c = cls.execute(cls, 'SELECT tag FROM tags WHERE tag_id=?', (t['tag_id'],)) + tag = c.fetchone()['tag'] + # put in dict + if ns in ns_tags: + ns_tags[ns].append(tag) + else: + ns_tags[ns] = [tag] + except: + continue + return ns_tags + + @staticmethod + def get_tags_from_namespace(namespace): + "Returns a dict with namespace as key and list of tags as value" + pass + + @staticmethod + def get_ns_tags_to_gallery(ns_tags): + """ + Returns all galleries linked to the namespace tags. + Receives a dict like this: {"namespace":["tag1","tag2"]} + """ + pass + + @classmethod + def get_all_tags(cls): + """ + Returns all tags in database in a list + """ + cursor = cls.execute(cls, 'SELECT tag FROM tags') + tags = [t['tag'] for t in cursor.fetchall()] + return tags + + @classmethod + def get_all_ns(cls): + """ + Returns all namespaces in database in a list + """ + cursor = cls.execute(cls, 'SELECT namespace FROM namespaces') + ns = [n['namespace'] for n in cursor.fetchall()] + return ns class ListDB(DBBase): - """ - """ - - - @classmethod - def init_lists(cls): - "Creates and returns lists fetched from DB" - lists = [] - c = cls.execute(cls, 'SELECT * FROM list') - list_rows = c.fetchall() - for l_row in list_rows: - l = GalleryList(l_row['list_name'], filter=l_row['list_filter'], id=l_row['list_id']) - if l_row['type'] == GalleryList.COLLECTION: - l.type = GalleryList.COLLECTION - elif l_row['type'] == GalleryList.REGULAR: - l.type = GalleryList.REGULAR - profile = l_row['profile'] - if profile: - l.profile = bytes.decode(profile) - l.enforce = bool(l_row['enforce']) - l.regex = bool(l_row['regex']) - l.case = bool(l_row['l_case']) - l.strict = bool(l_row['strict']) - lists.append(l) - app_constants.GALLERY_LISTS.add(l) - - return lists - - @classmethod - def query_gallery(cls, gallery): - "Maps gallery to the correct lists" - - c = cls.execute(cls, 'SELECT list_id FROM series_list_map WHERE series_id=?', (gallery.id,)) - list_rows = [x['list_id'] for x in c.fetchall()] - for l in app_constants.GALLERY_LISTS: - if l._id in list_rows: - l.add_gallery(gallery, False, _check_filter=False) - - @classmethod - def modify_list(cls, gallery_list): - assert isinstance(gallery_list, GalleryList) - if gallery_list._id: - cls.execute(cls, - """UPDATE list SET list_name=?, list_filter=?, profile=?, - type=?, enforce=?, regex=?, l_case=?, strict=? WHERE list_id=?""", - (gallery_list.name, gallery_list.filter, str.encode(gallery_list.profile), - gallery_list.type, int(gallery_list.enforce), int(gallery_list.regex), int(gallery_list.case), - int(gallery_list.strict), gallery_list._id)) - - @classmethod - def add_list(cls, gallery_list): - "Adds a list of GalleryList class to DB" - assert isinstance(gallery_list, GalleryList) - if gallery_list._id: - ListDB.modify_list(gallery_list) - else: - c = cls.execute(cls, """INSERT INTO list(list_name, list_filter, profile, type, - enforce, regex, l_case, strict) VALUES(?, ?, ?, ?, ?, ?, ?, ?)""", - (gallery_list.name, gallery_list.filter, str.encode(gallery_list.profile), gallery_list.type, - int(gallery_list.enforce), int(gallery_list.regex), int(gallery_list.case), int(gallery_list.strict))) - gallery_list._id = c.lastrowid - - ListDB.add_gallery_to_list(gallery_list.galleries(), gallery_list) - - @classmethod - def _g_id_or_list(cls, gallery_or_id_or_list): - "Returns gallery ids" - if isinstance(gallery_or_id_or_list, (Gallery, int)): - gallery_or_id_or_list = [gallery_or_id_or_list] - - if isinstance(gallery_or_id_or_list, list): - if gallery_or_id_or_list: - if isinstance(gallery_or_id_or_list[0], Gallery): - gallery_or_id_or_list = [g.id for g in gallery_or_id_or_list] - return gallery_or_id_or_list - - @classmethod - def add_gallery_to_list(cls, gallery_or_id_or_list, gallery_list): - assert isinstance(gallery_list, GalleryList) - "Maps provided gallery or list of galleries or gallery id to list" - g_ids = ListDB._g_id_or_list(gallery_or_id_or_list) - - values = [(gallery_list._id, x) for x in g_ids] - cls.executemany(cls, 'INSERT OR IGNORE INTO series_list_map(list_id, series_id) VALUES(?, ?)', values) - - @classmethod - def remove_list(cls, gallery_list): - "Deletes list from DB" - assert isinstance(gallery_list, GalleryList) - if gallery_list._id: - cls.execute(cls, 'DELETE FROM list WHERE list_id=?', (gallery_list._id,)) - try: - app_constants.GALLERY_LISTS.remove(gallery_list) - except KeyError: - pass - - @classmethod - def remove_gallery_from_list(cls, gallery_or_id_or_list, gallery_list): - assert isinstance(gallery_list, GalleryList) - "Removes provided gallery or list of galleries or gallery id from list" - if gallery_list._id: - g_ids = ListDB._g_id_or_list(gallery_or_id_or_list) - - values = [(gallery_list._id, x) for x in g_ids] - cls.executemany(cls, 'DELETE FROM series_list_map WHERE list_id=? AND series_id=?', values) + """ + """ + + + @classmethod + def init_lists(cls): + "Creates and returns lists fetched from DB" + lists = [] + c = cls.execute(cls, 'SELECT * FROM list') + list_rows = c.fetchall() + for l_row in list_rows: + l = GalleryList(l_row['list_name'], filter=l_row['list_filter'], id=l_row['list_id']) + if l_row['type'] == GalleryList.COLLECTION: + l.type = GalleryList.COLLECTION + elif l_row['type'] == GalleryList.REGULAR: + l.type = GalleryList.REGULAR + profile = l_row['profile'] + if profile: + l.profile = bytes.decode(profile) + l.enforce = bool(l_row['enforce']) + l.regex = bool(l_row['regex']) + l.case = bool(l_row['l_case']) + l.strict = bool(l_row['strict']) + lists.append(l) + app_constants.GALLERY_LISTS.add(l) + + return lists + + @classmethod + def query_gallery(cls, gallery): + "Maps gallery to the correct lists" + + c = cls.execute(cls, 'SELECT list_id FROM series_list_map WHERE series_id=?', (gallery.id,)) + list_rows = [x['list_id'] for x in c.fetchall()] + for l in app_constants.GALLERY_LISTS: + if l._id in list_rows: + l.add_gallery(gallery, False, _check_filter=False) + + @classmethod + def modify_list(cls, gallery_list): + assert isinstance(gallery_list, GalleryList) + if gallery_list._id: + cls.execute(cls, + """UPDATE list SET list_name=?, list_filter=?, profile=?, + type=?, enforce=?, regex=?, l_case=?, strict=? WHERE list_id=?""", + (gallery_list.name, gallery_list.filter, str.encode(gallery_list.profile), + gallery_list.type, int(gallery_list.enforce), int(gallery_list.regex), int(gallery_list.case), + int(gallery_list.strict), gallery_list._id)) + + @classmethod + def add_list(cls, gallery_list): + "Adds a list of GalleryList class to DB" + assert isinstance(gallery_list, GalleryList) + if gallery_list._id: + ListDB.modify_list(gallery_list) + else: + c = cls.execute(cls, """INSERT INTO list(list_name, list_filter, profile, type, + enforce, regex, l_case, strict) VALUES(?, ?, ?, ?, ?, ?, ?, ?)""", + (gallery_list.name, gallery_list.filter, str.encode(gallery_list.profile), gallery_list.type, + int(gallery_list.enforce), int(gallery_list.regex), int(gallery_list.case), int(gallery_list.strict))) + gallery_list._id = c.lastrowid + + ListDB.add_gallery_to_list(gallery_list.galleries(), gallery_list) + + @classmethod + def _g_id_or_list(cls, gallery_or_id_or_list): + "Returns gallery ids" + if isinstance(gallery_or_id_or_list, (Gallery, int)): + gallery_or_id_or_list = [gallery_or_id_or_list] + + if isinstance(gallery_or_id_or_list, list): + if gallery_or_id_or_list: + if isinstance(gallery_or_id_or_list[0], Gallery): + gallery_or_id_or_list = [g.id for g in gallery_or_id_or_list] + return gallery_or_id_or_list + + @classmethod + def add_gallery_to_list(cls, gallery_or_id_or_list, gallery_list): + assert isinstance(gallery_list, GalleryList) + "Maps provided gallery or list of galleries or gallery id to list" + g_ids = ListDB._g_id_or_list(gallery_or_id_or_list) + + values = [(gallery_list._id, x) for x in g_ids] + cls.executemany(cls, 'INSERT OR IGNORE INTO series_list_map(list_id, series_id) VALUES(?, ?)', values) + + @classmethod + def remove_list(cls, gallery_list): + "Deletes list from DB" + assert isinstance(gallery_list, GalleryList) + if gallery_list._id: + cls.execute(cls, 'DELETE FROM list WHERE list_id=?', (gallery_list._id,)) + try: + app_constants.GALLERY_LISTS.remove(gallery_list) + except KeyError: + pass + + @classmethod + def remove_gallery_from_list(cls, gallery_or_id_or_list, gallery_list): + assert isinstance(gallery_list, GalleryList) + "Removes provided gallery or list of galleries or gallery id from list" + if gallery_list._id: + g_ids = ListDB._g_id_or_list(gallery_or_id_or_list) + + values = [(gallery_list._id, x) for x in g_ids] + cls.executemany(cls, 'DELETE FROM series_list_map WHERE list_id=? AND series_id=?', values) class HashDB(DBBase): - """ - Contains the following methods: - - find_gallery -> returns galleries which matches the given list of hashes - get_gallery_hashes -> returns all hashes with the given gallery id in a list - get_gallery_hash -> returns hash of chapter specified. If page is specified, returns hash of chapter page - gen_gallery_hashes <- generates hashes for gallery's chapters and inserts them to db - rebuild_gallery_hashes <- inserts hashes into DB only if it doesnt already exist - """ - - @classmethod - def find_gallery(cls, hashes): - assert isinstance(hashes, list) - gallery_ids = {} - hash_status = [] - for hash in hashes: - r = cls.execute(cls, 'SELECT series_id FROM hashes WHERE hash=?', (hash,)) - try: - g_ids = r.fetchall() - for r in g_ids: - g_id = r['series_id'] - if g_id not in gallery_ids: - gallery_ids[g_id] = 1 - else: - gallery_ids[g_id] = gallery_ids[g_id] + 1 - if g_ids: - hash_status.append(True) - else: - hash_status.append(False) - except KeyError: - hash_status.append(False) - except TypeError: - hash_status.append(False) - - if all(hash_status): - # the one with most matching hashes - g_id = None - h_match_count = 0 - for g in gallery_ids: - if gallery_ids[g] > h_match_count: - h_match_count = gallery_ids[h] - g_id = g - if g_id: - weak_gallery = Gallery() - weak_gallery.id = g_id - return weak_gallery - - return None - - @classmethod - def get_gallery_hashes(cls, gallery_id): - "Returns all hashes with the given gallery id in a list" - cursor = cls.execute(cls, 'SELECT hash FROM hashes WHERE series_id=?', - (gallery_id,)) - hashes = [] - try: - for row in cursor.fetchall(): - hashes.append(row['hash']) - except IndexError: - return [] - return hashes - - @classmethod - def get_gallery_hash(cls, gallery_id, chapter, page=None): - """ - returns hash of chapter. If page is specified, returns hash of chapter page - """ - assert isinstance(gallery_id, int) - assert isinstance(chapter, int) - if page: - assert isinstance(page, int) - chap_id = ChapterDB.get_chapter_id(gallery_id, chapter) - if not chap_id: - return None - if page: - exceuting = ["SELECT hash FROM hashes WHERE series_id=? AND chapter_id=? AND page=?", - (gallery_id, chap_id, page)] - else: - exceuting = ["SELECT hash FROM hashes WHERE series_id=? AND chapter_id=?", - (gallery_id, chap_id)] - hashes = [] - c = cls.execute(cls, *exceuting) - for h in c.fetchall(): - try: - hashes.append(h['hash']) - except KeyError: - pass - return hashes - - @classmethod - def gen_gallery_hash(cls, gallery, chapter, page=None, color_img=False, _name=None): - """ - Generate hash for a specific chapter. - Set page to only generate specific page - page: 'mid' or number or list of numbers - color_img: if true then a hash to colored img will be returned if possible - Returns dict with chapter number or 'mid' as key and hash as value - """ - assert isinstance(gallery, Gallery) - assert isinstance(chapter, int) - if page != None: - assert isinstance(page, (int, str, list)) - skip_gen = False - if gallery.id: - chap_id = ChapterDB.get_chapter_id(gallery.id, chapter) - - c = cls.execute(cls, 'SELECT hash, page FROM hashes WHERE series_id=? AND chapter_id=?', - (gallery.id, chap_id,)) - hashes = {} - for r in c.fetchall(): - try: - if r['hash'] and r['page'] != None: - hashes[r['page']] = r['hash'] - except TypeError: - pass - if isinstance(page, (int, list)): - if isinstance(page, int): - _page = [page] - else: - _page = page - h = {} - t = False - for p in _page: - if p in hashes: - h[p] = hashes[p] - else: - t = True - if not t: - skip_gen = True - hashes = h - - elif gallery.chapters[chapter].pages == len(hashes.keys()): - skip_gen = True - if page == "mid": - try: - hashes = {'mid':hashes[len(hashes) // 2]} - except KeyError: - skip_gen = False - - - if not skip_gen or color_img: - - def look_exists(page): - """check if hash already exists in database - returns hash, else returns None""" - c = cls.execute(cls, 'SELECT hash FROM hashes WHERE page=? AND chapter_id=?', - (page, chap_id,)) - try: # exists - return c.fetchone()['hash'] - except TypeError: # doesnt exist - return None - except IndexError: - return None - - if gallery.dead_link: - log_e("Could not generate hash of dead gallery: {}".format(gallery.title.encode(errors='ignore'))) - return {} - - try: - chap = gallery.chapters[chapter] - except KeyError: - utils.make_chapters(gallery) - try: - chap = gallery.chapters[chapter] - except KeyError: - return {} - - executing = [] - try: - if gallery.is_archive: - raise NotADirectoryError - imgs = sorted([x.path for x in scandir.scandir(chap.path) if x.path.endswith(utils.IMG_FILES)]) - pages = {} - for n, i in enumerate(imgs): - pages[n] = i - - if page != None: - pages = {} - if color_img: - # if first img is colored, then return filepath of that - if not utils.image_greyscale(imgs[0]): - return {'color':imgs[0]} - if page == 'mid': - imgs = imgs[len(imgs) // 2] - pages[len(imgs) // 2] = imgs - elif isinstance(page, list): - try: - for p in page: - pages[p] = imgs[p] - except IndexError: - raise app_constants.InternalPagesMismatch - else: - imgs = imgs[page] - pages = {page:imgs} - - hashes = {} - if gallery.id != None: - for p in pages: - h = look_exists(p) - if not h: - with open(pages[p], 'rb') as f: - h = generate_img_hash(f) - executing.append((h, gallery.id, chap_id, p,)) - hashes[p] = h - else: - for i in pages: - with open(pages[i], 'rb') as f: - hashes[i] = generate_img_hash(f) - - except NotADirectoryError: - temp_dir = os.path.join(app_constants.temp_dir, str(uuid.uuid4())) - is_archive = gallery.is_archive - try: - if is_archive: - zip = ArchiveFile(gallery.path) - else: - zip = ArchiveFile(chap.path) - except app_constants.CreateArchiveFail: - log_e('Could not generate hash: CreateZipFail') - return {} - - pages = {} - if page != None: - p = 0 - con = sorted(zip.dir_contents(chap.path)) - if color_img: - # if first img is colored, then return hash of that - f_bytes = io.BytesIO(zip.open(con[0], False)) - if not utils.image_greyscale(f_bytes): - return {'color':zip.extract(con[0])} - f_bytes.close() - if page == 'mid': - p = len(con) // 2 - img = con[p] - pages = {p:zip.open(img, True)} - elif isinstance(page, list): - for x in page: - pages[x] = zip.open(con[x], True) - else: - p = page - img = con[p] - pages = {p:zip.open(img, True)} - - - else: - imgs = sorted(zip.dir_contents(chap.path)) - for n, img in enumerate(imgs): - pages[n] = zip.open(img, True) - zip.close() - - hashes = {} - if gallery.id != None: - for p in pages: - h = look_exists(p) - if not h: - h = generate_img_hash(pages[p]) - executing.append((h, gallery.id, chap_id, p,)) - hashes[p] = h - else: - for i in pages: - hashes[i] = generate_img_hash(pages[i]) - - if executing: - cls.executemany(cls, 'INSERT INTO hashes(hash, series_id, chapter_id, page) VALUES(?, ?, ?, ?)', - executing) - - - if page == 'mid': - r_hash = {'mid':list(hashes.values())[0]} - else: - r_hash = hashes - - if _name != None: - try: - r_hash[_name] = r_hash[page] - except KeyError: - pass - return r_hash - - @classmethod - def gen_gallery_hashes(cls, gallery): - "Generates hashes for gallery's first chapter and inserts them to DB" - return HashDB.gen_gallery_hash(gallery, 0) - - @staticmethod - def rebuild_gallery_hashes(gallery): - "Inserts hashes into DB only if it doesnt already exist" - assert isinstance(gallery, Gallery) - hashes = HashDB.get_gallery_hashes(gallery.id) - - if not hashes: - hashes = HashDB.gen_gallery_hashes(gallery) - return hashes - - @classmethod - def del_gallery_hashes(cls, gallery_id): - "Deletes all hashes linked to the given gallery id" - cls.execute(cls, 'DELETE FROM hashes WHERE series_id=?', (gallery_id,)) + """ + Contains the following methods: + + find_gallery -> returns galleries which matches the given list of hashes + get_gallery_hashes -> returns all hashes with the given gallery id in a list + get_gallery_hash -> returns hash of chapter specified. If page is specified, returns hash of chapter page + gen_gallery_hashes <- generates hashes for gallery's chapters and inserts them to db + rebuild_gallery_hashes <- inserts hashes into DB only if it doesnt already exist + """ + + @classmethod + def find_gallery(cls, hashes): + assert isinstance(hashes, list) + gallery_ids = {} + hash_status = [] + for hash in hashes: + r = cls.execute(cls, 'SELECT series_id FROM hashes WHERE hash=?', (hash,)) + try: + g_ids = r.fetchall() + for r in g_ids: + g_id = r['series_id'] + if g_id not in gallery_ids: + gallery_ids[g_id] = 1 + else: + gallery_ids[g_id] = gallery_ids[g_id] + 1 + if g_ids: + hash_status.append(True) + else: + hash_status.append(False) + except KeyError: + hash_status.append(False) + except TypeError: + hash_status.append(False) + + if all(hash_status): + # the one with most matching hashes + g_id = None + h_match_count = 0 + for g in gallery_ids: + if gallery_ids[g] > h_match_count: + h_match_count = gallery_ids[h] + g_id = g + if g_id: + weak_gallery = Gallery() + weak_gallery.id = g_id + return weak_gallery + + return None + + @classmethod + def get_gallery_hashes(cls, gallery_id): + "Returns all hashes with the given gallery id in a list" + cursor = cls.execute(cls, 'SELECT hash FROM hashes WHERE series_id=?', + (gallery_id,)) + hashes = [] + try: + for row in cursor.fetchall(): + hashes.append(row['hash']) + except IndexError: + return [] + return hashes + + @classmethod + def get_gallery_hash(cls, gallery_id, chapter, page=None): + """ + returns hash of chapter. If page is specified, returns hash of chapter page + """ + assert isinstance(gallery_id, int) + assert isinstance(chapter, int) + if page: + assert isinstance(page, int) + chap_id = ChapterDB.get_chapter_id(gallery_id, chapter) + if not chap_id: + return None + if page: + exceuting = ["SELECT hash FROM hashes WHERE series_id=? AND chapter_id=? AND page=?", + (gallery_id, chap_id, page)] + else: + exceuting = ["SELECT hash FROM hashes WHERE series_id=? AND chapter_id=?", + (gallery_id, chap_id)] + hashes = [] + c = cls.execute(cls, *exceuting) + for h in c.fetchall(): + try: + hashes.append(h['hash']) + except KeyError: + pass + return hashes + + @classmethod + def gen_gallery_hash(cls, gallery, chapter, page=None, color_img=False, _name=None): + """ + Generate hash for a specific chapter. + Set page to only generate specific page + page: 'mid' or number or list of numbers + color_img: if true then a hash to colored img will be returned if possible + Returns dict with chapter number or 'mid' as key and hash as value + """ + assert isinstance(gallery, Gallery) + assert isinstance(chapter, int) + if page != None: + assert isinstance(page, (int, str, list)) + skip_gen = False + if gallery.id: + chap_id = ChapterDB.get_chapter_id(gallery.id, chapter) + + c = cls.execute(cls, 'SELECT hash, page FROM hashes WHERE series_id=? AND chapter_id=?', + (gallery.id, chap_id,)) + hashes = {} + for r in c.fetchall(): + try: + if r['hash'] and r['page'] != None: + hashes[r['page']] = r['hash'] + except TypeError: + pass + if isinstance(page, (int, list)): + if isinstance(page, int): + _page = [page] + else: + _page = page + h = {} + t = False + for p in _page: + if p in hashes: + h[p] = hashes[p] + else: + t = True + if not t: + skip_gen = True + hashes = h + + elif gallery.chapters[chapter].pages == len(hashes.keys()): + skip_gen = True + if page == "mid": + try: + hashes = {'mid':hashes[len(hashes) // 2]} + except KeyError: + skip_gen = False + + + if not skip_gen or color_img: + + def look_exists(page): + """check if hash already exists in database + returns hash, else returns None""" + c = cls.execute(cls, 'SELECT hash FROM hashes WHERE page=? AND chapter_id=?', + (page, chap_id,)) + try: # exists + return c.fetchone()['hash'] + except TypeError: # doesnt exist + return None + except IndexError: + return None + + if gallery.dead_link: + log_e("Could not generate hash of dead gallery: {}".format(gallery.title.encode(errors='ignore'))) + return {} + + try: + chap = gallery.chapters[chapter] + except KeyError: + utils.make_chapters(gallery) + try: + chap = gallery.chapters[chapter] + except KeyError: + return {} + + executing = [] + try: + if gallery.is_archive: + raise NotADirectoryError + imgs = sorted([x.path for x in scandir.scandir(chap.path) if x.path.endswith(utils.IMG_FILES)]) + pages = {} + for n, i in enumerate(imgs): + pages[n] = i + + if page != None: + pages = {} + if color_img: + # if first img is colored, then return filepath of that + if not utils.image_greyscale(imgs[0]): + return {'color':imgs[0]} + if page == 'mid': + imgs = imgs[len(imgs) // 2] + pages[len(imgs) // 2] = imgs + elif isinstance(page, list): + try: + for p in page: + pages[p] = imgs[p] + except IndexError: + raise app_constants.InternalPagesMismatch + else: + imgs = imgs[page] + pages = {page:imgs} + + hashes = {} + if gallery.id != None: + for p in pages: + h = look_exists(p) + if not h: + with open(pages[p], 'rb') as f: + h = generate_img_hash(f) + executing.append((h, gallery.id, chap_id, p,)) + hashes[p] = h + else: + for i in pages: + with open(pages[i], 'rb') as f: + hashes[i] = generate_img_hash(f) + + except NotADirectoryError: + temp_dir = os.path.join(app_constants.temp_dir, str(uuid.uuid4())) + is_archive = gallery.is_archive + try: + if is_archive: + zip = ArchiveFile(gallery.path) + else: + zip = ArchiveFile(chap.path) + except app_constants.CreateArchiveFail: + log_e('Could not generate hash: CreateZipFail') + return {} + + pages = {} + if page != None: + p = 0 + con = sorted(zip.dir_contents(chap.path)) + if color_img: + # if first img is colored, then return hash of that + f_bytes = io.BytesIO(zip.open(con[0], False)) + if not utils.image_greyscale(f_bytes): + return {'color':zip.extract(con[0])} + f_bytes.close() + if page == 'mid': + p = len(con) // 2 + img = con[p] + pages = {p:zip.open(img, True)} + elif isinstance(page, list): + for x in page: + pages[x] = zip.open(con[x], True) + else: + p = page + img = con[p] + pages = {p:zip.open(img, True)} + + + else: + imgs = sorted(zip.dir_contents(chap.path)) + for n, img in enumerate(imgs): + pages[n] = zip.open(img, True) + zip.close() + + hashes = {} + if gallery.id != None: + for p in pages: + h = look_exists(p) + if not h: + h = generate_img_hash(pages[p]) + executing.append((h, gallery.id, chap_id, p,)) + hashes[p] = h + else: + for i in pages: + hashes[i] = generate_img_hash(pages[i]) + + if executing: + cls.executemany(cls, 'INSERT INTO hashes(hash, series_id, chapter_id, page) VALUES(?, ?, ?, ?)', + executing) + + + if page == 'mid': + r_hash = {'mid':list(hashes.values())[0]} + else: + r_hash = hashes + + if _name != None: + try: + r_hash[_name] = r_hash[page] + except KeyError: + pass + return r_hash + + @classmethod + def gen_gallery_hashes(cls, gallery): + "Generates hashes for gallery's first chapter and inserts them to DB" + return HashDB.gen_gallery_hash(gallery, 0) + + @staticmethod + def rebuild_gallery_hashes(gallery): + "Inserts hashes into DB only if it doesnt already exist" + assert isinstance(gallery, Gallery) + hashes = HashDB.get_gallery_hashes(gallery.id) + + if not hashes: + hashes = HashDB.gen_gallery_hashes(gallery) + return hashes + + @classmethod + def del_gallery_hashes(cls, gallery_id): + "Deletes all hashes linked to the given gallery id" + cls.execute(cls, 'DELETE FROM hashes WHERE series_id=?', (gallery_id,)) class GalleryList: - """ - Provides access to lists.. - methods: - - add_gallery <- adds a gallery of Gallery class to list - - remove_gallery <- removes galleries matching the provided gallery id - - clear <- removes all galleries from the list - - galleries -> returns a list with all galleries in list - - scan <- scans for galleries matching the listfilter and adds them to gallery - """ - # types - REGULAR, COLLECTION = range(2) - - def __init__(self, name, list_of_galleries=[], filter=None, id=None, _db=True): - self._id = id # shouldnt ever be touched - self.name = name - self.profile = '' - self.type = self.REGULAR - self.filter = filter - self.enforce = False - self.regex = False - self.case = False - self.strict = False - self._galleries = set() - self._ids_chache = [] - self._scanning = False - self.add_gallery(list_of_galleries, _db) - - def add_gallery(self, gallery_or_list_of, _db=True, _check_filter=True): - "add_gallery <- adds a gallery of Gallery class to list" - assert isinstance(gallery_or_list_of, (Gallery, list)) - if isinstance(gallery_or_list_of, Gallery): - gallery_or_list_of = [gallery_or_list_of] - if _check_filter and self.filter and self.enforce: - execute(self.scan, True, gallery_or_list_of) - return - new_galleries = [] - for gallery in gallery_or_list_of: - self._galleries.add(gallery) - if not utils.b_search(self._ids_chache, gallery.id): - new_galleries.append(gallery) - self._ids_chache.append(gallery.id) - # uses timsort algorithm so it's ok - self._ids_chache.sort() - if _db: - execute(ListDB.add_gallery_to_list, True, new_galleries, self) - - def remove_gallery(self, gallery_id_or_list_of): - "remove_gallery <- removes galleries matching the provided gallery id" - if isinstance(gallery_id_or_list_of, int): - gallery_id_or_list_of = [gallery_id_or_list_of] - g_ids = gallery_id_or_list_of - g_ids_to_delete = [] - g_to_delete = [] - for g in self._galleries: - if g.id in g_ids: - g_to_delete.append(g) - try: - self._ids_chache.remove(g.id) - except ValueError: - pass - g_ids_to_delete.append(g.id) - for g in g_to_delete: - self._galleries.remove(g) - execute(ListDB.remove_gallery_from_list, True, g_ids_to_delete, self) - - def clear(self): - "removes all galleries from the list" - if self._galleries: - execute(ListDB.remove_gallery_from_list, True, list(self._galleries), self) - self._galleries.clear() - self._ids_chache.clear() - - def galleries(self): - "returns a list with all galleries in list" - return list(self._galleries) - - def __contains__(self, g): - return utils.b_search(self._ids_chache, g.id) - - def add_to_db(self): - app_constants.GALLERY_LISTS.add(self) - execute(ListDB.add_list, True, self) - - def scan(self, galleries=None): - if self.filter and not self._scanning: - self._scanning = True - if isinstance(galleries, Gallery): - galleries = [galleries] - if not galleries: - galleries = app_constants.GALLERY_DATA - new_galleries = [] - filter_term = ' '.join(self.filter.split()) - args = [] - if self.regex: - args.append(app_constants.Search.Regex) - if self.case: - args.append(app_constants.Search.Case) - if self.strict: - args.append(app_constants.Search.Strict) - search_pieces = utils.get_terms(filter_term) - - def _search_g(gallery): - all_terms = {t: False for t in search_pieces} - - for t in search_pieces: - if gallery.contains(t, args): - all_terms[t] = True - - if all(all_terms.values()): - return True - return False - - for gallery in galleries: - if _search_g(gallery): - new_galleries.append(gallery) - - if self.enforce: - g_to_remove = [] - for g in self.galleries(): - if not _search_g(g): - g_to_remove.append(g.id) - if g_to_remove: - self.remove_gallery(g_to_remove) - self.add_gallery(new_galleries, _check_filter=False) - self._scanning = False - - def __lt__(self, other): - return self.name < other.name + """ + Provides access to lists.. + methods: + - add_gallery <- adds a gallery of Gallery class to list + - remove_gallery <- removes galleries matching the provided gallery id + - clear <- removes all galleries from the list + - galleries -> returns a list with all galleries in list + - scan <- scans for galleries matching the listfilter and adds them to gallery + """ + # types + REGULAR, COLLECTION = range(2) + + def __init__(self, name, list_of_galleries=[], filter=None, id=None, _db=True): + self._id = id # shouldnt ever be touched + self.name = name + self.profile = '' + self.type = self.REGULAR + self.filter = filter + self.enforce = False + self.regex = False + self.case = False + self.strict = False + self._galleries = set() + self._ids_chache = [] + self._scanning = False + self.add_gallery(list_of_galleries, _db) + + def add_gallery(self, gallery_or_list_of, _db=True, _check_filter=True): + "add_gallery <- adds a gallery of Gallery class to list" + assert isinstance(gallery_or_list_of, (Gallery, list)) + if isinstance(gallery_or_list_of, Gallery): + gallery_or_list_of = [gallery_or_list_of] + if _check_filter and self.filter and self.enforce: + execute(self.scan, True, gallery_or_list_of) + return + new_galleries = [] + for gallery in gallery_or_list_of: + self._galleries.add(gallery) + if not utils.b_search(self._ids_chache, gallery.id): + new_galleries.append(gallery) + self._ids_chache.append(gallery.id) + # uses timsort algorithm so it's ok + self._ids_chache.sort() + if _db: + execute(ListDB.add_gallery_to_list, True, new_galleries, self) + + def remove_gallery(self, gallery_id_or_list_of): + "remove_gallery <- removes galleries matching the provided gallery id" + if isinstance(gallery_id_or_list_of, int): + gallery_id_or_list_of = [gallery_id_or_list_of] + g_ids = gallery_id_or_list_of + g_ids_to_delete = [] + g_to_delete = [] + for g in self._galleries: + if g.id in g_ids: + g_to_delete.append(g) + try: + self._ids_chache.remove(g.id) + except ValueError: + pass + g_ids_to_delete.append(g.id) + for g in g_to_delete: + self._galleries.remove(g) + execute(ListDB.remove_gallery_from_list, True, g_ids_to_delete, self) + + def clear(self): + "removes all galleries from the list" + if self._galleries: + execute(ListDB.remove_gallery_from_list, True, list(self._galleries), self) + self._galleries.clear() + self._ids_chache.clear() + + def galleries(self): + "returns a list with all galleries in list" + return list(self._galleries) + + def __contains__(self, g): + return utils.b_search(self._ids_chache, g.id) + + def add_to_db(self): + app_constants.GALLERY_LISTS.add(self) + execute(ListDB.add_list, True, self) + + def scan(self, galleries=None): + if self.filter and not self._scanning: + self._scanning = True + if isinstance(galleries, Gallery): + galleries = [galleries] + if not galleries: + galleries = app_constants.GALLERY_DATA + new_galleries = [] + filter_term = ' '.join(self.filter.split()) + args = [] + if self.regex: + args.append(app_constants.Search.Regex) + if self.case: + args.append(app_constants.Search.Case) + if self.strict: + args.append(app_constants.Search.Strict) + search_pieces = utils.get_terms(filter_term) + + def _search_g(gallery): + all_terms = {t: False for t in search_pieces} + + for t in search_pieces: + if gallery.contains(t, args): + all_terms[t] = True + + if all(all_terms.values()): + return True + return False + + for gallery in galleries: + if _search_g(gallery): + new_galleries.append(gallery) + + if self.enforce: + g_to_remove = [] + for g in self.galleries(): + if not _search_g(g): + g_to_remove.append(g.id) + if g_to_remove: + self.remove_gallery(g_to_remove) + self.add_gallery(new_galleries, _check_filter=False) + self._scanning = False + + def __lt__(self, other): + return self.name < other.name class Gallery: - """ - Base class for a gallery. - Available data: - id -> Not to be editied. Do not touch. - title <- [list of titles] or str - profile <- path to thumbnail - path <- path to gallery - artist <- str - chapters <- {:} - chapter_size <- int of number of chapters - info <- str - fav <- int (1 for true 0 for false) - rating <- float - type <- str (Manga? Doujin? Other?) - language <- str - status <- "unknown", "completed" or "ongoing" - tags <- list of str - pub_date <- date - date_added <- date, will be defaulted to today if not specified - last_read <- timestamp (e.g. time.time()) - times_read <- an integer telling us how many times the gallery has been opened - hashes <- a list of hashes of the gallery's chapters - exed <- indicator on if gallery metadata has been fetched - valid <- a bool indicating the validity of the gallery - - Takes ownership of ChaptersContainer - """ - - def __init__(self): - - self.id = None # Will be defaulted. - self.parent = None - self.title = "" - self.profile = "" - self._path = "" - self.path_in_archive = "" - self.is_archive = 0 - self.artist = "" - self._chapters = ChaptersContainer(self) - self.info = "" - self.fav = 0 - self.rating = 5 - self.type = "" - self.link = "" - self.language = "" - self.status = "" - self.tags = {} - self.pub_date = None - self.date_added = datetime.datetime.now().replace(microsecond=0) - self.last_read = None - self.times_read = 0 - self.valid = False - self._db_v = None - self.hashes = [] - self.exed = 0 - self.file_type = "folder" - self.view = app_constants.ViewType.Default # default view - - self._grid_visible = False - self._list_view_selected = False - self._profile_qimage = {} - self._profile_load_status = {} - self.dead_link = False - self.state = app_constants.GalleryState.Default - self.qtime = QTime() # used by views to record addition - - @property - def path(self): - return self._path - - @path.setter - def path(self, n_p): - self._path = n_p - _, ext = os.path.splitext(n_p) - if ext: - self.file_type = ext[1:].lower() # remove dot - - def set_defaults(self): - if not self.type: - self.type = app_constants.G_DEF_TYPE.capitalize() - if not self.language: - self.language = app_constants.G_DEF_LANGUAGE.capitalize() - if not self.status: - self.status = app_constants.G_DEF_STATUS.capitalize() - - def reset_profile(self): - self._profile_load_status.clear() - self._profile_qimage.clear() - - def _profile_loaded(self, img, ptype=None, method=None): - self._profile_load_status[ptype] = img - if method and img: - method(self, img) - - def get_profile(self, ptype, on_method=None): - psize = app_constants.THUMB_DEFAULT - if ptype == app_constants.ProfileType.Small: - psize = app_constants.THUMB_SMALL - - if ptype in self._profile_qimage: - f = self._profile_qimage[ptype] - if not f.done(): - return - if f.result(): - return f.result() - img = self._profile_load_status.get(ptype) - if not img: - self._profile_qimage[ptype] = Executors.load_thumbnail( - self.profile, psize, - on_method=self._profile_loaded, - ptype=ptype, method=on_method) - - return img - - def set_profile(self, future): - "set with profile with future object" - self.profile = future.result() - if self.id != None: - execute(GalleryDB.modify_gallery, True, self.id, profile=self.profile, priority=0) - - @property - def chapters(self): - return self._chapters - - @chapters.setter - def chapters(self, chp_cont): - assert isinstance(chp_cont, ChaptersContainer) - chp_cont.set_parent(self) - self._chapters = chp_cont - - def merge(galleries): - "Merge galleries into this galleries, adding them as chapters" - pass - - def gen_hashes(self): - "Generate hashes while inserting them into DB" - if not self.hashes: - hash = HashDB.gen_gallery_hashes(self) - if hash: - self.hashes = hash - return True - else: - return False - else: - return True - - def validate(self): - "Validates gallery, returns status" - # TODO: Extend this - validity = [] - status = False - - #if not self.hashes: - # HashDB.gen_gallery_hashes(self) - # self.hashes = HashDB.get_gallery_hashes(self.id) - - if all(validity): - status = True - self.valid = True - return status - - def invalidities(self): - """ - Checks all attributes for invalidities. - Returns list of string with invalid attribute names - """ - return [] - - def _keyword_search(self, ns, tag, args=[]): - term = '' - lt, gt = range(2) - def _search(term): - if app_constants.Search.Regex in args: - if utils.regex_search(tag, term, args): - return True - else: - if app_constants.DEBUG: - print(tag, term) - if utils.search_term(tag, term, args): - return True - return False - - def _operator_parse(tag): - o = None - if tag: - if tag[0] == '<': - o = lt - tag = tag[1:] - elif tag[0] == '>': - o = gt - tag = tag[1:] - return tag, o - - def _operator_supported(attr, date=False): - try: - o_tag, o = _operator_parse(tag) - if date: - o_tag = dateparser.parse(o_tag, dayfirst=True) - if o_tag: - o_tag = o_tag.date() - else: - o_tag = int(o_tag) - if o != None: - if o == gt: - return o_tag < attr - elif o == lt: - return o_tag > attr - else: - return o_tag == attr - except ValueError: - return False - - if ns == 'Title': - term = self.title - elif ns in ['Language', 'Lang']: - term = self.language - elif ns == 'Type': - term = self.type - elif ns == 'Status': - term = self.status - elif ns == 'Artist': - term = self.artist - elif ns in ['Descr', 'Description']: - term = self.info - elif ns in ['Chapter', 'Chapters']: - return _operator_supported(self.chapters.count()) - elif ns in ['Read_count', 'Read count', 'Times_read', 'Times read']: - return _operator_supported(self.times_read) - elif ns in ['Date_added', 'Date added']: - return _operator_supported(self.date_added.date(), True) - elif ns in ['Pub_date', 'Publication', 'Pub date']: - if self.pub_date: - return _operator_supported(self.pub_date.date(), True) - return False - elif ns in ['Last_read', 'Last read']: - if self.last_read: - return _operator_supported(self.last_read.date(), True) - return False - return _search(term) - - def __contains__(self, key): - assert isinstance(key, Chapter), "Can only check for chapters in gallery" - return self.chapters.__contains__(key) - - - def contains(self, key, args=[]): - "Check if gallery contains keyword" - is_exclude = False if key[0] == '-' else True - key = key[1:] if not is_exclude else key - default = False if is_exclude else True - if key: - # check in title/artist/language - found = False - if not ':' in key: - for g_attr in [self.title, self.artist, self.language]: - if not g_attr: - continue - if app_constants.Search.Regex in args: - if utils.regex_search(key, g_attr, args=args): - found = True - break - else: - if utils.search_term(key, g_attr, args=args): - found = True - break - - # check in tag - if not found: - tags = key.split(':') - ns = tag = '' - # only namespace is lowered and capitalized for now - if len(tags) > 1: - ns = tags[0].lower().capitalize() - tag = tags[1] - else: - tag = tags[0] - - # very special keywords - if ns: - key_word = ['none', 'null'] - if ns == 'Tag' and tag in key_word: - if not self.tags: - return is_exclude - elif ns == 'Artist' and tag in key_word: - if not self.artist: - return is_exclude - elif ns == 'Status' and tag in key_word: - if not self.status or self.status == 'Unknown': - return is_exclude - elif ns == 'Language' and tag in key_word: - if not self.language: - return is_exclude - elif ns == 'Url' and tag in key_word: - if not self.link: - return is_exclude - elif ns in ('Descr', 'Description') and tag in key_word: - if not self.info or self.info == 'No description..': - return is_exclude - elif ns == 'Type' and tag in key_word: - if not self.type: - return is_exclude - elif ns in ('Publication', 'Pub_date', 'Pub date') and tag in key_word: - if not self.pub_date: - return is_exclude - - if app_constants.Search.Regex in args: - if ns: - if self._keyword_search(ns, tag, args=args): - return is_exclude - - for x in self.tags: - if utils.regex_search(ns, x): - for t in self.tags[x]: - if utils.regex_search(tag, t, True, args=args): - return is_exclude - else: - for x in self.tags: - for t in self.tags[x]: - if utils.regex_search(tag, t, True, args=args): - return is_exclude - else: - if ns: - if self._keyword_search(ns, tag, args=args): - return is_exclude - - if ns in self.tags: - for t in self.tags[ns]: - if utils.search_term(tag, t, True, args=args): - return is_exclude - else: - for x in self.tags: - for t in self.tags[x]: - if utils.search_term(tag, t, True, args=args): - return is_exclude - else: - return is_exclude - return default - - def move_gallery(self, new_path=''): - log_i("Moving gallery...") - log_d("Old gallery path: {}".format(self.path)) - old_head, old_tail = os.path.split(self.path) - self.path = utils.move_files(self.path, new_path) - new_head, new_tail = os.path.split(self.path) - for chap in self.chapters: - if not chap.in_archive: - head, tail = os.path.split(chap.path) - log_d("old chapter path: {}".format(chap.path)) - if os.path.exists(os.path.join(self.path, tail)): - chap.path = os.path.join(self.path, tail) - continue - if os.path.join(old_head, old_tail) == os.path.join(head, tail): - chap.path = self.path - continue - - if self.is_archive: - utils.move_files(chap.path, os.path.join(new_head, tail)) - else: - utils.move_files(chap.path, os.path.join(self.path, tail)) - - def __lt__(self, other): - return self.id < other.id - - def __str__(self): - string = """ - ID: {} - Title: {} - Profile Path: {} - Path: {} - Path In Archive: {} - Is Archive: {} - Author: {} - Description: {} - Favorite: {} - Type: {} - Language: {} - Status: {} - Tags: {} - Publication Date: {} - Date Added: {} - Last Read: {} - Times Read: {} - Exed: {} - Hashes: {} - - Chapters: {} - """.format(self.id, self.title, self.profile, self.path.encode(errors='ignore'), self.path_in_archive.encode(errors='ignore'), - self.is_archive, self.artist, self.info, self.fav, self.type, self.language, self.status, self.tags, - self.pub_date, self.date_added, self.last_read, self.times_read, self.exed, len(self.hashes), self.chapters) - return string + """ + Base class for a gallery. + Available data: + id -> Not to be editied. Do not touch. + title <- [list of titles] or str + profile <- path to thumbnail + path <- path to gallery + artist <- str + chapters <- {:} + chapter_size <- int of number of chapters + info <- str + fav <- int (1 for true 0 for false) + rating <- float + type <- str (Manga? Doujin? Other?) + language <- str + status <- "unknown", "completed" or "ongoing" + tags <- list of str + pub_date <- date + date_added <- date, will be defaulted to today if not specified + last_read <- timestamp (e.g. time.time()) + times_read <- an integer telling us how many times the gallery has been opened + hashes <- a list of hashes of the gallery's chapters + exed <- indicator on if gallery metadata has been fetched + valid <- a bool indicating the validity of the gallery + + Takes ownership of ChaptersContainer + """ + + def __init__(self): + + self.id = None # Will be defaulted. + self.parent = None + self.title = "" + self.profile = "" + self._path = "" + self.path_in_archive = "" + self.is_archive = 0 + self.artist = "" + self._chapters = ChaptersContainer(self) + self.info = "" + self.fav = 0 + self.rating = 5 + self.type = "" + self.link = "" + self.language = "" + self.status = "" + self.tags = {} + self.pub_date = None + self.date_added = datetime.datetime.now().replace(microsecond=0) + self.last_read = None + self.times_read = 0 + self.valid = False + self._db_v = None + self.hashes = [] + self.exed = 0 + self.file_type = "folder" + self.view = app_constants.ViewType.Default # default view + + self._grid_visible = False + self._list_view_selected = False + self._profile_qimage = {} + self._profile_load_status = {} + self.dead_link = False + self.state = app_constants.GalleryState.Default + self.qtime = QTime() # used by views to record addition + + @property + def path(self): + return self._path + + @path.setter + def path(self, n_p): + self._path = n_p + _, ext = os.path.splitext(n_p) + if ext: + self.file_type = ext[1:].lower() # remove dot + + def set_defaults(self): + if not self.type: + self.type = app_constants.G_DEF_TYPE.capitalize() + if not self.language: + self.language = app_constants.G_DEF_LANGUAGE.capitalize() + if not self.status: + self.status = app_constants.G_DEF_STATUS.capitalize() + + def reset_profile(self): + self._profile_load_status.clear() + self._profile_qimage.clear() + + def _profile_loaded(self, img, ptype=None, method=None): + self._profile_load_status[ptype] = img + if method and img: + method(self, img) + + def get_profile(self, ptype, on_method=None): + psize = app_constants.THUMB_DEFAULT + if ptype == app_constants.ProfileType.Small: + psize = app_constants.THUMB_SMALL + + if ptype in self._profile_qimage: + f = self._profile_qimage[ptype] + if not f.done(): + return + if f.result(): + return f.result() + img = self._profile_load_status.get(ptype) + if not img: + self._profile_qimage[ptype] = Executors.load_thumbnail(self.profile, psize, + on_method=self._profile_loaded, + ptype=ptype, method=on_method) + + return img + + def set_profile(self, future): + "set with profile with future object" + self.profile = future.result() + if self.id != None: + execute(GalleryDB.modify_gallery, True, self.id, profile=self.profile, priority=0) + + @property + def chapters(self): + return self._chapters + + @chapters.setter + def chapters(self, chp_cont): + assert isinstance(chp_cont, ChaptersContainer) + chp_cont.set_parent(self) + self._chapters = chp_cont + + def merge(galleries): + "Merge galleries into this galleries, adding them as chapters" + pass + + def gen_hashes(self): + "Generate hashes while inserting them into DB" + if not self.hashes: + hash = HashDB.gen_gallery_hashes(self) + if hash: + self.hashes = hash + return True + else: + return False + else: + return True + + def validate(self): + "Validates gallery, returns status" + # TODO: Extend this + validity = [] + status = False + + #if not self.hashes: + # HashDB.gen_gallery_hashes(self) + # self.hashes = HashDB.get_gallery_hashes(self.id) + + if all(validity): + status = True + self.valid = True + return status + + def invalidities(self): + """ + Checks all attributes for invalidities. + Returns list of string with invalid attribute names + """ + return [] + + def _keyword_search(self, ns, tag, args=[]): + term = '' + lt, gt = range(2) + def _search(term): + if app_constants.Search.Regex in args: + if utils.regex_search(tag, term, args): + return True + else: + if app_constants.DEBUG: + print(tag, term) + if utils.search_term(tag, term, args): + return True + return False + + def _operator_parse(tag): + o = None + if tag: + if tag[0] == '<': + o = lt + tag = tag[1:] + elif tag[0] == '>': + o = gt + tag = tag[1:] + return tag, o + + def _operator_supported(attr, date=False): + try: + o_tag, o = _operator_parse(tag) + if date: + o_tag = dateparser.parse(o_tag, dayfirst=True) + if o_tag: + o_tag = o_tag.date() + else: + o_tag = int(o_tag) + if o != None: + if o == gt: + return o_tag < attr + elif o == lt: + return o_tag > attr + else: + return o_tag == attr + except ValueError: + return False + + if ns == 'Title': + term = self.title + elif ns in ['Language', 'Lang']: + term = self.language + elif ns == 'Type': + term = self.type + elif ns == 'Status': + term = self.status + elif ns == 'Artist': + term = self.artist + elif ns in ['Descr', 'Description']: + term = self.info + elif ns in ['Chapter', 'Chapters']: + return _operator_supported(self.chapters.count()) + elif ns in ['Read_count', 'Read count', 'Times_read', 'Times read']: + return _operator_supported(self.times_read) + elif ns in ['Date_added', 'Date added']: + return _operator_supported(self.date_added.date(), True) + elif ns in ['Pub_date', 'Publication', 'Pub date']: + if self.pub_date: + return _operator_supported(self.pub_date.date(), True) + return False + elif ns in ['Last_read', 'Last read']: + if self.last_read: + return _operator_supported(self.last_read.date(), True) + return False + return _search(term) + + def __contains__(self, key): + assert isinstance(key, Chapter), "Can only check for chapters in gallery" + return self.chapters.__contains__(key) + + + def contains(self, key, args=[]): + "Check if gallery contains keyword" + is_exclude = False if key[0] == '-' else True + key = key[1:] if not is_exclude else key + default = False if is_exclude else True + if key: + # check in title/artist/language + found = False + if not ':' in key: + for g_attr in [self.title, self.artist, self.language]: + if not g_attr: + continue + if app_constants.Search.Regex in args: + if utils.regex_search(key, g_attr, args=args): + found = True + break + else: + if utils.search_term(key, g_attr, args=args): + found = True + break + + # check in tag + if not found: + tags = key.split(':') + ns = tag = '' + # only namespace is lowered and capitalized for now + if len(tags) > 1: + ns = tags[0].lower().capitalize() + tag = tags[1] + else: + tag = tags[0] + + # very special keywords + if ns: + key_word = ['none', 'null'] + if ns == 'Tag' and tag in key_word: + if not self.tags: + return is_exclude + elif ns == 'Artist' and tag in key_word: + if not self.artist: + return is_exclude + elif ns == 'Status' and tag in key_word: + if not self.status or self.status == 'Unknown': + return is_exclude + elif ns == 'Language' and tag in key_word: + if not self.language: + return is_exclude + elif ns == 'Url' and tag in key_word: + if not self.link: + return is_exclude + elif ns in ('Descr', 'Description') and tag in key_word: + if not self.info or self.info == 'No description..': + return is_exclude + elif ns == 'Type' and tag in key_word: + if not self.type: + return is_exclude + elif ns in ('Publication', 'Pub_date', 'Pub date') and tag in key_word: + if not self.pub_date: + return is_exclude + elif ns == 'Path' and tag in key_word: + if self.dead_link: + return is_exclude + + if app_constants.Search.Regex in args: + if ns: + if self._keyword_search(ns, tag, args = args): + return is_exclude + + for x in self.tags: + if utils.regex_search(ns, x): + for t in self.tags[x]: + if utils.regex_search(tag, t, True, args=args): + return is_exclude + else: + for x in self.tags: + for t in self.tags[x]: + if utils.regex_search(tag, t, True, args=args): + return is_exclude + else: + if ns: + if self._keyword_search(ns, tag, args=args): + return is_exclude + + if ns in self.tags: + for t in self.tags[ns]: + if utils.search_term(tag, t, True, args=args): + return is_exclude + else: + for x in self.tags: + for t in self.tags[x]: + if utils.search_term(tag, t, True, args=args): + return is_exclude + else: + return is_exclude + return default + + def move_gallery(self, new_path=''): + log_i("Moving gallery...") + log_d("Old gallery path: {}".format(self.path)) + old_head, old_tail = os.path.split(self.path) + self.path = utils.move_files(self.path, new_path) + new_head, new_tail = os.path.split(self.path) + for chap in self.chapters: + if not chap.in_archive: + head, tail = os.path.split(chap.path) + log_d("old chapter path: {}".format(chap.path)) + if os.path.exists(os.path.join(self.path, tail)): + chap.path = os.path.join(self.path, tail) + continue + if os.path.join(old_head, old_tail) == os.path.join(head, tail): + chap.path = self.path + continue + + if self.is_archive: + utils.move_files(chap.path, os.path.join(new_head, tail)) + else: + utils.move_files(chap.path, os.path.join(self.path, tail)) + + def __lt__(self, other): + return self.id < other.id + + def __str__(self): + string = """ + ID: {} + Title: {} + Profile Path: {} + Path: {} + Path In Archive: {} + Is Archive: {} + Author: {} + Description: {} + Favorite: {} + Type: {} + Language: {} + Status: {} + Tags: {} + Publication Date: {} + Date Added: {} + Last Read: {} + Times Read: {} + Exed: {} + Hashes: {} + + Chapters: {} + """.format(self.id, self.title, self.profile, self.path.encode(errors='ignore'), self.path_in_archive.encode(errors='ignore'), + self.is_archive, self.artist, self.info, self.fav, self.type, self.language, self.status, self.tags, + self.pub_date, self.date_added, self.last_read, self.times_read, self.exed, len(self.hashes), self.chapters) + return string class Chapter: - """ - Base class for a chapter - Contains following attributes: - parent -> The ChapterContainer it belongs in - gallery -> The Gallery it belongs to - title -> title of chapter - path -> path to chapter - number -> chapter number - pages -> chapter pages - in_archive -> 1 if the chapter path is in an archive else 0 - """ - def __init__(self, parent, gallery, number=0, path='', pages=0, in_archive=0, title=''): - self.parent = parent - self.gallery = gallery - self.title = title - self.path = path - self.number = number - self.pages = pages - self.in_archive = in_archive - - def __lt__(self, other): - return self.number < other.number - - def __str__(self): - s = """ - Chapter: {} - Title: {} - Path: {} - Pages: {} - in_archive: {} - """.format(self.number, self.title, self.path, self.pages, self.in_archive) - return s - - @property - def next_chapter(self): - try: - return self.parent[self.number + 1] - except KeyError: - return None - - @property - def previous_chapter(self): - try: - return self.parent[self.number - 1] - except KeyError: - return None - - def open(self, stat_msg=True): - if stat_msg: - txt = "Opening chapter {} of {}".format(self.number + 1, self.gallery.title) - app_constants.STAT_MSG_METHOD(txt) - app_constants.NOTIF_BAR.add_text(txt) - if self.in_archive: - if self.gallery.is_archive: - execute(utils.open_chapter, True, self.path, self.gallery.path) - else: - execute(utils.open_chapter, True, '', self.path) - else: - execute(utils.open_chapter, True, self.path) - self.gallery.times_read += 1 - self.gallery.last_read = datetime.datetime.now().replace(microsecond=0) - execute(GalleryDB.modify_gallery, True, self.gallery.id, times_read=self.gallery.times_read, - last_read=self.gallery.last_read) + """ + Base class for a chapter + Contains following attributes: + parent -> The ChapterContainer it belongs in + gallery -> The Gallery it belongs to + title -> title of chapter + path -> path to chapter + number -> chapter number + pages -> chapter pages + in_archive -> 1 if the chapter path is in an archive else 0 + """ + def __init__(self, parent, gallery, number=0, path='', pages=0, in_archive=0, title=''): + self.parent = parent + self.gallery = gallery + self.title = title + self.path = path + self.number = number + self.pages = pages + self.in_archive = in_archive + + def __lt__(self, other): + return self.number < other.number + + def __str__(self): + s = """ + Chapter: {} + Title: {} + Path: {} + Pages: {} + in_archive: {} + """.format(self.number, self.title, self.path, self.pages, self.in_archive) + return s + + @property + def next_chapter(self): + try: + return self.parent[self.number + 1] + except KeyError: + return None + + @property + def previous_chapter(self): + try: + return self.parent[self.number - 1] + except KeyError: + return None + + def open(self, stat_msg=True): + if stat_msg: + txt = "Opening chapter {} of {}".format(self.number + 1, self.gallery.title) + app_constants.STAT_MSG_METHOD(txt) + app_constants.NOTIF_BAR.add_text(txt) + if self.in_archive: + if self.gallery.is_archive: + execute(utils.open_chapter, True, self.path, self.gallery.path) + else: + execute(utils.open_chapter, True, '', self.path) + else: + execute(utils.open_chapter, True, self.path) + self.gallery.times_read += 1 + self.gallery.last_read = datetime.datetime.now().replace(microsecond=0) + execute(GalleryDB.modify_gallery, True, self.gallery.id, times_read=self.gallery.times_read, + last_read=self.gallery.last_read) class ChaptersContainer: - """ - A container for chapters. - Acts like a list/dict of chapters. - - Iterable returns a ordered list of chapters - Sets to gallery.chapters - """ - def __init__(self, gallery=None): - self.parent = None - self._data = {} - - if gallery: - gallery.chapters = self - - def set_parent(self, gallery): - assert isinstance(gallery, (Gallery, None)) - self.parent = gallery - for n in self._data: - chap = self._data[n] - chap.gallery = gallery - - def add_chapter(self, chp, overwrite=True, db=False): - "Add a chapter of Chapter class to this container" - assert isinstance(chp, Chapter), "Chapter must be an instantiated Chapter class" - - if not overwrite: - try: - _ = self._data[chp.number] - raise app_constants.ChapterExists - except KeyError: - pass - chp.gallery = self.parent - chp.parent = self - self[chp.number] = chp - - - if db: - # TODO: implement this - pass - - def create_chapter(self, number=None): - """ - Creates Chapter class with the next chapter number or passed number arg and adds to container - The chapter will be returned - """ - if number: - chp = Chapter(self, self.parent, number=number) - self[number] = chp - else: - next_number = 0 - for n in list(self._data.keys()): - if n > next_number: - next_number = n - else: - next_number += 1 - chp = Chapter(self, self.parent, number=next_number) - self[next_number] = chp - return chp - - def update_chapter_pages(self, number): - "Returns status on success" - if self.parent.dead_link: - return False - chap = self[number] - if chap.in_archive: - _archive = utils.ArchiveFile(chap.gallery.path) - chap.pages = len([x for x in _archive.dir_contents(chap.path) if x.endswith(IMG_FILES)]) - _archive.close() - else: - chap.pages = len([x for x in scandir.scandir(chap.path) if x.path.endswith(IMG_FILES)]) - - execute(ChapterDB.update_chapter, True, self, [chap.number]) - return True - - def pages(self): - p = 0 - for c in self: - p += c.pages - return p - - def get_chapter(self, number): - return self[number] - - def get_all_chapters(self): - return list(self._data.values()) - - def count(self): - return len(self) - - def pop(self, key, default=None): - return self._data.pop(key, default) - - def __len__(self): - return len(self._data) - - def __getitem__(self, key): - return self._data[key] - - def __setitem__(self, key, value): - assert isinstance(key, int), "Key must be a chapter number" - assert isinstance(value, Chapter), "Value must be an instantiated Chapter class" - - if value.gallery != self.parent: - raise app_constants.ChapterWrongParentGallery - self._data[key] = value - - def __delitem__(self, key): - del self._data[key] - - def __iter__(self): - return iter([self[c] for c in sorted(self._data.keys())]) - - def __bool__(self): - return bool(self._data) - - def __str__(self): - s = "" - for c in self: - s += '\n' + '{}'.format(c) - if not s: - return '{}' - return s - - def __contains__(self, key): - if key.gallery == self.parent and key in [self.data[c] for c in self._data]: - return True - return False + """ + A container for chapters. + Acts like a list/dict of chapters. + + Iterable returns a ordered list of chapters + Sets to gallery.chapters + """ + def __init__(self, gallery=None): + self.parent = None + self._data = {} + + if gallery: + gallery.chapters = self + + def set_parent(self, gallery): + assert isinstance(gallery, (Gallery, None)) + self.parent = gallery + for n in self._data: + chap = self._data[n] + chap.gallery = gallery + + def add_chapter(self, chp, overwrite=True, db=False): + "Add a chapter of Chapter class to this container" + assert isinstance(chp, Chapter), "Chapter must be an instantiated Chapter class" + + if not overwrite: + try: + _ = self._data[chp.number] + raise app_constants.ChapterExists + except KeyError: + pass + chp.gallery = self.parent + chp.parent = self + self[chp.number] = chp + + + if db: + # TODO: implement this + pass + + def create_chapter(self, number=None): + """ + Creates Chapter class with the next chapter number or passed number arg and adds to container + The chapter will be returned + """ + if number: + chp = Chapter(self, self.parent, number=number) + self[number] = chp + else: + next_number = 0 + for n in list(self._data.keys()): + if n > next_number: + next_number = n + else: + next_number += 1 + chp = Chapter(self, self.parent, number=next_number) + self[next_number] = chp + return chp + + def update_chapter_pages(self, number): + "Returns status on success" + if self.parent.dead_link: + return False + chap = self[number] + if chap.in_archive: + _archive = utils.ArchiveFile(chap.gallery.path) + chap.pages = len([x for x in _archive.dir_contents(chap.path) if x.endswith(IMG_FILES)]) + _archive.close() + else: + chap.pages = len([x for x in scandir.scandir(chap.path) if x.path.endswith(IMG_FILES)]) + + execute(ChapterDB.update_chapter, True, self, [chap.number]) + return True + + def pages(self): + p = 0 + for c in self: + p += c.pages + return p + + def get_chapter(self, number): + return self[number] + + def get_all_chapters(self): + return list(self._data.values()) + + def count(self): + return len(self) + + def pop(self, key, default=None): + return self._data.pop(key, default) + + def __len__(self): + return len(self._data) + + def __getitem__(self, key): + return self._data[key] + + def __setitem__(self, key, value): + assert isinstance(key, int), "Key must be a chapter number" + assert isinstance(value, Chapter), "Value must be an instantiated Chapter class" + + if value.gallery != self.parent: + raise app_constants.ChapterWrongParentGallery + self._data[key] = value + + def __delitem__(self, key): + del self._data[key] + + def __iter__(self): + return iter([self[c] for c in sorted(self._data.keys())]) + + def __bool__(self): + return bool(self._data) + + def __str__(self): + s = "" + for c in self: + s += '\n' + '{}'.format(c) + if not s: + return '{}' + return s + + def __contains__(self, key): + if key.gallery == self.parent and key in [self.data[c] for c in self._data]: + return True + return False class AdminDB(QObject): - DONE = pyqtSignal(bool) - PROGRESS = pyqtSignal(int) - DATA_COUNT = pyqtSignal(int) - def __init__(self, parent=None): - super().__init__(parent) - - def from_v021_to_v022(self, old_db_path=db_constants.DB_PATH): - log_i("Started rebuilding database") - if DBBase._DB_CONN: - DBBase._DB_CONN.close() - DBBase._DB_CONN = db.init_db(old_db_path) - db_galleries = execute(GalleryDB.get_all_gallery, False, False, True, True) - galleries = [] - for g in db_galleries: - if not os.path.exists(g.path): - log_i("Gallery doesn't exist anymore: {}".format(g.title.encode(errors="ignore"))) - else: - galleries.append(g) - - n_galleries = [] - # get all chapters - log_i("Getting chapters...") - chap_rows = DBBase().execute("SELECT * FROM chapters").fetchall() - data_count = len(chap_rows) * 2 - self.DATA_COUNT.emit(data_count) - for n, chap_row in enumerate(chap_rows, -1): - log_d('Next chapter row') - for gallery in galleries: - if gallery.id == chap_row['series_id']: - log_d('Found gallery for chapter row') - chaps = ChaptersContainer(gallery) - chap = chaps.create_chapter(chap_row['chapter_number']) - c_path = bytes.decode(chap_row['chapter_path']) - if c_path: - try: - t = utils.title_parser(os.path.split(c_path)[1])['title'] - except IndexError: - t = c_path - else: - t = '' - chap.title = t - chap.path = c_path - chap.in_archive = chap_row['in_archive'] - if gallery.is_archive: - zip = utils.ArchiveFile(gallery.path) - chap.pages = len(zip.dir_contents(chap.path)) - zip.close() - else: - chap.pages = len(list(scandir.scandir(gallery.path))) - n_galleries.append(gallery) - galleries.remove(gallery) - break - self.PROGRESS.emit(n) - log_d("G: {} C:{}".format(len(n_galleries), data_count - 1)) - log_i("Database magic...") - if os.path.exists(db_constants.THUMBNAIL_PATH): - for root, dirs, files in scandir.walk(db_constants.THUMBNAIL_PATH, topdown=False): - for name in files: - os.remove(os.path.join(root, name)) - for name in dirs: - os.rmdir(os.path.join(root, name)) - - head = os.path.split(old_db_path)[0] - DBBase._DB_CONN.close() - t_db_path = os.path.join(head, 'temp.db') - conn = db.init_db(t_db_path) - DBBase._DB_CONN = conn - for n, g in enumerate(n_galleries, len(chap_rows) - 1): - log_d('Adding new gallery') - GalleryDB.add_gallery(g) - self.PROGRESS.emit(n) - - conn.commit() - conn.close() - - log_i("Cleaning up...") - if os.path.exists(old_db_path): - utils.backup_database(old_db_path) - os.remove(old_db_path) - if os.path.exists(db_constants.DB_PATH): - os.remove(db_constants.DB_PATH) - - os.rename(t_db_path, db_constants.DB_PATH) - self.PROGRESS.emit(data_count) - log_i("Finished rebuilding database") - self.DONE.emit(True) - return True - - def rebuild_database(self): - "Rebuilds database" - log_i("Initiating datbase rebuild") - utils.backup_database() - log_i("Getting galleries...") - galleries = GalleryDB.get_all_gallery() - self.DATA_COUNT.emit(len(galleries)) - db.DBBase._DB_CONN.close() - log_i("Removing old database...") - log_i("Initiating new database...") - temp_db = os.path.join(db_constants.DB_ROOT, "happypanda_temp.db") - if os.path.exists(temp_db): - os.remove(temp_db) - db.DBBase._DB_CONN = db.init_db(temp_db) - DBBase.begin() - log_i("Adding galleries...") - GalleryDB.clear_thumb_dir() - for n, g in enumerate(galleries): - if not os.path.exists(g.path): - log_i("Gallery doesn't exist anymore: {}".format(g.title.encode(errors="ignore"))) - else: - GalleryDB.add_gallery(g) - self.PROGRESS.emit(n) - DBBase.end() - DBBase._DB_CONN.close() - os.remove(db_constants.DB_PATH) - os.rename(temp_db, db_constants.DB_PATH) - db.DBBase._DB_CONN = db.init_db(db_constants.DB_PATH) - self.PROGRESS.emit(len(galleries)) - log_i("Succesfully rebuilt database") - self.DONE.emit(True) - return True - - def rebuild_galleries(self): - galleries = execute(GalleryDB.get_all_gallery, False) - if galleries: - self.DATA_COUNT.emit(len(galleries)) - log_i('Rebuilding galleries') - for n, g in enumerate(galleries, 1): - execute(GalleryDB.rebuild_gallery, False, g) - self.PROGRESS.emit(n) - self.DONE.emit(True) - - def rebuild_thumbs(self, clear_first): - if clear_first: - log_i("Clearing thumbanils dir..") - GalleryDB.clear_thumb_dir() - - gs = [] - gs.extend(app_constants.GALLERY_DATA) - gs.extend(app_constants.GALLERY_ADDITION_DATA) - self.DATA_COUNT.emit(len(app_constants.GALLERY_DATA)) - log_i('Regenerating thumbnails') - for n, g in enumerate(gs, 1): - execute(GalleryDB.rebuild_thumb, False, g) - g.reset_profile() - self.PROGRESS.emit(n) - self.DONE.emit(True) + DONE = pyqtSignal(bool) + PROGRESS = pyqtSignal(int) + DATA_COUNT = pyqtSignal(int) + def __init__(self, parent=None): + super().__init__(parent) + + def from_v021_to_v022(self, old_db_path=db_constants.DB_PATH): + log_i("Started rebuilding database") + if DBBase._DB_CONN: + DBBase._DB_CONN.close() + DBBase._DB_CONN = db.init_db(old_db_path) + db_galleries = execute(GalleryDB.get_all_gallery, False, False, True, True) + galleries = [] + for g in db_galleries: + if not os.path.exists(g.path): + log_i("Gallery doesn't exist anymore: {}".format(g.title.encode(errors="ignore"))) + else: + galleries.append(g) + + n_galleries = [] + # get all chapters + log_i("Getting chapters...") + chap_rows = DBBase().execute("SELECT * FROM chapters").fetchall() + data_count = len(chap_rows) * 2 + self.DATA_COUNT.emit(data_count) + for n, chap_row in enumerate(chap_rows, -1): + log_d('Next chapter row') + for gallery in galleries: + if gallery.id == chap_row['series_id']: + log_d('Found gallery for chapter row') + chaps = ChaptersContainer(gallery) + chap = chaps.create_chapter(chap_row['chapter_number']) + c_path = bytes.decode(chap_row['chapter_path']) + if c_path: + try: + t = utils.title_parser(os.path.split(c_path)[1])['title'] + except IndexError: + t = c_path + else: + t = '' + chap.title = t + chap.path = c_path + chap.in_archive = chap_row['in_archive'] + if gallery.is_archive: + zip = utils.ArchiveFile(gallery.path) + chap.pages = len(zip.dir_contents(chap.path)) + zip.close() + else: + chap.pages = len(list(scandir.scandir(gallery.path))) + n_galleries.append(gallery) + galleries.remove(gallery) + break + self.PROGRESS.emit(n) + log_d("G: {} C:{}".format(len(n_galleries), data_count - 1)) + log_i("Database magic...") + if os.path.exists(db_constants.THUMBNAIL_PATH): + for root, dirs, files in scandir.walk(db_constants.THUMBNAIL_PATH, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + + head = os.path.split(old_db_path)[0] + DBBase._DB_CONN.close() + t_db_path = os.path.join(head, 'temp.db') + conn = db.init_db(t_db_path) + DBBase._DB_CONN = conn + for n, g in enumerate(n_galleries, len(chap_rows) - 1): + log_d('Adding new gallery') + GalleryDB.add_gallery(g) + self.PROGRESS.emit(n) + + conn.commit() + conn.close() + + log_i("Cleaning up...") + if os.path.exists(old_db_path): + utils.backup_database(old_db_path) + os.remove(old_db_path) + if os.path.exists(db_constants.DB_PATH): + os.remove(db_constants.DB_PATH) + + os.rename(t_db_path, db_constants.DB_PATH) + self.PROGRESS.emit(data_count) + log_i("Finished rebuilding database") + self.DONE.emit(True) + return True + + def rebuild_database(self): + "Rebuilds database" + log_i("Initiating datbase rebuild") + utils.backup_database() + log_i("Getting galleries...") + galleries = GalleryDB.get_all_gallery() + self.DATA_COUNT.emit(len(galleries)) + db.DBBase._DB_CONN.close() + log_i("Removing old database...") + log_i("Initiating new database...") + temp_db = os.path.join(db_constants.DB_ROOT, "happypanda_temp.db") + if os.path.exists(temp_db): + os.remove(temp_db) + db.DBBase._DB_CONN = db.init_db(temp_db) + DBBase.begin() + log_i("Adding galleries...") + GalleryDB.clear_thumb_dir() + for n, g in enumerate(galleries): + if not os.path.exists(g.path): + log_i("Gallery doesn't exist anymore: {}".format(g.title.encode(errors="ignore"))) + else: + GalleryDB.add_gallery(g) + self.PROGRESS.emit(n) + DBBase.end() + DBBase._DB_CONN.close() + os.remove(db_constants.DB_PATH) + os.rename(temp_db, db_constants.DB_PATH) + db.DBBase._DB_CONN = db.init_db(db_constants.DB_PATH) + self.PROGRESS.emit(len(galleries)) + log_i("Succesfully rebuilt database") + self.DONE.emit(True) + return True + + def rebuild_galleries(self): + galleries = execute(GalleryDB.get_all_gallery, False) + if galleries: + self.DATA_COUNT.emit(len(galleries)) + log_i('Rebuilding galleries') + for n, g in enumerate(galleries, 1): + execute(GalleryDB.rebuild_gallery, False, g) + self.PROGRESS.emit(n) + self.DONE.emit(True) + + def rebuild_thumbs(self, clear_first): + if clear_first: + log_i("Clearing thumbanils dir..") + GalleryDB.clear_thumb_dir() + + gs = [] + gs.extend(app_constants.GALLERY_DATA) + gs.extend(app_constants.GALLERY_ADDITION_DATA) + self.DATA_COUNT.emit(len(app_constants.GALLERY_DATA)) + log_i('Regenerating thumbnails') + for n, g in enumerate(gs, 1): + execute(GalleryDB.rebuild_thumb, False, g) + g.reset_profile() + self.PROGRESS.emit(n) + self.DONE.emit(True) class DatabaseStartup(QObject): - """ - Fetches and emits database records - START: emitted when fetching from DB occurs - DONE: emitted when the initial fetching from DB finishes - """ - START = pyqtSignal() - DONE = pyqtSignal() - PROGRESS = pyqtSignal(str) - _DB = DBBase() - - - def __init__(self): - super().__init__() - ListDB.init_lists() - self._fetch_count = 500 - self._offset = 0 - self._fetching = False - self.count = 0 - self._finished = False - self._loaded_galleries = [] - - def startup(self, manga_views): - self.START.emit() - self._fetching = True - self.count = GalleryDB.gallery_count() - remaining = self.count - while remaining > 0: - self.PROGRESS.emit("Loading galleries: {}".format(remaining)) - rec_to_fetch = min(remaining, self._fetch_count) - self.fetch_galleries(self._offset, rec_to_fetch, manga_views) - self._offset += rec_to_fetch - remaining = self.count - self._offset - [v.list_view.manga_delegate._increment_paint_level() for v in manga_views] - self.PROGRESS.emit("Loading chapters...") - self.fetch_chapters() - self.PROGRESS.emit("Loading tags...") - self.fetch_tags() - [v.list_view.manga_delegate._increment_paint_level() for v in manga_views] - self.PROGRESS.emit("Loading hashes...") - self.fetch_hashes() - self._fetching = False - self.DONE.emit() - - def fetch_galleries(self, f, t, manga_views): - c = execute(self._DB.execute, False, 'SELECT * FROM series LIMIT {}, {}'.format(f, t)) - if c: - new_data = c.fetchall() - gallery_list = execute(GalleryDB.gen_galleries, False, new_data, {"chapters":False, "tags":False, "hashes":False}) - #self._current_data.extend(gallery_list) - if gallery_list: - self._loaded_galleries.extend(gallery_list) - for view in manga_views: - view_galleries = [g for g in gallery_list if g.view == view.view_type] - view.gallery_model._gallery_to_add = view_galleries - view.gallery_model.insertRows(view.gallery_model.rowCount(), len(view_galleries)) - - def fetch_chapters(self): - for g in self._loaded_galleries: - g.chapters = execute(ChapterDB.get_chapters_for_gallery, False, g.id) - - def fetch_tags(self): - for g in self._loaded_galleries: - g.tags = execute(TagDB.get_gallery_tags, False, g.id) - - def fetch_hashes(self): - for g in self._loaded_galleries: - g.hashes = execute(HashDB.get_gallery_hashes, False, g.id) + """ + Fetches and emits database records + START: emitted when fetching from DB occurs + DONE: emitted when the initial fetching from DB finishes + """ + START = pyqtSignal() + DONE = pyqtSignal() + PROGRESS = pyqtSignal(str) + _DB = DBBase() + + + def __init__(self): + super().__init__() + ListDB.init_lists() + self._fetch_count = 500 + self._offset = 0 + self._fetching = False + self.count = 0 + self._finished = False + self._loaded_galleries = [] + + def startup(self, manga_views): + self.START.emit() + self._fetching = True + self.count = GalleryDB.gallery_count() + remaining = self.count + while remaining > 0: + self.PROGRESS.emit("Loading galleries: {}".format(remaining)) + rec_to_fetch = min(remaining, self._fetch_count) + self.fetch_galleries(self._offset, rec_to_fetch, manga_views) + self._offset += rec_to_fetch + remaining = self.count - self._offset + [v.list_view.manga_delegate._increment_paint_level() for v in manga_views] + self.PROGRESS.emit("Loading chapters...") + self.fetch_chapters() + self.PROGRESS.emit("Loading tags...") + self.fetch_tags() + [v.list_view.manga_delegate._increment_paint_level() for v in manga_views] + self.PROGRESS.emit("Loading hashes...") + self.fetch_hashes() + self._fetching = False + self.DONE.emit() + + def fetch_galleries(self, f, t, manga_views): + c = execute(self._DB.execute, False, 'SELECT * FROM series LIMIT {}, {}'.format(f, t)) + if c: + new_data = c.fetchall() + gallery_list = execute(GalleryDB.gen_galleries, False, new_data, {"chapters":False, "tags":False, "hashes":False}) + #self._current_data.extend(gallery_list) + if gallery_list: + self._loaded_galleries.extend(gallery_list) + for view in manga_views: + view_galleries = [g for g in gallery_list if g.view == view.view_type] + view.gallery_model._gallery_to_add = view_galleries + view.gallery_model.insertRows(view.gallery_model.rowCount(), len(view_galleries)) + + def fetch_chapters(self): + for g in self._loaded_galleries: + g.chapters = execute(ChapterDB.get_chapters_for_gallery, False, g.id) + + def fetch_tags(self): + for g in self._loaded_galleries: + g.tags = execute(TagDB.get_gallery_tags, False, g.id) + + def fetch_hashes(self): + for g in self._loaded_galleries: + g.hashes = execute(HashDB.get_gallery_hashes, False, g.id) if __name__ == '__main__': - #unit testing here - pass + #unit testing here + pass diff --git a/version/misc.py b/version/misc.py index 0c3c44e..11c334d 100644 --- a/version/misc.py +++ b/version/misc.py @@ -11,39 +11,48 @@ #You should have received a copy of the GNU General Public License #along with Happypanda. If not, see . #""" -import os, threading, queue, time, logging, math, random, functools, scandir +import os +import threading +import queue +import time +import logging +import math +import random +import functools +import scandir from datetime import datetime from PyQt5.QtCore import (Qt, QDate, QPoint, pyqtSignal, QThread, - QTimer, QObject, QSize, QRect, QFileInfo, - QMargins, QPropertyAnimation, QRectF, - QTimeLine, QMargins, QPropertyAnimation, QByteArray, - QPointF, QSizeF, QProcess) + QTimer, QObject, QSize, QRect, QFileInfo, + QMargins, QPropertyAnimation, QRectF, + QTimeLine, QMargins, QPropertyAnimation, QByteArray, + QPointF, QSizeF, QProcess) from PyQt5.QtGui import (QTextCursor, QIcon, QMouseEvent, QFont, - QPixmapCache, QPalette, QPainter, QBrush, - QColor, QPen, QPixmap, QMovie, QPaintEvent, QFontMetrics, - QPolygonF, QRegion, QCursor, QTextOption, QTextLayout) + QPixmapCache, QPalette, QPainter, QBrush, + QColor, QPen, QPixmap, QMovie, QPaintEvent, QFontMetrics, + QPolygonF, QRegion, QCursor, QTextOption, QTextLayout, + QPalette) from PyQt5.QtWidgets import (QWidget, QProgressBar, QLabel, - QVBoxLayout, QHBoxLayout, - QDialog, QGridLayout, QLineEdit, - QFormLayout, QPushButton, QTextEdit, - QComboBox, QDateEdit, QGroupBox, - QDesktopWidget, QMessageBox, QFileDialog, - QCompleter, QListWidgetItem, - QListWidget, QApplication, QSizePolicy, - QCheckBox, QFrame, QListView, - QAbstractItemView, QTreeView, QSpinBox, - QAction, QStackedLayout, QTabWidget, - QGridLayout, QScrollArea, QLayout, QButtonGroup, - QRadioButton, QFileIconProvider, QFontDialog, - QColorDialog, QScrollArea, QSystemTrayIcon, - QMenu, QGraphicsBlurEffect, QActionGroup, - QCommonStyle, QApplication, QTableWidget, - QTableWidgetItem, QTableView, QSplitter, - QSplitterHandle, QStyledItemDelegate, QStyleOption) + QVBoxLayout, QHBoxLayout, + QDialog, QGridLayout, QLineEdit, + QFormLayout, QPushButton, QTextEdit, + QComboBox, QDateEdit, QGroupBox, + QDesktopWidget, QMessageBox, QFileDialog, + QCompleter, QListWidgetItem, + QListWidget, QApplication, QSizePolicy, + QCheckBox, QFrame, QListView, + QAbstractItemView, QTreeView, QSpinBox, + QAction, QStackedLayout, QTabWidget, + QGridLayout, QScrollArea, QLayout, QButtonGroup, + QRadioButton, QFileIconProvider, QFontDialog, + QColorDialog, QScrollArea, QSystemTrayIcon, + QMenu, QGraphicsBlurEffect, QActionGroup, + QCommonStyle, QApplication, QTableWidget, + QTableWidgetItem, QTableView, QSplitter, + QSplitterHandle, QStyledItemDelegate, QStyleOption) from utils import (tag_to_string, tag_to_dict, title_parser, ARCHIVE_FILES, - ArchiveFile, IMG_FILES) + ArchiveFile, IMG_FILES) from executors import Executors import utils import app_constants @@ -59,1901 +68,1894 @@ log_c = log.critical def text_layout(text, width, font, font_metrics, alignment=Qt.AlignCenter): - "Lays out wrapped text" - text_option = QTextOption(alignment) - text_option.setUseDesignMetrics(True) - text_option.setWrapMode(QTextOption.WordWrap) - layout = QTextLayout(text, font) - layout.setTextOption(text_option) - leading = font_metrics.leading() - height = 0 - layout.setCacheEnabled(True) - layout.beginLayout() - while True: - line = layout.createLine() - if not line.isValid(): - break - line.setLineWidth(width) - height += leading - line.setPosition(QPointF(0, height)) - height += line.height() - layout.endLayout() - return layout + "Lays out wrapped text" + text_option = QTextOption(alignment) + text_option.setUseDesignMetrics(True) + text_option.setWrapMode(QTextOption.WordWrap) + layout = QTextLayout(text, font) + layout.setTextOption(text_option) + leading = font_metrics.leading() + height = 0 + layout.setCacheEnabled(True) + layout.beginLayout() + while True: + line = layout.createLine() + if not line.isValid(): + break + line.setLineWidth(width) + height += leading + line.setPosition(QPointF(0, height)) + height += line.height() + layout.endLayout() + return layout def centerWidget(widget, parent_widget=None): - if parent_widget: - r = parent_widget.rect() - else: - r = QDesktopWidget().availableGeometry() - - widget.setGeometry( - QCommonStyle.alignedRect( - Qt.LeftToRight, - Qt.AlignCenter, - widget.size(), - r - ) - ) + if parent_widget: + r = parent_widget.rect() + else: + r = QDesktopWidget().availableGeometry() + + widget.setGeometry(QCommonStyle.alignedRect(Qt.LeftToRight, + Qt.AlignCenter, + widget.size(), + r)) def clearLayout(layout): - if layout != None: - while layout.count(): - child = layout.takeAt(0) - if child.widget() is not None: - child.widget().deleteLater() - elif child.layout() is not None: - clearLayout(child.layout()) + if layout != None: + while layout.count(): + child = layout.takeAt(0) + if child.widget() is not None: + child.widget().deleteLater() + elif child.layout() is not None: + clearLayout(child.layout()) def create_animation(parent, prop): - p_array = QByteArray().append(prop) - return QPropertyAnimation(parent, p_array) + p_array = QByteArray().append(prop) + return QPropertyAnimation(parent, p_array) class ArrowHandle(QWidget): - "Arrow Handle" - IN, OUT = range(2) - CLICKED = pyqtSignal(int) - def __init__(self, parent): - super().__init__(parent) - self.parent_widget = parent - self.current_arrow = self.IN - self.arrow_height = 20 - self.setFixedWidth(10) - self.setCursor(Qt.PointingHandCursor) - - def paintEvent(self, event): - rect = self.rect() - x, y, w, h = rect.getRect() - painter = QPainter(self) - painter.setPen(QColor("white")) - painter.setBrush(QBrush(QColor(0,0,0,100))) - painter.fillRect(rect, QColor(0,0,0,100)) - - arrow_points = [] - - # for horizontal - if self.current_arrow == self.IN: - arrow_1 = QPointF(x+w, h/2-self.arrow_height/2) - middle_point = QPointF(x, h/2) - arrow_2 = QPointF(x+w, h/2+self.arrow_height/2) - else: - arrow_1 = QPointF(x, h/2-self.arrow_height/2) - middle_point = QPointF(x+w, h/2) - arrow_2 = QPointF(x, h/2+self.arrow_height/2) - - arrow_points.append(arrow_1) - arrow_points.append(middle_point) - arrow_points.append(arrow_2) - painter.drawPolygon(QPolygonF(arrow_points)) - - def click(self): - if self.current_arrow == self.IN: - self.current_arrow = self.OUT - self.CLICKED.emit(1) - else: - self.current_arrow = self.IN - self.CLICKED.emit(0) - self.update() - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton: - self.click() - return super().mousePressEvent(event) + "Arrow Handle" + IN, OUT = range(2) + CLICKED = pyqtSignal(int) + def __init__(self, parent): + super().__init__(parent) + self.parent_widget = parent + self.current_arrow = self.IN + self.arrow_height = 20 + self.setFixedWidth(10) + self.setCursor(Qt.PointingHandCursor) + + def paintEvent(self, event): + rect = self.rect() + x, y, w, h = rect.getRect() + painter = QPainter(self) + painter.setPen(QColor("white")) + painter.setBrush(QBrush(QColor(0,0,0,100))) + painter.fillRect(rect, QColor(0,0,0,100)) + + arrow_points = [] + + # for horizontal + if self.current_arrow == self.IN: + arrow_1 = QPointF(x + w, h / 2 - self.arrow_height / 2) + middle_point = QPointF(x, h / 2) + arrow_2 = QPointF(x + w, h / 2 + self.arrow_height / 2) + else: + arrow_1 = QPointF(x, h / 2 - self.arrow_height / 2) + middle_point = QPointF(x + w, h / 2) + arrow_2 = QPointF(x, h / 2 + self.arrow_height / 2) + + arrow_points.append(arrow_1) + arrow_points.append(middle_point) + arrow_points.append(arrow_2) + painter.drawPolygon(QPolygonF(arrow_points)) + + def click(self): + if self.current_arrow == self.IN: + self.current_arrow = self.OUT + self.CLICKED.emit(1) + else: + self.current_arrow = self.IN + self.CLICKED.emit(0) + self.update() + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self.click() + return super().mousePressEvent(event) class Line(QFrame): - "'v' for vertical line or 'h' for horizontail line, color is hex string" - def __init__(self, orentiation, parent=None): - super().__init__(parent) - self.setFrameStyle(self.StyledPanel) - if orentiation == 'v': - self.setFrameShape(self.VLine) - else: - self.setFrameShape(self.HLine) - self.setFrameShadow(self.Sunken) + "'v' for vertical line or 'h' for horizontail line, color is hex string" + def __init__(self, orentiation, parent=None): + super().__init__(parent) + self.setFrameStyle(self.StyledPanel) + if orentiation == 'v': + self.setFrameShape(self.VLine) + else: + self.setFrameShape(self.HLine) + self.setFrameShadow(self.Sunken) class CompleterPopupView(QListView): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def _setup(self): - self.fade_animation = create_animation(self, 'windowOpacity') - self.fade_animation.setDuration(200) - self.fade_animation.setStartValue(0.0) - self.fade_animation.setEndValue(1.0) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setFrameStyle(self.StyledPanel) - - def showEvent(self, event): - self.setWindowOpacity(0) - self.fade_animation.start() - super().showEvent(event) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _setup(self): + self.fade_animation = create_animation(self, 'windowOpacity') + self.fade_animation.setDuration(200) + self.fade_animation.setStartValue(0.0) + self.fade_animation.setEndValue(1.0) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setFrameStyle(self.StyledPanel) + + def showEvent(self, event): + self.setWindowOpacity(0) + self.fade_animation.start() + super().showEvent(event) class ElidedLabel(QLabel): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - def paintEvent(self, event): - painter = QPainter(self) - metrics = QFontMetrics(self.font()) - elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) - painter.drawText(self.rect(), self.alignment(), elided) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + def paintEvent(self, event): + painter = QPainter(self) + metrics = QFontMetrics(self.font()) + elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) + painter.drawText(self.rect(), self.alignment(), elided) class BaseMoveWidget(QWidget): - def __init__(self, parent=None, **kwargs): - move_listener = kwargs.pop('move_listener', True) - super().__init__(parent, **kwargs) - self.parent_widget = parent - self.setAttribute(Qt.WA_DeleteOnClose) - if parent and move_listener: - try: - parent.move_listener.connect(self.update_move) - except AttributeError: - pass - - def update_move(self, new_size=None): - if new_size: - self.move(new_size) - return - if self.parent_widget: - self.move(self.parent_widget.window().frameGeometry().center() -\ - self.window().rect().center()) + def __init__(self, parent=None, **kwargs): + move_listener = kwargs.pop('move_listener', True) + super().__init__(parent, **kwargs) + self.parent_widget = parent + self.setAttribute(Qt.WA_DeleteOnClose) + if parent and move_listener: + try: + parent.move_listener.connect(self.update_move) + except AttributeError: + pass + + def update_move(self, new_size=None): + if new_size: + self.move(new_size) + return + if self.parent_widget: + self.move(self.parent_widget.window().frameGeometry().center() - \ + self.window().rect().center()) class SortMenu(QMenu): - new_sort = pyqtSignal(str) - def __init__(self, app_inst, parent=None): - super().__init__(parent) - self.parent_widget = app_inst - - self.sort_actions = QActionGroup(self, exclusive=True) - asc_desc_act = QAction("Asc/Desc", self) - asc_desc_act.triggered.connect(self.asc_desc) - s_title = self.sort_actions.addAction(QAction("Title", self.sort_actions, checkable=True)) - s_title.triggered.connect(functools.partial(self.new_sort.emit, 'title')) - s_artist = self.sort_actions.addAction(QAction("Author", self.sort_actions, checkable=True)) - s_artist.triggered.connect(functools.partial(self.new_sort.emit, 'artist')) - s_date = self.sort_actions.addAction(QAction("Date Added", self.sort_actions, checkable=True)) - s_date.triggered.connect(functools.partial(self.new_sort.emit, 'date_added')) - s_pub_d = self.sort_actions.addAction(QAction("Date Published", self.sort_actions, checkable=True)) - s_pub_d.triggered.connect(functools.partial(self.new_sort.emit, 'pub_date')) - s_times_read = self.sort_actions.addAction(QAction("Read Count", self.sort_actions, checkable=True)) - s_times_read.triggered.connect(functools.partial(self.new_sort.emit, 'times_read')) - s_last_read = self.sort_actions.addAction(QAction("Last Read", self.sort_actions, checkable=True)) - s_last_read.triggered.connect(functools.partial(self.new_sort.emit, 'last_read')) - - self.addAction(asc_desc_act) - self.addSeparator() - self.addAction(s_title) - self.addAction(s_artist) - self.addAction(s_date) - self.addAction(s_pub_d) - self.addAction(s_times_read) - self.addAction(s_last_read) - - self.set_current_sort() - - def set_current_sort(self): - def check_key(act, key): - if self.parent_widget.current_manga_view.list_view.current_sort == key: - act.setChecked(True) - - for act in self.sort_actions.actions(): - if act.text() == 'Title': - check_key(act, 'title') - elif act.text() == 'Artist': - check_key(act, 'artist') - elif act.text() == 'Date Added': - check_key(act, 'date_added') - elif act.text() == 'Date Published': - check_key(act, 'pub_date') - elif act.text() == 'Read Count': - check_key(act, 'times_read') - elif act.text() == 'Last Read': - check_key(act, 'last_read') - - def asc_desc(self): - if self.parent_widget.current_manga_view.sort_model.sortOrder() == Qt.AscendingOrder: - self.parent_widget.current_manga_view.sort_model.sort(0, Qt.DescendingOrder) - else: - self.parent_widget.current_manga_view.sort_model.sort(0, Qt.AscendingOrder) - - def showEvent(self, event): - self.set_current_sort() - super().showEvent(event) + new_sort = pyqtSignal(str) + def __init__(self, app_inst, parent=None): + super().__init__(parent) + self.parent_widget = app_inst + + self.sort_actions = QActionGroup(self, exclusive=True) + asc_desc_act = QAction("Asc/Desc", self) + asc_desc_act.triggered.connect(self.asc_desc) + s_title = self.sort_actions.addAction(QAction("Title", self.sort_actions, checkable=True)) + s_title.triggered.connect(functools.partial(self.new_sort.emit, 'title')) + s_artist = self.sort_actions.addAction(QAction("Author", self.sort_actions, checkable=True)) + s_artist.triggered.connect(functools.partial(self.new_sort.emit, 'artist')) + s_date = self.sort_actions.addAction(QAction("Date Added", self.sort_actions, checkable=True)) + s_date.triggered.connect(functools.partial(self.new_sort.emit, 'date_added')) + s_pub_d = self.sort_actions.addAction(QAction("Date Published", self.sort_actions, checkable=True)) + s_pub_d.triggered.connect(functools.partial(self.new_sort.emit, 'pub_date')) + s_times_read = self.sort_actions.addAction(QAction("Read Count", self.sort_actions, checkable=True)) + s_times_read.triggered.connect(functools.partial(self.new_sort.emit, 'times_read')) + s_last_read = self.sort_actions.addAction(QAction("Last Read", self.sort_actions, checkable=True)) + s_last_read.triggered.connect(functools.partial(self.new_sort.emit, 'last_read')) + + self.addAction(asc_desc_act) + self.addSeparator() + self.addAction(s_title) + self.addAction(s_artist) + self.addAction(s_date) + self.addAction(s_pub_d) + self.addAction(s_times_read) + self.addAction(s_last_read) + + self.set_current_sort() + + def set_current_sort(self): + def check_key(act, key): + if self.parent_widget.current_manga_view.list_view.current_sort == key: + act.setChecked(True) + + for act in self.sort_actions.actions(): + if act.text() == 'Title': + check_key(act, 'title') + elif act.text() == 'Artist': + check_key(act, 'artist') + elif act.text() == 'Date Added': + check_key(act, 'date_added') + elif act.text() == 'Date Published': + check_key(act, 'pub_date') + elif act.text() == 'Read Count': + check_key(act, 'times_read') + elif act.text() == 'Last Read': + check_key(act, 'last_read') + + def asc_desc(self): + if self.parent_widget.current_manga_view.sort_model.sortOrder() == Qt.AscendingOrder: + self.parent_widget.current_manga_view.sort_model.sort(0, Qt.DescendingOrder) + else: + self.parent_widget.current_manga_view.sort_model.sort(0, Qt.AscendingOrder) + + def showEvent(self, event): + self.set_current_sort() + super().showEvent(event) class ToolbarButton(QPushButton): - select = pyqtSignal(object) - close_tab = pyqtSignal(object) - def __init__(self, parent = None, txt=''): - super().__init__(parent) - self._text = txt - self._font_metrics = self.fontMetrics() - if txt: - self.setText(txt) - self._selected = False - self.clicked.connect(lambda: self.select.emit(self)) - self._enable_contextmenu = True - - @property - def selected(self): - return self._selected - - @selected.setter - def selected(self, b): - self._selected = b - self.update() - - def contextMenuEvent(self, event): - if self._enable_contextmenu: - m = QMenu(self) - m.addAction("Close Tab").triggered.connect(lambda: self.close_tab.emit(self)) - m.exec_(event.globalPos()) - event.accept() - else: - event.ignore() - - def paintEvent(self, event): - assert isinstance(event, QPaintEvent) - painter = QPainter(self) - painter.setPen(QColor(164,164,164,120)) - painter.setBrush(Qt.NoBrush) - if self._selected: - painter.setBrush(QBrush(QColor(164,164,164,120))) - #painter.setPen(Qt.NoPen) - painter.setRenderHint(painter.Antialiasing) - ch_width = self._font_metrics.averageCharWidth()/2 - ch_height = self._font_metrics.height() - but_rect = QRectF(ch_width, ch_width, self.width()-ch_width*2, self.height()-ch_width*2) - select_rect = QRectF(0,0, self.width(), self.height()) - - painter.drawRoundedRect(but_rect, ch_width,ch_width) - txt_to_draw = self._font_metrics.elidedText(self._text, - Qt.ElideRight, but_rect.width()) - - but_center = (but_rect.height() - ch_height)/2 - text_rect = QRectF(but_rect.x()+ch_width*2, but_rect.y()+but_center, but_rect.width(), - but_rect.height()) - painter.setPen(QColor('white')) - painter.drawText(text_rect, txt_to_draw) - - if self.underMouse(): - painter.save() - painter.setPen(Qt.NoPen) - painter.setBrush(QBrush(QColor(164,164,164,90))) - painter.drawRoundedRect(select_rect, 5,5) - painter.restore() - - def setText(self, txt): - self._text = txt - self.update() - super().setText(txt) - - def text(self): - return self._text + select = pyqtSignal(object) + close_tab = pyqtSignal(object) + def __init__(self, parent=None, txt=''): + super().__init__(parent) + self._text = txt + self._font_metrics = self.fontMetrics() + if txt: + self.setText(txt) + self._selected = False + self.clicked.connect(lambda: self.select.emit(self)) + self._enable_contextmenu = True + + @property + def selected(self): + return self._selected + + @selected.setter + def selected(self, b): + self._selected = b + self.update() + + def contextMenuEvent(self, event): + if self._enable_contextmenu: + m = QMenu(self) + m.addAction("Close Tab").triggered.connect(lambda: self.close_tab.emit(self)) + m.exec_(event.globalPos()) + event.accept() + else: + event.ignore() + + def paintEvent(self, event): + assert isinstance(event, QPaintEvent) + painter = QPainter(self) + opt = QStyleOption() + opt.initFrom(self) + #self.style().drawPrimitive(self.style().PE_FrameButtonTool, opt, painter, self) + + #painter.setPen(QColor(164,164,164,120)) + #painter.setBrush(Qt.NoBrush) + if self._selected: + painter.setPen(QColor("#d64933")) + #painter.setPen(Qt.NoPen) + painter.setRenderHint(painter.Antialiasing) + ch_width = self._font_metrics.averageCharWidth() / 2 + ch_height = self._font_metrics.height() + but_rect = QRectF(ch_width, ch_width, self.width() - ch_width * 2, self.height() - ch_width * 2) + select_rect = QRectF(0,0, self.width(), self.height()) + + painter.drawRoundedRect(but_rect, ch_width,ch_width) + txt_to_draw = self._font_metrics.elidedText(self._text, + Qt.ElideRight, but_rect.width()) + + but_center = (but_rect.height() - ch_height) / 2 + text_rect = QRectF(but_rect.x() + ch_width * 2, but_rect.y() + but_center, but_rect.width(), + but_rect.height()) + #painter.setPen(QColor('white')) + painter.drawText(text_rect, txt_to_draw) + + if self.underMouse(): + painter.save() + painter.setPen(Qt.NoPen) + painter.setBrush(QBrush(QColor(164,164,164,90))) + painter.drawRoundedRect(select_rect, 2,2) + painter.restore() + + def setText(self, txt): + self._text = txt + self.update() + super().setText(txt) + + def text(self): + return self._text class TransparentWidget(BaseMoveWidget): - def __init__(self, parent=None, **kwargs): - super().__init__(parent, **kwargs) - self.setAttribute(Qt.WA_TranslucentBackground) + def __init__(self, parent = None, **kwargs): + super().__init__(parent, **kwargs) + self.setAttribute(Qt.WA_TranslucentBackground) class ArrowWindow(TransparentWidget): - LEFT, RIGHT, TOP, BOTTOM = range(4) - - def __init__(self, parent): - super().__init__(parent, flags=Qt.Window | Qt.FramelessWindowHint, move_listener=False) - self.setAttribute(Qt.WA_ShowWithoutActivating) - self.resize(550,300) - self.direction = self.LEFT - self._arrow_size = QSizeF(20, 20) - self.content_margin = 0 - - @property - def arrow_size(self): - return self._arrow_size - - @arrow_size.setter - def arrow_size(self, w_h_tuple): - "a tuple of width and height" - if not isinstance(w_h_tuple, (tuple, list)) or len(w_h_tuple) != 2: - return - - if self.direction in (self.LEFT, self.RIGHT): - s = QSizeF(w_h_tuple[1], w_h_tuple[0]) - else: - s = QSizeF(w_h_tuple[0], w_h_tuple[1]) - - self._arrow_size = s - self.update() - - - def paintEvent(self, event): - assert isinstance(event, QPaintEvent) - - opt = QStyleOption() - opt.initFrom(self) - - painter = QPainter(self) - painter.setRenderHint(painter.Antialiasing) - - size = self.size() - if self.direction in (self.LEFT, self.RIGHT): - actual_size = QSizeF(size.width()-self.arrow_size.width(), size.height()) - else: - actual_size = QSizeF(size.width(), size.height()-self.arrow_size.height()) - - starting_point = QPointF(0, 0) - if self.direction == self.LEFT: - starting_point = QPointF(self.arrow_size.width(), 0) - elif self.direction == self.TOP: - starting_point = QPointF(0, self.arrow_size.height()) - - #painter.save() - #painter.translate(starting_point) - self.style().drawPrimitive(QCommonStyle.PE_Widget, opt, painter, self); - #painter.restore() - painter.setBrush(QBrush(painter.pen().color())) - - # draw background - background_rect = QRectF(starting_point, actual_size) - #painter.drawRoundedRect(background_rect, 5, 5) - - # calculate the arrow - arrow_points = [] - if self.direction == self.LEFT: - middle_point = QPointF(0, actual_size.height()/2) - arrow_1 = QPointF(self.arrow_size.width(), middle_point.y()-self.arrow_size.height()/2) - arrow_2 = QPointF(self.arrow_size.width(), middle_point.y()+self.arrow_size.height()/2) - arrow_points.append(arrow_1) - arrow_points.append(middle_point) - arrow_points.append(arrow_2) - elif self.direction == self.RIGHT: - middle_point = QPointF(actual_size.width()+self.arrow_size.width(), actual_size.height()/2) - arrow_1 = QPointF(actual_size.width(), middle_point.y()+self.arrow_size.height()/2) - arrow_2 = QPointF(actual_size.width(), middle_point.y()-self.arrow_size.height()/2) - arrow_points.append(arrow_1) - arrow_points.append(middle_point) - arrow_points.append(arrow_2) - elif self.direction == self.TOP: - middle_point = QPointF(actual_size.width()/2, 0) - arrow_1 = QPointF(actual_size.width()/2+self.arrow_size.width()/2, self.arrow_size.height()) - arrow_2 = QPointF(actual_size.width()/2-self.arrow_size.width()/2, self.arrow_size.height()) - arrow_points.append(arrow_1) - arrow_points.append(middle_point) - arrow_points.append(arrow_2) - elif self.direction == self.BOTTOM: - middle_point = QPointF(actual_size.width()/2, actual_size.height()+self.arrow_size.height()) - arrow_1 = QPointF(actual_size.width()/2-self.arrow_size.width()/2, actual_size.height()) - arrow_2 = QPointF(actual_size.width()/2+self.arrow_size.width()/2, actual_size.height()) - arrow_points.append(arrow_1) - arrow_points.append(middle_point) - arrow_points.append(arrow_2) - - # draw it! - painter.drawPolygon(QPolygonF(arrow_points)) + LEFT, RIGHT, TOP, BOTTOM = range(4) + + def __init__(self, parent): + super().__init__(parent, flags=Qt.Window | Qt.FramelessWindowHint, move_listener=False) + self.setAttribute(Qt.WA_ShowWithoutActivating) + self.resize(550,300) + self.direction = self.LEFT + self._arrow_size = QSizeF(30, 30) + self.content_margin = 0 + + @property + def arrow_size(self): + return self._arrow_size + + @arrow_size.setter + def arrow_size(self, w_h_tuple): + "a tuple of width and height" + if not isinstance(w_h_tuple, (tuple, list)) or len(w_h_tuple) != 2: + return + + if self.direction in (self.LEFT, self.RIGHT): + s = QSizeF(w_h_tuple[1], w_h_tuple[0]) + else: + s = QSizeF(w_h_tuple[0], w_h_tuple[1]) + + self._arrow_size = s + self.update() + + + def paintEvent(self, event): + assert isinstance(event, QPaintEvent) + + opt = QStyleOption() + opt.initFrom(self) + + painter = QPainter(self) + painter.setRenderHint(painter.Antialiasing) + + size = self.size() + if self.direction in (self.LEFT, self.RIGHT): + actual_size = QSizeF(size.width() - self.arrow_size.width(), size.height()) + else: + actual_size = QSizeF(size.width(), size.height() - self.arrow_size.height()) + + starting_point = QPointF(0, 0) + if self.direction == self.LEFT: + starting_point = QPointF(self.arrow_size.width(), 0) + elif self.direction == self.TOP: + starting_point = QPointF(0, self.arrow_size.height()) + + #painter.save() + #painter.translate(starting_point) + self.style().drawPrimitive(QCommonStyle.PE_Widget, opt, painter, self) + #painter.restore() + painter.setBrush(QBrush(painter.pen().color())) + + # draw background + background_rect = QRectF(starting_point, actual_size) + #painter.drawRoundedRect(background_rect, 5, 5) + + # calculate the arrow + arrow_points = [] + if self.direction == self.LEFT: + middle_point = QPointF(0, actual_size.height() / 2) + arrow_1 = QPointF(self.arrow_size.width(), middle_point.y() - self.arrow_size.height() / 2) + arrow_2 = QPointF(self.arrow_size.width(), middle_point.y() + self.arrow_size.height() / 2) + arrow_points.append(arrow_1) + arrow_points.append(middle_point) + arrow_points.append(arrow_2) + elif self.direction == self.RIGHT: + middle_point = QPointF(actual_size.width() + self.arrow_size.width(), actual_size.height() / 2) + arrow_1 = QPointF(actual_size.width(), middle_point.y() + self.arrow_size.height() / 2) + arrow_2 = QPointF(actual_size.width(), middle_point.y() - self.arrow_size.height() / 2) + arrow_points.append(arrow_1) + arrow_points.append(middle_point) + arrow_points.append(arrow_2) + elif self.direction == self.TOP: + middle_point = QPointF(actual_size.width() / 2, 0) + arrow_1 = QPointF(actual_size.width() / 2 + self.arrow_size.width() / 2, self.arrow_size.height()) + arrow_2 = QPointF(actual_size.width() / 2 - self.arrow_size.width() / 2, self.arrow_size.height()) + arrow_points.append(arrow_1) + arrow_points.append(middle_point) + arrow_points.append(arrow_2) + elif self.direction == self.BOTTOM: + middle_point = QPointF(actual_size.width() / 2, actual_size.height() + self.arrow_size.height()) + arrow_1 = QPointF(actual_size.width() / 2 - self.arrow_size.width() / 2, actual_size.height()) + arrow_2 = QPointF(actual_size.width() / 2 + self.arrow_size.width() / 2, actual_size.height()) + arrow_points.append(arrow_1) + arrow_points.append(middle_point) + arrow_points.append(arrow_2) + + # draw it! + painter.drawPolygon(QPolygonF(arrow_points)) class GalleryMetaWindow(ArrowWindow): - def __init__(self, parent): - super().__init__(parent) - # gallery data stuff - - self.content_margin = 10 - self.current_gallery = None - self.g_widget = self.GalleryLayout(self, parent) - self.hide_timer = QTimer() - self.hide_timer.timeout.connect(self.delayed_hide) - self.hide_timer.setSingleShot(True) - self.hide_animation = create_animation(self, 'windowOpacity') - self.hide_animation.setDuration(250) - self.hide_animation.setStartValue(1.0) - self.hide_animation.setEndValue(0.0) - self.hide_animation.finished.connect(self.hide) - self.show_animation = create_animation(self, 'windowOpacity') - self.show_animation.setDuration(350) - self.show_animation.setStartValue(0.0) - self.show_animation.setEndValue(1.0) - self.setFocusPolicy(Qt.NoFocus) - - def show(self): - if not self.hide_animation.Running: - self.setWindowOpacity(0) - super().show() - self.show_animation.start() - else: - self.hide_animation.stop() - super().show() - self.show_animation.setStartValue(self.windowOpacity()) - self.show_animation.start() - - def focusOutEvent(self, event): - self.delayed_hide() - return super().focusOutEvent(event) - - def _mouse_in_gallery(self): - mouse_p = QCursor.pos() - h = self.idx_top_l.x() <= mouse_p.x() <= self.idx_top_r.x() - v = self.idx_top_l.y() <= mouse_p.y() <= self.idx_btm_l.y() - if h and v: - return True - return False - - def mouseMoveEvent(self, event): - if self.isVisible(): - if not self._mouse_in_gallery(): - if not self.hide_timer.isActive(): - self.hide_timer.start(300) - return super().mouseMoveEvent(event) - - def delayed_hide(self): - if not self.underMouse() and not self._mouse_in_gallery(): - self.hide_animation.start() - - def show_gallery(self, index, view): - self.resize(app_constants.POPUP_WIDTH, app_constants.POPUP_HEIGHT) - self.view = view - desktop_w = QDesktopWidget().width() - desktop_h = QDesktopWidget().height() - - margin_offset = 20 # should be higher than gallery_touch_offset - gallery_touch_offset = 10 # How far away the window is from touching gallery - - index_rect = view.visualRect(index) - self.idx_top_l = index_top_left = view.mapToGlobal(index_rect.topLeft()) - self.idx_top_r = index_top_right = view.mapToGlobal(index_rect.topRight()) - self.idx_btm_l = index_btm_left = view.mapToGlobal(index_rect.bottomLeft()) - index_btm_right = view.mapToGlobal(index_rect.bottomRight()) - - if app_constants.DEBUG: - for idx in (index_top_left, index_top_right, index_btm_left, index_btm_right): - print(idx.x(), idx.y()) - - # adjust placement - - def check_left(): - middle = (index_top_left.y() + index_btm_left.y())/2 # middle of gallery left side - left = (index_top_left.x() - self.width() - margin_offset) > 0 # if the width can be there - top = (middle - (self.height()/2) - margin_offset) > 0 # if the top half of window can be there - btm = (middle + (self.height()/2) + margin_offset) < desktop_h # same as above, just for the bottom - if left and top and btm: - self.direction = self.RIGHT - x = index_top_left.x() - gallery_touch_offset - self.width() - y = middle - (self.height()/2) - appear_point = QPoint(int(x), int(y)) - self.move(appear_point) - return True - return False - - def check_right(): - middle = (index_top_right.y() + index_btm_right.y())/2 # middle of gallery right side - right = (index_top_right.x() + self.width() + margin_offset) < desktop_w # if the width can be there - top = (middle - (self.height()/2) - margin_offset) > 0 # if the top half of window can be there - btm = (middle + (self.height()/2) + margin_offset) < desktop_h # same as above, just for the bottom - - if right and top and btm: - self.direction = self.LEFT - x = index_top_right.x() + gallery_touch_offset - y = middle - (self.height()/2) - appear_point = QPoint(int(x), int(y)) - self.move(appear_point) - return True - return False - - def check_top(): - middle = (index_top_left.x() + index_top_right.x())/2 # middle of gallery top side - top = (index_top_right.y() - self.height() - margin_offset) > 0 # if the height can be there - left = (middle - (self.width()/2) - margin_offset) > 0 # if the left half of window can be there - right = (middle + (self.width()/2) + margin_offset) < desktop_w # same as above, just for the right - - if top and left and right: - self.direction = self.BOTTOM - x = middle - (self.width()/2) - y = index_top_left.y() - gallery_touch_offset - self.height() - appear_point = QPoint(int(x), int(y)) - self.move(appear_point) - return True - return False - - def check_bottom(override=False): - middle = (index_btm_left.x() + index_btm_right.x())/2 # middle of gallery bottom side - btm = (index_btm_right.y() + self.height() + margin_offset) < desktop_h # if the height can be there - left = (middle - (self.width()/2) - margin_offset) > 0 # if the left half of window can be there - right = (middle + (self.width()/2) + margin_offset) < desktop_w # same as above, just for the right - - if (btm and left and right) or override: - self.direction = self.TOP - x = middle - (self.width()/2) - y = index_btm_left.y() + gallery_touch_offset - appear_point = QPoint(int(x), int(y)) - self.move(appear_point) - return True - return False - - for pos in (check_bottom, check_right, check_left, check_top): - if pos(): - break - else: # default pos is bottom - check_bottom(True) - - self._set_gallery(index.data(Qt.UserRole+1)) - self.show() - - def closeEvent(self, ev): - ev.ignore() - self.delayed_hide() - - def _set_gallery(self, gallery): - self.current_gallery = gallery - self.g_widget.apply_gallery(gallery) - self.g_widget.resize(self.width()-self.content_margin, - self.height()-self.content_margin) - if self.direction == self.LEFT: - start_point = QPoint(self.arrow_size.width(), 0) - elif self.direction == self.TOP: - start_point = QPoint(0, self.arrow_size.height()) - else: - start_point = QPoint(0, 0) - # title - #title_region = QRegion(0, 0, self.g_title_lbl.width(), self.g_title_lbl.height()) - self.g_widget.move(start_point) - - class GalleryLayout(QFrame): - class ChapterList(QTableWidget): - def __init__(self, parent): - super().__init__(parent) - self.setColumnCount(3) - self.setEditTriggers(self.NoEditTriggers) - self.setFocusPolicy(Qt.NoFocus) - self.verticalHeader().setSectionResizeMode(self.verticalHeader().ResizeToContents) - self.horizontalHeader().setSectionResizeMode(0, self.horizontalHeader().ResizeToContents) - self.horizontalHeader().setSectionResizeMode(1, self.horizontalHeader().Stretch) - self.horizontalHeader().setSectionResizeMode(2, self.horizontalHeader().ResizeToContents) - self.horizontalHeader().hide() - self.verticalHeader().hide() - self.setSelectionMode(self.SingleSelection) - self.setSelectionBehavior(self.SelectRows) - self.setShowGrid(False) - self.viewport().setBackgroundRole(self.palette().Dark) - palette = self.viewport().palette() - palette.setColor(palette.Highlight, QColor(88, 88, 88, 70)) - palette.setColor(palette.HighlightedText, QColor('black')) - self.viewport().setPalette(palette) - self.setWordWrap(False) - self.setTextElideMode(Qt.ElideRight) - self.doubleClicked.connect(lambda idx: self._get_chap(idx).open()) - - def set_chapters(self, chapter_container): - for r in range(self.rowCount()): - self.removeRow(0) - def t_item(txt=''): - t = QTableWidgetItem(txt) - t.setBackground(QBrush(QColor('#585858'))) - return t - - for chap in chapter_container: - c_row = self.rowCount()+1 - self.setRowCount(c_row) - c_row -= 1 - n = t_item() - n.setData(Qt.DisplayRole, chap.number+1) - n.setData(Qt.UserRole+1, chap) - self.setItem(c_row, 0, n) - title = chap.title - if not title: - title = chap.gallery.title - t = t_item(title) - self.setItem(c_row, 1, t) - p = t_item(str(chap.pages)) - self.setItem(c_row, 2, p) - self.sortItems(0) - - def _get_chap(self, idx): - r = idx.row() - t = self.item(r, 0) - return t.data(Qt.UserRole+1) - - def contextMenuEvent(self, event): - idx = self.indexAt(event.pos()) - if idx.isValid(): - chap = self._get_chap(idx) - menu = QMenu(self) - open = menu.addAction('Open', lambda: chap.open()) - def open_source(): - text = 'Opening archive...' if chap.in_archive else 'Opening folder...' - app_constants.STAT_MSG_METHOD(text) - path = chap.gallery.path if chap.in_archive else chap.path - utils.open_path(path) - t = "Open archive" if chap.in_archive else "Open folder" - open_path = menu.addAction(t, open_source) - menu.exec_(event.globalPos()) - event.accept() - del menu - else: - event.ignore() - - def __init__(self, parent, appwindow): - super().__init__(parent) - self.appwindow = appwindow - self.setStyleSheet('color:white;') - main_layout = QHBoxLayout(self) - self.stacked_l = stacked_l = QStackedLayout() - general_info = QWidget(self) - chapter_info = QWidget(self) - chapter_layout = QVBoxLayout(chapter_info) - self.general_index = stacked_l.addWidget(general_info) - self.chap_index = stacked_l.addWidget(chapter_info) - self.chapter_list = self.ChapterList(self) - back_btn = TagText('Back') - back_btn.clicked.connect(lambda: stacked_l.setCurrentIndex(self.general_index)) - chapter_layout.addWidget(back_btn, 0, Qt.AlignCenter) - chapter_layout.addWidget(self.chapter_list) - self.left_layout = QFormLayout() - self.main_left_layout = QVBoxLayout(general_info) - self.main_left_layout.addLayout(self.left_layout) - self.right_layout = QFormLayout() - main_layout.addLayout(stacked_l, 1) - main_layout.addWidget(Line('v')) - main_layout.addLayout(self.right_layout) - def get_label(txt): - lbl = QLabel(txt) - lbl.setWordWrap(True) - return lbl - self.g_title_lbl = get_label('') - self.g_title_lbl.setStyleSheet('color:white;font-weight:bold;') - self.left_layout.addRow(self.g_title_lbl) - self.g_artist_lbl = ClickedLabel() - self.g_artist_lbl.setWordWrap(True) - self.g_artist_lbl.clicked.connect(lambda a: appwindow.search("artist:{}".format(a))) - self.g_artist_lbl.setStyleSheet('color:#bdc3c7;') - self.g_artist_lbl.setToolTip("Click to see more from this artist") - self.left_layout.addRow(self.g_artist_lbl) - for lbl in (self.g_title_lbl, self.g_artist_lbl): - lbl.setAlignment(Qt.AlignCenter) - self.left_layout.addRow(Line('h')) - - first_layout = QHBoxLayout() - self.g_type_lbl = ClickedLabel() - self.g_type_lbl.setStyleSheet('text-decoration: underline') - self.g_type_lbl.clicked.connect(lambda a: appwindow.search("type:{}".format(a))) - self.g_lang_lbl = ClickedLabel() - self.g_lang_lbl.setStyleSheet('text-decoration: underline') - self.g_lang_lbl.clicked.connect(lambda a: appwindow.search("language:{}".format(a))) - self.g_chapters_lbl = TagText('Chapters') - self.g_chapters_lbl.clicked.connect(lambda: stacked_l.setCurrentIndex(self.chap_index)) - self.g_chap_count_lbl = QLabel() - self.right_layout.addRow(self.g_type_lbl) - self.right_layout.addRow(self.g_lang_lbl) - self.right_layout.addRow(self.g_chap_count_lbl) - #first_layout.addWidget(self.g_lang_lbl, 0, Qt.AlignLeft) - first_layout.addWidget(self.g_chapters_lbl, 0, Qt.AlignCenter) - #first_layout.addWidget(self.g_type_lbl, 0, Qt.AlignRight) - self.left_layout.addRow(first_layout) - - self.g_status_lbl = QLabel() - self.g_d_added_lbl = QLabel() - self.g_pub_lbl = QLabel() - self.g_last_read_lbl = QLabel() - self.g_read_count_lbl = QLabel() - self.g_pages_total_lbl = QLabel() - self.right_layout.addRow(self.g_read_count_lbl) - self.right_layout.addRow('Pages:', self.g_pages_total_lbl) - self.right_layout.addRow('Status:', self.g_status_lbl) - self.right_layout.addRow('Added:', self.g_d_added_lbl) - self.right_layout.addRow('Published:', self.g_pub_lbl) - self.right_layout.addRow('Last read:', self.g_last_read_lbl) - - self.g_info_lbl = get_label('') - self.left_layout.addRow(self.g_info_lbl) - - self.g_url_lbl = ClickedLabel() - self.g_url_lbl.clicked.connect(lambda: utils.open_web_link(self.g_url_lbl.text())) - self.g_url_lbl.setWordWrap(True) - self.left_layout.addRow('URL:', self.g_url_lbl) - #self.left_layout.addRow(Line('h')) - - self.tags_scroll = QScrollArea(self) - self.tags_widget = QWidget(self.tags_scroll) - self.tags_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.tags_layout = QFormLayout(self.tags_widget) - self.tags_layout.setSizeConstraint(self.tags_layout.SetMaximumSize) - self.tags_scroll.setWidget(self.tags_widget) - self.tags_scroll.setWidgetResizable(True) - self.tags_scroll.setFrameShape(QFrame.NoFrame) - self.main_left_layout.addWidget(self.tags_scroll) - - - def has_tags(self, tags): - t_len = len(tags) - if not t_len: - return False - if t_len == 1: - if 'default' in tags: - if not tags['default']: - return False - return True - - def apply_gallery(self, gallery): - self.stacked_l.setCurrentIndex(self.general_index) - self.chapter_list.set_chapters(gallery.chapters) - self.g_title_lbl.setText(gallery.title) - self.g_artist_lbl.setText(gallery.artist) - self.g_lang_lbl.setText(gallery.language) - chap_txt = "chapters" if gallery.chapters.count() > 1 else "chapter" - self.g_chap_count_lbl.setText('{} {}'.format(gallery.chapters.count(), chap_txt)) - self.g_type_lbl.setText(gallery.type) - pages = gallery.chapters.pages() - self.g_pages_total_lbl.setText('{}'.format(pages)) - self.g_status_lbl.setText(gallery.status) - self.g_d_added_lbl.setText(gallery.date_added.strftime('%d %b %Y')) - if gallery.pub_date: - self.g_pub_lbl.setText(gallery.pub_date.strftime('%d %b %Y')) - else: - self.g_pub_lbl.setText('Unknown') - last_read_txt = '{} ago'.format(utils.get_date_age(gallery.last_read)) if gallery.last_read else "Never!" - self.g_last_read_lbl.setText(last_read_txt) - self.g_read_count_lbl.setText('Read {} times'.format(gallery.times_read)) - self.g_info_lbl.setText(gallery.info) - if gallery.link: - self.g_url_lbl.setText(gallery.link) - self.g_url_lbl.show() - else: - self.g_url_lbl.hide() - - - clearLayout(self.tags_layout) - if self.has_tags(gallery.tags): - ns_layout = QFormLayout() - self.tags_layout.addRow(ns_layout) - for namespace in sorted(gallery.tags): - tags_lbls = FlowLayout() - if namespace == 'default': - self.tags_layout.insertRow(0, tags_lbls) - else: - self.tags_layout.addRow(namespace, tags_lbls) - - for n, tag in enumerate(sorted(gallery.tags[namespace]), 1): - if namespace == 'default': - t = TagText(search_widget=self.appwindow) - else: - t = TagText(search_widget=self.appwindow, namespace=namespace) - t.setText(tag) - tags_lbls.addWidget(t) - t.setAutoFillBackground(True) - self.tags_widget.adjustSize() + def __init__(self, parent): + super().__init__(parent) + # gallery data stuff + + self.content_margin = 10 + self.current_gallery = None + self.g_widget = self.GalleryLayout(self, parent) + self.hide_timer = QTimer() + self.hide_timer.timeout.connect(self.delayed_hide) + self.hide_timer.setSingleShot(True) + self.hide_animation = create_animation(self, 'windowOpacity') + self.hide_animation.setDuration(250) + self.hide_animation.setStartValue(1.0) + self.hide_animation.setEndValue(0.0) + self.hide_animation.finished.connect(self.hide) + self.show_animation = create_animation(self, 'windowOpacity') + self.show_animation.setDuration(350) + self.show_animation.setStartValue(0.0) + self.show_animation.setEndValue(1.0) + self.setFocusPolicy(Qt.NoFocus) + + def show(self): + if not self.hide_animation.Running: + self.setWindowOpacity(0) + super().show() + self.show_animation.start() + else: + self.hide_animation.stop() + super().show() + self.show_animation.setStartValue(self.windowOpacity()) + self.show_animation.start() + + def focusOutEvent(self, event): + self.delayed_hide() + return super().focusOutEvent(event) + + def _mouse_in_gallery(self): + mouse_p = QCursor.pos() + h = self.idx_top_l.x() <= mouse_p.x() <= self.idx_top_r.x() + v = self.idx_top_l.y() <= mouse_p.y() <= self.idx_btm_l.y() + if h and v: + return True + return False + + def mouseMoveEvent(self, event): + if self.isVisible(): + if not self._mouse_in_gallery(): + if not self.hide_timer.isActive(): + self.hide_timer.start(300) + return super().mouseMoveEvent(event) + + def delayed_hide(self): + if not self.underMouse() and not self._mouse_in_gallery(): + self.hide_animation.start() + + def show_gallery(self, index, view): + self.resize(app_constants.POPUP_WIDTH, app_constants.POPUP_HEIGHT) + self.view = view + desktop_w = QDesktopWidget().width() + desktop_h = QDesktopWidget().height() + + margin_offset = 20 # should be higher than gallery_touch_offset + gallery_touch_offset = 10 # How far away the window is from touching gallery + + index_rect = view.visualRect(index) + self.idx_top_l = index_top_left = view.mapToGlobal(index_rect.topLeft()) + self.idx_top_r = index_top_right = view.mapToGlobal(index_rect.topRight()) + self.idx_btm_l = index_btm_left = view.mapToGlobal(index_rect.bottomLeft()) + index_btm_right = view.mapToGlobal(index_rect.bottomRight()) + + if app_constants.DEBUG: + for idx in (index_top_left, index_top_right, index_btm_left, index_btm_right): + print(idx.x(), idx.y()) + + # adjust placement + + def check_left(): + middle = (index_top_left.y() + index_btm_left.y()) / 2 # middle of gallery left side + left = (index_top_left.x() - self.width() - margin_offset) > 0 # if the width can be there + top = (middle - (self.height() / 2) - margin_offset) > 0 # if the top half of window can be there + btm = (middle + (self.height() / 2) + margin_offset) < desktop_h # same as above, just for the bottom + if left and top and btm: + self.direction = self.RIGHT + x = index_top_left.x() - gallery_touch_offset - self.width() + y = middle - (self.height() / 2) + appear_point = QPoint(int(x), int(y)) + self.move(appear_point) + return True + return False + + def check_right(): + middle = (index_top_right.y() + index_btm_right.y()) / 2 # middle of gallery right side + right = (index_top_right.x() + self.width() + margin_offset) < desktop_w # if the width can be there + top = (middle - (self.height() / 2) - margin_offset) > 0 # if the top half of window can be there + btm = (middle + (self.height() / 2) + margin_offset) < desktop_h # same as above, just for the bottom + + if right and top and btm: + self.direction = self.LEFT + x = index_top_right.x() + gallery_touch_offset + y = middle - (self.height() / 2) + appear_point = QPoint(int(x), int(y)) + self.move(appear_point) + return True + return False + + def check_top(): + middle = (index_top_left.x() + index_top_right.x()) / 2 # middle of gallery top side + top = (index_top_right.y() - self.height() - margin_offset) > 0 # if the height can be there + left = (middle - (self.width() / 2) - margin_offset) > 0 # if the left half of window can be there + right = (middle + (self.width() / 2) + margin_offset) < desktop_w # same as above, just for the right + + if top and left and right: + self.direction = self.BOTTOM + x = middle - (self.width() / 2) + y = index_top_left.y() - gallery_touch_offset - self.height() + appear_point = QPoint(int(x), int(y)) + self.move(appear_point) + return True + return False + + def check_bottom(override=False): + middle = (index_btm_left.x() + index_btm_right.x()) / 2 # middle of gallery bottom side + btm = (index_btm_right.y() + self.height() + margin_offset) < desktop_h # if the height can be there + left = (middle - (self.width() / 2) - margin_offset) > 0 # if the left half of window can be there + right = (middle + (self.width() / 2) + margin_offset) < desktop_w # same as above, just for the right + + if (btm and left and right) or override: + self.direction = self.TOP + x = middle - (self.width() / 2) + y = index_btm_left.y() + gallery_touch_offset + appear_point = QPoint(int(x), int(y)) + self.move(appear_point) + return True + return False + + for pos in (check_bottom, check_right, check_left, check_top): + if pos(): + break + else: # default pos is bottom + check_bottom(True) + + self._set_gallery(index.data(Qt.UserRole + 1)) + self.show() + + def closeEvent(self, ev): + ev.ignore() + self.delayed_hide() + + def _set_gallery(self, gallery): + self.current_gallery = gallery + self.g_widget.apply_gallery(gallery) + self.g_widget.resize(self.width() - self.content_margin, + self.height() - self.content_margin) + if self.direction == self.LEFT: + start_point = QPoint(self.arrow_size.width(), 0) + elif self.direction == self.TOP: + start_point = QPoint(0, self.arrow_size.height()) + else: + start_point = QPoint(0, 0) + # title + #title_region = QRegion(0, 0, self.g_title_lbl.width(), + #self.g_title_lbl.height()) + self.g_widget.move(start_point) + + class GalleryLayout(QFrame): + class ChapterList(QTableWidget): + def __init__(self, parent): + super().__init__(parent) + self.setColumnCount(3) + self.setEditTriggers(self.NoEditTriggers) + self.setFocusPolicy(Qt.NoFocus) + self.verticalHeader().setSectionResizeMode(self.verticalHeader().ResizeToContents) + self.horizontalHeader().setSectionResizeMode(0, self.horizontalHeader().ResizeToContents) + self.horizontalHeader().setSectionResizeMode(1, self.horizontalHeader().Stretch) + self.horizontalHeader().setSectionResizeMode(2, self.horizontalHeader().ResizeToContents) + self.horizontalHeader().hide() + self.verticalHeader().hide() + self.setSelectionMode(self.SingleSelection) + self.setSelectionBehavior(self.SelectRows) + self.setShowGrid(False) + self.viewport().setBackgroundRole(self.palette().Dark) + palette = self.viewport().palette() + palette.setColor(palette.Highlight, QColor(88, 88, 88, 70)) + palette.setColor(palette.HighlightedText, QColor('black')) + self.viewport().setPalette(palette) + self.setWordWrap(False) + self.setTextElideMode(Qt.ElideRight) + self.doubleClicked.connect(lambda idx: self._get_chap(idx).open()) + + def set_chapters(self, chapter_container): + for r in range(self.rowCount()): + self.removeRow(0) + def t_item(txt=''): + t = QTableWidgetItem(txt) + t.setBackground(QBrush(QColor('#585858'))) + return t + + for chap in chapter_container: + c_row = self.rowCount() + 1 + self.setRowCount(c_row) + c_row -= 1 + n = t_item() + n.setData(Qt.DisplayRole, chap.number + 1) + n.setData(Qt.UserRole + 1, chap) + self.setItem(c_row, 0, n) + title = chap.title + if not title: + title = chap.gallery.title + t = t_item(title) + self.setItem(c_row, 1, t) + p = t_item(str(chap.pages)) + self.setItem(c_row, 2, p) + self.sortItems(0) + + def _get_chap(self, idx): + r = idx.row() + t = self.item(r, 0) + return t.data(Qt.UserRole + 1) + + def contextMenuEvent(self, event): + idx = self.indexAt(event.pos()) + if idx.isValid(): + chap = self._get_chap(idx) + menu = QMenu(self) + open = menu.addAction('Open', lambda: chap.open()) + def open_source(): + text = 'Opening archive...' if chap.in_archive else 'Opening folder...' + app_constants.STAT_MSG_METHOD(text) + path = chap.gallery.path if chap.in_archive else chap.path + utils.open_path(path) + t = "Open archive" if chap.in_archive else "Open folder" + open_path = menu.addAction(t, open_source) + menu.exec_(event.globalPos()) + event.accept() + del menu + else: + event.ignore() + + def __init__(self, parent, appwindow): + super().__init__(parent) + self.appwindow = appwindow + self.setStyleSheet('color:white;') + main_layout = QHBoxLayout(self) + self.stacked_l = stacked_l = QStackedLayout() + general_info = QWidget(self) + chapter_info = QWidget(self) + chapter_layout = QVBoxLayout(chapter_info) + self.general_index = stacked_l.addWidget(general_info) + self.chap_index = stacked_l.addWidget(chapter_info) + self.chapter_list = self.ChapterList(self) + back_btn = TagText('Back') + back_btn.clicked.connect(lambda: stacked_l.setCurrentIndex(self.general_index)) + chapter_layout.addWidget(back_btn, 0, Qt.AlignCenter) + chapter_layout.addWidget(self.chapter_list) + self.left_layout = QFormLayout() + self.main_left_layout = QVBoxLayout(general_info) + self.main_left_layout.addLayout(self.left_layout) + self.right_layout = QFormLayout() + main_layout.addLayout(stacked_l, 1) + main_layout.addWidget(Line('v')) + main_layout.addLayout(self.right_layout) + def get_label(txt): + lbl = QLabel(txt) + lbl.setWordWrap(True) + return lbl + self.g_title_lbl = get_label('') + self.g_title_lbl.setStyleSheet('color:white;font-weight:bold;') + self.left_layout.addRow(self.g_title_lbl) + self.g_artist_lbl = ClickedLabel() + self.g_artist_lbl.setWordWrap(True) + self.g_artist_lbl.clicked.connect(lambda a: appwindow.search("artist:{}".format(a))) + self.g_artist_lbl.setStyleSheet('color:#bdc3c7;') + self.g_artist_lbl.setToolTip("Click to see more from this artist") + self.left_layout.addRow(self.g_artist_lbl) + for lbl in (self.g_title_lbl, self.g_artist_lbl): + lbl.setAlignment(Qt.AlignCenter) + self.left_layout.addRow(Line('h')) + + first_layout = QHBoxLayout() + self.g_type_lbl = ClickedLabel() + self.g_type_lbl.setStyleSheet('text-decoration: underline') + self.g_type_lbl.clicked.connect(lambda a: appwindow.search("type:{}".format(a))) + self.g_lang_lbl = ClickedLabel() + self.g_lang_lbl.setStyleSheet('text-decoration: underline') + self.g_lang_lbl.clicked.connect(lambda a: appwindow.search("language:{}".format(a))) + self.g_chapters_lbl = TagText('Chapters') + self.g_chapters_lbl.clicked.connect(lambda: stacked_l.setCurrentIndex(self.chap_index)) + self.g_chap_count_lbl = QLabel() + self.right_layout.addRow(self.g_type_lbl) + self.right_layout.addRow(self.g_lang_lbl) + self.right_layout.addRow(self.g_chap_count_lbl) + #first_layout.addWidget(self.g_lang_lbl, 0, Qt.AlignLeft) + first_layout.addWidget(self.g_chapters_lbl, 0, Qt.AlignCenter) + #first_layout.addWidget(self.g_type_lbl, 0, Qt.AlignRight) + self.left_layout.addRow(first_layout) + + self.g_status_lbl = QLabel() + self.g_d_added_lbl = QLabel() + self.g_pub_lbl = QLabel() + self.g_last_read_lbl = QLabel() + self.g_read_count_lbl = QLabel() + self.g_pages_total_lbl = QLabel() + self.right_layout.addRow(self.g_read_count_lbl) + self.right_layout.addRow('Pages:', self.g_pages_total_lbl) + self.right_layout.addRow('Status:', self.g_status_lbl) + self.right_layout.addRow('Added:', self.g_d_added_lbl) + self.right_layout.addRow('Published:', self.g_pub_lbl) + self.right_layout.addRow('Last read:', self.g_last_read_lbl) + + self.g_info_lbl = get_label('') + self.left_layout.addRow(self.g_info_lbl) + + self.g_url_lbl = ClickedLabel() + self.g_url_lbl.clicked.connect(lambda: utils.open_web_link(self.g_url_lbl.text())) + self.g_url_lbl.setWordWrap(True) + self.left_layout.addRow('URL:', self.g_url_lbl) + #self.left_layout.addRow(Line('h')) + + self.tags_scroll = QScrollArea(self) + self.tags_widget = QWidget(self.tags_scroll) + self.tags_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.tags_layout = QFormLayout(self.tags_widget) + self.tags_layout.setSizeConstraint(self.tags_layout.SetMaximumSize) + self.tags_scroll.setWidget(self.tags_widget) + self.tags_scroll.setWidgetResizable(True) + self.tags_scroll.setFrameShape(QFrame.NoFrame) + self.main_left_layout.addWidget(self.tags_scroll) + + + def has_tags(self, tags): + t_len = len(tags) + if not t_len: + return False + if t_len == 1: + if 'default' in tags: + if not tags['default']: + return False + return True + + def apply_gallery(self, gallery): + self.stacked_l.setCurrentIndex(self.general_index) + self.chapter_list.set_chapters(gallery.chapters) + self.g_title_lbl.setText(gallery.title) + self.g_artist_lbl.setText(gallery.artist) + self.g_lang_lbl.setText(gallery.language) + chap_txt = "chapters" if gallery.chapters.count() > 1 else "chapter" + self.g_chap_count_lbl.setText('{} {}'.format(gallery.chapters.count(), chap_txt)) + self.g_type_lbl.setText(gallery.type) + pages = gallery.chapters.pages() + self.g_pages_total_lbl.setText('{}'.format(pages)) + self.g_status_lbl.setText(gallery.status) + self.g_d_added_lbl.setText(gallery.date_added.strftime('%d %b %Y')) + if gallery.pub_date: + self.g_pub_lbl.setText(gallery.pub_date.strftime('%d %b %Y')) + else: + self.g_pub_lbl.setText('Unknown') + last_read_txt = '{} ago'.format(utils.get_date_age(gallery.last_read)) if gallery.last_read else "Never!" + self.g_last_read_lbl.setText(last_read_txt) + self.g_read_count_lbl.setText('Read {} times'.format(gallery.times_read)) + self.g_info_lbl.setText(gallery.info) + if gallery.link: + self.g_url_lbl.setText(gallery.link) + self.g_url_lbl.show() + else: + self.g_url_lbl.hide() + + + clearLayout(self.tags_layout) + if self.has_tags(gallery.tags): + ns_layout = QFormLayout() + self.tags_layout.addRow(ns_layout) + for namespace in sorted(gallery.tags): + tags_lbls = FlowLayout() + if namespace == 'default': + self.tags_layout.insertRow(0, tags_lbls) + else: + self.tags_layout.addRow(namespace, tags_lbls) + + for n, tag in enumerate(sorted(gallery.tags[namespace]), 1): + if namespace == 'default': + t = TagText(search_widget=self.appwindow) + else: + t = TagText(search_widget=self.appwindow, namespace=namespace) + t.setText(tag) + tags_lbls.addWidget(t) + t.setAutoFillBackground(True) + self.tags_widget.adjustSize() class Spinner(TransparentWidget): - """ - Spinner widget - """ - activated = pyqtSignal() - deactivated = pyqtSignal() - about_to_show, about_to_hide = range(2) - _OFFSET_X_TOPRIGHT = [0] - - def __init__(self, parent, position='topright'): - "Position can be: 'center', 'topright' or QPoint" - super().__init__(parent, flags=Qt.Window|Qt.FramelessWindowHint, move_listener=False) - self.setAttribute(Qt.WA_ShowWithoutActivating) - self.fps = 21 - self.border = 2 - self.line_width = 5 - self.arc_length = 100 - self.seconds_per_spin = 1 - self.text_layout = None - - self.text = '' - self._text_margin = 5 - - self._timer = QTimer(self) - self._timer.timeout.connect(self._on_timer_timeout) - - # keep track of the current start angle to avoid - # unnecessary repaints - self._start_angle = 0 - - self._offset_x_topright = self._OFFSET_X_TOPRIGHT[0] - self.margin = 10 - self._position = position - self._min_size = 0 - - self.state_timer = QTimer() - self.current_state = self.about_to_show - self.state_timer.timeout.connect(super().hide) - self.state_timer.setSingleShot(True) - - # animation - self.fade_animation = create_animation(self, 'windowOpacity') - self.fade_animation.setDuration(800) - self.fade_animation.setStartValue(0.0) - self.fade_animation.setEndValue(1.0) - self.setWindowOpacity(0.0) - self._update_layout() - self.set_size(50) - self._set_position(position) - - def _update_layout(self): - self.text_layout = text_layout(self.text, self.width()-self._text_margin, self.font(), self.fontMetrics()) - self.setFixedHeight(self._min_size+self.text_layout.boundingRect().height()) - - def set_size(self, w): - self.setFixedWidth(w) - self._min_size = w - self._update_layout() - self.update() - - def set_text(self, txt): - self.text = txt - self._update_layout() - self.update() - - def _set_position(self, new_pos): - "'center', 'topright' or QPoint" - p = self.parent_widget - - # topleft - if new_pos == "topright": - def topright(): - return QPoint(p.pos().x() + p.width() - 65 - self._offset_x_topright, p.pos().y() + p.toolbar.height() + 55) - self.move(topright()) - p.move_listener.connect(lambda: self.update_move(topright())) - - elif new_pos == "center": - p.move_listener.connect(lambda: self.update_move(QPoint(p.pos().x() + p.width() // 2, - p.pos().y() + p.height() // 2))) - - elif isinstance(new_pos, QPoint): - p.move_listener.connect(lambda: self.update_move(new_pos)) - - def paintEvent(self, event): - # call the base paint event: - super().paintEvent(event) - - painter = QPainter() - painter.begin(self) - try: - painter.setRenderHint(QPainter.Antialiasing) - - txt_rect = QRectF(0,0,0,0) - if not self.text: - txt_rect.setHeight(self.fontMetrics().height()) - - painter.save() - painter.setPen(Qt.NoPen) - painter.setBrush(QBrush(QColor(88,88,88,180))) - painter.drawRoundedRect(QRect(0,0, self.width(), self.height() - txt_rect.height()), 5, 5) - painter.restore() - - pen = QPen(QColor('#F2F2F2')) - pen.setWidth(self.line_width) - painter.setPen(pen) - - border = self.border + int(math.ceil(self.line_width / 2.0)) - r = QRectF((txt_rect.height())/2, (txt_rect.height()/2), - self.width()-txt_rect.height(), self.width()-txt_rect.height()) - r.adjust(border, border, -border, -border) - - # draw the arc: - painter.drawArc(r, -self._start_angle * 16, self.arc_length * 16) - - # draw text if there is - if self.text: - txt_rect = self.text_layout.boundingRect() - self.text_layout.draw(painter, QPointF(self._text_margin, self.height()-txt_rect.height()-self._text_margin/2)) - - r = None - - finally: - painter.end() - painter = None - - def showEvent(self, event): - if self._position == "topright": - self._OFFSET_X_TOPRIGHT[0] += + self.width() + self.margin - if not self._timer.isActive(): - self.fade_animation.start() - self.current_state = self.about_to_show - self.state_timer.stop() - self.activated.emit() - self._timer.start(1000 / max(1, self.fps)) - super().showEvent(event) - - def hideEvent(self, event): - self._timer.stop() - self.deactivated.emit() - super().hideEvent(event) - - def before_hide(self): - if self.current_state == self.about_to_hide: - return - self.current_state = self.about_to_hide - if self._position == "topright": - self._OFFSET_X_TOPRIGHT[0] -= self.width() + self.margin - self.state_timer.start(5000) - - def closeEvent(self, event): - self._timer.stop() - super().closeEvent(event) - - def _on_timer_timeout(self): - if not self.isVisible(): - return - - # calculate the spin angle as a function of the current time so that all - # spinners appear in sync! - t = time.time() - whole_seconds = int(t) - p = (whole_seconds % self.seconds_per_spin) + (t - whole_seconds) - angle = int((360 * p)/self.seconds_per_spin) - - if angle == self._start_angle: - return - - self._start_angle = angle - self.update() + """ + Spinner widget + """ + activated = pyqtSignal() + deactivated = pyqtSignal() + about_to_show, about_to_hide = range(2) + _OFFSET_X_TOPRIGHT = [0] + + def __init__(self, parent, position='topright'): + "Position can be: 'center', 'topright' or QPoint" + super().__init__(parent, flags=Qt.Window | Qt.FramelessWindowHint, move_listener=False) + self.setAttribute(Qt.WA_ShowWithoutActivating) + self.fps = 21 + self.border = 2 + self.line_width = 5 + self.arc_length = 100 + self.seconds_per_spin = 1 + self.text_layout = None + + self.text = '' + self._text_margin = 5 + + self._timer = QTimer(self) + self._timer.timeout.connect(self._on_timer_timeout) + + # keep track of the current start angle to avoid + # unnecessary repaints + self._start_angle = 0 + + self._offset_x_topright = self._OFFSET_X_TOPRIGHT[0] + self.margin = 10 + self._position = position + self._min_size = 0 + + self.state_timer = QTimer() + self.current_state = self.about_to_show + self.state_timer.timeout.connect(super().hide) + self.state_timer.setSingleShot(True) + + # animation + self.fade_animation = create_animation(self, 'windowOpacity') + self.fade_animation.setDuration(800) + self.fade_animation.setStartValue(0.0) + self.fade_animation.setEndValue(1.0) + self.setWindowOpacity(0.0) + self._update_layout() + self.set_size(50) + self._set_position(position) + + def _update_layout(self): + self.text_layout = text_layout(self.text, self.width() - self._text_margin, self.font(), self.fontMetrics()) + self.setFixedHeight(self._min_size + self.text_layout.boundingRect().height()) + + def set_size(self, w): + self.setFixedWidth(w) + self._min_size = w + self._update_layout() + self.update() + + def set_text(self, txt): + self.text = txt + self._update_layout() + self.update() + + def _set_position(self, new_pos): + "'center', 'topright' or QPoint" + p = self.parent_widget + + # topleft + if new_pos == "topright": + def topright(): + return QPoint(p.pos().x() + p.width() - 65 - self._offset_x_topright, p.pos().y() + p.toolbar.height() + 55) + self.move(topright()) + p.move_listener.connect(lambda: self.update_move(topright())) + + elif new_pos == "center": + p.move_listener.connect(lambda: self.update_move(QPoint(p.pos().x() + p.width() // 2, + p.pos().y() + p.height() // 2))) + + elif isinstance(new_pos, QPoint): + p.move_listener.connect(lambda: self.update_move(new_pos)) + + def paintEvent(self, event): + # call the base paint event: + super().paintEvent(event) + + painter = QPainter() + painter.begin(self) + try: + painter.setRenderHint(QPainter.Antialiasing) + + txt_rect = QRectF(0,0,0,0) + if not self.text: + txt_rect.setHeight(self.fontMetrics().height()) + + painter.save() + painter.setPen(Qt.NoPen) + painter.setBrush(QBrush(QColor(88,88,88,180))) + painter.drawRoundedRect(QRect(0,0, self.width(), self.height() - txt_rect.height()), 5, 5) + painter.restore() + + pen = QPen(QColor('#F2F2F2')) + pen.setWidth(self.line_width) + painter.setPen(pen) + + border = self.border + int(math.ceil(self.line_width / 2.0)) + r = QRectF((txt_rect.height()) / 2, (txt_rect.height() / 2), + self.width() - txt_rect.height(), self.width() - txt_rect.height()) + r.adjust(border, border, -border, -border) + + # draw the arc: + painter.drawArc(r, -self._start_angle * 16, self.arc_length * 16) + + # draw text if there is + if self.text: + txt_rect = self.text_layout.boundingRect() + self.text_layout.draw(painter, QPointF(self._text_margin, self.height() - txt_rect.height() - self._text_margin / 2)) + + r = None + + finally: + painter.end() + painter = None + + def showEvent(self, event): + if self._position == "topright": + self._OFFSET_X_TOPRIGHT[0] += + self.width() + self.margin + if not self._timer.isActive(): + self.fade_animation.start() + self.current_state = self.about_to_show + self.state_timer.stop() + self.activated.emit() + self._timer.start(1000 / max(1, self.fps)) + super().showEvent(event) + + def hideEvent(self, event): + self._timer.stop() + self.deactivated.emit() + super().hideEvent(event) + + def before_hide(self): + if self.current_state == self.about_to_hide: + return + self.current_state = self.about_to_hide + if self._position == "topright": + self._OFFSET_X_TOPRIGHT[0] -= self.width() + self.margin + self.state_timer.start(5000) + + def closeEvent(self, event): + self._timer.stop() + super().closeEvent(event) + + def _on_timer_timeout(self): + if not self.isVisible(): + return + + # calculate the spin angle as a function of the current time so that all + # spinners appear in sync! + t = time.time() + whole_seconds = int(t) + p = (whole_seconds % self.seconds_per_spin) + (t - whole_seconds) + angle = int((360 * p) / self.seconds_per_spin) + + if angle == self._start_angle: + return + + self._start_angle = angle + self.update() class GalleryMenu(QMenu): - delete_galleries = pyqtSignal(bool) - edit_gallery = pyqtSignal(object, object) - - def __init__(self, view, index, sort_model, app_window, selected_indexes=None): - super().__init__(app_window) - self.parent_widget = app_window - self.view = view - self.sort_model = sort_model - self.index = index - self.gallery = index.data(Qt.UserRole+1) - self.selected = selected_indexes - if self.view.view_type == app_constants.ViewType.Default: - if not self.selected: - favourite_act = self.addAction('Favorite', - lambda: self.parent_widget.manga_list_view.favorite(self.index)) - favourite_act.setCheckable(True) - if self.gallery.fav: - favourite_act.setChecked(True) - favourite_act.setText('Unfavorite') - else: - favourite_act.setChecked(False) - else: - favourite_act = self.addAction('Favorite selected', self.favourite_select) - favourite_act.setCheckable(True) - f = [] - for idx in self.selected: - if idx.data(Qt.UserRole+1).fav: - f.append(True) - else: - f.append(False) - if all(f): - favourite_act.setChecked(True) - favourite_act.setText('Unfavorite selected') - else: - favourite_act.setChecked(False) - elif self.view.view_type == app_constants.ViewType.Addition: - - send_to_lib = self.addAction('Send to library', - self.send_to_lib) - add_to_ignore = self.addAction('Ignore and remove', - self.add_to_ignore) - self.addSeparator() - if not self.selected and isinstance(view, QTableView): - chapters_menu = self.addAction('Chapters') - open_chapters = QMenu(self) - chapters_menu.setMenu(open_chapters) - for number, chap in enumerate(self.gallery.chapters, 1): - chap_action = QAction("Open chapter {}".format(number), - open_chapters, - triggered = functools.partial(chap.open)) - open_chapters.addAction(chap_action) - if self.selected: - open_f_chapters = self.addAction('Open first chapters', self.open_first_chapters) - - if self.view.view_type != app_constants.ViewType.Duplicate: - if not self.selected: - add_chapters = self.addAction('Add chapters', self.add_chapters) - if self.view.view_type == app_constants.ViewType.Default: - add_to_list_txt = "Add selected to list" if self.selected else "Add to list" - add_to_list = self.addAction(add_to_list_txt) - add_to_list_menu = QMenu(self) - add_to_list.setMenu(add_to_list_menu) - for g_list in sorted(app_constants.GALLERY_LISTS): - add_to_list_menu.addAction(g_list.name, functools.partial(self.add_to_list, g_list)) - self.addSeparator() - if not self.selected: - get_metadata = self.addAction('Get metadata', - lambda: self.parent_widget.get_metadata(index.data(Qt.UserRole+1))) - else: - gals = [] - for idx in self.selected: - gals.append(idx.data(Qt.UserRole+1)) - get_select_metadata = self.addAction('Get metadata for selected', - lambda: self.parent_widget.get_metadata(gals)) - self.addSeparator() - edit = self.addAction('Edit', lambda: self.edit_gallery.emit(self.parent_widget, - self.index.data(Qt.UserRole+1) if not self.selected else [idx.data(Qt.UserRole+1) for idx in self.selected])) - if not self.selected: - text = 'folder' if not self.index.data(Qt.UserRole+1).is_archive else 'archive' - op_folder_act = self.addAction('Open {}'.format(text), self.op_folder) - op_cont_folder_act = self.addAction('Show in folder', lambda: self.op_folder(containing=True)) - else: - text = 'folders' if not self.index.data(Qt.UserRole+1).is_archive else 'archives' - op_folder_select = self.addAction('Open {}'.format(text), lambda: self.op_folder(True)) - op_cont_folder_select = self.addAction('Show in folders', lambda: self.op_folder(True, True)) - - if self.index.data(Qt.UserRole+1).link and not self.selected: - op_link = self.addAction('Open URL', self.op_link) - if self.selected and all([idx.data(Qt.UserRole+1).link for idx in self.selected]): - op_links = self.addAction('Open URLs', lambda: self.op_link(True)) - - remove_act = self.addAction('Remove') - remove_menu = QMenu(self) - remove_act.setMenu(remove_menu) - if self.view.view_type == app_constants.ViewType.Default: - if self.sort_model.current_gallery_list: - remove_f_g_list_txt = "Remove selected from list" if self.selected else "Remove from list" - remove_f_g_list = remove_menu.addAction(remove_f_g_list_txt, self.remove_from_list) - if not self.selected: - remove_g = remove_menu.addAction('Remove gallery', - lambda: self.delete_galleries.emit(False)) - remove_ch = remove_menu.addAction('Remove chapter') - remove_ch_menu = QMenu(self) - remove_ch.setMenu(remove_ch_menu) - for number, chap_number in enumerate(range(len( - self.index.data(Qt.UserRole+1).chapters)), 1): - chap_action = QAction("Remove chapter {}".format(number), - remove_ch_menu, - triggered = functools.partial( - self.parent_widget.manga_list_view.del_chapter, - index, - chap_number)) - remove_ch_menu.addAction(chap_action) - else: - remove_select_g = remove_menu.addAction('Remove selected', lambda: self.delete_galleries.emit(False)) - remove_menu.addSeparator() - if not self.selected: - remove_source_g = remove_menu.addAction('Remove and delete files', - lambda: self.delete_galleries.emit(True)) - else: - remove_source_select_g = remove_menu.addAction('Remove selected and delete files', - lambda: self.delete_galleries.emit(True)) - self.addSeparator() - advanced = self.addAction('Advanced') - adv_menu = QMenu(self) - advanced.setMenu(adv_menu) - if not self.selected: - change_cover = adv_menu.addAction('Change cover...', self.change_cover) - - if self.selected: - allow_metadata_count = 0 - for i in self.selected: - if i.data(Qt.UserRole+1).exed: - allow_metadata_count += 1 - self.allow_metadata_exed = allow_metadata_count >= len(self.selected)//2 - else: - self.allow_metadata_exed = False if not self.gallery.exed else True - - if self.selected: - allow_metadata_txt = "Include selected in auto metadata fetch" if self.allow_metadata_exed else "Exclude selected in auto metadata fetch" - else: - allow_metadata_txt = "Include in auto metadata fetch" if self.allow_metadata_exed else "Exclude in auto metadata fetch" - adv_menu.addAction(allow_metadata_txt, self.allow_metadata_fetch) - - def add_to_ignore(self): - if self.selected: - gs = self.selected - else: - gs = [self.index] - galleries = [idx.data(Qt.UserRole+1) for idx in gs] - - paths = set() - for g in galleries: - for chap in g.chapters: - if not chap.in_archive: - paths.add(chap.path) - else: - paths.add(g.path) - app_constants.IGNORE_PATHS.extend(paths) - - settings.set(app_constants.IGNORE_PATHS, 'Application', 'ignore paths') - self.delete_galleries.emit(False) - - def send_to_lib(self): - if self.selected: - gs = self.selected - else: - gs = [self.index] - galleries = [idx.data(Qt.UserRole+1) for idx in gs] - rows = len(galleries) - self.view.gallery_model._gallery_to_remove.extend(galleries) - self.view.gallery_model.removeRows(self.view.gallery_model.rowCount()-rows, rows) - self.parent_widget.default_manga_view.add_gallery(galleries) - for g in galleries: - gallerydb.execute(gallerydb.GalleryDB.modify_gallery, - True, g.id, view=g.view) - - def allow_metadata_fetch(self): - exed = 0 if self.allow_metadata_exed else 1 - if self.selected: - for idx in self.selected: - g = idx.data(Qt.UserRole+1) - g.exed = exed - gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, g.id, {'exed':exed}) - else: - self.gallery.exed = exed - gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, self.gallery.id, {'exed':exed}) - - def add_to_list(self, g_list): - galleries = [] - if self.selected: - for idx in self.selected: - galleries.append(idx.data(Qt.UserRole+1)) - else: - galleries.append(self.gallery) - g_list.add_gallery(galleries) - - def remove_from_list(self): - g_list = self.sort_model.current_gallery_list - if self.selected: - g_ids = [] - for idx in self.selected: - g_ids.append(idx.data(Qt.UserRole+1).id) - else: - g_ids = self.gallery.id - self.sort_model.current_gallery_list.remove_gallery(g_ids) - self.sort_model.init_search(self.sort_model.current_term) - - def favourite_select(self): - for idx in self.selected: - self.parent_widget.manga_list_view.favorite(idx) - - def change_cover(self): - gallery = self.index.data(Qt.UserRole+1) - log_i('Attempting to change cover of {}'.format(gallery.title.encode(errors='ignore'))) - if gallery.is_archive: - try: - zip = utils.ArchiveFile(gallery.path) - except utils.app_constants.CreateArchiveFail: - app_constants.NOTIF_BAR.add_text('Attempt to change cover failed. Could not create archive.') - return - path = zip.extract_all() - else: - path = gallery.path - - new_cover = QFileDialog.getOpenFileName(self, - 'Select a new gallery cover', - filter='Image {}'.format(utils.IMG_FILTER), - directory=path)[0] - if new_cover and new_cover.lower().endswith(utils.IMG_FILES): - gallerydb.GalleryDB.clear_thumb(gallery.profile) - Executors.generate_thumbnail(gallery, img=new_cover, on_method=gallery.set_profile) - gallery.reset_profile() - log_i('Changed cover successfully!') - - def open_first_chapters(self): - txt = "Opening first chapters of selected galleries" - app_constants.STAT_MSG_METHOD(txt) - for idx in self.selected: - idx.data(Qt.UserRole+1).chapters[0].open(False) - - def op_link(self, select=False): - if select: - for x in self.selected: - gal = x.data(Qt.UserRole+1) - utils.open_web_link(gal.link) - else: - utils.open_web_link(self.index.data(Qt.UserRole+1).link) - - - def op_folder(self, select=False, containing=False): - if select: - for x in self.selected: - text = 'Opening archives...' if self.index.data(Qt.UserRole+1).is_archive else 'Opening folders...' - text = 'Opening containing folders...' if containing else text - self.view.STATUS_BAR_MSG.emit(text) - gal = x.data(Qt.UserRole+1) - path = os.path.split(gal.path)[0] if containing else gal.path - if containing: - utils.open_path(path, gal.path) - else: - utils.open_path(path) - else: - text = 'Opening archive...' if self.index.data(Qt.UserRole+1).is_archive else 'Opening folder...' - text = 'Opening containing folder...' if containing else text - self.view.STATUS_BAR_MSG.emit(text) - gal = self.index.data(Qt.UserRole+1) - path = os.path.split(gal.path)[0] if containing else gal.path - if containing: - utils.open_path(path, gal.path) - else: - utils.open_path(path) - - - def add_chapters(self): - def add_chdb(chaps_container): - gallery = self.index.data(Qt.UserRole+1) - log_i('Adding new chapter for {}'.format(gallery.title.encode(errors='ignore'))) - gallerydb.execute(gallerydb.ChapterDB.add_chapters_raw, False, gallery.id, chaps_container) - ch_widget = ChapterAddWidget(self.index.data(Qt.UserRole+1), self.parent_widget) - ch_widget.CHAPTERS.connect(add_chdb) - ch_widget.show() + delete_galleries = pyqtSignal(bool) + edit_gallery = pyqtSignal(object, object) + + def __init__(self, view, index, sort_model, app_window, selected_indexes=None): + super().__init__(app_window) + self.parent_widget = app_window + self.view = view + self.sort_model = sort_model + self.index = index + self.gallery = index.data(Qt.UserRole + 1) + self.selected = selected_indexes + if self.view.view_type == app_constants.ViewType.Default: + if not self.selected: + favourite_act = self.addAction('Favorite', + lambda: self.parent_widget.manga_list_view.favorite(self.index)) + favourite_act.setCheckable(True) + if self.gallery.fav: + favourite_act.setChecked(True) + favourite_act.setText('Unfavorite') + else: + favourite_act.setChecked(False) + else: + favourite_act = self.addAction('Favorite selected', self.favourite_select) + favourite_act.setCheckable(True) + f = [] + for idx in self.selected: + if idx.data(Qt.UserRole + 1).fav: + f.append(True) + else: + f.append(False) + if all(f): + favourite_act.setChecked(True) + favourite_act.setText('Unfavorite selected') + else: + favourite_act.setChecked(False) + elif self.view.view_type == app_constants.ViewType.Addition: + + send_to_lib = self.addAction('Send to library', + self.send_to_lib) + add_to_ignore = self.addAction('Ignore and remove', + self.add_to_ignore) + self.addSeparator() + if not self.selected and isinstance(view, QTableView): + chapters_menu = self.addAction('Chapters') + open_chapters = QMenu(self) + chapters_menu.setMenu(open_chapters) + for number, chap in enumerate(self.gallery.chapters, 1): + chap_action = QAction("Open chapter {}".format(number), + open_chapters, + triggered = functools.partial(chap.open)) + open_chapters.addAction(chap_action) + if self.selected: + open_f_chapters = self.addAction('Open first chapters', self.open_first_chapters) + + if self.view.view_type != app_constants.ViewType.Duplicate: + if not self.selected: + add_chapters = self.addAction('Add chapters', self.add_chapters) + if self.view.view_type == app_constants.ViewType.Default: + add_to_list_txt = "Add selected to list" if self.selected else "Add to list" + add_to_list = self.addAction(add_to_list_txt) + add_to_list_menu = QMenu(self) + add_to_list.setMenu(add_to_list_menu) + for g_list in sorted(app_constants.GALLERY_LISTS): + add_to_list_menu.addAction(g_list.name, functools.partial(self.add_to_list, g_list)) + self.addSeparator() + if not self.selected: + get_metadata = self.addAction('Get metadata', + lambda: self.parent_widget.get_metadata(index.data(Qt.UserRole + 1))) + else: + gals = [] + for idx in self.selected: + gals.append(idx.data(Qt.UserRole + 1)) + get_select_metadata = self.addAction('Get metadata for selected', + lambda: self.parent_widget.get_metadata(gals)) + self.addSeparator() + edit = self.addAction('Edit', lambda: self.edit_gallery.emit(self.parent_widget, + self.index.data(Qt.UserRole + 1) if not self.selected else [idx.data(Qt.UserRole + 1) for idx in self.selected])) + if not self.selected: + text = 'folder' if not self.index.data(Qt.UserRole + 1).is_archive else 'archive' + op_folder_act = self.addAction('Open {}'.format(text), self.op_folder) + op_cont_folder_act = self.addAction('Show in folder', lambda: self.op_folder(containing=True)) + else: + text = 'folders' if not self.index.data(Qt.UserRole + 1).is_archive else 'archives' + op_folder_select = self.addAction('Open {}'.format(text), lambda: self.op_folder(True)) + op_cont_folder_select = self.addAction('Show in folders', lambda: self.op_folder(True, True)) + + if self.index.data(Qt.UserRole + 1).link and not self.selected: + op_link = self.addAction('Open URL', self.op_link) + if self.selected and all([idx.data(Qt.UserRole + 1).link for idx in self.selected]): + op_links = self.addAction('Open URLs', lambda: self.op_link(True)) + + remove_act = self.addAction('Remove') + remove_menu = QMenu(self) + remove_act.setMenu(remove_menu) + if self.view.view_type == app_constants.ViewType.Default: + if self.sort_model.current_gallery_list: + remove_f_g_list_txt = "Remove selected from list" if self.selected else "Remove from list" + remove_f_g_list = remove_menu.addAction(remove_f_g_list_txt, self.remove_from_list) + if not self.selected: + remove_g = remove_menu.addAction('Remove gallery', + lambda: self.delete_galleries.emit(False)) + remove_ch = remove_menu.addAction('Remove chapter') + remove_ch_menu = QMenu(self) + remove_ch.setMenu(remove_ch_menu) + for number, chap_number in enumerate(range(len(self.index.data(Qt.UserRole + 1).chapters)), 1): + chap_action = QAction("Remove chapter {}".format(number), + remove_ch_menu, + triggered = functools.partial(self.parent_widget.manga_list_view.del_chapter, + index, + chap_number)) + remove_ch_menu.addAction(chap_action) + else: + remove_select_g = remove_menu.addAction('Remove selected', lambda: self.delete_galleries.emit(False)) + remove_menu.addSeparator() + if not self.selected: + remove_source_g = remove_menu.addAction('Remove and delete files', + lambda: self.delete_galleries.emit(True)) + else: + remove_source_select_g = remove_menu.addAction('Remove selected and delete files', + lambda: self.delete_galleries.emit(True)) + self.addSeparator() + advanced = self.addAction('Advanced') + adv_menu = QMenu(self) + advanced.setMenu(adv_menu) + if not self.selected: + change_cover = adv_menu.addAction('Change cover...', self.change_cover) + + if self.selected: + allow_metadata_count = 0 + for i in self.selected: + if i.data(Qt.UserRole + 1).exed: + allow_metadata_count += 1 + self.allow_metadata_exed = allow_metadata_count >= len(self.selected) // 2 + else: + self.allow_metadata_exed = False if not self.gallery.exed else True + + if self.selected: + allow_metadata_txt = "Include selected in auto metadata fetch" if self.allow_metadata_exed else "Exclude selected in auto metadata fetch" + else: + allow_metadata_txt = "Include in auto metadata fetch" if self.allow_metadata_exed else "Exclude in auto metadata fetch" + adv_menu.addAction(allow_metadata_txt, self.allow_metadata_fetch) + + def add_to_ignore(self): + if self.selected: + gs = self.selected + else: + gs = [self.index] + galleries = [idx.data(Qt.UserRole + 1) for idx in gs] + + paths = set() + for g in galleries: + for chap in g.chapters: + if not chap.in_archive: + paths.add(chap.path) + else: + paths.add(g.path) + app_constants.IGNORE_PATHS.extend(paths) + + settings.set(app_constants.IGNORE_PATHS, 'Application', 'ignore paths') + self.delete_galleries.emit(False) + + def send_to_lib(self): + if self.selected: + gs = self.selected + else: + gs = [self.index] + galleries = [idx.data(Qt.UserRole + 1) for idx in gs] + rows = len(galleries) + self.view.gallery_model._gallery_to_remove.extend(galleries) + self.view.gallery_model.removeRows(self.view.gallery_model.rowCount() - rows, rows) + self.parent_widget.default_manga_view.add_gallery(galleries) + for g in galleries: + gallerydb.execute(gallerydb.GalleryDB.modify_gallery, + True, g.id, view=g.view) + + def allow_metadata_fetch(self): + exed = 0 if self.allow_metadata_exed else 1 + if self.selected: + for idx in self.selected: + g = idx.data(Qt.UserRole + 1) + g.exed = exed + gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, g.id, {'exed':exed}) + else: + self.gallery.exed = exed + gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, self.gallery.id, {'exed':exed}) + + def add_to_list(self, g_list): + galleries = [] + if self.selected: + for idx in self.selected: + galleries.append(idx.data(Qt.UserRole + 1)) + else: + galleries.append(self.gallery) + g_list.add_gallery(galleries) + + def remove_from_list(self): + g_list = self.sort_model.current_gallery_list + if self.selected: + g_ids = [] + for idx in self.selected: + g_ids.append(idx.data(Qt.UserRole + 1).id) + else: + g_ids = self.gallery.id + self.sort_model.current_gallery_list.remove_gallery(g_ids) + self.sort_model.init_search(self.sort_model.current_term) + + def favourite_select(self): + for idx in self.selected: + self.parent_widget.manga_list_view.favorite(idx) + + def change_cover(self): + gallery = self.index.data(Qt.UserRole + 1) + log_i('Attempting to change cover of {}'.format(gallery.title.encode(errors='ignore'))) + if gallery.is_archive: + try: + zip = utils.ArchiveFile(gallery.path) + except utils.app_constants.CreateArchiveFail: + app_constants.NOTIF_BAR.add_text('Attempt to change cover failed. Could not create archive.') + return + path = zip.extract_all() + else: + path = gallery.path + + new_cover = QFileDialog.getOpenFileName(self, + 'Select a new gallery cover', + filter='Image {}'.format(utils.IMG_FILTER), + directory=path)[0] + if new_cover and new_cover.lower().endswith(utils.IMG_FILES): + gallerydb.GalleryDB.clear_thumb(gallery.profile) + Executors.generate_thumbnail(gallery, img=new_cover, on_method=gallery.set_profile) + gallery.reset_profile() + log_i('Changed cover successfully!') + + def open_first_chapters(self): + txt = "Opening first chapters of selected galleries" + app_constants.STAT_MSG_METHOD(txt) + for idx in self.selected: + idx.data(Qt.UserRole + 1).chapters[0].open(False) + + def op_link(self, select=False): + if select: + for x in self.selected: + gal = x.data(Qt.UserRole + 1) + utils.open_web_link(gal.link) + else: + utils.open_web_link(self.index.data(Qt.UserRole + 1).link) + + + def op_folder(self, select=False, containing=False): + if select: + for x in self.selected: + text = 'Opening archives...' if self.index.data(Qt.UserRole + 1).is_archive else 'Opening folders...' + text = 'Opening containing folders...' if containing else text + self.view.STATUS_BAR_MSG.emit(text) + gal = x.data(Qt.UserRole + 1) + path = os.path.split(gal.path)[0] if containing else gal.path + if containing: + utils.open_path(path, gal.path) + else: + utils.open_path(path) + else: + text = 'Opening archive...' if self.index.data(Qt.UserRole + 1).is_archive else 'Opening folder...' + text = 'Opening containing folder...' if containing else text + self.view.STATUS_BAR_MSG.emit(text) + gal = self.index.data(Qt.UserRole + 1) + path = os.path.split(gal.path)[0] if containing else gal.path + if containing: + utils.open_path(path, gal.path) + else: + utils.open_path(path) + + + def add_chapters(self): + def add_chdb(chaps_container): + gallery = self.index.data(Qt.UserRole + 1) + log_i('Adding new chapter for {}'.format(gallery.title.encode(errors='ignore'))) + gallerydb.execute(gallerydb.ChapterDB.add_chapters_raw, False, gallery.id, chaps_container) + ch_widget = ChapterAddWidget(self.index.data(Qt.UserRole + 1), self.parent_widget) + ch_widget.CHAPTERS.connect(add_chdb) + ch_widget.show() class SystemTray(QSystemTrayIcon): - """ - Pass True to minimized arg in showMessage method to only - show message if application is minimized. - """ - def __init__(self, icon, parent=None): - super().__init__(icon, parent=None) - self.parent_widget = parent - - def showMessage(self, title, msg, icon=QSystemTrayIcon.Information, - msecs=10000, minimized=False): - # NOTE: Crashes on linux - # TODO: Fix this!! - if not app_constants.OS_NAME == "linux": - if minimized: - if self.parent_widget.isMinimized() or not self.parent_widget.isActiveWindow(): - return super().showMessage(title, msg, icon, msecs) - else: - return super().showMessage(title, msg, icon, msecs) + """ + Pass True to minimized arg in showMessage method to only + show message if application is minimized. + """ + def __init__(self, icon, parent=None): + super().__init__(icon, parent=None) + self.parent_widget = parent + + def showMessage(self, title, msg, icon=QSystemTrayIcon.Information, + msecs=10000, minimized=False): + # NOTE: Crashes on linux + # TODO: Fix this!! + if not app_constants.OS_NAME == "linux": + if minimized: + if self.parent_widget.isMinimized() or not self.parent_widget.isActiveWindow(): + return super().showMessage(title, msg, icon, msecs) + else: + return super().showMessage(title, msg, icon, msecs) class ClickedLabel(QLabel): - """ - A QLabel which emits clicked signal on click - """ - clicked = pyqtSignal(str) - def __init__(self, s="", **kwargs): - super().__init__(s, **kwargs) - self.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) - - def enterEvent(self, event): - if self.text(): - self.setCursor(Qt.PointingHandCursor) - else: - self.setCursor(Qt.ArrowCursor) - return super().enterEvent(event) - - def mousePressEvent(self, event): - self.clicked.emit(self.text()) - return super().mousePressEvent(event) + """ + A QLabel which emits clicked signal on click + """ + clicked = pyqtSignal(str) + def __init__(self, s="", **kwargs): + super().__init__(s, **kwargs) + self.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) + + def enterEvent(self, event): + if self.text(): + self.setCursor(Qt.PointingHandCursor) + else: + self.setCursor(Qt.ArrowCursor) + return super().enterEvent(event) + + def mousePressEvent(self, event): + self.clicked.emit(self.text()) + return super().mousePressEvent(event) class TagText(QPushButton): - def __init__(self, *args, **kwargs): - self.search_widget = kwargs.pop('search_widget', None) - self.namespace = kwargs.pop('namespace', None) - super().__init__(*args, **kwargs) - if self.search_widget: - if self.namespace: - self.clicked.connect(lambda: self.search_widget.search('{}:{}'.format(self.namespace, self.text()))) - else: - self.clicked.connect(lambda: self.search_widget.search('{}'.format(self.text()))) - - def enterEvent(self, event): - if self.text(): - self.setCursor(Qt.PointingHandCursor) - else: - self.setCursor(Qt.ArrowCursor) - return super().enterEvent(event) + def __init__(self, *args, **kwargs): + self.search_widget = kwargs.pop('search_widget', None) + self.namespace = kwargs.pop('namespace', None) + super().__init__(*args, **kwargs) + if self.search_widget: + if self.namespace: + self.clicked.connect(lambda: self.search_widget.search('{}:{}'.format(self.namespace, self.text()))) + else: + self.clicked.connect(lambda: self.search_widget.search('{}'.format(self.text()))) + + def enterEvent(self, event): + if self.text(): + self.setCursor(Qt.PointingHandCursor) + else: + self.setCursor(Qt.ArrowCursor) + return super().enterEvent(event) class BasePopup(TransparentWidget): - graphics_blur = None - def __init__(self, parent=None, **kwargs): - blur = True - if kwargs: - blur = kwargs.pop('blur', True) - super().__init__(parent, **kwargs) - else: - super().__init__(parent, flags= Qt.Dialog | Qt.FramelessWindowHint) - main_layout = QVBoxLayout() - self.main_widget = QFrame() - self.main_widget.setFrameStyle(QFrame.StyledPanel) - self.setLayout(main_layout) - main_layout.addWidget(self.main_widget) - self.generic_buttons = QHBoxLayout() - self.generic_buttons.addWidget(Spacer('h')) - self.yes_button = QPushButton('Yes') - self.no_button = QPushButton('No') - self.buttons_layout = QHBoxLayout() - self.buttons_layout.addWidget(Spacer('h'), 3) - self.generic_buttons.addWidget(self.yes_button) - self.generic_buttons.addWidget(self.no_button) - self.setMaximumWidth(500) - self.resize(500,350) - self.curr_pos = QPoint() - if parent and blur: - try: - self.graphics_blur = parent.graphics_blur - parent.setGraphicsEffect(self.graphics_blur) - except AttributeError: - pass - - # animation - self.fade_animation = create_animation(self, 'windowOpacity') - self.fade_animation.setDuration(800) - self.fade_animation.setStartValue(0.0) - self.fade_animation.setEndValue(1.0) - self.setWindowOpacity(0.0) - - def mousePressEvent(self, event): - self.curr_pos = event.pos() - return super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - if event.buttons() == Qt.LeftButton: - diff = event.pos() - self.curr_pos - newpos = self.pos()+diff - self.move(newpos) - return super().mouseMoveEvent(event) - - def showEvent(self, event): - self.activateWindow() - self.fade_animation.start() - if self.graphics_blur: - self.graphics_blur.setEnabled(True) - return super().showEvent(event) - - def closeEvent(self, event): - if self.graphics_blur: - self.graphics_blur.setEnabled(False) - return super().closeEvent(event) - - def hideEvent(self, event): - if self.graphics_blur: - self.graphics_blur.setEnabled(False) - return super().hideEvent(event) - - def add_buttons(self, *args): - """ - Pass names of buttons, from right to left. - Returns list of buttons in same order as they came in. - Note: Remember to add buttons_layout to main layout! - """ - b = [] - for name in args: - button = QPushButton(name) - self.buttons_layout.addWidget(button) - b.append(button) - return b + graphics_blur = None + def __init__(self, parent=None, **kwargs): + blur = True + if kwargs: + blur = kwargs.pop('blur', True) + super().__init__(parent, **kwargs) + else: + super().__init__(parent, flags= Qt.Dialog | Qt.FramelessWindowHint) + main_layout = QVBoxLayout() + self.main_widget = QFrame() + self.main_widget.setFrameStyle(QFrame.StyledPanel) + self.setLayout(main_layout) + main_layout.addWidget(self.main_widget) + self.generic_buttons = QHBoxLayout() + self.generic_buttons.addWidget(Spacer('h')) + self.yes_button = QPushButton('Yes') + self.no_button = QPushButton('No') + self.buttons_layout = QHBoxLayout() + self.buttons_layout.addWidget(Spacer('h'), 3) + self.generic_buttons.addWidget(self.yes_button) + self.generic_buttons.addWidget(self.no_button) + self.setMaximumWidth(500) + self.resize(500,350) + self.curr_pos = QPoint() + if parent and blur: + try: + self.graphics_blur = parent.graphics_blur + parent.setGraphicsEffect(self.graphics_blur) + except AttributeError: + pass + + # animation + self.fade_animation = create_animation(self, 'windowOpacity') + self.fade_animation.setDuration(800) + self.fade_animation.setStartValue(0.0) + self.fade_animation.setEndValue(1.0) + self.setWindowOpacity(0.0) + + def mousePressEvent(self, event): + self.curr_pos = event.pos() + return super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + if event.buttons() == Qt.LeftButton: + diff = event.pos() - self.curr_pos + newpos = self.pos() + diff + self.move(newpos) + return super().mouseMoveEvent(event) + + def showEvent(self, event): + self.activateWindow() + self.fade_animation.start() + if self.graphics_blur: + self.graphics_blur.setEnabled(True) + return super().showEvent(event) + + def closeEvent(self, event): + if self.graphics_blur: + self.graphics_blur.setEnabled(False) + return super().closeEvent(event) + + def hideEvent(self, event): + if self.graphics_blur: + self.graphics_blur.setEnabled(False) + return super().hideEvent(event) + + def add_buttons(self, *args): + """ + Pass names of buttons, from right to left. + Returns list of buttons in same order as they came in. + Note: Remember to add buttons_layout to main layout! + """ + b = [] + for name in args: + button = QPushButton(name) + self.buttons_layout.addWidget(button) + b.append(button) + return b class AppBubble(BasePopup): - "For application notifications" - def __init__(self, parent): - super().__init__(parent, flags= Qt.Window | Qt.FramelessWindowHint, blur=False) - self.hide_timer = QTimer(self) - self.hide_timer.timeout.connect(self.hide) - self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) - main_layout = QVBoxLayout(self.main_widget) - self.title = QLabel() - self.title.setTextFormat(Qt.RichText) - main_layout.addWidget(self.title) - self.content = QLabel() - self.content.setWordWrap(True) - self.content.setTextFormat(Qt.RichText) - self.content.setOpenExternalLinks(True) - main_layout.addWidget(self.content) - self.adjustSize() - - def update_text(self, title, txt='', duration=20): - "Duration in seconds!" - if self.hide_timer.isActive(): - self.hide_timer.stop() - self.title.setText('

{}

'.format(title)) - self.content.setText(txt) - self.hide_timer.start(duration*1000) - self.show() - self.adjustSize() - self.update_move() - - def update_move(self): - if self.parent_widget: - tl = self.parent_widget.geometry().topLeft() - x = tl.x() + self.parent_widget.width() - self.width() - 10 - y = tl.y() + self.parent_widget.height() - self.height() - self.parent_widget.statusBar().height() - self.move(x, y) - - def mousePressEvent(self, event): - if event.button() == Qt.RightButton: - self.close() - super().mousePressEvent(event) + "For application notifications" + def __init__(self, parent): + super().__init__(parent, flags= Qt.Window | Qt.FramelessWindowHint, blur=False) + self.hide_timer = QTimer(self) + self.hide_timer.timeout.connect(self.hide) + self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) + main_layout = QVBoxLayout(self.main_widget) + self.title = QLabel() + self.title.setTextFormat(Qt.RichText) + main_layout.addWidget(self.title) + self.content = QLabel() + self.content.setWordWrap(True) + self.content.setTextFormat(Qt.RichText) + self.content.setOpenExternalLinks(True) + main_layout.addWidget(self.content) + self.adjustSize() + + def update_text(self, title, txt='', duration=20): + "Duration in seconds!" + if self.hide_timer.isActive(): + self.hide_timer.stop() + self.title.setText('

{}

'.format(title)) + self.content.setText(txt) + self.hide_timer.start(duration * 1000) + self.show() + self.adjustSize() + self.update_move() + + def update_move(self): + if self.parent_widget: + tl = self.parent_widget.geometry().topLeft() + x = tl.x() + self.parent_widget.width() - self.width() - 10 + y = tl.y() + self.parent_widget.height() - self.height() - self.parent_widget.statusBar().height() + self.move(x, y) + + def mousePressEvent(self, event): + if event.button() == Qt.RightButton: + self.close() + super().mousePressEvent(event) class AppDialog(BasePopup): - # modes - PROGRESS, MESSAGE = range(2) - closing_down = pyqtSignal() - - def __init__(self, parent, mode=PROGRESS): - self.mode = mode - if mode == self.MESSAGE: - super().__init__(parent, flags=Qt.Dialog) - else: - super().__init__(parent) - self.parent_widget = parent - main_layout = QVBoxLayout() - - self.info_lbl = QLabel() - self.info_lbl.setAlignment(Qt.AlignCenter) - main_layout.addWidget(self.info_lbl) - if mode == self.PROGRESS: - self.info_lbl.setText("Updating your galleries to newest version...") - self.info_lbl.setWordWrap(True) - class progress(QProgressBar): - reached_maximum = pyqtSignal() - def __init__(self, parent=None): - super().__init__(parent) - - def setValue(self, v): - if v == self.maximum(): - self.reached_maximum.emit() - return super().setValue(v) - - self.prog = progress(self) - - self.prog.reached_maximum.connect(self.close) - main_layout.addWidget(self.prog) - self.note_info = QLabel("Note: This popup will close itself when everything is ready") - self.note_info.setAlignment(Qt.AlignCenter) - self.restart_info = QLabel("Please wait.. It is safe to restart if there is no sign of progress.") - self.restart_info.setAlignment(Qt.AlignCenter) - main_layout.addWidget(self.note_info) - main_layout.addWidget(self.restart_info) - elif mode == self.MESSAGE: - self.info_lbl.setText("An exception has ben encountered.\nContact the developer to get this fixed."+ - "\nStability from this point onward cannot be guaranteed.") - self.setWindowTitle("It was too big!") - - self.main_widget.setLayout(main_layout) - self.adjustSize() - - def closeEvent(self, event): - self.parent_widget.setEnabled(True) - if self.mode == self.MESSAGE: - self.closing_down.emit() - return super().closeEvent(event) - else: - return super().closeEvent(event) - - def showEvent(self, event): - self.parent_widget.setEnabled(False) - return super().showEvent(event) - - def init_restart(self): - if self.mode == self.PROGRESS: - self.prog.hide() - self.note_info.hide() - self.restart_info.hide() - log_i('Application requires restart') - self.note_info.setText("Application requires restart!") + # modes + PROGRESS, MESSAGE = range(2) + closing_down = pyqtSignal() + + def __init__(self, parent, mode=PROGRESS): + self.mode = mode + if mode == self.MESSAGE: + super().__init__(parent, flags=Qt.Dialog) + else: + super().__init__(parent) + self.parent_widget = parent + main_layout = QVBoxLayout() + + self.info_lbl = QLabel() + self.info_lbl.setAlignment(Qt.AlignCenter) + main_layout.addWidget(self.info_lbl) + if mode == self.PROGRESS: + self.info_lbl.setText("Updating your galleries to newest version...") + self.info_lbl.setWordWrap(True) + class progress(QProgressBar): + reached_maximum = pyqtSignal() + def __init__(self, parent=None): + super().__init__(parent) + + def setValue(self, v): + if v == self.maximum(): + self.reached_maximum.emit() + return super().setValue(v) + + self.prog = progress(self) + + self.prog.reached_maximum.connect(self.close) + main_layout.addWidget(self.prog) + self.note_info = QLabel("Note: This popup will close itself when everything is ready") + self.note_info.setAlignment(Qt.AlignCenter) + self.restart_info = QLabel("Please wait.. It is safe to restart if there is no sign of progress.") + self.restart_info.setAlignment(Qt.AlignCenter) + main_layout.addWidget(self.note_info) + main_layout.addWidget(self.restart_info) + elif mode == self.MESSAGE: + self.info_lbl.setText("An exception has ben encountered.\nContact the developer to get this fixed." + "\nStability from this point onward cannot be guaranteed.") + self.setWindowTitle("It was too big!") + + self.main_widget.setLayout(main_layout) + self.adjustSize() + + def closeEvent(self, event): + self.parent_widget.setEnabled(True) + if self.mode == self.MESSAGE: + self.closing_down.emit() + return super().closeEvent(event) + else: + return super().closeEvent(event) + + def showEvent(self, event): + self.parent_widget.setEnabled(False) + return super().showEvent(event) + + def init_restart(self): + if self.mode == self.PROGRESS: + self.prog.hide() + self.note_info.hide() + self.restart_info.hide() + log_i('Application requires restart') + self.note_info.setText("Application requires restart!") class NotificationOverlay(QWidget): - """ - A notifaction bar - """ - clicked = pyqtSignal() - _show_signal = pyqtSignal() - _hide_signal = pyqtSignal() - _unset_cursor = pyqtSignal() - _set_cursor = pyqtSignal(object) - def __init__(self, parent=None): - super().__init__(parent) - self._main_layout = QHBoxLayout(self) - self._default_height = 20 - self._dynamic_height = 0 - self._lbl = QLabel() - self._main_layout.addWidget(self._lbl) - self._lbl.setAlignment(Qt.AlignCenter) - self.setAutoFillBackground(True) - self.setBackgroundRole(self.palette().Shadow) - self.setContentsMargins(-10,-10,-10,-10) - self._click = False - self._override_hide = False - self.text_queue = [] - - self.slide_animation = create_animation(self, 'minimumHeight') - self.slide_animation.setDuration(500) - self.slide_animation.setStartValue(0) - self.slide_animation.setEndValue(self._default_height) - self.slide_animation.valueChanged.connect(self.set_dynamic_height) - self._show_signal.connect(self.show) - self._hide_signal.connect(self.hide) - self._unset_cursor.connect(self.unsetCursor) - self._set_cursor.connect(self.setCursor) - - def set_dynamic_height(self, h): - self._dynamic_height = h - - def mousePressEvent(self, event): - if self._click: - self.clicked.emit() - return super().mousePressEvent(event) - - def set_clickable(self, d=True): - self._click = d - self._set_cursor.emit(Qt.PointingHandCursor) - - def resize(self, x, y=0): - return super().resize(x, self._dynamic_height) - - def add_text(self, text, autohide=True): - """ - Add new text to the bar, deleting the previous one - """ - try: - self._reset() - except TypeError: - pass - if not self.isVisible(): - self._show_signal.emit() - self._lbl.setText(text) - if autohide: - if not self._override_hide: - threading.Timer(10, self._hide_signal.emit).start() - - def begin_show(self): - """ - Control how long you will show notification bar. - end_show() must be called to hide the bar. - """ - self._override_hide = True - self._show_signal.emit() - - def end_show(self): - self._override_hide = False - QTimer.singleShot(5000, self._hide_signal.emit) - - def _reset(self): - self._unset_cursor.emit() - self._click = False - self.clicked.disconnect() - - def showEvent(self, event): - self.slide_animation.start() - return super().showEvent(event) + """ + A notifaction bar + """ + clicked = pyqtSignal() + _show_signal = pyqtSignal() + _hide_signal = pyqtSignal() + _unset_cursor = pyqtSignal() + _set_cursor = pyqtSignal(object) + def __init__(self, parent=None): + super().__init__(parent) + self._main_layout = QHBoxLayout(self) + self._default_height = 20 + self._dynamic_height = 0 + self._lbl = QLabel() + self._main_layout.addWidget(self._lbl) + self._lbl.setAlignment(Qt.AlignCenter) + self.setAutoFillBackground(True) + self.setBackgroundRole(self.palette().Shadow) + self.setContentsMargins(-10,-10,-10,-10) + self._click = False + self._override_hide = False + self.text_queue = [] + + self.slide_animation = create_animation(self, 'minimumHeight') + self.slide_animation.setDuration(500) + self.slide_animation.setStartValue(0) + self.slide_animation.setEndValue(self._default_height) + self.slide_animation.valueChanged.connect(self.set_dynamic_height) + self._show_signal.connect(self.show) + self._hide_signal.connect(self.hide) + self._unset_cursor.connect(self.unsetCursor) + self._set_cursor.connect(self.setCursor) + + def set_dynamic_height(self, h): + self._dynamic_height = h + + def mousePressEvent(self, event): + if self._click: + self.clicked.emit() + return super().mousePressEvent(event) + + def set_clickable(self, d=True): + self._click = d + self._set_cursor.emit(Qt.PointingHandCursor) + + def resize(self, x, y=0): + return super().resize(x, self._dynamic_height) + + def add_text(self, text, autohide=True): + """ + Add new text to the bar, deleting the previous one + """ + try: + self._reset() + except TypeError: + pass + if not self.isVisible(): + self._show_signal.emit() + self._lbl.setText(text) + if autohide: + if not self._override_hide: + threading.Timer(10, self._hide_signal.emit).start() + + def begin_show(self): + """ + Control how long you will show notification bar. + end_show() must be called to hide the bar. + """ + self._override_hide = True + self._show_signal.emit() + + def end_show(self): + self._override_hide = False + QTimer.singleShot(5000, self._hide_signal.emit) + + def _reset(self): + self._unset_cursor.emit() + self._click = False + self.clicked.disconnect() + + def showEvent(self, event): + self.slide_animation.start() + return super().showEvent(event) class GalleryShowcaseWidget(QWidget): - """ - Pass a gallery or set a gallery via -> set_gallery - """ - - double_clicked = pyqtSignal(gallerydb.Gallery) - - def __init__(self, gallery=None, parent=None, menu=None): - super().__init__(parent) - self.setAttribute(Qt.WA_DeleteOnClose) - self.main_layout = QVBoxLayout(self) - self.parent_widget = parent - if menu: - menu.gallery_widget = self - self._menu = menu - self.gallery = gallery - self.extra_text = QLabel() - self.profile = QLabel(self) - self.profile.setAlignment(Qt.AlignCenter) - self.text = QLabel(self) - self.font_M = self.text.fontMetrics() - self.main_layout.addWidget(self.extra_text) - self.extra_text.hide() - self.main_layout.addWidget(self.profile) - self.main_layout.addWidget(self.text) - self.h = 0 - self.w = 0 - if gallery: - self.h = 220 - self.w = 143 - self.set_gallery(gallery, (self.w, self.h)) - - self.resize(self.w, self.h) - self.setMouseTracking(True) - - @property - def menu(self): - return self._menu - - @menu.setter - def contextmenu(self, new_menu): - new_menu.gallery_widget = self - self._menu = new_menu - - def set_pixmap(self, gallery, img): - self.profile.setPixmap(QPixmap.fromImage(img)) - - def set_gallery(self, gallery, size=app_constants.THUMB_SMALL): - assert isinstance(size, (list, tuple)) - self.w = size[0] - self.h = size[1] - self.gallery = gallery - img = gallery.get_profile(app_constants.ProfileType.Small, self.set_pixmap) - if img: - self.profile.setPixmap(QPixmap.fromImage(img)) - title = self.font_M.elidedText(gallery.title, Qt.ElideRight, self.w) - artist = self.font_M.elidedText(gallery.artist, Qt.ElideRight, self.w) - self.text.setText("{}\n{}".format(title, artist)) - self.setToolTip("{}\n{}".format(gallery.title, gallery.artist)) - self.resize(self.w, self.h+50) - - def paintEvent(self, event): - painter = QPainter(self) - if self.underMouse(): - painter.setBrush(QBrush(QColor(164,164,164,120))) - painter.drawRect(self.text.pos().x()-2, self.profile.pos().y()-5, - self.text.width()+2, self.profile.height()+self.text.height()+12) - super().paintEvent(event) - - def enterEvent(self, event): - self.update() - return super().enterEvent(event) - - def leaveEvent(self, event): - self.update() - return super().leaveEvent(event) - - def mouseDoubleClickEvent(self, event): - self.double_clicked.emit(self.gallery) - return super().mouseDoubleClickEvent(event) - - def contextMenuEvent(self, event): - if self._menu: - self._menu.exec_(event.globalPos()) - event.accept() - else: - event.ignore() + """ + Pass a gallery or set a gallery via -> set_gallery + """ + + double_clicked = pyqtSignal(gallerydb.Gallery) + + def __init__(self, gallery=None, parent=None, menu=None): + super().__init__(parent) + self.setAttribute(Qt.WA_DeleteOnClose) + self.main_layout = QVBoxLayout(self) + self.parent_widget = parent + if menu: + menu.gallery_widget = self + self._menu = menu + self.gallery = gallery + self.extra_text = QLabel() + self.profile = QLabel(self) + self.profile.setAlignment(Qt.AlignCenter) + self.text = QLabel(self) + self.font_M = self.text.fontMetrics() + self.main_layout.addWidget(self.extra_text) + self.extra_text.hide() + self.main_layout.addWidget(self.profile) + self.main_layout.addWidget(self.text) + self.h = 0 + self.w = 0 + if gallery: + self.h = 220 + self.w = 143 + self.set_gallery(gallery, (self.w, self.h)) + + self.resize(self.w, self.h) + self.setMouseTracking(True) + + @property + def menu(self): + return self._menu + + @menu.setter + def contextmenu(self, new_menu): + new_menu.gallery_widget = self + self._menu = new_menu + + def set_pixmap(self, gallery, img): + self.profile.setPixmap(QPixmap.fromImage(img)) + + def set_gallery(self, gallery, size=app_constants.THUMB_SMALL): + assert isinstance(size, (list, tuple)) + self.w = size[0] + self.h = size[1] + self.gallery = gallery + img = gallery.get_profile(app_constants.ProfileType.Small, self.set_pixmap) + if img: + self.profile.setPixmap(QPixmap.fromImage(img)) + title = self.font_M.elidedText(gallery.title, Qt.ElideRight, self.w) + artist = self.font_M.elidedText(gallery.artist, Qt.ElideRight, self.w) + self.text.setText("{}\n{}".format(title, artist)) + self.setToolTip("{}\n{}".format(gallery.title, gallery.artist)) + self.resize(self.w, self.h + 50) + + def paintEvent(self, event): + painter = QPainter(self) + if self.underMouse(): + painter.setBrush(QBrush(QColor(164,164,164,120))) + painter.drawRect(self.text.pos().x() - 2, self.profile.pos().y() - 5, + self.text.width() + 2, self.profile.height() + self.text.height() + 12) + super().paintEvent(event) + + def enterEvent(self, event): + self.update() + return super().enterEvent(event) + + def leaveEvent(self, event): + self.update() + return super().leaveEvent(event) + + def mouseDoubleClickEvent(self, event): + self.double_clicked.emit(self.gallery) + return super().mouseDoubleClickEvent(event) + + def contextMenuEvent(self, event): + if self._menu: + self._menu.exec_(event.globalPos()) + event.accept() + else: + event.ignore() class SingleGalleryChoices(BasePopup): - """ - Represent a single gallery with a list of choices below. - Pass a gallery and a list of tuple/list where the first index is a string in each - if text is passed, the text will be shown alongside gallery, else gallery be centered - """ - USER_CHOICE = pyqtSignal(object) - def __init__(self, gallery, tuple_first_idx, text=None, parent = None): - super().__init__(parent, flags= Qt.Dialog | Qt.FramelessWindowHint) - main_layout = QVBoxLayout() - self.main_widget.setLayout(main_layout) - g_showcase = GalleryShowcaseWidget() - g_showcase.set_gallery(gallery, (170//1.40, 170)) - if text: - t_layout = QHBoxLayout() - main_layout.addLayout(t_layout) - t_layout.addWidget(g_showcase, 1) - info = QLabel(text) - info.setWordWrap(True) - t_layout.addWidget(info) - else: - main_layout.addWidget(g_showcase, 0, Qt.AlignCenter) - self.list_w = QListWidget(self) - self.list_w.setAlternatingRowColors(True) - self.list_w.setWordWrap(True) - self.list_w.setTextElideMode(Qt.ElideNone) - main_layout.addWidget(self.list_w, 3) - main_layout.addLayout(self.buttons_layout) - for t in tuple_first_idx: - item = CustomListItem(t) - item.setText(t[0]) - self.list_w.addItem(item) - self.buttons = self.add_buttons('Skip All', 'Skip', 'Choose',) - self.buttons[2].clicked.connect(self.finish) - self.buttons[1].clicked.connect(self.skip) - self.buttons[0].clicked.connect(self.skipall) - self.resize(400, 400) - self.show() - - def finish(self): - item = self.list_w.selectedItems() - if item: - item = item[0] - self.USER_CHOICE.emit(item.item) - self.close() - - def skip(self): - self.USER_CHOICE.emit(()) - self.close() - - def skipall(self): - self.USER_CHOICE.emit(None) - self.close() + """ + Represent a single gallery with a list of choices below. + Pass a gallery and a list of tuple/list where the first index is a string in each + if text is passed, the text will be shown alongside gallery, else gallery be centered + """ + USER_CHOICE = pyqtSignal(object) + def __init__(self, gallery, tuple_first_idx, text=None, parent=None): + super().__init__(parent, flags= Qt.Dialog | Qt.FramelessWindowHint) + main_layout = QVBoxLayout() + self.main_widget.setLayout(main_layout) + g_showcase = GalleryShowcaseWidget() + g_showcase.set_gallery(gallery, (170 // 1.40, 170)) + if text: + t_layout = QHBoxLayout() + main_layout.addLayout(t_layout) + t_layout.addWidget(g_showcase, 1) + info = QLabel(text) + info.setWordWrap(True) + t_layout.addWidget(info) + else: + main_layout.addWidget(g_showcase, 0, Qt.AlignCenter) + self.list_w = QListWidget(self) + self.list_w.setAlternatingRowColors(True) + self.list_w.setWordWrap(True) + self.list_w.setTextElideMode(Qt.ElideNone) + main_layout.addWidget(self.list_w, 3) + main_layout.addLayout(self.buttons_layout) + for t in tuple_first_idx: + item = CustomListItem(t) + item.setText(t[0]) + self.list_w.addItem(item) + self.buttons = self.add_buttons('Skip All', 'Skip', 'Choose',) + self.buttons[2].clicked.connect(self.finish) + self.buttons[1].clicked.connect(self.skip) + self.buttons[0].clicked.connect(self.skipall) + self.resize(400, 400) + self.show() + + def finish(self): + item = self.list_w.selectedItems() + if item: + item = item[0] + self.USER_CHOICE.emit(item.item) + self.close() + + def skip(self): + self.USER_CHOICE.emit(()) + self.close() + + def skipall(self): + self.USER_CHOICE.emit(None) + self.close() class BaseUserChoice(QDialog): - USER_CHOICE = pyqtSignal(object) - def __init__(self, parent, **kwargs): - super().__init__(parent, **kwargs) - self.setAttribute(Qt.WA_DeleteOnClose) - self.setAttribute(Qt.WA_TranslucentBackground) - main_widget = QFrame(self) - layout = QVBoxLayout(self) - layout.addWidget(main_widget) - self.main_layout = QFormLayout(main_widget) - - def accept(self, choice): - self.USER_CHOICE.emit(choice) - super().accept() + USER_CHOICE = pyqtSignal(object) + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + self.setAttribute(Qt.WA_DeleteOnClose) + self.setAttribute(Qt.WA_TranslucentBackground) + main_widget = QFrame(self) + layout = QVBoxLayout(self) + layout.addWidget(main_widget) + self.main_layout = QFormLayout(main_widget) + + def accept(self, choice): + self.USER_CHOICE.emit(choice) + super().accept() class TorrentItem: - def __init__(self, url, name="", date=None, size=None, seeds=None, peers=None, uploader=None): - self.url = url - self.name = name - self.date = date - self.size = size - self.seeds = seeds - self.peers = peers - self.uploader = uploader + def __init__(self, url, name="", date=None, size=None, seeds=None, peers=None, uploader=None): + self.url = url + self.name = name + self.date = date + self.size = size + self.seeds = seeds + self.peers = peers + self.uploader = uploader class TorrentUserChoice(BaseUserChoice): - def __init__(self, parent, torrentitems=[], **kwargs): - super().__init__(parent, **kwargs) - title = QLabel('Torrents') - title.setAlignment(Qt.AlignCenter) - self.main_layout.addRow(title) - self._list_w = QListWidget(self) - self.main_layout.addRow(self._list_w) - for t in torrentitems: - self.add_torrent_item(t) - - btn_layout = QHBoxLayout() - choose_btn = QPushButton('Choose') - choose_btn.clicked.connect(self.accept) - btn_layout.addWidget(Spacer('h')) - btn_layout.addWidget(choose_btn) - self.main_layout.addRow(btn_layout) - - - def add_torrent_item(self, item): - list_item = CustomListItem(item) - list_item.setText("{}\nSeeds:{}\tPeers:{}\tSize:{}\tDate:{}\tUploader:{}".format( - item.name, item.seeds, item.peers, item.size, item.date, item.uploader)) - self._list_w.addItem(list_item) - - def accept(self): - items = self._list_w.selectedItems() - if items: - item = items[0] - super().accept(item.item) + def __init__(self, parent, torrentitems=[], **kwargs): + super().__init__(parent, **kwargs) + title = QLabel('Torrents') + title.setAlignment(Qt.AlignCenter) + self.main_layout.addRow(title) + self._list_w = QListWidget(self) + self.main_layout.addRow(self._list_w) + for t in torrentitems: + self.add_torrent_item(t) + + btn_layout = QHBoxLayout() + choose_btn = QPushButton('Choose') + choose_btn.clicked.connect(self.accept) + btn_layout.addWidget(Spacer('h')) + btn_layout.addWidget(choose_btn) + self.main_layout.addRow(btn_layout) + + + def add_torrent_item(self, item): + list_item = CustomListItem(item) + list_item.setText("{}\nSeeds:{}\tPeers:{}\tSize:{}\tDate:{}\tUploader:{}".format(item.name, item.seeds, item.peers, item.size, item.date, item.uploader)) + self._list_w.addItem(list_item) + + def accept(self): + items = self._list_w.selectedItems() + if items: + item = items[0] + super().accept(item.item) class LoadingOverlay(QWidget): - - def __init__(self, parent=None): - super().__init__(parent) - palette = QPalette(self.palette()) - palette.setColor(palette.Background, Qt.transparent) - self.setPalette(palette) - - def paintEngine(self, event): - painter = QPainter() - painter.begin(self) - painter.setRenderHint(QPainter.Antialiasing) - painter.fillRect(event.rect(), - QBrush(QColor(255,255,255,127))) - painter.setPen(QPen(Qt.NoPen)) - for i in range(6): - if (self.counter/5) % 6 == i: - painter.setBrush(QBrush(QColor(127+ - (self.counter%5)*32,127,127))) - else: - painter.setBrush(QBrush(QColor(127,127,127))) - painter.drawEllipse(self.width()/2+30* - math.cos(2*math.pi*i/6.0) - 10, - self.height()/2+30* - math.sin(2*math.pi*i/6.0) - 10, - 20,20) - - painter.end() - - def showEvent(self, event): - self.timer = self.startTimer(50) - self.counter = 0 - super().showEvent(event) - - def timerEvent(self, event): - self.counter += 1 - self.update() - if self.counter == 60: - self.killTimer(self.timer) - self.hide() + + def __init__(self, parent=None): + super().__init__(parent) + palette = QPalette(self.palette()) + palette.setColor(palette.Background, Qt.transparent) + self.setPalette(palette) + + def paintEngine(self, event): + painter = QPainter() + painter.begin(self) + painter.setRenderHint(QPainter.Antialiasing) + painter.fillRect(event.rect(), + QBrush(QColor(255,255,255,127))) + painter.setPen(QPen(Qt.NoPen)) + for i in range(6): + if (self.counter / 5) % 6 == i: + painter.setBrush(QBrush(QColor(127 + (self.counter % 5) * 32,127,127))) + else: + painter.setBrush(QBrush(QColor(127,127,127))) + painter.drawEllipse(self.width() / 2 + 30 * math.cos(2 * math.pi * i / 6.0) - 10, + self.height() / 2 + 30 * math.sin(2 * math.pi * i / 6.0) - 10, + 20,20) + + painter.end() + + def showEvent(self, event): + self.timer = self.startTimer(50) + self.counter = 0 + super().showEvent(event) + + def timerEvent(self, event): + self.counter += 1 + self.update() + if self.counter == 60: + self.killTimer(self.timer) + self.hide() class FileIcon: - - def __init__(self): - self.ico_types = {} - - def get_file_icon(self, path): - if os.path.isdir(path): - if not 'dir' in self.ico_types: - self.ico_types['dir'] = QFileIconProvider().icon(QFileInfo(path)) - return self.ico_types['dir'] - elif path.endswith(utils.ARCHIVE_FILES): - suff = '' - for s in utils.ARCHIVE_FILES: - if path.endswith(s): - suff = s - if not suff in self.ico_types: - self.ico_types[suff] = QFileIconProvider().icon(QFileInfo(path)) - return self.ico_types[suff] - - @staticmethod - def get_external_file_icon(): - if app_constants._REFRESH_EXTERNAL_VIEWER: - if os.path.exists(app_constants.GALLERY_EXT_ICO_PATH): - os.remove(app_constants.GALLERY_EXT_ICO_PATH) - info = QFileInfo(app_constants.EXTERNAL_VIEWER_PATH) - icon = QFileIconProvider().icon(info) - pixmap = icon.pixmap(QSize(32, 32)) - pixmap.save(app_constants.GALLERY_EXT_ICO_PATH, quality=100) - app_constants._REFRESH_EXTERNAL_VIEWER = False - - return QIcon(app_constants.GALLERY_EXT_ICO_PATH) - - @staticmethod - def refresh_default_icon(): - - if os.path.exists(app_constants.GALLERY_DEF_ICO_PATH): - os.remove(app_constants.GALLERY_DEF_ICO_PATH) - - def get_file(n): - gallery = gallerydb.GalleryDB.get_gallery_by_id(n) - if not gallery: - return False - file = "" - if gallery.path.endswith(tuple(ARCHIVE_FILES)): - try: - zip = ArchiveFile(gallery.path) - except utils.app_constants.CreateArchiveFail: - return False - for name in zip.namelist(): - if name.lower().endswith(tuple(IMG_FILES)): - folder = os.path.join( - app_constants.temp_dir, - '{}{}'.format(name, n)) - zip.extract(name, folder) - file = os.path.join( - folder, name) - break; - else: - for p in scandir.scandir(gallery.chapters[0].path): - if p.name.lower().endswith(tuple(IMG_FILES)): - file = p.path - break; - return file - - # TODO: fix this! (When there are no ids below 300? (because they go deleted)) - for x in range(1, 300): - try: - file = get_file(x) - break - except FileNotFoundError: - continue - except app_constants.CreateArchiveFail: - continue - - if not file: - return None - icon = QFileIconProvider().icon(QFileInfo(file)) - pixmap = icon.pixmap(QSize(32, 32)) - pixmap.save(app_constants.GALLERY_DEF_ICO_PATH, quality=100) - return True - - @staticmethod - def get_default_file_icon(): - s = True - if not os.path.isfile(app_constants.GALLERY_DEF_ICO_PATH): - s = FileIcon.refresh_default_icon() - if s: - return QIcon(app_constants.GALLERY_DEF_ICO_PATH) - else: return None + + def __init__(self): + self.ico_types = {} + + def get_file_icon(self, path): + if os.path.isdir(path): + if not 'dir' in self.ico_types: + self.ico_types['dir'] = QFileIconProvider().icon(QFileInfo(path)) + return self.ico_types['dir'] + elif path.endswith(utils.ARCHIVE_FILES): + suff = '' + for s in utils.ARCHIVE_FILES: + if path.endswith(s): + suff = s + if not suff in self.ico_types: + self.ico_types[suff] = QFileIconProvider().icon(QFileInfo(path)) + return self.ico_types[suff] + + @staticmethod + def get_external_file_icon(): + if app_constants._REFRESH_EXTERNAL_VIEWER: + if os.path.exists(app_constants.GALLERY_EXT_ICO_PATH): + os.remove(app_constants.GALLERY_EXT_ICO_PATH) + info = QFileInfo(app_constants.EXTERNAL_VIEWER_PATH) + icon = QFileIconProvider().icon(info) + pixmap = icon.pixmap(QSize(32, 32)) + pixmap.save(app_constants.GALLERY_EXT_ICO_PATH, quality=100) + app_constants._REFRESH_EXTERNAL_VIEWER = False + + return QIcon(app_constants.GALLERY_EXT_ICO_PATH) + + @staticmethod + def refresh_default_icon(): + + if os.path.exists(app_constants.GALLERY_DEF_ICO_PATH): + os.remove(app_constants.GALLERY_DEF_ICO_PATH) + + def get_file(n): + gallery = gallerydb.GalleryDB.get_gallery_by_id(n) + if not gallery: + return False + file = "" + if gallery.path.endswith(tuple(ARCHIVE_FILES)): + try: + zip = ArchiveFile(gallery.path) + except utils.app_constants.CreateArchiveFail: + return False + for name in zip.namelist(): + if name.lower().endswith(tuple(IMG_FILES)): + folder = os.path.join(app_constants.temp_dir, + '{}{}'.format(name, n)) + zip.extract(name, folder) + file = os.path.join(folder, name) + break + else: + for p in scandir.scandir(gallery.chapters[0].path): + if p.name.lower().endswith(tuple(IMG_FILES)): + file = p.path + break + return file + + # TODO: fix this! (When there are no ids below 300? (because they go + # deleted)) + for x in range(1, 300): + try: + file = get_file(x) + break + except FileNotFoundError: + continue + except app_constants.CreateArchiveFail: + continue + + if not file: + return None + icon = QFileIconProvider().icon(QFileInfo(file)) + pixmap = icon.pixmap(QSize(32, 32)) + pixmap.save(app_constants.GALLERY_DEF_ICO_PATH, quality=100) + return True + + @staticmethod + def get_default_file_icon(): + s = True + if not os.path.isfile(app_constants.GALLERY_DEF_ICO_PATH): + s = FileIcon.refresh_default_icon() + if s: + return QIcon(app_constants.GALLERY_DEF_ICO_PATH) + else: return None #def center_parent(parent, child): # "centers child window in parent" @@ -1977,606 +1979,597 @@ def get_default_file_icon(): # centerparent.setY(sg_rect.bottom() - child_frame.height()) # child.move(centerparent) - class Spacer(QWidget): - """ - To be used as a spacer. - Default mode is both. Specify mode with string: v, h or both - """ - def __init__(self, mode='both', parent=None): - super().__init__(parent) - if mode == 'h': - self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) - elif mode == 'v': - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - else: - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + """ + To be used as a spacer. + Default mode is both. Specify mode with string: v, h or both + """ + def __init__(self, mode='both', parent=None): + super().__init__(parent) + if mode == 'h': + self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + elif mode == 'v': + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + else: + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) class FlowLayout(QLayout): - def __init__(self, parent=None, margin=0, spacing=-1): - super(FlowLayout, self).__init__(parent) + def __init__(self, parent=None, margin=0, spacing=-1): + super(FlowLayout, self).__init__(parent) - if parent is not None: - self.setContentsMargins(margin, margin, margin, margin) + if parent is not None: + self.setContentsMargins(margin, margin, margin, margin) - self.setSpacing(spacing) + self.setSpacing(spacing) - self.itemList = [] + self.itemList = [] - def __del__(self): - item = self.takeAt(0) - while item: - item = self.takeAt(0) + def __del__(self): + item = self.takeAt(0) + while item: + item = self.takeAt(0) - def addItem(self, item): - self.itemList.append(item) + def addItem(self, item): + self.itemList.append(item) - def count(self): - return len(self.itemList) + def count(self): + return len(self.itemList) - # to keep it in style with the others.. - def rowCount(self): - return self.count() + # to keep it in style with the others.. + def rowCount(self): + return self.count() - def itemAt(self, index): - if index >= 0 and index < len(self.itemList): - return self.itemList[index] + def itemAt(self, index): + if index >= 0 and index < len(self.itemList): + return self.itemList[index] - return None + return None - def takeAt(self, index): - if index >= 0 and index < len(self.itemList): - return self.itemList.pop(index) + def takeAt(self, index): + if index >= 0 and index < len(self.itemList): + return self.itemList.pop(index) - return None + return None - def expandingDirections(self): - return Qt.Orientations(Qt.Orientation(0)) + def expandingDirections(self): + return Qt.Orientations(Qt.Orientation(0)) - def hasHeightForWidth(self): - return True + def hasHeightForWidth(self): + return True - def heightForWidth(self, width): - height = self.doLayout(QRect(0, 0, width, 0), True) - return height + def heightForWidth(self, width): + height = self.doLayout(QRect(0, 0, width, 0), True) + return height - def setGeometry(self, rect): - super(FlowLayout, self).setGeometry(rect) - self.doLayout(rect, False) + def setGeometry(self, rect): + super(FlowLayout, self).setGeometry(rect) + self.doLayout(rect, False) - def sizeHint(self): - return self.minimumSize() + def sizeHint(self): + return self.minimumSize() - def minimumSize(self): - size = QSize() + def minimumSize(self): + size = QSize() - for item in self.itemList: - size = size.expandedTo(item.minimumSize()) + for item in self.itemList: + size = size.expandedTo(item.minimumSize()) - margin, _, _, _ = self.getContentsMargins() + margin, _, _, _ = self.getContentsMargins() - size += QSize(2 * margin, 2 * margin) - return size + size += QSize(2 * margin, 2 * margin) + return size - def doLayout(self, rect, testOnly): - x = rect.x() - y = rect.y() - lineHeight = 0 + def doLayout(self, rect, testOnly): + x = rect.x() + y = rect.y() + lineHeight = 0 - for item in self.itemList: - wid = item.widget() - spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal) - spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical) - nextX = x + item.sizeHint().width() + spaceX - if nextX - spaceX > rect.right() and lineHeight > 0: - x = rect.x() - y = y + lineHeight + spaceY - nextX = x + item.sizeHint().width() + spaceX - lineHeight = 0 + for item in self.itemList: + wid = item.widget() + spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal) + spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical) + nextX = x + item.sizeHint().width() + spaceX + if nextX - spaceX > rect.right() and lineHeight > 0: + x = rect.x() + y = y + lineHeight + spaceY + nextX = x + item.sizeHint().width() + spaceX + lineHeight = 0 - if not testOnly: - item.setGeometry(QRect(QPoint(x, y), item.sizeHint())) + if not testOnly: + item.setGeometry(QRect(QPoint(x, y), item.sizeHint())) - x = nextX - lineHeight = max(lineHeight, item.sizeHint().height()) + x = nextX + lineHeight = max(lineHeight, item.sizeHint().height()) - return y + lineHeight - rect.y() + return y + lineHeight - rect.y() class LineEdit(QLineEdit): - """ - Custom Line Edit which sacrifices contextmenu for selectAll - """ - def __init__(self, parent=None): - super().__init__(parent) + """ + Custom Line Edit which sacrifices contextmenu for selectAll + """ + def __init__(self, parent=None): + super().__init__(parent) - def mousePressEvent(self, event): - if event.button() == Qt.RightButton: - self.selectAll() - else: - super().mousePressEvent(event) + def mousePressEvent(self, event): + if event.button() == Qt.RightButton: + self.selectAll() + else: + super().mousePressEvent(event) - def contextMenuEvent(self, QContextMenuEvent): - pass + def contextMenuEvent(self, QContextMenuEvent): + pass class PathLineEdit(QLineEdit): - """ - A lineedit which open a filedialog on right/left click - Set dir to false if you want files. - """ - def __init__(self, parent=None, dir=True, filters=utils.FILE_FILTER): - super().__init__(parent) - self.folder = dir - self.filters = filters - self.setPlaceholderText('Right/Left-click to open folder explorer.') - self.setToolTip('Right/Left-click to open folder explorer.') - - def openExplorer(self): - if self.folder: - path = QFileDialog.getExistingDirectory(self, - 'Choose folder') - else: - path = QFileDialog.getOpenFileName(self, - 'Choose file', filter=self.filters) - path = path[0] - if len(path) != 0: - self.setText(path) - - def mousePressEvent(self, event): - assert isinstance(event, QMouseEvent) - if len(self.text()) == 0: - if event.button() == Qt.LeftButton: - self.openExplorer() - else: - return super().mousePressEvent(event) - if event.button() == Qt.RightButton: - self.openExplorer() - - super().mousePressEvent(event) + """ + A lineedit which open a filedialog on right/left click + Set dir to false if you want files. + """ + def __init__(self, parent=None, dir=True, filters=utils.FILE_FILTER): + super().__init__(parent) + self.folder = dir + self.filters = filters + self.setPlaceholderText('Right/Left-click to open folder explorer.') + self.setToolTip('Right/Left-click to open folder explorer.') + + def openExplorer(self): + if self.folder: + path = QFileDialog.getExistingDirectory(self, + 'Choose folder') + else: + path = QFileDialog.getOpenFileName(self, + 'Choose file', filter=self.filters) + path = path[0] + if len(path) != 0: + self.setText(path) + + def mousePressEvent(self, event): + assert isinstance(event, QMouseEvent) + if len(self.text()) == 0: + if event.button() == Qt.LeftButton: + self.openExplorer() + else: + return super().mousePressEvent(event) + if event.button() == Qt.RightButton: + self.openExplorer() + + super().mousePressEvent(event) class ChapterAddWidget(QWidget): - CHAPTERS = pyqtSignal(gallerydb.ChaptersContainer) - def __init__(self, gallery, parent=None): - super().__init__(parent) - self.setWindowFlags(Qt.Window) - self.setAttribute(Qt.WA_DeleteOnClose) - self.current_chapters = gallery.chapters.count() - self.added_chaps = 0 - self.gallery = gallery - - layout = QFormLayout() - self.setLayout(layout) - lbl = QLabel('{} by {}'.format(gallery.title, gallery.artist)) - layout.addRow('Gallery:', lbl) - layout.addRow('Current chapters:', QLabel('{}'.format(self.current_chapters))) - - new_btn = QPushButton('Add directory') - new_btn.clicked.connect(lambda: self.add_new_chapter('f')) - new_btn.adjustSize() - new_btn_a = QPushButton('Add archive') - new_btn_a.clicked.connect(lambda: self.add_new_chapter('a')) - new_btn_a.adjustSize() - add_btn = QPushButton('Finish') - add_btn.clicked.connect(self.finish) - add_btn.adjustSize() - new_l = QHBoxLayout() - new_l.addWidget(add_btn, 1, alignment=Qt.AlignLeft) - new_l.addWidget(Spacer('h')) - new_l.addWidget(new_btn, alignment=Qt.AlignRight) - new_l.addWidget(new_btn_a, alignment=Qt.AlignRight) - layout.addRow(new_l) - - frame = QFrame() - frame.setFrameShape(frame.StyledPanel) - layout.addRow(frame) - - self.chapter_l = QVBoxLayout() - frame.setLayout(self.chapter_l) - - self.setMaximumHeight(550) - self.setFixedWidth(500) - if parent: - self.move(parent.window().frameGeometry().topLeft() + - parent.window().rect().center() - - self.rect().center()) - else: - frect = self.frameGeometry() - frect.moveCenter(QDesktopWidget().availableGeometry().center()) - self.move(frect.topLeft()) - self.setWindowTitle('Add Chapters') - - def add_new_chapter(self, mode): - chap_layout = QHBoxLayout() - self.added_chaps += 1 - curr_chap = self.current_chapters+self.added_chaps - - chp_numb = QSpinBox(self) - chp_numb.setMinimum(curr_chap-1) - chp_numb.setMaximum(curr_chap+1) - chp_numb.setValue(curr_chap) - curr_chap_lbl = QLabel('Chapter {}'.format(curr_chap)) - def ch_lbl(n): curr_chap_lbl.setText('Chapter {}'.format(n)) - chp_numb.valueChanged[int].connect(ch_lbl) - if mode =='f': - chp_path = PathLineEdit() - chp_path.setPlaceholderText('Right/Left-click to open folder explorer.'+ - ' Leave empty to not add.') - elif mode == 'a': - chp_path = PathLineEdit(dir=False) - chp_path.setPlaceholderText('Right/Left-click to open folder explorer.'+ - ' Leave empty to not add.') - - chp_path.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - if mode == 'f': - chap_layout.addWidget(QLabel('D')) - elif mode == 'a': - chap_layout.addWidget(QLabel('A')) - chap_layout.addWidget(chp_path, 3) - chap_layout.addWidget(chp_numb, 0) - self.chapter_l.addWidget(curr_chap_lbl, - alignment=Qt.AlignLeft) - self.chapter_l.addLayout(chap_layout) - - def finish(self): - chapters = self.gallery.chapters - widgets = [] - x = True - while x: - x = self.chapter_l.takeAt(0) - if x: - widgets.append(x) - for l in range(1, len(widgets), 1): - layout = widgets[l] - try: - line_edit = layout.itemAt(1).widget() - spin_box = layout.itemAt(2).widget() - except AttributeError: - continue - p = line_edit.text() - c = spin_box.value() - 1 # because of 0-based index - if os.path.exists(p): - chap = chapters.create_chapter(c) - chap.title = utils.title_parser(os.path.split(p)[1])['title'] - chap.path = p - if os.path.isdir(p): - chap.pages = len(list(scandir.scandir(p))) - elif p.endswith(utils.ARCHIVE_FILES): - chap.in_archive = 1 - arch = utils.ArchiveFile(p) - chap.pages = len(arch.dir_contents('')) - arch.close() - - self.CHAPTERS.emit(chapters) - self.close() + CHAPTERS = pyqtSignal(gallerydb.ChaptersContainer) + def __init__(self, gallery, parent=None): + super().__init__(parent) + self.setWindowFlags(Qt.Window) + self.setAttribute(Qt.WA_DeleteOnClose) + self.current_chapters = gallery.chapters.count() + self.added_chaps = 0 + self.gallery = gallery + + layout = QFormLayout() + self.setLayout(layout) + lbl = QLabel('{} by {}'.format(gallery.title, gallery.artist)) + layout.addRow('Gallery:', lbl) + layout.addRow('Current chapters:', QLabel('{}'.format(self.current_chapters))) + + new_btn = QPushButton('Add directory') + new_btn.clicked.connect(lambda: self.add_new_chapter('f')) + new_btn.adjustSize() + new_btn_a = QPushButton('Add archive') + new_btn_a.clicked.connect(lambda: self.add_new_chapter('a')) + new_btn_a.adjustSize() + add_btn = QPushButton('Finish') + add_btn.clicked.connect(self.finish) + add_btn.adjustSize() + new_l = QHBoxLayout() + new_l.addWidget(add_btn, 1, alignment=Qt.AlignLeft) + new_l.addWidget(Spacer('h')) + new_l.addWidget(new_btn, alignment=Qt.AlignRight) + new_l.addWidget(new_btn_a, alignment=Qt.AlignRight) + layout.addRow(new_l) + + frame = QFrame() + frame.setFrameShape(frame.StyledPanel) + layout.addRow(frame) + + self.chapter_l = QVBoxLayout() + frame.setLayout(self.chapter_l) + + self.setMaximumHeight(550) + self.setFixedWidth(500) + if parent: + self.move(parent.window().frameGeometry().topLeft() + parent.window().rect().center() - self.rect().center()) + else: + frect = self.frameGeometry() + frect.moveCenter(QDesktopWidget().availableGeometry().center()) + self.move(frect.topLeft()) + self.setWindowTitle('Add Chapters') + + def add_new_chapter(self, mode): + chap_layout = QHBoxLayout() + self.added_chaps += 1 + curr_chap = self.current_chapters + self.added_chaps + + chp_numb = QSpinBox(self) + chp_numb.setMinimum(curr_chap - 1) + chp_numb.setMaximum(curr_chap + 1) + chp_numb.setValue(curr_chap) + curr_chap_lbl = QLabel('Chapter {}'.format(curr_chap)) + def ch_lbl(n): curr_chap_lbl.setText('Chapter {}'.format(n)) + chp_numb.valueChanged[int].connect(ch_lbl) + if mode == 'f': + chp_path = PathLineEdit() + chp_path.setPlaceholderText('Right/Left-click to open folder explorer.' + ' Leave empty to not add.') + elif mode == 'a': + chp_path = PathLineEdit(dir=False) + chp_path.setPlaceholderText('Right/Left-click to open folder explorer.' + ' Leave empty to not add.') + + chp_path.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + if mode == 'f': + chap_layout.addWidget(QLabel('D')) + elif mode == 'a': + chap_layout.addWidget(QLabel('A')) + chap_layout.addWidget(chp_path, 3) + chap_layout.addWidget(chp_numb, 0) + self.chapter_l.addWidget(curr_chap_lbl, + alignment=Qt.AlignLeft) + self.chapter_l.addLayout(chap_layout) + + def finish(self): + chapters = self.gallery.chapters + widgets = [] + x = True + while x: + x = self.chapter_l.takeAt(0) + if x: + widgets.append(x) + for l in range(1, len(widgets), 1): + layout = widgets[l] + try: + line_edit = layout.itemAt(1).widget() + spin_box = layout.itemAt(2).widget() + except AttributeError: + continue + p = line_edit.text() + c = spin_box.value() - 1 # because of 0-based index + if os.path.exists(p): + chap = chapters.create_chapter(c) + chap.title = utils.title_parser(os.path.split(p)[1])['title'] + chap.path = p + if os.path.isdir(p): + chap.pages = len(list(scandir.scandir(p))) + elif p.endswith(utils.ARCHIVE_FILES): + chap.in_archive = 1 + arch = utils.ArchiveFile(p) + chap.pages = len(arch.dir_contents('')) + arch.close() + + self.CHAPTERS.emit(chapters) + self.close() class CustomListItem(QListWidgetItem): - def __init__(self, item=None, parent=None, txt='', type=QListWidgetItem.Type): - super().__init__(txt, parent, type) - self.item = item + def __init__(self, item=None, parent=None, txt='', type=QListWidgetItem.Type): + super().__init__(txt, parent, type) + self.item = item class CustomTableItem(QTableWidgetItem): - def __init__(self, item=None, txt='', type=QTableWidgetItem.Type): - super().__init__(txt, type) - self.item = item + def __init__(self, item=None, txt='', type=QTableWidgetItem.Type): + super().__init__(txt, type) + self.item = item class GalleryListView(QWidget): - SERIES = pyqtSignal(list) - def __init__(self, parent=None, modal=False): - super().__init__(parent) - self.setWindowFlags(Qt.Dialog) - self.setAttribute(Qt.WA_DeleteOnClose) - layout = QVBoxLayout() - self.setLayout(layout) - - if modal: - frame = QFrame() - frame.setFrameShape(frame.StyledPanel) - modal_layout = QHBoxLayout() - frame.setLayout(modal_layout) - layout.addWidget(frame) - info = QLabel('This mode let\'s you add galleries from ' + - 'different folders.') - f_folder = QPushButton('Add directories') - f_folder.clicked.connect(self.from_folder) - f_files = QPushButton('Add archives') - f_files.clicked.connect(self.from_files) - modal_layout.addWidget(info, 3, Qt.AlignLeft) - modal_layout.addWidget(f_folder, 0, Qt.AlignRight) - modal_layout.addWidget(f_files, 0, Qt.AlignRight) - - check_layout = QHBoxLayout() - layout.addLayout(check_layout) - if modal: - check_layout.addWidget(QLabel('Please uncheck galleries you do' + - ' not want to add. (Exisiting galleries won\'t be added'), - 3) - else: - check_layout.addWidget(QLabel('Please uncheck galleries you do' + - ' not want to add. (Existing galleries are hidden)'), - 3) - self.check_all = QCheckBox('Check/Uncheck All', self) - self.check_all.setChecked(True) - self.check_all.stateChanged.connect(self.all_check_state) - - check_layout.addWidget(self.check_all) - self.view_list = QListWidget() - self.view_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) - self.view_list.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.view_list.setAlternatingRowColors(True) - self.view_list.setEditTriggers(self.view_list.NoEditTriggers) - layout.addWidget(self.view_list) - - add_btn = QPushButton('Add checked') - add_btn.clicked.connect(self.return_gallery) - - cancel_btn = QPushButton('Cancel') - cancel_btn.clicked.connect(self.close_window) - btn_layout = QHBoxLayout() - - spacer = QWidget() - spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - btn_layout.addWidget(spacer) - btn_layout.addWidget(add_btn) - btn_layout.addWidget(cancel_btn) - layout.addLayout(btn_layout) - - self.resize(500,550) - frect = self.frameGeometry() - frect.moveCenter(QDesktopWidget().availableGeometry().center()) - self.move(frect.topLeft()) - self.setWindowTitle('Gallery List') - self.count = 0 - - def all_check_state(self, new_state): - row = 0 - done = False - while not done: - item = self.view_list.item(row) - if item: - row += 1 - if new_state == Qt.Unchecked: - item.setCheckState(Qt.Unchecked) - else: - item.setCheckState(Qt.Checked) - else: - done = True - - def add_gallery(self, item, name): - """ - Constructs an widgetitem to hold the provided item, - and adds it to the view_list - """ - assert isinstance(name, str) - gallery_item = CustomListItem(item) - gallery_item.setText(name) - gallery_item.setFlags(gallery_item.flags() | Qt.ItemIsUserCheckable) - gallery_item.setCheckState(Qt.Checked) - self.view_list.addItem(gallery_item) - self.count += 1 - - def update_count(self): - self.setWindowTitle('Gallery List ({})'.format(self.count)) - - def return_gallery(self): - gallery_list = [] - row = 0 - done = False - while not done: - item = self.view_list.item(row) - if not item: - done = True - else: - if item.checkState() == Qt.Checked: - gallery_list.append(item.item) - row += 1 - - self.SERIES.emit(gallery_list) - self.close() - - def from_folder(self): - file_dialog = QFileDialog() - file_dialog.setFileMode(QFileDialog.DirectoryOnly) - file_dialog.setOption(QFileDialog.DontUseNativeDialog, True) - file_view = file_dialog.findChild(QListView, 'listView') - if file_view: - file_view.setSelectionMode(QAbstractItemView.MultiSelection) - f_tree_view = file_dialog.findChild(QTreeView) - if f_tree_view: - f_tree_view.setSelectionMode(QAbstractItemView.MultiSelection) - - if file_dialog.exec(): - for path in file_dialog.selectedFiles(): - self.add_gallery(path, os.path.split(path)[1]) - - - def from_files(self): - gallery_list = QFileDialog.getOpenFileNames(self, - 'Select 1 or more gallery to add', - filter='Archives ({})'.format(utils.FILE_FILTER)) - for path in gallery_list[0]: - #Warning: will break when you add more filters - if len(path) != 0: - self.add_gallery(path, os.path.split(path)[1]) - - def close_window(self): - msgbox = QMessageBox() - msgbox.setText('Are you sure you want to cancel?') - msgbox.setStandardButtons(msgbox.Yes | msgbox.No) - msgbox.setDefaultButton(msgbox.No) - msgbox.setIcon(msgbox.Question) - if msgbox.exec() == QMessageBox.Yes: - self.close() + SERIES = pyqtSignal(list) + def __init__(self, parent=None, modal=False): + super().__init__(parent) + self.setWindowFlags(Qt.Dialog) + self.setAttribute(Qt.WA_DeleteOnClose) + layout = QVBoxLayout() + self.setLayout(layout) + + if modal: + frame = QFrame() + frame.setFrameShape(frame.StyledPanel) + modal_layout = QHBoxLayout() + frame.setLayout(modal_layout) + layout.addWidget(frame) + info = QLabel('This mode let\'s you add galleries from ' + 'different folders.') + f_folder = QPushButton('Add directories') + f_folder.clicked.connect(self.from_folder) + f_files = QPushButton('Add archives') + f_files.clicked.connect(self.from_files) + modal_layout.addWidget(info, 3, Qt.AlignLeft) + modal_layout.addWidget(f_folder, 0, Qt.AlignRight) + modal_layout.addWidget(f_files, 0, Qt.AlignRight) + + check_layout = QHBoxLayout() + layout.addLayout(check_layout) + if modal: + check_layout.addWidget(QLabel('Please uncheck galleries you do' + ' not want to add. (Exisiting galleries won\'t be added'), + 3) + else: + check_layout.addWidget(QLabel('Please uncheck galleries you do' + ' not want to add. (Existing galleries are hidden)'), + 3) + self.check_all = QCheckBox('Check/Uncheck All', self) + self.check_all.setChecked(True) + self.check_all.stateChanged.connect(self.all_check_state) + + check_layout.addWidget(self.check_all) + self.view_list = QListWidget() + self.view_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + self.view_list.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.view_list.setAlternatingRowColors(True) + self.view_list.setEditTriggers(self.view_list.NoEditTriggers) + layout.addWidget(self.view_list) + + add_btn = QPushButton('Add checked') + add_btn.clicked.connect(self.return_gallery) + + cancel_btn = QPushButton('Cancel') + cancel_btn.clicked.connect(self.close_window) + btn_layout = QHBoxLayout() + + spacer = QWidget() + spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + btn_layout.addWidget(spacer) + btn_layout.addWidget(add_btn) + btn_layout.addWidget(cancel_btn) + layout.addLayout(btn_layout) + + self.resize(500,550) + frect = self.frameGeometry() + frect.moveCenter(QDesktopWidget().availableGeometry().center()) + self.move(frect.topLeft()) + self.setWindowTitle('Gallery List') + self.count = 0 + + def all_check_state(self, new_state): + row = 0 + done = False + while not done: + item = self.view_list.item(row) + if item: + row += 1 + if new_state == Qt.Unchecked: + item.setCheckState(Qt.Unchecked) + else: + item.setCheckState(Qt.Checked) + else: + done = True + + def add_gallery(self, item, name): + """ + Constructs an widgetitem to hold the provided item, + and adds it to the view_list + """ + assert isinstance(name, str) + gallery_item = CustomListItem(item) + gallery_item.setText(name) + gallery_item.setFlags(gallery_item.flags() | Qt.ItemIsUserCheckable) + gallery_item.setCheckState(Qt.Checked) + self.view_list.addItem(gallery_item) + self.count += 1 + + def update_count(self): + self.setWindowTitle('Gallery List ({})'.format(self.count)) + + def return_gallery(self): + gallery_list = [] + row = 0 + done = False + while not done: + item = self.view_list.item(row) + if not item: + done = True + else: + if item.checkState() == Qt.Checked: + gallery_list.append(item.item) + row += 1 + + self.SERIES.emit(gallery_list) + self.close() + + def from_folder(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.DirectoryOnly) + file_dialog.setOption(QFileDialog.DontUseNativeDialog, True) + file_view = file_dialog.findChild(QListView, 'listView') + if file_view: + file_view.setSelectionMode(QAbstractItemView.MultiSelection) + f_tree_view = file_dialog.findChild(QTreeView) + if f_tree_view: + f_tree_view.setSelectionMode(QAbstractItemView.MultiSelection) + + if file_dialog.exec(): + for path in file_dialog.selectedFiles(): + self.add_gallery(path, os.path.split(path)[1]) + + + def from_files(self): + gallery_list = QFileDialog.getOpenFileNames(self, + 'Select 1 or more gallery to add', + filter='Archives ({})'.format(utils.FILE_FILTER)) + for path in gallery_list[0]: + #Warning: will break when you add more filters + if len(path) != 0: + self.add_gallery(path, os.path.split(path)[1]) + + def close_window(self): + msgbox = QMessageBox() + msgbox.setText('Are you sure you want to cancel?') + msgbox.setStandardButtons(msgbox.Yes | msgbox.No) + msgbox.setDefaultButton(msgbox.No) + msgbox.setIcon(msgbox.Question) + if msgbox.exec() == QMessageBox.Yes: + self.close() class Loading(BasePopup): - ON = False #to prevent multiple instances - def __init__(self, parent=None): - super().__init__(parent) - self.progress = QProgressBar() - self.progress.setStyleSheet("color:white") - self.text = QLabel() - self.text.setAlignment(Qt.AlignCenter) - self.text.setStyleSheet("color:white;background-color:transparent;") - inner_layout_ = QVBoxLayout() - inner_layout_.addWidget(self.text, 0, Qt.AlignHCenter) - inner_layout_.addWidget(self.progress) - self.main_widget.setLayout(inner_layout_) - self.resize(300,100) - #frect = self.frameGeometry() - #frect.moveCenter(QDesktopWidget().availableGeometry().center()) - #self.move(parent.window().frameGeometry().topLeft() + - # parent.window().rect().center() - - # self.rect().center() - QPoint(self.rect().width(),0)) - #self.setAttribute(Qt.WA_DeleteOnClose) - #self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - - def mousePressEvent(self, QMouseEvent): - pass - - def setText(self, string): - if string != self.text.text(): - self.text.setText(string) + ON = False #to prevent multiple instances + def __init__(self, parent=None): + super().__init__(parent) + self.progress = QProgressBar() + self.progress.setStyleSheet("color:white") + self.text = QLabel() + self.text.setAlignment(Qt.AlignCenter) + self.text.setStyleSheet("color:white;background-color:transparent;") + inner_layout_ = QVBoxLayout() + inner_layout_.addWidget(self.text, 0, Qt.AlignHCenter) + inner_layout_.addWidget(self.progress) + self.main_widget.setLayout(inner_layout_) + self.resize(300,100) + #frect = self.frameGeometry() + #frect.moveCenter(QDesktopWidget().availableGeometry().center()) + #self.move(parent.window().frameGeometry().topLeft() + + # parent.window().rect().center() - + # self.rect().center() - QPoint(self.rect().width(),0)) + #self.setAttribute(Qt.WA_DeleteOnClose) + #self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) + + def mousePressEvent(self, QMouseEvent): + pass + + def setText(self, string): + if string != self.text.text(): + self.text.setText(string) class CompleterTextEdit(QTextEdit): - """ - A textedit with autocomplete - """ - def __init__(self, **kwargs): - super().__init__(**kwargs) - self._completer = None - log_d('Instantiate CompleterTextEdit: OK') - - def setCompleter(self, c): - if self._completer is not None: - self._completer.activated.disconnect() - - self._completer = c - - c.setWidget(self) - c.setCompletionMode(QCompleter.PopupCompletion) - c.setCaseSensitivity(Qt.CaseInsensitive) - c.activated.connect(self.insertCompletion) - - def completer(self): - return self._completer - - def insertCompletion(self, completion): - if self._completer.widget() is not self: - return - - tc = self.textCursor() - extra = len(completion) - len(self._completer.completionPrefix()) - tc.movePosition(QTextCursor.Left) - tc.movePosition(QTextCursor.EndOfWord) - tc.insertText(completion[-extra:]) - self.setTextCursor(tc) - - def textUnderCursor(self): - tc = self.textCursor() - tc.select(QTextCursor.WordUnderCursor) - - return tc.selectedText() - - def focusInEvent(self, e): - if self._completer is not None: - self._completer.setWidget(self) - - super().focusInEvent(e) - - def keyPressEvent(self, e): - if self._completer is not None and self._completer.popup().isVisible(): - # The following keys are forwarded by the completer to the widget. - if e.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): - e.ignore() - # Let the completer do default behavior. - return - - isShortcut = e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_E - if self._completer is None or not isShortcut: - # Do not process the shortcut when we have a completer. - super().keyPressEvent(e) - - ctrlOrShift = e.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier) - if self._completer is None or (ctrlOrShift and len(e.text()) == 0): - return - - eow = "~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-=" - hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift - completionPrefix = self.textUnderCursor() - - if not isShortcut and (hasModifier or len(e.text()) == 0 or len(completionPrefix) < 3 or e.text()[-1] in eow): - self._completer.popup().hide() - return - - if completionPrefix != self._completer.completionPrefix(): - self._completer.setCompletionPrefix(completionPrefix) - self._completer.popup().setCurrentIndex( - self._completer.completionModel().index(0, 0)) - - cr = self.cursorRect() - cr.setWidth(self._completer.popup().sizeHintForColumn(0) + self._completer.popup().verticalScrollBar().sizeHint().width()) - if self._completer: - self._completer.complete(cr) + """ + A textedit with autocomplete + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._completer = None + log_d('Instantiate CompleterTextEdit: OK') + + def setCompleter(self, c): + if self._completer is not None: + self._completer.activated.disconnect() + + self._completer = c + + c.setWidget(self) + c.setCompletionMode(QCompleter.PopupCompletion) + c.setCaseSensitivity(Qt.CaseInsensitive) + c.activated.connect(self.insertCompletion) + + def completer(self): + return self._completer + + def insertCompletion(self, completion): + if self._completer.widget() is not self: + return + + tc = self.textCursor() + extra = len(completion) - len(self._completer.completionPrefix()) + tc.movePosition(QTextCursor.Left) + tc.movePosition(QTextCursor.EndOfWord) + tc.insertText(completion[-extra:]) + self.setTextCursor(tc) + + def textUnderCursor(self): + tc = self.textCursor() + tc.select(QTextCursor.WordUnderCursor) + + return tc.selectedText() + + def focusInEvent(self, e): + if self._completer is not None: + self._completer.setWidget(self) + + super().focusInEvent(e) + + def keyPressEvent(self, e): + if self._completer is not None and self._completer.popup().isVisible(): + # The following keys are forwarded by the completer to the widget. + if e.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): + e.ignore() + # Let the completer do default behavior. + return + + isShortcut = e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_E + if self._completer is None or not isShortcut: + # Do not process the shortcut when we have a completer. + super().keyPressEvent(e) + + ctrlOrShift = e.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier) + if self._completer is None or (ctrlOrShift and len(e.text()) == 0): + return + + eow = "~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-=" + hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift + completionPrefix = self.textUnderCursor() + + if not isShortcut and (hasModifier or len(e.text()) == 0 or len(completionPrefix) < 3 or e.text()[-1] in eow): + self._completer.popup().hide() + return + + if completionPrefix != self._completer.completionPrefix(): + self._completer.setCompletionPrefix(completionPrefix) + self._completer.popup().setCurrentIndex(self._completer.completionModel().index(0, 0)) + + cr = self.cursorRect() + cr.setWidth(self._completer.popup().sizeHintForColumn(0) + self._completer.popup().verticalScrollBar().sizeHint().width()) + if self._completer: + self._completer.complete(cr) class GCompleter(QCompleter): - def __init__(self, parent=None, title=True, artist=True, tags=True): - self.all_data = [] - d = set() - for g in app_constants.GALLERY_DATA: - if title: - d.add(g.title) - if artist: - d.add(g.artist) - if tags: - for ns in g.tags: - d.add(ns) - for t in g.tags[ns]: - d.add(t) - - self.all_data.extend(d) - super().__init__(self.all_data, parent) - self.setCaseSensitivity(Qt.CaseInsensitive) + def __init__(self, parent=None, title=True, artist=True, tags=True): + self.all_data = [] + d = set() + for g in app_constants.GALLERY_DATA: + if title: + d.add(g.title) + if artist: + d.add(g.artist) + if tags: + for ns in g.tags: + d.add(ns) + for t in g.tags[ns]: + d.add(t) + + self.all_data.extend(d) + super().__init__(self.all_data, parent) + self.setCaseSensitivity(Qt.CaseInsensitive) class ChapterListItem(QFrame): - move_pos = pyqtSignal(int, object) - def __init__(self, chapter, parent=None): - super().__init__(parent) - main_layout = QHBoxLayout(self) - chapter_layout = QFormLayout() - self.number_lbl = QLabel(str(chapter.number+1), self) - self.number_lbl.adjustSize() - self.number_lbl.setFixedSize(self.number_lbl.size()) - self.chapter_lbl = ElidedLabel(self) - self.set_chapter_title(chapter) - main_layout.addWidget(self.number_lbl) - chapter_layout.addRow(self.chapter_lbl) - g_title = '' - if chapter.gallery: - g_title = chapter.gallery.title - self.gallery_lbl = ElidedLabel(g_title, self) - g_lbl_font = QFont(self.gallery_lbl.font()) - g_lbl_font.setPixelSize(g_lbl_font.pixelSize()-2) - g_lbl_font.setItalic(True) - self.gallery_lbl.setFont(g_lbl_font) - chapter_layout.addRow(self.gallery_lbl) - self.chapter = chapter - main_layout.addLayout(chapter_layout) - buttons_layout = QVBoxLayout() - buttons_layout.setSpacing(0) - up_btn = QPushButton('▲') - up_btn.adjustSize() - up_btn.setFixedSize(up_btn.size()) - up_btn.clicked.connect(lambda: self.move_pos.emit(0, self)) - down_btn = QPushButton('▼') - down_btn.adjustSize() - down_btn.setFixedSize(down_btn.size()) - down_btn.clicked.connect(lambda: self.move_pos.emit(1, self)) - buttons_layout.addWidget(up_btn) - buttons_layout.addWidget(down_btn) - main_layout.addLayout(buttons_layout) - - def set_chapter_title(self, chapter): - if chapter.title: - self.chapter_lbl.setText(chapter.title) - else: - self.chapter_lbl.setText("Chapter "+str(chapter.number+1)) + move_pos = pyqtSignal(int, object) + def __init__(self, chapter, parent=None): + super().__init__(parent) + main_layout = QHBoxLayout(self) + chapter_layout = QFormLayout() + self.number_lbl = QLabel(str(chapter.number + 1), self) + self.number_lbl.adjustSize() + self.number_lbl.setFixedSize(self.number_lbl.size()) + self.chapter_lbl = ElidedLabel(self) + self.set_chapter_title(chapter) + main_layout.addWidget(self.number_lbl) + chapter_layout.addRow(self.chapter_lbl) + g_title = '' + if chapter.gallery: + g_title = chapter.gallery.title + self.gallery_lbl = ElidedLabel(g_title, self) + g_lbl_font = QFont(self.gallery_lbl.font()) + g_lbl_font.setPixelSize(g_lbl_font.pixelSize() - 2) + g_lbl_font.setItalic(True) + self.gallery_lbl.setFont(g_lbl_font) + chapter_layout.addRow(self.gallery_lbl) + self.chapter = chapter + main_layout.addLayout(chapter_layout) + buttons_layout = QVBoxLayout() + buttons_layout.setSpacing(0) + up_btn = QPushButton('▲') + up_btn.adjustSize() + up_btn.setFixedSize(up_btn.size()) + up_btn.clicked.connect(lambda: self.move_pos.emit(0, self)) + down_btn = QPushButton('▼') + down_btn.adjustSize() + down_btn.setFixedSize(down_btn.size()) + down_btn.clicked.connect(lambda: self.move_pos.emit(1, self)) + buttons_layout.addWidget(up_btn) + buttons_layout.addWidget(down_btn) + main_layout.addLayout(buttons_layout) + + def set_chapter_title(self, chapter): + if chapter.title: + self.chapter_lbl.setText(chapter.title) + else: + self.chapter_lbl.setText("Chapter " + str(chapter.number + 1)) diff --git a/version/misc_db.py b/version/misc_db.py index c1a8cc8..432db9f 100644 --- a/version/misc_db.py +++ b/version/misc_db.py @@ -9,20 +9,21 @@ #GNU General Public License for more details. #You should have received a copy of the GNU General Public License #along with Happypanda. If not, see . -import pickle, logging +import pickle +import logging from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QWidget, - QVBoxLayout, QTabWidget, QAction, QGraphicsScene, - QSizePolicy, QMenu, QAction, QApplication, - QListWidget, QHBoxLayout, QPushButton, QStackedLayout, - QFrame, QSizePolicy, QListView, QFormLayout, QLineEdit, - QLabel, QStyledItemDelegate, QStyleOptionViewItem, - QCheckBox) + QVBoxLayout, QTabWidget, QAction, QGraphicsScene, + QSizePolicy, QMenu, QAction, QApplication, + QListWidget, QHBoxLayout, QPushButton, QStackedLayout, + QFrame, QSizePolicy, QListView, QFormLayout, QLineEdit, + QLabel, QStyledItemDelegate, QStyleOptionViewItem, + QCheckBox, QButtonGroup) from PyQt5.QtCore import (Qt, QTimer, pyqtSignal, QRect, QSize, QEasingCurve, - QSortFilterProxyModel, QIdentityProxyModel, QModelIndex, - QPointF, QRectF, QObject) + QSortFilterProxyModel, QIdentityProxyModel, QModelIndex, + QPointF, QRectF, QObject) from PyQt5.QtGui import (QIcon, QStandardItem, QFont, QPainter, QColor, QBrush, - QPixmap, QPalette) + QPixmap, QPalette) import gallerydb import app_constants @@ -38,599 +39,611 @@ log_c = log.critical class ToolbarTabManager(QObject): - "" - def __init__(self, toolbar, parent=None): - super().__init__(parent) - self.parent_widget = parent - self.toolbar = toolbar - self._actions = [] - self._last_selected = None - self.idx_widget = self.toolbar.addWidget(QWidget(self.toolbar)) - self.idx_widget.setVisible(False) - self.library_btn = None - self.favorite_btn = self.addTab("Favorites", delegate_paint=False) - self.library_btn = self.addTab("Library", delegate_paint=False) - self.toolbar.addSeparator() - self.idx_widget = self.toolbar.addWidget(QWidget(self.toolbar)) - self.idx_widget.setVisible(False) - - def _manage_selected(self, b): - if self._last_selected == b: - return - if self._last_selected: - self._last_selected.selected = False - self._last_selected.view.list_view.sort_model.rowsInserted.disconnect(self.parent_widget.stat_row_info) - self._last_selected.view.list_view.sort_model.rowsRemoved.disconnect(self.parent_widget.stat_row_info) - self._last_selected.view.hide() - b.selected = True - self._last_selected = b - self.parent_widget.current_manga_view = b.view - b.view.list_view.sort_model.rowsInserted.connect(self.parent_widget.stat_row_info) - b.view.list_view.sort_model.rowsRemoved.connect(self.parent_widget.stat_row_info) - b.view.show() - - def addTab(self, name, view_type=app_constants.ViewType.Default, delegate_paint=True, allow_sidebarwidget=False): - if self.toolbar: - t = misc.ToolbarButton(self.toolbar, name) - t.select.connect(self._manage_selected) - t.close_tab.connect(self.removeTab) - if self.library_btn: - t.view = gallery.MangaViews(view_type, self.parent_widget, allow_sidebarwidget) - t.view.hide() - t.close_tab.connect(lambda:self.library_btn.click()) - if not allow_sidebarwidget: - t.clicked.connect(self.parent_widget.sidebar_list.arrow_handle.click) - else: - t.view = self.parent_widget.default_manga_view - if delegate_paint: - t.view.list_view.manga_delegate._paint_level = 9000 # over nine thousand!!! - self._actions.append(self.toolbar.insertWidget(self.idx_widget, t)) - return t - - def removeTab(self, button_or_index): - if self.toolbar: - if isinstance(button_or_index, int): - self.toolbar.removeAction(self._actions.pop(button_or_index)) - else: - act_to_remove = None - for act in self._actions: - w = self.toolbar.widgetForAction(act) - if w == button_or_index: - self.toolbar.removeAction(act) - act_to_remove = act - break - if act_to_remove: - self._actions.remove(act) + "" + def __init__(self, toolbar, parent=None): + super().__init__(parent) + self.parent_widget = parent + self.toolbar = toolbar + self._actions = [] + self._last_selected = None + self.idx_widget = self.toolbar.addWidget(QWidget(self.toolbar)) + self.idx_widget.setVisible(False) + self.library_btn = None + self.favorite_btn = self.addTab("Favorites", delegate_paint=False) + self.library_btn = self.addTab("Library", delegate_paint=False) + self.toolbar.addSeparator() + self.idx_widget = self.toolbar.addWidget(QWidget(self.toolbar)) + self.idx_widget.setVisible(False) + self.toolbar.addSeparator() + + def _manage_selected(self, b): + if self._last_selected == b: + return + if self._last_selected: + self._last_selected.selected = False + self._last_selected.view.list_view.sort_model.rowsInserted.disconnect(self.parent_widget.stat_row_info) + self._last_selected.view.list_view.sort_model.rowsRemoved.disconnect(self.parent_widget.stat_row_info) + self._last_selected.view.hide() + b.selected = True + self._last_selected = b + self.parent_widget.current_manga_view = b.view + b.view.list_view.sort_model.rowsInserted.connect(self.parent_widget.stat_row_info) + b.view.list_view.sort_model.rowsRemoved.connect(self.parent_widget.stat_row_info) + b.view.show() + + def addTab(self, name, view_type=app_constants.ViewType.Default, delegate_paint=True, allow_sidebarwidget=False): + if self.toolbar: + t = misc.ToolbarButton(self.toolbar, name) + t.select.connect(self._manage_selected) + t.close_tab.connect(self.removeTab) + if self.library_btn: + t.view = gallery.MangaViews(view_type, self.parent_widget, allow_sidebarwidget) + t.view.hide() + t.close_tab.connect(lambda:self.library_btn.click()) + if not allow_sidebarwidget: + t.clicked.connect(self.parent_widget.sidebar_list.arrow_handle.click) + else: + t.view = self.parent_widget.default_manga_view + if delegate_paint: + t.view.list_view.manga_delegate._paint_level = 9000 # over nine thousand!!! + self._actions.append(self.toolbar.insertWidget(self.idx_widget, t)) + return t + + def removeTab(self, button_or_index): + if self.toolbar: + if isinstance(button_or_index, int): + self.toolbar.removeAction(self._actions.pop(button_or_index)) + else: + act_to_remove = None + for act in self._actions: + w = self.toolbar.widgetForAction(act) + if w == button_or_index: + self.toolbar.removeAction(act) + act_to_remove = act + break + if act_to_remove: + self._actions.remove(act) class NoTooltipModel(QIdentityProxyModel): - def __init__(self, model, parent=None): - super().__init__(parent) - self.setSourceModel(model) + def __init__(self, model, parent=None): + super().__init__(parent) + self.setSourceModel(model) - def data(self, index, role=Qt.DisplayRole): - if role == Qt.ToolTipRole: - return None - if role == Qt.DecorationRole: - return QPixmap(app_constants.GARTIST_PATH) - return self.sourceModel().data(index, role) + def data(self, index, role=Qt.DisplayRole): + if role == Qt.ToolTipRole: + return None + if role == Qt.DecorationRole: + return QPixmap(app_constants.GARTIST_PATH) + return self.sourceModel().data(index, role) class UniqueInfoModel(QSortFilterProxyModel): - def __init__(self, gallerymodel, role, parent=None): - super().__init__(parent) - self.setSourceModel(NoTooltipModel(gallerymodel, parent)) - self._unique = set() - self._unique_role = role - self.custom_filter = None - self.setDynamicSortFilter(True) - - def filterAcceptsRow(self, source_row, parent_index): - if self.sourceModel(): - idx = self.sourceModel().index(source_row, 0, parent_index) - if idx.isValid(): - unique = idx.data(self._unique_role) - if unique: - if not unique in self._unique: - if self.custom_filter != None: - if not idx.data(Qt.UserRole + 1) in self.custom_filter: - return False - self._unique.add(unique) - return True - return False - - def invalidate(self): - self._unique.clear() - super().invalidate() + def __init__(self, gallerymodel, role, parent=None): + super().__init__(parent) + self.setSourceModel(NoTooltipModel(gallerymodel, parent)) + self._unique = set() + self._unique_role = role + self.custom_filter = None + self.setDynamicSortFilter(True) + + def filterAcceptsRow(self, source_row, parent_index): + if self.sourceModel(): + idx = self.sourceModel().index(source_row, 0, parent_index) + if idx.isValid(): + unique = idx.data(self._unique_role) + if unique: + if not unique in self._unique: + if self.custom_filter != None: + if not idx.data(Qt.UserRole + 1) in self.custom_filter: + return False + self._unique.add(unique) + return True + return False + + def invalidate(self): + self._unique.clear() + super().invalidate() class ListDelegate(QStyledItemDelegate): - def __init__(self, parent=None): - self.parent_widget = parent - super().__init__(parent) - self.create_new_list_txt = 'Create new list...' - - def sizeHint(self, option, index): - size = super().sizeHint(option, index) - if index.data(Qt.DisplayRole) == self.create_new_list_txt: - return size - return QSize(size.width(), size.height() * 2) + def __init__(self, parent=None): + self.parent_widget = parent + super().__init__(parent) + self.create_new_list_txt = 'Create new list...' + + def sizeHint(self, option, index): + size = super().sizeHint(option, index) + if index.data(Qt.DisplayRole) == self.create_new_list_txt: + return size + return QSize(size.width(), size.height() * 2) class GalleryArtistsList(QListView): - artist_clicked = pyqtSignal(str) - - def __init__(self, gallerymodel, parent=None): - super().__init__(parent) - self.g_artists_model = UniqueInfoModel(gallerymodel, gallerymodel.ARTIST_ROLE, self) - self.setModel(self.g_artists_model) - self.setModelColumn(app_constants.ARTIST) - self.g_artists_model.setSortRole(gallerymodel.ARTIST_ROLE) - self.g_artists_model.sort(0) - self.doubleClicked.connect(self._artist_clicked) - self.ARTIST_ROLE = gallerymodel.ARTIST_ROLE - - def _artist_clicked(self, idx): - if idx.isValid(): - self.artist_clicked.emit(idx.data(self.ARTIST_ROLE)) - - def set_current_glist(self, g_list=None): - if g_list: - self.g_artists_model.custom_filter = g_list - else: - self.g_artists_model.custom_filter = None - self.g_artists_model.invalidate() + artist_clicked = pyqtSignal(str) + + def __init__(self, gallerymodel, parent=None): + super().__init__(parent) + self.g_artists_model = UniqueInfoModel(gallerymodel, gallerymodel.ARTIST_ROLE, self) + self.setModel(self.g_artists_model) + self.setModelColumn(app_constants.ARTIST) + self.g_artists_model.setSortRole(gallerymodel.ARTIST_ROLE) + self.g_artists_model.sort(0) + self.doubleClicked.connect(self._artist_clicked) + self.ARTIST_ROLE = gallerymodel.ARTIST_ROLE + + def _artist_clicked(self, idx): + if idx.isValid(): + self.artist_clicked.emit(idx.data(self.ARTIST_ROLE)) + + def set_current_glist(self, g_list=None): + if g_list: + self.g_artists_model.custom_filter = g_list + else: + self.g_artists_model.custom_filter = None + self.g_artists_model.invalidate() class TagsTreeView(QTreeWidget): - TAG_SEARCH = pyqtSignal(str) - NEW_LIST = pyqtSignal(str, gallerydb.GalleryList) - def __init__(self, parent): - super().__init__(parent) - self.setSelectionBehavior(self.SelectItems) - self.setSelectionMode(self.ExtendedSelection) - self.clipboard = QApplication.clipboard() - self.itemDoubleClicked.connect(lambda i: self.search_tags([i]) if i.parent() else None) - - def _convert_to_str(self, items): - tags = {} - d_tags = [] - for item in items: - ns_item = item.parent() - if ns_item.text(0) == 'No namespace': - d_tags.append(item.text(0)) - continue - if ns_item.text(0) in tags: - tags[ns_item.text(0)].append(item.text(0)) - else: - tags[ns_item.text(0)] = [item.text(0)] - - search_txt = utils.tag_to_string(tags) - d_search_txt = '' - for x, d_t in enumerate(d_tags, 1): - if x == len(d_tags): - d_search_txt += '{}'.format(d_t) - else: - d_search_txt += '{}, '.format(d_t) - final_txt = search_txt + ', ' + d_search_txt if search_txt else d_search_txt - return final_txt - - def search_tags(self, items): - self.TAG_SEARCH.emit(self._convert_to_str(items)) - - def create_list(self, items): - g_list = gallerydb.GalleryList("New List", filter=self._convert_to_str(items)) - g_list.add_to_db() - - self.NEW_LIST.emit(g_list.name, g_list) - - def contextMenuEvent(self, event): - handled = False - selected = False - s_items = self.selectedItems() - - if len(s_items) > 1: - selected = True - - ns_count = 0 - for item in s_items: - if not item.text(0).islower(): - ns_count += 1 - contains_ns = True if ns_count > 0 else False - - def copy(with_ns=False): - if with_ns: - ns_item = s_items[0].parent() - ns = ns_item.text(0) - tag = s_items[0].text(0) - txt = "{}:{}".format(ns, tag) - self.clipboard.setText(txt) - else: - item = s_items[0] - self.clipboard.setText(item.text(0)) - - if s_items: - menu = QMenu(self) - if not selected: - copy_act = menu.addAction('Copy') - copy_act.triggered.connect(copy) - if not contains_ns: - if s_items[0].parent().text(0) != 'No namespace': - copy_ns_act = menu.addAction('Copy with namespace') - copy_ns_act.triggered.connect(lambda: copy(True)) - if not contains_ns: - search_act = menu.addAction('Search') - search_act.triggered.connect(lambda: self.search_tags(s_items)) - create_list_filter_act = menu.addAction('Create list with selected') - create_list_filter_act.triggered.connect(lambda: self.create_list(s_items)) - handled = True - - if handled: - menu.exec_(event.globalPos()) - event.accept() - del menu - else: - event.ignore() - - def setup_tags(self): - self.clear() - tags = gallerydb.execute(gallerydb.TagDB.get_ns_tags, False) - items = [] - for ns in tags: - top_item = QTreeWidgetItem(self) - if ns == 'default': - top_item.setText(0, 'No namespace') - else: - top_item.setText(0, ns) - for tag in tags[ns]: - child_item = QTreeWidgetItem(top_item) - child_item.setText(0, tag) - self.sortItems(0, Qt.AscendingOrder) + TAG_SEARCH = pyqtSignal(str) + NEW_LIST = pyqtSignal(str, gallerydb.GalleryList) + def __init__(self, parent): + super().__init__(parent) + self.setSelectionBehavior(self.SelectItems) + self.setSelectionMode(self.ExtendedSelection) + self.clipboard = QApplication.clipboard() + self.itemDoubleClicked.connect(lambda i: self.search_tags([i]) if i.parent() else None) + + def _convert_to_str(self, items): + tags = {} + d_tags = [] + for item in items: + ns_item = item.parent() + if ns_item.text(0) == 'No namespace': + d_tags.append(item.text(0)) + continue + if ns_item.text(0) in tags: + tags[ns_item.text(0)].append(item.text(0)) + else: + tags[ns_item.text(0)] = [item.text(0)] + + search_txt = utils.tag_to_string(tags) + d_search_txt = '' + for x, d_t in enumerate(d_tags, 1): + if x == len(d_tags): + d_search_txt += '{}'.format(d_t) + else: + d_search_txt += '{}, '.format(d_t) + final_txt = search_txt + ', ' + d_search_txt if search_txt else d_search_txt + return final_txt + + def search_tags(self, items): + self.TAG_SEARCH.emit(self._convert_to_str(items)) + + def create_list(self, items): + g_list = gallerydb.GalleryList("New List", filter=self._convert_to_str(items)) + g_list.add_to_db() + + self.NEW_LIST.emit(g_list.name, g_list) + + def contextMenuEvent(self, event): + handled = False + selected = False + s_items = self.selectedItems() + + if len(s_items) > 1: + selected = True + + ns_count = 0 + for item in s_items: + if not item.text(0).islower(): + ns_count += 1 + contains_ns = True if ns_count > 0 else False + + def copy(with_ns=False): + if with_ns: + ns_item = s_items[0].parent() + ns = ns_item.text(0) + tag = s_items[0].text(0) + txt = "{}:{}".format(ns, tag) + self.clipboard.setText(txt) + else: + item = s_items[0] + self.clipboard.setText(item.text(0)) + + if s_items: + menu = QMenu(self) + if not selected: + copy_act = menu.addAction('Copy') + copy_act.triggered.connect(copy) + if not contains_ns: + if s_items[0].parent().text(0) != 'No namespace': + copy_ns_act = menu.addAction('Copy with namespace') + copy_ns_act.triggered.connect(lambda: copy(True)) + if not contains_ns: + search_act = menu.addAction('Search') + search_act.triggered.connect(lambda: self.search_tags(s_items)) + create_list_filter_act = menu.addAction('Create list with selected') + create_list_filter_act.triggered.connect(lambda: self.create_list(s_items)) + handled = True + + if handled: + menu.exec_(event.globalPos()) + event.accept() + del menu + else: + event.ignore() + + def setup_tags(self): + self.clear() + tags = gallerydb.execute(gallerydb.TagDB.get_ns_tags, False) + items = [] + for ns in tags: + top_item = QTreeWidgetItem(self) + if ns == 'default': + top_item.setText(0, 'No namespace') + else: + top_item.setText(0, ns) + for tag in tags[ns]: + child_item = QTreeWidgetItem(top_item) + child_item.setText(0, tag) + self.sortItems(0, Qt.AscendingOrder) class GalleryListEdit(misc.BasePopup): - apply = pyqtSignal() - def __init__(self, parent): - super().__init__(parent, blur=False) - main_layout = QFormLayout(self.main_widget) - self.name_edit = QLineEdit(self) - main_layout.addRow("Name:", self.name_edit) - self.filter_edit = QLineEdit(self) - what_is_filter = misc.ClickedLabel("What is filter/enforce? (Hover)") - what_is_filter.setToolTip(app_constants.WHAT_IS_FILTER) - what_is_filter.setToolTipDuration(9999999999) - self.enforce = QCheckBox(self) - self.regex = QCheckBox(self) - self.case = QCheckBox(self) - self.strict = QCheckBox(self) - main_layout.addRow(what_is_filter) - main_layout.addRow("Filter", self.filter_edit) - main_layout.addRow("Enforce", self.enforce) - main_layout.addRow("Regex", self.regex) - main_layout.addRow("Case sensitive", self.case) - main_layout.addRow("Match whole terms", self.strict) - main_layout.addRow(self.buttons_layout) - self.add_buttons("Close")[0].clicked.connect(self.hide) - self.add_buttons("Apply")[0].clicked.connect(self.accept) - - def set_list(self, gallery_list, item): - self.gallery_list = gallery_list - self.name_edit.setText(gallery_list.name) - self.enforce.setChecked(gallery_list.enforce) - self.regex.setChecked(gallery_list.regex) - self.case.setChecked(gallery_list.case) - self.strict.setChecked(gallery_list.strict) - self.item = item - if gallery_list.filter: - self.filter_edit.setText(gallery_list.filter) - else: - self.filter_edit.setText('') - self.adjustSize() - self.setFixedWidth(self.parent_widget.width()) - - def accept(self): - name = self.name_edit.text() - self.item.setText(name) - self.gallery_list.name = name - self.gallery_list.filter = self.filter_edit.text() - self.gallery_list.enforce = self.enforce.isChecked() - self.gallery_list.regex = self.regex.isChecked() - self.gallery_list.case = self.case.isChecked() - self.gallery_list.strict = self.strict.isChecked() - gallerydb.execute(gallerydb.ListDB.modify_list, True, self.gallery_list) - self.apply.emit() - self.hide() + apply = pyqtSignal() + def __init__(self, parent): + super().__init__(parent, blur=False) + main_layout = QFormLayout(self.main_widget) + self.name_edit = QLineEdit(self) + main_layout.addRow("Name:", self.name_edit) + self.filter_edit = QLineEdit(self) + what_is_filter = misc.ClickedLabel("What is filter/enforce? (Hover)") + what_is_filter.setToolTip(app_constants.WHAT_IS_FILTER) + what_is_filter.setToolTipDuration(9999999999) + self.enforce = QCheckBox(self) + self.regex = QCheckBox(self) + self.case = QCheckBox(self) + self.strict = QCheckBox(self) + main_layout.addRow(what_is_filter) + main_layout.addRow("Filter", self.filter_edit) + main_layout.addRow("Enforce", self.enforce) + main_layout.addRow("Regex", self.regex) + main_layout.addRow("Case sensitive", self.case) + main_layout.addRow("Match whole terms", self.strict) + main_layout.addRow(self.buttons_layout) + self.add_buttons("Close")[0].clicked.connect(self.hide) + self.add_buttons("Apply")[0].clicked.connect(self.accept) + + def set_list(self, gallery_list, item): + self.gallery_list = gallery_list + self.name_edit.setText(gallery_list.name) + self.enforce.setChecked(gallery_list.enforce) + self.regex.setChecked(gallery_list.regex) + self.case.setChecked(gallery_list.case) + self.strict.setChecked(gallery_list.strict) + self.item = item + if gallery_list.filter: + self.filter_edit.setText(gallery_list.filter) + else: + self.filter_edit.setText('') + self.adjustSize() + self.setFixedWidth(self.parent_widget.width()) + + def accept(self): + name = self.name_edit.text() + self.item.setText(name) + self.gallery_list.name = name + self.gallery_list.filter = self.filter_edit.text() + self.gallery_list.enforce = self.enforce.isChecked() + self.gallery_list.regex = self.regex.isChecked() + self.gallery_list.case = self.case.isChecked() + self.gallery_list.strict = self.strict.isChecked() + gallerydb.execute(gallerydb.ListDB.modify_list, True, self.gallery_list) + self.apply.emit() + self.hide() class GalleryListContextMenu(QMenu): - def __init__(self, item, parent): - super().__init__(parent) - self.parent_widget = parent - self.item = item - self.gallery_list = item.item - edit = self.addAction("Edit", self.edit_list) - clear = self.addAction("Clear", self.clear_list) - remove = self.addAction("Delete", self.remove_list) - - def edit_list(self): - self.parent_widget.gallery_list_edit.set_list(self.gallery_list, self.item) - self.parent_widget.gallery_list_edit.show() - - def remove_list(self): - self.parent_widget.takeItem(self.parent_widget.row(self.item)) - gallerydb.execute(gallerydb.ListDB.remove_list, True, self.gallery_list) - self.parent_widget.GALLERY_LIST_REMOVED.emit() - - def clear_list(self): - self.gallery_list.clear() - self.parent_widget.GALLERY_LIST_CLICKED.emit(self.gallery_list) + def __init__(self, item, parent): + super().__init__(parent) + self.parent_widget = parent + self.item = item + self.gallery_list = item.item + edit = self.addAction("Edit", self.edit_list) + clear = self.addAction("Clear", self.clear_list) + remove = self.addAction("Delete", self.remove_list) + + def edit_list(self): + self.parent_widget.gallery_list_edit.set_list(self.gallery_list, self.item) + self.parent_widget.gallery_list_edit.show() + + def remove_list(self): + self.parent_widget.takeItem(self.parent_widget.row(self.item)) + gallerydb.execute(gallerydb.ListDB.remove_list, True, self.gallery_list) + self.parent_widget.GALLERY_LIST_REMOVED.emit() + + def clear_list(self): + self.gallery_list.clear() + self.parent_widget.GALLERY_LIST_CLICKED.emit(self.gallery_list) class GalleryLists(QListWidget): - CREATE_LIST_TYPE = misc.CustomListItem.UserType + 1 - GALLERY_LIST_CLICKED = pyqtSignal(gallerydb.GalleryList) - GALLERY_LIST_REMOVED = pyqtSignal() - def __init__(self, parent): - super().__init__(parent) - self.gallery_list_edit = GalleryListEdit(parent) - self.gallery_list_edit.hide() - self._g_list_icon = QIcon(app_constants.GLIST_PATH) - self._font_selected = QFont(self.font()) - self._font_selected.setBold(True) - self._font_selected.setUnderline(True) - self.itemDoubleClicked.connect(self._item_double_clicked) - self.setItemDelegate(ListDelegate(self)) - self.itemDelegate().closeEditor.connect(self._add_new_list) - self.setEditTriggers(self.NoEditTriggers) - self.viewport().setAcceptDrops(True) - self._in_proccess_item = None - self.current_selected = None - self.gallery_list_edit.apply.connect(lambda: self._item_double_clicked(self.current_selected)) - self.setup_lists() - - def dragEnterEvent(self, event): - if event.mimeData().hasFormat("list/gallery"): - event.acceptProposedAction() - else: - event.ignore() - - def dragMoveEvent(self, event): - item = self.itemAt(event.pos()) - self.clearSelection() - if item: - item.setSelected(True) - event.accept() - - def dropEvent(self, event): - galleries = [] - - galleries = pickle.loads(event.mimeData().data("list/gallery").data()) - - g_list_item = self.itemAt(event.pos()) - if galleries and g_list_item: - txt = "galleries" if len(galleries) > 1 else "gallery" - app_constants.NOTIF_BUBBLE.update_text(g_list_item.item.name, 'Added {} to list...'.format(txt), 5) - log_i('Adding gallery to list') - g_list_item.item.add_gallery(galleries) - - super().dropEvent(event) - - - def _add_new_list(self, lineedit=None, hint=None, gallery_list=None): - if not self._in_proccess_item.text(): - self.takeItem(self.row(self._in_proccess_item)) - return - new_item = self._in_proccess_item - if not gallery_list: - new_list = gallerydb.GalleryList(new_item.text()) - new_list.add_to_db() - else: - new_list = gallery_list - new_item.item = new_list - new_item.setIcon(self._g_list_icon) - self.sortItems() - - def create_new_list(self, name=None, gallery_list=None): - new_item = misc.CustomListItem() - self._in_proccess_item = new_item - new_item.setFlags(new_item.flags() | Qt.ItemIsEditable) - new_item.setIcon(QIcon(app_constants.LIST_PATH)) - self.insertItem(0, new_item) - if name: - new_item.setText(name) - self._add_new_list(gallery_list=gallery_list) - else: - self.editItem(new_item) - - def _item_double_clicked(self, item): - if item: - self._reset_selected() - if item.item.filter: - app_constants.NOTIF_BUBBLE.update_text(item.item.name, "Updating list..", 5) - gallerydb.execute(item.item.scan, True) - self.GALLERY_LIST_CLICKED.emit(item.item) - item.setFont(self._font_selected) - self.current_selected = item - - def _reset_selected(self): - if self.current_selected: - self.current_selected.setFont(self.font()) - - def setup_lists(self): - for g_l in app_constants.GALLERY_LISTS: - if g_l.type == gallerydb.GalleryList.REGULAR: - self.create_new_list(g_l.name, g_l) - - def contextMenuEvent(self, event): - item = self.itemAt(event.pos()) - if item and item.type() != self.CREATE_LIST_TYPE: - menu = GalleryListContextMenu(item, self) - menu.exec_(event.globalPos()) - event.accept() - return - event.ignore() + CREATE_LIST_TYPE = misc.CustomListItem.UserType + 1 + GALLERY_LIST_CLICKED = pyqtSignal(gallerydb.GalleryList) + GALLERY_LIST_REMOVED = pyqtSignal() + def __init__(self, parent): + super().__init__(parent) + self.gallery_list_edit = GalleryListEdit(parent) + self.gallery_list_edit.hide() + self._g_list_icon = QIcon(app_constants.GLIST_PATH) + self._font_selected = QFont(self.font()) + self._font_selected.setBold(True) + self._font_selected.setUnderline(True) + self.itemDoubleClicked.connect(self._item_double_clicked) + self.setItemDelegate(ListDelegate(self)) + self.itemDelegate().closeEditor.connect(self._add_new_list) + self.setEditTriggers(self.NoEditTriggers) + self.viewport().setAcceptDrops(True) + self._in_proccess_item = None + self.current_selected = None + self.gallery_list_edit.apply.connect(lambda: self._item_double_clicked(self.current_selected)) + self.setup_lists() + + def dragEnterEvent(self, event): + if event.mimeData().hasFormat("list/gallery"): + event.acceptProposedAction() + else: + event.ignore() + + def dragMoveEvent(self, event): + item = self.itemAt(event.pos()) + self.clearSelection() + if item: + item.setSelected(True) + event.accept() + + def dropEvent(self, event): + galleries = [] + + galleries = pickle.loads(event.mimeData().data("list/gallery").data()) + + g_list_item = self.itemAt(event.pos()) + if galleries and g_list_item: + txt = "galleries" if len(galleries) > 1 else "gallery" + app_constants.NOTIF_BUBBLE.update_text(g_list_item.item.name, 'Added {} to list...'.format(txt), 5) + log_i('Adding gallery to list') + g_list_item.item.add_gallery(galleries) + + super().dropEvent(event) + + + def _add_new_list(self, lineedit=None, hint=None, gallery_list=None): + if not self._in_proccess_item.text(): + self.takeItem(self.row(self._in_proccess_item)) + return + new_item = self._in_proccess_item + if not gallery_list: + new_list = gallerydb.GalleryList(new_item.text()) + new_list.add_to_db() + else: + new_list = gallery_list + new_item.item = new_list + new_item.setIcon(self._g_list_icon) + self.sortItems() + + def create_new_list(self, name=None, gallery_list=None): + new_item = misc.CustomListItem() + self._in_proccess_item = new_item + new_item.setFlags(new_item.flags() | Qt.ItemIsEditable) + new_item.setIcon(QIcon(app_constants.LIST_PATH)) + self.insertItem(0, new_item) + if name: + new_item.setText(name) + self._add_new_list(gallery_list=gallery_list) + else: + self.editItem(new_item) + + def _item_double_clicked(self, item): + if item: + self._reset_selected() + if item.item.filter: + app_constants.NOTIF_BUBBLE.update_text(item.item.name, "Updating list..", 5) + gallerydb.execute(item.item.scan, True) + self.GALLERY_LIST_CLICKED.emit(item.item) + item.setFont(self._font_selected) + self.current_selected = item + + def _reset_selected(self): + if self.current_selected: + self.current_selected.setFont(self.font()) + + def setup_lists(self): + for g_l in app_constants.GALLERY_LISTS: + if g_l.type == gallerydb.GalleryList.REGULAR: + self.create_new_list(g_l.name, g_l) + + def contextMenuEvent(self, event): + item = self.itemAt(event.pos()) + if item and item.type() != self.CREATE_LIST_TYPE: + menu = GalleryListContextMenu(item, self) + menu.exec_(event.globalPos()) + event.accept() + return + event.ignore() class SideBarWidget(QFrame): - """ - """ - def __init__(self, parent): - super().__init__(parent) - self.setAcceptDrops(True) - self.parent_widget = parent - self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) - self.parent_widget - self._widget_layout = QHBoxLayout(self) - - # widget stuff - self._d_widget = QWidget(self) - self._widget_layout.addWidget(self._d_widget) - self.main_layout = QVBoxLayout(self._d_widget) - self.main_layout.setSpacing(0) - self.main_layout.setContentsMargins(0,0,0,0) - self.arrow_handle = misc.ArrowHandle(self) - self.arrow_handle.CLICKED.connect(self.slide) - - self._widget_layout.addWidget(self.arrow_handle) - self.setContentsMargins(0,0,-self.arrow_handle.width(),0) - - self.show_all_galleries_btn = QPushButton("Show all galleries") - self.show_all_galleries_btn.clicked.connect(lambda:parent.manga_list_view.sort_model.set_gallery_list()) - self.show_all_galleries_btn.clicked.connect(self.show_all_galleries_btn.hide) - self.show_all_galleries_btn.hide() - self.main_layout.addWidget(self.show_all_galleries_btn) - self.main_buttons_layout = QHBoxLayout() - self.main_layout.addLayout(self.main_buttons_layout) - - # buttons - self.lists_btn = QPushButton("Lists") - self.artist_btn = QPushButton("Artists") - self.ns_tags_btn = QPushButton("NS && Tags") - self.main_buttons_layout.addWidget(self.lists_btn) - self.main_buttons_layout.addWidget(self.artist_btn) - self.main_buttons_layout.addWidget(self.ns_tags_btn) - - # buttons contents - self.stacked_layout = QStackedLayout() - self.main_layout.addLayout(self.stacked_layout) - - # lists - gallery_lists_dummy = QWidget(self) - self.lists = GalleryLists(self) - create_new_list_btn = QPushButton() - create_new_list_btn.setIcon(QIcon(app_constants.PLUS_PATH)) - create_new_list_btn.setIconSize(QSize(15, 15)) - create_new_list_btn.clicked.connect(lambda: self.lists.create_new_list()) - create_new_list_btn.adjustSize() - create_new_list_btn.setFixedSize(create_new_list_btn.width(), create_new_list_btn.height()) - create_new_list_btn.setToolTip("Create a new list!") - lists_l = QVBoxLayout(gallery_lists_dummy) - lists_l.setContentsMargins(0,0,0,0) - lists_l.setSpacing(0) - lists_l.addWidget(self.lists) - lists_l.addWidget(create_new_list_btn) - lists_index = self.stacked_layout.addWidget(gallery_lists_dummy) - self.lists.GALLERY_LIST_CLICKED.connect(parent.manga_list_view.sort_model.set_gallery_list) - self.lists.GALLERY_LIST_CLICKED.connect(self.show_all_galleries_btn.show) - self.lists.GALLERY_LIST_REMOVED.connect(self.show_all_galleries_btn.click) - self.lists_btn.clicked.connect(lambda:self.stacked_layout.setCurrentIndex(lists_index)) - self.show_all_galleries_btn.clicked.connect(self.lists.clearSelection) - self.show_all_galleries_btn.clicked.connect(self.lists._reset_selected) - - # artists - self.artists_list = GalleryArtistsList(parent.manga_list_view.gallery_model, self) - self.artists_list.artist_clicked.connect(lambda a: parent.search('artist:"{}"'.format(a))) - artists_list_index = self.stacked_layout.addWidget(self.artists_list) - self.artist_btn.clicked.connect(lambda:self.stacked_layout.setCurrentIndex(artists_list_index)) - #self.lists.GALLERY_LIST_CLICKED.connect(self.artists_list.set_current_glist) - self.show_all_galleries_btn.clicked.connect(self.artists_list.clearSelection) - #self.show_all_galleries_btn.clicked.connect(lambda:self.artists_list.set_current_glist()) - - # ns_tags - self.tags_tree = TagsTreeView(self) - self.tags_tree.TAG_SEARCH.connect(parent.search) - self.tags_tree.NEW_LIST.connect(self.lists.create_new_list) - self.tags_tree.setHeaderHidden(True) - self.show_all_galleries_btn.clicked.connect(self.tags_tree.clearSelection) - self.tags_layout = QVBoxLayout(self.tags_tree) - ns_tags_index = self.stacked_layout.addWidget(self.tags_tree) - self.ns_tags_btn.clicked.connect(lambda:self.stacked_layout.setCurrentIndex(ns_tags_index)) - - self.slide_animation = misc.create_animation(self, "maximumSize") - self.slide_animation.stateChanged.connect(self._slide_hide) - self.slide_animation.setEasingCurve(QEasingCurve.InOutQuad) - - def _slide_hide(self, state): - size = self.sizeHint() - if state == self.slide_animation.Stopped: - if self.arrow_handle.current_arrow == self.arrow_handle.OUT: - self._d_widget.hide() - elif self.slide_animation.Running: - if self.arrow_handle.current_arrow == self.arrow_handle.IN: - if not self.parent_widget.current_manga_view.allow_sidebarwidget: - self.arrow_handle.current_arrow = self.arrow_handle.OUT - self.arrow_handle.update() - else: - self._d_widget.show() - - - def slide(self, state): - self.slide_animation.setEndValue(QSize(self.arrow_handle.width() * 2, self.height())) - - if state: - self.slide_animation.setDirection(self.slide_animation.Forward) - self.slide_animation.start() - else: - self.slide_animation.setDirection(self.slide_animation.Backward) - self.slide_animation.start() - - def showEvent(self, event): - super().showEvent(event) - if not app_constants.SHOW_SIDEBAR_WIDGET: - self.arrow_handle.click() - - def _init_size(self, event=None): - h = self.parent_widget.height() - self._max_width = 200 - self.updateGeometry() - self.setMaximumWidth(self._max_width) - self.slide_animation.setStartValue(QSize(self._max_width, h)) - - def resizeEvent(self, event): - self._init_size(event) - return super().resizeEvent(event) + """ + """ + def __init__(self, parent): + super().__init__(parent) + self.setAcceptDrops(True) + self.parent_widget = parent + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) + self.parent_widget + self._widget_layout = QHBoxLayout(self) + + # widget stuff + self._d_widget = QWidget(self) + self._widget_layout.addWidget(self._d_widget) + self.main_layout = QVBoxLayout(self._d_widget) + self.main_layout.setSpacing(0) + self.main_layout.setContentsMargins(0,0,0,0) + self.arrow_handle = misc.ArrowHandle(self) + self.arrow_handle.CLICKED.connect(self.slide) + + self._widget_layout.addWidget(self.arrow_handle) + self.setContentsMargins(0,0,-self.arrow_handle.width(),0) + + self.show_all_galleries_btn = QPushButton("Show all galleries") + self.show_all_galleries_btn.clicked.connect(lambda:parent.manga_list_view.sort_model.set_gallery_list()) + self.show_all_galleries_btn.clicked.connect(self.show_all_galleries_btn.hide) + self.show_all_galleries_btn.hide() + self.main_layout.addWidget(self.show_all_galleries_btn) + self.main_buttons_layout = QHBoxLayout() + self.main_layout.addLayout(self.main_buttons_layout) + + # buttons + bgroup = QButtonGroup(self) + bgroup.setExclusive(True) + self.lists_btn = QPushButton("Lists") + self.lists_btn.setCheckable(True) + bgroup.addButton(self.lists_btn) + self.artist_btn = QPushButton("Artists") + self.artist_btn.setCheckable(True) + bgroup.addButton(self.artist_btn) + self.ns_tags_btn = QPushButton("NS && Tags") + self.ns_tags_btn.setCheckable(True) + bgroup.addButton(self.ns_tags_btn) + self.lists_btn.setChecked(True) + + + self.main_buttons_layout.addWidget(self.lists_btn) + self.main_buttons_layout.addWidget(self.artist_btn) + self.main_buttons_layout.addWidget(self.ns_tags_btn) + + # buttons contents + self.stacked_layout = QStackedLayout() + self.main_layout.addLayout(self.stacked_layout) + + # lists + gallery_lists_dummy = QWidget(self) + self.lists = GalleryLists(self) + create_new_list_btn = QPushButton() + create_new_list_btn.setIcon(QIcon(app_constants.PLUS_PATH)) + create_new_list_btn.setIconSize(QSize(15, 15)) + create_new_list_btn.clicked.connect(lambda: self.lists.create_new_list()) + create_new_list_btn.adjustSize() + create_new_list_btn.setFixedSize(create_new_list_btn.width(), create_new_list_btn.height()) + create_new_list_btn.setToolTip("Create a new list!") + lists_l = QVBoxLayout(gallery_lists_dummy) + lists_l.setContentsMargins(0,0,0,0) + lists_l.setSpacing(0) + lists_l.addWidget(self.lists) + lists_l.addWidget(create_new_list_btn) + lists_index = self.stacked_layout.addWidget(gallery_lists_dummy) + self.lists.GALLERY_LIST_CLICKED.connect(parent.manga_list_view.sort_model.set_gallery_list) + self.lists.GALLERY_LIST_CLICKED.connect(self.show_all_galleries_btn.show) + self.lists.GALLERY_LIST_REMOVED.connect(self.show_all_galleries_btn.click) + self.lists_btn.clicked.connect(lambda:self.stacked_layout.setCurrentIndex(lists_index)) + self.show_all_galleries_btn.clicked.connect(self.lists.clearSelection) + self.show_all_galleries_btn.clicked.connect(self.lists._reset_selected) + + # artists + self.artists_list = GalleryArtistsList(parent.manga_list_view.gallery_model, self) + self.artists_list.artist_clicked.connect(lambda a: parent.search('artist:"{}"'.format(a))) + artists_list_index = self.stacked_layout.addWidget(self.artists_list) + self.artist_btn.clicked.connect(lambda:self.stacked_layout.setCurrentIndex(artists_list_index)) + #self.lists.GALLERY_LIST_CLICKED.connect(self.artists_list.set_current_glist) + self.show_all_galleries_btn.clicked.connect(self.artists_list.clearSelection) + #self.show_all_galleries_btn.clicked.connect(lambda:self.artists_list.set_current_glist()) + + # ns_tags + self.tags_tree = TagsTreeView(self) + self.tags_tree.TAG_SEARCH.connect(parent.search) + self.tags_tree.NEW_LIST.connect(self.lists.create_new_list) + self.tags_tree.setHeaderHidden(True) + self.show_all_galleries_btn.clicked.connect(self.tags_tree.clearSelection) + self.tags_layout = QVBoxLayout(self.tags_tree) + ns_tags_index = self.stacked_layout.addWidget(self.tags_tree) + self.ns_tags_btn.clicked.connect(lambda:self.stacked_layout.setCurrentIndex(ns_tags_index)) + + self.slide_animation = misc.create_animation(self, "maximumSize") + self.slide_animation.stateChanged.connect(self._slide_hide) + self.slide_animation.setEasingCurve(QEasingCurve.InOutQuad) + + def _slide_hide(self, state): + size = self.sizeHint() + if state == self.slide_animation.Stopped: + if self.arrow_handle.current_arrow == self.arrow_handle.OUT: + self._d_widget.hide() + elif self.slide_animation.Running: + if self.arrow_handle.current_arrow == self.arrow_handle.IN: + if not self.parent_widget.current_manga_view.allow_sidebarwidget: + self.arrow_handle.current_arrow = self.arrow_handle.OUT + self.arrow_handle.update() + else: + self._d_widget.show() + + + def slide(self, state): + self.slide_animation.setEndValue(QSize(self.arrow_handle.width() * 2, self.height())) + + if state: + self.slide_animation.setDirection(self.slide_animation.Forward) + self.slide_animation.start() + else: + self.slide_animation.setDirection(self.slide_animation.Backward) + self.slide_animation.start() + + def showEvent(self, event): + super().showEvent(event) + if not app_constants.SHOW_SIDEBAR_WIDGET: + self.arrow_handle.click() + + def _init_size(self, event=None): + h = self.parent_widget.height() + self._max_width = 200 + self.updateGeometry() + self.setMaximumWidth(self._max_width) + self.slide_animation.setStartValue(QSize(self._max_width, h)) + + def resizeEvent(self, event): + self._init_size(event) + return super().resizeEvent(event) class DBOverview(QWidget): - """ - - """ - about_to_close = pyqtSignal() - def __init__(self, parent, window=False): - if window: - super().__init__(None, Qt.Window) - else: - super().__init__(parent) - self.setAttribute(Qt.WA_DeleteOnClose) - self.parent_widget = parent - main_layout = QVBoxLayout(self) - tabbar = QTabWidget(self) - main_layout.addWidget(tabbar) - - # Tags stats - self.tags_stats = QListWidget(self) - tabbar.addTab(self.tags_stats, 'Statistics') - tabbar.setTabEnabled(1, False) - - # About AD - self.about_db = QWidget(self) - tabbar.addTab(self.about_db, 'DB Info') - tabbar.setTabEnabled(2, False) - - self.resize(300, 600) - self.setWindowTitle('DB Overview') - self.setWindowIcon(QIcon(app_constants.APP_ICO_PATH)) - - def setup_stats(self): - pass - - def setup_about_db(self): - pass - - def closeEvent(self, event): - self.about_to_close.emit() - return super().closeEvent(event) \ No newline at end of file + """ + + """ + about_to_close = pyqtSignal() + def __init__(self, parent, window=False): + if window: + super().__init__(None, Qt.Window) + else: + super().__init__(parent) + self.setAttribute(Qt.WA_DeleteOnClose) + self.parent_widget = parent + main_layout = QVBoxLayout(self) + tabbar = QTabWidget(self) + main_layout.addWidget(tabbar) + + # Tags stats + self.tags_stats = QListWidget(self) + tabbar.addTab(self.tags_stats, 'Statistics') + tabbar.setTabEnabled(1, False) + + # About AD + self.about_db = QWidget(self) + tabbar.addTab(self.about_db, 'DB Info') + tabbar.setTabEnabled(2, False) + + self.resize(300, 600) + self.setWindowTitle('DB Overview') + self.setWindowIcon(QIcon(app_constants.APP_ICO_PATH)) + + def setup_stats(self): + pass + + def setup_about_db(self): + pass + + def closeEvent(self, event): + self.about_to_close.emit() + return super().closeEvent(event) \ No newline at end of file