[app] Added limited support for folders

UI doesn't allow creating or renaming the folders yet and there's
no easy way to move books between folders, but if folders are there,
they will be shown and the user will be able to navigate between them.

Folders can be deleted though.
This commit is contained in:
Slava Monich 2015-11-08 14:20:25 +03:00
parent ef19a5c2e7
commit 89f79e2492
22 changed files with 1029 additions and 243 deletions

View file

@ -117,6 +117,7 @@ SOURCES += \
src/BooksLoadingProperty.cpp \
src/BooksPageWidget.cpp \
src/BooksPaintContext.cpp \
src/BooksPathModel.cpp \
src/BooksSaveTimer.cpp \
src/BooksSettings.cpp \
src/BooksShelf.cpp \
@ -152,6 +153,7 @@ HEADERS += \
src/BooksLoadingProperty.h \
src/BooksPageWidget.h \
src/BooksPaintContext.h \
src/BooksPathModel.h \
src/BooksPos.h \
src/BooksSaveTimer.h \
src/BooksSettings.h \

View file

@ -36,6 +36,7 @@ MouseArea {
id: root
parent: dragInProgress ? dragParent : gridView
anchors.fill: parent
propagateComposedEvents: true
signal deleteItemAt(var index)
signal dropItem(var mouseX, var mouseY)
@ -84,11 +85,21 @@ MouseArea {
}
} else {
index = gridView.indexAt(mouseX + gridView.contentX, mouseY + currentShelfView.contentY)
if (index >= 0 && index === lastReleasedItemIndex) {
var item = shelf.get(index);
if (item.book && item.accessible) {
shelfView.openBook(item.book)
if (index >= 0) {
if (index === lastReleasedItemIndex) {
var item = shelf.get(index);
if (item.accessible) {
if (item.book) {
shelfView.openBook(item.book)
} else if (item.shelf) {
var path = shelfView.shelf.relativePath
shelfView.shelf.relativePath = path ? (path + "/" + item.shelf.name) : item.shelf.name
}
}
}
} else if (mouseY + gridView.contentY < 0) {
// Let the header item handle it
mouse.accepted = false
}
}
resetPressState()
@ -105,9 +116,17 @@ MouseArea {
} else {
pressedDeleteItemIndex = -1
}
if (mouseY + gridView.contentY < 0) {
// Let the header item handle it
mouse.accepted = false
}
}
onReleased: {
stopDrag(mouseX, mouseY)
if (mouseY + gridView.contentY < 0) {
// Let the header item handle it
mouse.accepted = false
}
}
onPressAndHold: {
if (!shelfView.editMode) {

View file

@ -0,0 +1,62 @@
/*
Copyright (C) 2015 Jolla Ltd.
Contact: Slava Monich <slava.monich@jolla.com>
You may use this file under the terms of BSD license as follows:
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Jolla Ltd nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
*/
import QtQuick 2.0
import Sailfish.Silica 1.0
Item {
// 0: nothing
// 1: loading
// 2: no books
property int footerState
property bool allowBusyIndicator
implicitHeight: Math.max(noBooks.visible ? noBooks.implicitHeight : 0, busy.visible ? busy.implicitHeight : 0) * 2
InfoLabel {
id: noBooks
//% "No books"
text: qsTrId("shelf-view-no-books")
visible: footerState == 2
anchors.centerIn: parent
}
BusyIndicator {
id: busy
visible: opacity > 0
anchors.centerIn: parent
size: BusyIndicatorSize.Large
running: footerState == 1 && allowBusyIndicator
Behavior on opacity { enabled: false }
}
}

View file

@ -64,19 +64,31 @@ Item {
readonly property bool animating: scaling || moving
property bool _deleting: deleting && !deletingAll
property real _borderRadius: Theme.paddingSmall
property color _borderColor: Theme.primaryColor
readonly property real _borderRadius: Theme.paddingSmall
readonly property color _borderColor: Theme.primaryColor
readonly property real _borderWidth: 2
property bool scaledDown: (editMode && !dragged && !pressed && !dropped)
Image {
anchors {
margins: root.margins
fill: parent
}
visible: !cover.book
source: "images/bookshelf.svg"
sourceSize.width: width
sourceSize.height: height
}
BookCover {
id: cover
anchors {
margins: root.margins
fill: parent
}
borderWidth: 2
borderRadius: _borderRadius
borderWidth: book ? _borderWidth : 0
borderColor: _borderColor
opacity: (copyingIn || copyingOut) ? 0.1 : 1
Behavior on opacity { FadeAnimation { } }

132
app/qml/BooksShelfTitle.qml Normal file
View file

@ -0,0 +1,132 @@
/*
Copyright (C) 2015 Jolla Ltd.
Contact: Slava Monich <slava.monich@jolla.com>
You may use this file under the terms of BSD license as follows:
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Jolla Ltd nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
*/
import QtQuick 2.0
import Sailfish.Silica 1.0
BackgroundItem {
id: root
implicitHeight: column.implicitHeight
property alias text: label.text
Column {
id: column
width: parent.width
spacing: 0
Item {
height: Theme.paddingSmall
width: parent.width
}
Item {
id: labelItem
width: Math.min(label.implicitWidth + icon.width + 3*Theme.paddingMedium, parent.width)
height: label.implicitHeight
Image {
id: icon
height: label.height*3/4
sourceSize.height: height
fillMode: Image.PreserveAspectFit
source: "images/folder.svg"
anchors {
left: parent.left
leftMargin: Theme.paddingMedium
verticalCenter: parent.verticalCenter
}
}
Label {
id: label
truncationMode: TruncationMode.Fade
width: Math.min(parent.width - 2*Theme.paddingMedium, implicitWidth)
anchors {
left: icon.right
leftMargin: Theme.paddingMedium
verticalCenter: parent.verticalCenter
}
color: (!root.enabled || pressed) ? Theme.highlightColor : Theme.primaryColor
Behavior on color { ColorAnimation { duration: 100 } }
}
}
Item {
height: Theme.paddingSmall
width: parent.width
}
/*
Item {
width: labelItem.width
height: 3*Math.floor(PointsPerInch/100)
Image {
id: shelfLeft
anchors {
left: parent.left
leftMargin: Theme.paddingMedium
bottom: parent.bottom
}
height: parent.height
sourceSize.height: height
fillMode: Image.PreserveAspectFit
source: "images/shelf-left.svg"
}
Image {
id: shelfRight
anchors {
right: parent.right
rightMargin: Theme.paddingMedium
bottom: parent.bottom
}
height: parent.height
sourceSize.height: height
fillMode: Image.PreserveAspectFit
source: "images/shelf-right.svg"
}
Image {
anchors {
left: shelfLeft.right
right: shelfRight.left
bottom: parent.bottom
}
height: parent.height
sourceSize.height: height
sourceSize.width: width
source: "images/shelf-middle.svg"
}
}
*/
}
}

View file

@ -56,10 +56,10 @@ SilicaFlickable {
signal scrollRight()
signal scrollLeft()
property bool _haveBooks: shelf && shelf.count
property bool _haveBooks: shelfModel && shelfModel.count
property int _cellsPerRow: Math.floor(width/cellWidth)
readonly property int _remorseTimeout: 5000
property bool _loading: !shelf || shelf.loading || startAnimationTimer.running
property bool _loading: !shelfModel || shelfModel.loading || startAnimationTimer.running
property var _remorse
on_HaveBooksChanged: if (!_haveBooks) shelfView.stopEditing()
@ -69,6 +69,12 @@ SilicaFlickable {
property bool needDummyItem: dragInProgress && dragItem.shelfIndex !== shelfView.shelfIndex
onNeedDummyItemChanged: if (needDummyItem) hasDummyItem = true
editMode: shelfView.editMode
onRelativePathChanged: longStartTimer.restart()
}
BooksPathModel {
id: pathModel
path: shelfModel.relativePath
}
onEditModeChanged: {
@ -123,27 +129,60 @@ SilicaFlickable {
BooksStorageHeader {
id: storageHeader
removable: removableStorage
count: shelfModel.count
count: shelfModel.bookCount
showCount: !_loading
enabled: grid.contentY > grid.minContentY || pathModel.count > 0
needed: !singleStorage || pathModel.count > 0
onClicked: {
if (!scrollToTopAnimation.running) {
if (grid.contentY > grid.minContentY) {
scrollToTopAnimation.start()
} else {
animationEnabled = true
shelfModel.relativePath = ""
}
}
}
}
NumberAnimation {
id: scrollToTopAnimation
target: grid
property: "contentY"
duration: 500
easing.type: Easing.InOutQuad
to: grid.minContentY
}
SilicaGridView {
id: grid
anchors {
top: singleStorage ? parent.top : storageHeader.bottom
top: storageHeader.bottom
left: parent.left
right: parent.right
bottom: parent.bottom
leftMargin: Math.floor((shelfView.width - _cellsPerRow * shelfView.cellWidth)/2)
}
model: shelfModel
interactive: !dragInProgress
interactive: !dragInProgress && !scrollToTopAnimation.running
clip: true
opacity: (!_loading && _haveBooks) ? 1 : 0
visible: opacity > 0
cellWidth: shelfView.cellWidth
cellHeight: shelfView.cellHeight
flickableDirection: Flickable.VerticalFlick
header: Column {
Repeater {
model: pathModel
BooksShelfTitle {
width: grid.width
text: model.name
enabled: model.index < (pathModel.count-1)
onClicked: {
console.log("switching to", model.path)
shelfModel.relativePath = model.path
}
}
}
}
delegate: BooksShelfItem {
editMode: shelfView.editMode
dropped: dragItem.dropShelfIndex >= 0 &&
@ -176,7 +215,16 @@ SilicaFlickable {
}
}
footer: BooksShelfFooter {
width: grid.width
height: visible ? Math.max(implicitHeight, (grid.height > grid.headerItem.height) ? (grid.height - grid.headerItem.height) : 0) : 0
allowBusyIndicator: !longStartTimer.running
footerState: _haveBooks ? 0 : _loading ? 1 : 2
visible: !_haveBooks
}
property real itemOpacity: 1
property real minContentY: -headerItem.height
moveDisplaced: Transition {
SmoothedAnimation { properties: "x,y"; duration: 150 }
@ -209,33 +257,9 @@ SilicaFlickable {
}
Behavior on y { SpringAnimation {} }
Behavior on opacity { FadeAnimation {} }
VerticalScrollDecorator {}
}
ViewPlaceholder {
//% "No books"
text: qsTrId("shelf-view-no-books")
enabled: !_loading && !_haveBooks
PulleyAnimationHint {
id: pulleyAnimationHint
flickable: storageView
anchors.fill: parent
enabled: parent.enabled && !editMode
}
}
property Item _busyIndicator
Component {
id: busyIndicatorComponent
BusyIndicator {
visible: opacity > 0
anchors.centerIn: parent
size: BusyIndicatorSize.Large
running: _loading && !longStartTimer.running
}
}
Timer {
id: longStartTimer
@ -245,7 +269,6 @@ SilicaFlickable {
if (shelf.loading) {
console.log(shelfModel.path, "startup is taking too long")
startAnimationTimer.start()
if (!_busyIndicator) _busyIndicator = busyIndicatorComponent.createObject(shelfView)
}
}
}

View file

@ -33,65 +33,97 @@ import QtQuick 2.0
import Sailfish.Silica 1.0
Column {
id: root
anchors {
top: parent.top
left: parent.left
right: parent.right
topMargin: Theme.paddingMedium
}
spacing: 0
visible: opacity > 0
opacity: singleStorage ? 0 : 1
Behavior on opacity { FadeAnimation {} }
y: needed ? Theme.paddingMedium : -height
property alias animationEnabled: yBehavior.enabled
Behavior on y {
id: yBehavior
enabled: false
NumberAnimation { duration: 200 }
}
property bool needed
property bool removable
property int count
property bool showCount: true
property int _shownCount
signal clicked()
function updateShownCount() {
if (count > 0) {
_shownCount = count
}
}
onCountChanged: updateShownCount()
Component.onCompleted: updateShownCount()
Item {
width: parent.width
height: Math.max(left.height, right.height)
Row {
id: left
height: Math.max(storageLabel.height, bookCount.height)
BooksSDCardIcon {
id: icon
anchors {
left: parent.left
leftMargin: Theme.paddingMedium
verticalCenter: parent.verticalCenter
}
spacing: 0
Item {
width: Theme.paddingMedium
height: parent.height
}
BooksSDCardIcon {
visible: removableStorage
anchors.bottom: parent.bottom
height: storageLabel.height*3/4
}
Item {
visible: removableStorage
width: Theme.paddingMedium
height: parent.height
}
Label {
id: storageLabel
anchors.bottom: parent.bottom
color: Theme.highlightColor
text: removable ?
//% "Memory card"
qsTrId("storage-removable") :
//% "Internal storage"
qsTrId("storage-internal")
}
visible: removableStorage
height: storageLabel.height*3/4
}
Label {
id: right
id: storageLabel
anchors {
left: removableStorage ? icon.right : parent.left
right: bookCount.visible ? bookCount.left : parent.right
leftMargin: Theme.paddingMedium
rightMargin: Theme.paddingMedium
bottom: parent.bottom
}
color: (root.enabled && !mouseArea.pressed) ? Theme.primaryColor : Theme.highlightColor
text: removable ?
//% "Memory card"
qsTrId("storage-removable") :
//% "Internal storage"
qsTrId("storage-internal")
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: root.clicked()
}
Behavior on color { ColorAnimation { duration: 100 } }
// The label overlaps with the Sailfish 2.0 pulley menu which
// doesn't look great. Hide it when it's not needed. The book
// count can be left there, it doesn't overlap with anything
opacity: (needed) ? 1 : 0
visible: opacity > 0
Behavior on opacity { FadeAnimation {} }
}
Label {
id: bookCount
anchors {
bottom: parent.bottom
right: parent.right
rightMargin: Theme.paddingMedium
}
//% "%0 book(s)"
text: qsTrId("storage-book-count",count).arg(count)
text: qsTrId("storage-book-count",_shownCount).arg(_shownCount)
font.pixelSize: Theme.fontSizeExtraSmall
color: Theme.highlightColor
opacity: (showCount && count > 0) ? 1 : 0

View file

@ -48,7 +48,8 @@ SilicaFlickable {
property var currentShelfView
property int currentShelfIndex: storageListWatcher.currentIndex
readonly property bool dragInProgress: draggedItem ? true : false
readonly property real maxContentY: currentShelfView ? Math.max(0, currentShelfView.contentHeight - currentShelfView.height) : 0
readonly property real maxContentY: currentShelfView ? Math.max(0, currentShelfView.contentHeight - currentShelfView.height) -
(currentShelfView.headerItem ? currentShelfView.headerItem.height : 0) : 0
readonly property real verticalScrollThreshold: _cellHeight/2
readonly property real horizontalScrollThreshold: _cellWidth/2

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="bookshelf.svg"
viewBox="0 0 200 300"
sodipodi:version="0.32"
inkscape:export-xdpi="90"
version="1.0"
inkscape:export-ydpi="90"
inkscape:version="0.48.4 r9939"
width="100%"
height="100%">
<defs
id="defs4">
<linearGradient
id="linearGradient3497"
y2="-255.63425"
gradientUnits="userSpaceOnUse"
x2="349.17249"
gradientTransform="matrix(2.0000003,0,0,-0.86134847,-271.73873,-18.503609)"
y1="-260.72025"
x1="269.13498"
inkscape:collect="always">
<stop
id="stop4084"
style="stop-color:#502d16"
offset="0" />
<stop
id="stop4086"
style="stop-color:#7b4d2f"
offset="1" />
</linearGradient>
</defs>
<g
id="layer1"
inkscape:label="Capa 1"
inkscape:groupmode="layer"
transform="translate(-261.64,-40.64)">
<g
id="g3354"
transform="matrix(1.1869435,0,0,2.2826699,-54.717604,-419.74382)">
<rect
id="rect3299"
style="fill:#784421"
height="127.04422"
width="168.5"
y="206.06738"
x="266.53131"
ry="15.942534"
rx="0" />
<path
id="rect3301"
sodipodi:nodetypes="ccccc"
style="fill:url(#linearGradient3497)"
d="m 266.53131,206.06739 168.50002,0 -8.42501,-4.38084 -151.65001,0 z"
inkscape:connector-curvature="0" />
<rect
id="rect4090"
style="fill:#422511"
height="122.66337"
width="160.07504"
y="208.2578"
x="270.7438" />
<rect
id="rect4106"
style="fill:#3b2312"
height="122.66337"
width="4.2124925"
y="208.2578"
x="426.60632" />
<rect
id="rect4104"
style="fill:#3d200b"
height="122.66337"
width="4.2124901"
y="208.2578"
x="270.7438" />
<path
id="path4098"
sodipodi:nodetypes="ccccc"
style="fill:#502d16"
d="m 270.74381,330.92118 160.07501,0 -4.2125,-6.57125 -151.65001,0 z"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

35
app/qml/images/folder.svg Normal file
View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 100 86"
version="1.1"
inkscape:version="0.48.4 r9939"
width="100%"
height="100%"
sodipodi:docname="folder.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<path
d="m 97.499974,-225.88637 0,40.5 c 0,3.5284 -1.256001,6.55821 -3.768025,9.08947 -2.512015,2.53124 -5.518827,3.79687 -9.020429,3.79687 l -69.423042,0 c -3.501602,0 -6.5084135,-1.26563 -9.0204269,-3.79687 -2.5120233,-2.53126 -3.7680251,-5.56107 -3.7680251,-9.08947 l 0,-55.22725 c 0,-3.5284 1.2560018,-6.55821 3.7680251,-9.08948 2.5120134,-2.53123 5.5188249,-3.79687 9.0204269,-3.79687 l 18.269222,0 c 3.501603,0 6.508405,1.26564 9.020427,3.79687 2.512013,2.53127 3.768025,5.56108 3.768025,9.08948 l 0,1.84091 38.365368,0 c 3.501602,0 6.508414,1.26564 9.020429,3.79688 2.512024,2.53127 3.768025,5.56108 3.768025,9.08946 z"
id="path6-5"
inkscape:connector-curvature="0"
style="fill:#422511;fill-opacity:1;stroke:#784421;stroke-width:5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:nodetypes="ssssssssssssscsss"
inkscape:transform-center-x="990.36694"
inkscape:transform-center-y="-12.926792" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -129,14 +129,14 @@ bool BooksBook::CoverPaintContext::gotIt() const
class BooksBook::CoverTask : public BooksTask
{
public:
CoverTask(BooksStorage aStorage, shared_ptr<Book> aBook) :
iStorage(aStorage), iBook(aBook), iCoverMissing(false) {}
CoverTask(QString aStateDir, shared_ptr<Book> aBook) :
iStateDir(aStateDir), iBook(aBook), iCoverMissing(false) {}
bool hasImage() const;
QString cachedImagePath() const;
public:
BooksStorage iStorage;
QString iStateDir;
shared_ptr<Book> iBook;
QImage iCoverImage;
bool iCoverMissing;
@ -149,8 +149,8 @@ inline bool BooksBook::CoverTask::hasImage() const
QString BooksBook::CoverTask::cachedImagePath() const
{
if (iStorage.isValid()) {
return iStorage.configDir().path() + "/" +
if (!iStateDir.isEmpty()) {
return iStateDir + "/" +
QString::fromStdString(iBook->file().name(false)) +
BOOK_COVER_SUFFIX + "jpg";
}
@ -164,9 +164,9 @@ QString BooksBook::CoverTask::cachedImagePath() const
class BooksBook::LoadCoverTask : public BooksBook::CoverTask
{
public:
LoadCoverTask(BooksStorage aStorage, shared_ptr<Book> aBook,
LoadCoverTask(QString aStateDir, shared_ptr<Book> aBook,
shared_ptr<FormatPlugin> aFormatPlugin) :
BooksBook::CoverTask(aStorage, aBook),
BooksBook::CoverTask(aStateDir, aBook),
iFormatPlugin(aFormatPlugin) {}
virtual void performTask();
@ -179,10 +179,10 @@ void BooksBook::LoadCoverTask::performTask()
{
if (!isCanceled()) {
// Try to load cached (or custom) cover
if (iStorage.isValid()) {
if (!iStateDir.isEmpty()) {
QString coverPrefix(QString::fromStdString(
iBook->file().name(false)) + BOOK_COVER_SUFFIX);
QDirIterator it(iStorage.configDir());
QDirIterator it(iStateDir);
while (it.hasNext()) {
QString path(it.next());
if (it.fileName().startsWith(coverPrefix)) {
@ -231,8 +231,8 @@ void BooksBook::LoadCoverTask::performTask()
class BooksBook::GuessCoverTask : public BooksBook::CoverTask
{
public:
GuessCoverTask(BooksStorage aStorage, shared_ptr<Book> aBook) :
BooksBook::CoverTask(aStorage, aBook) {}
GuessCoverTask(QString aStateDir, shared_ptr<Book> aBook) :
BooksBook::CoverTask(aStateDir, aBook) {}
virtual void performTask();
};
@ -262,8 +262,17 @@ void BooksBook::GuessCoverTask::performTask()
// Save the extracted image
QString coverPath(cachedImagePath());
if (!coverPath.isEmpty() && iCoverImage.save(coverPath)) {
HDEBUG("saved cover to" << qPrintable(coverPath));
if (!coverPath.isEmpty()) {
QFileInfo file(coverPath);
QDir dir(file.dir());
if (!dir.mkpath(dir.absolutePath())) {
HWARN("failed to create" << qPrintable(dir.absolutePath()));
}
if (iCoverImage.save(coverPath)) {
HDEBUG("saved cover to" << qPrintable(coverPath));
} else {
HWARN("failed to save" << qPrintable(coverPath));
}
}
} else if (isCanceled()) {
HDEBUG("cancelled" << iBook->title().c_str());
@ -295,7 +304,8 @@ BooksBook::BooksBook(QObject* aParent) :
init();
}
BooksBook::BooksBook(const BooksStorage& aStorage, shared_ptr<Book> aBook) :
BooksBook::BooksBook(const BooksStorage& aStorage, QString aRelativePath,
shared_ptr<Book> aBook) :
QObject(NULL),
iRef(1),
iStorage(aStorage),
@ -315,8 +325,8 @@ BooksBook::BooksBook(const BooksStorage& aStorage, shared_ptr<Book> aBook) :
iAuthors += QString::fromStdString(authors[i]->name());
}
if (iStorage.isValid()) {
iStateFilePath = iStorage.configDir().path() + "/" +
iFileName + BOOKS_STATE_FILE_SUFFIX;
iStateDir = iStorage.configDir().path() + "/" + aRelativePath;
iStateFilePath = iStateDir + "/" + iFileName + BOOKS_STATE_FILE_SUFFIX;
// Load the state
QVariantMap state;
if (HarbourJson::load(iStateFilePath, state)) {
@ -382,6 +392,11 @@ QString BooksBook::fileName() const
return iFileName;
}
bool BooksBook::accessible() const
{
return !iCopyingOut;
}
void BooksBook::setLastPos(const BooksPos& aPos)
{
if (iLastPos != aPos) {
@ -430,7 +445,7 @@ bool BooksBook::requestCoverImage()
if (!iBook.isNull() && !iFormatPlugin.isNull() &&
!iCoverTasksDone && !iCoverTask) {
HDEBUG(iTitle);
iCoverTask = new LoadCoverTask(iStorage, iBook, iFormatPlugin);
iCoverTask = new LoadCoverTask(iStateDir, iBook, iFormatPlugin);
connect(iCoverTask, SIGNAL(done()), SLOT(onLoadCoverTaskDone()));
iTaskQueue->submit(iCoverTask);
Q_EMIT loadingCoverChanged();
@ -469,7 +484,7 @@ void BooksBook::onLoadCoverTaskDone()
iCoverTasksDone = true;
Q_EMIT loadingCoverChanged();
} else {
iCoverTask = new GuessCoverTask(iStorage, iBook);
iCoverTask = new GuessCoverTask(iStateDir, iBook);
connect(iCoverTask, SIGNAL(done()), SLOT(onGuessCoverTaskDone()));
iTaskQueue->submit(iCoverTask);
}
@ -514,7 +529,7 @@ void BooksBook::deleteFiles()
} else {
HWARN("failed to delete" << qPrintable(iPath));
}
QDirIterator it(iStorage.configDir());
QDirIterator it(iStateDir);
while (it.hasNext()) {
QString path(it.next());
if (it.fileName().startsWith(iFileName)) {

View file

@ -65,8 +65,9 @@ class BooksBook : public QObject, public BooksItem
Q_PROPERTY(bool copyingOut READ copyingOut NOTIFY copyingOutChanged)
public:
BooksBook(QObject* aParent = NULL);
BooksBook(const BooksStorage& aStorage, shared_ptr<Book> aBook);
explicit BooksBook(QObject* aParent = NULL);
BooksBook(const BooksStorage& aStorage, QString aRelativePath,
shared_ptr<Book> aBook);
~BooksBook();
QString path() const { return iPath; }
@ -75,7 +76,7 @@ public:
BooksPos lastPos() const { return iLastPos; }
void setLastPos(const BooksPos& aPos);
shared_ptr<Book> bookRef() const { return iBook; }
bool accessible() const { return !iCopyingOut; }
bool copyingOut() const { return iCopyingOut; }
bool loadingCover() const { return !iCoverTasksDone; }
bool hasCoverImage() const;
@ -85,9 +86,8 @@ public:
QImage coverImage();
void setCopyingOut(bool aValue);
void deleteFiles();
// BooksListItem
// BooksItem
virtual BooksItem* retain();
virtual void release();
virtual QObject* object();
@ -95,6 +95,8 @@ public:
virtual BooksBook* book();
virtual QString name() const;
virtual QString fileName() const;
virtual bool accessible() const;
virtual void deleteFiles();
Q_SIGNALS:
void coverImageChanged();
@ -128,6 +130,7 @@ private:
QString iAuthors;
QString iFileName;
QString iPath;
QString iStateDir;
QString iStateFilePath;
};

View file

@ -227,6 +227,7 @@ void BooksImportModel::Task::scanDir(QDir aDir)
// Files first
if (!isCanceled()) {
HDEBUG("checking" << aDir.canonicalPath());
BooksStorage dummy;
QFileInfoList fileList = aDir.entryInfoList(QDir::Files |
QDir::Readable, QDir::Time);
const int n = fileList.count();
@ -238,7 +239,7 @@ void BooksImportModel::Task::scanDir(QDir aDir)
if (!book.isNull()) {
if (!isDuplicate(filePath, iDestFiles) &&
!isDuplicate(filePath, iSrcFiles)) {
BooksBook* newBook = new BooksBook(BooksStorage(), book);
BooksBook* newBook = new BooksBook(dummy, QString(), book);
newBook->moveToThread(thread());
iBooks.append(newBook);
iSrcFiles.append(fileInfo);

View file

@ -52,6 +52,8 @@ public:
virtual BooksBook* book() = 0;
virtual QString name() const = 0;
virtual QString fileName() const = 0;
virtual bool accessible() const = 0;
virtual void deleteFiles() = 0;
};
#endif // BOOKS_ITEM_H

125
app/src/BooksPathModel.cpp Normal file
View file

@ -0,0 +1,125 @@
/*
* Copyright (C) 2015 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Nemo Mobile nor the names of its contributors
* may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "BooksPathModel.h"
#include "HarbourDebug.h"
enum BooksPathModelRole {
BooksPathModelName = Qt::UserRole,
BooksPathModelPath
};
BooksPathModel::BooksPathModel(QObject* aParent) :
QAbstractListModel(aParent)
{
#if QT_VERSION < 0x050000
setRoleNames(roleNames());
#endif
}
void BooksPathModel::setPath(QString aPath)
{
HDEBUG(aPath);
if (iPath != aPath) {
iPath = aPath;
QStringList newNames = aPath.split('/', QString::SkipEmptyParts);
const int oldSize = iList.size();
const int newSize = newNames.size();
int i;
QString path;
QStringList newPaths;
for (i=0; i<newSize; i++) {
if (!path.isEmpty()) path += "/";
path += newNames.at(i);
newPaths.append(path);
}
if (oldSize < newSize) {
beginInsertRows(QModelIndex(), oldSize, newSize-1);
for (int i=oldSize; i<newSize; i++) {
iList.append(Data(newNames.at(i), newPaths.at(i)));
}
endInsertRows();
Q_EMIT countChanged();
} else if (oldSize > newSize) {
beginRemoveRows(QModelIndex(), newSize, oldSize-1);
do iList.removeLast(); while (iList.size() > newSize);
endRemoveRows();
Q_EMIT countChanged();
}
for (i=0; i<newSize; i++) {
bool changed = false;
if (iList.at(i).iName != newNames.at(i)) {
iList[i].iName = newNames.at(i);
changed = true;
}
if (iList.at(i).iPath != newPaths.at(i)) {
iList[i].iPath = newPaths.at(i);
changed = true;
}
if (changed) {
QModelIndex index = createIndex(i, 0);
Q_EMIT dataChanged(index, index);
}
}
Q_EMIT pathChanged();
}
}
QHash<int,QByteArray> BooksPathModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles.insert(BooksPathModelName, "name");
roles.insert(BooksPathModelPath, "path");
return roles;
}
int BooksPathModel::rowCount(const QModelIndex&) const
{
return iList.count();
}
QVariant BooksPathModel::data(const QModelIndex& aIndex, int aRole) const
{
const int i = aIndex.row();
if (validIndex(i)) {
switch (aRole) {
case BooksPathModelName: return iList.at(i).iName;
case BooksPathModelPath: return iList.at(i).iPath;
}
}
return QVariant();
}

86
app/src/BooksPathModel.h Normal file
View file

@ -0,0 +1,86 @@
/*
* Copyright (C) 2015 Jolla Ltd.
* Contact: Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Nemo Mobile nor the names of its contributors
* may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef BOOKS_PATH_MODEL_H
#define BOOKS_PATH_MODEL_H
#include "BooksTypes.h"
#include <QHash>
#include <QVariant>
#include <QByteArray>
#include <QAbstractListModel>
#include <QtQml>
class BooksPathModel: public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
public:
explicit BooksPathModel(QObject* aParent = NULL);
int count() const { return iList.count(); }
QString path() const { return iPath; }
void setPath(QString aPath);
// QAbstractListModel
virtual QHash<int,QByteArray> roleNames() const;
virtual int rowCount(const QModelIndex& aParent) const;
virtual QVariant data(const QModelIndex& aIndex, int aRole) const;
Q_SIGNALS:
void countChanged();
void pathChanged();
private:
bool validIndex(int aIndex) const;
private:
class Data {
public:
QString iName;
QString iPath;
Data(QString aName, QString aPath) : iName(aName), iPath(aPath) {}
};
QList<Data> iList;
QString iPath;
};
QML_DECLARE_TYPE(BooksPathModel)
inline bool BooksPathModel::validIndex(int aIndex) const
{ return aIndex >= 0 && aIndex < iList.count(); }
#endif // BOOKS_PATH_MODEL_H

View file

@ -364,12 +364,13 @@ BooksSettings::updateCurrentBook()
} else if (!iCurrentBook || iCurrentBook->path() != path) {
shared_ptr<Book> book = BooksUtil::bookFromFile(path);
if (!book.isNull()) {
QString rel;
QFileInfo info(path);
BooksStorageManager* mgr = BooksStorageManager::instance();
BooksStorage storage = mgr->storageForPath(info.path());
BooksStorage storage = mgr->storageForPath(info.path(), &rel);
if (storage.isValid()) {
if (iCurrentBook) iCurrentBook->release();
iCurrentBook = new BooksBook(storage, book);
iCurrentBook = new BooksBook(storage, rel, book);
iCurrentBook->requestCoverImage();
return true;
}

View file

@ -91,11 +91,10 @@ public:
class BooksShelf::LoadTask : public BooksTask
{
Q_OBJECT
public:
LoadTask(BooksStorage aStorage, QString aPath, QString aStateFilePath) :
iStorage(aStorage), iPath(aPath), iStateFilePath(aStateFilePath) {}
LoadTask(BooksStorage aStorage, QString aRelPath, QString aStateFile) :
iStorage(aStorage), iRelativePath(aRelPath),
iStateFilePath(aStateFile) {}
~LoadTask();
void performTask();
@ -103,20 +102,17 @@ public:
int findBook(QString aFileName) const;
static int find(QFileInfoList aList, QString aFileName, int aStart);
Q_SIGNALS:
void bookFound(BooksBook* aBook);
public:
BooksStorage iStorage;
QString iPath;
QString iRelativePath;
QString iStateFilePath;
QList<BooksBook*> iBooks;
QList<BooksItem*> iItems;
};
BooksShelf::LoadTask::~LoadTask()
{
const int n = iBooks.count();
for (int i=0; i<n; i++) iBooks.at(i)->release();
const int n = iItems.count();
for (int i=0; i<n; i++) iItems.at(i)->release();
}
int BooksShelf::LoadTask::find(QFileInfoList aList, QString aName, int aStart)
@ -135,9 +131,10 @@ int BooksShelf::LoadTask::find(QFileInfoList aList, QString aName, int aStart)
int BooksShelf::LoadTask::findBook(QString aFileName) const
{
if (!aFileName.isEmpty()) {
const int n = iBooks.count();
const int n = iItems.count();
for (int i=0; i<n; i++) {
if (iBooks.at(i)->fileName() == aFileName) {
BooksItem* item = iItems.at(i);
if (item->book() && item->fileName() == aFileName) {
return i;
}
}
@ -148,9 +145,11 @@ int BooksShelf::LoadTask::findBook(QString aFileName) const
void BooksShelf::LoadTask::performTask()
{
if (!isCanceled()) {
QDir dir(iPath);
HDEBUG("checking" << iPath);
QFileInfoList list = dir.entryInfoList(QDir::Files, QDir::Time);
QString path(iStorage.fullPath(iRelativePath));
HDEBUG("checking" << path);
QDir dir(path);
QFileInfoList list = dir.entryInfoList(QDir::Files |
QDir::Dirs | QDir::NoDotAndDotDot, QDir::Time);
// Restore the order
QVariantMap state;
@ -175,18 +174,31 @@ void BooksShelf::LoadTask::performTask()
const int n = list.count();
for (int i=0; i<n && !isCanceled(); i++) {
std::string path(list.at(i).filePath().toStdString());
shared_ptr<Book> book = BooksUtil::bookFromFile(path);
if (!book.isNull()) {
BooksBook* newBook = new BooksBook(iStorage, book);
newBook->moveToThread(thread());
iBooks.append(newBook);
HDEBUG("[" << iBooks.size() << "]" <<
qPrintable(newBook->fileName()) <<
newBook->title());
Q_EMIT bookFound(newBook);
const QFileInfo& info = list.at(i);
QString path(info.filePath());
if (info.isDir()) {
HDEBUG("directory:" << qPrintable(path));
QString folderPath(iRelativePath);
if (!folderPath.isEmpty() && !folderPath.endsWith('/')) {
folderPath += '/';
}
folderPath += info.fileName();
BooksShelf* newShelf = new BooksShelf(iStorage, folderPath);
newShelf->moveToThread(thread());
iItems.append(newShelf);
} else {
HDEBUG("not a book:" << path.c_str());
shared_ptr<Book> book = BooksUtil::bookFromFile(path);
if (!book.isNull()) {
BooksBook* newBook = new BooksBook(iStorage,
iRelativePath, book);
newBook->moveToThread(thread());
iItems.append(newBook);
HDEBUG("[" << iItems.size() << "]" <<
qPrintable(newBook->fileName()) <<
newBook->title());
} else {
HDEBUG("not a book:" << qPrintable(path));
}
}
}
}
@ -231,7 +243,9 @@ public:
QObject* object() { return iItem ? iItem->object() : NULL; }
BooksBook* book() { return iItem ? iItem->book() : NULL; }
BooksShelf* shelf() { return iItem ? iItem->shelf() : NULL; }
bool accessible();
bool accessible() const { return !iCopyTask && iItem && iItem->accessible(); }
bool isBook() const { return iItem && iItem->book(); }
bool isShelf() const { return iItem && iItem->shelf(); }
bool copyingOut();
bool copyingIn() { return iCopyTask != NULL; }
int copyPercent() { return iCopyTask ? iCopyTask->iCopyPercent : 0; }
@ -298,16 +312,6 @@ void BooksShelf::Data::setBook(BooksBook* aBook, bool aExternal)
}
}
inline bool BooksShelf::Data::accessible()
{
if (iCopyTask) {
return false;
} else {
BooksBook* bookItem = book();
return bookItem && bookItem->accessible();
}
}
inline bool BooksShelf::Data::copyingOut()
{
BooksBook* bookItem = book();
@ -429,33 +433,83 @@ class BooksShelf::DeleteTask : public BooksTask
{
Q_OBJECT
public:
DeleteTask(BooksBook* aBook);
DeleteTask(BooksItem* aItem);
~DeleteTask();
void performTask();
public:
BooksBook* iBook;
BooksItem* iItem;
};
BooksShelf::DeleteTask::DeleteTask(BooksBook* aBook) :
iBook(aBook)
BooksShelf::DeleteTask::DeleteTask(BooksItem* aItem) :
iItem(aItem)
{
iBook->retain();
iBook->cancelCoverRequest();
iItem->retain();
}
BooksShelf::DeleteTask::~DeleteTask()
{
iBook->release();
iItem->release();
}
void BooksShelf::DeleteTask::performTask()
{
if (isCanceled()) {
HDEBUG("cancelled" << iBook->title());
HDEBUG("cancelled" << iItem->fileName());
} else {
HDEBUG(iBook->title());
iBook->deleteFiles();
iItem->deleteFiles();
}
}
// ==========================================================================
// BooksShelf::Counts
// ==========================================================================
class BooksShelf::Counts {
public:
Counts(BooksShelf* aShelf);
void count(BooksShelf* aShelf);
void emitSignals(BooksShelf* aShelf);
int iTotalCount;
int iBookCount;
int iShelfCount;
};
BooksShelf::Counts::Counts(BooksShelf* aShelf)
{
count(aShelf);
}
void BooksShelf::Counts::count(BooksShelf* aShelf)
{
iTotalCount = aShelf->iList.count(),
iBookCount = 0;
iShelfCount = 0;
for (int i=0; i<iTotalCount; i++) {
const Data* data = aShelf->iList.at(i);
if (data->isBook()) {
iBookCount++;
} else if (data->isShelf()) {
iShelfCount++;
}
}
}
void BooksShelf::Counts::emitSignals(BooksShelf* aShelf)
{
const int oldTotalCount = iTotalCount;
const int oldBookCount = iBookCount;
const int oldShelfCount = iShelfCount;
count(aShelf);
if (oldBookCount != iBookCount) {
Q_EMIT aShelf->bookCountChanged();
}
if (oldShelfCount != iShelfCount) {
Q_EMIT aShelf->shelfCountChanged();
}
if (oldTotalCount != iTotalCount) {
Q_EMIT aShelf->countChanged();
}
}
@ -472,33 +526,48 @@ BooksShelf::BooksShelf(QObject* aParent) :
iSaveTimer(new BooksSaveTimer(this)),
iTaskQueue(BooksTaskQueue::instance())
{
#if QT_VERSION < 0x050000
setRoleNames(roleNames());
#endif
QQmlEngine::setObjectOwnership(&iStorage, QQmlEngine::CppOwnership);
init();
connect(iSaveTimer, SIGNAL(save()), SLOT(saveState()));
}
BooksShelf::BooksShelf(BooksStorage aStorage, QString aRelativePath) :
iLoadTask(NULL),
iRelativePath(aRelativePath),
iStorage(aStorage),
iDummyItemIndex(-1),
iEditMode(false),
iRef(1),
iSaveTimer(NULL),
iTaskQueue(BooksTaskQueue::instance())
{
init();
// Refcounted BooksShelf objects are managed by C++ code
// They also don't need to read the content of the directory -
// only the objects allocated by QML do that.
QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
int slashPos = aRelativePath.lastIndexOf('/');
iFileName = (slashPos >= 0) ?
aRelativePath.right(aRelativePath.length() - slashPos - 1) :
aRelativePath;
updatePath();
}
BooksShelf::~BooksShelf()
{
const int n = iDeleteTasks.count();
for (int i=0; i<n; i++) iDeleteTasks.at(i)->release(this);
if (iLoadTask) iLoadTask->release(this);
if (iSaveTimer->saveRequested()) saveState();
removeAllBooks();
if (iSaveTimer && iSaveTimer->saveRequested()) saveState();
qDeleteAll(iList);
HDEBUG("destroyed");
}
void BooksShelf::removeAllBooks()
void BooksShelf::init()
{
while (!iList.isEmpty()) {
Data* data = iList.takeLast();
BooksBook* book = data->book();
if (book) {
Q_EMIT bookRemoved(book);
}
delete data;
}
#if QT_VERSION < 0x050000
setRoleNames(roleNames());
#endif
QQmlEngine::setObjectOwnership(&iStorage, QQmlEngine::CppOwnership);
}
void BooksShelf::setRelativePath(QString aPath)
@ -521,25 +590,31 @@ void BooksShelf::setDevice(QString aDevice)
void BooksShelf::updatePath()
{
BooksLoadingSignalBlocker block(this);
const QString oldPath = iPath;
iPath.clear();
if (iStorage.isValid()) {
QString newPath(iStorage.root());
if (!iRelativePath.isEmpty()) {
if (!newPath.endsWith('/')) newPath += '/';
newPath += iRelativePath;
}
iPath = QDir::cleanPath(newPath);
iPath = iStorage.fullPath(iRelativePath);
}
if (oldPath != iPath) {
const int oldCount = iList.count();
const int oldDummyItemIndex = iDummyItemIndex;
beginResetModel();
Counts counts(this);
HDEBUG(iPath);
removeAllBooks();
// Clear the model
if (!iList.isEmpty()) {
beginRemoveRows(QModelIndex(), 0, iList.size()-1);
while (!iList.isEmpty()) {
Data* data = iList.takeLast();
BooksBook* book = data->book();
if (book) {
Q_EMIT bookRemoved(book);
}
delete data;
}
endRemoveRows();
}
iDummyItemIndex = -1;
if (!iPath.isEmpty()) loadBookList();
endResetModel();
Q_EMIT pathChanged();
if (oldDummyItemIndex != iDummyItemIndex) {
Q_EMIT dummyItemIndexChanged();
@ -547,9 +622,7 @@ void BooksShelf::updatePath()
Q_EMIT hasDummyItemChanged();
}
}
if (oldCount != iList.count()) {
Q_EMIT countChanged();
}
counts.emitSignals(this);
}
}
@ -557,45 +630,42 @@ void BooksShelf::onLoadTaskDone()
{
HASSERT(iLoadTask);
HASSERT(iLoadTask == sender());
iLoadTask->release(this);
iLoadTask = NULL;
Q_EMIT loadingChanged();
}
void BooksShelf::onBookFound(BooksBook* aBook)
{
if (iLoadTask && iLoadTask == sender()) {
beginInsertRows(QModelIndex(), iList.count(), iList.count());
iList.append(new Data(this, aBook->retain(), false));
endInsertRows();
Q_EMIT bookAdded(aBook);
Q_EMIT countChanged();
BooksLoadingSignalBlocker block(this);
const int oldSize = iList.size();
const int newSize = iLoadTask->iItems.size();
HASSERT(iList.isEmpty());
if (newSize > 0) {
Counts counts(this);
beginInsertRows(QModelIndex(), oldSize, oldSize + newSize - 1);
for (int i=0; i<newSize; i++) {
BooksItem* item = iLoadTask->iItems.at(i);
BooksBook* book = item->book();
if (book) {
Q_EMIT bookAdded(book);
}
iList.append(new Data(this, item->retain(), false));
}
endInsertRows();
counts.emitSignals(this);
}
iLoadTask->release(this);
iLoadTask = NULL;
}
}
void BooksShelf::loadBookList()
{
if (!iList.isEmpty()) {
beginResetModel();
removeAllBooks();
endResetModel();
}
const bool wasLoading = loading();
BooksLoadingSignalBlocker block(this);
if (iLoadTask) iLoadTask->release(this);
if (iPath.isEmpty()) {
iLoadTask = NULL;
} else {
HDEBUG(iPath);
iLoadTask = new LoadTask(iStorage, iPath, stateFileName());
connect(iLoadTask, SIGNAL(bookFound(BooksBook*)),
SLOT(onBookFound(BooksBook*)), Qt::QueuedConnection);
iLoadTask = new LoadTask(iStorage, iRelativePath, stateFileName());
connect(iLoadTask, SIGNAL(done()), SLOT(onLoadTaskDone()));
iTaskQueue->submit(iLoadTask);
}
if (wasLoading != loading()) {
Q_EMIT loadingChanged();
}
}
void BooksShelf::saveState()
@ -614,7 +684,7 @@ void BooksShelf::saveState()
void BooksShelf::queueStateSave()
{
if (iEditMode) {
if (iEditMode && iSaveTimer) {
iSaveTimer->requestSave();
}
}
@ -622,7 +692,7 @@ void BooksShelf::queueStateSave()
QString BooksShelf::stateFileName() const
{
return iStorage.isValid() ?
iStorage.configDir().path() + ("/" SHELF_STATE_FILE) :
iStorage.configDir().path() + "/" + iRelativePath + ("/" SHELF_STATE_FILE) :
QString();
}
@ -672,7 +742,7 @@ void BooksShelf::setEditMode(bool aEditMode)
if (iEditMode != aEditMode) {
iEditMode = aEditMode;
HDEBUG(iEditMode);
if (iSaveTimer->saveRequested()) {
if (iSaveTimer && iSaveTimer->saveRequested()) {
iSaveTimer->cancelSave();
saveState();
}
@ -723,7 +793,7 @@ BooksBook* BooksShelf::book()
QString BooksShelf::name() const
{
return iName;
return iFileName;
}
QString BooksShelf::fileName() const
@ -731,11 +801,34 @@ QString BooksShelf::fileName() const
return iFileName;
}
bool BooksShelf::accessible() const
{
return true;
}
int BooksShelf::count() const
{
return iList.count();
}
int BooksShelf::bookCount() const
{
int n=0, total = iList.count();
for(int i=0; i<total; i++) {
if (iList.at(i)->book()) n++;
}
return n;
}
int BooksShelf::shelfCount() const
{
int n=0, total = iList.count();
for(int i=0; i<total; i++) {
if (iList.at(i)->shelf()) n++;
}
return n;
}
QObject* BooksShelf::get(int aIndex) const
{
if (validIndex(aIndex)) {
@ -802,25 +895,28 @@ void BooksShelf::move(int aFrom, int aTo)
}
}
void BooksShelf::remove(int aIndex)
void BooksShelf::submitDeleteTask(int aIndex)
{
BooksBook* book = removeBook(aIndex);
if (book) {
Q_EMIT bookRemoved(book);
BooksItem* item = iList.at(aIndex)->iItem;
if (item) {
DeleteTask* task = new DeleteTask(item);
iDeleteTasks.append(task);
iTaskQueue->submit(task);
BooksBook* book = item->book();
if (book) {
book->cancelCoverRequest();
Q_EMIT bookRemoved(book);
}
}
}
BooksBook* BooksShelf::removeBook(int aIndex)
void BooksShelf::remove(int aIndex)
{
if (validIndex(aIndex)) {
Counts counts(this);
HDEBUG(iList.at(aIndex)->name());
beginRemoveRows(QModelIndex(), aIndex, aIndex);
BooksBook* book = iList.at(aIndex)->book();
if (book) {
DeleteTask* task = new DeleteTask(book);
iDeleteTasks.append(task);
iTaskQueue->submit(task);
}
submitDeleteTask(aIndex);
if (iDummyItemIndex == aIndex) {
iDummyItemIndex = -1;
Q_EMIT hasDummyItemChanged();
@ -828,26 +924,19 @@ BooksBook* BooksShelf::removeBook(int aIndex)
}
delete iList.takeAt(aIndex);
queueStateSave();
Q_EMIT countChanged();
counts.emitSignals(this);
endRemoveRows();
return book;
}
return NULL;
}
void BooksShelf::removeAll()
{
if (!iList.isEmpty()) {
Counts counts(this);
beginRemoveRows(QModelIndex(), 0, iList.count()-1);
const int n = iList.count();
for (int i=0; i<n; i++) {
BooksBook* book = iList.at(i)->book();
if (book) {
DeleteTask* task = new DeleteTask(book);
iDeleteTasks.append(task);
iTaskQueue->submit(task);
Q_EMIT bookRemoved(book);
}
submitDeleteTask(i);
}
if (iDummyItemIndex >= 0) {
iDummyItemIndex = -1;
@ -857,7 +946,7 @@ void BooksShelf::removeAll()
qDeleteAll(iList);
iList.clear();
queueStateSave();
Q_EMIT countChanged();
counts.emitSignals(this);
endRemoveRows();
}
}
@ -908,11 +997,12 @@ void BooksShelf::importBook(QObject* aBook)
} else {
HDEBUG(qPrintable(book->path()) << "->" << qPrintable(iPath));
beginInsertRows(QModelIndex(), 0, 0);
Counts counts(this);
Data* data = new Data(this, book->retain(), true);
iList.insert(0, data);
iTaskQueue->submit(new CopyTask(data, book));
counts.emitSignals(this);
endInsertRows();
Q_EMIT countChanged();
saveState();
}
}
@ -990,7 +1080,7 @@ void BooksShelf::onCopyTaskDone()
if (task->iSuccess) {
shared_ptr<Book> book = BooksUtil::bookFromFile(task->iDest);
if (!book.isNull()) {
copy = new BooksBook(iStorage, book);
copy = new BooksBook(iStorage, iRelativePath, book);
copy->setLastPos(src->lastPos());
copy->setCoverImage(src->coverImage());
copy->requestCoverImage();
@ -1024,7 +1114,7 @@ void BooksShelf::onCopyTaskDone()
QModelIndex index(createIndex(row, 0));
Q_EMIT dataChanged(index, index);
} else {
removeBook(row);
remove(row);
}
}
}
@ -1039,6 +1129,22 @@ void BooksShelf::onDeleteTaskDone()
}
}
void BooksShelf::deleteFiles()
{
if (iStorage.isValid()) {
QString path(iStorage.fullPath(iRelativePath));
HDEBUG("removing" << path);
if (!QDir(path).removeRecursively()) {
HWARN("some content couldn't be deleted under" << path);
}
path = iStorage.configDir().path() + "/" + iRelativePath;
HDEBUG("removing" << path);
if (!QDir(path).removeRecursively()) {
HWARN("some content couldn't be deleted under" << path);
}
}
}
QHash<int,QByteArray> BooksShelf::roleNames() const
{
QHash<int, QByteArray> roles;

View file

@ -39,6 +39,7 @@
#include "BooksSaveTimer.h"
#include "BooksTask.h"
#include "BooksTaskQueue.h"
#include "BooksLoadingProperty.h"
#include <QHash>
#include <QVariant>
@ -46,11 +47,14 @@
#include <QAbstractListModel>
#include <QtQml>
class BooksShelf: public QAbstractListModel, public BooksItem
class BooksShelf: public QAbstractListModel, public BooksItem, public BooksLoadingProperty
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(int bookCount READ bookCount NOTIFY bookCountChanged)
Q_PROPERTY(int shelfCount READ shelfCount NOTIFY shelfCountChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
Q_PROPERTY(bool accessible READ accessible CONSTANT)
Q_PROPERTY(QString path READ path NOTIFY pathChanged)
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString device READ device WRITE setDevice NOTIFY deviceChanged)
@ -59,10 +63,12 @@ class BooksShelf: public QAbstractListModel, public BooksItem
Q_PROPERTY(bool hasDummyItem READ hasDummyItem WRITE setHasDummyItem NOTIFY hasDummyItemChanged)
Q_PROPERTY(int dummyItemIndex READ dummyItemIndex WRITE setDummyItemIndex NOTIFY dummyItemIndexChanged)
Q_PROPERTY(BooksBook* book READ book CONSTANT)
Q_PROPERTY(BooksShelf* shelf READ shelf CONSTANT)
Q_PROPERTY(QObject* storage READ storage CONSTANT)
public:
explicit BooksShelf(QObject* aParent = NULL);
BooksShelf(BooksStorage aStorage, QString aRelativePath);
~BooksShelf();
Q_INVOKABLE QObject* get(int aIndex) const;
@ -77,6 +83,8 @@ public:
bool loading() const { return iLoadTask != NULL; }
int count() const;
int bookCount() const;
int shelfCount() const;
QString path() const { return iPath; }
QString relativePath() const { return iRelativePath; }
void setRelativePath(QString aPath);
@ -100,7 +108,7 @@ public:
virtual int rowCount(const QModelIndex& aParent) const;
virtual QVariant data(const QModelIndex& aIndex, int aRole) const;
// BooksListItem
// BooksItem
virtual BooksItem* retain();
virtual void release();
virtual QObject* object();
@ -108,10 +116,14 @@ public:
virtual BooksBook* book();
virtual QString name() const;
virtual QString fileName() const;
virtual bool accessible() const;
virtual void deleteFiles();
Q_SIGNALS:
void loadingChanged();
void countChanged();
void bookCountChanged();
void shelfCountChanged();
void pathChanged();
void nameChanged();
void deviceChanged();
@ -124,7 +136,6 @@ Q_SIGNALS:
private Q_SLOTS:
void onLoadTaskDone();
void onBookFound(BooksBook* aBook);
void onBookAccessibleChanged();
void onBookCopyingOutChanged();
void onBookMovedAway();
@ -134,6 +145,7 @@ private Q_SLOTS:
void saveState();
private:
void init();
QString stateFileName() const;
int bookIndex(BooksBook* aBook) const;
int itemIndex(QString aFileName, int aStartIndex = 0) const;
@ -142,19 +154,20 @@ private:
void queueStateSave();
void loadBookList();
void updatePath();
void removeAllBooks();
BooksBook* removeBook(int aIndex);
void submitDeleteTask(int aIndex);
private:
class Data;
class Counts;
class CopyTask;
class LoadTask;
class ImportTask;
class DeleteTask;
friend class Counts;
QList<Data*> iList;
QList<DeleteTask*> iDeleteTasks;
LoadTask* iLoadTask;
QString iName;
QString iFileName;
QString iPath;
QString iRelativePath;

View file

@ -203,6 +203,19 @@ bool BooksStorage::isPresent() const
return iPrivate && iPrivate->iPresent;
}
QString BooksStorage::fullPath(QString aRelativePath) const
{
if (iPrivate) {
QString path(booksDir().path());
if (!aRelativePath.isEmpty()) {
if (!path.endsWith('/')) path += '/';
path += aRelativePath;
}
return QDir::cleanPath(path);
}
return QString();
}
bool BooksStorage::equal(const BooksStorage& aStorage) const
{
if (iPrivate == aStorage.iPrivate) {
@ -224,7 +237,6 @@ BooksStorage& BooksStorage::operator = (const BooksStorage& aStorage)
return *this;
}
// ==========================================================================
// BooksStorageManager::Private
// ==========================================================================
@ -250,7 +262,7 @@ public:
~Private();
int findDevice(QString aDevice) const;
int findPath(QString aPath) const;
int findPath(QString aPath, QString* aRelPath) const;
public:
QList<BooksStorage> iStorageList;
@ -335,13 +347,22 @@ int BooksStorageManager::Private::findDevice(QString aDevice) const
return -1;
}
int BooksStorageManager::Private::findPath(QString aPath) const
int BooksStorageManager::Private::findPath(QString aPath, QString* aRelPath) const
{
if (!aPath.isEmpty()) {
const int n = iStorageList.count();
for (int i=0; i<n; i++) {
BooksStorage::Private* data = iStorageList.at(i).iPrivate;
if (aPath.startsWith(data->iBooksDir.path())) {
if (aRelPath) {
int i = data->iBooksDir.path().length();
while (aPath.length() > i && aPath.at(i) == '/') i++;
if (aPath.length() > i) {
*aRelPath = aPath.right(aPath.length() - i);
} else {
*aRelPath = QString();
}
}
return i;
}
}
@ -414,9 +435,9 @@ BooksStorage BooksStorageManager::storageForDevice(QString aDevice) const
return (index >= 0) ? iPrivate->iStorageList.at(index) : BooksStorage();
}
BooksStorage BooksStorageManager::storageForPath(QString aPath) const
BooksStorage BooksStorageManager::storageForPath(QString aPath, QString* aRelPath) const
{
int index = iPrivate->findPath(aPath);
int index = iPrivate->findPath(aPath, aRelPath);
return (index >= 0) ? iPrivate->iStorageList.at(index) : BooksStorage();
}

View file

@ -57,6 +57,8 @@ public:
QDir configDir() const;
QString label() const { return booksDir().dirName(); }
QString root() const { return booksDir().path(); }
QString fullConfigPath(QString aRelativePath) const;
QString fullPath(QString aRelativePath) const;
bool isValid() const { return iPrivate != NULL; }
bool isInternal() const;
@ -93,7 +95,7 @@ public:
int count() const;
QList<BooksStorage> storageList() const;
BooksStorage storageForDevice(QString aDevice) const;
BooksStorage storageForPath(QString aPath) const;
BooksStorage storageForPath(QString aPath, QString* aRelPath = NULL) const;
Q_SIGNALS:
void storageAdded(BooksStorage aStorage);

View file

@ -39,6 +39,7 @@
#include "BooksConfig.h"
#include "BooksSettings.h"
#include "BooksImportModel.h"
#include "BooksPathModel.h"
#include "BooksStorageModel.h"
#include "BooksPageWidget.h"
#include "BooksListWatcher.h"
@ -73,6 +74,7 @@ Q_DECL_EXPORT int main(int argc, char **argv)
BOOKS_QML_REGISTER(BooksBookModel, "BookModel");
BOOKS_QML_REGISTER(BooksCoverModel, "CoverModel");
BOOKS_QML_REGISTER(BooksImportModel, "BooksImportModel");
BOOKS_QML_REGISTER(BooksPathModel, "BooksPathModel");
BOOKS_QML_REGISTER(BooksStorageModel, "BookStorage");
BOOKS_QML_REGISTER(BooksPageWidget, "PageWidget");
BOOKS_QML_REGISTER(BooksListWatcher, "ListWatcher");