diff --git a/build.mk b/build.mk index f9c3929c404cb33f7f810a365b4eae03510256a1..407d92775b978912199cace3b08730f48894d289 100644 --- a/build.mk +++ b/build.mk @@ -1,5 +1,5 @@ core := weboob/tools/application/qt5 -applications := qboobmsg qhavedate qwebcontentedit qflatboob qcineoob qcookboob qhandjoob qbooblyrics +applications := qboobmsg qhavedate qwebcontentedit qflatboob qcineoob qcookboob qhandjoob qbooblyrics qgalleroob ifeq ($(WIN32),) applications += qvideoob endif diff --git a/desktop/qgalleroob.desktop b/desktop/qgalleroob.desktop new file mode 100644 index 0000000000000000000000000000000000000000..e419be574e904658d651c32cf678788251ac9099 --- /dev/null +++ b/desktop/qgalleroob.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=QGalleroob +Comment=Search for galleries and images on many websites, and get info about them +Exec=qgalleroob +Icon=qgalleroob +Terminal=false +Type=Application +StartupNotify=true +Categories=Graphics;Viewer;Qt; +Keywords=image;gallery;view; diff --git a/setup.py b/setup.py index 64b8dee055285bfe2209093ad9d28ef151a52f21..b7a2ebf35c5a0d922dd97679af44a7a2db17bca4 100755 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ def install_weboob(): hildon_scripts = set(('masstransit',)) qt_scripts = set(('qboobmsg', 'qhavedate', + 'qgalleroob', 'qvideoob', 'weboob-config-qt', 'qwebcontentedit', @@ -105,7 +106,9 @@ def install_weboob(): 'weboob.applications.qwebcontentedit', 'weboob.applications.qwebcontentedit.ui' 'weboob.applications.qflatboob', - 'weboob.applications.qflatboob.ui' + 'weboob.applications.qflatboob.ui', + 'weboob.applications.qgalleroob', + 'weboob.applications.qgalleroob.ui', )) if not options.hildon: diff --git a/weboob/applications/qgalleroob/__init__.py b/weboob/applications/qgalleroob/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8fdddbd257008e5391beb176abab7f4637fca3e6 --- /dev/null +++ b/weboob/applications/qgalleroob/__init__.py @@ -0,0 +1,3 @@ +from .qgalleroob import QGalleroob + +__all__ = ['QGalleroob'] diff --git a/weboob/applications/qgalleroob/main_window.py b/weboob/applications/qgalleroob/main_window.py new file mode 100644 index 0000000000000000000000000000000000000000..2a085a7e0326e304fce19f300abd763024354b6d --- /dev/null +++ b/weboob/applications/qgalleroob/main_window.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2016 Vincent A +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +import os + +from PyQt5.QtCore import QModelIndex, pyqtSlot as Slot + +from weboob.tools.application.qt5 import QtMainWindow +from weboob.tools.application.qt5.backendcfg import BackendCfg +from weboob.tools.application.qt5.models import BackendListModel, ResultModel, FilterTypeModel +from weboob.capabilities.collection import BaseCollection, CapCollection +from weboob.capabilities.gallery import CapGallery, BaseGallery +from weboob.capabilities.image import CapImage, BaseImage + +from .ui.mainwindow_ui import Ui_MainWindow +from .viewer import Viewer + + +def size_format(n): + UNITS = [ + (1 << 40, 'TiB'), + (1 << 30, 'GiB'), + (1 << 20, 'MiB'), + (1 << 10, 'KiB'), + (0, 'B') + ] + for f, u in UNITS: + if n > f: + return '%.2f %s' % (n / float(f), u) + + +class MainWindow(QtMainWindow): + def __init__(self, config, storage, weboob, parent=None): + super(MainWindow, self).__init__(parent) + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + self.mdl = ResultModel(weboob) + self.mdl.setColumnFields([['name', 'title'],['url']]) + self.proxy_mdl = FilterTypeModel() + self.proxy_mdl.setAcceptedTypes([BaseCollection]) + self.proxy_mdl.setSourceModel(self.mdl) + + self.ui.collectionTree.setModel(self.proxy_mdl) + self.ui.collectionTree.selectionModel().currentChanged.connect(self.showCollection) + + self.config = config + self.storage = storage + self.weboob = weboob + + self.ui.browseButton.clicked.connect(self.startBrowse) + self.ui.searchEdit.returnPressed.connect(self.startSearch) + self.ui.searchButton.clicked.connect(self.startSearch) + self.ui.galleryList.setModel(self.mdl) + self.ui.galleryList.selectionModel().currentChanged.connect(self.showGallery) + self.ui.galleryList.hide() + + self.ui.imageList.setModel(self.mdl) + self.ui.imageList.selectionModel().currentChanged.connect(self.showImageInfo) + self.ui.imageList.activated.connect(self.openImage) + + # backendEdit choice + + self.fillBackends() + self.ui.backendEdit.currentIndexChanged.connect(self.changeBackend) + + if self.weboob.count_backends() == 0: + self.backendsConfig() + self.ui.actionBackends.triggered.connect(self.backendsConfig) + + self.lastSaveDir = os.path.expanduser('~') + + @Slot() + def backendsConfig(self): + cfg = BackendCfg(self.weboob, (CapImage,), self) + if cfg.run(): + self.fillBackends() + + def fillBackends(self): + model = BackendListModel(self.weboob) + model.addBackends(CapGallery, entry_title=True) + model.addBackends(CapImage, entry_title=True) + self.ui.backendEdit.setModel(model) + + def selectedBackend(self): + cap = self.ui.backendEdit.currentData(BackendListModel.RoleCapability) + backend = self.ui.backendEdit.currentData(BackendListModel.RoleBackendName) + return cap, backend + + def _collectionBackends(self): + cap, backend = self.selectedBackend() + if backend is not None: + return [backend] + + backends = self.weboob.iter_backends(caps=cap) + return [b for b in backends if b.has_caps(CapCollection)] + + @Slot() + def changeBackend(self): + cap, backend = self.selectedBackend() + + if cap is CapImage: + res_class = BaseImage + else: + res_class = BaseGallery + self.mdl.setResourceClasses([res_class]) + + @Slot(QModelIndex) + def showCollection(self, qidx): + cap, _ = self.selectedBackend() + + qidx = self.proxy_mdl.mapToSource(qidx) + qidx = qidx.sibling(qidx.row(), 0) + if cap is CapImage: + self.ui.galleryList.hide() + self.ui.imageList.setRootIndex(qidx) + self.ui.imageList.setEnabled(True) + self.ui.imageList.show() + else: + self.ui.galleryList.show() + self.ui.galleryList.setRootIndex(qidx) + self.ui.galleryList.setEnabled(True) + + @Slot(QModelIndex) + def showGallery(self, qidx): + self.ui.imageList.setEnabled(True) + self.ui.imageList.show() + qidx = qidx.sibling(qidx.row(), 0) + self.ui.imageList.setRootIndex(qidx) + + def showNoneItem(self): + self.ui.labelTitle.setText('-') + self.ui.labelDescription.setText('-') + self.ui.labelAuthor.setText('-') + self.ui.labelDate.setText('-') + self.ui.labelLink.setText('-') + self.ui.labelSize.setText('-') + self.ui.labelRating.setText('-') + + @Slot(QModelIndex) + def showImageInfo(self, qidx): + image = qidx.data(self.mdl.RoleObject) + if image is None: + self.showNoneItem() + return + + self.ui.labelTitle.setText(image.title or '') + self.ui.labelDescription.setText(image.description or '') + self.ui.labelAuthor.setText(image.author or '') + if image.size: + self.ui.labelSize.setText(size_format(image.size) or '') + else: + self.ui.labelSize.setText('-') + if image.url: + self.ui.labelLink.setText('Link' % image.url) + else: + self.ui.labelLink.setText('') + + @Slot(QModelIndex) + def openImage(self, qidx): + viewer = Viewer(self.weboob, self) + viewer.setData(self.mdl, qidx) + viewer.show() + + @Slot() + def startSearch(self): + pattern = self.ui.searchEdit.text() + if not pattern: + return + + self.mdl.clear() + cap, backend = self.selectedBackend() + if cap is CapImage: + self.ui.galleryList.hide() + self.ui.imageList.setRootIndex(QModelIndex()) + self.mdl.addRootDo('search_image', pattern, backends=backend) + self.ui.imageList.setEnabled(True) + self.ui.imageList.show() + elif cap is CapGallery: + self.ui.imageList.hide() + self.ui.galleryList.setRootIndex(QModelIndex()) + self.mdl.addRootDo('search_galleries', pattern, backends=backend) + self.ui.galleryList.setEnabled(True) + self.ui.galleryList.show() + + @Slot() + def startBrowse(self): + self.ui.collectionTree.setEnabled(True) + self.ui.galleryList.setEnabled(False) + self.ui.imageList.setEnabled(False) + + self.mdl.clear() + + cap, backend = self.selectedBackend() + if cap is CapImage: + self.ui.galleryList.hide() + res_class = BaseImage + else: + self.ui.galleryList.show() + res_class = BaseGallery + self.mdl.setResourceClasses([res_class]) + + backends = self._collectionBackends() + self.mdl.addRootDo('iter_resources', [res_class], [], backends=backends) diff --git a/weboob/applications/qgalleroob/qgalleroob.py b/weboob/applications/qgalleroob/qgalleroob.py new file mode 100644 index 0000000000000000000000000000000000000000..512d979ae934103b5d86b995b523d5f15ee24392 --- /dev/null +++ b/weboob/applications/qgalleroob/qgalleroob.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2016 Vincent A +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.capabilities.gallery import CapGallery +from weboob.capabilities.image import CapImage +from weboob.tools.application.qt5 import QtApplication +from weboob.tools.config.yamlconfig import YamlConfig + +from .main_window import MainWindow + + +class QGalleroob(QtApplication): + APPNAME = 'qgalleroob' + VERSION = '1.1' + COPYRIGHT = u'Copyright(C) 2016 Vincent A' + DESCRIPTION = "Qt application to view image galleries." + SHORT_DESCRIPTION = "search for images" + #~ CONFIG = {'queries': {}} + #~ STORAGE = {'bookmarks': [], 'read': [], 'notes': {}} + + def main(self, argv): + self.load_backends(CapGallery) + self.load_backends(CapImage) + self.create_storage() + self.load_config(klass=YamlConfig) + + main_window = MainWindow(self.config, self.storage, self.weboob) + main_window.show() + return self.weboob.loop() diff --git a/weboob/applications/qgalleroob/ui/Makefile b/weboob/applications/qgalleroob/ui/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..85940ca51919cc00f42b9369b58e58d0d9e6fa72 --- /dev/null +++ b/weboob/applications/qgalleroob/ui/Makefile @@ -0,0 +1,13 @@ +UI_FILES = $(wildcard *.ui) +UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py) +PYUIC = pyuic5 + +all: $(UI_PY_FILES) + +%_ui.py: %.ui + $(PYUIC) -o $@ $^ + +clean: + rm -f *.pyc + rm -f $(UI_PY_FILES) + diff --git a/weboob/applications/qgalleroob/ui/__init__.py b/weboob/applications/qgalleroob/ui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/weboob/applications/qgalleroob/ui/mainwindow.ui b/weboob/applications/qgalleroob/ui/mainwindow.ui new file mode 100644 index 0000000000000000000000000000000000000000..d1b30a881add1da31f8d027d523068cdd8cf6c94 --- /dev/null +++ b/weboob/applications/qgalleroob/ui/mainwindow.ui @@ -0,0 +1,491 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + QGalleroob + + + + + + + true + + + + + 0 + 0 + 464 + 472 + + + + + + + Qt::Vertical + + + + Qt::ActionsContextMenu + + + QListView::Static + + + QListView::LeftToRight + + + true + + + QListView::Adjust + + + + 256 + 256 + + + + QListView::IconMode + + + true + + + + + QListView::Static + + + QListView::LeftToRight + + + true + + + QListView::Adjust + + + + 256 + 256 + + + + QListView::IconMode + + + true + + + + + + + + + + + + + + 0 + 0 + 800 + 19 + + + + + + ToolBar + + + false + + + false + + + TopToolBarArea + + + false + + + + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Info + + + 2 + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 75 + true + + + + Title + + + + + + + + 75 + true + + + + Description + + + + + + + + 75 + true + + + + Author + + + + + + + - + + + Qt::PlainText + + + + + + + - + + + true + + + + + + + - + + + Qt::PlainText + + + + + + + - + + + true + + + true + + + + + + + + 75 + true + + + + Date + + + + + + + - + + + Qt::PlainText + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 75 + true + + + + Size + + + + + + + + 75 + true + + + + Rating + + + + + + + - + + + Qt::PlainText + + + + + + + - + + + Qt::PlainText + + + true + + + + + + + + + + + + QDockWidget::NoDockWidgetFeatures + + + Qt::TopDockWidgetArea + + + 4 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Find + + + 1 + + + + + + + 0 + + + + + 0 + 0 + 167 + 399 + + + + Search + + + + + + + + Text to search... + + + + + + + Go + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 0 + 196 + 118 + + + + Browse + + + + + + + + Start browsing collections + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + false + + + 0 + + + + + + + + + + + + + Backends + + + + + Download gallery... + + + + + Save image... + + + + + Download to CBZ... + + + + + + diff --git a/weboob/applications/qgalleroob/ui/viewer.ui b/weboob/applications/qgalleroob/ui/viewer.ui new file mode 100644 index 0000000000000000000000000000000000000000..74ddb76aa82c13657d437c6202016d73cda78930 --- /dev/null +++ b/weboob/applications/qgalleroob/ui/viewer.ui @@ -0,0 +1,231 @@ + + + Viewer + + + + 0 + 0 + 800 + 600 + + + + QGalleroob viewer + + + + + + + true + + + Qt::AlignCenter + + + + + 0 + 0 + 780 + 503 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::AlignCenter + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + |< + + + + + + + + + + < + + + + + + + + + + 1/N + + + + + + + > + + + + + + + + + + >| + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + 0 + 0 + 800 + 19 + + + + + File + + + + + + + + + Close + + + + + + + + Save image... + + + + + + + + Original size + + + G + + + + + + + + Fit to window + + + F + + + + + + + + Zoom out + + + - + + + + + + + + Zoom in + + + + + + + + + + diff --git a/weboob/applications/qgalleroob/viewer.py b/weboob/applications/qgalleroob/viewer.py new file mode 100644 index 0000000000000000000000000000000000000000..2824374d2098fc63ebf51aec729cfc083290e222 --- /dev/null +++ b/weboob/applications/qgalleroob/viewer.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2016 Vincent A +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +import os +import re + +from PyQt5.QtWidgets import QFileDialog, QMessageBox +from PyQt5.QtGui import QPixmap, QImage, QKeySequence +from PyQt5.QtCore import Qt, pyqtSlot as Slot + +from weboob.tools.application.qt5 import QtMainWindow, QtDo +from weboob.tools.application.qt5.models import ResultModel +from weboob.capabilities.base import NotLoaded + +from .ui.viewer_ui import Ui_Viewer + + +ZOOM_FACTOR, ZOOM_FIT = range(2) + + +class Viewer(QtMainWindow): + def __init__(self, weboob, parent=None): + super(Viewer, self).__init__(parent) + self.ui = Ui_Viewer() + self.ui.setupUi(self) + self.ui.prevButton.clicked.connect(self.prev) + self.ui.nextButton.clicked.connect(self.next) + self.ui.firstButton.clicked.connect(self.first) + self.ui.lastButton.clicked.connect(self.last) + self.ui.actionZoomIn.triggered.connect(self.zoomIn) + self.ui.actionZoomOut.triggered.connect(self.zoomOut) + self.ui.actionFullSize.triggered.connect(self.zoomFullSize) + self.ui.actionFitWindow.triggered.connect(self.zoomFit) + + self.ui.actionSaveImage.setShortcut(QKeySequence.Save) + self.ui.actionSaveImage.triggered.connect(self.saveImage) + self.ui.actionClose.setShortcut(QKeySequence.Close) + self.ui.actionClose.triggered.connect(self.close) + + self.model = None + self.current = None + self.total = 0 + self.zoomFactor = 1 + self.zoomMode = ZOOM_FACTOR + self.weboob = weboob + + def setData(self, model, qidx): + self.model = model + self.current = qidx + + self.model.rowsInserted.connect(self.updatePos) + self.model.rowsRemoved.connect(self.updatePos) + self.model.rowsInserted.connect(self.updateNavButtons) + self.model.rowsRemoved.connect(self.updateNavButtons) + + self.updateImage() + + def updateNavButtons(self): + prev = self.current.row() > 0 + self.ui.prevButton.setEnabled(prev) + self.ui.firstButton.setEnabled(prev) + next = self.current.row() < self.total - 1 + self.ui.nextButton.setEnabled(next) + self.ui.lastButton.setEnabled(next) + + def updatePos(self): + self.total = self.model.rowCount(self.current.parent()) + self.ui.posLabel.setText('%d / %d' % (self.current.row() + 1, self.total)) + + def _gotData(self, obj, qidx): + if self.current == qidx: + self.pixmap = QPixmap(QImage.fromData(obj.data)) + self._rebuildImage() + + def updateImage(self): + self.updatePos() + self.updateNavButtons() + + obj = self.current.data(ResultModel.RoleObject) + pixmap = QPixmap() + + if obj.data is NotLoaded: + qidx = self.current + process = QtDo(self.weboob, lambda r: self._gotData(r, qidx)) + process.do('fillobj', obj, ['data']) + elif obj.data: + pixmap = QPixmap(QImage.fromData(obj.data)) + self.pixmap = pixmap + + self._rebuildImage() + + @Slot() + def next(self): + new = self.current.sibling(self.current.row() + 1, 0) + if not new.isValid(): + return + self.current = new + self.updateImage() + + @Slot() + def prev(self): + if self.current.row() == 0: + return + self.current = self.current.sibling(self.current.row() - 1, 0) + self.updateImage() + + @Slot() + def first(self): + self.current = self.current.sibling(0, 0) + self.updateImage() + + @Slot() + def last(self): + self.current = self.current.sibling(self.total - 1, 0) + self.updateImage() + + @Slot() + def zoomIn(self): + self.zoomFactor *= 1.25 + self.zoomMode = ZOOM_FACTOR + self._rebuildImage() + + @Slot() + def zoomOut(self): + self.zoomFactor *= 0.75 + self.zoomMode = ZOOM_FACTOR + self._rebuildImage() + + @Slot() + def zoomFullSize(self): + self.zoomFactor = 1 + self.zoomMode = ZOOM_FACTOR + self._rebuildImage() + + @Slot() + def zoomFit(self): + self.zoomMode = ZOOM_FIT + self._rebuildImage() + + def resizeEvent(self, ev): + super(Viewer, self).resizeEvent(ev) + if self.zoomMode == ZOOM_FIT: + self._rebuildImage() + + def _rebuildZoom(self): + if self.zoomMode == ZOOM_FACTOR: + new_width = int(self.pixmap.width() * self.zoomFactor) + pixmap = self.pixmap.scaledToWidth(new_width, Qt.SmoothTransformation) + else: + new_size = self.ui.scrollArea.viewport().size() + pixmap = self.pixmap.scaled(new_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.zoomFactor = pixmap.width() / float(self.pixmap.width()) + return pixmap + + def _rebuildImage(self): + if self.pixmap.isNull(): + pixmap = self.pixmap + else: + pixmap = self._rebuildZoom() + + self.ui.view.setPixmap(pixmap) + + @Slot() + def saveImage(self): + def ext_for_filter(s): + return re.match(r'(?:[A-Z]+) \(\*\.([a-z]+)\)$', s).group(1) + + if not self.pixmap: + return + + filters = ['PNG (*.png)', 'JPEG (*.jpg)', 'GIF (*.gif)'] + + obj = self.current.data(ResultModel.RoleObject) + name = '%s.%s' % (obj.title or obj.id or u'', obj.ext or 'png') + default = filters[0] + for f in filters: + if name.endswith(ext_for_filter(f)): + default = f + filters = ';;'.join(filters) + + target = os.path.join(self.parent().lastSaveDir, name) + out, filter = QFileDialog.getSaveFileName(self, 'Save image', target, filters, default) + if not out: + return + + ext = ext_for_filter(filter) + + self.parent().lastSaveDir = os.path.dirname(out) + if not os.path.splitext(out)[1]: + out = '%s.%s' % (out, ext) + + if os.path.exists(out): + q = self.tr('%s already exists, are you sure you want to replace it?') % out + reply = QMessageBox.question(self, self.tr('Overwrite?'), q) + if reply == QMessageBox.No: + return self.saveImage() + + self.pixmap.save(out, ext.upper())