diff --git a/src/app/qml/Book.qml b/src/app/qml/Book.qml index 5a0339d..66b8ad4 100644 --- a/src/app/qml/Book.qml +++ b/src/app/qml/Book.qml @@ -1,528 +1,528 @@ /* * 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 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"); shortcut: root.isCurrentPage && bookInfo.sheetOpen ? "" : StandardKey.MoveToPreviousChar; 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; 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 !== filename) { + 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(detailsTile.filename, true); + 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/Bookshelf.qml b/src/app/qml/Bookshelf.qml index 8d4497c..b1334a5 100644 --- a/src/app/qml/Bookshelf.qml +++ b/src/app/qml/Bookshelf.qml @@ -1,221 +1,221 @@ /* * 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 1.4 as QtControls import org.kde.kirigami 2.1 as Kirigami import org.kde.peruse 0.1 as Peruse import "listcomponents" as ListComponents /** * @brief This lays out the books and categories. * * It uses BookTileTall for the books and CategoryTileTall * for showing the categories. Categories can be selected to open * a new bookshelf from the right, showing the entries in that * subcategory. This is particularly in use with the folder category. * * There is also access to the SearchBox, and it is possible to access * a BookTile by press+holding the thumbnail. * This holds information about the book. */ Kirigami.ScrollablePage { id: root; title: headerText; property string categoryName: "bookshelf"; objectName: "bookshelf"; property alias model: shelfList.model; property string sectionRole: "title"; property int sectionCriteria: ViewSection.FirstCharacter; signal bookSelected(string filename, int currentPage); property string headerText; function openBook(index) { applicationWindow().contextDrawer.close(); if(shelfList.model.indexIsBook(index)) { var book = shelfList.model.get(index); root.bookSelected(book.readProperty("filename"), book.readProperty("currentPage")); } else { var catEntry = shelfList.model.getEntry(index); applicationWindow().pageStack.push(bookshelf, { focus: true, headerText: catEntry.readProperty("title"), model: catEntry.readProperty("entriesModel") }); } } function closeShelf() { applicationWindow().contextDrawer.close(); applicationWindow().pageStack.pop(); } property list mobileActions; property list desktopActions: [ Kirigami.Action { text: i18nc("Navigate one page back", "Back"); shortcut: bookDetails.sheetOpen ? "" : "Esc"; iconName: "dialog-close"; onTriggered: closeShelf(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && applicationWindow().pageStack.currentIndex > 0; }, // Kirigami.Action { // text: i18nc("Select the previous book in the list", "Select Previous Book"); // shortcut: StandardKey.MoveToPreviousChar // iconName: "go-previous"; // onTriggered: shelfList.previousEntry(); // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; // }, // Kirigami.Action { // text: i18nc("Select the next book in the list", "Select Next Book"); // shortcut: StandardKey.MoveToNextChar; // iconName: "go-next"; // onTriggered: shelfList.nextEntry(); // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; // }, Kirigami.Action { text: i18nc("Open the book which is currently selected in the list", "Open Selected Book"); shortcut: bookDetails.sheetOpen? "" : "Return"; iconName: "document-open"; onTriggered: openBook(shelfList.currentIndex); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; } ] actions { contextualActions: PLASMA_PLATFORM.substring(0, 5) === "phone" ? mobileActions : desktopActions; main: bookDetails.sheetOpen ? bookDetailsAction : mainShelfAction; } Kirigami.Action { id: mainShelfAction; text: i18nc("search in the list of books (not inside the books)", "Search Books"); iconName: "system-search"; onTriggered: searchBox.activate(); enabled: root.isCurrentPage; } Kirigami.Action { id: bookDetailsAction; text: i18n("Closes the book details drawer", "Close"); shortcut: bookDetails.sheetOpen ? "Esc" : ""; iconName: "dialog-cancel"; onTriggered: bookDetails.close(); enabled: root.isCurrentPage; } GridView { id: shelfList; SearchBox { id: searchBox; anchors { top: parent.top; left: parent.left; right: parent.right; } maxHeight: parent.height; model: root.model; onBookSelected: root.bookSelected(filename, currentPage); } clip: true; footer: Item { width: parent.width; height: Kirigami.Units.iconSizes.large + Kirigami.Units.largeSpacing; } cellWidth: width / 2; cellHeight: root.height * 3 / 8; currentIndex: -1; function previousEntry() { if(currentIndex > 0) { currentIndex--; } } function nextEntry() { if(currentIndex < model.rowCount() - 1) { currentIndex++; } } delegate: Item { height: model.categoryEntriesCount === 0 ? bookTile.neededHeight : categoryTile.neededHeight; width: root.width / 2; ListComponents.CategoryTileTall { id: categoryTile; height: model.categoryEntriesCount > 0 ? neededHeight : 0; width: parent.width; count: model.categoryEntriesCount; title: model.title; entriesModel: model.categoryEntriesModel ? model.categoryEntriesModel : null; selected: shelfList.currentIndex === index; } ListComponents.BookTileTall { id: bookTile; height: model.categoryEntriesCount < 1 ? neededHeight : 0; width: parent.width; author: model.author ? model.author : i18nc("used for the author data in book lists if author is empty", "(unknown)"); title: model.title; filename: model.filename; thumbnail: model.categoryEntriesCount < 1 ? model.thumbnail : ""; categoryEntriesCount: model.categoryEntriesCount; currentPage: model.currentPage; totalPages: model.totalPages; onBookSelected: root.bookSelected(filename, currentPage); selected: shelfList.currentIndex === index; onPressAndHold: bookDetails.showBookInfo(model.index); pressIndicator: true; } } } Kirigami.OverlaySheet { id: bookDetails; function showBookInfo(index) { currentBook = root.model.getEntry(index); open(); } property QtObject currentBook: fakeBook; property QtObject fakeBook: Peruse.PropertyContainer { property string author: ""; property string title: ""; property string filename: ""; property string publisher: ""; property string thumbnail: ""; property string currentPage: "0"; property string totalPages: "0"; property string comment: ""; } ListComponents.BookTile { id: detailsTile; height: neededHeight; width: shelfList.width - Kirigami.Units.largeSpacing * 2; author: bookDetails.currentBook.readProperty("author"); publisher: bookDetails.currentBook.readProperty("publisher"); title: bookDetails.currentBook.readProperty("title"); filename: bookDetails.currentBook.readProperty("filename"); thumbnail: bookDetails.currentBook.readProperty("thumbnail"); categoryEntriesCount: 0; currentPage: bookDetails.currentBook.readProperty("currentPage"); totalPages: bookDetails.currentBook.readProperty("totalPages"); description: bookDetails.currentBook.readProperty("description"); onBookSelected: { bookDetails.close(); - applicationWindow().showBook(filename, currentPage); + applicationWindow().showBook(fileSelected, currentPage); } onBookDeleteRequested: { - contentList.removeBook(detailsTile.filename, true); + contentList.removeBook(fileSelected, true); close(); } } } } diff --git a/src/app/qml/listcomponents/BookTile.qml b/src/app/qml/listcomponents/BookTile.qml index edf978c..d3e78a7 100644 --- a/src/app/qml/listcomponents/BookTile.qml +++ b/src/app/qml/listcomponents/BookTile.qml @@ -1,422 +1,427 @@ /* * 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.Controls 2.2 as QtControls import org.kde.kirigami 2.1 as Kirigami /** * @brief small window with book information. * * This shows a bit of information about the book and gives a * selector with the other books in the series. * * It pops up after finishing a book in Book, and when pressing long * on a BookTileTall item in BookShelf. * */ Item { id: root; property bool selected: false; property alias title: bookTitle.text; property var author: []; property string publisher; property alias filename: bookFile.text; property alias thumbnail: coverImage.source; property int categoryEntriesCount; property string currentPage; property string totalPages; property var description: []; property string comment: peruseConfig.getFilesystemProperty(root.filename, "comment"); property var tags: peruseConfig.getFilesystemProperty(root.filename, "tags").split(","); property int rating: peruseConfig.getFilesystemProperty(root.filename, "rating"); - signal bookSelected(string filename, int currentPage); - signal bookDeleteRequested(); + signal bookSelected(string fileSelected, int currentPage); + signal bookDeleteRequested(string fileSelected); property int neededHeight: bookCover.height;// + bookAuthorLabel.height + bookFile.height + Kirigami.Units.smallSpacing * 4; property bool showCommentTags: neededHeight > bookTitle.height + bookAuthorLabel.height + bookPublisherLabel.height + ratingContainer.height + tagsContainer.height + commentContainer.height + deleteButton.height + Kirigami.Units.smallSpacing * 7; visible: height > 1; enabled: visible; clip: true; onRatingChanged: { - contentList.setBookData(root.filename, "rating", rating); - peruseConfig.setFilesystemProperty(root.filename, "rating", rating); + if (peruseConfig.getFilesystemProperty(root.filename, "rating") !== rating) { + contentList.setBookData(root.filename, "rating", rating); + peruseConfig.setFilesystemProperty(root.filename, "rating", rating); + } } onTagsChanged: { - contentList.setBookData(root.filename, "tags", tags.join(",")); - peruseConfig.setFilesystemProperty(root.filename, "tags", tags.join(",")); + if (tags.join(",") !== peruseConfig.getFilesystemProperty(root.filename, "tags")) { + contentList.setBookData(root.filename, "tags", tags.join(",")); + peruseConfig.setFilesystemProperty(root.filename, "tags", tags.join(",")); + } } onCommentChanged: { contentList.setBookData(root.filename, "comment", comment); peruseConfig.setFilesystemProperty(root.filename, "comment", comment); } onFilenameChanged: { comment = peruseConfig.getFilesystemProperty(root.filename, "comment"); tags = peruseConfig.getFilesystemProperty(root.filename, "tags").split(","); rating = peruseConfig.getFilesystemProperty(root.filename, "rating"); ratingRow.potentialRating = rating; } Rectangle { anchors.fill: parent; color: Kirigami.Theme.highlightColor; opacity: root.selected ? 1 : 0; Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } } Item { id: bookCover; anchors { top: parent.top; left: parent.left; } width: root.width / 3; height: width * 1.5; Image { id: coverImage; anchors { fill: parent; margins: Kirigami.Units.smallSpacing; } asynchronous: true; fillMode: Image.PreserveAspectFit; } MouseArea { anchors.fill: parent; onClicked: root.bookSelected(root.filename, root.currentPage); } } Kirigami.Heading { id: bookTitle; anchors { top: parent.top; leftMargin: Kirigami.Units.smallSpacing; left: bookCover.right; right: parent.right; } maximumLineCount: 1; elide: Text.ElideMiddle; font.weight: Font.Bold; MouseArea { anchors.fill: parent; onClicked: root.bookSelected(root.filename, root.currentPage); } Rectangle { anchors { left: parent.left; top: parent.baseline; topMargin: 2; } height: 2; width: parent.paintedWidth; color: Kirigami.Theme.linkColor; } } QtControls.Label { id: bookAuthorLabel; anchors { top: bookTitle.bottom; left: bookCover.right; leftMargin: Kirigami.Units.smallSpacing; } width: paintedWidth; text: i18nc("Label for authors", "Author(s)"); font.weight: Font.Bold; } QtControls.Label { id: bookAuthor; anchors { top: bookTitle.bottom; left: bookAuthorLabel.right; leftMargin: Kirigami.Units.smallSpacing; right: parent.right; } elide: Text.ElideRight; text: root.author.length === 0 ? "(unknown)" : root.author.join(", "); opacity: (text === "(unknown)" || text === "") ? 0.3 : 1; } QtControls.Label { id: bookPublisherLabel; anchors { top: bookAuthorLabel.bottom; left: bookCover.right; leftMargin: Kirigami.Units.smallSpacing; } width: paintedWidth; text: i18nc("Label for publisher", "Publisher"); font.weight: Font.Bold; } QtControls.Label { id: bookPublisher; anchors { top: bookAuthor.bottom; left: bookPublisherLabel.right; leftMargin: Kirigami.Units.smallSpacing; right: parent.right; } elide: Text.ElideRight; text: root.publisher === "" ? "(unknown)" : root.publisher; opacity: (text === "(unknown)" || text === "") ? 0.3 : 1; } QtControls.Label { id: bookFile; anchors { top: bookPublisherLabel.bottom; left: bookCover.right; leftMargin: Kirigami.Units.smallSpacing; right: parent.right; } elide: Text.ElideMiddle; opacity: 0.3; font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8; maximumLineCount: 1; } Item { id: ratingContainer; anchors { top: bookFile.bottom; left: bookCover.right; right: parent.right; margins: Kirigami.Units.smallSpacing; } Row { id: ratingRow; QtControls.Label { width: paintedWidth; text: i18nc("label for rating widget","Rating"); height: Kirigami.Units.iconSizes.medium; font.weight: Font.Bold; anchors.rightMargin: Kirigami.Units.smallSpacing; } property int potentialRating: root.rating; Repeater{ model: 5; Item { height: Kirigami.Units.iconSizes.medium; width: Kirigami.Units.iconSizes.medium; Kirigami.Icon { source: "rating"; opacity: (ratingRow.potentialRating-2)/2 >= index? 1.0: 0.3; anchors.fill:parent; MouseArea { anchors.fill: parent; hoverEnabled: true; onEntered: { if (ratingRow.potentialRating === (index+1)*2) { ratingRow.potentialRating = ratingRow.potentialRating-1; } else { ratingRow.potentialRating = (index+1)*2; } } onExited: { ratingRow.potentialRating = root.rating; } onClicked: root.rating === ratingRow.potentialRating? root.rating = ratingRow.potentialRating-1 : root.rating = ratingRow.potentialRating; } } Kirigami.Icon { source: "rating"; height: parent.height/2; clip: true; anchors.centerIn: parent; width: height; visible: ratingRow.potentialRating === (index*2)+1; } } } } height: childrenRect.height; } Item { id: tagsContainer; height: root.showCommentTags? childrenRect.height: 0; visible: root.showCommentTags; anchors { top: ratingContainer.bottom; left: bookCover.right; right: parent.right; margins: Kirigami.Units.smallSpacing; } QtControls.Label { text: i18nc("label for tags field","Tags"); height: tagField.height; font.weight: Font.Bold; id: tagsLabel; } QtControls.TextField { id: tagField; anchors{ leftMargin: Kirigami.Units.smallSpacing; left: tagsLabel.right; top: parent.top; right: parent.right; } width: {parent.width - tagsLabel.width - Kirigami.Units.smallSpacing;} text: root.tags.length !== 0? root.tags.join(", "): ""; placeholderText: i18nc("Placeholder tag field", "(No tags)"); onEditingFinished: { var tags = text.split(","); for (var i in tags) { tags[i] = tags[i].trim(); } root.tags = tags; } } } Item { id: commentContainer; anchors { top: tagsContainer.bottom; left: bookCover.right; right: parent.right; margins: Kirigami.Units.smallSpacing; } QtControls.Label { text: i18nc("label for comment field","Comment"); height: tagField.height; font.weight: Font.Bold; id: commentLabel; } QtControls.TextField { id: commentField; anchors{ leftMargin: Kirigami.Units.smallSpacing; left: commentLabel.right; top: parent.top; right: parent.right; } width: parent.width - commentLabel.width - Kirigami.Units.smallSpacing; text: root.comment !== ""? root.comment: ""; placeholderText: i18nc("Placeholder comment field", "(No comment)"); onEditingFinished: { root.comment = text; } } height: root.showCommentTags? childrenRect.height: 0; visible: root.showCommentTags; } Item { id: descriptionContainer; anchors { top: commentContainer.bottom; left: bookCover.right; right: parent.right; bottom: deleteBase.top; margins: Kirigami.Units.smallSpacing; } QtControls.Label { anchors.fill: parent; verticalAlignment: Text.AlignTop; text: root.description.length !== 0? root.description.join("\n\n"): i18nc("Placeholder text for the book description field when no description is set", "(no description available for this book)"); wrapMode: Text.WrapAtWordBoundaryOrAnywhere opacity: root.description.length !== 0? 1.0: 0.3; } } Item { id: deleteBase; anchors { left: bookCover.right; leftMargin: Kirigami.Units.smallSpacing; right: parent.right; bottom: parent.bottom; } height: deleteButton.height + Kirigami.Units.smallSpacing * 2; Behavior on height { PropertyAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } states: [ State { name: "confirmDelete"; PropertyChanges { target: deleteButton; opacity: 0; } PropertyChanges { target: deleteConfirmBase; opacity: 1; } PropertyChanges { target: deleteBase; height: deleteConfirmBase.height; } } ] QtControls.Button { id: deleteButton; text: i18nc("Spawn inline dialog box to confirm permanent removal of this book", "Delete from Device"); anchors { bottom: parent.bottom; right: parent.right; margins: Kirigami.Units.smallSpacing; } // iconName: "edit-delete"; onClicked: deleteBase.state = "confirmDelete"; Behavior on opacity { PropertyAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } } - Item { + Rectangle { id: deleteConfirmBase; opacity: 0; width: parent.width; Behavior on opacity { PropertyAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } height: yesDelete.height + confirmDeleteLabel.height + Kirigami.Units.largeSpacing * 2 + Kirigami.Units.smallSpacing; + color: Kirigami.Theme.viewBackgroundColor; QtControls.Label { id: confirmDeleteLabel; anchors { top: parent.top; topMargin: Kirigami.Units.largeSpacing; left: parent.left; right: parent.right; } height: paintedHeight; wrapMode: Text.WordWrap; horizontalAlignment: Text.AlignHCenter; text: i18nc("Dialog text for delete book dialog", "Are you sure you want to delete this from your device?"); } QtControls.Button { id: yesDelete; anchors { top: confirmDeleteLabel.bottom; topMargin: Kirigami.Units.smallSpacing; right: parent.horizontalCenter; - rightMargin: (parent.width - width) / 4; + rightMargin: (Kirigami.Units.smallSpacing) / 2; } text: i18nc("Confirmation button for book delete dialog", "Yes, Really Delete"); // iconName: "dialog-ok"; - onClicked: root.bookDeleteRequested(); + onClicked: {root.bookDeleteRequested(root.filename); deleteBase.state = "";} } QtControls.Button { anchors { top: confirmDeleteLabel.bottom; topMargin: Kirigami.Units.smallSpacing; left: parent.horizontalCenter; - leftMargin: (parent.width - width) / 4; + leftMargin: (Kirigami.Units.smallSpacing) / 2; } text: i18nc("Cancellation button or book delete dialog", "No, Cancel Delete"); // iconName: "dialog-cancel"; onClicked: deleteBase.state = ""; } } } } diff --git a/src/qtquick/BookListModel.cpp b/src/qtquick/BookListModel.cpp index 9fa5a09..792abe1 100644 --- a/src/qtquick/BookListModel.cpp +++ b/src/qtquick/BookListModel.cpp @@ -1,410 +1,412 @@ /* * 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 . * */ #include "BookListModel.h" #include "BookDatabase.h" #include "CategoryEntriesModel.h" #include "ArchiveBookModel.h" #include "AcbfAuthor.h" #include "AcbfSequence.h" #include "AcbfBookinfo.h" #include #include #include #include #include #include #include #include class BookListModel::Private { public: Private() : contentModel(nullptr) , titleCategoryModel(nullptr) , newlyAddedCategoryModel(nullptr) , authorCategoryModel(nullptr) , seriesCategoryModel(nullptr) , publisherCategoryModel(nullptr) , folderCategoryModel(nullptr) , cacheLoaded(false) { db = new BookDatabase(); }; ~Private() { qDeleteAll(entries); db->deleteLater(); } QList entries; QAbstractListModel* contentModel; CategoryEntriesModel* titleCategoryModel; CategoryEntriesModel* newlyAddedCategoryModel; CategoryEntriesModel* authorCategoryModel; CategoryEntriesModel* seriesCategoryModel; CategoryEntriesModel* publisherCategoryModel; CategoryEntriesModel* folderCategoryModel; BookDatabase* db; bool cacheLoaded; void initializeSubModels(BookListModel* q) { if(!titleCategoryModel) { titleCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), titleCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), titleCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->titleCategoryModelChanged(); } if(!newlyAddedCategoryModel) { newlyAddedCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), newlyAddedCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), newlyAddedCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->newlyAddedCategoryModelChanged(); } if(!authorCategoryModel) { authorCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), authorCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), authorCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->authorCategoryModelChanged(); } if(!seriesCategoryModel) { seriesCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), seriesCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), seriesCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->seriesCategoryModelChanged(); } if(!publisherCategoryModel) { publisherCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), publisherCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), publisherCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->publisherCategoryModelChanged(); } if(!folderCategoryModel) { folderCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), folderCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), folderCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->folderCategoryModel(); } } void addEntry(BookListModel* q, BookEntry* entry) { entries.append(entry); q->append(entry); titleCategoryModel->addCategoryEntry(entry->title.left(1).toUpper(), entry); for (int i=0; iauthor.size(); i++) { authorCategoryModel->addCategoryEntry(entry->author.at(i), entry); } for (int i=0; iseries.size(); i++) { seriesCategoryModel->addCategoryEntry(entry->series.at(i), entry, SeriesRole); } if (newlyAddedCategoryModel->indexOfFile(entry->filename) == -1) { newlyAddedCategoryModel->append(entry, CreatedRole); } publisherCategoryModel->addCategoryEntry(entry->publisher, entry); QUrl url(entry->filename.left(entry->filename.lastIndexOf("/"))); folderCategoryModel->addCategoryEntry(url.path().mid(1), entry); if (folderCategoryModel->indexOfFile(entry->filename) == -1) { folderCategoryModel->append(entry); } } void loadCache(BookListModel* q) { QList entries = db->loadEntries(); if(entries.count() > 0) { initializeSubModels(q); } int i = 0; foreach(BookEntry* entry, entries) { addEntry(q, entry); if(++i % 100 == 0) { emit q->countChanged(); qApp->processEvents(); } } cacheLoaded = true; emit q->cacheLoadedChanged(); } }; BookListModel::BookListModel(QObject* parent) : CategoryEntriesModel(parent) , d(new Private) { } BookListModel::~BookListModel() { delete d; } void BookListModel::componentComplete() { QTimer::singleShot(0, this, [this](){ d->loadCache(this); }); } bool BookListModel::cacheLoaded() const { return d->cacheLoaded; } void BookListModel::setContentModel(QObject* newModel) { if(d->contentModel) { d->contentModel->disconnect(this); } d->contentModel = qobject_cast(newModel); if(d->contentModel) { connect(d->contentModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(contentModelItemsInserted(QModelIndex,int, int))); } emit contentModelChanged(); } QObject * BookListModel::contentModel() const { return d->contentModel; } void BookListModel::contentModelItemsInserted(QModelIndex index, int first, int last) { d->initializeSubModels(this); int newRow = d->entries.count(); beginInsertRows(QModelIndex(), newRow, newRow + (last - first)); int role = d->contentModel->roleNames().key("filePath"); for(int i = first; i < last + 1; ++i) { QVariant filePath = d->contentModel->data(d->contentModel->index(first, 0, index), role); BookEntry* entry = new BookEntry(); entry->filename = filePath.toUrl().toLocalFile(); QStringList splitName = entry->filename.split("/"); if (!splitName.isEmpty()) entry->filetitle = splitName.takeLast(); if(!splitName.isEmpty()) { entry->series = QStringList(splitName.takeLast()); // hahahaheuristics (dumb assumptions about filesystems, go!) entry->seriesNumbers = QStringList("0"); entry->seriesVolumes = QStringList("0"); } // just in case we end up without a title... using complete basename here, // as we would rather have "book one. part two" and the odd "book one - part two.tar" QFileInfo fileinfo(entry->filename); entry->title = fileinfo.completeBaseName(); if(entry->filename.toLower().endsWith("cbr")) { entry->thumbnail = QString("image://comiccover/").append(entry->filename); } #ifdef USE_PERUSE_PDFTHUMBNAILER else if(entry->filename.toLower().endsWith("pdf")) { entry->thumbnail = QString("image://pdfcover/").append(entry->filename); } #endif else { entry->thumbnail = QString("image://preview/").append(entry->filename); } KFileMetaData::UserMetaData data(entry->filename); entry->rating = data.rating(); entry->comment = data.userComment(); entry->tags = data.tags(); QVariantHash metadata = d->contentModel->data(d->contentModel->index(first, 0, index), Qt::UserRole + 2).toHash(); QVariantHash::const_iterator it = metadata.constBegin(); for (; it != metadata.constEnd(); it++) { if(it.key() == QLatin1String("author")) { entry->author = it.value().toStringList(); } else if(it.key() == QLatin1String("title")) { entry->title = it.value().toString().trimmed(); } else if(it.key() == QLatin1String("publisher")) { entry->publisher = it.value().toString().trimmed(); } else if(it.key() == QLatin1String("created")) { entry->created = it.value().toDateTime(); } else if(it.key() == QLatin1String("currentPage")) { entry->currentPage = it.value().toInt(); } else if(it.key() == QLatin1String("totalPages")) { entry->totalPages = it.value().toInt(); } else if(it.key() == QLatin1String("comments")) { entry->comment = it.value().toString();} else if(it.key() == QLatin1Literal("tags")) { entry->tags = it.value().toStringList();} else if(it.key() == QLatin1String("rating")) { entry->rating = it.value().toInt();} } // ACBF information is always preferred for CBRs, so let's just use that if it's there QMimeDatabase db; QString mimetype = db.mimeTypeForFile(entry->filename).name(); if(mimetype == "application/x-cbz" || mimetype == "application/x-cbr" || mimetype == "application/vnd.comicbook+zip" || mimetype == "application/vnd.comicbook+rar") { ArchiveBookModel* bookModel = new ArchiveBookModel(this); bookModel->setFilename(entry->filename); AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(bookModel->acbfData()); if(acbfDocument) { for(AdvancedComicBookFormat::Sequence* sequence : acbfDocument->metaData()->bookInfo()->sequence()) { if (!entry->series.contains(sequence->title())) { entry->series.append(sequence->title()); entry->seriesNumbers.append(QString::number(sequence->number())); entry->seriesVolumes.append(QString::number(sequence->volume())); } else { int series = entry->series.indexOf(sequence->title()); entry->seriesNumbers.replace(series, QString::number(sequence->number())); entry->seriesVolumes.replace(series, QString::number(sequence->volume())); } } for(AdvancedComicBookFormat::Author* author : acbfDocument->metaData()->bookInfo()->author()) { entry->author.append(author->displayName()); } entry->description = acbfDocument->metaData()->bookInfo()->annotation(""); } if (entry->author.isEmpty()) { entry->author.append(bookModel->author()); } entry->title = bookModel->title(); entry->publisher = bookModel->publisher(); entry->totalPages = bookModel->pageCount(); bookModel->deleteLater(); } d->addEntry(this, entry); d->db->addEntry(entry); } endInsertRows(); emit countChanged(); qApp->processEvents(); } QObject * BookListModel::titleCategoryModel() const { return d->titleCategoryModel; } QObject * BookListModel::newlyAddedCategoryModel() const { return d->newlyAddedCategoryModel; } QObject * BookListModel::authorCategoryModel() const { return d->authorCategoryModel; } QObject * BookListModel::seriesCategoryModel() const { return d->seriesCategoryModel; } QObject * BookListModel::seriesModelForEntry(QString fileName) { Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { return d->seriesCategoryModel->leafModelForEntry(entry); } } return nullptr; } QObject *BookListModel::publisherCategoryModel() const { return d->publisherCategoryModel; } QObject * BookListModel::folderCategoryModel() const { return d->folderCategoryModel; } int BookListModel::count() const { return d->entries.count(); } void BookListModel::setBookData(QString fileName, QString property, QString value) { + qDebug() << fileName << property << value; Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { if(property == "totalPages") { entry->totalPages = value.toInt(); } else if(property == "currentPage") { entry->currentPage = value.toInt(); } else if(property == "rating") { entry->rating = value.toInt(); } else if(property == "tags") { entry->tags = value.split(","); } else if(property == "comment") { entry->comment = value; } emit entryDataUpdated(entry); break; } } } void BookListModel::removeBook(QString fileName, bool deleteFile) { + qDebug() << QUrl::fromLocalFile(fileName) << deleteFile; if(deleteFile) { KIO::DeleteJob* job = KIO::del(QUrl::fromLocalFile(fileName), KIO::HideProgressInfo); job->start(); } Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { emit entryRemoved(entry); delete entry; break; } } } QStringList BookListModel::knownBookFiles() const { QStringList files; foreach(BookEntry* entry, d->entries) { files.append(entry->filename); } return files; }