diff --git a/src/app/qml/Book.qml b/src/app/qml/Book.qml index 66b8ad4..70677d1 100644 --- a/src/app/qml/Book.qml +++ b/src/app/qml/Book.qml @@ -1,528 +1,560 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ import QtQuick 2.2 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.2 as QtControls import QtQuick.Window 2.2 import org.kde.kirigami 2.1 as Kirigami import org.kde.peruse 0.1 as Peruse import "listcomponents" as ListComponents /** * @brief Page that handles reading the book. * * */ Kirigami.Page { id: root; objectName: "bookViewer"; clip: true; // Remove all the padding when we've hidden controls. Content is king! topPadding: applicationWindow().controlsVisible ? (applicationWindow() && applicationWindow().header ? applicationWindow().header.height : 0) : 0; leftPadding: applicationWindow().controlsVisible ? Kirigami.Units.gridUnit : 0; rightPadding: applicationWindow().controlsVisible ? Kirigami.Units.gridUnit : 0; bottomPadding: applicationWindow().controlsVisible ? Kirigami.Units.gridUnit : 0; background: Rectangle { anchors.fill: parent; opacity: applicationWindow().controlsVisible ? 0 : 1; Behavior on opacity { NumberAnimation { duration: applicationWindow().animationDuration; } } color: "black"; } // Perhaps we should store and restore this? property bool showControls: true; property Item pageStackItem: applicationWindow().pageStack.layers.currentItem; onPageStackItemChanged: { if(root.isCurrentPage) { applicationWindow().controlsVisible = root.showControls; } else { root.showControls = applicationWindow().controlsVisible; applicationWindow().controlsVisible = true; } } property bool rtlMode: false; /** * zoomMode: Peruse.Config.ZoomMode */ property int zoomMode: Peruse.Config.ZoomFull; property string file; property int currentPage; property int totalPages; onCurrentPageChanged: { // set off a timer to slightly postpone saving the current page, so it doesn't happen during animations etc updateCurrent.start(); } + function nextFrame() { + // If there is a next frame to go to, or whether it is supported at all + if(viewLoader.item.hasFrames === true) { + viewLoader.item.nextFrame(); + } + else { + nextPage(); + } + } + function previousFrame() { + // If there is a next frame to go to, or whether it is supported at all + if(viewLoader.item.hasFrames === true) { + viewLoader.item.previousFrame(); + } + else { + previousPage(); + } + } function nextPage() { if(viewLoader.item.currentPage < viewLoader.item.pageCount - 1) { viewLoader.item.currentPage++; } else { bookInfo.showBookInfo(file); } } function previousPage() { if(viewLoader.item.currentPage > 0) { viewLoader.item.currentPage--; } else { bookInfo.showBookInfo(file); } } function closeBook() { applicationWindow().contextDrawer.close(); // also for storing current page (otherwise postponed a bit after page change, done here as well to ensure it really happens) applicationWindow().controlsVisible = true; applicationWindow().pageStack.layers.pop(); applicationWindow().globalDrawer.open(); } property Item contextualTopItems: ListView { id: thumbnailNavigator; anchors.fill: parent; clip: true; delegate: thumbnailComponent; } Component { id: thumbnailComponent; Item { width: parent.width; height: Kirigami.Units.gridUnit * 6; MouseArea { anchors.fill: parent; onClicked: viewLoader.item.currentPage = model.index; } Rectangle { anchors.fill: parent; color: Kirigami.Theme.highlightColor; opacity: root.currentPage === model.index ? 1 : 0; Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } } Image { anchors { top: parent.top; left: parent.left; right: parent.right; margins: Kirigami.Units.smallSpacing; } height: parent.height - pageTitle.height - Kirigami.Units.smallSpacing * 2; asynchronous: true; fillMode: Image.PreserveAspectFit; source: model.url; } QtControls.Label { id: pageTitle; anchors { left: parent.left; right: parent.right; bottom: parent.bottom; } height: paintedHeight; text: model.title; elide: Text.ElideMiddle; horizontalAlignment: Text.AlignHCenter; } } } function toggleFullscreen() { applicationWindow().contextDrawer.close(); if(applicationWindow().visibility !== Window.FullScreen) { applicationWindow().visibility = Window.FullScreen; } else { applicationWindow().visibility = Window.AutomaticVisibility; } } property list mobileActions: [ Kirigami.Action { text: applicationWindow().visibility !== Window.FullScreen ? i18nc("Enter full screen mode on a touch-based device", "Go Full Screen") : i18nc("Exit full sceen mode on a touch based device", "Exit Full Screen"); iconName: "view-fullscreen"; onTriggered: toggleFullscreen(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypePhone; }, Kirigami.Action { text: i18nc("Action used on touch devices to close the currently open book and return to whatever page was most recently shown", "Close Book"); shortcut: bookInfo.sheetOpen ? "" : "Esc"; iconName: "dialog-close"; onTriggered: closeBook(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypePhone; } ] property list desktopActions: [ Kirigami.Action { text: i18nc("Top level entry leading to a submenu with options for the book display", "View Options"); iconName: "configure"; QtObject { property string text: "Reading Direction" } Kirigami.Action { text: "Left to Right" iconName: "format-text-direction-ltr"; shortcut: rtlMode ? "r" : ""; enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.rtlMode === true; onTriggered: { root.rtlMode = false; } } Kirigami.Action { text: "Right to Left" iconName: "format-text-direction-rtl"; shortcut: rtlMode ? "" : "r"; enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.rtlMode === false; onTriggered: { root.rtlMode = true; } } QtObject {} // QtObject { // property string text: "Zoom" // } // Kirigami.Action { // text: "Fit full page" // iconName: "zoom-fit-best"; // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.zoomMode !== Peruse.Config.ZoomFull; // onTriggered: { root.zoomMode = Peruse.Config.ZoomFull; } // } // Kirigami.Action { // text: "Fit width" // iconName: "zoom-fit-width"; // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.zoomMode !== Peruse.Config.ZoomFitWidth; // onTriggered: { root.zoomMode = Peruse.Config.ZoomFitWidth; } // } // Kirigami.Action { // text: "Fit height" // iconName: "zoom-fit-height"; // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.zoomMode !== Peruse.Config.ZoomFitHeight; // onTriggered: { root.zoomMode = Peruse.Config.ZoomFitHeight; } // } // QtObject {} }, Kirigami.Action { - text: i18nc("Go to the previous page in the book", "Previous Page"); + text: i18nc("Go to the previous frame on the current page", "Previous Frame"); shortcut: root.isCurrentPage && bookInfo.sheetOpen ? "" : StandardKey.MoveToPreviousChar; iconName: "go-previous"; + onTriggered: previousFrame(); + enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; + }, + Kirigami.Action { + text: i18nc("Go to the next frame on the current page", "Next Frame"); + shortcut: root.isCurrentPage && bookInfo.sheetOpen ? "" : StandardKey.MoveToNextChar; + iconName: "go-next"; + onTriggered: nextFrame(); + enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; + }, + Kirigami.Action { + text: i18nc("Go to the previous page in the book", "Previous Page"); + shortcut: root.isCurrentPage && bookInfo.sheetOpen ? "" : StandardKey.MoveToNextPage; + iconName: "go-previous"; onTriggered: previousPage(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { text: i18nc("Go to the next page in the book", "Next Page"); - shortcut: bookInfo.sheetOpen ? "" : StandardKey.MoveToNextChar; + shortcut: bookInfo.sheetOpen ? "" : StandardKey.MoveToNextPage; iconName: "go-next"; onTriggered: nextPage(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { text: applicationWindow().visibility !== Window.FullScreen ? i18nc("Enter full screen mode on a non-touch-based device", "Go Full Screen") : i18nc("Exit full sceen mode on a non-touch based device", "Exit Full Screen"); shortcut: (applicationWindow().visibility === Window.FullScreen) ? (bookInfo.sheetOpen ? "" : "Esc") : "f"; iconName: "view-fullscreen"; onTriggered: toggleFullscreen(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { text: i18nc("Action used on non-touch devices to close the currently open book and return to whatever page was most recently shown", "Close Book"); shortcut: (applicationWindow().visibility === Window.FullScreen) ? "" : (bookInfo.sheetOpen ? "" : "Esc"); iconName: "dialog-close"; onTriggered: closeBook(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, // Invisible actions, for use in bookInfo Kirigami.Action { visible: false; shortcut: bookInfo.sheetOpen ? StandardKey.MoveToPreviousChar : ""; onTriggered: bookInfo.previousBook(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { visible: false; shortcut: bookInfo.sheetOpen ? StandardKey.MoveToNextChar : ""; onTriggered: bookInfo.nextBook(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { visible: false; shortcut: bookInfo.sheetOpen ? "Return" : ""; onTriggered: bookInfo.openSelected(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; } ] actions { contextualActions: applicationWindow().deviceType === applicationWindow().deviceTypePhone ? mobileActions : desktopActions; main: bookInfo.sheetOpen ? bookInfoAction : mainBookAction; } Kirigami.Action { id: mainBookAction; text: applicationWindow().visibility !== Window.FullScreen ? i18nc("Enter full screen mode on any device type", "Go Full Screen") : i18nc("Exit full screen mode on any device type", "Exit Full Screen"); iconName: "view-fullscreen"; onTriggered: toggleFullscreen(); enabled: root.isCurrentPage; } Kirigami.Action { id: bookInfoAction; text: i18nc("Closes the book information drawer", "Close"); shortcut: bookInfo.sheetOpen ? "Esc" : ""; iconName: "dialog-cancel"; onTriggered: bookInfo.close(); enabled: root.isCurrentPage; } /** * This holds an instance of ViewerBase, which can either be the * Okular viewer(the fallback one), or one of the type specific * ones(ImageBrowser based). */ Item { width: root.width - (root.leftPadding + root.rightPadding); height: root.height - (root.topPadding + root.bottomPadding); Timer { id: updateCurrent; interval: applicationWindow().animationDuration; running: false; repeat: false; onTriggered: { if(viewLoader.item && viewLoader.item.pagesModel && viewLoader.item.pagesModel.currentPage !== undefined) { viewLoader.item.pagesModel.currentPage = root.currentPage; } } } NumberAnimation { id: thumbnailMovementAnimation; target: thumbnailNavigator; property: "contentY"; duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } Loader { id: viewLoader; anchors.fill: parent; property bool loadingCompleted: false; onLoaded: { if(status === Loader.Error) { // huh! problem... } else { item.file = root.file; } } Binding { target: viewLoader.item; property: "rtlMode"; value: root.rtlMode; } Binding { target: viewLoader.item; property: "zoomMode"; value: root.zoomMode; } Connections { target: viewLoader.item; onLoadingCompleted: { if(success) { thumbnailNavigator.model = viewLoader.item.pagesModel; if(viewLoader.item.thumbnailComponent) { thumbnailNavigator.delegate = viewLoader.item.thumbnailComponent; } else { thumbnailNavigator.delegate = thumbnailComponent; } peruseConfig.setFilesystemProperty(root.file, "totalPages", viewLoader.item.pageCount); if(root.totalPages !== viewLoader.item.pageCount) { root.totalPages = viewLoader.item.pageCount; } viewLoader.item.currentPage = root.currentPage; viewLoader.loadingCompleted = true; root.title = viewLoader.item.title; applicationWindow().globalDrawer.close(); } } onCurrentPageChanged: { if(root.currentPage !== viewLoader.item.currentPage && viewLoader.loadingCompleted) { root.currentPage = viewLoader.item.currentPage; } thumbnailMovementAnimation.running = false; var currentPos = thumbnailNavigator.contentY; var newPos; thumbnailNavigator.positionViewAtIndex(viewLoader.item.currentPage, ListView.Center); newPos = thumbnailNavigator.contentY; thumbnailMovementAnimation.from = currentPos; thumbnailMovementAnimation.to = newPos; thumbnailMovementAnimation.running = true; } onGoNextPage: root.nextPage(); onGoPreviousPage: root.previousPage(); } } } /** * Overlay with book information and a series selection. */ Kirigami.OverlaySheet { id: bookInfo; function setNewCurrentIndex(newIndex) { seriesListAnimation.running = false; var currentPos = seriesListView.contentX; var newPos; seriesListView.positionViewAtIndex(newIndex, ListView.Center); newPos = seriesListView.contentX; seriesListAnimation.from = currentPos; seriesListAnimation.to = newPos; seriesListAnimation.running = true; seriesListView.currentIndex = newIndex; } function nextBook() { if(seriesListView.currentIndex < seriesListView.model.rowCount()) { setNewCurrentIndex(seriesListView.currentIndex + 1); } } function previousBook() { if(seriesListView.currentIndex > 0) { setNewCurrentIndex(seriesListView.currentIndex - 1); } } function openSelected() { if (detailsTile.filename!==root.file) { closeBook(); applicationWindow().showBook(detailsTile.filename, detailsTile.currentPage); } } function showBookInfo(filename) { if(sheetOpen) { return; } seriesListView.model = contentList.seriesModelForEntry(filename); if (seriesListView.model) { setNewCurrentIndex(seriesListView.model.indexOfFile(filename)); } open(); } onSheetOpenChanged: { if(sheetOpen === false) { applicationWindow().controlsVisible = controlsShown; } else { controlsShown = applicationWindow().controlsVisible; applicationWindow().controlsVisible = true; } } property bool controlsShown; property QtObject currentBook: fakeBook; property QtObject fakeBook: Peruse.PropertyContainer { property var author: [""]; property string title: ""; property string filename: ""; property string publisher: ""; property string thumbnail: ""; property string currentPage: "0"; property string totalPages: "0"; property string comment: ""; property var tags: [""]; property var description: [""]; property string rating: "0"; } Column { clip: true; width: root.width - Kirigami.Units.largeSpacing * 2; height: childrenRect.height + Kirigami.Units.largeSpacing * 2; spacing: Kirigami.Units.largeSpacing; ListComponents.BookTile { id: detailsTile; height: neededHeight; width: parent.width; author: bookInfo.currentBook.readProperty("author"); publisher: bookInfo.currentBook.readProperty("publisher"); title: bookInfo.currentBook.readProperty("title"); filename: bookInfo.currentBook.readProperty("filename"); thumbnail: bookInfo.currentBook.readProperty("thumbnail"); categoryEntriesCount: 0; currentPage: bookInfo.currentBook.readProperty("currentPage"); totalPages: bookInfo.currentBook.readProperty("totalPages"); description: bookInfo.currentBook.readProperty("description"); onBookSelected: { if(root.file !== fileSelected) { openSelected(); } } onBookDeleteRequested: { // Not strictly needed for the listview itself, but it's kind of // nice for making sure the details tile is right var oldIndex = seriesListView.currentIndex; seriesListView.currentIndex = -1; contentList.removeBook(fileSelected, true); seriesListView.currentIndex = oldIndex; } } // tags and ratings, comment by self // store hook for known series with more content ListView { id: seriesListView; width: parent.width; height: Kirigami.Units.gridUnit * 12; orientation: ListView.Horizontal; NumberAnimation { id: seriesListAnimation; target: seriesListView; property: "contentX"; duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } delegate: ListComponents.BookTileTall { height: model.filename !== "" ? neededHeight : 1; width: seriesListView.width / 3; author: model.author; title: model.title; filename: model.filename; thumbnail: model.thumbnail; categoryEntriesCount: 0; currentPage: model.currentPage; totalPages: model.totalPages; onBookSelected:{ if (seriesListView.currentIndex !== model.index) { bookInfo.setNewCurrentIndex(model.index); } else { bookInfo.openSelected(); } } selected: seriesListView.currentIndex === model.index; } onCurrentIndexChanged: { bookInfo.currentBook = model.get(currentIndex); } } } } onFileChanged: { // Let's set the page title to something useful var book = contentList.bookFromFile(file); root.title = book.readProperty("title"); // The idea is to have a number of specialised options as relevant to various // types of comic books, and then finally fall back to Okular as a catch-all // but generic viewer component. var attemptFallback = true; var mimetype = contentList.contentModel.getMimetype(file); console.debug("Mimetype is " + mimetype); if(mimetype == "application/x-cbz" || mimetype == "application/x-cbr" || mimetype == "application/vnd.comicbook+zip" || mimetype == "application/vnd.comicbook+rar") { viewLoader.source = "viewers/cbr.qml"; attemptFallback = false; } if(mimetype == "inode/directory" || mimetype == "image/jpeg" || mimetype == "image/png") { viewLoader.source = "viewers/folderofimages.qml"; attemptFallback = false; } if(attemptFallback) { viewLoader.source = "viewers/okular.qml"; } } } diff --git a/src/app/qml/viewers/ImageBrowser.qml b/src/app/qml/viewers/ImageBrowser.qml index abd3a2c..a549c3d 100644 --- a/src/app/qml/viewers/ImageBrowser.qml +++ b/src/app/qml/viewers/ImageBrowser.qml @@ -1,294 +1,298 @@ /* * Copyright (C) 2015 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ import QtQuick 2.3 // import QtQuick.Layouts 1.1 // import QtQuick.Controls 1.0 as QtControls import org.kde.kirigami 2.1 as Kirigami /** * @brief The image viewer used by the CBR and Folder Viewer Base classes. * * It handles drawing the image and the different zoom modes. */ ListView { id: root + function goNextFrame() { root.currentItem.goNextFrame(); } + function goPreviousFrame() { root.currentItem.goPreviousFrame(); } signal goNextPage(); signal goPreviousPage(); property int imageWidth property int imageHeight orientation: ListView.Horizontal snapMode: ListView.SnapOneItem cacheBuffer: 3000 // This ensures that the current index is always up to date, which we need to ensure we can track the current page // as required by the thumbnail navigator, and the resume-reading-from functionality onMovementEnded: { var indexHere = indexAt(contentX + width / 2, contentY + height / 2); if(currentIndex !== indexHere) { currentIndex = indexHere; } } /** * An interactive area with an image. * * Clicking once on the image will hide all other controls from view. * Clicking twice will instead zoom in. * * Pinch will zoom in as well. */ delegate: Flickable { id: flick width: imageWidth height: imageHeight contentWidth: imageWidth contentHeight: imageHeight interactive: contentWidth > width || contentHeight > height onInteractiveChanged: root.interactive = !interactive; z: interactive ? 1000 : 0 + function goNextFrame() { image.nextFrame(); } + function goPreviousFrame() { image.previousFrame(); } PinchArea { width: Math.max(flick.contentWidth, flick.width) height: Math.max(flick.contentHeight, flick.height) property real initialWidth property real initialHeight onPinchStarted: { initialWidth = flick.contentWidth initialHeight = flick.contentHeight } onPinchUpdated: { // adjust content pos due to drag flick.contentX += pinch.previousCenter.x - pinch.center.x flick.contentY += pinch.previousCenter.y - pinch.center.y // resize content flick.resizeContent(Math.max(imageWidth, initialWidth * pinch.scale), Math.max(imageHeight, initialHeight * pinch.scale), pinch.center) } onPinchFinished: { // Move its content within bounds. flick.returnToBounds(); } Image { id: image width: flick.contentWidth height: flick.contentHeight source: model.url fillMode: Image.PreserveAspectFit asynchronous: true property bool shouldCheat: imageWidth * 2 > maxTextureSize || imageHeight * 2 > maxTextureSize; property bool isTall: imageHeight < imageWidth; property int fixedWidth: isTall ? maxTextureSize * (imageWidth / imageHeight) : maxTextureSize; property int fixedHeight: isTall ? maxTextureSize : maxTextureSize * (imageHeight / imageWidth); sourceSize.width: shouldCheat ? fixedWidth : imageWidth * 2; sourceSize.height: shouldCheat ? fixedHeight : imageHeight * 2; MouseArea { anchors.fill: parent onClicked: startToggleControls(); onDoubleClicked: { abortToggleControls(); if (flick.interactive) { flick.resizeContent(imageWidth, imageHeight, {x: imageWidth/2, y: imageHeight/2}); } else { flick.resizeContent(imageWidth * 2, imageHeight * 2, Qt.point(mouse.x, mouse.y)); } } } // Setup for all the entries. property QtObject currentPageObject: { if (root.model.acbfData) { if (model.index===0) { currentPageObject = root.model.acbfData.metaData.bookInfo.coverpage(); } else if (model.index > 0) { currentPageObject = root.model.acbfData.body.page(model.index-1); } } else { null; } } property real muliplier: isTall? (paintedHeight / implicitHeight): (paintedWidth / implicitWidth); property int offsetX: (width-paintedWidth)/2; property int offsetY: (height-paintedHeight)/2; function focusOnFrame(index) { if (index>-1) { var frameBounds = image.currentPageObject.frame(index).bounds; var frameMultiplier = image.implicitWidth/frameBounds.width; //Check if the height of the final frame is higher than the contentHeight //When we're using a *fit to with scheme. if ((frameBounds.height/frameBounds.width)*contentWidth > contentHeight) { frameMultiplier = image.implicitHeight/frameBounds.height; frameMultiplier = frameMultiplier * (contentHeight/image.paintedHeight); } else { frameMultiplier = frameMultiplier * (contentWidth/image.paintedWidth); } flick.resizeContent(imageWidth * frameMultiplier, imageHeight * frameMultiplier, Qt.point(0,0)); var frameRect = Qt.rect(image.muliplier * frameBounds.x + image.offsetX, image.muliplier * frameBounds.y+ image.offsetY, image.muliplier * frameBounds.width, image.muliplier * frameBounds.height); flick.contentX = frameRect.x - (flick.width-frameRect.width)/2; flick.contentY = frameRect.y - (flick.height-frameRect.height)/2; } } function nextFrame() { if (image.totalFrames > 0 && image.currentFrame+1 < image.totalFrames) { image.currentFrame++; } else { root.goNextPage(); image.currentFrame = -1; } } function previousFrame() { if (image.totalFrames > 0 && image.currentFrame-1 > -1) { image.currentFrame--; } else { root.goPreviousPage(); image.currentFrame = -1; } } property int totalFrames: image.currentPageObject? image.currentPageObject.framePointStrings.length: 0; property int currentFrame: -1; onCurrentFrameChanged: focusOnFrame(currentFrame); Repeater { model: image.currentPageObject? image.currentPageObject.framePointStrings: 0; Rectangle { id: frame; x: image.muliplier * image.currentPageObject.frame(index).bounds.x + image.offsetX; y: image.muliplier * image.currentPageObject.frame(index).bounds.y + image.offsetY; width: { image.muliplier * image.currentPageObject.frame(index).bounds.width; } height: image.muliplier * image.currentPageObject.frame(index).bounds.height; color: "blue"; opacity: 0; MouseArea { anchors.fill: parent; onClicked: startToggleControls(); preventStealing: true; onDoubleClicked: { abortToggleControls(); if (flick.interactive && image.currentFrame == index) { flick.resizeContent(imageWidth, imageHeight, {x: imageWidth/2, y: imageHeight/2}); image.currentFrame = -1; } else { image.currentFrame = index; mouse.accepted; } } } } } MouseArea { anchors { top: parent.top; left: parent.left; bottom: parent.bottom; } width: Math.max(parent.width / 6, (parent.width-image.paintedWidth)/2); preventStealing: true; onClicked: root.layoutDirection === Qt.RightToLeft? image.previousFrame(): image.nextFrame(); hoverEnabled: true; onPositionChanged: { var hWidth = width/2; var hHeight = height/2; var opacityX = mouse.x>hWidth? hWidth-(mouse.x-hWidth) : mouse.x; opacityX = opacityX/(hWidth - (Kirigami.Units.iconSizes.huge/2)); var opacityY = mouse.y>hHeight? hHeight-(mouse.y-hHeight) : mouse.y; opacityY = opacityY/(hHeight - (Kirigami.Units.iconSizes.huge/2)); leftPageIcon.opacity = opacityX*opacityY; } onExited: { leftPageIcon.opacity = 0; } Rectangle { id: leftPageIcon; anchors.centerIn: parent; width: Kirigami.Units.iconSizes.huge; height: width; radius:width/2; color: Kirigami.Theme.highlightColor; opacity: 0; Kirigami.Icon { anchors.centerIn: parent; source: "go-previous" width: parent.width*(2/3); height: width; } } } MouseArea { anchors { top: parent.top; right: parent.right; bottom: parent.bottom; } width: Math.max(parent.width / 6, (parent.width-image.paintedWidth)/2); preventStealing: true; onClicked: root.layoutDirection === Qt.RightToLeft? image.nextFrame(): image.previousFrame(); hoverEnabled: true; onPositionChanged: { var hWidth = width/2; var hHeight = height/2; var opacityX = mouse.x>hWidth? hWidth-(mouse.x-hWidth) : mouse.x; opacityX = opacityX/(hWidth - (Kirigami.Units.iconSizes.huge/2)); var opacityY = mouse.y>hHeight? hHeight-(mouse.y-hHeight) : mouse.y; opacityY = opacityY/(hHeight - (Kirigami.Units.iconSizes.huge/2)); rightPageIcon.opacity = opacityX*opacityY; } onExited: { rightPageIcon.opacity = 0; } Rectangle { id: rightPageIcon; anchors.centerIn: parent; width: Kirigami.Units.iconSizes.huge; height: width; radius:width/2; color: Kirigami.Theme.highlightColor; opacity: 0; Kirigami.Icon { anchors.centerIn: parent; source: "go-next" width: parent.width*(2/3); height: width; } } } } } } } diff --git a/src/app/qml/viewers/ViewerBase.qml b/src/app/qml/viewers/ViewerBase.qml index 58160c0..5bcbaf6 100644 --- a/src/app/qml/viewers/ViewerBase.qml +++ b/src/app/qml/viewers/ViewerBase.qml @@ -1,74 +1,81 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.0 /** * @brief a base for holding the image browser. * * It keeps track of the comic data and deals with all sorts * of side functions like keeping track of the current page, * right to left mode and the controls. * * This is slightly different between filetypes. */ Item { id: root; signal goNextPage(); signal goPreviousPage(); // Fired when the viewer has completed loading the file. // Success is true if this was successful, and false otherwise. signal loadingCompleted(bool success); property string file; property int currentPage; property bool rtlMode: false; property int zoomMode: 0; property int pageCount; property var pagesModel; property Component thumbnailComponent; signal restoreCurrentPage(); + /** + * Whether or not the viewer supports frame based navigation + */ + property bool hasFrames: false; + signal nextFrame(); + signal previousFrame(); + // This all looks a little silly, however, without this, we can't double-click on the // page (which is, these days, used for zooming purposes). It also works around some // minor small annoyances, like accidental swiping when the finger doesn't leave the // surface of the page when switching occurs. function startToggleControls() { controlsToggler.start(); } function abortToggleControls() { controlsToggler.stop(); } Timer { id: controlsToggler; interval: 500; running: false; repeat: false; onTriggered: { if(applicationWindow().controlsVisible === true) { applicationWindow().controlsVisible = false; } else { applicationWindow().controlsVisible = true; } } } } diff --git a/src/app/qml/viewers/cbr.qml b/src/app/qml/viewers/cbr.qml index c4eef9c..617c191 100644 --- a/src/app/qml/viewers/cbr.qml +++ b/src/app/qml/viewers/cbr.qml @@ -1,100 +1,103 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.0 import org.kde.peruse 0.1 as Peruse /** * @brief a ViewerBase for CBR style books. * * It is called from Book when the opened book is one of the following files: * - application/x-cbz * - application/x-cbr * - application/vnd.comicbook+zip * - application/vnd.comicbook+rar */ ViewerBase { id: root; property string title: imageBrowser.model.title; pagesModel: imageBrowser.model; pageCount: imageBrowser.model.pageCount; onRtlModeChanged: { if(rtlMode === true) { imageBrowser.layoutDirection = Qt.RightToLeft; } else { imageBrowser.layoutDirection = Qt.LeftToRight; } root.restoreCurrentPage(); } onRestoreCurrentPage: { // This is un-pretty, quite obviously. But thanks to the ListView's inability to // stay in place when the geometry changes, well, this makes things simple. imageBrowser.positionViewAtIndex(imageBrowser.currentIndex, ListView.Center); } + hasFrames: true; + onNextFrame: imageBrowser.goNextFrame(); + onPreviousFrame: imageBrowser.goPreviousFrame(); onCurrentPageChanged: { if(currentPage !== imageBrowser.currentIndex) { pageChangeAnimation.running = false; var currentPos = imageBrowser.contentX; var newPos; imageBrowser.positionViewAtIndex(currentPage, ListView.Center); imageBrowser.currentIndex = currentPage; newPos = imageBrowser.contentX; pageChangeAnimation.from = currentPos; pageChangeAnimation.to = newPos; pageChangeAnimation.running = true; } } NumberAnimation { id: pageChangeAnimation; target: imageBrowser; property: "contentX"; duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } Timer { id: initialPageChange; interval: applicationWindow().animationDuration; running: false; repeat: false; onTriggered: root.currentPage = imageBrowser.model.currentPage; } ImageBrowser { id: imageBrowser; anchors.fill: parent; model: Peruse.ArchiveBookModel { filename: root.file; qmlEngine: globalQmlEngine; onLoadingCompleted: { root.loadingCompleted(success); initialPageChange.start(); } } onCurrentIndexChanged: { if(root.currentPage !== currentIndex) { root.currentPage = currentIndex; } } onGoNextPage: root.goNextPage(); onGoPreviousPage: root.goPreviousPage(); imageWidth: root.width; imageHeight: root.height; } }