[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:
parent
ef19a5c2e7
commit
89f79e2492
22 changed files with 1029 additions and 243 deletions
|
@ -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 \
|
||||
|
|
|
@ -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) {
|
||||
|
|
62
app/qml/BooksShelfFooter.qml
Normal file
62
app/qml/BooksShelfFooter.qml
Normal 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 }
|
||||
}
|
||||
}
|
|
@ -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
132
app/qml/BooksShelfTitle.qml
Normal 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"
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
91
app/qml/images/bookshelf.svg
Normal file
91
app/qml/images/bookshelf.svg
Normal 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
35
app/qml/images/folder.svg
Normal 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 |
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
125
app/src/BooksPathModel.cpp
Normal 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
86
app/src/BooksPathModel.h
Normal 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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue