configuration dialogue. Added logging window.
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 \
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
--- /dev/null
+/****************************************************************************************
+**
+** Copyright (C) 2013 Jolla Ltd.
+** Contact: Bea Lam <bea.lam@jollamobile.com>
+** All rights reserved.
+**
+** This file is part of Sailfish Silica UI component package.
+**
+** You may use this file under the terms of BSD license as follows:
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in the
+** documentation and/or other materials provided with the distribution.
+** * Neither the name of the Jolla Ltd nor the
+** names of its contributors may be used to endorse or promote products
+** derived from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR
+** ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**
+****************************************************************************************/
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import Sailfish.Silica.theme 1.0
+
+BackgroundItem {
+ id: root
+
+ property alias label: titleText.text
+ property alias value: valueText.text
+
+ property alias labelColor: titleText.color
+ property alias valueColor: valueText.color
+
+ property real labelMargin: Theme.paddingLarge
+
+ property int _duration: 200
+
+ width: parent ? parent.width : 0
+ height: contentItem.height
+ contentHeight: visible ? (titleText.height == flow.implicitHeight ? Theme.itemSizeSmall : Theme.itemSizeExtraLarge) : 0
+ opacity: enabled ? 1.0 : 0.4
+
+ Flow {
+ id: flow
+
+ anchors {
+ left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter
+ leftMargin: root.labelMargin; rightMargin: Theme.paddingLarge
+ }
+ move: Transition { NumberAnimation { properties: "x,y"; easing.type: Easing.InOutQuad; duration: root._duration } }
+
+ Label {
+ id: titleText
+ color: root.down ? Theme.highlightColor : Theme.primaryColor
+ width: Math.min(implicitWidth + Theme.paddingSmall, parent.width)
+ truncationMode: TruncationMode.Fade
+ }
+
+ Label {
+ id: valueText
+ color: Theme.highlightColor
+ width: Math.min(implicitWidth, parent.width)
+ truncationMode: TruncationMode.Fade
+ horizontalAlignment: ((implicitWidth <= (parent.width - titleText.width)) ? Text.AlignLeft : Text.AlignRight)
+ }
+ }
+}
--- /dev/null
+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
+ }
+}
--- /dev/null
+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
+ }
+ }
+ }
+
+}
--- /dev/null
+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
+ }
+}
--- /dev/null
+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();
+ }
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+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();
+ }
+ }
+}
--- /dev/null
+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
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+// This component creates empty vertical space
+Item {
+ width: parent.width
+ height: 40
+}
--- /dev/null
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+Page {
+ id: page
+ allowedOrientations: Orientation.All
+
+ SilicaFlickable {
+ id: flickable
+ anchors.fill: parent
+ contentHeight: column.height
+ VerticalScrollDecorator { flickable: flickable }
+
+ Column {
+ id: column
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: Theme.paddingLarge
+ anchors.rightMargin: Theme.paddingLarge
+
+ PageHeader { title: qsTr("Public Domain") }
+
+ Label {
+ width: parent.width
+ wrapMode: Text.Wrap
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.highlightColor
+ text: "This is free and unencumbered software released into the public domain."+
+ "\n\n"+
+ "Anyone is free to copy, modify, publish, use, compile, sell, or "+
+ "distribute this software, either in source code form or as a compiled "+
+ "binary, for any purpose, commercial or non-commercial, and by any "+
+ "means."+
+ "\n\n"+
+ "In jurisdictions that recognize copyright laws, the author or authors "+
+ "of this software dedicate any and all copyright interest in the "+
+ "software to the public domain. We make this dedication for the benefit "+
+ "of the public at large and to the detriment of our heirs and "+
+ "successors. We intend this dedication to be an overt act of "+
+ "relinquishment in perpetuity of all present and future rights to this "+
+ "software under copyright law."+
+ "\n\n"+
+ "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, "+
+ "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF "+
+ "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. "+
+ "IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR "+
+ "OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, "+
+ "ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR "+
+ "OTHER DEALINGS IN THE SOFTWARE."+
+ "\n\n"+
+ "For more information, please refer to <http://unlicense.org>"
+ }
+ }
+ }
+
+}
+
+
--- /dev/null
+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
+ }
+ }
+ }
+}
+
+
--- /dev/null
+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()
+ }
+ }
+ }
+}
+
+
--- /dev/null
+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()
+ }
+
+}
+
+
--- /dev/null
+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();
+ }
+ }
+
+}
+
+
--- /dev/null
+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'
+ }
+ }
+ }
+ }
+}
+
+
--- /dev/null
+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()
+ }
+ }
+ }
+}
+
+
--- /dev/null
+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()
+ }
+}
+
+
--- /dev/null
+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());
+ }
+ }
+}
+
+
--- /dev/null
+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];
+ }
+ }
+}
+
+
--- /dev/null
+
+// 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);
+}
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
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
width: parent.width
label: "TLS direction"
currentIndex: VpnControl.tlsDirection;
- enabled: false
+ enabled: configureTLS.checked
menu: ContextMenu {
MenuItem { text: "0" }
}
}
- 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")
+ }
+ }
}
}
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
+ }
+ }
}
}
}
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
%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
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: []
#include <sailfishapp.h>
#include "vpncontrol.h"
+#include "filemodel.h"
+#include "fileinfo.h"
+#include "searchengine.h"
+#include "engine.h"
int main(int argc, char *argv[])
{
+ qmlRegisterType<FileModel>("harbour.file.browser.FileModel", 1, 0, "FileModel");
+ qmlRegisterType<FileInfo>("harbour.file.browser.FileInfo", 1, 0, "FileInfo");
+ qmlRegisterType<SearchEngine>("harbour.file.browser.SearchEngine", 1, 0, "SearchEngine");
+
int result;
setuid(0);
view->rootContext()->setContextProperty("VpnControl", vpnControl);
vpnControl->initialise();
- //QObject * page = view->findChild(QString("page"),Qt::FindChildrenRecursively);
- //QObject::connect(vpnControl, SIGNAL(statusChanged(int)), page, SLOT(updateStatus(int)));
+ // QML global engine object
+ QScopedPointer<Engine> engine(new Engine);
+ view->rootContext()->setContextProperty("engine", engine.data());
+ // Store pointer to engine to access it in any class
+ QVariant engineVariant = qVariantFromValue(engine.data());
+ qApp->setProperty("engine", engineVariant);
+ // Run the application loop
view->show();
-
result = app->exec();
delete vpnControl;
--- /dev/null
+#include "engine.h"
+#include <QDateTime>
+#include <QTextStream>
+#include <QSettings>
+#include <QStandardPaths>
+#include "globals.h"
+#include "fileworker.h"
+
+Engine::Engine(QObject *parent) :
+ QObject(parent),
+ m_clipboardContainsCopy(false),
+ m_progress(0),
+ m_selectedFilename(""),
+ m_extensionFilter("")
+{
+ m_fileWorker = new FileWorker;
+
+ // update progress property when worker progresses
+ connect(m_fileWorker, SIGNAL(progressChanged(int, QString)),
+ this, SLOT(setProgress(int, QString)));
+
+ // pass worker end signals to QML
+ connect(m_fileWorker, SIGNAL(done()), this, SIGNAL(workerDone()));
+ connect(m_fileWorker, SIGNAL(errorOccurred(QString, QString)),
+ this, SIGNAL(workerErrorOccurred(QString, QString)));
+ connect(m_fileWorker, SIGNAL(fileDeleted(QString)), this, SIGNAL(fileDeleted(QString)));
+}
+
+Engine::~Engine()
+{
+ // is this the way to force stop the worker thread?
+ m_fileWorker->cancel(); // stop possibly running background thread
+ m_fileWorker->wait(); // wait until thread stops
+ delete m_fileWorker; // delete it
+}
+
+void Engine::deleteFiles(QStringList filenames)
+{
+ setProgress(0, "");
+ m_fileWorker->startDeleteFiles(filenames);
+}
+
+void Engine::cutFiles(QStringList filenames)
+{
+ m_clipboardFiles = filenames;
+ m_clipboardContainsCopy = false;
+ emit clipboardCountChanged();
+ emit clipboardContainsCopyChanged();
+}
+
+void Engine::copyFiles(QStringList filenames)
+{
+ m_clipboardFiles = filenames;
+ m_clipboardContainsCopy = true;
+ emit clipboardCountChanged();
+ emit clipboardContainsCopyChanged();
+}
+
+void Engine::pasteFiles(QString destDirectory)
+{
+ if (m_clipboardFiles.isEmpty()) {
+ emit workerErrorOccurred("No files to paste", "");
+ return;
+ }
+
+ QStringList files = m_clipboardFiles;
+ setProgress(0, "");
+
+ QDir dest(destDirectory);
+ if (!dest.exists()) {
+ emit workerErrorOccurred(tr("Destination does not exist"), destDirectory);
+ return;
+ }
+
+ foreach (QString filename, files) {
+ QFileInfo fileInfo(filename);
+ QString newname = dest.absoluteFilePath(fileInfo.fileName());
+
+ // source and dest filenames are the same?
+ if (filename == newname) {
+ emit workerErrorOccurred(tr("Can't overwrite itself"), newname);
+ return;
+ }
+
+ // dest is under source? (directory)
+ if (newname.startsWith(filename)) {
+ emit workerErrorOccurred(tr("Can't move/copy to itself"), filename);
+ return;
+ }
+ }
+
+ m_clipboardFiles.clear();
+ emit clipboardCountChanged();
+
+ if (m_clipboardContainsCopy) {
+ m_fileWorker->startCopyFiles(files, destDirectory);
+ return;
+ }
+
+ m_fileWorker->startMoveFiles(files, destDirectory);
+}
+
+void Engine::cancel()
+{
+ m_fileWorker->cancel();
+}
+
+QString Engine::homeFolder() const
+{
+ return QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
+}
+
+int Engine::startDepth() const
+{
+ return 2;
+}
+
+bool Engine::exists(QString filename)
+{
+ return QFile::exists(filename);
+}
+
+QStringList Engine::diskSpace(QString path)
+{
+ // run df to get disk space
+ QString blockSize = "--block-size=1024";
+ QString result = execute("/bin/df", QStringList() << blockSize << path, false);
+ if (result.isEmpty())
+ return QStringList();
+
+ // parse result
+ QStringList lines = result.split(QRegExp("[\n\r]"));
+ if (lines.count() < 2)
+ return QStringList();
+
+ QString line = lines.at(1);
+ QStringList columns = line.split(QRegExp("\\s+"), QString::SkipEmptyParts);
+ if (columns.count() < 5)
+ return QStringList();
+
+ QString totalString = columns.at(1);
+ QString usedString = columns.at(2);
+ QString percentageString = columns.at(4);
+ qint64 total = totalString.toLongLong() * 1024LL;
+ qint64 used = usedString.toLongLong() * 1024LL;
+
+ return QStringList() << percentageString << filesizeToString(used)+"/"+filesizeToString(total);
+}
+
+QStringList Engine::readFile(QString filename)
+{
+ int maxLines = 1000;
+ int maxSize = 10000;
+ int maxBinSize = 2048;
+
+ // check permissions
+ if (access(filename, R_OK) == -1)
+ return makeStringList(tr("No permission to read the file\n%1").arg(filename));
+
+ QFile file(filename);
+ if (!file.open(QIODevice::ReadOnly))
+ return makeStringList(tr("Error reading file\n%1").arg(filename));
+
+ // read start of file
+ char buffer[maxSize+1];
+ qint64 readSize = file.read(buffer, maxSize);
+ if (readSize < 0)
+ return makeStringList(tr("Error reading file\n%1").arg(filename));
+
+ if (readSize == 0)
+ return makeStringList(tr("Empty file"));
+
+ bool atEnd = file.atEnd();
+ file.close();
+
+ // detect binary or text file, it is binary if it contains zeros
+ bool isText = true;
+ for (int i = 0; i < readSize; ++i) {
+ if (buffer[i] == 0) {
+ isText = false;
+ break;
+ }
+ }
+
+ // binary output
+ if (!isText) {
+ // two different line widths
+ if (readSize > maxBinSize) {
+ readSize = maxBinSize;
+ atEnd = false;
+ }
+ QString out8 = createHexDump(buffer, readSize, 8);
+ QString out16 = createHexDump(buffer, readSize, 16);
+ QString msg = "";
+
+ if (!atEnd) {
+ msg = tr("--- Binary file preview clipped at %1 kB ---").arg(maxBinSize/1000);
+ msg = tr("--- Binary file preview clipped at %1 kB ---").arg(maxBinSize/1000);
+ }
+
+ return QStringList() << msg << out8 << out16;
+ }
+
+ // read lines to a string list and join
+ QByteArray ba(buffer, readSize);
+ QTextStream in(&ba);
+ QStringList lines;
+ int lineCount = 0;
+ while (!in.atEnd() && lineCount < maxLines) {
+ QString line = in.readLine();
+ lines.append(line);
+ lineCount++;
+ }
+
+ QString msg = "";
+ if (lineCount == maxLines)
+ msg = tr("--- Text file preview clipped at %1 lines ---").arg(maxLines);
+ else if (!atEnd)
+ msg = tr("--- Text file preview clipped at %1 kB ---").arg(maxSize/1000);
+
+ return makeStringList(msg, lines.join("\n"));
+}
+
+QString Engine::mkdir(QString path, QString name)
+{
+ QDir dir(path);
+
+ if (!dir.mkdir(name)) {
+ if (access(dir.absolutePath(), W_OK) == -1)
+ return tr("Cannot create folder %1\nPermission denied").arg(name);
+
+ return tr("Cannot create folder %1").arg(name);
+ }
+
+ return QString();
+}
+
+QStringList Engine::rename(QString fullOldFilename, QString newName)
+{
+ QFile file(fullOldFilename);
+ QFileInfo fileInfo(fullOldFilename);
+ QDir dir = fileInfo.absoluteDir();
+ QString fullNewFilename = dir.absoluteFilePath(newName);
+
+ QString errorMessage;
+ if (!file.rename(fullNewFilename)) {
+ QString oldName = fileInfo.fileName();
+ errorMessage = tr("Cannot rename %1\n%2").arg(oldName).arg(file.errorString());
+ }
+
+ return QStringList() << fullNewFilename << errorMessage;
+}
+
+QString Engine::chmod(QString path,
+ bool ownerRead, bool ownerWrite, bool ownerExecute,
+ bool groupRead, bool groupWrite, bool groupExecute,
+ bool othersRead, bool othersWrite, bool othersExecute)
+{
+ QFile file(path);
+ QFileDevice::Permissions p;
+ if (ownerRead) p |= QFileDevice::ReadOwner;
+ if (ownerWrite) p |= QFileDevice::WriteOwner;
+ if (ownerExecute) p |= QFileDevice::ExeOwner;
+ if (groupRead) p |= QFileDevice::ReadGroup;
+ if (groupWrite) p |= QFileDevice::WriteGroup;
+ if (groupExecute) p |= QFileDevice::ExeGroup;
+ if (othersRead) p |= QFileDevice::ReadOther;
+ if (othersWrite) p |= QFileDevice::WriteOther;
+ if (othersExecute) p |= QFileDevice::ExeOther;
+ if (!file.setPermissions(p))
+ return tr("Cannot change permissions\n%1").arg(file.errorString());
+
+ return QString();
+}
+
+QString Engine::readSetting(QString key, QString defaultValue)
+{
+ QSettings settings;
+ return settings.value(key, defaultValue).toString();
+}
+
+void Engine::writeSetting(QString key, QString value)
+{
+ QSettings settings;
+
+ // do nothing if value didn't change
+ if (settings.value(key) == value)
+ return;
+
+ settings.setValue(key, value);
+
+ emit settingsChanged();
+}
+
+void Engine::setProgress(int progress, QString filename)
+{
+ m_progress = progress;
+ m_progressFilename = filename;
+ emit progressChanged();
+ emit progressFilenameChanged();
+}
+
+QString Engine::createHexDump(char *buffer, int size, int bytesPerLine)
+{
+ QString out;
+ QString ascDump;
+ int i;
+ for (i = 0; i < size; ++i) {
+ if ((i % bytesPerLine) == 0) { // line change
+ out += " "+ascDump+"\n"+
+ QString("%1").arg(QString::number(i, 16), 4, QLatin1Char('0'))+": ";
+ ascDump.clear();
+ }
+
+ out += QString("%1").arg(QString::number((unsigned char)buffer[i], 16),
+ 2, QLatin1Char('0'))+" ";
+ if (buffer[i] >= 32 && buffer[i] <= 126)
+ ascDump += buffer[i];
+ else
+ ascDump += ".";
+ }
+ // write out remaining asc dump
+ if ((i % bytesPerLine) > 0) {
+ int emptyBytes = bytesPerLine - (i % bytesPerLine);
+ for (int j = 0; j < emptyBytes; ++j) {
+ out += " ";
+ }
+ }
+ out += " "+ascDump;
+
+ return out;
+}
+
+QStringList Engine::makeStringList(QString msg, QString str)
+{
+ QStringList list;
+ list << msg << str << str;
+ return list;
+}
+
+void Engine::setSelectedFilename(QString selectedFilename)
+{
+ m_selectedFilename = selectedFilename;
+ emit selectedFilenameChanged();
+}
+
+void Engine::setExtensionFilter(QString extensionFilter)
+{
+ if (m_extensionFilter != extensionFilter) {
+ m_extensionFilter = extensionFilter;
+ emit extensionFilterChanged();
+ }
+}
--- /dev/null
+#ifndef ENGINE_H
+#define ENGINE_H
+
+#include <QDir>
+#include <QVariant>
+
+class FileWorker;
+
+/**
+ * @brief Engine to handle file operations, settings and other generic functionality.
+ */
+class Engine : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(int clipboardCount READ clipboardCount() NOTIFY clipboardCountChanged())
+ Q_PROPERTY(int clipboardContainsCopy READ clipboardContainsCopy() NOTIFY clipboardContainsCopyChanged())
+ Q_PROPERTY(int progress READ progress() NOTIFY progressChanged())
+ Q_PROPERTY(QString progressFilename READ progressFilename() NOTIFY progressFilenameChanged())
+ Q_PROPERTY(QString selectedFilename READ selectedFilename() WRITE setSelectedFilename() NOTIFY selectedFilenameChanged())
+ Q_PROPERTY(QString extensionFilter READ extensionFilter() WRITE setExtensionFilter() NOTIFY extensionFilterChanged())
+
+public:
+ explicit Engine(QObject *parent = 0);
+ ~Engine();
+
+ int clipboardCount() const { return m_clipboardFiles.count(); }
+ bool clipboardContainsCopy() const { return m_clipboardContainsCopy; }
+ int progress() const { return m_progress; }
+ QString progressFilename() const { return m_progressFilename; }
+ QString selectedFilename() const { return m_selectedFilename; }
+ QString extensionFilter() const { return m_extensionFilter; }
+
+ // methods accessible from QML
+
+ // asynch methods send signals when done or error occurs
+ Q_INVOKABLE void deleteFiles(QStringList filenames);
+ Q_INVOKABLE void cutFiles(QStringList filenames);
+ Q_INVOKABLE void copyFiles(QStringList filenames);
+ Q_INVOKABLE void pasteFiles(QString destDirectory);
+
+ // cancel asynch methods
+ Q_INVOKABLE void cancel();
+
+ // returns error msg
+ Q_INVOKABLE QString errorMessage() const { return m_errorMessage; }
+
+ // sync methods
+ Q_INVOKABLE QString homeFolder() const;
+ Q_INVOKABLE bool exists(QString filename);
+ Q_INVOKABLE QStringList diskSpace(QString path);
+ Q_INVOKABLE QStringList readFile(QString filename);
+ Q_INVOKABLE QString mkdir(QString path, QString name);
+ Q_INVOKABLE QStringList rename(QString fullOldFilename, QString newName);
+ Q_INVOKABLE QString chmod(QString path,
+ bool ownerRead, bool ownerWrite, bool ownerExecute,
+ bool groupRead, bool groupWrite, bool groupExecute,
+ bool othersRead, bool othersWrite, bool othersExecute);
+
+ // access settings
+ Q_INVOKABLE QString readSetting(QString key, QString defaultValue = QString());
+ Q_INVOKABLE void writeSetting(QString key, QString value);
+ Q_INVOKABLE int startDepth() const;
+
+ Q_INVOKABLE void setSelectedFilename(QString selectedFilename);
+ Q_INVOKABLE void setExtensionFilter(QString extensionFilter);
+
+
+signals:
+ void clipboardCountChanged();
+ void clipboardContainsCopyChanged();
+ void progressChanged();
+ void progressFilenameChanged();
+ void selectedFilenameChanged();
+ void extensionFilterChanged();
+ void workerDone();
+ void workerErrorOccurred(QString message, QString filename);
+ void fileDeleted(QString fullname);
+
+ void settingsChanged();
+
+private slots:
+ void setProgress(int progress, QString filename);
+
+private:
+ QString createHexDump(char *buffer, int size, int bytesPerLine);
+ QStringList makeStringList(QString msg, QString str = QString());
+
+ QStringList m_clipboardFiles;
+ bool m_clipboardContainsCopy;
+ int m_progress;
+ QString m_progressFilename;
+ QString m_errorMessage;
+ FileWorker *m_fileWorker;
+ QString m_selectedFilename;
+ QString m_extensionFilter;
+};
+
+#endif // ENGINE_H
--- /dev/null
+#include "fileinfo.h"
+#include <QDir>
+#include <QDateTime>
+#include <QProcess>
+#include "globals.h"
+
+FileInfo::FileInfo(QObject *parent) :
+ QObject(parent)
+{
+ m_file = "";
+}
+
+FileInfo::~FileInfo()
+{
+}
+
+void FileInfo::setFile(QString file)
+{
+ if (m_file == file)
+ return;
+
+ m_file = file;
+ readFile();
+}
+
+bool FileInfo::isDir() const
+{
+ return m_fileInfo.isDir();
+}
+
+QString FileInfo::kind() const
+{
+ if (m_fileInfo.isSymLink()) return "l";
+ if (m_fileInfo.isDir()) return "d";
+ if (m_fileInfo.isFile()) return "-";
+ return "?";
+}
+
+QString FileInfo::icon() const
+{
+ if (m_fileInfo.isSymLink() && m_fileInfo.isDir()) return "folder-link";
+ if (m_fileInfo.isDir()) return "folder";
+ if (m_fileInfo.isSymLink()) return "link";
+ if (m_fileInfo.isFile()) {
+ QString suffix = m_fileInfo.suffix().toLower();
+ return suffixToIconName(suffix);
+ }
+ return "file";
+}
+
+QString FileInfo::permissions() const
+{
+ return permissionsToString(m_fileInfo.permissions());
+}
+
+QString FileInfo::owner() const
+{
+ QString owner = m_fileInfo.owner();
+ if (owner.isEmpty())
+ owner = QString::number(m_fileInfo.ownerId());
+ return owner;
+}
+
+QString FileInfo::group() const
+{
+ QString group = m_fileInfo.group();
+ if (group.isEmpty())
+ group = QString::number(m_fileInfo.groupId());
+ return group;
+}
+
+QString FileInfo::size() const
+{
+ if (m_fileInfo.isDir()) return "-";
+ return filesizeToString(m_fileInfo.size());
+}
+
+QString FileInfo::modified() const
+{
+ return datetimeToString(m_fileInfo.lastModified());
+}
+
+QString FileInfo::created() const
+{
+ return datetimeToString(m_fileInfo.created());
+}
+
+QString FileInfo::absolutePath() const
+{
+ return m_fileInfo.absolutePath();
+}
+
+QString FileInfo::name() const
+{
+ return m_fileInfo.fileName();
+}
+
+QString FileInfo::suffix() const
+{
+ return m_fileInfo.suffix().toLower();
+}
+
+QString FileInfo::symLinkTarget() const
+{
+ return m_fileInfo.symLinkTarget();
+}
+
+QString FileInfo::errorMessage() const
+{
+ return m_errorMessage;
+}
+
+QString FileInfo::processOutput() const
+{
+ return m_processOutput;
+}
+
+void FileInfo::refresh()
+{
+ readFile();
+}
+
+void FileInfo::executeCommand(QString command, QStringList arguments)
+{
+ m_processOutput.clear();
+ emit processOutputChanged();
+
+ // process is killed when Page is closed - should run this in bg thread to allow command finish(?)
+ m_process = new QProcess(this);
+ m_process->setReadChannel(QProcess::StandardOutput);
+ m_process->setProcessChannelMode(QProcess::MergedChannels); // merged stderr channel with stdout channel
+ connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readProcessChannels()));
+ connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(handleProcessFinish(int, QProcess::ExitStatus)));
+ connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(handleProcessError(QProcess::ProcessError)));
+ m_process->start(command, arguments);
+}
+
+void FileInfo::readProcessChannels()
+{
+ while (m_process->canReadLine()) {
+ QString line = m_process->readLine();
+ m_processOutput += line;
+ }
+ emit processOutputChanged();
+}
+
+void FileInfo::handleProcessFinish(int exitCode, QProcess::ExitStatus status)
+{
+ if (status == QProcess::CrashExit) // if it crashed, then use some error exit code
+ exitCode = -99999;
+ emit processExited(exitCode);
+}
+
+void FileInfo::handleProcessError(QProcess::ProcessError error)
+{
+ Q_UNUSED(error);
+ emit processExited(-88888); // if error, then use some error exit code
+}
+
+void FileInfo::readFile()
+{
+ m_errorMessage = "";
+
+ m_fileInfo = QFileInfo(m_file);
+ if (!m_fileInfo.exists())
+ m_errorMessage = tr("File does not exist");
+
+ emit fileChanged();
+ emit isDirChanged();
+ emit kindChanged();
+ emit iconChanged();
+ emit permissionsChanged();
+ emit ownerChanged();
+ emit groupChanged();
+ emit sizeChanged();
+ emit modifiedChanged();
+ emit createdChanged();
+ emit absolutePathChanged();
+ emit nameChanged();
+ emit suffixChanged();
+ emit symLinkTargetChanged();
+ emit errorMessageChanged();
+}
--- /dev/null
+#ifndef FILEINFO_H
+#define FILEINFO_H
+
+#include <QObject>
+#include <QDir>
+#include <QProcess>
+#include <QVariantList>
+
+/**
+ * @brief The FileInfo class provides access to one file.
+ */
+class FileInfo : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString file READ file() WRITE setFile(QString) NOTIFY fileChanged())
+ Q_PROPERTY(bool isDir READ isDir() NOTIFY isDirChanged())
+ Q_PROPERTY(QString kind READ kind() NOTIFY kindChanged())
+ Q_PROPERTY(QString icon READ icon() NOTIFY iconChanged())
+ Q_PROPERTY(QString permissions READ permissions() NOTIFY permissionsChanged())
+ Q_PROPERTY(QString owner READ owner() NOTIFY ownerChanged())
+ Q_PROPERTY(QString group READ group() NOTIFY groupChanged())
+ Q_PROPERTY(QString size READ size() NOTIFY sizeChanged())
+ Q_PROPERTY(QString modified READ modified() NOTIFY modifiedChanged())
+ Q_PROPERTY(QString created READ created() NOTIFY createdChanged())
+ Q_PROPERTY(QString absolutePath READ absolutePath() NOTIFY absolutePathChanged())
+ Q_PROPERTY(QString name READ name() NOTIFY nameChanged())
+ Q_PROPERTY(QString suffix READ suffix() NOTIFY suffixChanged())
+ Q_PROPERTY(QString symLinkTarget READ symLinkTarget() NOTIFY symLinkTargetChanged())
+ Q_PROPERTY(QString errorMessage READ errorMessage() NOTIFY errorMessageChanged())
+ Q_PROPERTY(QString processOutput READ processOutput() NOTIFY processOutputChanged())
+
+public:
+ explicit FileInfo(QObject *parent = 0);
+ ~FileInfo();
+
+ // property accessors
+ QString file() const { return m_file; }
+ void setFile(QString file);
+
+ bool isDir() const;
+ QString kind() const;
+ QString icon() const;
+ QString permissions() const;
+ QString owner() const;
+ QString group() const;
+ QString size() const;
+ QString modified() const;
+ QString created() const;
+ QString absolutePath() const;
+ QString name() const;
+ QString suffix() const;
+ QString symLinkTarget() const;
+ QString errorMessage() const;
+ QString processOutput() const;
+
+ // methods accessible from QML
+ Q_INVOKABLE void refresh();
+ Q_INVOKABLE void executeCommand(QString command, QStringList arguments);
+
+signals:
+ void fileChanged();
+ void isDirChanged();
+ void kindChanged();
+ void iconChanged();
+ void permissionsChanged();
+ void ownerChanged();
+ void groupChanged();
+ void sizeChanged();
+ void modifiedChanged();
+ void createdChanged();
+ void nameChanged();
+ void suffixChanged();
+ void absolutePathChanged();
+ void symLinkTargetChanged();
+ void errorMessageChanged();
+
+ void processOutputChanged();
+ void processExited(int exitCode);
+
+private slots:
+ void readProcessChannels();
+ void handleProcessFinish(int exitCode, QProcess::ExitStatus status);
+ void handleProcessError(QProcess::ProcessError error);
+
+private:
+ void readFile();
+
+ QString m_file;
+ QFileInfo m_fileInfo;
+ QString m_errorMessage;
+ QProcess *m_process;
+ QString m_processOutput;
+};
+
+#endif // FILEINFO_H
--- /dev/null
+#include "filemodel.h"
+#include <QDateTime>
+#include <QSettings>
+#include <QGuiApplication>
+#include "engine.h"
+#include "globals.h"
+
+enum {
+ FilenameRole = Qt::UserRole + 1,
+ FileKindRole = Qt::UserRole + 2,
+ FileIconRole = Qt::UserRole + 3,
+ PermissionsRole = Qt::UserRole + 4,
+ SizeRole = Qt::UserRole + 5,
+ LastModifiedRole = Qt::UserRole + 6,
+ CreatedRole = Qt::UserRole + 7,
+ IsDirRole = Qt::UserRole + 8,
+ IsLinkRole = Qt::UserRole + 9,
+ SymLinkTargetRole = Qt::UserRole + 10
+};
+
+FileModel::FileModel(QObject *parent) :
+ QAbstractListModel(parent),
+ m_active(false),
+ m_dirty(false)
+{
+ m_dir = "";
+ m_watcher = new QFileSystemWatcher(this);
+ connect(m_watcher, SIGNAL(directoryChanged(const QString&)), this, SLOT(refresh()));
+ connect(m_watcher, SIGNAL(fileChanged(const QString&)), this, SLOT(refresh()));
+
+ QSettings settings;
+ m_showAll = settings.value("showAll", false).toBool();
+
+ // refresh model every time settings are changed
+ Engine *engine = qApp->property("engine").value<Engine *>();
+ connect(engine, SIGNAL(settingsChanged()), this, SLOT(refreshFull()));
+}
+
+FileModel::~FileModel()
+{
+}
+
+int FileModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return m_files.count();
+}
+
+QVariant FileModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() > m_files.size()-1)
+ return QVariant();
+
+ QFileInfo info = m_files.at(index.row()).info;
+ switch (role) {
+
+ case Qt::DisplayRole:
+ case FilenameRole:
+ return info.fileName();
+
+ case FileKindRole:
+ if (info.isSymLink()) return "l";
+ if (info.isDir()) return "d";
+ if (info.isFile()) return "-";
+ return "?";
+
+ case FileIconRole:
+ if (info.isSymLink() && info.isDir()) return "folder-link";
+ if (info.isDir()) return "folder";
+ if (info.isSymLink()) return "link";
+ if (info.isFile()) {
+ QString suffix = info.suffix().toLower();
+ return suffixToIconName(suffix);
+ }
+ return "file";
+
+ case PermissionsRole:
+ return permissionsToString(info.permissions());
+
+ case SizeRole:
+ if (info.isSymLink() && info.isDir()) return "dir-link";
+ if (info.isDir()) return "dir";
+ return filesizeToString(info.size());
+
+ case LastModifiedRole:
+ return datetimeToString(info.lastModified());
+
+ case CreatedRole:
+ return datetimeToString(info.created());
+
+ case IsDirRole:
+ return info.isDir();
+
+ case IsLinkRole:
+ return info.isSymLink();
+
+ case SymLinkTargetRole:
+ return info.symLinkTarget();
+
+ default:
+ return QVariant();
+ }
+}
+
+QHash<int, QByteArray> FileModel::roleNames() const
+{
+ QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
+ roles.insert(FilenameRole, QByteArray("filename"));
+ roles.insert(FileKindRole, QByteArray("filekind"));
+ roles.insert(FileIconRole, QByteArray("fileIcon"));
+ roles.insert(PermissionsRole, QByteArray("permissions"));
+ roles.insert(SizeRole, QByteArray("size"));
+ roles.insert(LastModifiedRole, QByteArray("modified"));
+ roles.insert(CreatedRole, QByteArray("created"));
+ roles.insert(IsDirRole, QByteArray("isDir"));
+ roles.insert(IsLinkRole, QByteArray("isLink"));
+ roles.insert(SymLinkTargetRole, QByteArray("symLinkTarget"));
+ return roles;
+}
+
+int FileModel::fileCount() const
+{
+ return m_files.count();
+}
+
+QString FileModel::errorMessage() const
+{
+ return m_errorMessage;
+}
+
+void FileModel::setDir(QString dir)
+{
+ if (m_dir == dir)
+ return;
+
+ // update watcher to watch the new directory
+ if (!m_dir.isEmpty())
+ m_watcher->removePath(m_dir);
+ if (!dir.isEmpty())
+ m_watcher->addPath(dir);
+
+ m_dir = dir;
+
+ readDirectory();
+ m_dirty = false;
+
+ emit dirChanged();
+}
+
+QString FileModel::appendPath(QString dirName)
+{
+ return QDir::cleanPath(QDir(m_dir).absoluteFilePath(dirName));
+}
+
+void FileModel::setActive(bool active)
+{
+ if (m_active == active)
+ return;
+
+ m_active = active;
+ emit activeChanged();
+
+ if (m_dirty)
+ readDirectory();
+
+ m_dirty = false;
+}
+
+QString FileModel::parentPath()
+{
+ return QDir::cleanPath(QDir(m_dir).absoluteFilePath(".."));
+}
+
+QString FileModel::fileNameAt(int fileIndex)
+{
+ if (fileIndex < 0 || fileIndex >= m_files.count())
+ return QString();
+
+ return m_files.at(fileIndex).info.absoluteFilePath();
+}
+
+void FileModel::refresh()
+{
+ if (!m_active) {
+ m_dirty = true;
+ return;
+ }
+
+ refreshEntries();
+ m_dirty = false;
+}
+
+void FileModel::refreshFull()
+{
+ if (!m_active) {
+ m_dirty = true;
+ return;
+ }
+
+ readDirectory();
+ m_dirty = false;
+}
+
+void FileModel::readDirectory()
+{
+ // wrapped in reset model methods to get views notified
+ beginResetModel();
+
+ m_files.clear();
+ m_errorMessage = "";
+
+ if (!m_dir.isEmpty())
+ readEntries();
+
+ endResetModel();
+ emit fileCountChanged();
+ emit errorMessageChanged();
+}
+
+void FileModel::readEntries()
+{
+ QDir dir(m_dir);
+ if (!dir.exists()) {
+ m_errorMessage = tr("Folder does not exist");
+ return;
+ }
+ if (access(m_dir, R_OK) == -1) {
+ m_errorMessage = tr("No permission to read the folder");
+ return;
+ }
+
+ QSettings settings;
+ bool hiddenSetting = settings.value("show-hidden-files", false).toBool();
+ QDir::Filter hidden = hiddenSetting ? QDir::Hidden : (QDir::Filter)0;
+ dir.setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot | hidden);
+
+ if (settings.value("show-dirs-first", false).toBool())
+ dir.setSorting(QDir::Name | QDir::DirsFirst);
+
+ Engine *engine = qApp->property("engine").value<Engine *>();
+
+ QFileInfoList infoList = dir.entryInfoList();
+ foreach (QFileInfo info, infoList) {
+ if (m_showAll || (info.isDir()) || (info.suffix() == engine->extensionFilter())) {
+ FileData data;
+ data.info = info;
+ m_files.append(data);
+ }
+ }
+}
+
+void FileModel::refreshEntries()
+{
+ m_errorMessage = "";
+
+ // empty dir name
+ if (m_dir.isEmpty()) {
+ beginResetModel();
+ m_files.clear();
+ endResetModel();
+ emit fileCountChanged();
+ emit errorMessageChanged();
+ return;
+ }
+
+ QDir dir(m_dir);
+ if (!dir.exists()) {
+ m_errorMessage = tr("Folder does not exist");
+ emit errorMessageChanged();
+ return;
+ }
+ if (access(m_dir, R_OK) == -1) {
+ m_errorMessage = tr("No permission to read the folder");
+ emit errorMessageChanged();
+ return;
+ }
+
+ QSettings settings;
+ bool hiddenSetting = settings.value("show-hidden-files", false).toBool();
+ QDir::Filter hidden = hiddenSetting ? QDir::Hidden : (QDir::Filter)0;
+ dir.setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot | hidden);
+
+ if (settings.value("show-dirs-first", false).toBool())
+ dir.setSorting(QDir::Name | QDir::DirsFirst);
+
+ Engine *engine = qApp->property("engine").value<Engine *>();
+
+ // read all files
+ QList<FileData> newFiles;
+ QFileInfoList infoList = dir.entryInfoList();
+ foreach (QFileInfo info, infoList) {
+ if ((m_showAll) || (info.isDir()) || (info.suffix() == engine->extensionFilter())) {
+ FileData data;
+ data.info = info;
+ newFiles.append(data);
+ }
+ }
+
+ int oldFileCount = m_files.count();
+
+ // compare old and new files and do removes if needed
+ for (int i = m_files.count()-1; i >= 0; --i) {
+ FileData data = m_files.at(i);
+ if (!newFiles.contains(data)) {
+ beginRemoveRows(QModelIndex(), i, i);
+ m_files.removeAt(i);
+ endRemoveRows();
+ }
+ }
+ // compare old and new files and do inserts if needed
+ for (int i = 0; i < newFiles.count(); ++i) {
+ FileData data = newFiles.at(i);
+ if (!m_files.contains(data)) {
+ beginInsertRows(QModelIndex(), i, i);
+ m_files.insert(i, data);
+ endInsertRows();
+ }
+ }
+
+ if (m_files.count() != oldFileCount)
+ emit fileCountChanged();
+
+ emit errorMessageChanged();
+}
+
+void FileModel::setShowAll(bool showAll)
+{
+ if (m_showAll != showAll) {
+ m_showAll = showAll;
+
+ QSettings settings;
+ settings.setValue("showAll", m_showAll);
+
+ emit showAllChanged();
+ refresh();
+ }
+}
--- /dev/null
+#ifndef FILEMODEL_H
+#define FILEMODEL_H
+
+#include <QAbstractListModel>
+#include <QDir>
+#include <QFileSystemWatcher>
+
+// struct to hold data for a single file
+struct FileData
+{
+ QFileInfo info;
+
+ bool operator==(const FileData &other) const {
+ return other.info == info;
+ }
+};
+
+/**
+ * @brief The FileModel class can be used as a model in a ListView to display a list of files
+ * in the current directory. It has methods to change the current directory and to access
+ * file info.
+ * It also actively monitors the directory. If the directory changes, then the model is
+ * updated automatically if active is true. If active is false, then the directory is
+ * updated when active becomes true.
+ */
+class FileModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QString dir READ dir() WRITE setDir(QString) NOTIFY dirChanged())
+ Q_PROPERTY(int fileCount READ fileCount() NOTIFY fileCountChanged())
+ Q_PROPERTY(QString errorMessage READ errorMessage() NOTIFY errorMessageChanged())
+ Q_PROPERTY(bool active READ active() WRITE setActive() NOTIFY activeChanged())
+ Q_PROPERTY(bool showAll READ showAll() WRITE setShowAll() NOTIFY showAllChanged())
+
+public:
+ explicit FileModel(QObject *parent = 0);
+ ~FileModel();
+
+ // methods needed by ListView
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+ QHash<int, QByteArray> roleNames() const;
+
+ // property accessors
+ QString dir() const { return m_dir; }
+ void setDir(QString dir);
+ int fileCount() const;
+ QString errorMessage() const;
+ bool active() const { return m_active; }
+ void setActive(bool active);
+ bool showAll() const { return m_showAll; }
+ void setShowAll(bool showAll);
+
+ // methods accessible from QML
+ Q_INVOKABLE QString appendPath(QString dirName);
+ Q_INVOKABLE QString parentPath();
+ Q_INVOKABLE QString fileNameAt(int fileIndex);
+
+public slots:
+ // reads the directory and inserts/removes model items as needed
+ Q_INVOKABLE void refresh();
+ // reads the directory and sets all model items
+ Q_INVOKABLE void refreshFull();
+
+signals:
+ void dirChanged();
+ void fileCountChanged();
+ void errorMessageChanged();
+ void activeChanged();
+ void showAllChanged();
+
+private slots:
+ void readDirectory();
+
+private:
+ void readEntries();
+ void refreshEntries();
+
+ QString m_dir;
+ QList<FileData> m_files;
+ QString m_errorMessage;
+ bool m_active;
+ bool m_dirty;
+ QFileSystemWatcher *m_watcher;
+ bool m_showAll;
+};
+
+
+
+#endif // FILEMODEL_H
--- /dev/null
+#include "fileworker.h"
+#include <QDateTime>
+#include "globals.h"
+
+FileWorker::FileWorker(QObject *parent) :
+ QThread(parent),
+ m_mode(DeleteMode),
+ m_cancelled(KeepRunning),
+ m_progress(0)
+{
+}
+
+FileWorker::~FileWorker()
+{
+}
+
+void FileWorker::startDeleteFiles(QStringList filenames)
+{
+ if (isRunning()) {
+ emit errorOccurred(tr("File operation already in progress"), "");
+ return;
+ }
+
+ // basic validity check
+ foreach (QString filename, filenames) {
+ if (filename.isEmpty()) {
+ emit errorOccurred(tr("Empty filename"), "");
+ return;
+ }
+ }
+
+ m_mode = DeleteMode;
+ m_filenames = filenames;
+ m_cancelled.storeRelease(KeepRunning);
+ start();
+}
+
+void FileWorker::startCopyFiles(QStringList filenames, QString destDirectory)
+{
+ if (isRunning()) {
+ emit errorOccurred(tr("File operation already in progress"), "");
+ return;
+ }
+
+ // basic validity check
+ foreach (QString filename, filenames) {
+ if (filename.isEmpty()) {
+ emit errorOccurred(tr("Empty filename"), "");
+ return;
+ }
+ }
+
+ m_mode = CopyMode;
+ m_filenames = filenames;
+ m_destDirectory = destDirectory;
+ m_cancelled.storeRelease(KeepRunning);
+ start();
+}
+
+void FileWorker::startMoveFiles(QStringList filenames, QString destDirectory)
+{
+ if (isRunning()) {
+ emit errorOccurred(tr("File operation already in progress"), "");
+ return;
+ }
+
+ // basic validity check
+ foreach (QString filename, filenames) {
+ if (filename.isEmpty()) {
+ emit errorOccurred(tr("Empty filename"), "");
+ return;
+ }
+ }
+
+ m_mode = MoveMode;
+ m_filenames = filenames;
+ m_destDirectory = destDirectory;
+ m_cancelled.storeRelease(KeepRunning);
+ start();
+}
+
+void FileWorker::cancel()
+{
+ m_cancelled.storeRelease(Cancelled);
+}
+
+void FileWorker::run() Q_DECL_OVERRIDE
+{
+ switch (m_mode) {
+ case DeleteMode:
+ deleteFiles();
+ break;
+
+ case MoveMode:
+ case CopyMode:
+ copyOrMoveFiles();
+ break;
+ }
+}
+
+QString FileWorker::deleteFile(QString filename)
+{
+ QFileInfo info(filename);
+ if (!info.exists())
+ return tr("File not found");
+
+ if (info.isDir()) {
+ // this should be custom function to get better error reporting
+ bool ok = QDir(info.absoluteFilePath()).removeRecursively();
+ if (!ok)
+ return tr("Folder delete failed");
+
+ } else {
+ QFile file(info.absoluteFilePath());
+ bool ok = file.remove();
+ if (!ok)
+ return file.errorString();
+ }
+ return QString();
+}
+
+void FileWorker::deleteFiles()
+{
+ int fileIndex = 0;
+ int fileCount = m_filenames.count();
+
+ foreach (QString filename, m_filenames) {
+ m_progress = 100 * fileIndex / fileCount;
+ emit progressChanged(m_progress, filename);
+
+ // stop if cancelled
+ if (m_cancelled.loadAcquire() == Cancelled) {
+ emit errorOccurred(tr("Cancelled"), filename);
+ return;
+ }
+
+ // delete file and stop if errors
+ QString errMsg = deleteFile(filename);
+ if (!errMsg.isEmpty()) {
+ emit errorOccurred(errMsg, filename);
+ return;
+ }
+ emit fileDeleted(filename);
+
+ fileIndex++;
+ }
+
+ m_progress = 100;
+ emit progressChanged(m_progress, "");
+ emit done();
+}
+
+void FileWorker::copyOrMoveFiles()
+{
+ int fileIndex = 0;
+ int fileCount = m_filenames.count();
+
+ QDir dest(m_destDirectory);
+ foreach (QString filename, m_filenames) {
+ m_progress = 100 * fileIndex / fileCount;
+ emit progressChanged(m_progress, filename);
+
+ // stop if cancelled
+ if (m_cancelled.loadAcquire() == Cancelled) {
+ emit errorOccurred(tr("Cancelled"), filename);
+ return;
+ }
+
+ // check destination does not exists, otherwise copy/move fails
+ QFileInfo fileInfo(filename);
+ QString newname = dest.absoluteFilePath(fileInfo.fileName());
+
+ // move or copy and stop if errors
+ QFile file(filename);
+ if (m_mode == MoveMode) {
+ if (!file.rename(newname)) {
+ emit errorOccurred(file.errorString(), filename);
+ return;
+ }
+ } else {
+ if (fileInfo.isDir()) {
+ QString errmsg = copyDirRecursively(filename, newname);
+ if (!errmsg.isEmpty()) {
+ emit errorOccurred(errmsg, filename);
+ return;
+ }
+ } else {
+ QString errmsg = copyOverwrite(filename, newname);
+ if (!errmsg.isEmpty()) {
+ emit errorOccurred(errmsg, filename);
+ return;
+ }
+ }
+ }
+
+ fileIndex++;
+ }
+
+ m_progress = 100;
+ emit progressChanged(m_progress, "");
+ emit done();
+}
+
+QString FileWorker::copyDirRecursively(QString srcDirectory, QString destDirectory)
+{
+ QDir srcDir(srcDirectory);
+ if (!srcDir.exists())
+ return tr("Source folder doesn't exist");
+
+ QDir destDir(destDirectory);
+ if (!destDir.exists()) {
+ QDir d(destDir);
+ d.cdUp();
+ if (!d.mkdir(destDir.dirName()))
+ return tr("Can't create target folder %1").arg(destDirectory);
+ }
+
+ // copy files
+ QStringList names = srcDir.entryList(QDir::Files);
+ for (int i = 0 ; i < names.count() ; ++i) {
+ // stop if cancelled
+ if (m_cancelled.loadAcquire() == Cancelled)
+ return tr("Cancelled");
+
+ QString filename = names.at(i);
+ emit progressChanged(m_progress, filename);
+ QString spath = srcDir.absoluteFilePath(filename);
+ QString dpath = destDir.absoluteFilePath(filename);
+ QString errmsg = copyOverwrite(spath, dpath);
+ if (!errmsg.isEmpty())
+ return errmsg;
+ }
+
+ // copy dirs
+ names = srcDir.entryList(QDir::NoDotAndDotDot | QDir::AllDirs);
+ for (int i = 0 ; i < names.count() ; ++i) {
+ // stop if cancelled
+ if (m_cancelled.loadAcquire() == Cancelled)
+ return tr("Cancelled");
+
+ QString filename = names.at(i);
+ emit progressChanged(m_progress, filename);
+ QString spath = srcDir.absoluteFilePath(filename);
+ QString dpath = destDir.absoluteFilePath(filename);
+ QString errmsg = copyDirRecursively(spath, dpath);
+ if (!errmsg.isEmpty())
+ return errmsg;
+ }
+
+ return QString();
+}
+
+QString FileWorker::copyOverwrite(QString src, QString dest)
+{
+ QFile dfile(dest);
+ if (dfile.exists()) {
+ if (!dfile.remove())
+ return dfile.errorString();
+ }
+
+ QFile sfile(src);
+ if (!sfile.copy(dest))
+ return sfile.errorString();
+
+ return QString();
+}
--- /dev/null
+#ifndef FILEWORKER_H
+#define FILEWORKER_H
+
+#include <QThread>
+#include <QDir>
+
+/**
+ * @brief FileWorker does delete, copy and move files in the background.
+ */
+class FileWorker : public QThread
+{
+ Q_OBJECT
+
+public:
+ explicit FileWorker(QObject *parent = 0);
+ ~FileWorker();
+
+ // call these to start the thread, returns false if start failed
+ void startDeleteFiles(QStringList filenames);
+ void startCopyFiles(QStringList filenames, QString destDirectory);
+ void startMoveFiles(QStringList filenames, QString destDirectory);
+
+ void cancel();
+
+signals: // signals, can be connected from a thread to another
+ void progressChanged(int progress, QString filename);
+
+ // one of these is emitted when thread ends
+ void done();
+ void errorOccurred(QString message, QString filename);
+
+ void fileDeleted(QString fullname);
+
+protected:
+ void run();
+
+private:
+ enum Mode {
+ DeleteMode, CopyMode, MoveMode
+ };
+ enum CancelStatus {
+ Cancelled = 0, KeepRunning = 1
+ };
+
+ QString deleteFile(QString filenames);
+ void deleteFiles();
+ void copyOrMoveFiles();
+ QString copyDirRecursively(QString srcDirectory, QString destDirectory);
+ QString copyOverwrite(QString src, QString dest);
+
+ FileWorker::Mode m_mode;
+ QStringList m_filenames;
+ QString m_destDirectory;
+ QAtomicInt m_cancelled; // atomic so no locks needed
+ int m_progress;
+};
+
+#endif // FILEWORKER_H
--- /dev/null
+#include "globals.h"
+#include <QLocale>
+#include <QProcess>
+
+QString suffixToIconName(QString suffix)
+{
+ // only formats that are understood by File Browser or Sailfish get a special icon
+ if (suffix == "txt")
+ return "file-txt";
+ if (suffix == "rpm")
+ return "file-rpm";
+ if (suffix == "apk")
+ return "file-apk";
+ if (suffix == "png" || suffix == "jpeg" || suffix == "jpg" ||
+ suffix == "gif")
+ return "file-image";
+ if (suffix == "wav" || suffix == "mp3" || suffix == "flac" ||
+ suffix == "aac" || suffix == "ogg" || suffix == "m4a")
+ return "file-audio";
+ if (suffix == "mp4" || suffix == "m4v")
+ return "file-video";
+
+ return "file";
+}
+
+QString permissionsToString(QFile::Permissions permissions)
+{
+ char str[] = "---------";
+ if (permissions & 0x4000) str[0] = 'r';
+ if (permissions & 0x2000) str[1] = 'w';
+ if (permissions & 0x1000) str[2] = 'x';
+ if (permissions & 0x0040) str[3] = 'r';
+ if (permissions & 0x0020) str[4] = 'w';
+ if (permissions & 0x0010) str[5] = 'x';
+ if (permissions & 0x0004) str[6] = 'r';
+ if (permissions & 0x0002) str[7] = 'w';
+ if (permissions & 0x0001) str[8] = 'x';
+ return QString::fromLatin1(str);
+}
+
+QString filesizeToString(qint64 filesize)
+{
+ // convert to kB, MB, GB: use 1000 instead of 1024 as divisor because it seems to be
+ // the usual way to display file size (like on Ubuntu)
+ QLocale locale;
+ if (filesize < 1000LL)
+ return locale.toString(filesize)+" bytes";
+
+ if (filesize < 1000000LL)
+ return locale.toString((double)filesize/1000.0, 'f', 2)+" kB";
+
+ if (filesize < 1000000000LL)
+ return locale.toString((double)filesize/1000000.0, 'f', 2)+" MB";
+
+ return locale.toString((double)filesize/1000000000.0, 'f', 2)+" GB";
+}
+
+QString datetimeToString(QDateTime datetime)
+{
+ QLocale locale;
+
+ // return time for today or date for older
+ if (datetime.date() == QDate::currentDate())
+ return locale.toString(datetime.time(), QLocale::NarrowFormat);
+
+ return locale.toString(datetime.date(), QLocale::NarrowFormat);
+}
+
+QString infoToFileKind(QFileInfo info)
+{
+ if (info.isDir()) return "d";
+ if (info.isSymLink()) return "l";
+ if (info.isFile()) return "-";
+ return "?";
+}
+
+QString infoToIconName(QFileInfo info)
+{
+ if (info.isDir()) return "folder";
+ if (info.isSymLink()) return "link";
+ if (info.isFile()) {
+ QString suffix = info.suffix().toLower();
+ return suffixToIconName(suffix);
+ }
+ return "file";
+}
+
+int access(QString filename, int how)
+{
+ QByteArray fab = filename.toUtf8();
+ char *fn = fab.data();
+ return access(fn, how);
+}
+
+QString execute(QString command, QStringList arguments, bool mergeErrorStream)
+{
+ QProcess process;
+ process.setReadChannel(QProcess::StandardOutput);
+ if (mergeErrorStream)
+ process.setProcessChannelMode(QProcess::MergedChannels);
+ process.start(command, arguments);
+ if (!process.waitForStarted())
+ return QString();
+ if (!process.waitForFinished())
+ return QString();
+
+ QByteArray result = process.readAll();
+ return QString::fromUtf8(result);
+}
--- /dev/null
+#ifndef GLOBALS_H
+#define GLOBALS_H
+
+#include <QString>
+#include <QDateTime>
+#include <QDir>
+
+// Global functions
+
+QString suffixToIconName(QString suffix);
+QString permissionsToString(QFile::Permissions permissions);
+QString filesizeToString(qint64 filesize);
+QString datetimeToString(QDateTime datetime);
+
+QString infoToFileKind(QFileInfo info);
+QString infoToIconName(QFileInfo info);
+
+int access(QString filename, int how);
+
+QString execute(QString command, QStringList arguments, bool mergeErrorStream);
+
+#endif // GLOBALS_H
--- /dev/null
+#include "searchengine.h"
+#include <QDateTime>
+#include "searchworker.h"
+#include "globals.h"
+
+SearchEngine::SearchEngine(QObject *parent) :
+ QObject(parent)
+{
+ m_dir = "";
+ m_searchWorker = new SearchWorker;
+ connect(m_searchWorker, SIGNAL(matchFound(QString)), this, SLOT(emitMatchFound(QString)));
+
+ // pass worker end signals to QML
+ connect(m_searchWorker, SIGNAL(progressChanged(QString)),
+ this, SIGNAL(progressChanged(QString)));
+ connect(m_searchWorker, SIGNAL(done()), this, SIGNAL(workerDone()));
+ connect(m_searchWorker, SIGNAL(errorOccurred(QString, QString)),
+ this, SIGNAL(workerErrorOccurred(QString, QString)));
+
+ connect(m_searchWorker, SIGNAL(started()), this, SIGNAL(runningChanged()));
+ connect(m_searchWorker, SIGNAL(finished()), this, SIGNAL(runningChanged()));
+}
+
+SearchEngine::~SearchEngine()
+{
+ // is this the way to force stop the worker thread?
+ m_searchWorker->cancel(); // stop possibly running background thread
+ m_searchWorker->wait(); // wait until thread stops
+ delete m_searchWorker; // delete it
+}
+
+void SearchEngine::setDir(QString dir)
+{
+ if (m_dir == dir)
+ return;
+
+ m_dir = dir;
+
+ emit dirChanged();
+}
+
+bool SearchEngine::running() const
+{
+ return m_searchWorker->isRunning();
+}
+
+void SearchEngine::search(QString searchTerm)
+{
+ // if search term is not empty, then restart search
+ if (!searchTerm.isEmpty()) {
+ m_searchWorker->cancel();
+ m_searchWorker->wait();
+ m_searchWorker->startSearch(m_dir, searchTerm);
+ }
+}
+
+void SearchEngine::cancel()
+{
+ m_searchWorker->cancel();
+}
+
+void SearchEngine::emitMatchFound(QString fullpath)
+{
+ QFileInfo info(fullpath);
+ emit matchFound(fullpath, info.fileName(), info.absoluteDir().absolutePath(),
+ infoToIconName(info), infoToFileKind(info));
+}
--- /dev/null
+#ifndef SEARCHENGINE_H
+#define SEARCHENGINE_H
+
+#include <QDir>
+
+class SearchWorker;
+
+/**
+ * @brief The SearchEngine is a front-end for the SearchWorker class.
+ * These two classes could be merged, but it is clearer to keep the background thread
+ * in its own class.
+ */
+class SearchEngine : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString dir READ dir() WRITE setDir(QString) NOTIFY dirChanged())
+ Q_PROPERTY(bool running READ running() NOTIFY runningChanged())
+
+public:
+ explicit SearchEngine(QObject *parent = 0);
+ ~SearchEngine();
+
+ // property accessors
+ QString dir() const { return m_dir; }
+ void setDir(QString dir);
+ bool running() const;
+
+ // callable from QML
+ Q_INVOKABLE void search(QString searchTerm);
+ Q_INVOKABLE void cancel();
+
+signals:
+ void dirChanged();
+ void runningChanged();
+
+ void progressChanged(QString directory);
+ void matchFound(QString fullname, QString filename, QString absoluteDir,
+ QString fileIcon, QString fileKind);
+ void workerDone();
+ void workerErrorOccurred(QString message, QString filename);
+
+private slots:
+ void emitMatchFound(QString fullpath);
+
+private:
+ QString m_dir;
+ QString m_errorMessage;
+ SearchWorker *m_searchWorker;
+};
+
+#endif // SEARCHENGINE_H
--- /dev/null
+#include "searchworker.h"
+#include <QDateTime>
+#include <QSettings>
+#include "globals.h"
+
+SearchWorker::SearchWorker(QObject *parent) :
+ QThread(parent),
+ m_cancelled(NotCancelled)
+{
+}
+
+SearchWorker::~SearchWorker()
+{
+}
+
+void SearchWorker::startSearch(QString directory, QString searchTerm)
+{
+ if (isRunning()) {
+ emit errorOccurred(tr("Search already in progress"), "");
+ return;
+ }
+ if (directory.isEmpty() || searchTerm.isEmpty()) {
+ emit errorOccurred(tr("Bad search parameters"), "");
+ return;
+ }
+
+ m_directory = directory;
+ m_searchTerm = searchTerm;
+ m_currentDirectory = directory;
+ m_cancelled.storeRelease(NotCancelled);
+ start();
+}
+
+void SearchWorker::cancel()
+{
+ m_cancelled.storeRelease(Cancelled);
+}
+
+void SearchWorker::run() Q_DECL_OVERRIDE
+{
+ QString errMsg = searchRecursively(m_directory, m_searchTerm.toLower());
+ if (!errMsg.isEmpty())
+ emit errorOccurred(errMsg, m_currentDirectory);
+
+ emit progressChanged("");
+ emit done();
+}
+
+QString SearchWorker::searchRecursively(QString directory, QString searchTerm)
+{
+ // skip some system folders - they don't really have any interesting stuff
+ if (directory.startsWith("/proc") ||
+ directory.startsWith("/sys/block"))
+ return QString();
+
+ QDir dir(directory);
+ if (!dir.exists()) // skip "non-existent" directories (found in /dev)
+ return QString();
+
+ // update progress
+ m_currentDirectory = directory;
+ emit progressChanged(m_currentDirectory);
+
+ QSettings settings;
+ bool hiddenSetting = settings.value("show-hidden-files", false).toBool();
+ QDir::Filter hidden = hiddenSetting ? QDir::Hidden : (QDir::Filter)0;
+
+ // search dirs
+ QStringList names = dir.entryList(QDir::NoDotAndDotDot | QDir::AllDirs | hidden);
+ for (int i = 0 ; i < names.count() ; ++i) {
+ // stop if cancelled
+ if (m_cancelled.loadAcquire() == Cancelled)
+ return QString();
+
+ QString filename = names.at(i);
+ QString fullpath = dir.absoluteFilePath(filename);
+
+ if (filename.toLower().indexOf(searchTerm) >= 0)
+ emit matchFound(fullpath);
+
+ QFileInfo info(fullpath); // skip symlinks to prevent infinite loops
+ if (info.isSymLink())
+ continue;
+
+ QString errmsg = searchRecursively(fullpath, searchTerm);
+ if (!errmsg.isEmpty())
+ return errmsg;
+ }
+
+ // search files
+ names = dir.entryList(QDir::Files | hidden);
+ for (int i = 0 ; i < names.count() ; ++i) {
+ // stop if cancelled
+ if (m_cancelled.loadAcquire() == Cancelled)
+ return QString();
+
+ QString filename = names.at(i);
+ QString fullpath = dir.absoluteFilePath(filename);
+
+ if (filename.toLower().indexOf(searchTerm) >= 0)
+ emit matchFound(fullpath);
+ }
+
+ return QString();
+}
--- /dev/null
+#ifndef SEARCHWORKER_H
+#define SEARCHWORKER_H
+
+#include <QThread>
+#include <QDir>
+
+/**
+ * @brief SearchWorker does searching in the background.
+ */
+class SearchWorker : public QThread
+{
+ Q_OBJECT
+
+public:
+ explicit SearchWorker(QObject *parent = 0);
+ ~SearchWorker();
+
+ void startSearch(QString directory, QString searchTerm);
+
+ void cancel();
+
+signals: // signals, can be connected from a thread to another
+
+ void progressChanged(QString directory);
+
+ void matchFound(QString fullname);
+
+ // one of these is emitted when thread ends
+ void done();
+ void errorOccurred(QString message, QString filename);
+
+protected:
+ void run();
+
+private:
+ enum CancelStatus {
+ Cancelled = 0, NotCancelled = 1
+ };
+
+ QString searchRecursively(QString directory, QString searchTerm);
+
+ QString m_directory;
+ QString m_searchTerm;
+ QAtomicInt m_cancelled; // atomic so no locks needed
+ QString m_currentDirectory;
+};
+
+#endif // SEARCHWORKER_H
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()
if (vpnStatus != newStatus) {
vpnStatus = newStatus;
emit statusChanged(newStatus);
- printf ("Emitting status %d\n", newStatus);
}
}
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);
void VPNControl::setUseTLS(bool value)
{
if (value != useTLS) {
- printf ("Use TLS set to %d\n", value);
useTLS = value;
settingsSetValue("useTLS", value);
emit useTLSChanged(useTLS);
void VPNControl::setCompressed(bool value)
{
if (value != compressed) {
- printf ("Use compression set to %d\n", value);
compressed = value;
settingsSetValue("compressed", value);
emit compressedChanged(compressed);
void VPNControl::setPort(unsigned int value)
{
if (value != port) {
- printf ("Port set to %d\n", value);
port = value;
settingsSetValue("port", value);
emit portChanged(port);
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;
printf ("Process already running.\n");
}
else {
- printf ("Connect\n");
-
vpnProcess = new QProcess();
QString program = "openvpn";
collectArguments ();
void VPNControl::vpnDisconnect() {
if (vpnProcess != NULL) {
- printf ("Disconnect\n");
vpnProcess->terminate();
setStatus(VPNSTATUS_DISCONNECTING);
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;
{
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);
+ }
+}
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;
bool compressed;
bool useTLS;
int tlsDirection;
+ QString logText;
void collectArguments ();
void setStatus (VPNSTATUS newStatus);
bool getCompressed() const;
bool getUseTLS() const;
int getTlsDirection() const;
+ QString getLogText() const;
signals:
void statusChanged(int status);
void compressedChanged(bool compressed);
void useTLSChanged(bool useTLS);
void tlsDirectionChanged (int direction);
+ void logTextChanged (QString logText);
public slots:
void vpnConnect ();
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