From: David Date: Thu, 13 Mar 2014 02:18:11 +0000 (+0000) Subject: Integrated file selection dialogue with the main code. Improved the X-Git-Url: https://www.flypig.org.uk/git/?p=openvpnui.git;a=commitdiff_plain;h=e24363e314aca32e7bee952f02f517a04a8dc5f2 Integrated file selection dialogue with the main code. Improved the configuration dialogue. Added logging window. --- 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 0000000..e56e762 Binary files /dev/null and b/qml/filebrowse/images/large-file-apk.png differ diff --git a/qml/filebrowse/images/large-file-audio.png b/qml/filebrowse/images/large-file-audio.png new file mode 100644 index 0000000..9bd0e17 Binary files /dev/null and b/qml/filebrowse/images/large-file-audio.png differ diff --git a/qml/filebrowse/images/large-file-image.png b/qml/filebrowse/images/large-file-image.png new file mode 100644 index 0000000..5c1eab5 Binary files /dev/null and b/qml/filebrowse/images/large-file-image.png differ diff --git a/qml/filebrowse/images/large-file-rpm.png b/qml/filebrowse/images/large-file-rpm.png new file mode 100644 index 0000000..24eff8a Binary files /dev/null and b/qml/filebrowse/images/large-file-rpm.png differ diff --git a/qml/filebrowse/images/large-file-txt.png b/qml/filebrowse/images/large-file-txt.png new file mode 100644 index 0000000..ecdd511 Binary files /dev/null and b/qml/filebrowse/images/large-file-txt.png differ diff --git a/qml/filebrowse/images/large-file-video.png b/qml/filebrowse/images/large-file-video.png new file mode 100644 index 0000000..f3dbe68 Binary files /dev/null and b/qml/filebrowse/images/large-file-video.png differ diff --git a/qml/filebrowse/images/large-file.png b/qml/filebrowse/images/large-file.png new file mode 100644 index 0000000..6f3c825 Binary files /dev/null and b/qml/filebrowse/images/large-file.png differ diff --git a/qml/filebrowse/images/large-folder-link.png b/qml/filebrowse/images/large-folder-link.png new file mode 100644 index 0000000..b7295d4 Binary files /dev/null and b/qml/filebrowse/images/large-folder-link.png differ diff --git a/qml/filebrowse/images/large-folder.png b/qml/filebrowse/images/large-folder.png new file mode 100644 index 0000000..0bfd0e2 Binary files /dev/null and b/qml/filebrowse/images/large-folder.png differ diff --git a/qml/filebrowse/images/large-link.png b/qml/filebrowse/images/large-link.png new file mode 100644 index 0000000..f288a66 Binary files /dev/null and b/qml/filebrowse/images/large-link.png differ diff --git a/qml/filebrowse/images/small-file-apk.png b/qml/filebrowse/images/small-file-apk.png new file mode 100644 index 0000000..eb07ad4 Binary files /dev/null and b/qml/filebrowse/images/small-file-apk.png differ diff --git a/qml/filebrowse/images/small-file-audio.png b/qml/filebrowse/images/small-file-audio.png new file mode 100644 index 0000000..12ac2e9 Binary files /dev/null and b/qml/filebrowse/images/small-file-audio.png differ diff --git a/qml/filebrowse/images/small-file-image.png b/qml/filebrowse/images/small-file-image.png new file mode 100644 index 0000000..ddc7108 Binary files /dev/null and b/qml/filebrowse/images/small-file-image.png differ diff --git a/qml/filebrowse/images/small-file-rpm.png b/qml/filebrowse/images/small-file-rpm.png new file mode 100644 index 0000000..136cff5 Binary files /dev/null and b/qml/filebrowse/images/small-file-rpm.png differ diff --git a/qml/filebrowse/images/small-file-text.png b/qml/filebrowse/images/small-file-text.png new file mode 100644 index 0000000..ecfa861 Binary files /dev/null and b/qml/filebrowse/images/small-file-text.png differ diff --git a/qml/filebrowse/images/small-file-txt.png b/qml/filebrowse/images/small-file-txt.png new file mode 100644 index 0000000..ecfa861 Binary files /dev/null and b/qml/filebrowse/images/small-file-txt.png differ diff --git a/qml/filebrowse/images/small-file-video.png b/qml/filebrowse/images/small-file-video.png new file mode 100644 index 0000000..f6162d1 Binary files /dev/null and b/qml/filebrowse/images/small-file-video.png differ diff --git a/qml/filebrowse/images/small-file.png b/qml/filebrowse/images/small-file.png new file mode 100644 index 0000000..632fd83 Binary files /dev/null and b/qml/filebrowse/images/small-file.png differ diff --git a/qml/filebrowse/images/small-folder-link.png b/qml/filebrowse/images/small-folder-link.png new file mode 100644 index 0000000..d556cc8 Binary files /dev/null and b/qml/filebrowse/images/small-folder-link.png differ diff --git a/qml/filebrowse/images/small-folder.png b/qml/filebrowse/images/small-folder.png new file mode 100644 index 0000000..ef84395 Binary files /dev/null and b/qml/filebrowse/images/small-folder.png differ diff --git a/qml/filebrowse/images/small-link.png b/qml/filebrowse/images/small-link.png new file mode 100644 index 0000000..15ce793 Binary files /dev/null and b/qml/filebrowse/images/small-link.png differ diff --git a/qml/filebrowse/pages/AboutPage.qml b/qml/filebrowse/pages/AboutPage.qml new file mode 100644 index 0000000..c3f3701 --- /dev/null +++ b/qml/filebrowse/pages/AboutPage.qml @@ -0,0 +1,58 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: page + allowedOrientations: Orientation.All + + SilicaFlickable { + id: flickable + anchors.fill: parent + contentHeight: column.height + VerticalScrollDecorator { flickable: flickable } + + Column { + id: column + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Theme.paddingLarge + anchors.rightMargin: Theme.paddingLarge + + PageHeader { title: qsTr("Public Domain") } + + Label { + width: parent.width + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + text: "This is free and unencumbered software released into the public domain."+ + "\n\n"+ + "Anyone is free to copy, modify, publish, use, compile, sell, or "+ + "distribute this software, either in source code form or as a compiled "+ + "binary, for any purpose, commercial or non-commercial, and by any "+ + "means."+ + "\n\n"+ + "In jurisdictions that recognize copyright laws, the author or authors "+ + "of this software dedicate any and all copyright interest in the "+ + "software to the public domain. We make this dedication for the benefit "+ + "of the public at large and to the detriment of our heirs and "+ + "successors. We intend this dedication to be an overt act of "+ + "relinquishment in perpetuity of all present and future rights to this "+ + "software under copyright law."+ + "\n\n"+ + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, "+ + "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF "+ + "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. "+ + "IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR "+ + "OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, "+ + "ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR "+ + "OTHER DEALINGS IN THE SOFTWARE."+ + "\n\n"+ + "For more information, please refer to " + } + } + } + +} + + 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