From e24363e314aca32e7bee952f02f517a04a8dc5f2 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 13 Mar 2014 02:18:11 +0000 Subject: [PATCH] Integrated file selection dialogue with the main code. Improved the configuration dialogue. Added logging window. --- OpenVPNUI.pro | 62 ++- qml/components/ValueButtonAlignRight.qml | 80 ++++ qml/filebrowse/components/CenteredField.qml | 33 ++ qml/filebrowse/components/DirPopup.qml | 117 ++++++ qml/filebrowse/components/DoubleMenuItem.qml | 16 + .../components/InteractionBlocker.qml | 38 ++ qml/filebrowse/components/LetterSwitch.qml | 23 ++ .../components/NotificationPanel.qml | 109 ++++++ qml/filebrowse/components/ProgressPanel.qml | 112 ++++++ qml/filebrowse/components/Spacer.qml | 8 + qml/filebrowse/images/large-file-apk.png | Bin 0 -> 2149 bytes qml/filebrowse/images/large-file-audio.png | Bin 0 -> 1075 bytes qml/filebrowse/images/large-file-image.png | Bin 0 -> 1998 bytes qml/filebrowse/images/large-file-rpm.png | Bin 0 -> 2424 bytes qml/filebrowse/images/large-file-txt.png | Bin 0 -> 1709 bytes qml/filebrowse/images/large-file-video.png | Bin 0 -> 1115 bytes qml/filebrowse/images/large-file.png | Bin 0 -> 669 bytes qml/filebrowse/images/large-folder-link.png | Bin 0 -> 1322 bytes qml/filebrowse/images/large-folder.png | Bin 0 -> 966 bytes qml/filebrowse/images/large-link.png | Bin 0 -> 890 bytes qml/filebrowse/images/small-file-apk.png | Bin 0 -> 741 bytes qml/filebrowse/images/small-file-audio.png | Bin 0 -> 397 bytes qml/filebrowse/images/small-file-image.png | Bin 0 -> 620 bytes qml/filebrowse/images/small-file-rpm.png | Bin 0 -> 919 bytes qml/filebrowse/images/small-file-text.png | Bin 0 -> 552 bytes qml/filebrowse/images/small-file-txt.png | Bin 0 -> 552 bytes qml/filebrowse/images/small-file-video.png | Bin 0 -> 490 bytes qml/filebrowse/images/small-file.png | Bin 0 -> 286 bytes qml/filebrowse/images/small-folder-link.png | Bin 0 -> 537 bytes qml/filebrowse/images/small-folder.png | Bin 0 -> 440 bytes qml/filebrowse/images/small-link.png | Bin 0 -> 376 bytes qml/filebrowse/pages/AboutPage.qml | 58 +++ qml/filebrowse/pages/ConsolePage.qml | 102 +++++ qml/filebrowse/pages/CreateFolderDialog.qml | 64 ++++ qml/filebrowse/pages/DirectoryPage.qml | 237 ++++++++++++ qml/filebrowse/pages/FilePage.qml | 299 +++++++++++++++ qml/filebrowse/pages/PermissionsDialog.qml | 218 +++++++++++ qml/filebrowse/pages/RenameDialog.qml | 74 ++++ qml/filebrowse/pages/SearchPage.qml | 262 +++++++++++++ qml/filebrowse/pages/SettingsPage.qml | 113 ++++++ qml/filebrowse/pages/ViewPage.qml | 80 ++++ qml/filebrowse/pages/functions.js | 182 +++++++++ qml/pages/ConfigurePage.qml | 106 ++++-- qml/pages/ConnectPage.qml | 28 ++ rpm/OpenVPNUI.spec | 20 +- rpm/OpenVPNUI.yaml | 20 +- src/OpenVPNUI.cpp | 18 +- src/filebrowse/engine.cpp | 353 ++++++++++++++++++ src/filebrowse/engine.h | 98 +++++ src/filebrowse/fileinfo.cpp | 183 +++++++++ src/filebrowse/fileinfo.h | 95 +++++ src/filebrowse/filemodel.cpp | 337 +++++++++++++++++ src/filebrowse/filemodel.h | 90 +++++ src/filebrowse/fileworker.cpp | 266 +++++++++++++ src/filebrowse/fileworker.h | 58 +++ src/filebrowse/globals.cpp | 109 ++++++ src/filebrowse/globals.h | 22 ++ src/filebrowse/searchengine.cpp | 67 ++++ src/filebrowse/searchengine.h | 51 +++ src/filebrowse/searchworker.cpp | 105 ++++++ src/filebrowse/searchworker.h | 48 +++ src/vpncontrol.cpp | 52 ++- src/vpncontrol.h | 7 + 63 files changed, 4341 insertions(+), 79 deletions(-) create mode 100644 qml/components/ValueButtonAlignRight.qml create mode 100644 qml/filebrowse/components/CenteredField.qml create mode 100644 qml/filebrowse/components/DirPopup.qml create mode 100644 qml/filebrowse/components/DoubleMenuItem.qml create mode 100644 qml/filebrowse/components/InteractionBlocker.qml create mode 100644 qml/filebrowse/components/LetterSwitch.qml create mode 100644 qml/filebrowse/components/NotificationPanel.qml create mode 100644 qml/filebrowse/components/ProgressPanel.qml create mode 100644 qml/filebrowse/components/Spacer.qml create mode 100644 qml/filebrowse/images/large-file-apk.png create mode 100644 qml/filebrowse/images/large-file-audio.png create mode 100644 qml/filebrowse/images/large-file-image.png create mode 100644 qml/filebrowse/images/large-file-rpm.png create mode 100644 qml/filebrowse/images/large-file-txt.png create mode 100644 qml/filebrowse/images/large-file-video.png create mode 100644 qml/filebrowse/images/large-file.png create mode 100644 qml/filebrowse/images/large-folder-link.png create mode 100644 qml/filebrowse/images/large-folder.png create mode 100644 qml/filebrowse/images/large-link.png create mode 100644 qml/filebrowse/images/small-file-apk.png create mode 100644 qml/filebrowse/images/small-file-audio.png create mode 100644 qml/filebrowse/images/small-file-image.png create mode 100644 qml/filebrowse/images/small-file-rpm.png create mode 100644 qml/filebrowse/images/small-file-text.png create mode 100644 qml/filebrowse/images/small-file-txt.png create mode 100644 qml/filebrowse/images/small-file-video.png create mode 100644 qml/filebrowse/images/small-file.png create mode 100644 qml/filebrowse/images/small-folder-link.png create mode 100644 qml/filebrowse/images/small-folder.png create mode 100644 qml/filebrowse/images/small-link.png create mode 100644 qml/filebrowse/pages/AboutPage.qml create mode 100644 qml/filebrowse/pages/ConsolePage.qml create mode 100644 qml/filebrowse/pages/CreateFolderDialog.qml create mode 100644 qml/filebrowse/pages/DirectoryPage.qml create mode 100644 qml/filebrowse/pages/FilePage.qml create mode 100644 qml/filebrowse/pages/PermissionsDialog.qml create mode 100644 qml/filebrowse/pages/RenameDialog.qml create mode 100644 qml/filebrowse/pages/SearchPage.qml create mode 100644 qml/filebrowse/pages/SettingsPage.qml create mode 100644 qml/filebrowse/pages/ViewPage.qml create mode 100644 qml/filebrowse/pages/functions.js create mode 100644 src/filebrowse/engine.cpp create mode 100644 src/filebrowse/engine.h create mode 100644 src/filebrowse/fileinfo.cpp create mode 100644 src/filebrowse/fileinfo.h create mode 100644 src/filebrowse/filemodel.cpp create mode 100644 src/filebrowse/filemodel.h create mode 100644 src/filebrowse/fileworker.cpp create mode 100644 src/filebrowse/fileworker.h create mode 100644 src/filebrowse/globals.cpp create mode 100644 src/filebrowse/globals.h create mode 100644 src/filebrowse/searchengine.cpp create mode 100644 src/filebrowse/searchengine.h create mode 100644 src/filebrowse/searchworker.cpp create mode 100644 src/filebrowse/searchworker.h diff --git a/OpenVPNUI.pro b/OpenVPNUI.pro index 90c7f7f..6457ec7 100644 --- a/OpenVPNUI.pro +++ b/OpenVPNUI.pro @@ -11,7 +11,14 @@ TARGET = OpenVPNUI 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 \ @@ -21,8 +28,57 @@ OTHER_FILES += qml/OpenVPNUI.qml \ 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 += \ - 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 index 0000000..899e16f --- /dev/null +++ b/qml/components/ValueButtonAlignRight.qml @@ -0,0 +1,80 @@ +/**************************************************************************************** +** +** Copyright (C) 2013 Jolla Ltd. +** Contact: Bea Lam +** 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 index 0000000..8d26e32 --- /dev/null +++ b/qml/filebrowse/components/CenteredField.qml @@ -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 index 0000000..b7a4eee --- /dev/null +++ b/qml/filebrowse/components/DirPopup.qml @@ -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 index 0000000..9507baf --- /dev/null +++ b/qml/filebrowse/components/DoubleMenuItem.qml @@ -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 index 0000000..89ea2e5 --- /dev/null +++ b/qml/filebrowse/components/InteractionBlocker.qml @@ -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 index 0000000..5012aff --- /dev/null +++ b/qml/filebrowse/components/LetterSwitch.qml @@ -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 index 0000000..0f9874c --- /dev/null +++ b/qml/filebrowse/components/NotificationPanel.qml @@ -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 index 0000000..13b31aa --- /dev/null +++ b/qml/filebrowse/components/ProgressPanel.qml @@ -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 index 0000000..4336426 --- /dev/null +++ b/qml/filebrowse/components/Spacer.qml @@ -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 index 0000000000000000000000000000000000000000..e56e762e8a0b8c6920e22eb4c201210e7f38982f GIT binary patch literal 2149 zcmai$cTm&W7RT=|)Ig*uC5V(Pia=-vAxbf5A_k=kqI4qx6iB26X$f!Fl^O(HS3o4< z6Eq@0d~}eg5Ky9sJXVk*0t<+cpojrOdGYP+-+O1yJ@BVMML}Bu z0I-F41n(uuj2|H{E7_kphW1F3blgGWae2u~l|Pdy(QxXKlW_pp2L1>L$v7-I0AxJD zFW#F%j;Dpjh5;IlW*!-HE{+sR4Kt_2hL;HJwE>WuAmW{lC*4>mrDyIqq2ARVUSFa< zV(6P$N586ln>Cd$>z#YRC3|q*PDt1tm6^4yoO3GbY#(oDq0L$Ug5ra7CZ}(mx^L|y zm2YG9t8KXMIiic$+0+!Tf`>SqFAi+jl% zhl-qzz67XR#pFc}F99g!bdQF~nz;hiA!uCw>v(XEJQstCbuWSxo$&(cMd#y|Q&l*5 zt~V++*!W3y(AM3kVaQ%N#HRu)L$$@|nOmnX3rpkCEtNb zR4nUm<3)LWR4|~zxZ*0dxM=k)p`%KQpQ^?!1!E7{=_pjbhHC$+4h~-rn*~YjKFa{J z`82Yo_>Eo0My{#ogA+OZbr`x5mf1Z63<2)}JB^>(eUb%K3WjT_s0a1zbROTkdIGyu z;s9o0ek31<4*r1zHABl}@o|vKWq;p^p?47wMiPeyfDEeY*t+Cty%{AHOH?gV(dZAk z2kc5PA37Z%XW0NyUyRW@YoFrw{u}WO0|cSd@q@AfsR~$WhZUdC381XEpP>kSEsU+3 zdbeq=q`NQT5b|aKsWB%9Qx6(2OF2sKe6Hc}Zc>BA#xHi_R6qKcVLj1qm>AVV3UT1uKeK>lkAQZ$i#?P zn1}ij+c3QDHCE5->JsJU<*j3wlbq&Re<;Za7lvew#&BE;r#wmg_aN2Awcp771~$~< zuGV!d(^jQVIdfJPwI7ADh;JD!(Mi0Z)!GAWeuLMLUlzM-lXRc{>C4mb6zpkB0>PLwiuo7RW8NnFF`k6ZX@8oex0A6pJhe!`TVM#DDkhkn%7q|ueePgl4;biqX|;wxD8bk zd2w-ZV!7zaH+PQaLo3vZ2uIg`2lMYUKqzxphrFT4cpu)AYbDn_Dlq!K9$jcyCQIe` zq}B|aUGE80F7;YX2HPz*lyP`(-gOOz@^UusjkSX@iVj;;1)s7a(l~;>B>q0Y2@M^g z3p*@%Yug#__lrw!QVD4J&j%TYZ+|X}X%HVNu5c%3+k%y`mi99m9Wirl)m*KmBlibt zDR0?omEx9bu=xF@{#g+gstf#K-))3m-g$d?Nb^wC<9Sxf1y88@aLcGJQcsVL$}Sh~ zk4pR{GZ73LpxOf)Vn%PiEU_J?eZH0@y25pl#wN`DcV%wi{>9AspEuQ1qYcW0Vkqj`I^0R7J9cAjm56Hopj|RT&b<(ZbX96zZ{D5#IQL?>_-Ym{j^QGsU;cd0 zWSA9i{;qx%4K{eyL2l3^wiW$JOqsAvXw+EjQy{$$a5`2hbtaT7egaSEYB6Aa$ZNY|@$~xx$)jVwcoTJI5eKW^^RZn@GL22M3#}NC zDAo8-^lk^CugI;^yR$0gDf zvjy}88LY$qT{e>yFIRj_2NW~UWna##b0)L&HtC4EQ=H1kWJShpzavEy7`C|LqnV7^ zy;v9cx+vW>BqrHz&eTUl@>3Ad+b)4S7Jrl6uhmoXT8{R6(V7V>?&HBnnIc`@^)6HS zPieBd)lvJwI94|`XG(8pJAJWM?!DYH@(4!%HMOJOsYtfSyM2L42 z$nb0eIk{)Z>MXyt$2w?KKX^0N<99&rf-u%Uc=*NEw-gB2kF#9w7@l2f%eYlY>-K?Q zU7ad5wo8srpI%luDdc>6l-ca@E)f>?_+TJhWd{uAEF-U@u1`JA;Ce#;EH;#f%n;mX T>#8}DF99GD-0;;0L;m$YAymj0 literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/large-file-audio.png b/qml/filebrowse/images/large-file-audio.png new file mode 100644 index 0000000000000000000000000000000000000000..9bd0e172858f397bdf6413566529cda11846781d GIT binary patch literal 1075 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSEX7WqAsj$Z!;#Vf4nJ zn8$)Jqhx~{kij5X;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6axeE22U5q zkcv5P@9y;zNfbHuv3Sl^hl6VO8mFk3rAePQG&5efQD8#Xv}@9a)2@gKMR3nL>yvzA zQrm?ak3};Mi3vp(>m(>C8g_gxJomw7e^<@}+sS9`^$))}w128D^OZOuTF7 zdVpcp_0=3%^O^5VNu6o7H!Y>!l6{rZ1fA1%rL{ig2W1!7?!Cb&;qdv{^?iKujLaGL zmVMD+V!N@F)oMb4WNC#JV3~V0vW)6buOm znsH!P$AXW)nJ$!;tWObbytY?-qP0UkuS?4IcS<}9?oZmqZNN}q!@gqW>uSZ>cE=y^ zp#=m|m;jmY?q)MywA*d*>$zGHJHv5?1BMI*JPb0-Q0Dh{RtJ1*>(9sC-ocCgebgo&Z$6lC_{_Yn6Ps4)&AurtC>Z)TD{Z+_(W+JP&p!XW_v+QF@4rp< zYd>Vt=z6qsRi3)%ro6Q7tB=#_-)%S8EGiheQqF(Uli0{rI2KKm7~j@AG9v8W?w<*Tsx=hd8o4uK+vmo8n(Y+T)Faes0(!{wJH3swaSR<>E( zpZzLx&Ef3tDC-zMQ;zt20Q%Vb-!@ zV{g|d8xE%OpVjfbTvB)2yb2`I(BU!fdHUv?I>A@>?#f>I>Ryi7=}$KHuTR(bzYAy5 z&d$!ZcYWd;DDpnEG}`6b(@#>r_y15-0(y9I>dioTptpn_RL_4_7POq@qn2+Q*Y|ba z{jCLzA?xN{kiGIVma~x0p^k|70A`AHAHOqydHes_!pE>A^5vKNd;z(4h)e^E(0AtR Yjqhz=c0V)$n86u5UHx3vIVCg!02h_JJOBUy literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/large-file-image.png b/qml/filebrowse/images/large-file-image.png new file mode 100644 index 0000000000000000000000000000000000000000..5c1eab53a9e7c035ab12e149fac4e0e26d348cae GIT binary patch literal 1998 zcma)-cU05K7RM()eh2}i%2Fa_5kXl*AtIeTbc-n0YyMD5+DItg7kzH|NZq&z-q5ch0$U=9@-xbzHBY zs{jC4PjqtdKt|e+F24@3kMV)8kO7XiCwj^wjVgbFf%xc1C!c5lY6l>cj`kjo ztXlY5lKq^?Pik!a1(BV}O!%25HE;86U&=_i_I;doUiyZMwc{`BBPK3K)KE;59k1_; ziJLoGlj7Gh?R~kosN-?>T`vCwmuMovb7v&E4G7LsP__N1!`(Rph|F<0^iVkJSiN9m!T0&jaV3(09%|X)jAmC!Y{@0KAe2&TnY{J@cY+3Y$l;5Lp8TC39^+1 z)UGp`uo(|Jc?GdaC)0m$Sn?0fwV*5X0wxlyYkwOyBP2WJ5oAwlhas8tbByT{hyhA?%M}((KO&W|SkF;r^Sj8I8n7vZ*?2s1GFg5{i`8CEs|)dX0)f z6@o}y`)52Y4hXpB&D@rj<=&&2Ub;=*bvZOq>rd`CL3g7VP=hlXX?@%QjMN~*vLJfe zh}QGLcJ<*sqZKY3u%7oj#P}Q3zjI2xZ;x7h(#l{cV_d`NH#h$M=JD^B`U+x!JEWBQ zINB$FST-{JkKycKpYyS{8;kEWS0`t%HXFL|EwKK7=UVSOE*f%(IYTCs^}wDY*K3TX z&K5}MG;!~6Cv~d}f3N$7DjWA${%d4p0J2Z@7FsoTy^mTOV%SCeBR9^aA0@Ju1G`Kf?K86=n z)O%CFTrp~0@*{VnCep}3hog7v<94-Y)Su5}RC_*a*rBbICGZm4J#7ZkrR6BS>^k$k z`=@NB`0Vp45q>2P+mf?lQMD^S_b7K#xvFO@AK5~^ap8`Q;-;_r@bmPS%IYu)XJkiP`h+QR4Hcb>d(yV(h$p?Z zO9IUTYDh6<*re<>z>3pIIflIIa~#IV`EI@9^Qz)M-!I>Ie=q2lo)5k>^Hi%5QI|M=(0aQW53kK%PCBZr zw`U=OZ#|fzu!D~7+J0d_k-5meeaqF8whvja=%lg>5^HcRS1ANp?Z^6E{+S=Yb(p^z zLCv~Nw0VnAngOr|J5dq17UQH^YVpV3)}KQnD{D(cS_>Cj$%Bd>y>+-!D34VxmBnB% z;&+8|p0q+AwhCBDSR-;IgkNB!I_KEfOXa}=k=Fg)Dhw^&?uU?2mqwPZ)o1T_lPJDp z%Q+~LPPxhvQW=8DywYkU&QgV;8Q>h9Kglee7Rd}zO8FBDePuX94;tE_y?fguBeuWl zfxhHjpHOF^46?GBf4=PwuiklzSq-{qQPZj%esv{5|AKYP3-^4yH>c z(;Ovuw;fQ?baI{B_mx$Rpp4fNG)9q*%(A3j5v!-EV*0_+b)r{vkHBo)te~u9Xz7t* zFh`9dylolMFfR5&T_TO4LRK+69r9GcmdmRJ4Ic}U&zDg){^(;zw*P&BU&Q2YFA%t} zv|{RlFZqwG{Flc3pU#n`nRO;at9i$G&4O{Ie+$&FolPfFGrIprw*GbG8mgz6;TGpT R^cB%wKs?~;P;DQO^bdVCmsJ1& literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/large-file-rpm.png b/qml/filebrowse/images/large-file-rpm.png new file mode 100644 index 0000000000000000000000000000000000000000..24eff8ab5430d43d96f693e49e4447c1c0977b9c GIT binary patch literal 2424 zcma)8cTkhr7QbHtNKk~J^rlfa7`nj~K`Hvtk!BD?11Jb62qH+}(LO{pNX@Q5f>d1u z8(opIBt9Tf2%xYO3!)$drEfr_DEs+#=Dm3{?~iw9&ONuB`Q0<;{^mC)*~!65QbI`r z03c~&4ZENo`C;N>=-gs*ssnW*VTWue;^>GG_obtI+y(37VF2u-{xC=pu|yX@G2FsE z{Ah?@IPKKMGk`{;>6{G?4D&g4;fzkmMgN;K2b2I1U$lYEDUo^8`GG+yZnC^}T2nGj z*?6n1bWEAFxU-siq@gQplXCxYBQJ29cr8i7T2g}%kaa^2#yt!GuG)Bf32Y#ebS%?= z@*uY%dF-H1UzVLH9xq2Jla?k)7T31c%Gz45z1|;FIrM;6^Nz=V;2+!VSltxeogY>% z_%3+Mdq7L2n^npC*DDuk>5@Q)GvuBw!MR8Q*|#y3DzeB}8PKv4_1^!#u72}S&1CJv ze%+6xALBU~l0h)xR?EXpuX{Fe`1=kSvz2y(4Xh|T&BVv{fWM{)t`+68w{AwRF{4gs zV!4~iA(*+1S}mEqR~5i2YdJexBxRMHz<(?CUL5I@ZBs+E;c&hF_vbO>csY)_Rsx2d7U#Wx@%Ntb$SwQv@<%qr*BgdxEzrb?OFwCeO ze!9N}7k*lW7y8OZ;GclsJK(Lr@hJS{KxokYWm~pB%|Vei6gVuchAm5hVH&ibXkmb#9P=t$$-jiJ}^&GL7EkrwUg| z3}UC(#zs+ZTP=yhQ$&WvouD&?WzXMGH?wd%sH|zTU}R2yW=s$U@$uF+__U;?kCSM z9TP6I`>*3TdO{++bMDp_M$VthYax^Kqo%d#KKEpJZf!cRih4^OD{M-8GX^H#3J(Mg zRu=R5MEIkXpetWfP9o_psc&{Kmoojpwh(M1FqzCJ9KK`nv+vE#%|727$Yk;?nM*le zp+Byuwf4E+%(kRQlFKBf;+l~(eIAIZ&Q^Cf5(x)i>H6bNG}_YL4<9}(!4t;VqG5%w zKQ0Xw|HxPA!1vF^#l=$n_k2^6KjPFOw4$NHW8K061qFpUQ_Ecjgr0o|4|XOdCgvDi z%o{a)lT+PTzN!P3ozfcVS&E$QMly@v#f+(NEOALQVFe0ZSS`Y)Y5L5e<5k89>qkm^ zS>Z>vp?NgSpu~H7Z)rw>Sd7=K@S7*_g62H3R_f$Z+WK6B%ze@7Jr+~6w6bbU z)%ObExM{zRmT~V(*6(@q#@N|;fd)*qvurCUtShla(C|@{VIq_lPF?CIxJVfKo_BK_ zump~KrANn4S3N0p(zv17K_n7G{y9RtoAk7NJ4e`5dY{=Z)*4Jdslx-F64i=6Iu)T` z$<0e4vokYax<}IJQ*&A0lxn29gXJ1UDn)eRt(q6;`dr_0Dzu<-$tfu&%+oU}S z!KvFR^()YU6Ww2e8A6{>M-nDQC$j{pfCF@2BvU4zOm&wpv|T;Z80#iu5~Ryzq>t zKN+#N!@fj({bGN&Ko~Eg_gS-9xw)ip)X@3J_Xo0^6RG;wn9I+rs;XR_jpZ5fVDOKD zfx5=zmiUS2S=!mkMHSFW=x;Hof*0X>FO80l(te&;b;U0) ztw|yAn8?jF$_IzTREW_dt;sa$moM)q#eJD_2Bq0$GGjfu^I^z%p2E>CG!bU>sxeqS zIWVL}$Pl5hJ1z|+oYYwg%5OHcfWN2R6!oonC3Yf)aj4iSg1Tgm$F-}id(;I}2TQzm zec1%npPbP{yu_HtlAk4@BHRiA`L!f2Q+;V+!QM)Nrv?E;5JmHj7@GC}Ha9oNyPYa~ z6@@2(_{X_>xKkO1l#y$QwPOqmm2N_(TnLr$LL3FE z&2BvdOi{(Hp2D~nSrc5KR=et1u|2|X%%SQ)3Oz+{LQ%Z*-MXxLkENARdL0$Vx;M`&CvhRC;V3n(X|eB XnL7>R1#yXJmjrAq9N_Xp-k1LZbsAWT literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/large-file-txt.png b/qml/filebrowse/images/large-file-txt.png new file mode 100644 index 0000000000000000000000000000000000000000..ecdd5118268be04988747f3cfece6aeaa0a078a2 GIT binary patch literal 1709 zcmcIlSx}Q#7`<5_0bz*j1O;S?6fFV)f|b1q5VTYl6QMyiNl-vQkd)0v9Bfh+5eyo@ zfDyE)2vpGohD8yv3Is(qqr^1{qAUgp7191Y*7mh`&Ue3i&UfcLoVhcXvopwFTNA4Z z0MI4}5JRBOS}n9XwD)+%+=m)6-IpAOhE^_mFB{?-sR7~X01zTq3ueOdgATwXgS0y% zG=-kQj5!z&m`tYqz5|KraWSd!_9+J$RWt5b0MzZsM4zyqtKU8SF=1W2ao@ZY-|omE zk~CWL(PqTLg8Z|X>9d75&rBdU<@@C9YfV_2V{4#cP`@E!r_W`|MRtX&(WPQka2YQ- zq~B*MP$L8rsFP3985uX*AL!aXk>x(l6=s#=_I`fnE|x#)$7e0$a<}pJwwuP%ZAF)b zgxVMNF!Y6g;#A}`oG+i!(fzy&(fl;6`h<}12R2&`9i-kZ=gU0|RDP825on5GH;4Y4 zHv+vUl+-NCVfmH3+&Pqpo6uExe?UKT8>9x$?sHkt#PH7xbrZnNs;D{nZU0&rGG$w( zEnM3odaO%E_yLLaMpu3VQR@NSBkC9doedd=KeQSGmZh)I&MdaI3pDg47kMMtyB}H( zf!NYINIq?C1Id15=r%k2$=U>j}5AN17N^)k&D#EAXSXOGbPP1RImyweqN2>$=bueG$^#4&K z$)hTa;3yWEKz$lUx26kiD;h4XRlw6&bc?4 zcasIACXmD7nfKGLW+CV7o)+JG{T>1AksUL|b?>*cW|!MxG}ofVwkz$28dTfzJ}#UA zo=yX7MV91x6Y^T`?SOdD(a~{=t+462Ybl5;FE3wzUUi9bsRk&Vi%K3}_rNM}dX`)m zU?od7aunNp6q6#6NQKpb91-1m$CO(rcHF!pDGnQA+ff#R3whG0jB|(bGErc-OK3vh zlW~;F2p^4FBORn)&AB-8h61LI%os8wck%M$v}#9?8mT&9`CZcPGLAx&EV;fC={5B5 zq0^p*Fta_fmsl`Y_k3x&bG9lU39j9^5k=c?czzt-oV@%bLeABTu5i%eTfbRYr*eANNM^C@G zamiS&R5~G*dYQf-jooO;@;#f%WoN=1R@%5#SRKp*2^6T;v97C295et)5%1Qe6b!_6CIk#$Yv1ZLZJh0?kS=_4{HuQUF)X z%uJ%9?>k+1GT2|^*IHwZ(d$+y6zX(OVSeZ`ZS1MR$78y_L||oQ#oK|yYt<&($n_qa z<#0GTzLOM7HoErQxi~K{9Jj=PncMV(u?fxo^FVkvD?!X@^BFT+ea*td*zu8(U9FoF zQ7q9N6MR?Nn@0`B8S`GZ!uJDGo)rb$#LQ=UKU)U+`-xwm zs=+kPkIFM`n^+O+wY}Z`3&QRi)A6NpJ8n|88YT#_iUR}3)i27)6hyq@8=e0*cG%qz literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/large-file-video.png b/qml/filebrowse/images/large-file-video.png new file mode 100644 index 0000000000000000000000000000000000000000..f3dbe68feaebe78bd1faae08b891f4a5b97d7965 GIT binary patch literal 1115 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSEX7WqAsj$Z!;#Vf4nJ zn8$)Jqhx~{kij5X;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6axeE4Nn)x zkcv5P@7VfDJ4zh;Sk82G8H1|%1DTL@hXka$b~x;FjN)rp`h+j$09UK#i!zti9gL#6 zv$m*ob>}47XrAHP@M!DYxpUV){}~$Sl#ue*_;Zze-==F^537W$7)8XG8ICg?Fk~p; zVUPhcbGQFxJ0@Z0F{4^!qVU?U7GIpq4$2;wt1I|3t>>pmmE!CKhP>SF6Ruu@1`OXP zbuIYHv%w>K?X|8JyTZq<-*Imn`JWB>h~HdDT#mj zW99z+U4ykld(aI|35VlJ{SAy20>vzI&oX4B|I^xdj^z%cO;@3M0>iwcVvNij3d=Yo z927RX9AIGTQe#M10FkZ1x&~xB+XkmV&y`r4) zYr~I&62=#z>iPFBaNWcGfw|7Xh2Mdt#vz5_yaM}?H}9CDR4kZ6iKHJbyC4OO6 z(5v>X-xjWZ|NiyY$L@#Cz9+1e6>}}T_xkIy)%$JocoJ%Z1uet9c(-1g`}g$sUdAp?8P183=ruXlGKmHI&}T0XPGDtPWIF9#yaR#U{m*=N$KcrhEGk5 z4ap1-D#Qge@3BsR=t0djKvPde?f$h|O6OkG#Q5C#5=HFLe6=>6!ArUv-F{|<@8X}8 XZ%*&A`lJUe2pBwF{an^LB{Ts5Xj#h= literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/large-file.png b/qml/filebrowse/images/large-file.png new file mode 100644 index 0000000000000000000000000000000000000000..6f3c825d061617471b33c333789b8fe47dd181a3 GIT binary patch literal 669 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSEX7WqAsj$Z!;#Vf4nJ zn8$)Jqhx~{kij5X;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6axd3l&6bh zNX4ADw+*wH4Fw!7-mKtX&b;I!>tiL$2EOSE;+={tK{Kv;DgXOFb+zNsaXTdy73 zvG$GxfEtaq<5S_(pRdcSSeRbi zHx+YIXgK$`=JRw%g$Cg$&PE2mEKwFFi;bcJ91ossQWiMSU<4!o&nd$6QhCb!jn7W8^Cbh55`(9!pUXO@geCwfrsDnp literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/large-folder-link.png b/qml/filebrowse/images/large-folder-link.png new file mode 100644 index 0000000000000000000000000000000000000000..b7295d497bb064f18e1c952d866694cba1fe9437 GIT binary patch literal 1322 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSEX7WqAsj$Z!;#Vf4nJ zn8$)Jqhx~{kij5X;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6wsjMo-U3d z6?5Ls*_$WrF4B5_4Rg98W7d@=LDTm%%$ic7I8mYHfMx^>$As@5%1J`JS=>Ve0kcsHj{If9C4FwaCYqQ zU?@^yuoP;L;dJO@QE+FPpv-t;BEu6Z3eP{{r(kmJ))k4*<*wW3OC+z*$PoF+L{-~ddh|q!g$1CUkzahq{;$Ru2+?aX$J4@IewjUGf8EPKS`Mq=5 z{q0j;zkXfb-roNIS6}vBr44Cv%oTY(opag0egD3nt>B%tUog5~GE3wWEM)2?s+|1( z`Ff9G|F>`7_VrE;*4d=}|TdMYozEt|hy>iQSE11ty1Ka0;P-m!CM=M=w? zc{)43-QMNcJC8{Z2tHou74H678|$!`)xy^HEaRRi=@Z?t;ukU)m)*F*nWKG7Vz-_f z)0XM``}+IuujS6#!*I@0+PYmg_3a=2oJNMnA1f};?ckm0&b`sKX%Ux!RPQ;U8!~O= zmP_9`)^=)n>J25wi!2hoZs`nuj3t|Oto5}I{{6d`cV>8W!MWV`A_n1qB$ujMSXx%{ zeq|C%>}I*DccAIfc@6Ex<|;<{iO+ZH3Wcv`xXgNgy0+6PuY}X-SN-0-dzUkDM-=Oe z60849Gk=L3I4zoZPe>w}>ukA>#q)c7Y|T#W$=UOntmgWKHL{d_K6A@?lFGJ%oYqR6 zrA(YTGtNXc>q<%FN=pPaDcR<$b>%TOp6Fc=Amq`|zrmGx@%AQXkAjktFKqnn&PSNH zWh9g^HYf;LJfBm=82rgb--fl~uW}kN?5C-ml>R@Z$#C+6%;lF)+WnZx`k}IA&Nkld zn>PplIJ-gApTS?ujStd;HbPB(Pz6<}AJ0OfNp#Dm>oG z_Tu~Ra0acPI$tVm{D48kej!$H+nOH@EHe^oo3=mV6j<=%73-ekzgXPG6X)dX!z|!0Uqv z4@zy;l`(4R*eNisdEaoWza=!H#(^o~KI2w-PUa7y~Ygo zIzKMD9cBMwXqfX`IN=x*rJI=I1ILL4P!_>}^;ypLK?M zmhy`0ujek!tP)!B!#L+awQ1+?yLV&XI~!h%K6tq`(Ie-S@mlrjnAv*ylp6k1?=!>Bn*ouN?wz%FBd9 mA2}6lCQvmWV=E*6u_tZ3F5Y%t^DVGkWAJqKb6Mw<&;$U7-5g2) literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/large-folder.png b/qml/filebrowse/images/large-folder.png new file mode 100644 index 0000000000000000000000000000000000000000..0bfd0e2e4999f1acb86e176e5153e47d527e5eb2 GIT binary patch literal 966 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSEX7WqAsj$Z!;#Vf4nJ zn8$)Jqhx~{kij5X;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6axdZv!{z= zNX4ADcXtO!NYvF>^t+E^UoL8I3B#8Icwj& zd-wj``0eN>^iuZ0-%UFA<>ch{z4@!4+$qwaa#!c{r@0)bT)7>TnzRI&Cv+@`xWb^K zq#%~;?zc@kto|HnvjXkCg3>z!68yJ#4Ut?fA@@y?5Q^(n8W>rQTuc~)-A~Cl&|Jz%( z+KhR|^Gd<(4T5vy<9CNHozGKre{%x6o}^O4ju;~shQgh|2z*p@Qh@Qu(@Ba<9j8^D zmogS`OL$-8bgH0in<$!L2#8GpGmH%ct$6EHkE;DZI=IDSZ8VNu&JDqfK{q0X?*%bRt7i z!D}VPBM+{*GIbnW+sz`-94%hW^gyhD=Pb}i$_#mo?H6m9C;Y0f++zLx;(6QCECzPp zb%YQ6xU<=Z;c@YKBgP(>;CHqU8ZTFVPF`?#vK7$EbcXf+|L=MKQ{qL@{bJ?`OEM#O peKXu~Z$FcP1rZ4sXUeW-bS?dyX=J-~6EM#)c)I$ztaD0e0sypCi1Gjc literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/large-link.png b/qml/filebrowse/images/large-link.png new file mode 100644 index 0000000000000000000000000000000000000000..f288a6624a816bc557ed538e6a91dc81d1048667 GIT binary patch literal 890 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSEX7WqAsj$Z!;#Vf4nJ zn8$)Jqhx~{kij5X;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6axd(cTX3` zkcv5P?;7Uab`WvB=pV^CeZtZuDbE*9IOHm|UhP!cA|<^TmQOp9XU^C#ViSZ3}?s} z%>K%xr~5qSwYc1$&W_(xE|q_EdUk{H)XvV1e1Cuc`B%ATDmQGp=6m@~R#yYVsnuFd ztF4*$ObyDKa{hn6@kzy|7r*~4{IMwQ24l&p8=M>q_DsKd{u2ksf(@Co-Ys4PV(fb_ z57Np4B3!aM8yH%$Iv%`w&wS$CqhrQPXH%9;RL+)=fG4J{WeMYxIolsw{F7)~7`pE8Or3@~pUsr^ zxg2?G(bw;?K#?=S><_b&fTrAmi4Wxk8XYb&#PQe2``ff|aA`|9Trbtny?xL1$mh;> zK}W}<`i!yHdtCzfPbyB}c%@h^>A_Mu;U0$)lkLy72}&0xu?sk|c%|}8a8yxx^3vhHzK$VTQlopT>F-AiWJvEaMJf#TcSVwm)vtT8^2w)v;5VE3QB ze+_s*t`aP)ew4`b|In%%j1v3$Y#7?stMWEHe0`EB;YaBNmIEJN2Li?7r$3VCO#V@- z`ETNejvtke6nXYgTe~DWM4fz3gCT literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/small-file-apk.png b/qml/filebrowse/images/small-file-apk.png new file mode 100644 index 0000000000000000000000000000000000000000..eb07ad490cc700e54f521b598812b41afd114251 GIT binary patch literal 741 zcmVB%tqKzCI8j2GQ5|4nc zqg+{tH-HyFLNvfhQIwqnQ6R)CxJ*go28b_~^0Pxh0?B&)6NQlGrdrL8=lf>n+tnJG z84hXUpy-zb0Kh5RP)glJ#B%_+wj;&`GvAM*=(BATZ6YM1I{@$=5g+z?y)V0-YP| zMD!w#2z8-0z^~*@RW#7 zmacCA@UjNnYFpogN~u$3ehmO;0N?|FkBFZD;Ehu1wC3|dan=!3bpR2cESGZ4Esed# z1gs;-^SqQm7>0fgNF@O?x0QhBc^g4QL`(3Hh${6Fh9R!U*X3^7fnF7;#meH*bXY~0 zW!c`INoH|Y5$+2d0MIPXIs)JK%?_Mz2~-we$ZaJs9*;MIEMls(iugiq>j=^`tJP9thXg7>41Wq)`-|*uVNqz%0(%0V(C)PJvmRRRaz3m}B_R5Wi5I z?McR3>syZF=*42ej^k*pHLuo-#e$EHj`Vy!=RD7Kr_U^8G#Wqw-6EG_lR&ACrlqA3TEE$t*vc7tR?Wj?cje#mjr$R X$ZawC6x;G@00000NkvXXu0mjf*?34A literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/small-file-audio.png b/qml/filebrowse/images/small-file-audio.png new file mode 100644 index 0000000000000000000000000000000000000000..12ac2e97b00c9b74ea59b5a364a57d6937c4475e GIT binary patch literal 397 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=E81o-U3d z5v^~h8|EE0;Boyg-W8{ue&U|yZ>#laQuo~T*V^sRq)+&vE!Tg1hNxKD;qXoHs zFh{u@u2T8R*)b^~c;Ty;3~{@p)6BN2q%+Kxcs%`ia#?gjT9;wbo0r#F>KNYG2rP}- zz`VhH^L8zU>t|vGj_NHGU$lbTM&M}fjOG_N3;6DC{3LiFJFuZzOpa-_9p`H1o~*|7 znCj&(|F2>A@FVrN_f_o;3_1*v;wHYExfE+WZZR;Ne60L&B?FIQjaSrU9tMS%b5$4w z7>quykv+ccL3Pp{-UU-GO?%>a$@5a@6UC)lj^>x`tcaVG7j}Ewt9NI&w*8B}%gTLV o0dIQ6yq#C8XW2$yjV@rE8UKv)lrFCyFz6UOUHx3vIVCg!006j|&mDqUu*p-nRb@jO6~cuyqh2py+{9{3 z1j;Meg&>XOZo4#gy}Bb6W({hBOXJQDkUW54z@VGLE3E8~G`3dR6ge=>?ChRz=Kkz1 zi3qBcs?;68hxCNCcGDwy25_Xcp7|`MbO@}qqP3>?x+fd>383Qxv8@nv09;#Z>y-c` zJNJUvS`zv~Y_$q}AhuqH>mV{^m{uIefphK{KsT8h20;)u8Vy%;&x%sM0kHEXfW-V* zL{0&Khz8C%02t-1Usb|+U_K$}ib&5Gb8C#b6_FlZPh(^Kd#yM}Ln0b5a}Rh40f1py z0BcVm2!aqmNJJwd8UYBCxr~_kiZhN zccs)-9LIaU_j&_Nr_CAkVXTDIY)p`(-zE3_B0_5#* z@nRvOuSMny8Dl~KckA+@5P*n2<(X)$XG*Dkug@)|J_9&hnGa?4fua#bQO#Q0FM765 zE8a-$cKaT{ebM#GUq=4{DhJ3-!V-SZyp{Pi``fBK1it~Omx>a))FagZ0000m?_9XwjoN_y>5(B8cME9tt6cnu}0cb1^$_ejQ4P5v0(ITPSo7QK7O_)U26JcIN$h za5wBuX468FjR#+NVR+tWzR%}*-e=yK5t7)?q4qWUM*skBw*}w#&x*(lfKPWA<1NVx zTI=7E7A2_(jWJ&VxGy3Xhlht3cM`ps$S+E%ONByVF225H3reNZmn5f1ez()$0HC#A zveu4RYbT8{-^ACqRKOVX20;H#19uq>rPSdvc0SGV*F}ZtiFlMdwI9)mksLwByMo0c=u=$e4(nAXyQSQ^uH4rPMz_|gPoHrd=O%zN>2&%UfZI_NjRCj<;8y@G0BI%H8i`ww%jGtmNdDn@-e~}T z0{FTOuBepy0zlevoNt@HDy#gyFCq(2k5gBUpdk)|p$>Un z>A#W#x-?<2SZq1{1c1%#ZivV~BtH?6>tPs{+ixN{E+Q}T`Fst)q}CcUGc%JSvc64n z0LhN&`@SV9ar)Lt*zNi zCc|2-W`iJLHk)O&TD7@cj+IKqmdj-pi$#m~JEk?|Vd=ekyJg5k<1Y t)^?p_6VLNH8Zq&+yq9C&|BOBYyaU%|^ppL1;=}*|002ovPDHLkV1lb}olF1# literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/small-file-text.png b/qml/filebrowse/images/small-file-text.png new file mode 100644 index 0000000000000000000000000000000000000000..ecfa86178b627c34b8ecaf54455bda48a009b8f7 GIT binary patch literal 552 zcmV+@0@wYCP)kEBh#b2MV&x}Dpm9Jc}FH;>|9z zzkM_RVb>(lq_rB|9cYg_QcBnTB?YkTz5iThv5lr6r6ljYRo#7ffFoeF3`AWiSOwmu zl$II+7@oWoL~TV_7NS-al!2&ShF3vckl`e3jFdkCGqnPYlkjC=jd#K+z!YE#Fa;Rf z3H$y21LxeMvpzHXJs1p5%xpG<@Btv_?14X$W`J8GVZM0p4=Mq)+wDb3J>U*72OLQH z=A2uX^ggAuBI%czMPS9u9!t6pJOsW2hrl*a4WQfY?f^SE=M70c@BPB*T#Rwc%-)z; zA!#>+un+7H=Z=7PA%th+;=?s+pwsER25tlQ%kEBh#b2MV&x}Dpm9Jc}FH;>|9z zzkM_RVb>(lq_rB|9cYg_QcBnTB?YkTz5iThv5lr6r6ljYRo#7ffFoeF3`AWiSOwmu zl$II+7@oWoL~TV_7NS-al!2&ShF3vckl`e3jFdkCGqnPYlkjC=jd#K+z!YE#Fa;Rf z3H$y21LxeMvpzHXJs1p5%xpG<@Btv_?14X$W`J8GVZM0p4=Mq)+wDb3J>U*72OLQH z=A2uX^ggAuBI%czMPS9u9!t6pJOsW2hrl*a4WQfY?f^SE=M70c@BPB*T#Rwc%-)z; zA!#>+un+7H=Z=7PA%th+;=?s+pwsER25tlQ%K~z|U?N_l%#6S>z2_okY_!a(uaK&kHOhQ^a8w>5O_Zzgk zkFc>mW=NqaY{b%EI1v1RBVgFENn^5Zl1*}HZeSq^GjAXBGPARRs$xnDC*2*uQT2m} z^i9bRfJ-8JYvrw~e-<;V?YZq@W@Y9tW}dh4w@M%)3lUlL$ifwXXAzk zP17@fF7k=Peth(>e@ zU?U>yEX!tH@rJB|Ri5W30B*bB4b_OYs(MXCcU=SfHliJXhoUGRlO);w=j6ikl|TVJ zsp@?cMcXEpE<7uNCNsvI$8r4b6I-*PC!(%+gP#Rm@g|>&4-yRAE)y!$t gk*!8x-!{1fzp}w8Gr;c$B>(^b07*qoM6N<$g7F;76#xJL literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/small-file.png b/qml/filebrowse/images/small-file.png new file mode 100644 index 0000000000000000000000000000000000000000..632fd83a1d79682d087afce72157c71e5b345466 GIT binary patch literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt;1XT^vIq zTHjvZ$lGAR!*YP%$;Z|8DXWnxca7^&gEtO>Qd&(1n0lmlyLjw-P~#G$$vW@qjxzQO za`F>uetU*7G}sxtrnJ)YSrMvje&fd>h#k z%$1{8jz^plaE=S6Sc6clIYR Y_g7nQRyJ{o13Htz)78&qol`;+03PXI&;S4c literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/small-folder-link.png b/qml/filebrowse/images/small-folder-link.png new file mode 100644 index 0000000000000000000000000000000000000000..d556cc8198922dacb773402b30b0161c41fe63d9 GIT binary patch literal 537 zcmV+!0_OdRP)xkpX9eFicYJH#gLUS&+|UXFaLS;2q2dRKiutc8#d?vZ1(nnh9kF~;nZq$=7+(s=Km0rai4UxA8~FplFf$%dqD z@BNmf?JAe|z6oFtz+^h+QUK@N_cTqrGhcC61$u#QRstY-AgNP^ko0rS;!lzWD*+@) zazgT~3?b#+ZB#Adce^ zfVuuj)3ht8L-Nd8d+D5OlYCy|B(>9MG~NJ~{(^GxBuP#HU^1E9N(x>-NSb!LeZLZb zq=#~G0BE&Zzwz2iq5As|P>7YzxqASIfdE(5+ISj-?uP5M;-DM^Iq;s^_<5iPr~zt# bjRU*@+m6TXk%wC(00000NkvXXu0mjfD9`7< literal 0 HcmV?d00001 diff --git a/qml/filebrowse/images/small-folder.png b/qml/filebrowse/images/small-folder.png new file mode 100644 index 0000000000000000000000000000000000000000..ef843958234be540e1f92eb10fccae662b3a9641 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=E8>o-U3d z5v^~h+2%Dn2(;ecJVB?XY3&1nEt=a-%)K1+gy&_FmnS4Sq+g(zwaMq!w!IU3l$NZTwCv@*sM~KD4n*CaXmh-Q znc?K-t=pLI_w~&3n+48QbkiCM5<36h&kP_T=;3b3q0g1~UClp0uUHA_3F{r(3*|_=F)Ewa( bEbnG+sO@~}U&Kt*3YT^vIq zTHj9H$a}~@!1cdk%ZrE(r9~f88NP8yZ(w}m=sdl+xhX*+N=qqFhIfI7hFOGR-XCf6 z^uOx0zvWkI1!YaSV#}P;z{8LrQ{?HgPqW{;fz^0^tg}dvi_)SqKKmpY#2LKBUEXj^ zV<=NHdN0k<^?-fuyyuJuo~&oM+Yyt0OtP_K&DyQA6%W=iDJ+_~D$G!E4#Tk-qJp`} zPJ8Y" + } + } + } + +} + + diff --git a/qml/filebrowse/pages/ConsolePage.qml b/qml/filebrowse/pages/ConsolePage.qml new file mode 100644 index 0000000..96f4874 --- /dev/null +++ b/qml/filebrowse/pages/ConsolePage.qml @@ -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 index 0000000..d228fba --- /dev/null +++ b/qml/filebrowse/pages/CreateFolderDialog.qml @@ -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 index 0000000..eb55286 --- /dev/null +++ b/qml/filebrowse/pages/DirectoryPage.qml @@ -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 index 0000000..1519d41 --- /dev/null +++ b/qml/filebrowse/pages/FilePage.qml @@ -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 index 0000000..cdee48a --- /dev/null +++ b/qml/filebrowse/pages/PermissionsDialog.qml @@ -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 index 0000000..2e0d11b --- /dev/null +++ b/qml/filebrowse/pages/RenameDialog.qml @@ -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 index 0000000..e11d8e8 --- /dev/null +++ b/qml/filebrowse/pages/SearchPage.qml @@ -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 index 0000000..454d1d5 --- /dev/null +++ b/qml/filebrowse/pages/SettingsPage.qml @@ -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 index 0000000..e0b945a --- /dev/null +++ b/qml/filebrowse/pages/ViewPage.qml @@ -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 index 0000000..932ef7c --- /dev/null +++ b/qml/filebrowse/pages/functions.js @@ -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); +} diff --git a/qml/pages/ConfigurePage.qml b/qml/pages/ConfigurePage.qml index 1692d1e..1615c97 100644 --- a/qml/pages/ConfigurePage.qml +++ b/qml/pages/ConfigurePage.qml @@ -34,16 +34,42 @@ 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 + property int _fileDialogue: 0 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 @@ -95,10 +121,6 @@ Dialog { 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 @@ -107,7 +129,7 @@ Dialog { width: parent.width label: "TLS direction" currentIndex: VpnControl.tlsDirection; - enabled: false + enabled: configureTLS.checked 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") + } + } } } diff --git a/qml/pages/ConnectPage.qml b/qml/pages/ConnectPage.qml index 23bd65b..6d32705 100644 --- a/qml/pages/ConnectPage.qml +++ b/qml/pages/ConnectPage.qml @@ -148,6 +148,34 @@ Page { 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 + } + } } } } diff --git a/rpm/OpenVPNUI.spec b/rpm/OpenVPNUI.spec index 5ee1235..93b06cf 100644 --- a/rpm/OpenVPNUI.spec +++ b/rpm/OpenVPNUI.spec @@ -21,10 +21,10 @@ URL: http://example.org/ 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(Qt5Core) +BuildRequires: pkgconfig(Qt5Qml) +BuildRequires: pkgconfig(Qt5Quick) BuildRequires: desktop-file-utils %description @@ -63,13 +63,13 @@ desktop-file-install --delete-original \ %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/applications +/usr/share/OpenVPNUI +/usr/bin +%{_datadir}/icons/hicolor/86x86/apps/%{name}.png +%{_datadir}/applications/%{name}.desktop +%{_datadir}/%{name}/qml +%{_bindir} # >> files # << files diff --git a/rpm/OpenVPNUI.yaml b/rpm/OpenVPNUI.yaml index daa3264..6c5cd7b 100644 --- a/rpm/OpenVPNUI.yaml +++ b/rpm/OpenVPNUI.yaml @@ -12,19 +12,19 @@ Description: | Configure: none Builder: qtc5 PkgConfigBR: -- Qt5Quick -- Qt5Qml -- Qt5Core - sailfishapp >= 0.0.10 +- Qt5Core +- Qt5Qml +- Qt5Quick 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/applications +- /usr/share/OpenVPNUI +- /usr/bin +- '%{_datadir}/icons/hicolor/86x86/apps/%{name}.png' +- '%{_datadir}/applications/%{name}.desktop' +- '%{_datadir}/%{name}/qml' +- '%{_bindir}' PkgBR: [] diff --git a/src/OpenVPNUI.cpp b/src/OpenVPNUI.cpp index 40e28ef..2df2800 100644 --- a/src/OpenVPNUI.cpp +++ b/src/OpenVPNUI.cpp @@ -38,9 +38,17 @@ #include #include "vpncontrol.h" +#include "filemodel.h" +#include "fileinfo.h" +#include "searchengine.h" +#include "engine.h" int main(int argc, char *argv[]) { + qmlRegisterType("harbour.file.browser.FileModel", 1, 0, "FileModel"); + qmlRegisterType("harbour.file.browser.FileInfo", 1, 0, "FileInfo"); + qmlRegisterType("harbour.file.browser.SearchEngine", 1, 0, "SearchEngine"); + int result; setuid(0); @@ -72,12 +80,16 @@ int main(int argc, char *argv[]) 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(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(); - result = app->exec(); delete vpnControl; diff --git a/src/filebrowse/engine.cpp b/src/filebrowse/engine.cpp new file mode 100644 index 0000000..b5418e1 --- /dev/null +++ b/src/filebrowse/engine.cpp @@ -0,0 +1,353 @@ +#include "engine.h" +#include +#include +#include +#include +#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 index 0000000..c984958 --- /dev/null +++ b/src/filebrowse/engine.h @@ -0,0 +1,98 @@ +#ifndef ENGINE_H +#define ENGINE_H + +#include +#include + +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 index 0000000..25c4ef8 --- /dev/null +++ b/src/filebrowse/fileinfo.cpp @@ -0,0 +1,183 @@ +#include "fileinfo.h" +#include +#include +#include +#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 index 0000000..9b8337a --- /dev/null +++ b/src/filebrowse/fileinfo.h @@ -0,0 +1,95 @@ +#ifndef FILEINFO_H +#define FILEINFO_H + +#include +#include +#include +#include + +/** + * @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 index 0000000..562f29e --- /dev/null +++ b/src/filebrowse/filemodel.cpp @@ -0,0 +1,337 @@ +#include "filemodel.h" +#include +#include +#include +#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(); + 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 FileModel::roleNames() const +{ + QHash 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(); + + 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(); + + // read all files + QList 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 index 0000000..0704d14 --- /dev/null +++ b/src/filebrowse/filemodel.h @@ -0,0 +1,90 @@ +#ifndef FILEMODEL_H +#define FILEMODEL_H + +#include +#include +#include + +// 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 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 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 index 0000000..4637862 --- /dev/null +++ b/src/filebrowse/fileworker.cpp @@ -0,0 +1,266 @@ +#include "fileworker.h" +#include +#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 index 0000000..7b1ce5c --- /dev/null +++ b/src/filebrowse/fileworker.h @@ -0,0 +1,58 @@ +#ifndef FILEWORKER_H +#define FILEWORKER_H + +#include +#include + +/** + * @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 index 0000000..2ab117f --- /dev/null +++ b/src/filebrowse/globals.cpp @@ -0,0 +1,109 @@ +#include "globals.h" +#include +#include + +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 index 0000000..d96523a --- /dev/null +++ b/src/filebrowse/globals.h @@ -0,0 +1,22 @@ +#ifndef GLOBALS_H +#define GLOBALS_H + +#include +#include +#include + +// 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 index 0000000..9ba2671 --- /dev/null +++ b/src/filebrowse/searchengine.cpp @@ -0,0 +1,67 @@ +#include "searchengine.h" +#include +#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 index 0000000..47a2d0b --- /dev/null +++ b/src/filebrowse/searchengine.h @@ -0,0 +1,51 @@ +#ifndef SEARCHENGINE_H +#define SEARCHENGINE_H + +#include + +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 index 0000000..0b44930 --- /dev/null +++ b/src/filebrowse/searchworker.cpp @@ -0,0 +1,105 @@ +#include "searchworker.h" +#include +#include +#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 index 0000000..2714d53 --- /dev/null +++ b/src/filebrowse/searchworker.h @@ -0,0 +1,48 @@ +#ifndef SEARCHWORKER_H +#define SEARCHWORKER_H + +#include +#include + +/** + * @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 diff --git a/src/vpncontrol.cpp b/src/vpncontrol.cpp index 7bd002f..057cb36 100644 --- a/src/vpncontrol.cpp +++ b/src/vpncontrol.cpp @@ -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(); + settings.setValue("showAll", false); } void VPNControl::initialise() @@ -33,7 +34,6 @@ void VPNControl::setStatus(VPNSTATUS newStatus) if (vpnStatus != newStatus) { vpnStatus = newStatus; emit statusChanged(newStatus); - printf ("Emitting status %d\n", newStatus); } } int VPNControl::getTlsDirection() const @@ -44,7 +44,6 @@ int VPNControl::getTlsDirection() const void VPNControl::setTlsDirection(int value) { if (value != tlsDirection) { - printf ("TLS direction set to %d\n", 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) { - printf ("Use TLS set to %d\n", value); useTLS = value; settingsSetValue("useTLS", value); emit useTLSChanged(useTLS); @@ -74,7 +72,6 @@ bool VPNControl::getCompressed() const void VPNControl::setCompressed(bool value) { if (value != compressed) { - printf ("Use compression set to %d\n", value); 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) { - printf ("Port set to %d\n", value); 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) { - printf ("Server set to %s\n", value.toUtf8().constData()); 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; @@ -128,8 +134,6 @@ void VPNControl::vpnConnect() { printf ("Process already running.\n"); } else { - printf ("Connect\n"); - vpnProcess = new QProcess(); QString program = "openvpn"; collectArguments (); @@ -191,7 +195,6 @@ void VPNControl::addValue (QString key) { void VPNControl::vpnDisconnect() { if (vpnProcess != NULL) { - printf ("Disconnect\n"); vpnProcess->terminate(); setStatus(VPNSTATUS_DISCONNECTING); @@ -201,21 +204,21 @@ void VPNControl::vpnDisconnect() { 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")) { - printf ("We're connected!\n"); setStatus(VPNSTATUS_CONNECTED); } } } void VPNControl::started() { - printf ("Started\n"); setStatus(VPNSTATUS_CONNECTING); } void VPNControl::finished(int code) { - printf ("Finished with code %d\n", code); if (vpnProcess != NULL) { //delete vpnProcess; vpnProcess = NULL; @@ -242,3 +245,26 @@ void VPNControl::updateConfiguration() { 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); + } +} diff --git a/src/vpncontrol.h b/src/vpncontrol.h index 542ec95..33dbc38 100644 --- a/src/vpncontrol.h +++ b/src/vpncontrol.h @@ -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 (QString logText READ getLogText WRITE setLogText NOTIFY logTextChanged) + private: QProcess * vpnProcess; VPNSTATUS vpnStatus; @@ -37,6 +39,7 @@ private: bool compressed; bool useTLS; int tlsDirection; + QString logText; void collectArguments (); void setStatus (VPNSTATUS newStatus); @@ -55,6 +58,7 @@ public: bool getCompressed() const; bool getUseTLS() const; int getTlsDirection() const; + QString getLogText() const; signals: void statusChanged(int status); @@ -63,6 +67,7 @@ signals: void compressedChanged(bool compressed); void useTLSChanged(bool useTLS); void tlsDirectionChanged (int direction); + void logTextChanged (QString logText); public slots: void vpnConnect (); @@ -77,6 +82,8 @@ public slots: 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 -- 2.25.1