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