[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/BooksLoadingProperty.cpp \
src/BooksPageWidget.cpp \ src/BooksPageWidget.cpp \
src/BooksPaintContext.cpp \ src/BooksPaintContext.cpp \
src/BooksPathModel.cpp \
src/BooksSaveTimer.cpp \ src/BooksSaveTimer.cpp \
src/BooksSettings.cpp \ src/BooksSettings.cpp \
src/BooksShelf.cpp \ src/BooksShelf.cpp \
@ -152,6 +153,7 @@ HEADERS += \
src/BooksLoadingProperty.h \ src/BooksLoadingProperty.h \
src/BooksPageWidget.h \ src/BooksPageWidget.h \
src/BooksPaintContext.h \ src/BooksPaintContext.h \
src/BooksPathModel.h \
src/BooksPos.h \ src/BooksPos.h \
src/BooksSaveTimer.h \ src/BooksSaveTimer.h \
src/BooksSettings.h \ src/BooksSettings.h \

View file

@ -36,6 +36,7 @@ MouseArea {
id: root id: root
parent: dragInProgress ? dragParent : gridView parent: dragInProgress ? dragParent : gridView
anchors.fill: parent anchors.fill: parent
propagateComposedEvents: true
signal deleteItemAt(var index) signal deleteItemAt(var index)
signal dropItem(var mouseX, var mouseY) signal dropItem(var mouseX, var mouseY)
@ -84,13 +85,23 @@ MouseArea {
} }
} else { } else {
index = gridView.indexAt(mouseX + gridView.contentX, mouseY + currentShelfView.contentY) index = gridView.indexAt(mouseX + gridView.contentX, mouseY + currentShelfView.contentY)
if (index >= 0 && index === lastReleasedItemIndex) { if (index >= 0) {
if (index === lastReleasedItemIndex) {
var item = shelf.get(index); var item = shelf.get(index);
if (item.book && item.accessible) { if (item.accessible) {
if (item.book) {
shelfView.openBook(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() resetPressState()
dragScrollAnimation.stop() dragScrollAnimation.stop()
} }
@ -105,9 +116,17 @@ MouseArea {
} else { } else {
pressedDeleteItemIndex = -1 pressedDeleteItemIndex = -1
} }
if (mouseY + gridView.contentY < 0) {
// Let the header item handle it
mouse.accepted = false
}
} }
onReleased: { onReleased: {
stopDrag(mouseX, mouseY) stopDrag(mouseX, mouseY)
if (mouseY + gridView.contentY < 0) {
// Let the header item handle it
mouse.accepted = false
}
} }
onPressAndHold: { onPressAndHold: {
if (!shelfView.editMode) { 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 readonly property bool animating: scaling || moving
property bool _deleting: deleting && !deletingAll property bool _deleting: deleting && !deletingAll
property real _borderRadius: Theme.paddingSmall readonly property real _borderRadius: Theme.paddingSmall
property color _borderColor: Theme.primaryColor readonly property color _borderColor: Theme.primaryColor
readonly property real _borderWidth: 2
property bool scaledDown: (editMode && !dragged && !pressed && !dropped) 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 { BookCover {
id: cover id: cover
anchors { anchors {
margins: root.margins margins: root.margins
fill: parent fill: parent
} }
borderWidth: 2
borderRadius: _borderRadius borderRadius: _borderRadius
borderWidth: book ? _borderWidth : 0
borderColor: _borderColor borderColor: _borderColor
opacity: (copyingIn || copyingOut) ? 0.1 : 1 opacity: (copyingIn || copyingOut) ? 0.1 : 1
Behavior on opacity { FadeAnimation { } } 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 scrollRight()
signal scrollLeft() signal scrollLeft()
property bool _haveBooks: shelf && shelf.count property bool _haveBooks: shelfModel && shelfModel.count
property int _cellsPerRow: Math.floor(width/cellWidth) property int _cellsPerRow: Math.floor(width/cellWidth)
readonly property int _remorseTimeout: 5000 readonly property int _remorseTimeout: 5000
property bool _loading: !shelf || shelf.loading || startAnimationTimer.running property bool _loading: !shelfModel || shelfModel.loading || startAnimationTimer.running
property var _remorse property var _remorse
on_HaveBooksChanged: if (!_haveBooks) shelfView.stopEditing() on_HaveBooksChanged: if (!_haveBooks) shelfView.stopEditing()
@ -69,6 +69,12 @@ SilicaFlickable {
property bool needDummyItem: dragInProgress && dragItem.shelfIndex !== shelfView.shelfIndex property bool needDummyItem: dragInProgress && dragItem.shelfIndex !== shelfView.shelfIndex
onNeedDummyItemChanged: if (needDummyItem) hasDummyItem = true onNeedDummyItemChanged: if (needDummyItem) hasDummyItem = true
editMode: shelfView.editMode editMode: shelfView.editMode
onRelativePathChanged: longStartTimer.restart()
}
BooksPathModel {
id: pathModel
path: shelfModel.relativePath
} }
onEditModeChanged: { onEditModeChanged: {
@ -123,27 +129,60 @@ SilicaFlickable {
BooksStorageHeader { BooksStorageHeader {
id: storageHeader id: storageHeader
removable: removableStorage removable: removableStorage
count: shelfModel.count count: shelfModel.bookCount
showCount: !_loading 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 { SilicaGridView {
id: grid id: grid
anchors { anchors {
top: singleStorage ? parent.top : storageHeader.bottom top: storageHeader.bottom
left: parent.left left: parent.left
right: parent.right right: parent.right
bottom: parent.bottom bottom: parent.bottom
leftMargin: Math.floor((shelfView.width - _cellsPerRow * shelfView.cellWidth)/2) leftMargin: Math.floor((shelfView.width - _cellsPerRow * shelfView.cellWidth)/2)
} }
model: shelfModel model: shelfModel
interactive: !dragInProgress interactive: !dragInProgress && !scrollToTopAnimation.running
clip: true clip: true
opacity: (!_loading && _haveBooks) ? 1 : 0
visible: opacity > 0
cellWidth: shelfView.cellWidth cellWidth: shelfView.cellWidth
cellHeight: shelfView.cellHeight cellHeight: shelfView.cellHeight
flickableDirection: Flickable.VerticalFlick 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 { delegate: BooksShelfItem {
editMode: shelfView.editMode editMode: shelfView.editMode
dropped: dragItem.dropShelfIndex >= 0 && 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 itemOpacity: 1
property real minContentY: -headerItem.height
moveDisplaced: Transition { moveDisplaced: Transition {
SmoothedAnimation { properties: "x,y"; duration: 150 } SmoothedAnimation { properties: "x,y"; duration: 150 }
@ -209,33 +257,9 @@ SilicaFlickable {
} }
Behavior on y { SpringAnimation {} } Behavior on y { SpringAnimation {} }
Behavior on opacity { FadeAnimation {} }
VerticalScrollDecorator {} 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 { Timer {
id: longStartTimer id: longStartTimer
@ -245,7 +269,6 @@ SilicaFlickable {
if (shelf.loading) { if (shelf.loading) {
console.log(shelfModel.path, "startup is taking too long") console.log(shelfModel.path, "startup is taking too long")
startAnimationTimer.start() startAnimationTimer.start()
if (!_busyIndicator) _busyIndicator = busyIndicatorComponent.createObject(shelfView)
} }
} }
} }

View file

@ -33,65 +33,97 @@ import QtQuick 2.0
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
Column { Column {
id: root
anchors { anchors {
top: parent.top
left: parent.left left: parent.left
right: parent.right right: parent.right
topMargin: Theme.paddingMedium topMargin: Theme.paddingMedium
} }
spacing: 0 spacing: 0
visible: opacity > 0 y: needed ? Theme.paddingMedium : -height
opacity: singleStorage ? 0 : 1 property alias animationEnabled: yBehavior.enabled
Behavior on opacity { FadeAnimation {} }
Behavior on y {
id: yBehavior
enabled: false
NumberAnimation { duration: 200 }
}
property bool needed
property bool removable property bool removable
property int count property int count
property bool showCount: true property bool showCount: true
property int _shownCount
signal clicked()
function updateShownCount() {
if (count > 0) {
_shownCount = count
}
}
onCountChanged: updateShownCount()
Component.onCompleted: updateShownCount()
Item { Item {
width: parent.width width: parent.width
height: Math.max(left.height, right.height) height: Math.max(storageLabel.height, bookCount.height)
Row {
id: left BooksSDCardIcon {
id: icon
anchors { anchors {
left: parent.left left: parent.left
leftMargin: Theme.paddingMedium
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
} }
spacing: 0
Item {
width: Theme.paddingMedium
height: parent.height
}
BooksSDCardIcon {
visible: removableStorage visible: removableStorage
anchors.bottom: parent.bottom
height: storageLabel.height*3/4 height: storageLabel.height*3/4
} }
Item {
visible: removableStorage
width: Theme.paddingMedium
height: parent.height
}
Label { Label {
id: storageLabel id: storageLabel
anchors.bottom: parent.bottom anchors {
color: Theme.highlightColor 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 ? text: removable ?
//% "Memory card" //% "Memory card"
qsTrId("storage-removable") : qsTrId("storage-removable") :
//% "Internal storage" //% "Internal storage"
qsTrId("storage-internal") 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 { Label {
id: right id: bookCount
anchors { anchors {
bottom: parent.bottom bottom: parent.bottom
right: parent.right right: parent.right
rightMargin: Theme.paddingMedium rightMargin: Theme.paddingMedium
} }
//% "%0 book(s)" //% "%0 book(s)"
text: qsTrId("storage-book-count",count).arg(count) text: qsTrId("storage-book-count",_shownCount).arg(_shownCount)
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
color: Theme.highlightColor color: Theme.highlightColor
opacity: (showCount && count > 0) ? 1 : 0 opacity: (showCount && count > 0) ? 1 : 0

View file

@ -48,7 +48,8 @@ SilicaFlickable {
property var currentShelfView property var currentShelfView
property int currentShelfIndex: storageListWatcher.currentIndex property int currentShelfIndex: storageListWatcher.currentIndex
readonly property bool dragInProgress: draggedItem ? true : false 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 verticalScrollThreshold: _cellHeight/2
readonly property real horizontalScrollThreshold: _cellWidth/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 class BooksBook::CoverTask : public BooksTask
{ {
public: public:
CoverTask(BooksStorage aStorage, shared_ptr<Book> aBook) : CoverTask(QString aStateDir, shared_ptr<Book> aBook) :
iStorage(aStorage), iBook(aBook), iCoverMissing(false) {} iStateDir(aStateDir), iBook(aBook), iCoverMissing(false) {}
bool hasImage() const; bool hasImage() const;
QString cachedImagePath() const; QString cachedImagePath() const;
public: public:
BooksStorage iStorage; QString iStateDir;
shared_ptr<Book> iBook; shared_ptr<Book> iBook;
QImage iCoverImage; QImage iCoverImage;
bool iCoverMissing; bool iCoverMissing;
@ -149,8 +149,8 @@ inline bool BooksBook::CoverTask::hasImage() const
QString BooksBook::CoverTask::cachedImagePath() const QString BooksBook::CoverTask::cachedImagePath() const
{ {
if (iStorage.isValid()) { if (!iStateDir.isEmpty()) {
return iStorage.configDir().path() + "/" + return iStateDir + "/" +
QString::fromStdString(iBook->file().name(false)) + QString::fromStdString(iBook->file().name(false)) +
BOOK_COVER_SUFFIX + "jpg"; BOOK_COVER_SUFFIX + "jpg";
} }
@ -164,9 +164,9 @@ QString BooksBook::CoverTask::cachedImagePath() const
class BooksBook::LoadCoverTask : public BooksBook::CoverTask class BooksBook::LoadCoverTask : public BooksBook::CoverTask
{ {
public: public:
LoadCoverTask(BooksStorage aStorage, shared_ptr<Book> aBook, LoadCoverTask(QString aStateDir, shared_ptr<Book> aBook,
shared_ptr<FormatPlugin> aFormatPlugin) : shared_ptr<FormatPlugin> aFormatPlugin) :
BooksBook::CoverTask(aStorage, aBook), BooksBook::CoverTask(aStateDir, aBook),
iFormatPlugin(aFormatPlugin) {} iFormatPlugin(aFormatPlugin) {}
virtual void performTask(); virtual void performTask();
@ -179,10 +179,10 @@ void BooksBook::LoadCoverTask::performTask()
{ {
if (!isCanceled()) { if (!isCanceled()) {
// Try to load cached (or custom) cover // Try to load cached (or custom) cover
if (iStorage.isValid()) { if (!iStateDir.isEmpty()) {
QString coverPrefix(QString::fromStdString( QString coverPrefix(QString::fromStdString(
iBook->file().name(false)) + BOOK_COVER_SUFFIX); iBook->file().name(false)) + BOOK_COVER_SUFFIX);
QDirIterator it(iStorage.configDir()); QDirIterator it(iStateDir);
while (it.hasNext()) { while (it.hasNext()) {
QString path(it.next()); QString path(it.next());
if (it.fileName().startsWith(coverPrefix)) { if (it.fileName().startsWith(coverPrefix)) {
@ -231,8 +231,8 @@ void BooksBook::LoadCoverTask::performTask()
class BooksBook::GuessCoverTask : public BooksBook::CoverTask class BooksBook::GuessCoverTask : public BooksBook::CoverTask
{ {
public: public:
GuessCoverTask(BooksStorage aStorage, shared_ptr<Book> aBook) : GuessCoverTask(QString aStateDir, shared_ptr<Book> aBook) :
BooksBook::CoverTask(aStorage, aBook) {} BooksBook::CoverTask(aStateDir, aBook) {}
virtual void performTask(); virtual void performTask();
}; };
@ -262,8 +262,17 @@ void BooksBook::GuessCoverTask::performTask()
// Save the extracted image // Save the extracted image
QString coverPath(cachedImagePath()); QString coverPath(cachedImagePath());
if (!coverPath.isEmpty() && iCoverImage.save(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)); HDEBUG("saved cover to" << qPrintable(coverPath));
} else {
HWARN("failed to save" << qPrintable(coverPath));
}
} }
} else if (isCanceled()) { } else if (isCanceled()) {
HDEBUG("cancelled" << iBook->title().c_str()); HDEBUG("cancelled" << iBook->title().c_str());
@ -295,7 +304,8 @@ BooksBook::BooksBook(QObject* aParent) :
init(); init();
} }
BooksBook::BooksBook(const BooksStorage& aStorage, shared_ptr<Book> aBook) : BooksBook::BooksBook(const BooksStorage& aStorage, QString aRelativePath,
shared_ptr<Book> aBook) :
QObject(NULL), QObject(NULL),
iRef(1), iRef(1),
iStorage(aStorage), iStorage(aStorage),
@ -315,8 +325,8 @@ BooksBook::BooksBook(const BooksStorage& aStorage, shared_ptr<Book> aBook) :
iAuthors += QString::fromStdString(authors[i]->name()); iAuthors += QString::fromStdString(authors[i]->name());
} }
if (iStorage.isValid()) { if (iStorage.isValid()) {
iStateFilePath = iStorage.configDir().path() + "/" + iStateDir = iStorage.configDir().path() + "/" + aRelativePath;
iFileName + BOOKS_STATE_FILE_SUFFIX; iStateFilePath = iStateDir + "/" + iFileName + BOOKS_STATE_FILE_SUFFIX;
// Load the state // Load the state
QVariantMap state; QVariantMap state;
if (HarbourJson::load(iStateFilePath, state)) { if (HarbourJson::load(iStateFilePath, state)) {
@ -382,6 +392,11 @@ QString BooksBook::fileName() const
return iFileName; return iFileName;
} }
bool BooksBook::accessible() const
{
return !iCopyingOut;
}
void BooksBook::setLastPos(const BooksPos& aPos) void BooksBook::setLastPos(const BooksPos& aPos)
{ {
if (iLastPos != aPos) { if (iLastPos != aPos) {
@ -430,7 +445,7 @@ bool BooksBook::requestCoverImage()
if (!iBook.isNull() && !iFormatPlugin.isNull() && if (!iBook.isNull() && !iFormatPlugin.isNull() &&
!iCoverTasksDone && !iCoverTask) { !iCoverTasksDone && !iCoverTask) {
HDEBUG(iTitle); HDEBUG(iTitle);
iCoverTask = new LoadCoverTask(iStorage, iBook, iFormatPlugin); iCoverTask = new LoadCoverTask(iStateDir, iBook, iFormatPlugin);
connect(iCoverTask, SIGNAL(done()), SLOT(onLoadCoverTaskDone())); connect(iCoverTask, SIGNAL(done()), SLOT(onLoadCoverTaskDone()));
iTaskQueue->submit(iCoverTask); iTaskQueue->submit(iCoverTask);
Q_EMIT loadingCoverChanged(); Q_EMIT loadingCoverChanged();
@ -469,7 +484,7 @@ void BooksBook::onLoadCoverTaskDone()
iCoverTasksDone = true; iCoverTasksDone = true;
Q_EMIT loadingCoverChanged(); Q_EMIT loadingCoverChanged();
} else { } else {
iCoverTask = new GuessCoverTask(iStorage, iBook); iCoverTask = new GuessCoverTask(iStateDir, iBook);
connect(iCoverTask, SIGNAL(done()), SLOT(onGuessCoverTaskDone())); connect(iCoverTask, SIGNAL(done()), SLOT(onGuessCoverTaskDone()));
iTaskQueue->submit(iCoverTask); iTaskQueue->submit(iCoverTask);
} }
@ -514,7 +529,7 @@ void BooksBook::deleteFiles()
} else { } else {
HWARN("failed to delete" << qPrintable(iPath)); HWARN("failed to delete" << qPrintable(iPath));
} }
QDirIterator it(iStorage.configDir()); QDirIterator it(iStateDir);
while (it.hasNext()) { while (it.hasNext()) {
QString path(it.next()); QString path(it.next());
if (it.fileName().startsWith(iFileName)) { 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) Q_PROPERTY(bool copyingOut READ copyingOut NOTIFY copyingOutChanged)
public: public:
BooksBook(QObject* aParent = NULL); explicit BooksBook(QObject* aParent = NULL);
BooksBook(const BooksStorage& aStorage, shared_ptr<Book> aBook); BooksBook(const BooksStorage& aStorage, QString aRelativePath,
shared_ptr<Book> aBook);
~BooksBook(); ~BooksBook();
QString path() const { return iPath; } QString path() const { return iPath; }
@ -75,7 +76,7 @@ public:
BooksPos lastPos() const { return iLastPos; } BooksPos lastPos() const { return iLastPos; }
void setLastPos(const BooksPos& aPos); void setLastPos(const BooksPos& aPos);
shared_ptr<Book> bookRef() const { return iBook; } shared_ptr<Book> bookRef() const { return iBook; }
bool accessible() const { return !iCopyingOut; }
bool copyingOut() const { return iCopyingOut; } bool copyingOut() const { return iCopyingOut; }
bool loadingCover() const { return !iCoverTasksDone; } bool loadingCover() const { return !iCoverTasksDone; }
bool hasCoverImage() const; bool hasCoverImage() const;
@ -85,9 +86,8 @@ public:
QImage coverImage(); QImage coverImage();
void setCopyingOut(bool aValue); void setCopyingOut(bool aValue);
void deleteFiles();
// BooksListItem // BooksItem
virtual BooksItem* retain(); virtual BooksItem* retain();
virtual void release(); virtual void release();
virtual QObject* object(); virtual QObject* object();
@ -95,6 +95,8 @@ public:
virtual BooksBook* book(); virtual BooksBook* book();
virtual QString name() const; virtual QString name() const;
virtual QString fileName() const; virtual QString fileName() const;
virtual bool accessible() const;
virtual void deleteFiles();
Q_SIGNALS: Q_SIGNALS:
void coverImageChanged(); void coverImageChanged();
@ -128,6 +130,7 @@ private:
QString iAuthors; QString iAuthors;
QString iFileName; QString iFileName;
QString iPath; QString iPath;
QString iStateDir;
QString iStateFilePath; QString iStateFilePath;
}; };

View file

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

View file

@ -52,6 +52,8 @@ public:
virtual BooksBook* book() = 0; virtual BooksBook* book() = 0;
virtual QString name() const = 0; virtual QString name() const = 0;
virtual QString fileName() const = 0; virtual QString fileName() const = 0;
virtual bool accessible() const = 0;
virtual void deleteFiles() = 0;
}; };
#endif // BOOKS_ITEM_H #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) { } else if (!iCurrentBook || iCurrentBook->path() != path) {
shared_ptr<Book> book = BooksUtil::bookFromFile(path); shared_ptr<Book> book = BooksUtil::bookFromFile(path);
if (!book.isNull()) { if (!book.isNull()) {
QString rel;
QFileInfo info(path); QFileInfo info(path);
BooksStorageManager* mgr = BooksStorageManager::instance(); BooksStorageManager* mgr = BooksStorageManager::instance();
BooksStorage storage = mgr->storageForPath(info.path()); BooksStorage storage = mgr->storageForPath(info.path(), &rel);
if (storage.isValid()) { if (storage.isValid()) {
if (iCurrentBook) iCurrentBook->release(); if (iCurrentBook) iCurrentBook->release();
iCurrentBook = new BooksBook(storage, book); iCurrentBook = new BooksBook(storage, rel, book);
iCurrentBook->requestCoverImage(); iCurrentBook->requestCoverImage();
return true; return true;
} }

View file

@ -91,11 +91,10 @@ public:
class BooksShelf::LoadTask : public BooksTask class BooksShelf::LoadTask : public BooksTask
{ {
Q_OBJECT
public: public:
LoadTask(BooksStorage aStorage, QString aPath, QString aStateFilePath) : LoadTask(BooksStorage aStorage, QString aRelPath, QString aStateFile) :
iStorage(aStorage), iPath(aPath), iStateFilePath(aStateFilePath) {} iStorage(aStorage), iRelativePath(aRelPath),
iStateFilePath(aStateFile) {}
~LoadTask(); ~LoadTask();
void performTask(); void performTask();
@ -103,20 +102,17 @@ public:
int findBook(QString aFileName) const; int findBook(QString aFileName) const;
static int find(QFileInfoList aList, QString aFileName, int aStart); static int find(QFileInfoList aList, QString aFileName, int aStart);
Q_SIGNALS:
void bookFound(BooksBook* aBook);
public: public:
BooksStorage iStorage; BooksStorage iStorage;
QString iPath; QString iRelativePath;
QString iStateFilePath; QString iStateFilePath;
QList<BooksBook*> iBooks; QList<BooksItem*> iItems;
}; };
BooksShelf::LoadTask::~LoadTask() BooksShelf::LoadTask::~LoadTask()
{ {
const int n = iBooks.count(); const int n = iItems.count();
for (int i=0; i<n; i++) iBooks.at(i)->release(); for (int i=0; i<n; i++) iItems.at(i)->release();
} }
int BooksShelf::LoadTask::find(QFileInfoList aList, QString aName, int aStart) 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 int BooksShelf::LoadTask::findBook(QString aFileName) const
{ {
if (!aFileName.isEmpty()) { if (!aFileName.isEmpty()) {
const int n = iBooks.count(); const int n = iItems.count();
for (int i=0; i<n; i++) { 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; return i;
} }
} }
@ -148,9 +145,11 @@ int BooksShelf::LoadTask::findBook(QString aFileName) const
void BooksShelf::LoadTask::performTask() void BooksShelf::LoadTask::performTask()
{ {
if (!isCanceled()) { if (!isCanceled()) {
QDir dir(iPath); QString path(iStorage.fullPath(iRelativePath));
HDEBUG("checking" << iPath); HDEBUG("checking" << path);
QFileInfoList list = dir.entryInfoList(QDir::Files, QDir::Time); QDir dir(path);
QFileInfoList list = dir.entryInfoList(QDir::Files |
QDir::Dirs | QDir::NoDotAndDotDot, QDir::Time);
// Restore the order // Restore the order
QVariantMap state; QVariantMap state;
@ -175,18 +174,31 @@ void BooksShelf::LoadTask::performTask()
const int n = list.count(); const int n = list.count();
for (int i=0; i<n && !isCanceled(); i++) { for (int i=0; i<n && !isCanceled(); i++) {
std::string path(list.at(i).filePath().toStdString()); 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 {
shared_ptr<Book> book = BooksUtil::bookFromFile(path); shared_ptr<Book> book = BooksUtil::bookFromFile(path);
if (!book.isNull()) { if (!book.isNull()) {
BooksBook* newBook = new BooksBook(iStorage, book); BooksBook* newBook = new BooksBook(iStorage,
iRelativePath, book);
newBook->moveToThread(thread()); newBook->moveToThread(thread());
iBooks.append(newBook); iItems.append(newBook);
HDEBUG("[" << iBooks.size() << "]" << HDEBUG("[" << iItems.size() << "]" <<
qPrintable(newBook->fileName()) << qPrintable(newBook->fileName()) <<
newBook->title()); newBook->title());
Q_EMIT bookFound(newBook);
} else { } else {
HDEBUG("not a book:" << path.c_str()); HDEBUG("not a book:" << qPrintable(path));
}
} }
} }
} }
@ -231,7 +243,9 @@ public:
QObject* object() { return iItem ? iItem->object() : NULL; } QObject* object() { return iItem ? iItem->object() : NULL; }
BooksBook* book() { return iItem ? iItem->book() : NULL; } BooksBook* book() { return iItem ? iItem->book() : NULL; }
BooksShelf* shelf() { return iItem ? iItem->shelf() : 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 copyingOut();
bool copyingIn() { return iCopyTask != NULL; } bool copyingIn() { return iCopyTask != NULL; }
int copyPercent() { return iCopyTask ? iCopyTask->iCopyPercent : 0; } 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() inline bool BooksShelf::Data::copyingOut()
{ {
BooksBook* bookItem = book(); BooksBook* bookItem = book();
@ -429,33 +433,83 @@ class BooksShelf::DeleteTask : public BooksTask
{ {
Q_OBJECT Q_OBJECT
public: public:
DeleteTask(BooksBook* aBook); DeleteTask(BooksItem* aItem);
~DeleteTask(); ~DeleteTask();
void performTask(); void performTask();
public: public:
BooksBook* iBook; BooksItem* iItem;
}; };
BooksShelf::DeleteTask::DeleteTask(BooksBook* aBook) : BooksShelf::DeleteTask::DeleteTask(BooksItem* aItem) :
iBook(aBook) iItem(aItem)
{ {
iBook->retain(); iItem->retain();
iBook->cancelCoverRequest();
} }
BooksShelf::DeleteTask::~DeleteTask() BooksShelf::DeleteTask::~DeleteTask()
{ {
iBook->release(); iItem->release();
} }
void BooksShelf::DeleteTask::performTask() void BooksShelf::DeleteTask::performTask()
{ {
if (isCanceled()) { if (isCanceled()) {
HDEBUG("cancelled" << iBook->title()); HDEBUG("cancelled" << iItem->fileName());
} else { } else {
HDEBUG(iBook->title()); iItem->deleteFiles();
iBook->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)), iSaveTimer(new BooksSaveTimer(this)),
iTaskQueue(BooksTaskQueue::instance()) iTaskQueue(BooksTaskQueue::instance())
{ {
#if QT_VERSION < 0x050000 init();
setRoleNames(roleNames());
#endif
QQmlEngine::setObjectOwnership(&iStorage, QQmlEngine::CppOwnership);
connect(iSaveTimer, SIGNAL(save()), SLOT(saveState())); 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() BooksShelf::~BooksShelf()
{ {
const int n = iDeleteTasks.count(); const int n = iDeleteTasks.count();
for (int i=0; i<n; i++) iDeleteTasks.at(i)->release(this); for (int i=0; i<n; i++) iDeleteTasks.at(i)->release(this);
if (iLoadTask) iLoadTask->release(this); if (iLoadTask) iLoadTask->release(this);
if (iSaveTimer->saveRequested()) saveState(); if (iSaveTimer && iSaveTimer->saveRequested()) saveState();
removeAllBooks(); qDeleteAll(iList);
HDEBUG("destroyed"); HDEBUG("destroyed");
} }
void BooksShelf::removeAllBooks() void BooksShelf::init()
{ {
while (!iList.isEmpty()) { #if QT_VERSION < 0x050000
Data* data = iList.takeLast(); setRoleNames(roleNames());
BooksBook* book = data->book(); #endif
if (book) { QQmlEngine::setObjectOwnership(&iStorage, QQmlEngine::CppOwnership);
Q_EMIT bookRemoved(book);
}
delete data;
}
} }
void BooksShelf::setRelativePath(QString aPath) void BooksShelf::setRelativePath(QString aPath)
@ -521,25 +590,31 @@ void BooksShelf::setDevice(QString aDevice)
void BooksShelf::updatePath() void BooksShelf::updatePath()
{ {
BooksLoadingSignalBlocker block(this);
const QString oldPath = iPath; const QString oldPath = iPath;
iPath.clear(); iPath.clear();
if (iStorage.isValid()) { if (iStorage.isValid()) {
QString newPath(iStorage.root()); iPath = iStorage.fullPath(iRelativePath);
if (!iRelativePath.isEmpty()) {
if (!newPath.endsWith('/')) newPath += '/';
newPath += iRelativePath;
}
iPath = QDir::cleanPath(newPath);
} }
if (oldPath != iPath) { if (oldPath != iPath) {
const int oldCount = iList.count();
const int oldDummyItemIndex = iDummyItemIndex; const int oldDummyItemIndex = iDummyItemIndex;
beginResetModel(); Counts counts(this);
HDEBUG(iPath); 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; iDummyItemIndex = -1;
if (!iPath.isEmpty()) loadBookList(); if (!iPath.isEmpty()) loadBookList();
endResetModel();
Q_EMIT pathChanged(); Q_EMIT pathChanged();
if (oldDummyItemIndex != iDummyItemIndex) { if (oldDummyItemIndex != iDummyItemIndex) {
Q_EMIT dummyItemIndexChanged(); Q_EMIT dummyItemIndexChanged();
@ -547,9 +622,7 @@ void BooksShelf::updatePath()
Q_EMIT hasDummyItemChanged(); Q_EMIT hasDummyItemChanged();
} }
} }
if (oldCount != iList.count()) { counts.emitSignals(this);
Q_EMIT countChanged();
}
} }
} }
@ -557,45 +630,42 @@ void BooksShelf::onLoadTaskDone()
{ {
HASSERT(iLoadTask); HASSERT(iLoadTask);
HASSERT(iLoadTask == sender()); HASSERT(iLoadTask == sender());
if (iLoadTask && iLoadTask == sender()) {
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->release(this);
iLoadTask = NULL; 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();
} }
} }
void BooksShelf::loadBookList() void BooksShelf::loadBookList()
{ {
if (!iList.isEmpty()) { BooksLoadingSignalBlocker block(this);
beginResetModel();
removeAllBooks();
endResetModel();
}
const bool wasLoading = loading();
if (iLoadTask) iLoadTask->release(this); if (iLoadTask) iLoadTask->release(this);
if (iPath.isEmpty()) { if (iPath.isEmpty()) {
iLoadTask = NULL; iLoadTask = NULL;
} else { } else {
HDEBUG(iPath); HDEBUG(iPath);
iLoadTask = new LoadTask(iStorage, iPath, stateFileName()); iLoadTask = new LoadTask(iStorage, iRelativePath, stateFileName());
connect(iLoadTask, SIGNAL(bookFound(BooksBook*)),
SLOT(onBookFound(BooksBook*)), Qt::QueuedConnection);
connect(iLoadTask, SIGNAL(done()), SLOT(onLoadTaskDone())); connect(iLoadTask, SIGNAL(done()), SLOT(onLoadTaskDone()));
iTaskQueue->submit(iLoadTask); iTaskQueue->submit(iLoadTask);
} }
if (wasLoading != loading()) {
Q_EMIT loadingChanged();
}
} }
void BooksShelf::saveState() void BooksShelf::saveState()
@ -614,7 +684,7 @@ void BooksShelf::saveState()
void BooksShelf::queueStateSave() void BooksShelf::queueStateSave()
{ {
if (iEditMode) { if (iEditMode && iSaveTimer) {
iSaveTimer->requestSave(); iSaveTimer->requestSave();
} }
} }
@ -622,7 +692,7 @@ void BooksShelf::queueStateSave()
QString BooksShelf::stateFileName() const QString BooksShelf::stateFileName() const
{ {
return iStorage.isValid() ? return iStorage.isValid() ?
iStorage.configDir().path() + ("/" SHELF_STATE_FILE) : iStorage.configDir().path() + "/" + iRelativePath + ("/" SHELF_STATE_FILE) :
QString(); QString();
} }
@ -672,7 +742,7 @@ void BooksShelf::setEditMode(bool aEditMode)
if (iEditMode != aEditMode) { if (iEditMode != aEditMode) {
iEditMode = aEditMode; iEditMode = aEditMode;
HDEBUG(iEditMode); HDEBUG(iEditMode);
if (iSaveTimer->saveRequested()) { if (iSaveTimer && iSaveTimer->saveRequested()) {
iSaveTimer->cancelSave(); iSaveTimer->cancelSave();
saveState(); saveState();
} }
@ -723,7 +793,7 @@ BooksBook* BooksShelf::book()
QString BooksShelf::name() const QString BooksShelf::name() const
{ {
return iName; return iFileName;
} }
QString BooksShelf::fileName() const QString BooksShelf::fileName() const
@ -731,11 +801,34 @@ QString BooksShelf::fileName() const
return iFileName; return iFileName;
} }
bool BooksShelf::accessible() const
{
return true;
}
int BooksShelf::count() const int BooksShelf::count() const
{ {
return iList.count(); 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 QObject* BooksShelf::get(int aIndex) const
{ {
if (validIndex(aIndex)) { 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); 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) { if (book) {
book->cancelCoverRequest();
Q_EMIT bookRemoved(book); Q_EMIT bookRemoved(book);
} }
} }
}
BooksBook* BooksShelf::removeBook(int aIndex) void BooksShelf::remove(int aIndex)
{ {
if (validIndex(aIndex)) { if (validIndex(aIndex)) {
Counts counts(this);
HDEBUG(iList.at(aIndex)->name()); HDEBUG(iList.at(aIndex)->name());
beginRemoveRows(QModelIndex(), aIndex, aIndex); beginRemoveRows(QModelIndex(), aIndex, aIndex);
BooksBook* book = iList.at(aIndex)->book(); submitDeleteTask(aIndex);
if (book) {
DeleteTask* task = new DeleteTask(book);
iDeleteTasks.append(task);
iTaskQueue->submit(task);
}
if (iDummyItemIndex == aIndex) { if (iDummyItemIndex == aIndex) {
iDummyItemIndex = -1; iDummyItemIndex = -1;
Q_EMIT hasDummyItemChanged(); Q_EMIT hasDummyItemChanged();
@ -828,26 +924,19 @@ BooksBook* BooksShelf::removeBook(int aIndex)
} }
delete iList.takeAt(aIndex); delete iList.takeAt(aIndex);
queueStateSave(); queueStateSave();
Q_EMIT countChanged(); counts.emitSignals(this);
endRemoveRows(); endRemoveRows();
return book;
} }
return NULL;
} }
void BooksShelf::removeAll() void BooksShelf::removeAll()
{ {
if (!iList.isEmpty()) { if (!iList.isEmpty()) {
Counts counts(this);
beginRemoveRows(QModelIndex(), 0, iList.count()-1); beginRemoveRows(QModelIndex(), 0, iList.count()-1);
const int n = iList.count(); const int n = iList.count();
for (int i=0; i<n; i++) { for (int i=0; i<n; i++) {
BooksBook* book = iList.at(i)->book(); submitDeleteTask(i);
if (book) {
DeleteTask* task = new DeleteTask(book);
iDeleteTasks.append(task);
iTaskQueue->submit(task);
Q_EMIT bookRemoved(book);
}
} }
if (iDummyItemIndex >= 0) { if (iDummyItemIndex >= 0) {
iDummyItemIndex = -1; iDummyItemIndex = -1;
@ -857,7 +946,7 @@ void BooksShelf::removeAll()
qDeleteAll(iList); qDeleteAll(iList);
iList.clear(); iList.clear();
queueStateSave(); queueStateSave();
Q_EMIT countChanged(); counts.emitSignals(this);
endRemoveRows(); endRemoveRows();
} }
} }
@ -908,11 +997,12 @@ void BooksShelf::importBook(QObject* aBook)
} else { } else {
HDEBUG(qPrintable(book->path()) << "->" << qPrintable(iPath)); HDEBUG(qPrintable(book->path()) << "->" << qPrintable(iPath));
beginInsertRows(QModelIndex(), 0, 0); beginInsertRows(QModelIndex(), 0, 0);
Counts counts(this);
Data* data = new Data(this, book->retain(), true); Data* data = new Data(this, book->retain(), true);
iList.insert(0, data); iList.insert(0, data);
iTaskQueue->submit(new CopyTask(data, book)); iTaskQueue->submit(new CopyTask(data, book));
counts.emitSignals(this);
endInsertRows(); endInsertRows();
Q_EMIT countChanged();
saveState(); saveState();
} }
} }
@ -990,7 +1080,7 @@ void BooksShelf::onCopyTaskDone()
if (task->iSuccess) { if (task->iSuccess) {
shared_ptr<Book> book = BooksUtil::bookFromFile(task->iDest); shared_ptr<Book> book = BooksUtil::bookFromFile(task->iDest);
if (!book.isNull()) { if (!book.isNull()) {
copy = new BooksBook(iStorage, book); copy = new BooksBook(iStorage, iRelativePath, book);
copy->setLastPos(src->lastPos()); copy->setLastPos(src->lastPos());
copy->setCoverImage(src->coverImage()); copy->setCoverImage(src->coverImage());
copy->requestCoverImage(); copy->requestCoverImage();
@ -1024,7 +1114,7 @@ void BooksShelf::onCopyTaskDone()
QModelIndex index(createIndex(row, 0)); QModelIndex index(createIndex(row, 0));
Q_EMIT dataChanged(index, index); Q_EMIT dataChanged(index, index);
} else { } 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> BooksShelf::roleNames() const
{ {
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;

View file

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

View file

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

View file

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

View file

@ -39,6 +39,7 @@
#include "BooksConfig.h" #include "BooksConfig.h"
#include "BooksSettings.h" #include "BooksSettings.h"
#include "BooksImportModel.h" #include "BooksImportModel.h"
#include "BooksPathModel.h"
#include "BooksStorageModel.h" #include "BooksStorageModel.h"
#include "BooksPageWidget.h" #include "BooksPageWidget.h"
#include "BooksListWatcher.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(BooksBookModel, "BookModel");
BOOKS_QML_REGISTER(BooksCoverModel, "CoverModel"); BOOKS_QML_REGISTER(BooksCoverModel, "CoverModel");
BOOKS_QML_REGISTER(BooksImportModel, "BooksImportModel"); BOOKS_QML_REGISTER(BooksImportModel, "BooksImportModel");
BOOKS_QML_REGISTER(BooksPathModel, "BooksPathModel");
BOOKS_QML_REGISTER(BooksStorageModel, "BookStorage"); BOOKS_QML_REGISTER(BooksStorageModel, "BookStorage");
BOOKS_QML_REGISTER(BooksPageWidget, "PageWidget"); BOOKS_QML_REGISTER(BooksPageWidget, "PageWidget");
BOOKS_QML_REGISTER(BooksListWatcher, "ListWatcher"); BOOKS_QML_REGISTER(BooksListWatcher, "ListWatcher");