Integrated file selection dialogue with the main code. Improved the
authorDavid <david@flypig.co.uk>
Thu, 13 Mar 2014 02:18:11 +0000 (02:18 +0000)
committerDavid <david@flypig.co.uk>
Thu, 13 Mar 2014 02:18:11 +0000 (02:18 +0000)
configuration dialogue. Added logging window.

63 files changed:
OpenVPNUI.pro
qml/components/ValueButtonAlignRight.qml [new file with mode: 0644]
qml/filebrowse/components/CenteredField.qml [new file with mode: 0644]
qml/filebrowse/components/DirPopup.qml [new file with mode: 0644]
qml/filebrowse/components/DoubleMenuItem.qml [new file with mode: 0644]
qml/filebrowse/components/InteractionBlocker.qml [new file with mode: 0644]
qml/filebrowse/components/LetterSwitch.qml [new file with mode: 0644]
qml/filebrowse/components/NotificationPanel.qml [new file with mode: 0644]
qml/filebrowse/components/ProgressPanel.qml [new file with mode: 0644]
qml/filebrowse/components/Spacer.qml [new file with mode: 0644]
qml/filebrowse/images/large-file-apk.png [new file with mode: 0644]
qml/filebrowse/images/large-file-audio.png [new file with mode: 0644]
qml/filebrowse/images/large-file-image.png [new file with mode: 0644]
qml/filebrowse/images/large-file-rpm.png [new file with mode: 0644]
qml/filebrowse/images/large-file-txt.png [new file with mode: 0644]
qml/filebrowse/images/large-file-video.png [new file with mode: 0644]
qml/filebrowse/images/large-file.png [new file with mode: 0644]
qml/filebrowse/images/large-folder-link.png [new file with mode: 0644]
qml/filebrowse/images/large-folder.png [new file with mode: 0644]
qml/filebrowse/images/large-link.png [new file with mode: 0644]
qml/filebrowse/images/small-file-apk.png [new file with mode: 0644]
qml/filebrowse/images/small-file-audio.png [new file with mode: 0644]
qml/filebrowse/images/small-file-image.png [new file with mode: 0644]
qml/filebrowse/images/small-file-rpm.png [new file with mode: 0644]
qml/filebrowse/images/small-file-text.png [new file with mode: 0644]
qml/filebrowse/images/small-file-txt.png [new file with mode: 0644]
qml/filebrowse/images/small-file-video.png [new file with mode: 0644]
qml/filebrowse/images/small-file.png [new file with mode: 0644]
qml/filebrowse/images/small-folder-link.png [new file with mode: 0644]
qml/filebrowse/images/small-folder.png [new file with mode: 0644]
qml/filebrowse/images/small-link.png [new file with mode: 0644]
qml/filebrowse/pages/AboutPage.qml [new file with mode: 0644]
qml/filebrowse/pages/ConsolePage.qml [new file with mode: 0644]
qml/filebrowse/pages/CreateFolderDialog.qml [new file with mode: 0644]
qml/filebrowse/pages/DirectoryPage.qml [new file with mode: 0644]
qml/filebrowse/pages/FilePage.qml [new file with mode: 0644]
qml/filebrowse/pages/PermissionsDialog.qml [new file with mode: 0644]
qml/filebrowse/pages/RenameDialog.qml [new file with mode: 0644]
qml/filebrowse/pages/SearchPage.qml [new file with mode: 0644]
qml/filebrowse/pages/SettingsPage.qml [new file with mode: 0644]
qml/filebrowse/pages/ViewPage.qml [new file with mode: 0644]
qml/filebrowse/pages/functions.js [new file with mode: 0644]
qml/pages/ConfigurePage.qml
qml/pages/ConnectPage.qml
rpm/OpenVPNUI.spec
rpm/OpenVPNUI.yaml
src/OpenVPNUI.cpp
src/filebrowse/engine.cpp [new file with mode: 0644]
src/filebrowse/engine.h [new file with mode: 0644]
src/filebrowse/fileinfo.cpp [new file with mode: 0644]
src/filebrowse/fileinfo.h [new file with mode: 0644]
src/filebrowse/filemodel.cpp [new file with mode: 0644]
src/filebrowse/filemodel.h [new file with mode: 0644]
src/filebrowse/fileworker.cpp [new file with mode: 0644]
src/filebrowse/fileworker.h [new file with mode: 0644]
src/filebrowse/globals.cpp [new file with mode: 0644]
src/filebrowse/globals.h [new file with mode: 0644]
src/filebrowse/searchengine.cpp [new file with mode: 0644]
src/filebrowse/searchengine.h [new file with mode: 0644]
src/filebrowse/searchworker.cpp [new file with mode: 0644]
src/filebrowse/searchworker.h [new file with mode: 0644]
src/vpncontrol.cpp
src/vpncontrol.h

index 90c7f7f..6457ec7 100644 (file)
@@ -11,7 +11,14 @@ TARGET = OpenVPNUI
 CONFIG += sailfishapp
 
 SOURCES += src/OpenVPNUI.cpp \
 CONFIG += sailfishapp
 
 SOURCES += src/OpenVPNUI.cpp \
-    src/vpncontrol.cpp
+    src/vpncontrol.cpp \
+    src/filebrowse/searchworker.cpp \
+    src/filebrowse/searchengine.cpp \
+    src/filebrowse/globals.cpp \
+    src/filebrowse/fileworker.cpp \
+    src/filebrowse/filemodel.cpp \
+    src/filebrowse/fileinfo.cpp \
+    src/filebrowse/engine.cpp
 
 OTHER_FILES += qml/OpenVPNUI.qml \
     qml/cover/CoverPage.qml \
 
 OTHER_FILES += qml/OpenVPNUI.qml \
     qml/cover/CoverPage.qml \
@@ -21,8 +28,57 @@ OTHER_FILES += qml/OpenVPNUI.qml \
     qml/pages/ConnectPage.qml \
     qml/pages/ConfigurePage.qml \
     OpenVPN-help.txt \
     qml/pages/ConnectPage.qml \
     qml/pages/ConfigurePage.qml \
     OpenVPN-help.txt \
-    client.ovpn
+    client.ovpn \
+    qml/filebrowse/images/small-link.png \
+    qml/filebrowse/images/small-folder-link.png \
+    qml/filebrowse/images/small-folder.png \
+    qml/filebrowse/images/small-file-video.png \
+    qml/filebrowse/images/small-file-txt.png \
+    qml/filebrowse/images/small-file-text.png \
+    qml/filebrowse/images/small-file-rpm.png \
+    qml/filebrowse/images/small-file-image.png \
+    qml/filebrowse/images/small-file-audio.png \
+    qml/filebrowse/images/small-file-apk.png \
+    qml/filebrowse/images/small-file.png \
+    qml/filebrowse/images/large-link.png \
+    qml/filebrowse/images/large-folder-link.png \
+    qml/filebrowse/images/large-folder.png \
+    qml/filebrowse/images/large-file-video.png \
+    qml/filebrowse/images/large-file-txt.png \
+    qml/filebrowse/images/large-file-rpm.png \
+    qml/filebrowse/images/large-file-image.png \
+    qml/filebrowse/images/large-file-audio.png \
+    qml/filebrowse/images/large-file-apk.png \
+    qml/filebrowse/images/large-file.png \
+    qml/filebrowse/pages/functions.js \
+    qml/filebrowse/pages/ViewPage.qml \
+    qml/filebrowse/pages/SettingsPage.qml \
+    qml/filebrowse/pages/SearchPage.qml \
+    qml/filebrowse/pages/RenameDialog.qml \
+    qml/filebrowse/pages/PermissionsDialog.qml \
+    qml/filebrowse/pages/FilePage.qml \
+    qml/filebrowse/pages/DirectoryPage.qml \
+    qml/filebrowse/pages/CreateFolderDialog.qml \
+    qml/filebrowse/pages/ConsolePage.qml \
+    qml/filebrowse/pages/AboutPage.qml \
+    qml/filebrowse/components/Spacer.qml \
+    qml/filebrowse/components/ProgressPanel.qml \
+    qml/filebrowse/components/NotificationPanel.qml \
+    qml/filebrowse/components/LetterSwitch.qml \
+    qml/filebrowse/components/InteractionBlocker.qml \
+    qml/filebrowse/components/DoubleMenuItem.qml \
+    qml/filebrowse/components/DirPopup.qml \
+    qml/filebrowse/components/CenteredField.qml \
+    qml/components/ValueButtonAlignRight.qml
 
 HEADERS += \
 
 HEADERS += \
-    src/vpncontrol.h
+    src/vpncontrol.h \
+    src/filebrowse/searchworker.h \
+    src/filebrowse/searchengine.h \
+    src/filebrowse/globals.h \
+    src/filebrowse/fileworker.h \
+    src/filebrowse/filemodel.h \
+    src/filebrowse/fileinfo.h \
+    src/filebrowse/engine.h
 
 
+INCLUDEPATH += src/filebrowse
diff --git a/qml/components/ValueButtonAlignRight.qml b/qml/components/ValueButtonAlignRight.qml
new file mode 100644 (file)
index 0000000..899e16f
--- /dev/null
@@ -0,0 +1,80 @@
+/****************************************************************************************
+**
+** Copyright (C) 2013 Jolla Ltd.
+** Contact: Bea Lam <bea.lam@jollamobile.com>
+** All rights reserved.
+**
+** This file is part of Sailfish Silica UI component package.
+**
+** 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
+import Sailfish.Silica.theme 1.0
+
+BackgroundItem {
+    id: root
+
+    property alias label: titleText.text
+    property alias value: valueText.text
+
+    property alias labelColor: titleText.color
+    property alias valueColor: valueText.color
+
+    property real labelMargin: Theme.paddingLarge
+
+    property int _duration: 200
+
+    width: parent ? parent.width : 0
+    height: contentItem.height
+    contentHeight: visible ? (titleText.height == flow.implicitHeight ? Theme.itemSizeSmall : Theme.itemSizeExtraLarge) : 0
+    opacity: enabled ? 1.0 : 0.4
+
+    Flow {
+        id: flow
+
+        anchors {
+            left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter
+            leftMargin: root.labelMargin; rightMargin: Theme.paddingLarge
+        }
+        move: Transition { NumberAnimation { properties: "x,y"; easing.type: Easing.InOutQuad; duration: root._duration } }
+
+        Label {
+            id: titleText
+            color: root.down ? Theme.highlightColor : Theme.primaryColor
+            width: Math.min(implicitWidth + Theme.paddingSmall, parent.width)
+            truncationMode: TruncationMode.Fade
+        }
+
+        Label {
+            id: valueText
+            color: Theme.highlightColor
+            width: Math.min(implicitWidth, parent.width)
+            truncationMode: TruncationMode.Fade
+            horizontalAlignment: ((implicitWidth <= (parent.width - titleText.width)) ? Text.AlignLeft : Text.AlignRight)
+        }
+    }
+}
diff --git a/qml/filebrowse/components/CenteredField.qml b/qml/filebrowse/components/CenteredField.qml
new file mode 100644 (file)
index 0000000..8d26e32
--- /dev/null
@@ -0,0 +1,33 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+// This component displays a label and a value as a row
+Row {
+    spacing: 10
+    width: parent.width
+
+    // label text
+    property string label: ""
+
+    // value text
+    property string value: ""
+
+    // font size
+    property int pixelSize: Theme.fontSizeExtraSmall
+
+    Label {
+        text: label
+        color: Theme.secondaryColor
+        width: parent.width/2
+        horizontalAlignment: Text.AlignRight
+        wrapMode: Text.Wrap
+        font.pixelSize: pixelSize
+    }
+    Label {
+        text: value
+        color: Theme.highlightColor
+        width: parent.width/2
+        wrapMode: Text.Wrap
+        font.pixelSize: pixelSize
+    }
+}
diff --git a/qml/filebrowse/components/DirPopup.qml b/qml/filebrowse/components/DirPopup.qml
new file mode 100644 (file)
index 0000000..b7a4eee
--- /dev/null
@@ -0,0 +1,117 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import "../pages/functions.js" as Functions
+
+// This component displays a list of dir options on top of a page
+Item {
+    id: item
+    property int menuTop: 100
+
+    property int _selectedMenu: 0
+    property Item _contextMenu
+
+    function show()
+    {
+        if (!_contextMenu)
+            _contextMenu = contextMenuComponent.createObject(rect);
+        _selectedMenu = 0;
+
+        // update spaces
+        var rootSpace = engine.diskSpace("/");
+        if (rootSpace.length > 0) {
+            _contextMenu.rootSpaceText = qsTr("Root (%1)").arg(rootSpace[0]);
+            _contextMenu.rootSpaceSubtext = rootSpace[1];
+        } else {
+            _contextMenu.rootSpaceText = qsTr("Root");
+            _contextMenu.rootSpaceSubtext = "";
+        }
+
+        var sdCardSpace = engine.diskSpace(Functions.sdcardPath());
+        if (sdCardSpace.length > 0) {
+            _contextMenu.sdCardSpaceText = qsTr("SD Card (%1)").arg(sdCardSpace[0]);
+            _contextMenu.sdCardSpaceSubtext = sdCardSpace[1];
+        } else {
+            _contextMenu.sdCardSpaceText = qsTr("SD Card");
+            _contextMenu.sdCardSpaceSubtext = "";
+        }
+
+        _contextMenu.show(rect);
+    }
+
+    Column {
+        anchors.fill: parent
+
+        Item {
+            id: spacer
+            width: parent.width
+            height: menuTop
+        }
+        // bg rectangle for context menu so it covers underlying items
+        Rectangle {
+            id: rect
+            color: "black"
+            width: parent.width
+            height: _contextMenu ? _contextMenu.height : 0
+        }
+    }
+
+    Component {
+        id: contextMenuComponent
+        ContextMenu {
+
+            property string sdCardSpaceText: ""
+            property string sdCardSpaceSubtext: ""
+            property string rootSpaceText: ""
+            property string rootSpaceSubtext: ""
+
+            // delayed action so that menu has already closed when page transition happens
+            onClosed: {
+                if (_selectedMenu == 1) {
+                    Functions.goToHome();
+
+                } else if (_selectedMenu == 2) {
+                    var sdcard = Functions.sdcardPath();
+                    if (engine.exists(sdcard)) {
+                        Functions.goToFolder(sdcard);
+                    } else {
+                        // this assumes that the page has a notificationPanel
+                        notificationPanel.showText(qsTr("SD Card not found"), sdcard);
+                    }
+
+                } else if (_selectedMenu == 3) {
+                    var androidSdcard = Functions.androidSdcardPath();
+                    if (engine.exists(androidSdcard)) {
+                        Functions.goToFolder(androidSdcard);
+                    } else {
+                        // this assumes that the page has a notificationPanel
+                        notificationPanel.showText(qsTr("Android Storage not found"), androidSdcard);
+                    }
+
+                } else if (_selectedMenu == 4) {
+                    Functions.goToRoot();
+                }
+                _selectedMenu = 0;
+            }
+
+            MenuItem {
+                text: qsTr("Home")
+                onClicked: _selectedMenu = 1
+            }
+            DoubleMenuItem {
+                text: sdCardSpaceText
+                subtext: sdCardSpaceSubtext
+                onClicked: _selectedMenu = 2
+            }
+            MenuItem {
+                text: qsTr("Android Storage")
+                onClicked: _selectedMenu = 3
+            }
+            DoubleMenuItem {
+                text: rootSpaceText
+                subtext: rootSpaceSubtext
+                onClicked: _selectedMenu = 4
+            }
+        }
+    }
+
+}
diff --git a/qml/filebrowse/components/DoubleMenuItem.qml b/qml/filebrowse/components/DoubleMenuItem.qml
new file mode 100644 (file)
index 0000000..9507baf
--- /dev/null
@@ -0,0 +1,16 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+// This component creates a menu item with two lines
+MenuItem {
+    property string subtext: ""
+
+    Label {
+        visible: subtext !== ""
+        anchors.horizontalCenter: parent.horizontalCenter
+        anchors.bottom: parent.bottom
+        text: subtext
+        color: Theme.secondaryColor
+        font.pixelSize: Theme.fontSizeExtraSmall
+    }
+}
diff --git a/qml/filebrowse/components/InteractionBlocker.qml b/qml/filebrowse/components/InteractionBlocker.qml
new file mode 100644 (file)
index 0000000..89ea2e5
--- /dev/null
@@ -0,0 +1,38 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+// This component blocks all components under it and displays a dark background
+Rectangle {
+    id: interactionBlocker
+
+    // clicked signal is emitted when the component is clicked
+    signal clicked
+
+    visible: false
+    color: "#000000"
+    opacity: 0.4
+
+    MouseArea {
+        anchors.fill: parent
+        enabled: true
+        onClicked: interactionBlocker.clicked()
+    }
+    // use a timer to delay the visibility of interaction blocker by adjusting opacity
+    // this is done to prevent flashing if the file operation is fast
+    onVisibleChanged: {
+        if (visible === true) {
+            interactionBlocker.opacity = 0;
+            blockerTimer.start();
+        } else {
+            blockerTimer.stop();
+        }
+    }
+    Timer {
+        id: blockerTimer
+        interval: 300
+        onTriggered: {
+            interactionBlocker.opacity = 0.3;
+            stop();
+        }
+    }
+}
diff --git a/qml/filebrowse/components/LetterSwitch.qml b/qml/filebrowse/components/LetterSwitch.qml
new file mode 100644 (file)
index 0000000..5012aff
--- /dev/null
@@ -0,0 +1,23 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+// This component is a toggle switch, which displays a letter or a dash '-'
+MouseArea {
+    // checked status of the switch
+    property bool checked: false
+
+    // letter to be displayed
+    property string letter: ""
+
+    height: parent.height
+
+    Label {
+        id: label
+        anchors.horizontalCenter: parent.horizontalCenter
+        anchors.verticalCenter: parent.verticalCenter
+        text: checked ? letter : "-"
+        color: Theme.primaryColor
+    }
+
+    onClicked: checked = !checked
+}
diff --git a/qml/filebrowse/components/NotificationPanel.qml b/qml/filebrowse/components/NotificationPanel.qml
new file mode 100644 (file)
index 0000000..0f9874c
--- /dev/null
@@ -0,0 +1,109 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+// This component displays a notification panel at top of page
+Item {
+    anchors.fill: parent
+
+    // reference to page to prevent back navigation (required)
+    property Item page
+
+    // open status of the panel
+    property alias open: dockedPanel.open
+
+    // shows the panel
+    function showText(header, txt) {
+        headerLabel.text = header;
+        textLabel.text = txt;
+        dockedPanel.show();
+    }
+
+    // shows the panel, maximum 5 secs
+    function showTextWithTimer(header, txt) {
+        headerLabel.text = header;
+        textLabel.text = txt;
+        dockedPanel.show();
+        timer.start();
+    }
+
+    // hides the panel
+    function hide() {
+        timer.stop()
+        dockedPanel.hide();
+    }
+
+
+    //// internal
+
+    InteractionBlocker {
+        anchors.fill: parent
+        visible: dockedPanel.open
+        onClicked: {
+            dockedPanel.hide();
+            timer.stop();
+        }
+    }
+
+    DockedPanel {
+        id: dockedPanel
+
+        width: parent.width
+        height: Theme.itemSizeExtraLarge + Theme.paddingLarge
+
+        dock: Dock.Top
+        open: false
+        onOpenChanged: page.backNavigation = !open; // disable back navigation
+
+        Rectangle {
+            anchors.fill: parent
+            color: "black"
+            opacity: 0.7
+        }
+        MouseArea {
+            anchors.fill: parent
+            enabled: true
+            onClicked: {
+                dockedPanel.hide();
+                timer.stop();
+            }
+        }
+        Label {
+            id: headerLabel
+            visible: dockedPanel.open
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.top: parent.top
+            anchors.leftMargin: Theme.paddingLarge
+            anchors.rightMargin: Theme.paddingLarge
+            anchors.topMargin: 40
+            horizontalAlignment: Text.AlignHCenter
+            text: ""
+            wrapMode: Text.Wrap
+            color: Theme.primaryColor
+        }
+        Label {
+            id: textLabel
+            visible: dockedPanel.open
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.top: headerLabel.bottom
+            anchors.leftMargin: Theme.paddingLarge
+            anchors.rightMargin: Theme.paddingLarge
+            horizontalAlignment: Text.AlignHCenter
+            text: ""
+            wrapMode: Text.Wrap
+            font.pixelSize: Theme.fontSizeTiny
+            color: Theme.primaryColor
+        }
+    }
+
+    // timer to auto-hide panel
+    Timer {
+        id: timer
+        interval: 5000
+        onTriggered: {
+            dockedPanel.hide();
+            stop();
+        }
+    }
+}
diff --git a/qml/filebrowse/components/ProgressPanel.qml b/qml/filebrowse/components/ProgressPanel.qml
new file mode 100644 (file)
index 0000000..13b31aa
--- /dev/null
@@ -0,0 +1,112 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+// This component displays a progress panel at top of page and blocks all interactions under it
+Item {
+    id: progressPanel
+    anchors.fill: parent
+
+    // reference to page to prevent back navigation (required)
+    property Item page
+
+    // large text displayed on panel
+    property string headerText: ""
+
+    // small text displayed on panel
+    property string text: ""
+
+    // open status of the panel
+    property alias open: dockedPanel.open
+
+    // shows the panel
+    function showText(txt) {
+        headerText = txt;
+        text = "";
+        dockedPanel.show();
+    }
+
+    // hides the panel
+    function hide() {
+        dockedPanel.hide();
+    }
+
+    // cancelled signal is emitted when user presses the cancel button
+    signal cancelled
+
+
+    //// internal
+
+    InteractionBlocker {
+        anchors.fill: parent
+        visible: dockedPanel.open
+    }
+
+    DockedPanel {
+        id: dockedPanel
+
+        width: parent.width
+        height: Theme.itemSizeExtraLarge + Theme.paddingLarge
+
+        dock: Dock.Top
+        open: false
+        onOpenChanged: page.backNavigation = !open; // disable back navigation
+
+        Rectangle {
+            anchors.fill: parent
+            color: "black"
+            opacity: 0.7
+        }
+        BusyIndicator {
+            id: progressBusy
+            anchors.right: progressHeader.left
+            anchors.rightMargin: Theme.paddingLarge
+            anchors.verticalCenter: parent.verticalCenter
+            running: true
+            size: BusyIndicatorSize.Small
+        }
+        Rectangle {
+            id: cancelButton
+            anchors.right: parent.right
+            width: 100
+            anchors.top: parent.top
+            anchors.bottom: parent.bottom
+            color: cancelMouseArea.pressed ? Theme.secondaryHighlightColor : "transparent"
+
+            MouseArea {
+                id: cancelMouseArea
+                anchors.fill: parent
+                onClicked: cancelled();
+                enabled: true
+                Text {
+                    anchors.centerIn: parent
+                    color: Theme.primaryColor
+                    text: "X"
+                }
+            }
+        }
+        Label {
+            id: progressHeader
+            visible: dockedPanel.open
+            anchors.left: parent.left
+            anchors.right: cancelButton.left
+            anchors.top: parent.top
+            anchors.topMargin: 40
+            anchors.leftMargin: progressBusy.width + Theme.paddingLarge*4
+            anchors.rightMargin: Theme.paddingLarge
+            text: progressPanel.headerText
+            color: Theme.primaryColor
+        }
+        Label {
+            id: progressText
+            visible: dockedPanel.open
+            anchors.left: progressHeader.left
+            anchors.right: cancelButton.left
+            anchors.rightMargin: Theme.paddingLarge
+            anchors.top: progressHeader.bottom
+            text: progressPanel.text
+            wrapMode: Text.Wrap
+            font.pixelSize: Theme.fontSizeTiny
+            color: Theme.primaryColor
+        }
+    }
+}
diff --git a/qml/filebrowse/components/Spacer.qml b/qml/filebrowse/components/Spacer.qml
new file mode 100644 (file)
index 0000000..4336426
--- /dev/null
@@ -0,0 +1,8 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+// This component creates empty vertical space
+Item {
+    width: parent.width
+    height: 40
+}
diff --git a/qml/filebrowse/images/large-file-apk.png b/qml/filebrowse/images/large-file-apk.png
new file mode 100644 (file)
index 0000000..e56e762
Binary files /dev/null and b/qml/filebrowse/images/large-file-apk.png differ
diff --git a/qml/filebrowse/images/large-file-audio.png b/qml/filebrowse/images/large-file-audio.png
new file mode 100644 (file)
index 0000000..9bd0e17
Binary files /dev/null and b/qml/filebrowse/images/large-file-audio.png differ
diff --git a/qml/filebrowse/images/large-file-image.png b/qml/filebrowse/images/large-file-image.png
new file mode 100644 (file)
index 0000000..5c1eab5
Binary files /dev/null and b/qml/filebrowse/images/large-file-image.png differ
diff --git a/qml/filebrowse/images/large-file-rpm.png b/qml/filebrowse/images/large-file-rpm.png
new file mode 100644 (file)
index 0000000..24eff8a
Binary files /dev/null and b/qml/filebrowse/images/large-file-rpm.png differ
diff --git a/qml/filebrowse/images/large-file-txt.png b/qml/filebrowse/images/large-file-txt.png
new file mode 100644 (file)
index 0000000..ecdd511
Binary files /dev/null and b/qml/filebrowse/images/large-file-txt.png differ
diff --git a/qml/filebrowse/images/large-file-video.png b/qml/filebrowse/images/large-file-video.png
new file mode 100644 (file)
index 0000000..f3dbe68
Binary files /dev/null and b/qml/filebrowse/images/large-file-video.png differ
diff --git a/qml/filebrowse/images/large-file.png b/qml/filebrowse/images/large-file.png
new file mode 100644 (file)
index 0000000..6f3c825
Binary files /dev/null and b/qml/filebrowse/images/large-file.png differ
diff --git a/qml/filebrowse/images/large-folder-link.png b/qml/filebrowse/images/large-folder-link.png
new file mode 100644 (file)
index 0000000..b7295d4
Binary files /dev/null and b/qml/filebrowse/images/large-folder-link.png differ
diff --git a/qml/filebrowse/images/large-folder.png b/qml/filebrowse/images/large-folder.png
new file mode 100644 (file)
index 0000000..0bfd0e2
Binary files /dev/null and b/qml/filebrowse/images/large-folder.png differ
diff --git a/qml/filebrowse/images/large-link.png b/qml/filebrowse/images/large-link.png
new file mode 100644 (file)
index 0000000..f288a66
Binary files /dev/null and b/qml/filebrowse/images/large-link.png differ
diff --git a/qml/filebrowse/images/small-file-apk.png b/qml/filebrowse/images/small-file-apk.png
new file mode 100644 (file)
index 0000000..eb07ad4
Binary files /dev/null and b/qml/filebrowse/images/small-file-apk.png differ
diff --git a/qml/filebrowse/images/small-file-audio.png b/qml/filebrowse/images/small-file-audio.png
new file mode 100644 (file)
index 0000000..12ac2e9
Binary files /dev/null and b/qml/filebrowse/images/small-file-audio.png differ
diff --git a/qml/filebrowse/images/small-file-image.png b/qml/filebrowse/images/small-file-image.png
new file mode 100644 (file)
index 0000000..ddc7108
Binary files /dev/null and b/qml/filebrowse/images/small-file-image.png differ
diff --git a/qml/filebrowse/images/small-file-rpm.png b/qml/filebrowse/images/small-file-rpm.png
new file mode 100644 (file)
index 0000000..136cff5
Binary files /dev/null and b/qml/filebrowse/images/small-file-rpm.png differ
diff --git a/qml/filebrowse/images/small-file-text.png b/qml/filebrowse/images/small-file-text.png
new file mode 100644 (file)
index 0000000..ecfa861
Binary files /dev/null and b/qml/filebrowse/images/small-file-text.png differ
diff --git a/qml/filebrowse/images/small-file-txt.png b/qml/filebrowse/images/small-file-txt.png
new file mode 100644 (file)
index 0000000..ecfa861
Binary files /dev/null and b/qml/filebrowse/images/small-file-txt.png differ
diff --git a/qml/filebrowse/images/small-file-video.png b/qml/filebrowse/images/small-file-video.png
new file mode 100644 (file)
index 0000000..f6162d1
Binary files /dev/null and b/qml/filebrowse/images/small-file-video.png differ
diff --git a/qml/filebrowse/images/small-file.png b/qml/filebrowse/images/small-file.png
new file mode 100644 (file)
index 0000000..632fd83
Binary files /dev/null and b/qml/filebrowse/images/small-file.png differ
diff --git a/qml/filebrowse/images/small-folder-link.png b/qml/filebrowse/images/small-folder-link.png
new file mode 100644 (file)
index 0000000..d556cc8
Binary files /dev/null and b/qml/filebrowse/images/small-folder-link.png differ
diff --git a/qml/filebrowse/images/small-folder.png b/qml/filebrowse/images/small-folder.png
new file mode 100644 (file)
index 0000000..ef84395
Binary files /dev/null and b/qml/filebrowse/images/small-folder.png differ
diff --git a/qml/filebrowse/images/small-link.png b/qml/filebrowse/images/small-link.png
new file mode 100644 (file)
index 0000000..15ce793
Binary files /dev/null and b/qml/filebrowse/images/small-link.png differ
diff --git a/qml/filebrowse/pages/AboutPage.qml b/qml/filebrowse/pages/AboutPage.qml
new file mode 100644 (file)
index 0000000..c3f3701
--- /dev/null
@@ -0,0 +1,58 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+Page {
+    id: page
+    allowedOrientations: Orientation.All
+
+    SilicaFlickable {
+        id: flickable
+        anchors.fill: parent
+        contentHeight: column.height
+        VerticalScrollDecorator { flickable: flickable }
+
+        Column {
+            id: column
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.leftMargin: Theme.paddingLarge
+            anchors.rightMargin: Theme.paddingLarge
+
+            PageHeader { title: qsTr("Public Domain") }
+
+            Label {
+                width: parent.width
+                wrapMode: Text.Wrap
+                font.pixelSize: Theme.fontSizeSmall
+                color: Theme.highlightColor
+                text: "This is free and unencumbered software released into the public domain."+
+                      "\n\n"+
+                      "Anyone is free to copy, modify, publish, use, compile, sell, or "+
+                      "distribute this software, either in source code form or as a compiled "+
+                      "binary, for any purpose, commercial or non-commercial, and by any "+
+                      "means."+
+                      "\n\n"+
+                      "In jurisdictions that recognize copyright laws, the author or authors "+
+                      "of this software dedicate any and all copyright interest in the "+
+                      "software to the public domain. We make this dedication for the benefit "+
+                      "of the public at large and to the detriment of our heirs and "+
+                      "successors. We intend this dedication to be an overt act of "+
+                      "relinquishment in perpetuity of all present and future rights to this "+
+                      "software under copyright law."+
+                      "\n\n"+
+                      "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, "+
+                      "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF "+
+                      "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. "+
+                      "IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR "+
+                      "OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, "+
+                      "ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR "+
+                      "OTHER DEALINGS IN THE SOFTWARE."+
+                      "\n\n"+
+                      "For more information, please refer to <http://unlicense.org>"
+            }
+        }
+    }
+
+}
+
+
diff --git a/qml/filebrowse/pages/ConsolePage.qml b/qml/filebrowse/pages/ConsolePage.qml
new file mode 100644 (file)
index 0000000..96f4874
--- /dev/null
@@ -0,0 +1,102 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import harbour.file.browser.FileInfo 1.0
+import "../components"
+
+Page {
+    id: page
+    allowedOrientations: Orientation.All
+    property string title: ""
+    property string command: ""
+    property variant arguments // this must be set to a string list, e.g. [ "arg1", "arg2" ]
+    property string initialText: qsTr("Installing...")
+    property string successText: qsTr("Successful")
+    property string infoText: ""
+    property color consoleColor: Theme.secondaryColor
+
+    // execute command when page activates
+    onStatusChanged: {
+        if (status === PageStatus.Activating) {
+            fileInfo.executeCommand(page.command, page.arguments);
+        }
+    }
+
+    FileInfo {
+        id: fileInfo
+
+        // called when command exits
+        onProcessExited: {
+            busyIndicator.running = false;
+            if (exitCode == 0) {
+                statusLabel.text = page.successText;
+                infoLabel.text = page.infoText;
+            } else {
+                statusLabel.text = qsTr("Failed! Error code: %1").arg(exitCode);
+            }
+        }
+    }
+
+    SilicaFlickable {
+        id: flickable
+        anchors.fill: parent
+        contentHeight: column.height
+        VerticalScrollDecorator { flickable: flickable }
+
+        Column {
+            id: column
+            width: parent.width
+
+            PageHeader { title: page.title }
+
+            BusyIndicator {
+                id: busyIndicator
+                anchors.horizontalCenter: parent.horizontalCenter
+                running: true
+                size: BusyIndicatorSize.Small
+            }
+            Label {
+                id: statusLabel
+                anchors.horizontalCenter: parent.horizontalCenter
+                text: page.initialText
+                color: Theme.highlightColor
+            }
+            Label {
+                id: infoLabel
+                visible: text !== ""
+                text: ""
+                anchors.left: parent.left
+                anchors.right: parent.right
+                anchors.leftMargin: Theme.paddingLarge
+                anchors.rightMargin: Theme.paddingLarge
+                wrapMode: Text.Wrap
+                font.pixelSize: Theme.fontSizeTiny
+                horizontalAlignment: Text.AlignHCenter
+                color: Theme.secondaryColor
+            }
+
+            Spacer { height: 40 }
+
+            // command line text
+            Label {
+                width: parent.width
+                text: "$ "+page.command+" "+page.arguments.join(" ")
+                wrapMode: Text.WrapAnywhere
+                font.pixelSize: Theme.fontSizeTiny
+                font.family: "Monospace"
+                color: Theme.secondaryColor
+            }
+
+            // command output
+            Label {
+                width: parent.width
+                text: fileInfo.processOutput
+                wrapMode: Text.WrapAnywhere
+                font.pixelSize: Theme.fontSizeTiny
+                font.family: "Monospace"
+                color: page.consoleColor
+            }
+        }
+    }
+}
+
+
diff --git a/qml/filebrowse/pages/CreateFolderDialog.qml b/qml/filebrowse/pages/CreateFolderDialog.qml
new file mode 100644 (file)
index 0000000..d228fba
--- /dev/null
@@ -0,0 +1,64 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import "../components"
+
+Dialog {
+    property string path: ""
+
+    // return value
+    property string errorMessage: ""
+
+    id: dialog
+    allowedOrientations: Orientation.All
+    canAccept: folderName.text !== ""
+
+    onAccepted: errorMessage = engine.mkdir(path, folderName.text);
+
+    SilicaFlickable {
+        id: flickable
+        anchors.fill: parent
+        contentHeight: column.height
+        VerticalScrollDecorator { flickable: flickable }
+
+        Column {
+            id: column
+            anchors.left: parent.left
+            anchors.right: parent.right
+
+            DialogHeader {
+                id: dialogHeader
+                title: qsTr("Create Folder")
+                acceptText: qsTr("Create")
+            }
+
+            Label {
+                anchors.left: parent.left
+                anchors.right: parent.right
+                anchors.leftMargin: Theme.paddingLarge
+                anchors.rightMargin: Theme.paddingLarge
+                text: qsTr("Create a new folder under\n%1").arg(path)
+                color: Theme.secondaryColor
+                wrapMode: Text.Wrap
+            }
+
+            Spacer {
+                height: 20
+            }
+
+            TextField {
+                id: folderName
+                width: parent.width
+                placeholderText: qsTr("Folder name")
+                label: qsTr("Folder name")
+                focus: true
+
+                // return key on virtual keyboard accepts the dialog
+                EnterKey.enabled: folderName.text.length > 0
+                EnterKey.iconSource: "image://theme/icon-m-enter-accept"
+                EnterKey.onClicked: dialog.accept()
+            }
+        }
+    }
+}
+
+
diff --git a/qml/filebrowse/pages/DirectoryPage.qml b/qml/filebrowse/pages/DirectoryPage.qml
new file mode 100644 (file)
index 0000000..eb55286
--- /dev/null
@@ -0,0 +1,237 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import harbour.file.browser.FileModel 1.0
+import "functions.js" as Functions
+import "../components"
+
+Page {
+    id: page
+    allowedOrientations: Orientation.All
+    property string dir: "/"
+    property string initialDir: ""
+    property bool initial: false // this is set to true if the page is initial page
+    property int _selectedMenu: 0
+
+    FileModel {
+        id: fileModel
+        dir: page.dir
+        // page.status does not exactly work - root folder seems to be active always??
+        active: page.status === PageStatus.Active
+    }
+
+    SilicaListView {
+        id: fileList
+        anchors.fill: parent
+
+        model: fileModel
+
+        VerticalScrollDecorator { flickable: fileList }
+
+        PullDownMenu {
+            id: pullMenu;
+
+            onActiveChanged: {
+                switch (_selectedMenu) {
+                    case 1:
+                        Functions.cancel()
+                        break;
+                    case 2:
+                        pageStack.push(Qt.resolvedUrl("SearchPage.qml"), { dir: page.dir });
+                        break;
+                    case 3:
+                        fileModel.showAll = true
+                        menuShowAll.visible = false
+                        menuShowFiltered.visible = true
+                        break;
+                    case 4:
+                        fileModel.showAll = false
+                        menuShowFiltered.visible = false
+                        menuShowAll.visible = true
+                        break;
+                }
+                _selectedMenu = 0
+            }
+
+            MenuItem {
+                text: qsTr("Cancel")
+                onClicked: _selectedMenu = 1
+            }
+            MenuItem {
+                text: qsTr("Search")
+                onClicked: _selectedMenu = 2
+            }
+            MenuItem {
+                id: menuShowAll
+                visible: !fileModel.showAll
+                text: qsTr("Show all files")
+                onClicked: _selectedMenu = 3
+            }
+            MenuItem {
+                id: menuShowFiltered
+                visible: fileModel.showAll
+                text: qsTr("Show only ") + engine.extensionFilter + qsTr(" files")
+                onClicked: _selectedMenu = 4
+            }
+        }
+
+        header: PageHeader {
+            title: Functions.formatPathForTitle(page.dir) + " " +
+                   Functions.unicodeBlackDownPointingTriangle()
+            MouseArea {
+                anchors.fill: parent
+                onClicked: dirPopup.show();
+            }
+        }
+
+        delegate: ListItem {
+            id: fileItem
+            menu: contextMenu
+            width: ListView.view.width
+            contentHeight: listLabel.height+listSize.height + 13
+
+            Image {
+                id: listIcon
+                anchors.left: parent.left
+                anchors.leftMargin: Theme.paddingLarge
+                anchors.top: parent.top
+                anchors.topMargin: 11
+                source: "../images/small-"+fileIcon+".png"
+            }
+            Label {
+                id: listLabel
+                anchors.left: listIcon.right
+                anchors.leftMargin: 10
+                anchors.right: parent.right
+                anchors.rightMargin: Theme.paddingLarge
+                anchors.top: parent.top
+                anchors.topMargin: 5
+                text: filename
+                elide: Text.ElideRight
+            }
+            Label {
+                id: listSize
+                anchors.left: listIcon.right
+                anchors.leftMargin: 10
+                anchors.top: listLabel.bottom
+                text: !(isLink && isDir) ? size : Functions.unicodeArrow()+" "+symLinkTarget
+                color: Theme.secondaryColor
+                font.pixelSize: Theme.fontSizeExtraSmall
+            }
+            Label {
+                visible: !(isLink && isDir)
+                anchors.top: listLabel.bottom
+                anchors.horizontalCenter: parent.horizontalCenter
+                text: filekind+permissions
+                color: Theme.secondaryColor
+                font.pixelSize: Theme.fontSizeExtraSmall
+            }
+            Label {
+                visible: !(isLink && isDir)
+                anchors.top: listLabel.bottom
+                anchors.right: listLabel.right
+                text: modified
+                color: Theme.secondaryColor
+                font.pixelSize: Theme.fontSizeExtraSmall
+            }
+
+            onClicked: {
+                if (model.isDir) {
+                    pageStack.push(Qt.resolvedUrl("DirectoryPage.qml"),
+                                   { dir: fileModel.appendPath(listLabel.text) });
+                }
+                else {
+                    Functions.cancel()
+                    Functions.fileSelect(fileModel.appendPath(listLabel.text))
+                    //pageStack.push(Qt.resolvedUrl("FilePage.qml"),
+                    //               { file: fileModel.appendPath(listLabel.text) });
+                }
+            }
+
+            // delete file after remorse time
+            ListView.onRemove: animateRemoval(fileItem)
+            function deleteFile(deleteFilename) {
+                remorseAction(qsTr("Deleting"), function() {
+                    progressPanel.showText(qsTr("Deleting"));
+                    engine.deleteFiles([ deleteFilename ]);
+                }, 5000)
+            }
+
+            // context menu is activated with long press
+            Component {
+                 id: contextMenu
+                 ContextMenu {
+                     MenuItem {
+                         visible: true
+                         text: qsTr("Properties")
+                         onClicked:  {
+                             pageStack.push(Qt.resolvedUrl("FilePage.qml"),
+                                            { file: fileModel.fileNameAt(index) });
+                         }
+                     }
+                 }
+             }
+        }
+
+        // text if no files or error message
+        Text {
+            width: parent.width
+            anchors.leftMargin: Theme.paddingLarge
+            anchors.rightMargin: Theme.paddingLarge
+            horizontalAlignment: Qt.AlignHCenter
+            y: -fileList.contentY + 100
+            visible: fileModel.fileCount === 0 || fileModel.errorMessage !== ""
+            text: fileModel.errorMessage !== "" ? fileModel.errorMessage : (fileModel.showAll ? qsTr("No files") : qsTr("No key files"))
+            color: Theme.highlightColor
+        }
+    }
+
+    // update cover
+    onStatusChanged: {
+        if (status === PageStatus.Activating) {
+            // go to Home on startup
+            if (page.initial) {
+                page.initial = false;
+                Functions.goToInitial(dir);
+            }
+        }
+    }
+
+    DirPopup {
+        id: dirPopup
+        anchors.fill: parent
+        menuTop: 100
+    }
+
+    // connect signals from engine to panels
+    Connections {
+        target: engine
+        onProgressChanged: progressPanel.text = engine.progressFilename
+        onWorkerDone: progressPanel.hide()
+        onWorkerErrorOccurred: {
+            // the error signal goes to all pages in pagestack, show it only in the active one
+            if (progressPanel.open) {
+                progressPanel.hide();
+                if (message === "Unknown error")
+                    filename = qsTr("Trying to move between phone and SD Card? It doesn't work, try copying.");
+                else if (message === "Failure to write block")
+                    filename = qsTr("Perhaps the storage is full?");
+
+                notificationPanel.showText(message, filename);
+            }
+        }
+    }
+
+    NotificationPanel {
+        id: notificationPanel
+        page: page
+    }
+
+    ProgressPanel {
+        id: progressPanel
+        page: page
+        onCancelled: engine.cancel()
+    }
+
+}
+
+
diff --git a/qml/filebrowse/pages/FilePage.qml b/qml/filebrowse/pages/FilePage.qml
new file mode 100644 (file)
index 0000000..1519d41
--- /dev/null
@@ -0,0 +1,299 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import harbour.file.browser.FileInfo 1.0
+import QtMultimedia 5.0
+import "functions.js" as Functions
+import "../components"
+
+Page {
+    id: page
+    allowedOrientations: Orientation.All
+    property string file: "/"
+
+    FileInfo {
+        id: fileInfo
+        file: page.file
+
+        // called when open command exits
+        onProcessExited: {
+            if (exitCode === 0) {
+                notificationPanel.showTextWithTimer(qsTr("Open successful"),
+                                               qsTr("Sometimes the application stays in the background"));
+            } else if (exitCode === 1) {
+                notificationPanel.showTextWithTimer(qsTr("Internal error"),
+                                               "xdg-open exit code 1");
+            } else if (exitCode === 2) {
+                notificationPanel.showTextWithTimer(qsTr("File not found"),
+                                               page.file);
+            } else if (exitCode === 3) {
+                notificationPanel.showTextWithTimer(qsTr("No application to open the file"),
+                                               qsTr("xdg-open found no preferred application (3)"));
+            } else if (exitCode === 4) {
+                notificationPanel.showTextWithTimer(qsTr("Action failed"),
+                                               "xdg-open exit code 4");
+            } else if (exitCode === -88888) {
+                notificationPanel.showTextWithTimer(qsTr("xdg-open not found"), "");
+
+            } else if (exitCode === -99999) {
+                notificationPanel.showTextWithTimer(qsTr("xdg-open crash?"), "");
+
+            } else {
+                notificationPanel.showTextWithTimer(qsTr("xdg-open error"), "exit code: "+exitCode);
+            }
+        }
+    }
+
+    SilicaFlickable {
+        id: flickable
+        anchors.fill: parent
+        contentHeight: column.height
+        VerticalScrollDecorator { flickable: flickable }
+
+        PullDownMenu {
+            enabled: !fileInfo.isDir
+            visible: !fileInfo.isDir
+
+            MenuItem {
+                text: qsTr("Select")
+                visible: !fileInfo.isDir
+                onClicked: Functions.fileSelect(page.file)
+            }
+            MenuItem {
+                text: qsTr("View Contents")
+                visible: !fileInfo.isDir
+                onClicked: pageStack.push(Qt.resolvedUrl("ViewPage.qml"),
+                                          { path: page.file });
+            }
+
+            // file type specific menu items
+
+            MenuItem {
+                text: qsTr("Go to Target")
+                visible: fileInfo.icon === "folder-link"
+                onClicked: Functions.goToFolder(fileInfo.symLinkTarget);
+            }
+        }
+
+        Column {
+            id: column
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.leftMargin: Theme.paddingLarge
+            anchors.rightMargin: Theme.paddingLarge
+
+            PageHeader {
+                title: Functions.formatPathForTitle(fileInfo.absolutePath) + " " +
+                       Functions.unicodeBlackDownPointingTriangle()
+                MouseArea {
+                    anchors.fill: parent
+                    onClicked: dirPopup.show();
+                }
+            }
+
+            // file info texts, visible if error is not set
+            Column {
+                visible: fileInfo.errorMessage === ""
+                anchors.left: parent.left
+                anchors.right: parent.right
+
+                Image { // preview of image, max height 400
+                    id: imagePreview
+                    visible: isImageFile(fileInfo)
+                    source: visible ? fileInfo.file : "" // access the source only if img is visible
+                    anchors.left: parent.left
+                    anchors.right: parent.right
+                    height: implicitHeight < 400 && implicitHeight != 0 ? implicitHeight : 400
+                    width: parent.width
+                    fillMode: Image.PreserveAspectFit
+                    asynchronous: true
+                }
+                IconButton {
+                    id: playButton
+                    visible: isAudioFile(fileInfo)
+                    icon.source: audioPlayer.playbackState !== MediaPlayer.PlayingState ?
+                                     "image://theme/icon-l-play" :
+                                     "image://theme/icon-l-pause"
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    onClicked: playAudio();
+                    MediaPlayer { // prelisten of audio
+                        id: audioPlayer
+                        source: ""
+                    }
+                }
+                Spacer { height: 10; visible: playButton.visible } // fix to playButton height
+                // clickable icon and filename
+                MouseArea {
+                    id: openButton
+                    width: parent.width
+                    height: openArea.height
+                    onClicked: openFile()
+
+                    Rectangle {
+                        anchors.fill: parent
+                        color: Theme.highlightColor
+                        opacity: 0.2
+                        visible: openButton.pressed
+                    }
+                    Column {
+                        id: openArea
+                        width: parent.width
+                        Image {
+                            id: icon
+                            anchors.topMargin: 6
+                            anchors.horizontalCenter: parent.horizontalCenter
+                            source: "../images/large-"+fileInfo.icon+".png"
+                            visible: !imagePreview.visible && !playButton.visible
+                        }
+                        Spacer { // spacing if image or play button is visible
+                            id: spacer
+                            height: 24
+                            visible: imagePreview.visible || playButton.visible
+                        }
+                        Label {
+                            id: filename
+                            width: parent.width
+                            text: fileInfo.name
+                            wrapMode: Text.Wrap
+                            horizontalAlignment: Text.AlignHCenter
+                        }
+                        Label {
+                            visible: fileInfo.symLinkTarget !== ""
+                            width: parent.width
+                            text: Functions.unicodeArrow()+" "+fileInfo.symLinkTarget
+                            wrapMode: Text.Wrap
+                            horizontalAlignment: Text.AlignHCenter
+                            font.pixelSize: Theme.fontSizeExtraSmall
+                        }
+                        Spacer { height: 20 }
+                    }
+                }
+                Spacer { height: 20 }
+
+                Label {
+                    visible: fileInfo.suffix === "apk" || fileInfo.suffix === "rpm" && !fileInfo.isDir
+                    width: parent.width
+                    text: qsTr("Installable packages may contain malware.")
+                    color: "red"
+                    font.pixelSize: Theme.fontSizeExtraSmall
+                    horizontalAlignment: Text.AlignHCenter
+                    wrapMode: Text.Wrap
+                }
+                Spacer {
+                    visible: fileInfo.suffix === "apk" || fileInfo.suffix === "rpm" && !fileInfo.isDir
+                    height: 40
+                }
+
+                CenteredField {
+                    label: qsTr("Location")
+                    value: fileInfo.absolutePath
+                }
+                CenteredField {
+                    label: qsTr("Size")
+                    value: fileInfo.size
+                }
+                CenteredField {
+                    label: qsTr("Permissions")
+                    value: fileInfo.permissions
+                }
+                CenteredField {
+                    label: qsTr("Owner")
+                    value: fileInfo.owner
+                }
+                CenteredField {
+                    label: qsTr("Group")
+                    value: fileInfo.group
+                }
+                CenteredField {
+                    label: qsTr("Last modified")
+                    value: fileInfo.modified
+                }
+                CenteredField {
+                    label: qsTr("Created")
+                    value: fileInfo.created
+                }
+            }
+
+            // error label, visible if error message is set
+            Label {
+                visible: fileInfo.errorMessage !== ""
+                anchors.left: parent.left
+                anchors.right: parent.right
+                horizontalAlignment: Text.AlignHCenter
+                text: fileInfo.errorMessage
+                color: Theme.highlightColor
+                wrapMode: Text.Wrap
+            }
+        }
+    }
+
+    DirPopup {
+        id: dirPopup
+        anchors.fill: parent
+        menuTop: 100
+    }
+
+    NotificationPanel {
+        id: notificationPanel
+        page: page
+    }
+
+    function isImageFile(fileInfo)
+    {
+        if (fileInfo.isDir) return false;
+        return fileInfo.suffix === "jpg" || fileInfo.suffix === "jpeg" ||
+                fileInfo.suffix === "png" || fileInfo.suffix === "gif";
+    }
+
+    function isAudioFile(fileInfo)
+    {
+        if (fileInfo.isDir) return false;
+        return fileInfo.suffix === "wav" || fileInfo.suffix === "mp3" ||
+                fileInfo.suffix === "ogg" || fileInfo.suffix === "flac" ||
+                fileInfo.suffix === "aac" || fileInfo.suffix === "m4a";
+    }
+
+    function isVideoFile(fileInfo)
+    {
+        if (fileInfo.isDir) return false;
+        return fileInfo.suffix === "mp4" || fileInfo.suffix === "m4v";
+    }
+
+    function isMediaFile(fileInfo)
+    {
+        if (fileInfo.isDir) return false;
+        return isAudioFile(fileInfo) | isVideoFile(fileInfo);
+    }
+
+    function openFile()
+    {
+        // perform action depending on file type
+        if (fileInfo.icon === "folder-link") {
+            Functions.goToFolder(fileInfo.symLinkTarget);
+
+        } else if (fileInfo.isDir) {
+            Functions.goToFolder(fileInfo.file);
+
+        } else if (isAudioFile(fileInfo)) {
+            playAudio();
+
+        } else if (isImageFile(fileInfo) || isVideoFile(fileInfo)) {
+            fileInfo.executeCommand("xdg-open", [ page.file ])
+
+        } else {
+            pageStack.push(Qt.resolvedUrl("ViewPage.qml"), { path: page.file });
+        }
+    }
+
+    function playAudio()
+    {
+        if (audioPlayer.playbackState !== MediaPlayer.PlayingState) {
+            audioPlayer.source = fileInfo.file;
+            audioPlayer.play();
+        } else {
+            audioPlayer.stop();
+        }
+    }
+
+}
+
+
diff --git a/qml/filebrowse/pages/PermissionsDialog.qml b/qml/filebrowse/pages/PermissionsDialog.qml
new file mode 100644 (file)
index 0000000..cdee48a
--- /dev/null
@@ -0,0 +1,218 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import harbour.file.browser.FileInfo 1.0
+import "../components"
+
+Dialog {
+    property string path: ""
+
+    // return value
+    property string errorMessage: ""
+
+    id: dialog
+    allowedOrientations: Orientation.All
+
+    property int _executeWidth: executeLabel.width
+
+    onAccepted: errorMessage = engine.chmod(path,
+                        ownerRead.checked, ownerWrite.checked, ownerExecute.checked,
+                        groupRead.checked, groupWrite.checked, groupExecute.checked,
+                        othersRead.checked, othersWrite.checked, othersExecute.checked);
+
+    FileInfo {
+        id: fileInfo
+        file: path
+    }
+
+    // copy values to fields when page shows up
+    Component.onCompleted: {
+        ownerName.text = fileInfo.owner
+        groupName.text = fileInfo.group
+        var permissions = fileInfo.permissions
+        if (permissions.charAt(0) !== '-') ownerRead.checked = true;
+        if (permissions.charAt(1) !== '-') ownerWrite.checked = true;
+        if (permissions.charAt(2) !== '-') ownerExecute.checked = true;
+        if (permissions.charAt(3) !== '-') groupRead.checked = true;
+        if (permissions.charAt(4) !== '-') groupWrite.checked = true;
+        if (permissions.charAt(5) !== '-') groupExecute.checked = true;
+        if (permissions.charAt(6) !== '-') othersRead.checked = true;
+        if (permissions.charAt(7) !== '-') othersWrite.checked = true;
+        if (permissions.charAt(8) !== '-') othersExecute.checked = true;
+    }
+
+    SilicaFlickable {
+        id: flickable
+        anchors.fill: parent
+        contentHeight: column.height
+        VerticalScrollDecorator { flickable: flickable }
+
+        Column {
+            id: column
+            anchors.left: parent.left
+            anchors.right: parent.right
+
+            DialogHeader {
+                id: dialogHeader
+                title: qsTr("Change Permissions")
+                acceptText: qsTr("Change")
+            }
+
+            Label {
+                anchors.left: parent.left
+                anchors.right: parent.right
+                anchors.leftMargin: Theme.paddingLarge
+                anchors.rightMargin: Theme.paddingLarge
+                text: qsTr("Change permissions for\n%1").arg(path)
+                color: Theme.secondaryColor
+                wrapMode: Text.Wrap
+            }
+
+            Spacer {
+                height: 40
+            }
+
+            // read, write, execute small labels
+            Row {
+                width: parent.width
+                Label {
+                    width: parent.width/2
+                    text: " "
+                }
+
+                Label {
+                    id: readLabel
+                    width: executeLabel.width
+                    text: qsTr("Read")
+                    font.pixelSize: Theme.fontSizeExtraSmall
+                    color: Theme.secondaryColor
+                    horizontalAlignment: Text.AlignHCenter
+                }
+                Label {
+                    id: writeLabel
+                    width: executeLabel.width
+                    text: qsTr("Write")
+                    font.pixelSize: Theme.fontSizeExtraSmall
+                    color: Theme.secondaryColor
+                    horizontalAlignment: Text.AlignHCenter
+                }
+                Label {
+                    id: executeLabel
+                    text: qsTr("Execute")
+                    font.pixelSize: Theme.fontSizeExtraSmall
+                    color: Theme.secondaryColor
+                    horizontalAlignment: Text.AlignHCenter
+                }
+            }
+
+            // owner
+            Row {
+                width: parent.width
+                Column {
+                    width: parent.width/2
+                    Label {
+                        id: ownerName
+                        width: parent.width-20
+                        text: ""
+                        color: Theme.highlightColor
+                        horizontalAlignment: Text.AlignRight
+                    }
+                    Label {
+                        width: parent.width-20
+                        text: qsTr("Owner")
+                        font.pixelSize: Theme.fontSizeExtraSmall
+                        color: Theme.secondaryColor
+                        horizontalAlignment: Text.AlignRight
+                    }
+                }
+                LetterSwitch {
+                    id: ownerRead
+                    width: _executeWidth
+                    letter: 'r'
+                }
+                LetterSwitch {
+                    id: ownerWrite
+                    width: _executeWidth
+                    letter: 'w'
+                }
+                LetterSwitch {
+                    id: ownerExecute
+                    width: _executeWidth
+                    letter: 'x'
+                }
+            }
+
+            // group
+            Row {
+                id: groupRow
+                width: parent.width
+                Column {
+                    width: parent.width/2
+                    Label {
+                        id: groupName
+                        width: parent.width-20
+                        text: ""
+                        color: Theme.highlightColor
+                        horizontalAlignment: Text.AlignRight
+                    }
+                    Label {
+                        width: parent.width-20
+                        text: qsTr("Group")
+                        font.pixelSize: Theme.fontSizeExtraSmall
+                        color: Theme.secondaryColor
+                        horizontalAlignment: Text.AlignRight
+                    }
+                }
+                LetterSwitch {
+                    id: groupRead
+                    width: _executeWidth
+                    letter: 'r'
+                }
+                LetterSwitch {
+                    id: groupWrite
+                    width: _executeWidth
+                    letter: 'w'
+                }
+                LetterSwitch {
+                    id: groupExecute
+                    width: _executeWidth
+                    letter: 'x'
+                }
+            }
+
+            // others
+            Row {
+                width: parent.width
+                height: groupRow.height
+                Item {
+                    width: parent.width/2
+                    height: parent.height
+                    Label {
+                        width: parent.width-20
+                        height: parent.height
+                        text: qsTr("Others")
+                        color: Theme.highlightColor
+                        horizontalAlignment: Text.AlignRight
+                        verticalAlignment: Text.AlignVCenter
+                    }
+                }
+                LetterSwitch {
+                    id: othersRead
+                    width: _executeWidth
+                    letter: 'r'
+                }
+                LetterSwitch {
+                    id: othersWrite
+                    width: _executeWidth
+                    letter: 'w'
+                }
+                LetterSwitch {
+                    id: othersExecute
+                    width: _executeWidth
+                    letter: 'x'
+                }
+            }
+        }
+    }
+}
+
+
diff --git a/qml/filebrowse/pages/RenameDialog.qml b/qml/filebrowse/pages/RenameDialog.qml
new file mode 100644 (file)
index 0000000..2e0d11b
--- /dev/null
@@ -0,0 +1,74 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import "functions.js" as Functions
+import "../components"
+
+Dialog {
+    property string path: ""
+
+    // return values
+    property string errorMessage: ""
+    property string newPath: ""
+
+    id: dialog
+    allowedOrientations: Orientation.All
+    canAccept: newName.text !== ""
+
+    onAccepted: {
+        var res = engine.rename(path, newName.text);
+        newPath = res[0]
+        errorMessage = res[1]
+    }
+
+    Component.onCompleted: {
+        newName.text = Functions.lastPartOfPath(path)
+    }
+
+    SilicaFlickable {
+        id: flickable
+        anchors.fill: parent
+        contentHeight: column.height
+        VerticalScrollDecorator { flickable: flickable }
+
+        Column {
+            id: column
+            anchors.left: parent.left
+            anchors.right: parent.right
+
+            DialogHeader {
+                id: dialogHeader
+                title: qsTr("Rename")
+                acceptText: qsTr("Rename")
+            }
+
+            Label {
+                anchors.left: parent.left
+                anchors.right: parent.right
+                anchors.leftMargin: Theme.paddingLarge
+                anchors.rightMargin: Theme.paddingLarge
+                text: qsTr("Give a new name for\n%1").arg(path)
+                color: Theme.secondaryColor
+                wrapMode: Text.Wrap
+            }
+
+            Spacer {
+                height: 20
+            }
+
+            TextField {
+                id: newName
+                width: parent.width
+                placeholderText: qsTr("New name")
+                label: qsTr("New name")
+                focus: true
+
+                // return key on virtual keyboard accepts the dialog
+                EnterKey.enabled: newName.text.length > 0
+                EnterKey.iconSource: "image://theme/icon-m-enter-accept"
+                EnterKey.onClicked: dialog.accept()
+            }
+        }
+    }
+}
+
+
diff --git a/qml/filebrowse/pages/SearchPage.qml b/qml/filebrowse/pages/SearchPage.qml
new file mode 100644 (file)
index 0000000..e11d8e8
--- /dev/null
@@ -0,0 +1,262 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import harbour.file.browser.SearchEngine 1.0
+import "functions.js" as Functions
+import "../components"
+
+Page {
+    id: page
+    allowedOrientations: Orientation.All
+    showNavigationIndicator: false // hide back indicator because it would be on top of search field
+    property string dir: "/"
+    property string currentDirectory: ""
+
+    // this and its bg worker thread will be destroyed when page in popped from stack
+    SearchEngine {
+        id: searchEngine
+        dir: page.dir
+
+        onProgressChanged: page.currentDirectory = directory
+        onMatchFound: listModel.append({ fullname: fullname, filename: filename,
+                                           absoluteDir: absoluteDir,
+                                           fileIcon: fileIcon, fileKind: fileKind });
+        onWorkerDone: { /* Nothing to do */ }
+        onWorkerErrorOccurred: { notificationPanel.showText(message, filename); }
+    }
+
+    SilicaListView {
+        id: fileList
+        anchors.fill: parent
+
+        // prevent newly added list delegates from stealing focus away from the search field
+        currentIndex: -1
+
+        model: ListModel {
+            id: listModel
+
+            function update(txt) {
+                if (txt === "")
+                    searchEngine.cancel();
+
+                clear();
+                if (txt !== "") {
+                    searchEngine.search(txt);
+                }
+            }
+
+            Component.onCompleted: update("")
+        }
+
+        VerticalScrollDecorator { flickable: fileList }
+
+        PullDownMenu {
+            MenuItem {
+                text: qsTr("Settings")
+                onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
+            }
+        }
+
+        header: Item {
+            width: parent.width
+            height: 110
+
+            SearchField {
+                id: searchField
+                anchors.left: parent.left
+                anchors.right: cancelSearchButton.left
+                placeholderText: qsTr("Search %1").arg(Functions.formatPathForSearch(page.dir))
+                inputMethodHints: Qt.ImhNoAutoUppercase
+
+                // get focus when page is shown for the first time
+                Component.onCompleted: forceActiveFocus()
+
+                // return key on virtual keyboard starts or restarts search
+                EnterKey.enabled: true
+                EnterKey.onClicked: {
+                    notificationPanel.hide();
+                    listModel.update(searchField.text);
+                    foundText.visible = true;
+                    searchField.focus = false;
+                }
+            }
+            // our own "IconButton" to make the mouse area large and easier to tap
+            Rectangle {
+                id: cancelSearchButton
+                anchors.right: parent.right
+                anchors.top: searchField.top
+                width: Theme.iconSizeMedium+Theme.paddingLarge
+                height: searchField.height
+                color: cancelSearchMouseArea.pressed ? Theme.secondaryHighlightColor : "transparent"
+                MouseArea {
+                    id: cancelSearchMouseArea
+                    anchors.fill: parent
+                    onClicked: {
+                        if (!searchEngine.running) {
+                            listModel.update(searchField.text);
+                            foundText.visible = true;
+                        } else {
+                            searchEngine.cancel()
+                        }
+                    }
+                    enabled: true
+                    Image {
+                        id: cancelSearchButtonImage
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.right: parent.right
+                        anchors.rightMargin: Theme.paddingLarge
+                        source: searchEngine.running ? "image://theme/icon-m-clear" :
+                                                       "image://theme/icon-m-right"
+                    }
+                    BusyIndicator {
+                        id: searchBusy
+                        anchors.centerIn: cancelSearchButtonImage
+                        running: searchEngine.running
+                        size: BusyIndicatorSize.Small
+                    }
+                }
+            }
+            Label {
+                id: foundText
+                visible: false
+                anchors.left: parent.left
+                anchors.leftMargin: searchField.textLeftMargin
+                anchors.top: searchField.bottom
+                anchors.topMargin: -26
+                text: qsTr("%1 hits").arg(listModel.count)
+                font.pixelSize: Theme.fontSizeTiny
+                color: Theme.secondaryColor
+            }
+            Label {
+                anchors.left: parent.left
+                anchors.leftMargin: 240
+                anchors.right: parent.right
+                anchors.rightMargin: Theme.paddingLarge
+                anchors.top: searchField.bottom
+                anchors.topMargin: -26
+                text: page.currentDirectory
+                font.pixelSize: Theme.fontSizeTiny
+                color: Theme.secondaryColor
+                elide: Text.ElideRight
+            }
+        }
+
+        delegate: ListItem {
+            id: fileItem
+            menu: contextMenu
+            width: ListView.view.width
+            contentHeight: listLabel.height+listAbsoluteDir.height + 13
+
+            Image {
+                id: listIcon
+                anchors.left: parent.left
+                anchors.leftMargin: Theme.paddingLarge
+                anchors.top: parent.top
+                anchors.topMargin: 11
+                source: "../images/small-"+fileIcon+".png"
+            }
+            Label {
+                id: listLabel
+                anchors.left: listIcon.right
+                anchors.leftMargin: 10
+                anchors.right: parent.right
+                anchors.rightMargin: Theme.paddingLarge
+                anchors.top: parent.top
+                anchors.topMargin: 5
+                text: filename
+                elide: Text.ElideRight
+            }
+            Label {
+                id: listAbsoluteDir
+                anchors.left: listIcon.right
+                anchors.leftMargin: 10
+                anchors.right: parent.right
+                anchors.rightMargin: Theme.paddingLarge
+                anchors.top: listLabel.bottom
+                text: absoluteDir
+                color: Theme.secondaryColor
+                font.pixelSize: Theme.fontSizeExtraSmall
+                elide: Text.ElideLeft
+            }
+
+            onClicked: {
+                if (model.fileKind === "d")
+                    pageStack.push(Qt.resolvedUrl("DirectoryPage.qml"),
+                                   { dir: model.fullname });
+                else
+                    pageStack.push(Qt.resolvedUrl("FilePage.qml"),
+                                   { file: model.fullname });
+            }
+
+            // delete file after remorse time
+            ListView.onRemove: animateRemoval(fileItem)
+            function deleteFile(deleteFilename) {
+                remorseAction(qsTr("Deleting"), function() {
+                    progressPanel.showText(qsTr("Deleting"));
+                    engine.deleteFiles([ deleteFilename ]);
+                }, 5000)
+            }
+
+            // context menu is activated with long press, visible if search is not running
+            Component {
+                 id: contextMenu
+                 ContextMenu {
+                     MenuItem {
+                         text: qsTr("Go to containing folder")
+                         onClicked: Functions.goToFolder(model.absoluteDir)
+                     }
+                     MenuItem {
+                         text: qsTr("Cut")
+                         onClicked: engine.cutFiles([ model.fullname ]);
+                     }
+                     MenuItem {
+                         text: qsTr("Copy")
+                         onClicked: engine.copyFiles([ model.fullname ]);
+                     }
+                     MenuItem {
+                         text: qsTr("Delete")
+                         onClicked: deleteFile(model.fullname);
+                     }
+                 }
+             }
+        }
+
+    }
+
+    // connect signals from engine to panels
+    Connections {
+        target: engine
+        onProgressChanged: progressPanel.text = engine.progressFilename
+        onWorkerDone: progressPanel.hide()
+        onWorkerErrorOccurred: {
+            // the error signal goes to all pages in pagestack, show it only in the active one
+            if (progressPanel.open) {
+                progressPanel.hide();
+                notificationPanel.showText(message, filename);
+            }
+        }
+
+        // item got deleted by worker, so remove it from list
+        onFileDeleted: {
+            for (var i = 0; i < listModel.count; ++i) {
+                var item = listModel.get(i);
+                if (item.fullname === fullname) {
+                    listModel.remove(i)
+                    return;
+                }
+            }
+        }
+    }
+
+    NotificationPanel {
+        id: notificationPanel
+        page: page
+    }
+
+    ProgressPanel {
+        id: progressPanel
+        page: page
+        onCancelled: engine.cancel()
+    }
+}
+
+
diff --git a/qml/filebrowse/pages/SettingsPage.qml b/qml/filebrowse/pages/SettingsPage.qml
new file mode 100644 (file)
index 0000000..454d1d5
--- /dev/null
@@ -0,0 +1,113 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import "functions.js" as Functions
+import "../components"
+
+Page {
+    id: page
+    allowedOrientations: Orientation.All
+
+    SilicaFlickable {
+        id: flickable
+        anchors.fill: parent
+        contentHeight: column.height
+        VerticalScrollDecorator { flickable: flickable }
+
+        Column {
+            id: column
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.leftMargin: Theme.paddingLarge
+            anchors.rightMargin: Theme.paddingLarge
+
+            PageHeader { title: qsTr("Settings") }
+
+            TextSwitch {
+                id: showDirsFirst
+                text: qsTr("Show folders first")
+            }
+            TextSwitch {
+                id: showHiddenFiles
+                text: qsTr("Show hidden files")
+            }
+
+            Spacer { height: 40 }
+
+            Label {
+                text: qsTr("About File Browser")
+                anchors.left: parent.left
+                anchors.right: parent.right
+                anchors.rightMargin: Theme.paddingLarge
+                horizontalAlignment: Text.AlignRight
+                color: Theme.highlightColor
+            }
+            Spacer { height: 20 }
+            Row {
+                anchors.left: parent.left
+                anchors.right: parent.right
+                anchors.leftMargin: Theme.paddingLarge
+                anchors.rightMargin: Theme.paddingLarge
+                Label {
+                    id: version
+                    text: qsTr("Version")+" "
+                    font.pixelSize: Theme.fontSizeExtraSmall
+                    color: Theme.secondaryColor
+                }
+                Label {
+                    text: "1.4.1" // Version number must be changed manually!
+                    font.pixelSize: Theme.fontSizeExtraSmall
+                    color: Theme.highlightColor
+                }
+            }
+            Spacer { height: 20 }
+            Label {
+                anchors.left: parent.left
+                anchors.right: parent.right
+                anchors.leftMargin: Theme.paddingLarge
+                anchors.rightMargin: Theme.paddingLarge
+                text: "File Browser is free and unencumbered software released "+
+                      "into the public domain.\nRead full text >>"
+                wrapMode: Text.Wrap
+                font.pixelSize: Theme.fontSizeExtraSmall
+                color: Theme.primaryColor
+
+                MouseArea {
+                    anchors.fill: parent
+                    onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml"))
+                }
+            }
+
+            Spacer { height: 20 }
+            Label {
+                anchors.left: parent.left
+                anchors.right: parent.right
+                anchors.leftMargin: Theme.paddingLarge
+                anchors.rightMargin: Theme.paddingLarge
+                text: qsTr("The source code is available at\nhttps://github.com/karip/harbour-file-browser")
+                wrapMode: Text.Wrap
+                font.pixelSize: Theme.fontSizeTiny
+                color: Theme.secondaryColor
+            }
+        }
+    }
+
+    onStatusChanged: {
+        // update cover
+        if (status === PageStatus.Activating)
+            coverPlaceholder.text = qsTr("Settings");
+
+        // read settings
+        if (status === PageStatus.Activating) {
+            showDirsFirst.checked = (engine.readSetting("show-dirs-first") === "true");
+            showHiddenFiles.checked = (engine.readSetting("show-hidden-files") === "true");
+        }
+
+        // write settings
+        if (status === PageStatus.Deactivating) {
+            engine.writeSetting("show-dirs-first", showDirsFirst.checked.toString());
+            engine.writeSetting("show-hidden-files", showHiddenFiles.checked.toString());
+        }
+    }
+}
+
+
diff --git a/qml/filebrowse/pages/ViewPage.qml b/qml/filebrowse/pages/ViewPage.qml
new file mode 100644 (file)
index 0000000..e0b945a
--- /dev/null
@@ -0,0 +1,80 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import "functions.js" as Functions
+import "../components"
+
+Page {
+    id: page
+    allowedOrientations: Orientation.All
+    property string path: ""
+
+    SilicaFlickable {
+        id: flickable
+        anchors.fill: parent
+        contentHeight: column.height
+        VerticalScrollDecorator { flickable: flickable }
+
+        Column {
+            id: column
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.leftMargin: Theme.paddingLarge
+            anchors.rightMargin: Theme.paddingLarge
+
+            PageHeader { title: Functions.lastPartOfPath(page.path) }
+
+            Label {
+                id: portraitText
+                textFormat: Text.PlainText
+                width: parent.width
+                wrapMode: Text.WrapAnywhere
+                font.pixelSize: Theme.fontSizeTiny
+                font.family: "Monospace"
+                color: Theme.highlightColor
+                visible: page.orientation === Orientation.Portrait
+            }
+            Label {
+                id: landscapeText
+                textFormat: Text.PlainText
+                width: parent.width
+                wrapMode: Text.WrapAnywhere
+                font.pixelSize: Theme.fontSizeTiny
+                font.family: "Monospace"
+                color: Theme.highlightColor
+                visible: page.orientation === Orientation.Landscape
+            }
+            Spacer {
+                height: 40
+                visible: message.text !== ""
+            }
+            Label {
+                id: message
+                width: parent.width
+                wrapMode: Text.Wrap
+                // show medium size if there is no portrait (or landscape text)
+                // in that case, this message becomes main message
+                font.pixelSize: portraitText.text === "" ? Theme.fontSizeMedium : Theme.fontSizeTiny
+                color: portraitText.text === "" ? Theme.highlightColor : Theme.secondaryColor
+                horizontalAlignment: Text.AlignHCenter
+                visible: message.text !== ""
+            }
+            Spacer {
+                height: 40
+                visible: message.text !== ""
+            }
+        }
+    }
+
+    // update cover
+    onStatusChanged: {
+        if (status === PageStatus.Activating) {
+            // reading file returns three texts, message, portrait and landscape texts
+            var txts = engine.readFile(page.path);
+            message.text = txts[0];
+            portraitText.text = txts[1];
+            landscapeText.text = txts[2];
+        }
+    }
+}
+
+
diff --git a/qml/filebrowse/pages/functions.js b/qml/filebrowse/pages/functions.js
new file mode 100644 (file)
index 0000000..932ef7c
--- /dev/null
@@ -0,0 +1,182 @@
+
+// Go to root using the optional operationType parameter
+// @param operationType PageStackAction.Immediate or Animated, Animated is default)
+function goToRoot(operationType)
+{
+    if (operationType !== PageStackAction.Immediate &&
+            operationType !== PageStackAction.Animated)
+        operationType = PageStackAction.Animated;
+
+    // find the first page
+    var firstPage = pageStack.previousPage();
+    if (!firstPage)
+        return;
+    while (pageStack.previousPage(firstPage)) {
+        firstPage = pageStack.previousPage(firstPage);
+    }
+
+    var start = engine.startDepth();
+    for (var up = 0; up < start; up++) {
+        firstPage = pageStack.nextPage(firstPage);
+    }
+
+    // pop to first page
+    pageStack.pop(firstPage, operationType);
+}
+
+function cancel()
+{
+    // find the first page
+    var firstPage = pageStack.previousPage();
+    if (!firstPage)
+        return;
+    while (pageStack.previousPage(firstPage)) {
+        firstPage = pageStack.previousPage(firstPage);
+    }
+
+    var start = engine.startDepth() - 1;
+    for (var up = 0; up < start; up++) {
+        firstPage = pageStack.nextPage(firstPage);
+    }
+
+    // pop to first page
+    pageStack.pop(firstPage, PageStackAction.Animated);
+}
+
+function fileSelect(fullPath)
+{
+    // set the selected file
+    engine.setSelectedFilename(fullPath);
+
+    // find the first page
+    var firstPage = pageStack.previousPage();
+    if (!firstPage)
+        return;
+    while (pageStack.previousPage(firstPage)) {
+        firstPage = pageStack.previousPage(firstPage);
+    }
+
+    var start = engine.startDepth() - 1;
+    for (var up = 0; up < start; up++) {
+        firstPage = pageStack.nextPage(firstPage);
+    }
+
+    // pop to first page
+    pageStack.pop(firstPage, PageStackAction.Animated);
+}
+
+// returns true if string s1 starts with string s2
+function startsWith(s1, s2)
+{
+    if (!s1 || !s2)
+        return false;
+
+    var start = s1.substring(0, s2.length);
+    return start === s2;
+}
+
+function goToFolder(folder)
+{
+    // first, go to root so that the page stack has only one page
+    goToRoot(PageStackAction.Immediate);
+
+    // open the folders one by one
+    var dirs = folder.split("/");
+    var path = "";
+    for (var i = 1; i < dirs.length; ++i) {
+        path += "/"+dirs[i];
+        // animate the last push
+        var action = (i < dirs.length-1) ? PageStackAction.Immediate : PageStackAction.Animated;
+        pageStack.push(Qt.resolvedUrl("DirectoryPage.qml"), { dir: path }, action);
+    }
+}
+
+// Goes to Home folder
+function goToHome()
+{
+    goToFolder(engine.homeFolder());
+}
+
+function goToInitial(folder, filter)
+{
+    engine.extensionFilter = filter;
+
+    // open the folders one by one
+    var dirs = folder.split("/");
+    var path = "";
+    for (var i = 0; i < dirs.length; ++i) {
+        path += "/"+dirs[i];
+        // animate the last push
+        var action = (i < dirs.length-1) ? PageStackAction.Immediate : PageStackAction.Animated;
+        pageStack.push(Qt.resolvedUrl("DirectoryPage.qml"), { dir: path }, action);
+    }
+}
+
+function sdcardPath()
+{
+    return "/run/user/100000/media/sdcard";
+}
+
+function androidSdcardPath()
+{
+    return "/data/sdcard";
+}
+
+function formatPathForTitle(path)
+{
+    if (path === "/")
+        return "File Browser: /";
+
+    var i = path.lastIndexOf("/");
+    if (i < -1)
+        return path;
+
+    return path.substring(i+1)+"/";
+}
+
+// returns the text after the last / in a path
+function lastPartOfPath(path)
+{
+    if (path === "/")
+        return "";
+
+    var i = path.lastIndexOf("/");
+    if (i < -1)
+        return path;
+
+    return path.substring(i+1);
+}
+
+function formatPathForSearch(path)
+{
+    if (path === "/")
+        return "root";
+
+    var i = path.lastIndexOf("/");
+    if (i < -1)
+        return path;
+
+    return path.substring(i+1);
+}
+
+function unicodeArrow()
+{
+    return "\u2192"; // unicode for arrow symbol
+}
+
+function unicodeBlackDownPointingTriangle()
+{
+    return "\u25bc"; // unicode for down pointing triangle symbol
+}
+
+function folderFromFile(path)
+{
+    if ((path === "Select") || (path === ""))
+        path = engine.homeFolder() + "/.";
+
+    var i = path.lastIndexOf("/");
+    if ((i < 0) || (i >= (path.length - 1)))
+        return path;
+
+    return path.substring(0, i);
+}
index 1692d1e..1615c97 100644 (file)
 
 import QtQuick 2.0
 import Sailfish.Silica 1.0
 
 import QtQuick 2.0
 import Sailfish.Silica 1.0
+import "../components"
+import "../filebrowse/pages/functions.js" as Functions
 
 Dialog {
     id: configurePage
     canAccept: true
     acceptDestinationAction: PageStackAction.Pop
 
 Dialog {
     id: configurePage
     canAccept: true
     acceptDestinationAction: PageStackAction.Pop
+    property int _fileDialogue: 0
 
     Connections {
         target:VpnControl
     }
 
 
     Connections {
         target:VpnControl
     }
 
+    // connect signals from engine to panels
+    Connections {
+        target: engine
+        onSelectedFilenameChanged: {
+            switch (_fileDialogue) {
+                case 1:
+                    caCertFilename.value = engine.selectedFilename
+                    break;
+                case 2:
+                    clientCertFilename.value = engine.selectedFilename
+                    break;
+                case 3:
+                    clientKeyFilename.value = engine.selectedFilename
+                    break;
+                case 4:
+                    tlsKeyFilename.value = engine.selectedFilename
+                    break;
+            }
+            _fileDialogue = 0;
+        }
+    }
+
+
     SilicaFlickable {
         // ComboBox requires a flickable ancestor
         width: parent.width
     SilicaFlickable {
         // ComboBox requires a flickable ancestor
         width: parent.width
@@ -95,10 +121,6 @@ Dialog {
                 id: configureTLS
                 text: "Use TLS authentication"
                 checked: VpnControl.useTLS
                 id: configureTLS
                 text: "Use TLS authentication"
                 checked: VpnControl.useTLS
-                onCheckedChanged: {
-                    configureTLSdirection.enabled = checked
-                    configureTLSinfo.visible = checked
-                }
                 automaticCheck: true
             }
             // set currentIndex to change the selected value
                 automaticCheck: true
             }
             // set currentIndex to change the selected value
@@ -107,7 +129,7 @@ Dialog {
                 width: parent.width
                 label: "TLS direction"
                 currentIndex: VpnControl.tlsDirection;
                 width: parent.width
                 label: "TLS direction"
                 currentIndex: VpnControl.tlsDirection;
-                enabled: false
+                enabled: configureTLS.checked
 
                 menu: ContextMenu {
                     MenuItem { text: "0" }
 
                 menu: ContextMenu {
                     MenuItem { text: "0" }
@@ -115,46 +137,50 @@ Dialog {
                 }
             }
 
                 }
             }
 
-            Button {
-                id: connect
-                text: "Select key"
-                enabled: true
-                onClicked: VpnControl.vpnConnect()
+            ValueButtonAlignRight {
+                id: caCertFilename
+                label: "CA cert"
+                value: "Select"
+                width: parent.width
+                onClicked: {
+                    _fileDialogue = 1
+                    Functions.goToInitial(Functions.folderFromFile(value), "crt")
+                }
             }
 
             }
 
-            Label {
-                text: "Place key files on SD card:"
-                color: Theme.secondaryColor
-                font.pixelSize: Theme.fontSizeSmall
-                x: Theme.paddingLarge
-            }
-            Label {
-                text: "\tca.crt"
-                color: Theme.secondaryColor
-                font.pixelSize: Theme.fontSizeSmall
-                x: Theme.paddingLarge
-            }
-            Label {
-                text: "\tclient.crt"
-                color: Theme.secondaryColor
-                font.pixelSize: Theme.fontSizeSmall
-                x: Theme.paddingLarge
-            }
-            Label {
-                text: "\tclient.key"
-                color: Theme.secondaryColor
-                font.pixelSize: Theme.fontSizeSmall
-                x: Theme.paddingLarge
+            ValueButtonAlignRight {
+                id: clientCertFilename
+                value: "Select"
+                label: "Client cert"
+                width: parent.width
+                onClicked: {
+                    _fileDialogue = 2;
+                    Functions.goToInitial(Functions.folderFromFile(value), "crt")
+                }
             }
             }
-            Label {
-                id: configureTLSinfo
-                visible: false
-                text: "\tta.key"
-                color: Theme.secondaryColor
-                font.pixelSize: Theme.fontSizeSmall
-                x: Theme.paddingLarge
+
+            ValueButtonAlignRight {
+                id: clientKeyFilename
+                value: "Select"
+                label: "Client key"
+                width: parent.width
+                onClicked: {
+                    _fileDialogue = 3;
+                    Functions.goToInitial(Functions.folderFromFile(value), "key")
+                }
             }
 
             }
 
+            ValueButtonAlignRight {
+                id: tlsKeyFilename
+                value: "Select"
+                label: "TLS key"
+                width: parent.width
+                enabled: configureTLS.checked
+                onClicked: {
+                    _fileDialogue = 4;
+                    Functions.goToInitial(Functions.folderFromFile(value), "key")
+                }
+            }
         }
     }
 
         }
     }
 
index 23bd65b..6d32705 100644 (file)
@@ -148,6 +148,34 @@ Page {
                     anchors.verticalCenter: parent.verticalCenter
                 }
             }
                     anchors.verticalCenter: parent.verticalCenter
                 }
             }
+
+            Rectangle {
+                color: "transparent"
+                border {
+                    color: Theme.highlightBackgroundColor
+                    width: 1
+                }
+                //radius: Theme.paddingSmall
+                anchors.horizontalCenter: parent.horizontalCenter
+                height: (20 * Theme.fontSizeTiny) + (2 * Theme.paddingLarge)
+                width: parent.width - 2 * Theme.paddingLarge
+                x: Theme.paddingLarge
+
+                Label {
+                    id: logOutput
+                    textFormat: Text.PlainText
+                    width: parent.width - 2 * Theme.paddingSmall
+                    wrapMode: Text.WrapAnywhere
+                    font.pixelSize: Theme.fontSizeTiny * 0.6
+                    font.family: "Monospace"
+                    color: Theme.highlightColor
+                    visible: true
+                    text: VpnControl.logText
+                    maximumLineCount: Math.floor(18 / 0.6)
+                    x: Theme.paddingSmall
+                    y: Theme.paddingSmall
+                }
+            }
         }
     }
 }
         }
     }
 }
index 5ee1235..93b06cf 100644 (file)
@@ -21,10 +21,10 @@ URL:        http://example.org/
 Source0:    %{name}-%{version}.tar.bz2
 Source100:  OpenVPNUI.yaml
 Requires:   sailfishsilica-qt5 >= 0.10.9
 Source0:    %{name}-%{version}.tar.bz2
 Source100:  OpenVPNUI.yaml
 Requires:   sailfishsilica-qt5 >= 0.10.9
-BuildRequires:  pkgconfig(Qt5Quick)
-BuildRequires:  pkgconfig(Qt5Qml)
-BuildRequires:  pkgconfig(Qt5Core)
 BuildRequires:  pkgconfig(sailfishapp) >= 0.0.10
 BuildRequires:  pkgconfig(sailfishapp) >= 0.0.10
+BuildRequires:  pkgconfig(Qt5Core)
+BuildRequires:  pkgconfig(Qt5Qml)
+BuildRequires:  pkgconfig(Qt5Quick)
 BuildRequires:  desktop-file-utils
 
 %description
 BuildRequires:  desktop-file-utils
 
 %description
@@ -63,13 +63,13 @@ desktop-file-install --delete-original       \
 
 %files
 %defattr(-,root,root,-)
 
 %files
 %defattr(-,root,root,-)
-%{_bindir}
-%{_datadir}/%{name}/qml
-%{_datadir}/applications/%{name}.desktop
-%{_datadir}/icons/hicolor/86x86/apps/%{name}.png
-/usr/bin
-/usr/share/OpenVPNUI
-/usr/share/applications
 /usr/share/icons/hicolor/86x86/apps
 /usr/share/icons/hicolor/86x86/apps
+/usr/share/applications
+/usr/share/OpenVPNUI
+/usr/bin
+%{_datadir}/icons/hicolor/86x86/apps/%{name}.png
+%{_datadir}/applications/%{name}.desktop
+%{_datadir}/%{name}/qml
+%{_bindir}
 # >> files
 # << files
 # >> files
 # << files
index daa3264..6c5cd7b 100644 (file)
@@ -12,19 +12,19 @@ Description: |
 Configure: none
 Builder: qtc5
 PkgConfigBR:
 Configure: none
 Builder: qtc5
 PkgConfigBR:
-- Qt5Quick
-- Qt5Qml
-- Qt5Core
 - sailfishapp >= 0.0.10
 - sailfishapp >= 0.0.10
+- Qt5Core
+- Qt5Qml
+- Qt5Quick
 Requires:
 - sailfishsilica-qt5 >= 0.10.9
 Files:
 Requires:
 - sailfishsilica-qt5 >= 0.10.9
 Files:
-- '%{_bindir}'
-- '%{_datadir}/%{name}/qml'
-- '%{_datadir}/applications/%{name}.desktop'
-- '%{_datadir}/icons/hicolor/86x86/apps/%{name}.png'
-- /usr/bin
-- /usr/share/OpenVPNUI
-- /usr/share/applications
 - /usr/share/icons/hicolor/86x86/apps
 - /usr/share/icons/hicolor/86x86/apps
+- /usr/share/applications
+- /usr/share/OpenVPNUI
+- /usr/bin
+- '%{_datadir}/icons/hicolor/86x86/apps/%{name}.png'
+- '%{_datadir}/applications/%{name}.desktop'
+- '%{_datadir}/%{name}/qml'
+- '%{_bindir}'
 PkgBR: []
 PkgBR: []
index 40e28ef..2df2800 100644 (file)
 
 #include <sailfishapp.h>
 #include "vpncontrol.h"
 
 #include <sailfishapp.h>
 #include "vpncontrol.h"
+#include "filemodel.h"
+#include "fileinfo.h"
+#include "searchengine.h"
+#include "engine.h"
 
 int main(int argc, char *argv[])
 {
 
 int main(int argc, char *argv[])
 {
+    qmlRegisterType<FileModel>("harbour.file.browser.FileModel", 1, 0, "FileModel");
+    qmlRegisterType<FileInfo>("harbour.file.browser.FileInfo", 1, 0, "FileInfo");
+    qmlRegisterType<SearchEngine>("harbour.file.browser.SearchEngine", 1, 0, "SearchEngine");
+
     int result;
 
     setuid(0);
     int result;
 
     setuid(0);
@@ -72,12 +80,16 @@ int main(int argc, char *argv[])
     view->rootContext()->setContextProperty("VpnControl", vpnControl);
 
     vpnControl->initialise();
     view->rootContext()->setContextProperty("VpnControl", vpnControl);
 
     vpnControl->initialise();
-    //QObject * page = view->findChild(QString("page"),Qt::FindChildrenRecursively);
 
 
-    //QObject::connect(vpnControl, SIGNAL(statusChanged(int)), page, SLOT(updateStatus(int)));
+    // QML global engine object
+    QScopedPointer<Engine> engine(new Engine);
+    view->rootContext()->setContextProperty("engine", engine.data());
+    // Store pointer to engine to access it in any class
+    QVariant engineVariant = qVariantFromValue(engine.data());
+    qApp->setProperty("engine", engineVariant);
 
 
+    // Run the application loop
     view->show();
     view->show();
-
     result = app->exec();
 
     delete vpnControl;
     result = app->exec();
 
     delete vpnControl;
diff --git a/src/filebrowse/engine.cpp b/src/filebrowse/engine.cpp
new file mode 100644 (file)
index 0000000..b5418e1
--- /dev/null
@@ -0,0 +1,353 @@
+#include "engine.h"
+#include <QDateTime>
+#include <QTextStream>
+#include <QSettings>
+#include <QStandardPaths>
+#include "globals.h"
+#include "fileworker.h"
+
+Engine::Engine(QObject *parent) :
+    QObject(parent),
+    m_clipboardContainsCopy(false),
+    m_progress(0),
+    m_selectedFilename(""),
+    m_extensionFilter("")
+{
+    m_fileWorker = new FileWorker;
+
+    // update progress property when worker progresses
+    connect(m_fileWorker, SIGNAL(progressChanged(int, QString)),
+            this, SLOT(setProgress(int, QString)));
+
+    // pass worker end signals to QML
+    connect(m_fileWorker, SIGNAL(done()), this, SIGNAL(workerDone()));
+    connect(m_fileWorker, SIGNAL(errorOccurred(QString, QString)),
+            this, SIGNAL(workerErrorOccurred(QString, QString)));
+    connect(m_fileWorker, SIGNAL(fileDeleted(QString)), this, SIGNAL(fileDeleted(QString)));
+}
+
+Engine::~Engine()
+{
+    // is this the way to force stop the worker thread?
+    m_fileWorker->cancel(); // stop possibly running background thread
+    m_fileWorker->wait();   // wait until thread stops
+    delete m_fileWorker;    // delete it
+}
+
+void Engine::deleteFiles(QStringList filenames)
+{
+    setProgress(0, "");
+    m_fileWorker->startDeleteFiles(filenames);
+}
+
+void Engine::cutFiles(QStringList filenames)
+{
+    m_clipboardFiles = filenames;
+    m_clipboardContainsCopy = false;
+    emit clipboardCountChanged();
+    emit clipboardContainsCopyChanged();
+}
+
+void Engine::copyFiles(QStringList filenames)
+{
+    m_clipboardFiles = filenames;
+    m_clipboardContainsCopy = true;
+    emit clipboardCountChanged();
+    emit clipboardContainsCopyChanged();
+}
+
+void Engine::pasteFiles(QString destDirectory)
+{
+    if (m_clipboardFiles.isEmpty()) {
+        emit workerErrorOccurred("No files to paste", "");
+        return;
+    }
+
+    QStringList files = m_clipboardFiles;
+    setProgress(0, "");
+
+    QDir dest(destDirectory);
+    if (!dest.exists()) {
+        emit workerErrorOccurred(tr("Destination does not exist"), destDirectory);
+        return;
+    }
+
+    foreach (QString filename, files) {
+        QFileInfo fileInfo(filename);
+        QString newname = dest.absoluteFilePath(fileInfo.fileName());
+
+        // source and dest filenames are the same?
+        if (filename == newname) {
+            emit workerErrorOccurred(tr("Can't overwrite itself"), newname);
+            return;
+        }
+
+        // dest is under source? (directory)
+        if (newname.startsWith(filename)) {
+            emit workerErrorOccurred(tr("Can't move/copy to itself"), filename);
+            return;
+        }
+    }
+
+    m_clipboardFiles.clear();
+    emit clipboardCountChanged();
+
+    if (m_clipboardContainsCopy) {
+        m_fileWorker->startCopyFiles(files, destDirectory);
+        return;
+    }
+
+    m_fileWorker->startMoveFiles(files, destDirectory);
+}
+
+void Engine::cancel()
+{
+    m_fileWorker->cancel();
+}
+
+QString Engine::homeFolder() const
+{
+    return QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
+}
+
+int Engine::startDepth() const
+{
+    return 2;
+}
+
+bool Engine::exists(QString filename)
+{
+    return QFile::exists(filename);
+}
+
+QStringList Engine::diskSpace(QString path)
+{
+    // run df to get disk space
+    QString blockSize = "--block-size=1024";
+    QString result = execute("/bin/df", QStringList() << blockSize << path, false);
+    if (result.isEmpty())
+        return QStringList();
+
+    // parse result
+    QStringList lines = result.split(QRegExp("[\n\r]"));
+    if (lines.count() < 2)
+        return QStringList();
+
+    QString line = lines.at(1);
+    QStringList columns = line.split(QRegExp("\\s+"), QString::SkipEmptyParts);
+    if (columns.count() < 5)
+        return QStringList();
+
+    QString totalString = columns.at(1);
+    QString usedString = columns.at(2);
+    QString percentageString = columns.at(4);
+    qint64 total = totalString.toLongLong() * 1024LL;
+    qint64 used = usedString.toLongLong() * 1024LL;
+
+    return QStringList() << percentageString << filesizeToString(used)+"/"+filesizeToString(total);
+}
+
+QStringList Engine::readFile(QString filename)
+{
+    int maxLines = 1000;
+    int maxSize = 10000;
+    int maxBinSize = 2048;
+
+    // check permissions
+    if (access(filename, R_OK) == -1)
+        return makeStringList(tr("No permission to read the file\n%1").arg(filename));
+
+    QFile file(filename);
+    if (!file.open(QIODevice::ReadOnly))
+        return makeStringList(tr("Error reading file\n%1").arg(filename));
+
+    // read start of file
+    char buffer[maxSize+1];
+    qint64 readSize = file.read(buffer, maxSize);
+    if (readSize < 0)
+        return makeStringList(tr("Error reading file\n%1").arg(filename));
+
+    if (readSize == 0)
+        return makeStringList(tr("Empty file"));
+
+    bool atEnd = file.atEnd();
+    file.close();
+
+    // detect binary or text file, it is binary if it contains zeros
+    bool isText = true;
+    for (int i = 0; i < readSize; ++i) {
+        if (buffer[i] == 0) {
+            isText = false;
+            break;
+        }
+    }
+
+    // binary output
+    if (!isText) {
+        // two different line widths
+        if (readSize > maxBinSize) {
+            readSize = maxBinSize;
+            atEnd = false;
+        }
+        QString out8 = createHexDump(buffer, readSize, 8);
+        QString out16 = createHexDump(buffer, readSize, 16);
+        QString msg = "";
+
+        if (!atEnd) {
+            msg = tr("--- Binary file preview clipped at %1 kB ---").arg(maxBinSize/1000);
+            msg = tr("--- Binary file preview clipped at %1 kB ---").arg(maxBinSize/1000);
+        }
+
+        return QStringList() << msg << out8 << out16;
+    }
+
+    // read lines to a string list and join
+    QByteArray ba(buffer, readSize);
+    QTextStream in(&ba);
+    QStringList lines;
+    int lineCount = 0;
+    while (!in.atEnd() && lineCount < maxLines) {
+        QString line = in.readLine();
+        lines.append(line);
+        lineCount++;
+    }
+
+    QString msg = "";
+    if (lineCount == maxLines)
+        msg = tr("--- Text file preview clipped at %1 lines ---").arg(maxLines);
+    else if (!atEnd)
+        msg = tr("--- Text file preview clipped at %1 kB ---").arg(maxSize/1000);
+
+    return makeStringList(msg, lines.join("\n"));
+}
+
+QString Engine::mkdir(QString path, QString name)
+{
+    QDir dir(path);
+
+    if (!dir.mkdir(name)) {
+        if (access(dir.absolutePath(), W_OK) == -1)
+            return tr("Cannot create folder %1\nPermission denied").arg(name);
+
+        return tr("Cannot create folder %1").arg(name);
+    }
+
+    return QString();
+}
+
+QStringList Engine::rename(QString fullOldFilename, QString newName)
+{
+    QFile file(fullOldFilename);
+    QFileInfo fileInfo(fullOldFilename);
+    QDir dir = fileInfo.absoluteDir();
+    QString fullNewFilename = dir.absoluteFilePath(newName);
+
+    QString errorMessage;
+    if (!file.rename(fullNewFilename)) {
+        QString oldName = fileInfo.fileName();
+        errorMessage = tr("Cannot rename %1\n%2").arg(oldName).arg(file.errorString());
+    }
+
+    return QStringList() << fullNewFilename << errorMessage;
+}
+
+QString Engine::chmod(QString path,
+                      bool ownerRead, bool ownerWrite, bool ownerExecute,
+                      bool groupRead, bool groupWrite, bool groupExecute,
+                      bool othersRead, bool othersWrite, bool othersExecute)
+{
+    QFile file(path);
+    QFileDevice::Permissions p;
+    if (ownerRead) p |= QFileDevice::ReadOwner;
+    if (ownerWrite) p |= QFileDevice::WriteOwner;
+    if (ownerExecute) p |= QFileDevice::ExeOwner;
+    if (groupRead) p |= QFileDevice::ReadGroup;
+    if (groupWrite) p |= QFileDevice::WriteGroup;
+    if (groupExecute) p |= QFileDevice::ExeGroup;
+    if (othersRead) p |= QFileDevice::ReadOther;
+    if (othersWrite) p |= QFileDevice::WriteOther;
+    if (othersExecute) p |= QFileDevice::ExeOther;
+    if (!file.setPermissions(p))
+        return tr("Cannot change permissions\n%1").arg(file.errorString());
+
+    return QString();
+}
+
+QString Engine::readSetting(QString key, QString defaultValue)
+{
+    QSettings settings;
+    return settings.value(key, defaultValue).toString();
+}
+
+void Engine::writeSetting(QString key, QString value)
+{
+    QSettings settings;
+
+    // do nothing if value didn't change
+    if (settings.value(key) == value)
+        return;
+
+    settings.setValue(key, value);
+
+    emit settingsChanged();
+}
+
+void Engine::setProgress(int progress, QString filename)
+{
+    m_progress = progress;
+    m_progressFilename = filename;
+    emit progressChanged();
+    emit progressFilenameChanged();
+}
+
+QString Engine::createHexDump(char *buffer, int size, int bytesPerLine)
+{
+    QString out;
+    QString ascDump;
+    int i;
+    for (i = 0; i < size; ++i) {
+        if ((i % bytesPerLine) == 0) { // line change
+            out += " "+ascDump+"\n"+
+                    QString("%1").arg(QString::number(i, 16), 4, QLatin1Char('0'))+": ";
+            ascDump.clear();
+        }
+
+        out += QString("%1").arg(QString::number((unsigned char)buffer[i], 16),
+                                       2, QLatin1Char('0'))+" ";
+        if (buffer[i] >= 32 && buffer[i] <= 126)
+            ascDump += buffer[i];
+        else
+            ascDump += ".";
+    }
+    // write out remaining asc dump
+    if ((i % bytesPerLine) > 0) {
+        int emptyBytes = bytesPerLine - (i % bytesPerLine);
+        for (int j = 0; j < emptyBytes; ++j) {
+            out += "   ";
+        }
+    }
+    out += " "+ascDump;
+
+    return out;
+}
+
+QStringList Engine::makeStringList(QString msg, QString str)
+{
+    QStringList list;
+    list << msg << str << str;
+    return list;
+}
+
+void Engine::setSelectedFilename(QString selectedFilename)
+{
+    m_selectedFilename = selectedFilename;
+    emit selectedFilenameChanged();
+}
+
+void Engine::setExtensionFilter(QString extensionFilter)
+{
+    if (m_extensionFilter != extensionFilter) {
+        m_extensionFilter = extensionFilter;
+        emit extensionFilterChanged();
+    }
+}
diff --git a/src/filebrowse/engine.h b/src/filebrowse/engine.h
new file mode 100644 (file)
index 0000000..c984958
--- /dev/null
@@ -0,0 +1,98 @@
+#ifndef ENGINE_H
+#define ENGINE_H
+
+#include <QDir>
+#include <QVariant>
+
+class FileWorker;
+
+/**
+ * @brief Engine to handle file operations, settings and other generic functionality.
+ */
+class Engine : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(int clipboardCount READ clipboardCount() NOTIFY clipboardCountChanged())
+    Q_PROPERTY(int clipboardContainsCopy READ clipboardContainsCopy() NOTIFY clipboardContainsCopyChanged())
+    Q_PROPERTY(int progress READ progress() NOTIFY progressChanged())
+    Q_PROPERTY(QString progressFilename READ progressFilename() NOTIFY progressFilenameChanged())
+    Q_PROPERTY(QString selectedFilename READ selectedFilename() WRITE setSelectedFilename() NOTIFY selectedFilenameChanged())
+    Q_PROPERTY(QString extensionFilter READ extensionFilter() WRITE setExtensionFilter() NOTIFY extensionFilterChanged())
+
+public:
+    explicit Engine(QObject *parent = 0);
+    ~Engine();
+
+    int clipboardCount() const { return m_clipboardFiles.count(); }
+    bool clipboardContainsCopy() const { return m_clipboardContainsCopy; }
+    int progress() const { return m_progress; }
+    QString progressFilename() const { return m_progressFilename; }
+    QString selectedFilename() const { return m_selectedFilename; }
+    QString extensionFilter() const { return m_extensionFilter; }
+
+    // methods accessible from QML
+
+    // asynch methods send signals when done or error occurs
+    Q_INVOKABLE void deleteFiles(QStringList filenames);
+    Q_INVOKABLE void cutFiles(QStringList filenames);
+    Q_INVOKABLE void copyFiles(QStringList filenames);
+    Q_INVOKABLE void pasteFiles(QString destDirectory);
+
+    // cancel asynch methods
+    Q_INVOKABLE void cancel();
+
+    // returns error msg
+    Q_INVOKABLE QString errorMessage() const { return m_errorMessage; }
+
+    // sync methods
+    Q_INVOKABLE QString homeFolder() const;
+    Q_INVOKABLE bool exists(QString filename);
+    Q_INVOKABLE QStringList diskSpace(QString path);
+    Q_INVOKABLE QStringList readFile(QString filename);
+    Q_INVOKABLE QString mkdir(QString path, QString name);
+    Q_INVOKABLE QStringList rename(QString fullOldFilename, QString newName);
+    Q_INVOKABLE QString chmod(QString path,
+                              bool ownerRead, bool ownerWrite, bool ownerExecute,
+                              bool groupRead, bool groupWrite, bool groupExecute,
+                              bool othersRead, bool othersWrite, bool othersExecute);
+
+    // access settings
+    Q_INVOKABLE QString readSetting(QString key, QString defaultValue = QString());
+    Q_INVOKABLE void writeSetting(QString key, QString value);
+    Q_INVOKABLE int startDepth() const;
+
+    Q_INVOKABLE void setSelectedFilename(QString selectedFilename);
+    Q_INVOKABLE void setExtensionFilter(QString extensionFilter);
+
+
+signals:
+    void clipboardCountChanged();
+    void clipboardContainsCopyChanged();
+    void progressChanged();
+    void progressFilenameChanged();
+    void selectedFilenameChanged();
+    void extensionFilterChanged();
+    void workerDone();
+    void workerErrorOccurred(QString message, QString filename);
+    void fileDeleted(QString fullname);
+
+    void settingsChanged();
+
+private slots:
+    void setProgress(int progress, QString filename);
+
+private:
+    QString createHexDump(char *buffer, int size, int bytesPerLine);
+    QStringList makeStringList(QString msg, QString str = QString());
+
+    QStringList m_clipboardFiles;
+    bool m_clipboardContainsCopy;
+    int m_progress;
+    QString m_progressFilename;
+    QString m_errorMessage;
+    FileWorker *m_fileWorker;
+    QString m_selectedFilename;
+    QString m_extensionFilter;
+};
+
+#endif // ENGINE_H
diff --git a/src/filebrowse/fileinfo.cpp b/src/filebrowse/fileinfo.cpp
new file mode 100644 (file)
index 0000000..25c4ef8
--- /dev/null
@@ -0,0 +1,183 @@
+#include "fileinfo.h"
+#include <QDir>
+#include <QDateTime>
+#include <QProcess>
+#include "globals.h"
+
+FileInfo::FileInfo(QObject *parent) :
+    QObject(parent)
+{
+    m_file = "";
+}
+
+FileInfo::~FileInfo()
+{
+}
+
+void FileInfo::setFile(QString file)
+{
+    if (m_file == file)
+        return;
+
+    m_file = file;
+    readFile();
+}
+
+bool FileInfo::isDir() const
+{
+    return m_fileInfo.isDir();
+}
+
+QString FileInfo::kind() const
+{
+    if (m_fileInfo.isSymLink()) return "l";
+    if (m_fileInfo.isDir()) return "d";
+    if (m_fileInfo.isFile()) return "-";
+    return "?";
+}
+
+QString FileInfo::icon() const
+{
+    if (m_fileInfo.isSymLink() && m_fileInfo.isDir()) return "folder-link";
+    if (m_fileInfo.isDir()) return "folder";
+    if (m_fileInfo.isSymLink()) return "link";
+    if (m_fileInfo.isFile()) {
+        QString suffix = m_fileInfo.suffix().toLower();
+        return suffixToIconName(suffix);
+    }
+    return "file";
+}
+
+QString FileInfo::permissions() const
+{
+    return permissionsToString(m_fileInfo.permissions());
+}
+
+QString FileInfo::owner() const
+{
+    QString owner = m_fileInfo.owner();
+    if (owner.isEmpty())
+        owner = QString::number(m_fileInfo.ownerId());
+    return owner;
+}
+
+QString FileInfo::group() const
+{
+    QString group = m_fileInfo.group();
+    if (group.isEmpty())
+        group = QString::number(m_fileInfo.groupId());
+    return group;
+}
+
+QString FileInfo::size() const
+{
+    if (m_fileInfo.isDir()) return "-";
+    return filesizeToString(m_fileInfo.size());
+}
+
+QString FileInfo::modified() const
+{
+    return datetimeToString(m_fileInfo.lastModified());
+}
+
+QString FileInfo::created() const
+{
+    return datetimeToString(m_fileInfo.created());
+}
+
+QString FileInfo::absolutePath() const
+{
+    return m_fileInfo.absolutePath();
+}
+
+QString FileInfo::name() const
+{
+    return m_fileInfo.fileName();
+}
+
+QString FileInfo::suffix() const
+{
+    return m_fileInfo.suffix().toLower();
+}
+
+QString FileInfo::symLinkTarget() const
+{
+    return m_fileInfo.symLinkTarget();
+}
+
+QString FileInfo::errorMessage() const
+{
+    return m_errorMessage;
+}
+
+QString FileInfo::processOutput() const
+{
+    return m_processOutput;
+}
+
+void FileInfo::refresh()
+{
+    readFile();
+}
+
+void FileInfo::executeCommand(QString command, QStringList arguments)
+{
+    m_processOutput.clear();
+    emit processOutputChanged();
+
+    // process is killed when Page is closed - should run this in bg thread to allow command finish(?)
+    m_process = new QProcess(this);
+    m_process->setReadChannel(QProcess::StandardOutput);
+    m_process->setProcessChannelMode(QProcess::MergedChannels); // merged stderr channel with stdout channel
+    connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readProcessChannels()));
+    connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(handleProcessFinish(int, QProcess::ExitStatus)));
+    connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(handleProcessError(QProcess::ProcessError)));
+    m_process->start(command, arguments);
+}
+
+void FileInfo::readProcessChannels()
+{
+    while (m_process->canReadLine()) {
+        QString line = m_process->readLine();
+        m_processOutput += line;
+    }
+    emit processOutputChanged();
+}
+
+void FileInfo::handleProcessFinish(int exitCode, QProcess::ExitStatus status)
+{
+    if (status == QProcess::CrashExit) // if it crashed, then use some error exit code
+        exitCode = -99999;
+    emit processExited(exitCode);
+}
+
+void FileInfo::handleProcessError(QProcess::ProcessError error)
+{
+    Q_UNUSED(error);
+    emit processExited(-88888); // if error, then use some error exit code
+}
+
+void FileInfo::readFile()
+{
+    m_errorMessage = "";
+
+    m_fileInfo = QFileInfo(m_file);
+    if (!m_fileInfo.exists())
+        m_errorMessage = tr("File does not exist");
+
+    emit fileChanged();
+    emit isDirChanged();
+    emit kindChanged();
+    emit iconChanged();
+    emit permissionsChanged();
+    emit ownerChanged();
+    emit groupChanged();
+    emit sizeChanged();
+    emit modifiedChanged();
+    emit createdChanged();
+    emit absolutePathChanged();
+    emit nameChanged();
+    emit suffixChanged();
+    emit symLinkTargetChanged();
+    emit errorMessageChanged();
+}
diff --git a/src/filebrowse/fileinfo.h b/src/filebrowse/fileinfo.h
new file mode 100644 (file)
index 0000000..9b8337a
--- /dev/null
@@ -0,0 +1,95 @@
+#ifndef FILEINFO_H
+#define FILEINFO_H
+
+#include <QObject>
+#include <QDir>
+#include <QProcess>
+#include <QVariantList>
+
+/**
+ * @brief The FileInfo class provides access to one file.
+ */
+class FileInfo : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString file READ file() WRITE setFile(QString) NOTIFY fileChanged())
+    Q_PROPERTY(bool isDir READ isDir() NOTIFY isDirChanged())
+    Q_PROPERTY(QString kind READ kind() NOTIFY kindChanged())
+    Q_PROPERTY(QString icon READ icon() NOTIFY iconChanged())
+    Q_PROPERTY(QString permissions READ permissions() NOTIFY permissionsChanged())
+    Q_PROPERTY(QString owner READ owner() NOTIFY ownerChanged())
+    Q_PROPERTY(QString group READ group() NOTIFY groupChanged())
+    Q_PROPERTY(QString size READ size() NOTIFY sizeChanged())
+    Q_PROPERTY(QString modified READ modified() NOTIFY modifiedChanged())
+    Q_PROPERTY(QString created READ created() NOTIFY createdChanged())
+    Q_PROPERTY(QString absolutePath READ absolutePath() NOTIFY absolutePathChanged())
+    Q_PROPERTY(QString name READ name() NOTIFY nameChanged())
+    Q_PROPERTY(QString suffix READ suffix() NOTIFY suffixChanged())
+    Q_PROPERTY(QString symLinkTarget READ symLinkTarget() NOTIFY symLinkTargetChanged())
+    Q_PROPERTY(QString errorMessage READ errorMessage() NOTIFY errorMessageChanged())
+    Q_PROPERTY(QString processOutput READ processOutput() NOTIFY processOutputChanged())
+
+public:
+    explicit FileInfo(QObject *parent = 0);
+    ~FileInfo();
+
+    // property accessors
+    QString file() const { return m_file; }
+    void setFile(QString file);
+
+    bool isDir() const;
+    QString kind() const;
+    QString icon() const;
+    QString permissions() const;
+    QString owner() const;
+    QString group() const;
+    QString size() const;
+    QString modified() const;
+    QString created() const;
+    QString absolutePath() const;
+    QString name() const;
+    QString suffix() const;
+    QString symLinkTarget() const;
+    QString errorMessage() const;
+    QString processOutput() const;
+
+    // methods accessible from QML
+    Q_INVOKABLE void refresh();
+    Q_INVOKABLE void executeCommand(QString command, QStringList arguments);
+
+signals:
+    void fileChanged();
+    void isDirChanged();
+    void kindChanged();
+    void iconChanged();
+    void permissionsChanged();
+    void ownerChanged();
+    void groupChanged();
+    void sizeChanged();
+    void modifiedChanged();
+    void createdChanged();
+    void nameChanged();
+    void suffixChanged();
+    void absolutePathChanged();
+    void symLinkTargetChanged();
+    void errorMessageChanged();
+
+    void processOutputChanged();
+    void processExited(int exitCode);
+
+private slots:
+    void readProcessChannels();
+    void handleProcessFinish(int exitCode, QProcess::ExitStatus status);
+    void handleProcessError(QProcess::ProcessError error);
+
+private:
+    void readFile();
+
+    QString m_file;
+    QFileInfo m_fileInfo;
+    QString m_errorMessage;
+    QProcess *m_process;
+    QString m_processOutput;
+};
+
+#endif // FILEINFO_H
diff --git a/src/filebrowse/filemodel.cpp b/src/filebrowse/filemodel.cpp
new file mode 100644 (file)
index 0000000..562f29e
--- /dev/null
@@ -0,0 +1,337 @@
+#include "filemodel.h"
+#include <QDateTime>
+#include <QSettings>
+#include <QGuiApplication>
+#include "engine.h"
+#include "globals.h"
+
+enum {
+    FilenameRole = Qt::UserRole + 1,
+    FileKindRole = Qt::UserRole + 2,
+    FileIconRole = Qt::UserRole + 3,
+    PermissionsRole = Qt::UserRole + 4,
+    SizeRole = Qt::UserRole + 5,
+    LastModifiedRole = Qt::UserRole + 6,
+    CreatedRole = Qt::UserRole + 7,
+    IsDirRole = Qt::UserRole + 8,
+    IsLinkRole = Qt::UserRole + 9,
+    SymLinkTargetRole = Qt::UserRole + 10
+};
+
+FileModel::FileModel(QObject *parent) :
+    QAbstractListModel(parent),
+    m_active(false),
+    m_dirty(false)
+{
+    m_dir = "";
+    m_watcher = new QFileSystemWatcher(this);
+    connect(m_watcher, SIGNAL(directoryChanged(const QString&)), this, SLOT(refresh()));
+    connect(m_watcher, SIGNAL(fileChanged(const QString&)), this, SLOT(refresh()));
+
+    QSettings settings;
+    m_showAll = settings.value("showAll", false).toBool();
+
+    // refresh model every time settings are changed
+    Engine *engine = qApp->property("engine").value<Engine *>();
+    connect(engine, SIGNAL(settingsChanged()), this, SLOT(refreshFull()));
+}
+
+FileModel::~FileModel()
+{
+}
+
+int FileModel::rowCount(const QModelIndex &parent) const
+{
+    Q_UNUSED(parent);
+    return m_files.count();
+}
+
+QVariant FileModel::data(const QModelIndex &index, int role) const
+{
+    if (!index.isValid() || index.row() > m_files.size()-1)
+        return QVariant();
+
+    QFileInfo info = m_files.at(index.row()).info;
+    switch (role) {
+
+    case Qt::DisplayRole:
+    case FilenameRole:
+        return info.fileName();
+
+    case FileKindRole:
+        if (info.isSymLink()) return "l";
+        if (info.isDir()) return "d";
+        if (info.isFile()) return "-";
+        return "?";
+
+    case FileIconRole:
+        if (info.isSymLink() && info.isDir()) return "folder-link";
+        if (info.isDir()) return "folder";
+        if (info.isSymLink()) return "link";
+        if (info.isFile()) {
+            QString suffix = info.suffix().toLower();
+            return suffixToIconName(suffix);
+        }
+        return "file";
+
+    case PermissionsRole:
+        return permissionsToString(info.permissions());
+
+    case SizeRole:
+        if (info.isSymLink() && info.isDir()) return "dir-link";
+        if (info.isDir()) return "dir";
+        return filesizeToString(info.size());
+
+    case LastModifiedRole:
+        return datetimeToString(info.lastModified());
+
+    case CreatedRole:
+        return datetimeToString(info.created());
+
+    case IsDirRole:
+        return info.isDir();
+
+    case IsLinkRole:
+        return info.isSymLink();
+
+    case SymLinkTargetRole:
+        return info.symLinkTarget();
+
+    default:
+        return QVariant();
+    }
+}
+
+QHash<int, QByteArray> FileModel::roleNames() const
+{
+    QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
+    roles.insert(FilenameRole, QByteArray("filename"));
+    roles.insert(FileKindRole, QByteArray("filekind"));
+    roles.insert(FileIconRole, QByteArray("fileIcon"));
+    roles.insert(PermissionsRole, QByteArray("permissions"));
+    roles.insert(SizeRole, QByteArray("size"));
+    roles.insert(LastModifiedRole, QByteArray("modified"));
+    roles.insert(CreatedRole, QByteArray("created"));
+    roles.insert(IsDirRole, QByteArray("isDir"));
+    roles.insert(IsLinkRole, QByteArray("isLink"));
+    roles.insert(SymLinkTargetRole, QByteArray("symLinkTarget"));
+    return roles;
+}
+
+int FileModel::fileCount() const
+{
+    return m_files.count();
+}
+
+QString FileModel::errorMessage() const
+{
+    return m_errorMessage;
+}
+
+void FileModel::setDir(QString dir)
+{
+    if (m_dir == dir)
+        return;
+
+    // update watcher to watch the new directory
+    if (!m_dir.isEmpty())
+        m_watcher->removePath(m_dir);
+    if (!dir.isEmpty())
+        m_watcher->addPath(dir);
+
+    m_dir = dir;
+
+    readDirectory();
+    m_dirty = false;
+
+    emit dirChanged();
+}
+
+QString FileModel::appendPath(QString dirName)
+{
+    return QDir::cleanPath(QDir(m_dir).absoluteFilePath(dirName));
+}
+
+void FileModel::setActive(bool active)
+{
+    if (m_active == active)
+        return;
+
+    m_active = active;
+    emit activeChanged();
+
+    if (m_dirty)
+        readDirectory();
+
+    m_dirty = false;
+}
+
+QString FileModel::parentPath()
+{
+    return QDir::cleanPath(QDir(m_dir).absoluteFilePath(".."));
+}
+
+QString FileModel::fileNameAt(int fileIndex)
+{
+    if (fileIndex < 0 || fileIndex >= m_files.count())
+        return QString();
+
+    return m_files.at(fileIndex).info.absoluteFilePath();
+}
+
+void FileModel::refresh()
+{
+    if (!m_active) {
+        m_dirty = true;
+        return;
+    }
+
+    refreshEntries();
+    m_dirty = false;
+}
+
+void FileModel::refreshFull()
+{
+    if (!m_active) {
+        m_dirty = true;
+        return;
+    }
+
+    readDirectory();
+    m_dirty = false;
+}
+
+void FileModel::readDirectory()
+{
+    // wrapped in reset model methods to get views notified
+    beginResetModel();
+
+    m_files.clear();
+    m_errorMessage = "";
+
+    if (!m_dir.isEmpty())
+        readEntries();
+
+    endResetModel();
+    emit fileCountChanged();
+    emit errorMessageChanged();
+}
+
+void FileModel::readEntries()
+{
+    QDir dir(m_dir);
+    if (!dir.exists()) {
+        m_errorMessage = tr("Folder does not exist");
+        return;
+    }
+    if (access(m_dir, R_OK) == -1) {
+        m_errorMessage = tr("No permission to read the folder");
+        return;
+    }
+
+    QSettings settings;
+    bool hiddenSetting = settings.value("show-hidden-files", false).toBool();
+    QDir::Filter hidden = hiddenSetting ? QDir::Hidden : (QDir::Filter)0;
+    dir.setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot | hidden);
+
+    if (settings.value("show-dirs-first", false).toBool())
+        dir.setSorting(QDir::Name | QDir::DirsFirst);
+
+    Engine *engine = qApp->property("engine").value<Engine *>();
+
+    QFileInfoList infoList = dir.entryInfoList();
+    foreach (QFileInfo info, infoList) {
+        if (m_showAll || (info.isDir()) || (info.suffix() == engine->extensionFilter())) {
+            FileData data;
+            data.info = info;
+            m_files.append(data);
+        }
+    }
+}
+
+void FileModel::refreshEntries()
+{
+    m_errorMessage = "";
+
+    // empty dir name
+    if (m_dir.isEmpty()) {
+        beginResetModel();
+        m_files.clear();
+        endResetModel();
+        emit fileCountChanged();
+        emit errorMessageChanged();
+        return;
+    }
+
+    QDir dir(m_dir);
+    if (!dir.exists()) {
+        m_errorMessage = tr("Folder does not exist");
+        emit errorMessageChanged();
+        return;
+    }
+    if (access(m_dir, R_OK) == -1) {
+        m_errorMessage = tr("No permission to read the folder");
+        emit errorMessageChanged();
+        return;
+    }
+
+    QSettings settings;
+    bool hiddenSetting = settings.value("show-hidden-files", false).toBool();
+    QDir::Filter hidden = hiddenSetting ? QDir::Hidden : (QDir::Filter)0;
+    dir.setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot | hidden);
+
+    if (settings.value("show-dirs-first", false).toBool())
+        dir.setSorting(QDir::Name | QDir::DirsFirst);
+
+    Engine *engine = qApp->property("engine").value<Engine *>();
+
+    // read all files
+    QList<FileData> newFiles;
+    QFileInfoList infoList = dir.entryInfoList();
+    foreach (QFileInfo info, infoList) {
+        if ((m_showAll) || (info.isDir()) || (info.suffix() == engine->extensionFilter())) {
+            FileData data;
+            data.info = info;
+            newFiles.append(data);
+        }
+    }
+
+    int oldFileCount = m_files.count();
+
+    // compare old and new files and do removes if needed
+    for (int i = m_files.count()-1; i >= 0; --i) {
+        FileData data = m_files.at(i);
+        if (!newFiles.contains(data)) {
+            beginRemoveRows(QModelIndex(), i, i);
+            m_files.removeAt(i);
+            endRemoveRows();
+        }
+    }
+    // compare old and new files and do inserts if needed
+    for (int i = 0; i < newFiles.count(); ++i) {
+        FileData data = newFiles.at(i);
+        if (!m_files.contains(data)) {
+            beginInsertRows(QModelIndex(), i, i);
+            m_files.insert(i, data);
+            endInsertRows();
+        }
+    }
+
+    if (m_files.count() != oldFileCount)
+        emit fileCountChanged();
+
+    emit errorMessageChanged();
+}
+
+void FileModel::setShowAll(bool showAll)
+{
+    if (m_showAll != showAll) {
+        m_showAll = showAll;
+
+        QSettings settings;
+        settings.setValue("showAll", m_showAll);
+
+        emit showAllChanged();
+        refresh();
+    }
+}
diff --git a/src/filebrowse/filemodel.h b/src/filebrowse/filemodel.h
new file mode 100644 (file)
index 0000000..0704d14
--- /dev/null
@@ -0,0 +1,90 @@
+#ifndef FILEMODEL_H
+#define FILEMODEL_H
+
+#include <QAbstractListModel>
+#include <QDir>
+#include <QFileSystemWatcher>
+
+// struct to hold data for a single file
+struct FileData
+{
+    QFileInfo info;
+
+    bool operator==(const FileData &other) const {
+        return other.info == info;
+    }
+};
+
+/**
+ * @brief The FileModel class can be used as a model in a ListView to display a list of files
+ * in the current directory. It has methods to change the current directory and to access
+ * file info.
+ * It also actively monitors the directory. If the directory changes, then the model is
+ * updated automatically if active is true. If active is false, then the directory is
+ * updated when active becomes true.
+ */
+class FileModel : public QAbstractListModel
+{
+    Q_OBJECT
+    Q_PROPERTY(QString dir READ dir() WRITE setDir(QString) NOTIFY dirChanged())
+    Q_PROPERTY(int fileCount READ fileCount() NOTIFY fileCountChanged())
+    Q_PROPERTY(QString errorMessage READ errorMessage() NOTIFY errorMessageChanged())
+    Q_PROPERTY(bool active READ active() WRITE setActive() NOTIFY activeChanged())
+    Q_PROPERTY(bool showAll READ showAll() WRITE setShowAll() NOTIFY showAllChanged())
+
+public:
+    explicit FileModel(QObject *parent = 0);
+    ~FileModel();
+
+    // methods needed by ListView
+    int rowCount(const QModelIndex &parent = QModelIndex()) const;
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+    QHash<int, QByteArray> roleNames() const;
+
+    // property accessors
+    QString dir() const { return m_dir; }
+    void setDir(QString dir);
+    int fileCount() const;
+    QString errorMessage() const;
+    bool active() const { return m_active; }
+    void setActive(bool active);
+    bool showAll() const { return m_showAll; }
+    void setShowAll(bool showAll);
+
+    // methods accessible from QML
+    Q_INVOKABLE QString appendPath(QString dirName);
+    Q_INVOKABLE QString parentPath();
+    Q_INVOKABLE QString fileNameAt(int fileIndex);
+
+public slots:
+    // reads the directory and inserts/removes model items as needed
+    Q_INVOKABLE void refresh();
+    // reads the directory and sets all model items
+    Q_INVOKABLE void refreshFull();
+
+signals:
+    void dirChanged();
+    void fileCountChanged();
+    void errorMessageChanged();
+    void activeChanged();
+    void showAllChanged();
+
+private slots:
+    void readDirectory();
+
+private:
+    void readEntries();
+    void refreshEntries();
+
+    QString m_dir;
+    QList<FileData> m_files;
+    QString m_errorMessage;
+    bool m_active;
+    bool m_dirty;
+    QFileSystemWatcher *m_watcher;
+    bool m_showAll;
+};
+
+
+
+#endif // FILEMODEL_H
diff --git a/src/filebrowse/fileworker.cpp b/src/filebrowse/fileworker.cpp
new file mode 100644 (file)
index 0000000..4637862
--- /dev/null
@@ -0,0 +1,266 @@
+#include "fileworker.h"
+#include <QDateTime>
+#include "globals.h"
+
+FileWorker::FileWorker(QObject *parent) :
+    QThread(parent),
+    m_mode(DeleteMode),
+    m_cancelled(KeepRunning),
+    m_progress(0)
+{
+}
+
+FileWorker::~FileWorker()
+{
+}
+
+void FileWorker::startDeleteFiles(QStringList filenames)
+{
+    if (isRunning()) {
+        emit errorOccurred(tr("File operation already in progress"), "");
+        return;
+    }
+
+    // basic validity check
+    foreach (QString filename, filenames) {
+        if (filename.isEmpty()) {
+            emit errorOccurred(tr("Empty filename"), "");
+            return;
+        }
+    }
+
+    m_mode = DeleteMode;
+    m_filenames = filenames;
+    m_cancelled.storeRelease(KeepRunning);
+    start();
+}
+
+void FileWorker::startCopyFiles(QStringList filenames, QString destDirectory)
+{
+    if (isRunning()) {
+        emit errorOccurred(tr("File operation already in progress"), "");
+        return;
+    }
+
+    // basic validity check
+    foreach (QString filename, filenames) {
+        if (filename.isEmpty()) {
+            emit errorOccurred(tr("Empty filename"), "");
+            return;
+        }
+    }
+
+    m_mode = CopyMode;
+    m_filenames = filenames;
+    m_destDirectory = destDirectory;
+    m_cancelled.storeRelease(KeepRunning);
+    start();
+}
+
+void FileWorker::startMoveFiles(QStringList filenames, QString destDirectory)
+{
+    if (isRunning()) {
+        emit errorOccurred(tr("File operation already in progress"), "");
+        return;
+    }
+
+    // basic validity check
+    foreach (QString filename, filenames) {
+        if (filename.isEmpty()) {
+            emit errorOccurred(tr("Empty filename"), "");
+            return;
+        }
+    }
+
+    m_mode = MoveMode;
+    m_filenames = filenames;
+    m_destDirectory = destDirectory;
+    m_cancelled.storeRelease(KeepRunning);
+    start();
+}
+
+void FileWorker::cancel()
+{
+    m_cancelled.storeRelease(Cancelled);
+}
+
+void FileWorker::run() Q_DECL_OVERRIDE
+{
+    switch (m_mode) {
+    case DeleteMode:
+        deleteFiles();
+        break;
+
+    case MoveMode:
+    case CopyMode:
+        copyOrMoveFiles();
+        break;
+    }
+}
+
+QString FileWorker::deleteFile(QString filename)
+{
+    QFileInfo info(filename);
+    if (!info.exists())
+        return tr("File not found");
+
+    if (info.isDir()) {
+        // this should be custom function to get better error reporting
+        bool ok = QDir(info.absoluteFilePath()).removeRecursively();
+        if (!ok)
+            return tr("Folder delete failed");
+
+    } else {
+        QFile file(info.absoluteFilePath());
+        bool ok = file.remove();
+        if (!ok)
+            return file.errorString();
+    }
+    return QString();
+}
+
+void FileWorker::deleteFiles()
+{
+    int fileIndex = 0;
+    int fileCount = m_filenames.count();
+
+    foreach (QString filename, m_filenames) {
+        m_progress = 100 * fileIndex / fileCount;
+        emit progressChanged(m_progress, filename);
+
+        // stop if cancelled
+        if (m_cancelled.loadAcquire() == Cancelled) {
+            emit errorOccurred(tr("Cancelled"), filename);
+            return;
+        }
+
+        // delete file and stop if errors
+        QString errMsg = deleteFile(filename);
+        if (!errMsg.isEmpty()) {
+            emit errorOccurred(errMsg, filename);
+            return;
+        }
+        emit fileDeleted(filename);
+
+        fileIndex++;
+    }
+
+    m_progress = 100;
+    emit progressChanged(m_progress, "");
+    emit done();
+}
+
+void FileWorker::copyOrMoveFiles()
+{
+    int fileIndex = 0;
+    int fileCount = m_filenames.count();
+
+    QDir dest(m_destDirectory);
+    foreach (QString filename, m_filenames) {
+        m_progress = 100 * fileIndex / fileCount;
+        emit progressChanged(m_progress, filename);
+
+        // stop if cancelled
+        if (m_cancelled.loadAcquire() == Cancelled) {
+            emit errorOccurred(tr("Cancelled"), filename);
+            return;
+        }
+
+        // check destination does not exists, otherwise copy/move fails
+        QFileInfo fileInfo(filename);
+        QString newname = dest.absoluteFilePath(fileInfo.fileName());
+
+        // move or copy and stop if errors
+        QFile file(filename);
+        if (m_mode == MoveMode) {
+            if (!file.rename(newname)) {
+                emit errorOccurred(file.errorString(), filename);
+                return;
+            }
+        } else {
+            if (fileInfo.isDir()) {
+                QString errmsg = copyDirRecursively(filename, newname);
+                if (!errmsg.isEmpty()) {
+                    emit errorOccurred(errmsg, filename);
+                    return;
+                }
+            } else {
+                QString errmsg = copyOverwrite(filename, newname);
+                if (!errmsg.isEmpty()) {
+                    emit errorOccurred(errmsg, filename);
+                    return;
+                }
+            }
+        }
+
+        fileIndex++;
+    }
+
+    m_progress = 100;
+    emit progressChanged(m_progress, "");
+    emit done();
+}
+
+QString FileWorker::copyDirRecursively(QString srcDirectory, QString destDirectory)
+{
+    QDir srcDir(srcDirectory);
+    if (!srcDir.exists())
+        return tr("Source folder doesn't exist");
+
+    QDir destDir(destDirectory);
+    if (!destDir.exists()) {
+        QDir d(destDir);
+        d.cdUp();
+        if (!d.mkdir(destDir.dirName()))
+            return tr("Can't create target folder %1").arg(destDirectory);
+    }
+
+    // copy files
+    QStringList names = srcDir.entryList(QDir::Files);
+    for (int i = 0 ; i < names.count() ; ++i) {
+        // stop if cancelled
+        if (m_cancelled.loadAcquire() == Cancelled)
+            return tr("Cancelled");
+
+        QString filename = names.at(i);
+        emit progressChanged(m_progress, filename);
+        QString spath = srcDir.absoluteFilePath(filename);
+        QString dpath = destDir.absoluteFilePath(filename);
+        QString errmsg = copyOverwrite(spath, dpath);
+        if (!errmsg.isEmpty())
+            return errmsg;
+    }
+
+    // copy dirs
+    names = srcDir.entryList(QDir::NoDotAndDotDot | QDir::AllDirs);
+    for (int i = 0 ; i < names.count() ; ++i) {
+        // stop if cancelled
+        if (m_cancelled.loadAcquire() == Cancelled)
+            return tr("Cancelled");
+
+        QString filename = names.at(i);
+        emit progressChanged(m_progress, filename);
+        QString spath = srcDir.absoluteFilePath(filename);
+        QString dpath = destDir.absoluteFilePath(filename);
+        QString errmsg = copyDirRecursively(spath, dpath);
+        if (!errmsg.isEmpty())
+            return errmsg;
+    }
+
+    return QString();
+}
+
+QString FileWorker::copyOverwrite(QString src, QString dest)
+{
+    QFile dfile(dest);
+    if (dfile.exists()) {
+        if (!dfile.remove())
+            return dfile.errorString();
+    }
+
+    QFile sfile(src);
+    if (!sfile.copy(dest))
+        return sfile.errorString();
+
+    return QString();
+}
diff --git a/src/filebrowse/fileworker.h b/src/filebrowse/fileworker.h
new file mode 100644 (file)
index 0000000..7b1ce5c
--- /dev/null
@@ -0,0 +1,58 @@
+#ifndef FILEWORKER_H
+#define FILEWORKER_H
+
+#include <QThread>
+#include <QDir>
+
+/**
+ * @brief FileWorker does delete, copy and move files in the background.
+ */
+class FileWorker : public QThread
+{
+    Q_OBJECT
+
+public:
+    explicit FileWorker(QObject *parent = 0);
+    ~FileWorker();
+
+    // call these to start the thread, returns false if start failed
+    void startDeleteFiles(QStringList filenames);
+    void startCopyFiles(QStringList filenames, QString destDirectory);
+    void startMoveFiles(QStringList filenames, QString destDirectory);
+
+    void cancel();
+
+signals: // signals, can be connected from a thread to another
+    void progressChanged(int progress, QString filename);
+
+    // one of these is emitted when thread ends
+    void done();
+    void errorOccurred(QString message, QString filename);
+
+    void fileDeleted(QString fullname);
+
+protected:
+    void run();
+
+private:
+    enum Mode {
+        DeleteMode, CopyMode, MoveMode
+    };
+    enum CancelStatus {
+        Cancelled = 0, KeepRunning = 1
+    };
+
+    QString deleteFile(QString filenames);
+    void deleteFiles();
+    void copyOrMoveFiles();
+    QString copyDirRecursively(QString srcDirectory, QString destDirectory);
+    QString copyOverwrite(QString src, QString dest);
+
+    FileWorker::Mode m_mode;
+    QStringList m_filenames;
+    QString m_destDirectory;
+    QAtomicInt m_cancelled; // atomic so no locks needed
+    int m_progress;
+};
+
+#endif // FILEWORKER_H
diff --git a/src/filebrowse/globals.cpp b/src/filebrowse/globals.cpp
new file mode 100644 (file)
index 0000000..2ab117f
--- /dev/null
@@ -0,0 +1,109 @@
+#include "globals.h"
+#include <QLocale>
+#include <QProcess>
+
+QString suffixToIconName(QString suffix)
+{
+    // only formats that are understood by File Browser or Sailfish get a special icon
+    if (suffix == "txt")
+        return "file-txt";
+    if (suffix == "rpm")
+        return "file-rpm";
+    if (suffix == "apk")
+        return "file-apk";
+    if (suffix == "png" || suffix == "jpeg" || suffix == "jpg" ||
+            suffix == "gif")
+        return "file-image";
+    if (suffix == "wav" || suffix == "mp3" || suffix == "flac" ||
+            suffix == "aac" || suffix == "ogg" || suffix == "m4a")
+        return "file-audio";
+    if (suffix == "mp4" || suffix == "m4v")
+        return "file-video";
+
+    return "file";
+}
+
+QString permissionsToString(QFile::Permissions permissions)
+{
+    char str[] = "---------";
+    if (permissions & 0x4000) str[0] = 'r';
+    if (permissions & 0x2000) str[1] = 'w';
+    if (permissions & 0x1000) str[2] = 'x';
+    if (permissions & 0x0040) str[3] = 'r';
+    if (permissions & 0x0020) str[4] = 'w';
+    if (permissions & 0x0010) str[5] = 'x';
+    if (permissions & 0x0004) str[6] = 'r';
+    if (permissions & 0x0002) str[7] = 'w';
+    if (permissions & 0x0001) str[8] = 'x';
+    return QString::fromLatin1(str);
+}
+
+QString filesizeToString(qint64 filesize)
+{
+    // convert to kB, MB, GB: use 1000 instead of 1024 as divisor because it seems to be
+    // the usual way to display file size (like on Ubuntu)
+    QLocale locale;
+    if (filesize < 1000LL)
+        return locale.toString(filesize)+" bytes";
+
+    if (filesize < 1000000LL)
+        return locale.toString((double)filesize/1000.0, 'f', 2)+" kB";
+
+    if (filesize < 1000000000LL)
+        return locale.toString((double)filesize/1000000.0, 'f', 2)+" MB";
+
+    return locale.toString((double)filesize/1000000000.0, 'f', 2)+" GB";
+}
+
+QString datetimeToString(QDateTime datetime)
+{
+    QLocale locale;
+
+    // return time for today or date for older
+    if (datetime.date() == QDate::currentDate())
+        return locale.toString(datetime.time(), QLocale::NarrowFormat);
+
+    return locale.toString(datetime.date(), QLocale::NarrowFormat);
+}
+
+QString infoToFileKind(QFileInfo info)
+{
+    if (info.isDir()) return "d";
+    if (info.isSymLink()) return "l";
+    if (info.isFile()) return "-";
+    return "?";
+}
+
+QString infoToIconName(QFileInfo info)
+{
+    if (info.isDir()) return "folder";
+    if (info.isSymLink()) return "link";
+    if (info.isFile()) {
+        QString suffix = info.suffix().toLower();
+        return suffixToIconName(suffix);
+    }
+    return "file";
+}
+
+int access(QString filename, int how)
+{
+    QByteArray fab = filename.toUtf8();
+    char *fn = fab.data();
+    return access(fn, how);
+}
+
+QString execute(QString command, QStringList arguments, bool mergeErrorStream)
+{
+    QProcess process;
+    process.setReadChannel(QProcess::StandardOutput);
+    if (mergeErrorStream)
+        process.setProcessChannelMode(QProcess::MergedChannels);
+    process.start(command, arguments);
+    if (!process.waitForStarted())
+        return QString();
+    if (!process.waitForFinished())
+        return QString();
+
+    QByteArray result = process.readAll();
+    return QString::fromUtf8(result);
+}
diff --git a/src/filebrowse/globals.h b/src/filebrowse/globals.h
new file mode 100644 (file)
index 0000000..d96523a
--- /dev/null
@@ -0,0 +1,22 @@
+#ifndef GLOBALS_H
+#define GLOBALS_H
+
+#include <QString>
+#include <QDateTime>
+#include <QDir>
+
+// Global functions
+
+QString suffixToIconName(QString suffix);
+QString permissionsToString(QFile::Permissions permissions);
+QString filesizeToString(qint64 filesize);
+QString datetimeToString(QDateTime datetime);
+
+QString infoToFileKind(QFileInfo info);
+QString infoToIconName(QFileInfo info);
+
+int access(QString filename, int how);
+
+QString execute(QString command, QStringList arguments, bool mergeErrorStream);
+
+#endif // GLOBALS_H
diff --git a/src/filebrowse/searchengine.cpp b/src/filebrowse/searchengine.cpp
new file mode 100644 (file)
index 0000000..9ba2671
--- /dev/null
@@ -0,0 +1,67 @@
+#include "searchengine.h"
+#include <QDateTime>
+#include "searchworker.h"
+#include "globals.h"
+
+SearchEngine::SearchEngine(QObject *parent) :
+    QObject(parent)
+{
+    m_dir = "";
+    m_searchWorker = new SearchWorker;
+    connect(m_searchWorker, SIGNAL(matchFound(QString)), this, SLOT(emitMatchFound(QString)));
+
+    // pass worker end signals to QML
+    connect(m_searchWorker, SIGNAL(progressChanged(QString)),
+            this, SIGNAL(progressChanged(QString)));
+    connect(m_searchWorker, SIGNAL(done()), this, SIGNAL(workerDone()));
+    connect(m_searchWorker, SIGNAL(errorOccurred(QString, QString)),
+            this, SIGNAL(workerErrorOccurred(QString, QString)));
+
+    connect(m_searchWorker, SIGNAL(started()), this, SIGNAL(runningChanged()));
+    connect(m_searchWorker, SIGNAL(finished()), this, SIGNAL(runningChanged()));
+}
+
+SearchEngine::~SearchEngine()
+{
+    // is this the way to force stop the worker thread?
+    m_searchWorker->cancel(); // stop possibly running background thread
+    m_searchWorker->wait();   // wait until thread stops
+    delete m_searchWorker;    // delete it
+}
+
+void SearchEngine::setDir(QString dir)
+{
+    if (m_dir == dir)
+        return;
+
+    m_dir = dir;
+
+    emit dirChanged();
+}
+
+bool SearchEngine::running() const
+{
+    return m_searchWorker->isRunning();
+}
+
+void SearchEngine::search(QString searchTerm)
+{
+    // if search term is not empty, then restart search
+    if (!searchTerm.isEmpty()) {
+        m_searchWorker->cancel();
+        m_searchWorker->wait();
+        m_searchWorker->startSearch(m_dir, searchTerm);
+    }
+}
+
+void SearchEngine::cancel()
+{
+    m_searchWorker->cancel();
+}
+
+void SearchEngine::emitMatchFound(QString fullpath)
+{
+    QFileInfo info(fullpath);
+    emit matchFound(fullpath, info.fileName(), info.absoluteDir().absolutePath(),
+                    infoToIconName(info), infoToFileKind(info));
+}
diff --git a/src/filebrowse/searchengine.h b/src/filebrowse/searchengine.h
new file mode 100644 (file)
index 0000000..47a2d0b
--- /dev/null
@@ -0,0 +1,51 @@
+#ifndef SEARCHENGINE_H
+#define SEARCHENGINE_H
+
+#include <QDir>
+
+class SearchWorker;
+
+/**
+ * @brief The SearchEngine is a front-end for the SearchWorker class.
+ * These two classes could be merged, but it is clearer to keep the background thread
+ * in its own class.
+ */
+class SearchEngine : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString dir READ dir() WRITE setDir(QString) NOTIFY dirChanged())
+    Q_PROPERTY(bool running READ running() NOTIFY runningChanged())
+
+public:
+    explicit SearchEngine(QObject *parent = 0);
+    ~SearchEngine();
+
+    // property accessors
+    QString dir() const { return m_dir; }
+    void setDir(QString dir);
+    bool running() const;
+
+    // callable from QML
+    Q_INVOKABLE void search(QString searchTerm);
+    Q_INVOKABLE void cancel();
+
+signals:
+    void dirChanged();
+    void runningChanged();
+
+    void progressChanged(QString directory);
+    void matchFound(QString fullname, QString filename, QString absoluteDir,
+                    QString fileIcon, QString fileKind);
+    void workerDone();
+    void workerErrorOccurred(QString message, QString filename);
+
+private slots:
+    void emitMatchFound(QString fullpath);
+
+private:
+    QString m_dir;
+    QString m_errorMessage;
+    SearchWorker *m_searchWorker;
+};
+
+#endif // SEARCHENGINE_H
diff --git a/src/filebrowse/searchworker.cpp b/src/filebrowse/searchworker.cpp
new file mode 100644 (file)
index 0000000..0b44930
--- /dev/null
@@ -0,0 +1,105 @@
+#include "searchworker.h"
+#include <QDateTime>
+#include <QSettings>
+#include "globals.h"
+
+SearchWorker::SearchWorker(QObject *parent) :
+    QThread(parent),
+    m_cancelled(NotCancelled)
+{
+}
+
+SearchWorker::~SearchWorker()
+{
+}
+
+void SearchWorker::startSearch(QString directory, QString searchTerm)
+{
+    if (isRunning()) {
+        emit errorOccurred(tr("Search already in progress"), "");
+        return;
+    }
+    if (directory.isEmpty() || searchTerm.isEmpty()) {
+        emit errorOccurred(tr("Bad search parameters"), "");
+        return;
+    }
+
+    m_directory = directory;
+    m_searchTerm = searchTerm;
+    m_currentDirectory = directory;
+    m_cancelled.storeRelease(NotCancelled);
+    start();
+}
+
+void SearchWorker::cancel()
+{
+    m_cancelled.storeRelease(Cancelled);
+}
+
+void SearchWorker::run() Q_DECL_OVERRIDE
+{
+    QString errMsg = searchRecursively(m_directory, m_searchTerm.toLower());
+    if (!errMsg.isEmpty())
+        emit errorOccurred(errMsg, m_currentDirectory);
+
+    emit progressChanged("");
+    emit done();
+}
+
+QString SearchWorker::searchRecursively(QString directory, QString searchTerm)
+{
+    // skip some system folders - they don't really have any interesting stuff
+    if (directory.startsWith("/proc") ||
+            directory.startsWith("/sys/block"))
+        return QString();
+
+    QDir dir(directory);
+    if (!dir.exists())  // skip "non-existent" directories (found in /dev)
+        return QString();
+
+    // update progress
+    m_currentDirectory = directory;
+    emit progressChanged(m_currentDirectory);
+
+    QSettings settings;
+    bool hiddenSetting = settings.value("show-hidden-files", false).toBool();
+    QDir::Filter hidden = hiddenSetting ? QDir::Hidden : (QDir::Filter)0;
+
+    // search dirs
+    QStringList names = dir.entryList(QDir::NoDotAndDotDot | QDir::AllDirs | hidden);
+    for (int i = 0 ; i < names.count() ; ++i) {
+        // stop if cancelled
+        if (m_cancelled.loadAcquire() == Cancelled)
+            return QString();
+
+        QString filename = names.at(i);
+        QString fullpath = dir.absoluteFilePath(filename);
+
+        if (filename.toLower().indexOf(searchTerm) >= 0)
+            emit matchFound(fullpath);
+
+        QFileInfo info(fullpath); // skip symlinks to prevent infinite loops
+        if (info.isSymLink())
+            continue;
+
+        QString errmsg = searchRecursively(fullpath, searchTerm);
+        if (!errmsg.isEmpty())
+            return errmsg;
+    }
+
+    // search files
+    names = dir.entryList(QDir::Files | hidden);
+    for (int i = 0 ; i < names.count() ; ++i) {
+        // stop if cancelled
+        if (m_cancelled.loadAcquire() == Cancelled)
+            return QString();
+
+        QString filename = names.at(i);
+        QString fullpath = dir.absoluteFilePath(filename);
+
+        if (filename.toLower().indexOf(searchTerm) >= 0)
+            emit matchFound(fullpath);
+    }
+
+    return QString();
+}
diff --git a/src/filebrowse/searchworker.h b/src/filebrowse/searchworker.h
new file mode 100644 (file)
index 0000000..2714d53
--- /dev/null
@@ -0,0 +1,48 @@
+#ifndef SEARCHWORKER_H
+#define SEARCHWORKER_H
+
+#include <QThread>
+#include <QDir>
+
+/**
+ * @brief SearchWorker does searching in the background.
+ */
+class SearchWorker : public QThread
+{
+    Q_OBJECT
+
+public:
+    explicit SearchWorker(QObject *parent = 0);
+    ~SearchWorker();
+
+    void startSearch(QString directory, QString searchTerm);
+
+    void cancel();
+
+signals: // signals, can be connected from a thread to another
+
+    void progressChanged(QString directory);
+
+    void matchFound(QString fullname);
+
+    // one of these is emitted when thread ends
+    void done();
+    void errorOccurred(QString message, QString filename);
+
+protected:
+    void run();
+
+private:
+    enum CancelStatus {
+        Cancelled = 0, NotCancelled = 1
+    };
+
+    QString searchRecursively(QString directory, QString searchTerm);
+
+    QString m_directory;
+    QString m_searchTerm;
+    QAtomicInt m_cancelled; // atomic so no locks needed
+    QString m_currentDirectory;
+};
+
+#endif // SEARCHWORKER_H
index 7bd002f..057cb36 100644 (file)
@@ -21,6 +21,7 @@ VPNControl::VPNControl(QObject *parent) :
     compressed = settings.value("compressed", true).toBool();
     useTLS = settings.value("useTLS", true).toBool();
     tlsDirection = settings.value("tlsDirection", 1).toInt();
     compressed = settings.value("compressed", true).toBool();
     useTLS = settings.value("useTLS", true).toBool();
     tlsDirection = settings.value("tlsDirection", 1).toInt();
+    settings.setValue("showAll", false);
 }
 
 void VPNControl::initialise()
 }
 
 void VPNControl::initialise()
@@ -33,7 +34,6 @@ void VPNControl::setStatus(VPNSTATUS newStatus)
     if (vpnStatus != newStatus) {
         vpnStatus = newStatus;
         emit statusChanged(newStatus);
     if (vpnStatus != newStatus) {
         vpnStatus = newStatus;
         emit statusChanged(newStatus);
-        printf ("Emitting status %d\n", newStatus);
     }
 }
 int VPNControl::getTlsDirection() const
     }
 }
 int VPNControl::getTlsDirection() const
@@ -44,7 +44,6 @@ int VPNControl::getTlsDirection() const
 void VPNControl::setTlsDirection(int value)
 {
     if (value != tlsDirection) {
 void VPNControl::setTlsDirection(int value)
 {
     if (value != tlsDirection) {
-        printf ("TLS direction set to %d\n", value);
         tlsDirection = value;
         settingsSetValue("tlsDirection", value);
         emit tlsDirectionChanged (value);
         tlsDirection = value;
         settingsSetValue("tlsDirection", value);
         emit tlsDirectionChanged (value);
@@ -59,7 +58,6 @@ bool VPNControl::getUseTLS() const
 void VPNControl::setUseTLS(bool value)
 {
     if (value != useTLS) {
 void VPNControl::setUseTLS(bool value)
 {
     if (value != useTLS) {
-        printf ("Use TLS set to %d\n", value);
         useTLS = value;
         settingsSetValue("useTLS", value);
         emit useTLSChanged(useTLS);
         useTLS = value;
         settingsSetValue("useTLS", value);
         emit useTLSChanged(useTLS);
@@ -74,7 +72,6 @@ bool VPNControl::getCompressed() const
 void VPNControl::setCompressed(bool value)
 {
     if (value != compressed) {
 void VPNControl::setCompressed(bool value)
 {
     if (value != compressed) {
-        printf ("Use compression set to %d\n", value);
         compressed = value;
         settingsSetValue("compressed", value);
         emit compressedChanged(compressed);
         compressed = value;
         settingsSetValue("compressed", value);
         emit compressedChanged(compressed);
@@ -89,7 +86,6 @@ unsigned int VPNControl::getPort() const
 void VPNControl::setPort(unsigned int value)
 {
     if (value != port) {
 void VPNControl::setPort(unsigned int value)
 {
     if (value != port) {
-        printf ("Port set to %d\n", value);
         port = value;
         settingsSetValue("port", value);
         emit portChanged(port);
         port = value;
         settingsSetValue("port", value);
         emit portChanged(port);
@@ -104,13 +100,23 @@ QString VPNControl::getServer() const
 void VPNControl::setServer(const QString &value)
 {
     if (value != server) {
 void VPNControl::setServer(const QString &value)
 {
     if (value != server) {
-        printf ("Server set to %s\n", value.toUtf8().constData());
         server = value;
         settingsSetValue("server", value);
         emit serverChanged(server);
     }
 }
 
         server = value;
         settingsSetValue("server", value);
         emit serverChanged(server);
     }
 }
 
+QString VPNControl::getLogText() const
+{
+    return logText;
+}
+
+void VPNControl::setLogText(const QString &value)
+{
+    logText = value;
+    emit logTextChanged(value);
+}
+
 void VPNControl::settingsSetValue (QString key, QString value) {
     QSettings settings;
 
 void VPNControl::settingsSetValue (QString key, QString value) {
     QSettings settings;
 
@@ -128,8 +134,6 @@ void VPNControl::vpnConnect() {
         printf ("Process already running.\n");
     }
     else {
         printf ("Process already running.\n");
     }
     else {
-        printf ("Connect\n");
-
         vpnProcess = new QProcess();
         QString program = "openvpn";
         collectArguments ();
         vpnProcess = new QProcess();
         QString program = "openvpn";
         collectArguments ();
@@ -191,7 +195,6 @@ void VPNControl::addValue (QString key) {
 
 void VPNControl::vpnDisconnect() {
     if (vpnProcess != NULL) {
 
 void VPNControl::vpnDisconnect() {
     if (vpnProcess != NULL) {
-        printf ("Disconnect\n");
 
         vpnProcess->terminate();
         setStatus(VPNSTATUS_DISCONNECTING);
 
         vpnProcess->terminate();
         setStatus(VPNSTATUS_DISCONNECTING);
@@ -201,21 +204,21 @@ void VPNControl::vpnDisconnect() {
 void VPNControl::readData() {
     while (vpnProcess->canReadLine()) {
         QByteArray read = vpnProcess->readLine();
 void VPNControl::readData() {
     while (vpnProcess->canReadLine()) {
         QByteArray read = vpnProcess->readLine();
-        printf ("Output: %s", read.data());
+        //printf ("Output: %s", read.data());
+
+        logAppend(read);
+
         if (read.endsWith("Initialization Sequence Completed\n")) {
         if (read.endsWith("Initialization Sequence Completed\n")) {
-            printf ("We're connected!\n");
             setStatus(VPNSTATUS_CONNECTED);
         }
     }
 }
 
 void VPNControl::started() {
             setStatus(VPNSTATUS_CONNECTED);
         }
     }
 }
 
 void VPNControl::started() {
-    printf ("Started\n");
     setStatus(VPNSTATUS_CONNECTING);
 }
 
 void VPNControl::finished(int code) {
     setStatus(VPNSTATUS_CONNECTING);
 }
 
 void VPNControl::finished(int code) {
-    printf ("Finished with code %d\n", code);
     if (vpnProcess != NULL) {
         //delete vpnProcess;
         vpnProcess = NULL;
     if (vpnProcess != NULL) {
         //delete vpnProcess;
         vpnProcess = NULL;
@@ -242,3 +245,26 @@ void VPNControl::updateConfiguration()
 {
     printf ("Update configuration\n");
 }
 {
     printf ("Update configuration\n");
 }
+
+void VPNControl::logAppend(const QString &text)
+{
+    if (!text.isEmpty()) {
+        // How many lines to add
+        int newLines = text.count('\n');
+        int currentLines = logText.count('\n');
+        int removeLines = currentLines + newLines - 18;
+
+        // Remove excess lines
+        while (removeLines > 0) {
+            int nextLine = logText.lastIndexOf('\n');
+            if (nextLine > 0) {
+                logText = logText.left(nextLine);
+            }
+            removeLines--;
+        }
+
+        // Add new lines
+        logText.prepend(text);
+        emit logTextChanged(logText);
+    }
+}
index 542ec95..33dbc38 100644 (file)
@@ -26,6 +26,8 @@ class VPNControl : public QObject
     Q_PROPERTY (bool useTLS READ getUseTLS WRITE setUseTLS NOTIFY useTLSChanged)
     Q_PROPERTY (int tlsDirection READ getTlsDirection WRITE setTlsDirection NOTIFY tlsDirectionChanged)
 
     Q_PROPERTY (bool useTLS READ getUseTLS WRITE setUseTLS NOTIFY useTLSChanged)
     Q_PROPERTY (int tlsDirection READ getTlsDirection WRITE setTlsDirection NOTIFY tlsDirectionChanged)
 
+    Q_PROPERTY (QString logText READ getLogText WRITE setLogText NOTIFY logTextChanged)
+
 private:
     QProcess * vpnProcess;
     VPNSTATUS vpnStatus;
 private:
     QProcess * vpnProcess;
     VPNSTATUS vpnStatus;
@@ -37,6 +39,7 @@ private:
     bool compressed;
     bool useTLS;
     int tlsDirection;
     bool compressed;
     bool useTLS;
     int tlsDirection;
+    QString logText;
 
     void collectArguments ();
     void setStatus (VPNSTATUS newStatus);
 
     void collectArguments ();
     void setStatus (VPNSTATUS newStatus);
@@ -55,6 +58,7 @@ public:
     bool getCompressed() const;
     bool getUseTLS() const;
     int getTlsDirection() const;
     bool getCompressed() const;
     bool getUseTLS() const;
     int getTlsDirection() const;
+    QString getLogText() const;
 
 signals:
     void statusChanged(int status);
 
 signals:
     void statusChanged(int status);
@@ -63,6 +67,7 @@ signals:
     void compressedChanged(bool compressed);
     void useTLSChanged(bool useTLS);
     void tlsDirectionChanged (int direction);
     void compressedChanged(bool compressed);
     void useTLSChanged(bool useTLS);
     void tlsDirectionChanged (int direction);
+    void logTextChanged (QString logText);
 
 public slots:
     void vpnConnect ();
 
 public slots:
     void vpnConnect ();
@@ -77,6 +82,8 @@ public slots:
     void setCompressed(bool value);
     void setUseTLS(bool value);
     void setTlsDirection(int value);
     void setCompressed(bool value);
     void setUseTLS(bool value);
     void setTlsDirection(int value);
+    void setLogText(const QString &value);
+    void logAppend(const QString &text);
 };
 
 #endif // VPNCONTROL_H
 };
 
 #endif // VPNCONTROL_H