├── .appveyor.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── VERSION ├── _config.yml ├── bootstrap.cmd ├── debian ├── changelog ├── compat ├── control ├── copyright ├── rules └── source │ └── format ├── scripts ├── images2ico.py ├── prepare_icons.sh ├── rclone-browser.desktop ├── release_macOS.sh ├── release_ubuntu.sh └── release_windows.cmd └── src ├── CMakeLists.txt ├── Info.plist ├── export_dialog.cpp ├── export_dialog.h ├── export_dialog.ui ├── icon.icns ├── icon.ico ├── icon.png ├── icon_cache.cpp ├── icon_cache.h ├── images ├── amazon_cloud_drive.png ├── b2.png ├── crypt.png ├── drive.png ├── dropbox.png ├── google_cloud_storage.png ├── hubic.png ├── local.png ├── onedrive.png ├── s3.png ├── swift.png ├── unknown.png └── yandex.png ├── item_model.cpp ├── item_model.h ├── job_widget.cpp ├── job_widget.h ├── job_widget.ui ├── main.cpp ├── main_window.cpp ├── main_window.h ├── main_window.ui ├── mount_widget.cpp ├── mount_widget.h ├── mount_widget.ui ├── osx_helper.h ├── osx_helper.mm ├── pch.cpp ├── pch.h ├── preferences_dialog.cpp ├── preferences_dialog.h ├── preferences_dialog.ui ├── progress_dialog.cpp ├── progress_dialog.h ├── progress_dialog.ui ├── remote_widget.cpp ├── remote_widget.h ├── remote_widget.ui ├── resources.qrc ├── resources.rc ├── stream_widget.cpp ├── stream_widget.h ├── stream_widget.ui ├── transfer_dialog.cpp ├── transfer_dialog.h ├── transfer_dialog.ui ├── utils.cpp └── utils.h /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '#{build}' 2 | branches: 3 | only: 4 | - master 5 | skip_tags: true 6 | clone_depth: 1 7 | build_script: 8 | - mkdir build 9 | - cd build 10 | - cmake -G "Visual Studio 12 Win64" -DCMAKE_PREFIX_PATH=C:\Qt\5.8\msvc2013_64 .. 11 | - cmake --build . 12 | test: off 13 | deploy: off 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | *.user* 4 | scripts/*.zip 5 | scripts/*.png 6 | obj-*-linux-gnu 7 | debian/files 8 | debian/debhelper-build-stamp 9 | debian/rclone-browser* 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 1 3 | 4 | matrix: 5 | include: 6 | 7 | - os: linux 8 | language: cpp 9 | dist: trusty 10 | sudo: false 11 | addons: 12 | apt: 13 | packages: 14 | - qttools5-dev 15 | script: 16 | - mkdir build && cd build 17 | - cmake .. 18 | - cmake --build . 19 | 20 | - os: osx 21 | language: cpp 22 | osx_image: xcode8.2 23 | install: 24 | - brew update 25 | - brew install qt5 26 | script: 27 | - mkdir build && cd build 28 | - cmake -DCMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake .. 29 | - cmake --build . 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.2] - 2017-03-11 4 | - Calculate size of folders, issue #4 5 | - Copy transfer command to clipboard, issue #20 6 | - Support custom .rclone.conf location, #21 7 | - Export list of files, issue #27 8 | - Bugfix for folder refresh not working after rename, issue #30 9 | - Remember empty text fields in transfer dialog, issue #32 10 | - Error message when too old rclone version is selected 11 | - Support portable mode, issue #28 12 | - Create .deb packages, issue #26 13 | 14 | ## [1.1] - 2017-01-31 15 | - Added `--transfer` option in UI, issue #1 16 | - Supports encrypted `.rclone.conf` configuration file, issue #2 17 | - Fixed crash when canceling active stream 18 | - Added ETA tooltip for transfer progress bars 19 | - Allow to specify extra arguments for rclone, issue #7 20 | - Fix for browsing Hubic remotes, issue #10 21 | - Support high-dpi mode for macOS 22 | 23 | ## [1.0.0] - 2017-01-29 24 | - Allows to browse and modify any rclone remote, including encrypted ones 25 | - Uses same configuration file as rclone, no extra configuration required 26 | - Simultaneously navigate multiple repositories in separate tabs 27 | - Lists files hierarchically with file name, size and modify date 28 | - All rclone commands are executed asynchronously, no freezing GUI 29 | - File hierarchy is lazily cached in memory, for faster traversal of folders 30 | - Allows to upload, download, create new folders, rename or delete files and folders 31 | - Can process multiple upload or download jobs in background 32 | - Drag & drop support for dragging files from local file explorer for uploading 33 | - Streaming media files for playback in player like mpv or similar 34 | - Mount and unmount folders on macOS and GNU/Linux 35 | - Optionally minimizes to tray, with notifications when upload/download finishes 36 | 37 | [1.2]: https://github.com/mmozeiko/RcloneBrowser/releases/tag/1.2 38 | [1.1]: https://github.com/mmozeiko/RcloneBrowser/releases/tag/1.1 39 | [1.0.0]: https://github.com/mmozeiko/RcloneBrowser/releases/tag/1.0.0 40 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(rclone-browser) 2 | 3 | cmake_minimum_required(VERSION 2.8) 4 | 5 | if(WIN32) 6 | # link automatically to qtmain.lib on Windows 7 | cmake_policy(SET CMP0020 NEW) 8 | endif() 9 | 10 | find_package(Qt5Widgets REQUIRED) 11 | if(WIN32) 12 | find_package(Qt5WinExtras REQUIRED) 13 | elseif(APPLE) 14 | find_package(Qt5MacExtras REQUIRED) 15 | find_library(COCOA_LIB Cocoa REQUIRED) 16 | endif() 17 | 18 | if(WIN32) 19 | set_property(GLOBAL PROPERTY USE_FOLDERS OFF) 20 | 21 | add_definitions("-D_UNICODE -DUNICODE -D_SCL_SECURE_NO_DEPRECATE -D_CRT_SECURE_NO_DEPRECATE") 22 | 23 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") 24 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} /GF /Gy /GS- /GR- /GL") 25 | 26 | set(CMAKE_EXE_LINKER_FLAGS "/INCREMENTAL:NO") 27 | set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG") 28 | set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/LTCG /OPT:ICF /OPT:REF") 29 | 30 | macro(use_pch HEADER SOURCE FILES) 31 | foreach(FILE ${FILES}) 32 | set_source_files_properties(${FILE} PROPERTIES COMPILE_FLAGS "/Yu${HEADER} /FI${HEADER}") 33 | endforeach() 34 | set_source_files_properties(${SOURCE} PROPERTIES COMPILE_FLAGS "/Yc${HEADER}") 35 | endmacro(use_pch) 36 | 37 | else() 38 | 39 | macro(use_pch TARGET HEADER SOURCE) 40 | # TODO 41 | endmacro(use_pch) 42 | 43 | endif() 44 | 45 | file(READ "VERSION" RCLONE_BROWSER_VERSION) 46 | 47 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${rclone-browser_BINARY_DIR}/build") 48 | 49 | add_subdirectory(src) 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **WARNING: This project is not longer active or maintaned.** 2 | 3 | **Initially I created it only because rclone mount did not work in the beginning. Now mount on Windows works fine, so this project is not useful for me anymore.** 4 | 5 | **I suggest to try out rclone built-in [web based GUI](https://rclone.org/gui/) instead** 6 | 7 | RcloneBrowser 8 | ============= 9 | 10 | [![Travis CI Build Status][img1]][1] [![AppVeyor Build Status][img2]][2] [![Downloads][img3]][3] [![Release][img4]][4] [![License][img5]][5] 11 | 12 | Simple cross platfrom GUI for rclone command line tool. 13 | Supports Windows, macOS and GNU/Linux. 14 | 15 | Features 16 | -------- 17 | 18 | * Allows to browse and modify any rclone remote, including encrypted ones 19 | * Uses same configuration file as rclone, no extra configuration required 20 | * Supports custom location and encryption for `.rclone.conf` configuration file 21 | * Simultaneously navigate multiple repositories in separate tabs 22 | * Lists files hierarchically with file name, size and modify date 23 | * All rclone commands are executed asynchronously, no freezing GUI 24 | * File hierarchy is lazily cached in memory, for faster traversal of folders 25 | * Allows to upload, download, create new folders, rename or delete files and folders 26 | * Allows to calculate size of folder, export list of files and copy rclone copmmand to clipboard 27 | * Can process multiple upload or download jobs in background 28 | * Drag & drop support for dragging files from local file explorer for uploading 29 | * Streaming media files for playback in player like [mpv][6] or similar 30 | * Mount and unmount folders on macOS and GNU/Linux 31 | * Optionally minimizes to tray, with notifications when upload/download finishes 32 | * Supports portable mode (create .ini file next to executable with same name), rclone and .rclone.conf path now can be relative to executable 33 | 34 | Download 35 | -------- 36 | 37 | Get Windows, macOS and Ubuntu package on [releases][3] page. 38 | 39 | For Ubuntu you can also install it from Launchpad: [Rclone Browser][launchpad]. 40 | 41 | ArchLinux users can install latest release from AUR repository: [rclone-browser][7]. 42 | 43 | Other GNU/Linux users will need to build from source. 44 | 45 | Screenshots 46 | ----------- 47 | 48 | ### Windows 49 | 50 | ![screenshot1.png][screenshot1] 51 | ![screenshot2.png][screenshot2] 52 | ![screenshot3.png][screenshot3] 53 | ![screenshot4.png][screenshot4] 54 | 55 | ### Ubuntu 56 | 57 | ![screenshot5.png][screenshot5] 58 | 59 | ### macOS 60 | 61 | ![screenshot6.png][screenshot6] 62 | 63 | Build instructions for Windows 64 | ------------------------------ 65 | 66 | 1. Get [Visual Studio 2013][8] 67 | 2. Install [CMake][9] 68 | 3. Install or build from source Qt v5 (64-bit) from [Qt website][10] 69 | 4. Set `QTDIR` environment variable to Qt installation, or adjust path to Qt in `bootstrap.cmd` file 70 | 5. Run `bootstrap.cmd`, it will generate Visual Studio 2013 solution in `build` folder 71 | 72 | Build instructions for GNU/Linux and macOS 73 | ------------------------------------------ 74 | 75 | 1. Make sure you have working compiler and [cmake][9] installed 76 | 2. Install Qt v5 with package manager or from [Qt website][10] 77 | 3. Create new `build` folder next to `src` folder 78 | 4. Run `cmake ..` from `build` folder to create makefile 79 | - if cmake doesn't find Qt, add `-DCMAKE_PREFIX_PATH=path/to/Qt` to previous command 80 | 5. Run `cmake --build .` from `build` folder to create binary 81 | 82 | License 83 | ------- 84 | 85 | This is free and unencumbered software released into the public domain. 86 | 87 | 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. 88 | 89 | [1]: https://travis-ci.org/mmozeiko/RcloneBrowser/ 90 | [2]: https://ci.appveyor.com/project/mmozeiko/RcloneBrowser 91 | [3]: https://github.com/mmozeiko/RcloneBrowser/releases 92 | [4]: https://github.com/mmozeiko/RcloneBrowser/releases/latest 93 | [5]: https://github.com/mmozeiko/RcloneBrowser/blob/master/LICENSE 94 | [6]: https://mpv.io/ 95 | [7]: https://aur.archlinux.org/packages/rclone-browser 96 | [8]: https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs 97 | [9]: http://www.cmake.org/ 98 | [10]: https://www.qt.io/download-open-source/ 99 | [img1]: https://api.travis-ci.org/mmozeiko/RcloneBrowser.svg?branch=master 100 | [img2]: https://ci.appveyor.com/api/projects/status/7s24ixolrk3ueggm/branch/master?svg=true 101 | [img3]: https://img.shields.io/github/downloads/mmozeiko/RcloneBrowser/total.svg?maxAge=3600 102 | [img4]: https://img.shields.io/github/release/mmozeiko/RcloneBrowser.svg?maxAge=3600 103 | [img5]: https://img.shields.io/github/license/mmozeiko/RcloneBrowser.svg?maxAge=2592000 104 | [screenshot1]: https://raw.githubusercontent.com/wiki/mmozeiko/RcloneBrowser/screenshot1.png 105 | [screenshot2]: https://raw.githubusercontent.com/wiki/mmozeiko/RcloneBrowser/screenshot2.png 106 | [screenshot3]: https://raw.githubusercontent.com/wiki/mmozeiko/RcloneBrowser/screenshot3.png 107 | [screenshot4]: https://raw.githubusercontent.com/wiki/mmozeiko/RcloneBrowser/screenshot4.png 108 | [screenshot5]: https://raw.githubusercontent.com/wiki/mmozeiko/RcloneBrowser/screenshot5.png 109 | [screenshot6]: https://raw.githubusercontent.com/wiki/mmozeiko/RcloneBrowser/screenshot6.png 110 | [launchpad]: https://launchpad.net/~mmozeiko/+archive/ubuntu/rclone-browser 111 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.2 -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /bootstrap.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | if "%QTDIR%" EQU "" ( 4 | set QT=C:\Qt\5.8.0-desktop-vs2013-x64 5 | ) else ( 6 | set QT=%QTDIR% 7 | ) 8 | set PATH=%QT%\bin;%PATH% 9 | 10 | set _IsNativeEnvironment=true 11 | call "%VS120COMNTOOLS%..\..\VC\vcvarsall.bat" x64 12 | 13 | set BUILD="%~dp0build" 14 | 15 | mkdir "%BUILD%" 2>nul 16 | pushd "%BUILD%" 17 | 18 | cmake -G "Visual Studio 12 Win64" -DCMAKE_CONFIGURATION_TYPES="Debug;Release" .. 19 | if %ERRORLEVEL% neq 0 ( 20 | popd 21 | exit /b 1 22 | ) 23 | devenv /useenv rclone-browser.sln 24 | 25 | popd 26 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | rclone-browser (1.2) unstable; urgency=medium 2 | 3 | * initial verision 4 | 5 | -- Mārtiņš Možeiko Tue, 07 Mar 2017 00:09:24 -0800 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: rclone-browser 2 | Section: net 3 | Priority: optional 4 | Maintainer: Mārtiņš Možeiko 5 | Build-Depends: debhelper (>= 9), qtbase5-dev, cmake (>= 2.8) 6 | Standards-Version: 3.9.8 7 | Homepage: https://mmozeiko.github.io/RcloneBrowser/ 8 | 9 | Package: rclone-browser 10 | Architecture: any 11 | Depends: ${shlibs:Depends}, ${misc:Depends} 12 | Description: Simple cross platform GUI for rclone 13 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: rclone-browser 3 | Source: https://mmozeiko.github.io/RcloneBrowser/ 4 | 5 | Files: * 6 | Copyright: 2017 Martins Mozeiko 7 | License: Unlicense 8 | This is free and unencumbered software released into the public domain. 9 | 10 | Anyone is free to copy, modify, publish, use, compile, sell, or 11 | distribute this software, either in source code form or as a compiled 12 | binary, for any purpose, commercial or non-commercial, and by any 13 | means. 14 | 15 | In jurisdictions that recognize copyright laws, the author or authors 16 | of this software dedicate any and all copyright interest in the 17 | software to the public domain. We make this dedication for the benefit 18 | of the public at large and to the detriment of our heirs and 19 | successors. We intend this dedication to be an overt act of 20 | relinquishment in perpetuity of all present and future rights to this 21 | software under copyright law. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 26 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 27 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 28 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 29 | OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | For more information, please refer to 32 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ --buildsystem=cmake 5 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /scripts/images2ico.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # packs multiple images (bmp/png/...) into ico file 4 | # width and height of images must be <= 256 5 | # pixel format of images must be 32-bit RGBA 6 | 7 | import argparse 8 | import struct 9 | import os 10 | from PIL import Image # https://python-pillow.org/ 11 | 12 | def pack(output, inp): 13 | count = len(inp) 14 | 15 | with open(output, "wb") as f: 16 | f.write(struct.pack("HHH", 0, 1, count)) 17 | offset = struct.calcsize("HHH") + struct.calcsize("BBBBHHII")*count 18 | 19 | for i in inp: 20 | size = os.stat(i).st_size 21 | img = Image.open(i) 22 | w = 0 if img.width == 256 else img.width 23 | h = 0 if img.height == 256 else img.height 24 | f.write(struct.pack("BBBBHHII", w, h, 0, 0, 1, 32, size, offset)) 25 | offset += size 26 | 27 | for i in inp: 28 | f.write(open(i, "rb").read()) 29 | 30 | if __name__ == "__main__": 31 | ap = argparse.ArgumentParser(description="pack multiple images into ico file") 32 | ap.add_argument("-o", "--out", help="output file") 33 | ap.add_argument("input", type=str, nargs='+', help="input images") 34 | args = ap.parse_args() 35 | pack(args.out, args.input) 36 | -------------------------------------------------------------------------------- /scripts/prepare_icons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | URL=https://raw.githubusercontent.com/ncw/rclone/master/graphics/rclone.png 6 | 7 | [ -f "rclone.png" ] || curl -o rclone.png -L $URL 8 | 9 | SIZES=(512 256 128 64 32 16) 10 | for s in "${SIZES[@]}" 11 | do 12 | convert rclone.png -resize $s rclone_$s.png 13 | optipng -o7 -strip all rclone_$s.png 14 | done 15 | 16 | if [ `uname` == "Linux" ] 17 | then 18 | ./images2ico.py -o ../src/icon.ico rclone_256.png rclone_128.png rclone_64.png rclone_32.png rclone_16.png 19 | else 20 | # brew install makeicns 21 | makeicns -out ../src/icon.icns -512 rclone_512.png -256 rclone_256.png -128 rclone_128.png -64 rclone_64.png -32 rclone_32.png -16 rclone_16.png 22 | fi 23 | -------------------------------------------------------------------------------- /scripts/rclone-browser.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Rclone Browser 3 | Comment=Simple cross-platform GUI for rclone 4 | Exec=/usr/bin/rclone-browser 5 | Icon=/usr/share/pixmaps/rclone-browser.png 6 | Terminal=false 7 | Type=Application 8 | Categories=Network 9 | StartupNotify=false 10 | -------------------------------------------------------------------------------- /scripts/release_macOS.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | QTDIR=~/Qt/5.8.0-desktop 6 | 7 | ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"/.. 8 | VERSION=`cat $ROOT/VERSION`-`git rev-parse --short HEAD` 9 | BUILD="$ROOT"/build 10 | TARGET=rclone-browser-$VERSION-macOS 11 | APP="$TARGET"/"Rclone Browser.app" 12 | 13 | rm -rf "$BUILD" 14 | mkdir -p "$BUILD" 15 | cd "$BUILD" 16 | cmake .. -DCMAKE_PREFIX_PATH="$QTDIR" -DCMAKE_BUILD_TYPE=Release 17 | make -j2 18 | cd .. 19 | 20 | rm -rf "$TARGET" 21 | mkdir "$TARGET" 22 | cp "$ROOT"/README.md "$TARGET"/Readme.txt 23 | cp "$ROOT"/CHANGELOG.md "$TARGET"/Changelog.txt 24 | cp "$ROOT"/LICENSE "$TARGET"/License.txt 25 | cp -R "$BUILD"/build/rclone-browser.app "$APP" 26 | mv "$APP"/Contents/MacOS/rclone-browser "$APP"/Contents/MacOS/"Rclone Browser" 27 | 28 | sed -i .bak 's/rclone-browser/Rclone Browser/g' "$APP"/Contents/Info.plist 29 | rm "$APP"/Contents/*.bak 30 | 31 | FRAMEWORKS=("Core" "Gui" "Widgets" "PrintSupport" "MacExtras") 32 | 33 | mkdir "$APP"/Contents/Frameworks 34 | for FX in "${FRAMEWORKS[@]}" 35 | do 36 | cp -R "$QTDIR"/lib/Qt${FX}.framework "$APP"/Contents/Frameworks/ 37 | FXPATH="$APP"/Contents/Frameworks/Qt${FX}.framework 38 | install_name_tool -id @executable_path/../Frameworks/Qt${FX}.framework/Versions/5/Qt${FX} "${FXPATH}"/Versions/5/Qt${FX} 39 | rm -rf "$FXPATH"/Headers 40 | rm -rf "$FXPATH"/*.prl 41 | rm -rf "$FXPATH"/*_debug 42 | rm -rf "$FXPATH"/Versions/5/Headers 43 | rm -rf "$FXPATH"/Versions/5/*_debug 44 | done 45 | 46 | mkdir -p "$APP"/Contents/Plugins/platforms 47 | cp "$QTDIR"/plugins/platforms/libqcocoa.dylib "$APP"/Contents/Plugins/platforms 48 | 49 | change() 50 | { 51 | for x in $2 52 | do 53 | install_name_tool -change @rpath/Qt$x.framework/Versions/5/Qt$x @executable_path/../Frameworks/Qt$x.framework/Versions/5/Qt$x "$1" 54 | done 55 | } 56 | 57 | change "$APP"/Contents/MacOS/"Rclone Browser" "Core Gui Widgets MacExtras" 58 | change "$APP"/Contents/Frameworks/QtGui.framework/QtGui "Core" 59 | change "$APP"/Contents/Frameworks/QtWidgets.framework/QtWidgets "Core Gui" 60 | change "$APP"/Contents/Frameworks/QtMacExtras.framework/QtMacExtras "Core Gui Widgets" 61 | change "$APP"/Contents/Frameworks/QtPrintSupport.framework/QtPrintSupport "Core Gui Widgets" 62 | change "$APP"/Contents/Plugins/platforms/libqcocoa.dylib "Core Gui Widgets PrintSupport" 63 | 64 | cat >"$APP"/Contents/MacOS/qt.conf <nul 46 | 47 | copy "%ROOT%\README.md" "%TARGET%\Readme.txt" 48 | copy "%ROOT%\CHANGELOG.md" "%TARGET%\Changelog.txt" 49 | copy "%ROOT%\LICENSE" "%TARGET%\License.txt" 50 | copy "%BUILD%\RcloneBrowser.exe" "%TARGET%" 51 | 52 | windeployqt.exe --no-translations --no-angle --no-compiler-runtime --no-svg "%TARGET%\RcloneBrowser.exe" 53 | rd /s /q "%TARGET%\imageformats" 54 | 55 | copy "%VS120COMNTOOLS%..\..\VC\redist\%ARCH%\Microsoft.VC120.CRT\msvcp120.dll" "%TARGET%" 56 | copy "%VS120COMNTOOLS%..\..\VC\redist\%ARCH%\Microsoft.VC120.CRT\msvcr120.dll" "%TARGET%" 57 | 58 | ( 59 | echo [Paths] 60 | echo Prefix = . 61 | echo LibraryExecutables = . 62 | echo Plugins = . 63 | )>"%TARGET%\qt.conf" 64 | 7za.exe a -mx=9 -r -tzip "%TARGET%.zip" "%TARGET%" 65 | rd /s /q "%TARGET%" 66 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(WIN32) 2 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /WX /wd4100 /wd4189") 3 | else() 4 | add_definitions("-pedantic -Wall -Wextra -Werror -std=c++11") 5 | endif() 6 | 7 | project(rclone-browser) 8 | 9 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 10 | 11 | set(UI 12 | main_window.ui 13 | remote_widget.ui 14 | transfer_dialog.ui 15 | export_dialog.ui 16 | progress_dialog.ui 17 | job_widget.ui 18 | mount_widget.ui 19 | stream_widget.ui 20 | preferences_dialog.ui 21 | ) 22 | 23 | set(MOC 24 | main_window.h 25 | remote_widget.h 26 | transfer_dialog.h 27 | export_dialog.h 28 | progress_dialog.h 29 | job_widget.h 30 | mount_widget.h 31 | stream_widget.h 32 | preferences_dialog.h 33 | icon_cache.h 34 | item_model.h 35 | ) 36 | 37 | set(OTHER 38 | pch.h 39 | utils.h 40 | ) 41 | 42 | set(SOURCE 43 | pch.cpp 44 | main.cpp 45 | main_window.cpp 46 | remote_widget.cpp 47 | transfer_dialog.cpp 48 | export_dialog.cpp 49 | progress_dialog.cpp 50 | job_widget.cpp 51 | mount_widget.cpp 52 | stream_widget.cpp 53 | preferences_dialog.cpp 54 | icon_cache.cpp 55 | item_model.cpp 56 | utils.cpp 57 | ) 58 | 59 | if(WIN32) 60 | set(OTHER ${OTHER} resources.rc) 61 | elseif(APPLE) 62 | set(OTHER ${OTHER} osx_helper.h) 63 | set(SOURCE ${SOURCE} osx_helper.mm) 64 | endif() 65 | 66 | set(QRC resources.qrc) 67 | 68 | add_definitions(-DRCLONE_BROWSER_VERSION="${RCLONE_BROWSER_VERSION}") 69 | 70 | qt5_wrap_ui(UI_OUT ${UI}) 71 | qt5_wrap_cpp(MOC_OUT ${MOC}) 72 | qt5_add_resources(QRC_OUT ${QRC} OPTIONS "-no-compress") 73 | 74 | source_group("" FILES ${SOURCE} ${MOC} ${UI} ${QRC} ${OTHER}) 75 | source_group("Generated" FILES ${MOC_OUT} ${UI_OUT} ${MOC_OUT} ${QRC_OUT}) 76 | 77 | use_pch(pch.h pch.cpp "${SOURCE}") 78 | use_pch(pch.h pch.cpp "${MOC_OUT}") 79 | 80 | if(WIN32) 81 | add_executable(RcloneBrowser WIN32 ${SOURCE} ${BACKEND} ${OTHER} ${MOC} ${MOC_OUT} ${UI_OUT} ${MOC_OUT} ${QRC_OUT}) 82 | target_link_libraries(RcloneBrowser Qt5::Widgets Qt5::WinExtras) 83 | elseif(APPLE) 84 | set_source_files_properties(icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") 85 | add_executable(rclone-browser MACOSX_BUNDLE ${SOURCE} ${BACKEND} ${OTHER} ${MOC} ${MOC_OUT} ${UI_OUT} ${MOC_OUT} ${QRC_OUT} icon.icns) 86 | target_link_libraries(rclone-browser Qt5::Widgets Qt5::MacExtras ${COCOA_LIB}) 87 | set_target_properties(rclone-browser PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist") 88 | 89 | add_custom_command(TARGET rclone-browser POST_BUILD COMMAND 90 | ${CMAKE_COMMAND} -E copy_directory "src/icon.icns" "$/../Resources/") 91 | else() 92 | add_executable(rclone-browser ${SOURCE} ${BACKEND} ${OTHER} ${MOC} ${MOC_OUT} ${UI_OUT} ${MOC_OUT} ${QRC_OUT}) 93 | target_link_libraries(rclone-browser Qt5::Widgets) 94 | 95 | install(TARGETS rclone-browser RUNTIME DESTINATION bin) 96 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icon.png" DESTINATION "share/pixmaps" RENAME "rclone-browser.png") 97 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/rclone-browser.desktop" DESTINATION "share/applications") 98 | endif() 99 | -------------------------------------------------------------------------------- /src/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrincipalClass 6 | NSApplication 7 | CFBundleIconFile 8 | icon.icns 9 | CFBundlePackageType 10 | APPL 11 | CFBundleExecutable 12 | rclone-browser 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/export_dialog.cpp: -------------------------------------------------------------------------------- 1 | #include "export_dialog.h" 2 | #include "utils.h" 3 | 4 | ExportDialog::ExportDialog(const QString& remote, const QDir& path, QWidget* parent) 5 | : QDialog(parent) 6 | { 7 | ui.setupUi(this); 8 | resize(0, 0); 9 | 10 | mTarget = remote + ":" + path.path(); 11 | 12 | QObject::connect(ui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [=]() 13 | { 14 | ui.rbText->setChecked(true); 15 | ui.checkVerbose->setChecked(false); 16 | ui.checkSameFilesystem->setChecked(false); 17 | ui.textMinSize->clear(); 18 | ui.textMinAge->clear(); 19 | ui.textMaxAge->clear(); 20 | ui.spinMaxDepth->setValue(0); 21 | ui.textExclude->clear(); 22 | ui.textExtra->clear(); 23 | }); 24 | ui.buttonBox->button(QDialogButtonBox::RestoreDefaults)->click(); 25 | 26 | QObject::connect(ui.buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); 27 | QObject::connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); 28 | 29 | QObject::connect(ui.fileBrowse, &QToolButton::clicked, this, [=]() 30 | { 31 | QString file = QFileDialog::getSaveFileName(this, "Choose destination file"); 32 | if (!file.isEmpty()) 33 | { 34 | ui.textFile->setText(QDir::toNativeSeparators(file)); 35 | } 36 | }); 37 | 38 | auto settings = GetSettings(); 39 | settings->beginGroup("Export"); 40 | ReadSettings(settings.get(), this); 41 | settings->endGroup(); 42 | } 43 | 44 | ExportDialog::~ExportDialog() 45 | { 46 | if (result() == QDialog::Accepted) 47 | { 48 | auto settings = GetSettings(); 49 | settings->beginGroup("Export"); 50 | WriteSettings(settings.get(), this); 51 | settings->remove("textFile"); 52 | settings->endGroup(); 53 | } 54 | } 55 | 56 | QString ExportDialog::getDestination() const 57 | { 58 | return ui.textFile->text(); 59 | } 60 | 61 | bool ExportDialog::onlyFilenames() const 62 | { 63 | return ui.rbText->isChecked(); 64 | } 65 | 66 | QStringList ExportDialog::getOptions() const 67 | { 68 | QStringList list; 69 | list << "lsl"; 70 | if (ui.checkVerbose->isChecked()) 71 | { 72 | list << "--verbose"; 73 | } 74 | if (ui.checkSameFilesystem->isChecked()) 75 | { 76 | list << "--one-file-system"; 77 | } 78 | if (!ui.textMinSize->text().isEmpty()) 79 | { 80 | list << "--min-size" << ui.textMinSize->text(); 81 | } 82 | if (!ui.textMinAge->text().isEmpty()) 83 | { 84 | list << "--min-age" << ui.textMinAge->text(); 85 | } 86 | if (!ui.textMaxAge->text().isEmpty()) 87 | { 88 | list << "--max-age" << ui.textMaxAge->text(); 89 | } 90 | if (ui.spinMaxDepth->value() != 0) 91 | { 92 | list << "--max-depth" << ui.spinMaxDepth->text(); 93 | } 94 | 95 | QString excluded = ui.textExclude->toPlainText().trimmed(); 96 | if (!excluded.isEmpty()) 97 | { 98 | for (auto line : excluded.split('\n')) 99 | { 100 | list << "--exclude" << line; 101 | } 102 | } 103 | 104 | QString extra = ui.textExtra->text().trimmed(); 105 | if (!extra.isEmpty()) 106 | { 107 | for (auto arg : extra.split(' ')) 108 | { 109 | list << arg; 110 | } 111 | } 112 | 113 | list << mTarget; 114 | 115 | return list; 116 | } 117 | 118 | void ExportDialog::done(int r) 119 | { 120 | if (r == QDialog::Accepted) 121 | { 122 | if (ui.textFile->text().isEmpty()) 123 | { 124 | QMessageBox::warning(this, "Warning", "Please enter destination filename!"); 125 | return; 126 | } 127 | } 128 | QDialog::done(r); 129 | } 130 | -------------------------------------------------------------------------------- /src/export_dialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | #include "ui_export_dialog.h" 5 | 6 | class ExportDialog : public QDialog 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | ExportDialog(const QString& remote, const QDir& path, QWidget* parent = nullptr); 12 | ~ExportDialog(); 13 | 14 | QString getDestination() const; 15 | bool onlyFilenames() const; 16 | QStringList getOptions() const; 17 | 18 | private: 19 | Ui::ExportDialog ui; 20 | QString mTarget; 21 | 22 | void done(int r) override; 23 | }; 24 | -------------------------------------------------------------------------------- /src/export_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ExportDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 614 10 | 358 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | QLayout::SetFixedSize 22 | 23 | 24 | 25 | 26 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 0 39 | 40 | 41 | 42 | File: 43 | 44 | 45 | textFile 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ... 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 66 | 67 | 68 | 69 | Settings 70 | 71 | 72 | 73 | 74 | 75 | Format 76 | 77 | 78 | 79 | 0 80 | 81 | 82 | 83 | 84 | 85 | 0 86 | 0 87 | 88 | 89 | 90 | Text (only filenames) 91 | 92 | 93 | true 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 0 102 | 0 103 | 104 | 105 | 106 | CSV (with size and datetime) 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Settings 117 | 118 | 119 | 120 | 121 | 122 | Extra arguments: 123 | 124 | 125 | 126 | 127 | 128 | 129 | Maximum age (s or ms|s|m|h|d|w|M|y suffix) 130 | 131 | 132 | textMaxAge 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 0 141 | 0 142 | 143 | 144 | 145 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 146 | 147 | 148 | 999999 149 | 150 | 151 | 152 | 153 | 154 | 155 | Minimum age (s or ms|s|m|h|d|w|M|y suffix) 156 | 157 | 158 | textMinAge 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 0 167 | 0 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 0 177 | 0 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | Minimum size (KiB or b|k|M|G suffix) 186 | 187 | 188 | textMinSize 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 0 197 | 0 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | Maximum depth 206 | 207 | 208 | spinMaxDepth 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | Don't cross filesystem boundaries 219 | 220 | 221 | 222 | 223 | 224 | 225 | Verbose output 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | Exclude 237 | 238 | 239 | 240 | 241 | 242 | QPlainTextEdit::NoWrap 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | textFile 254 | fileBrowse 255 | tabWidget 256 | rbText 257 | rbCSV 258 | textMinSize 259 | textMinAge 260 | textMaxAge 261 | spinMaxDepth 262 | checkVerbose 263 | checkSameFilesystem 264 | textExtra 265 | textExclude 266 | 267 | 268 | 269 | 270 | -------------------------------------------------------------------------------- /src/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/icon.icns -------------------------------------------------------------------------------- /src/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/icon.ico -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/icon.png -------------------------------------------------------------------------------- /src/icon_cache.cpp: -------------------------------------------------------------------------------- 1 | #include "icon_cache.h" 2 | #include "item_model.h" 3 | #if defined(Q_OS_OSX) 4 | #include "osx_helper.h" 5 | #endif 6 | 7 | IconCache::IconCache(QObject* parent) 8 | : QObject(parent) 9 | { 10 | mFileIcon = QFileIconProvider().icon(QFileIconProvider::File); 11 | 12 | #ifdef Q_OS_WIN32 13 | CoInitializeEx(NULL, COINIT_MULTITHREADED); 14 | #endif 15 | 16 | mThread.start(); 17 | moveToThread(&mThread); 18 | } 19 | 20 | IconCache::~IconCache() 21 | { 22 | mThread.quit(); 23 | mThread.wait(); 24 | 25 | #ifdef Q_OS_WIN32 26 | CoUninitialize(); 27 | #endif 28 | } 29 | 30 | void IconCache::getIcon(Item* item, const QPersistentModelIndex& parent) 31 | { 32 | QString ext = QFileInfo(item->name).suffix(); 33 | QIcon icon; 34 | auto it = mIcons.find(ext); 35 | if (it == mIcons.end()) 36 | { 37 | #if defined(Q_OS_WIN32) 38 | SHFILEINFOW info; 39 | if (SHGetFileInfoW(reinterpret_cast(("dummy." + ext).utf16()), 40 | FILE_ATTRIBUTE_NORMAL, &info, sizeof(info), SHGFI_ICON | SHGFI_USEFILEATTRIBUTES) && info.hIcon) 41 | { 42 | icon = QtWin::fromHICON(info.hIcon); 43 | DestroyIcon(info.hIcon); 44 | } 45 | #elif defined(Q_OS_OSX) 46 | icon = osxGetIcon(ext.toUtf8().constData()); 47 | #else 48 | QMimeType mime = mMimeDatabase.mimeTypeForFile(item->name, QMimeDatabase::MatchExtension); 49 | if (mime.isValid()) 50 | { 51 | icon = QIcon::fromTheme(mime.iconName()); 52 | } 53 | #endif 54 | if (icon.isNull()) 55 | { 56 | icon = mFileIcon; 57 | } 58 | mIcons.insert(ext, icon); 59 | } 60 | else 61 | { 62 | icon = it.value(); 63 | } 64 | 65 | emit iconReady(item, parent, icon); 66 | } 67 | -------------------------------------------------------------------------------- /src/icon_cache.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | 5 | struct Item; 6 | 7 | class IconCache : public QObject 8 | { 9 | Q_OBJECT 10 | public: 11 | IconCache(QObject* parent = nullptr); 12 | ~IconCache(); 13 | 14 | public slots: 15 | void getIcon(Item* item, const QPersistentModelIndex& parent); 16 | 17 | signals: 18 | void iconReady(Item* item, const QPersistentModelIndex& parent, const QIcon& icon); 19 | 20 | private: 21 | QThread mThread; 22 | QIcon mFileIcon; 23 | 24 | QHash mIcons; 25 | 26 | #if !defined(Q_OS_WIN32) && !defined(Q_OS_OSX) 27 | QMimeDatabase mMimeDatabase; 28 | #endif 29 | }; 30 | -------------------------------------------------------------------------------- /src/images/amazon_cloud_drive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/amazon_cloud_drive.png -------------------------------------------------------------------------------- /src/images/b2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/b2.png -------------------------------------------------------------------------------- /src/images/crypt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/crypt.png -------------------------------------------------------------------------------- /src/images/drive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/drive.png -------------------------------------------------------------------------------- /src/images/dropbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/dropbox.png -------------------------------------------------------------------------------- /src/images/google_cloud_storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/google_cloud_storage.png -------------------------------------------------------------------------------- /src/images/hubic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/hubic.png -------------------------------------------------------------------------------- /src/images/local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/local.png -------------------------------------------------------------------------------- /src/images/onedrive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/onedrive.png -------------------------------------------------------------------------------- /src/images/s3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/s3.png -------------------------------------------------------------------------------- /src/images/swift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/swift.png -------------------------------------------------------------------------------- /src/images/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/unknown.png -------------------------------------------------------------------------------- /src/images/yandex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmozeiko/RcloneBrowser/06200a05fd0ede12797d64b4afc7abef3380bc59/src/images/yandex.png -------------------------------------------------------------------------------- /src/item_model.cpp: -------------------------------------------------------------------------------- 1 | #include "item_model.h" 2 | #include "icon_cache.h" 3 | #include "utils.h" 4 | 5 | namespace 6 | { 7 | static void advanceSpinner(QString& text) 8 | { 9 | int spinnerPos = (int)((size_t)text.length() - 2); 10 | QChar current = text[spinnerPos]; 11 | static const QChar spinner[] = { '-', '\\', '|', '/' }; 12 | size_t spinnerCount = sizeof(spinner) / sizeof(*spinner); 13 | const QChar* found = qFind(spinner, spinner + spinnerCount, current); 14 | size_t idx = found - spinner; 15 | size_t next = idx == spinnerCount - 1 ? 0 : idx + 1; 16 | text[spinnerPos] = spinner[next]; 17 | } 18 | 19 | QString getNiceSize(quint64 size) 20 | { 21 | static const char prefix[] = " KMGTPE"; 22 | for (int i = sizeof(prefix) - 2; i >= 0; i--) 23 | { 24 | quint64 base = quint64(1) << (i * 10); 25 | if (size >= 10 * base) 26 | { 27 | return QString("%1 %2").arg(size / base).arg(QChar(prefix[i])).trimmed(); 28 | } 29 | } 30 | return "0"; 31 | } 32 | } 33 | 34 | class ItemSorter 35 | { 36 | public: 37 | inline ItemSorter(int column, Qt::SortOrder order) 38 | : mColumn(column) 39 | , mOrder(order) 40 | { 41 | mCompare.setNumericMode(true); 42 | } 43 | 44 | bool operator()(const Item* a, const Item* b) const 45 | { 46 | switch (mColumn) 47 | { 48 | case 0: 49 | if (a->isFolder != b->isFolder) 50 | { 51 | return a->isFolder; 52 | } 53 | return mOrder == Qt::AscendingOrder 54 | ? mCompare.compare(a->name, b->name) < 0 55 | : mCompare.compare(b->name, a->name) < 0; 56 | 57 | case 1: 58 | if (a->isFolder != b->isFolder) 59 | { 60 | return a->isFolder; 61 | } 62 | if (a->size == b->size) 63 | { 64 | return mOrder == Qt::AscendingOrder 65 | ? mCompare.compare(a->name, b->name) < 0 66 | : mCompare.compare(b->name, a->name) < 0; 67 | } 68 | return mOrder == Qt::AscendingOrder ? a->size < b->size : b->size < a->size; 69 | 70 | case 2: 71 | if (a->isFolder != b->isFolder) 72 | { 73 | return a->isFolder; 74 | } 75 | if (a->modified == b->modified) 76 | { 77 | return mOrder == Qt::AscendingOrder 78 | ? mCompare.compare(a->name, b->name) < 0 79 | : mCompare.compare(b->name, a->name) < 0; 80 | } 81 | return mOrder == Qt::AscendingOrder ? a->modified < b->modified : b->modified < a->modified; 82 | } 83 | Q_ASSERT(false); 84 | return false; 85 | } 86 | 87 | private: 88 | QCollator mCompare; 89 | int mColumn; 90 | Qt::SortOrder mOrder; 91 | }; 92 | 93 | ItemModel::ItemModel(IconCache* icons, const QString& remote, QObject* parent) 94 | : QAbstractItemModel(parent) 95 | , mRemote(remote) 96 | , mFixedFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)) 97 | , mRegExpFolder(R"(^[\d-]+ (\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d) \s*[\d-]+ (.+)$)") 98 | , mRegExpFile(R"(^(\d+) (\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)\.\d+ (.+)$)") 99 | { 100 | QStyle* style = qApp->style(); 101 | mDriveIcon = style->standardIcon(QStyle::SP_DriveNetIcon); 102 | mFolderIcon = style->standardIcon(QStyle::SP_DirIcon); 103 | mFileIcon = style->standardIcon(QStyle::SP_FileIcon); 104 | 105 | auto settings = GetSettings(); 106 | mFolderIcons = settings->value("Settings/showFolderIcons", true).toBool(); 107 | mFileIcons = settings->value("Settings/showFileIcons", true).toBool(); 108 | 109 | mRoot = new Item(); 110 | mRoot->isFolder = true; 111 | mRoot->state = Item::Ready; 112 | 113 | QObject::connect(this, &ItemModel::getIcon, icons, &IconCache::getIcon); 114 | QObject::connect(icons, &IconCache::iconReady, this, [=](Item* item, const QPersistentModelIndex& parent, const QIcon& icon) 115 | { 116 | item->state = Item::Ready; 117 | QString ext = QFileInfo(item->name).suffix(); 118 | if (!mLoadedIcons.contains(ext)) 119 | { 120 | mLoadedIcons.insert(ext, icon); 121 | } 122 | 123 | if (item->isDeleted) 124 | { 125 | delete item; 126 | return; 127 | } 128 | 129 | QModelIndex idx = index(item->num(), 0, parent); 130 | emit dataChanged(idx, idx, QVector{Qt::DecorationRole}); 131 | }); 132 | } 133 | 134 | ItemModel::~ItemModel() 135 | { 136 | delete mRoot; 137 | } 138 | 139 | const QDir& ItemModel::path(const QModelIndex& index) const 140 | { 141 | return get(index)->path; 142 | } 143 | 144 | bool ItemModel::isLoading(const QModelIndex& index) const 145 | { 146 | return get(index)->parent->isLoading(); 147 | } 148 | 149 | void ItemModel::refresh(const QModelIndex& index) 150 | { 151 | Item* item = get(index); 152 | Item* folderItem = item->isFolder ? item : item->parent; 153 | if (folderItem->isLoading()) 154 | { 155 | return; 156 | } 157 | load(item->isFolder ? index : index.parent(), folderItem); 158 | } 159 | 160 | void ItemModel::rename(const QModelIndex& index, const QString& name) 161 | { 162 | Item* item = get(index); 163 | item->name = name; 164 | item->path = item->parent->path.filePath(item->name); 165 | emit dataChanged(index, index, QVector{Qt::DisplayRole}); 166 | } 167 | 168 | bool ItemModel::isTopLevel(const QModelIndex& index) const 169 | { 170 | return get(index)->parent == mRoot; 171 | } 172 | 173 | bool ItemModel::isFolder(const QModelIndex& index) const 174 | { 175 | return get(index)->isFolder; 176 | } 177 | 178 | QModelIndex ItemModel::addRoot(const QString& name, const QString& path) 179 | { 180 | emit layoutAboutToBeChanged(); 181 | 182 | Item* item = new Item(); 183 | item->isFolder = true; 184 | item->name = name; 185 | item->path = path; 186 | item->parent = mRoot; 187 | mRoot->childs.append(item); 188 | 189 | emit layoutChanged(); 190 | 191 | return createIndex(item->num(), 0, item); 192 | } 193 | 194 | QModelIndex ItemModel::index(int row, int column, const QModelIndex& parent) const 195 | { 196 | if (!hasIndex(row, column, parent)) 197 | { 198 | return QModelIndex(); 199 | } 200 | 201 | Item* item = get(parent); 202 | return createIndex(row, column, item->childs[row]); 203 | } 204 | 205 | QModelIndex ItemModel::parent(const QModelIndex& index) const 206 | { 207 | if (!index.isValid()) 208 | { 209 | return QModelIndex(); 210 | } 211 | 212 | Item* child = get(index); 213 | if (child->parent == mRoot) 214 | { 215 | return QModelIndex(); 216 | } 217 | 218 | return createIndex(child->parent->num(), 0, child->parent); 219 | } 220 | 221 | bool ItemModel::hasChildren(const QModelIndex& parent) const 222 | { 223 | Item* item = get(parent); 224 | if (item->isFolder) 225 | { 226 | if (item->state == Item::Ready) 227 | { 228 | return !item->childs.isEmpty(); 229 | } 230 | return true; 231 | } 232 | return false; 233 | } 234 | 235 | int ItemModel::rowCount(const QModelIndex& parent) const 236 | { 237 | Item* item = get(parent); 238 | if (item->isFolder) 239 | { 240 | if (item->state == Item::Unknown) 241 | { 242 | const_cast(this)->load(parent, item); 243 | } 244 | } 245 | return item->childs.count(); 246 | } 247 | 248 | int ItemModel::columnCount(const QModelIndex& parent) const 249 | { 250 | Q_UNUSED(parent); 251 | return 3; 252 | } 253 | 254 | void ItemModel::sort(int column, Qt::SortOrder order) 255 | { 256 | mSortColumn = column; 257 | mSortOrder = order; 258 | sort(QModelIndex(), mRoot); 259 | } 260 | 261 | QVariant ItemModel::data(const QModelIndex& index, int role) const 262 | { 263 | if (!index.isValid()) 264 | { 265 | return QVariant(); 266 | } 267 | 268 | const Item* item = get(index); 269 | 270 | if (role == Qt::DecorationRole && index.column() == 0) 271 | { 272 | if (item->state == Item::Special) 273 | { 274 | return QIcon(); 275 | } 276 | 277 | if (item->isFolder) 278 | { 279 | if (mFolderIcons) 280 | { 281 | return item->parent == mRoot ? mDriveIcon : mFolderIcon; 282 | } 283 | return QIcon(); 284 | } 285 | 286 | if (mFileIcons) 287 | { 288 | QString ext = QFileInfo(item->name).suffix(); 289 | auto it = mLoadedIcons.find(ext); 290 | if (it == mLoadedIcons.end()) 291 | { 292 | return mFileIcon; 293 | } 294 | 295 | return it.value(); 296 | } 297 | 298 | return QIcon(); 299 | } 300 | 301 | if (role == Qt::TextAlignmentRole) 302 | { 303 | if (index.column() == 1) 304 | { 305 | return Qt::AlignRight + Qt::AlignVCenter; 306 | } 307 | return QVariant(); 308 | } 309 | 310 | if (role == Qt::DisplayRole) 311 | { 312 | switch (index.column()) 313 | { 314 | case 0: 315 | return item->name; 316 | case 1: 317 | if (item->isFolder || item->state == Item::Special) 318 | { 319 | return QString(); 320 | } 321 | else 322 | { 323 | return getNiceSize(item->size); 324 | } 325 | case 2: 326 | return item->modified; 327 | } 328 | Q_ASSERT(false); 329 | } 330 | return QVariant(); 331 | } 332 | 333 | QVariant ItemModel::headerData(int section, Qt::Orientation orientation, int role) const 334 | { 335 | if (orientation == Qt::Horizontal && role == Qt::DisplayRole) 336 | { 337 | switch (section) 338 | { 339 | case 0: 340 | return "Name"; 341 | case 1: 342 | return "Size"; 343 | case 2: 344 | return "Modified"; 345 | } 346 | } 347 | 348 | return QVariant(); 349 | } 350 | 351 | bool ItemModel::removeRows(int row, int count, const QModelIndex& parent) 352 | { 353 | if (!hasIndex(row, 0, parent)) 354 | { 355 | return false; 356 | } 357 | 358 | Item* item = get(parent); 359 | if (row + count > item->childs.count()) 360 | { 361 | return false; 362 | } 363 | 364 | emit beginRemoveRows(parent, row, row + count - 1); 365 | 366 | for (int i=row; ichilds.at(i); 369 | if (node->isLoading() || node->state == Item::LoadingIcon) 370 | { 371 | node->isDeleted = true; 372 | } 373 | else 374 | { 375 | delete node; 376 | } 377 | } 378 | item->childs.remove(row, count); 379 | 380 | emit endRemoveRows(); 381 | 382 | return true; 383 | } 384 | 385 | Qt::ItemFlags ItemModel::flags(const QModelIndex& index) const 386 | { 387 | Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); 388 | 389 | if (!index.isValid()) 390 | { 391 | return defaultFlags; 392 | } 393 | 394 | return Qt::ItemIsDropEnabled | defaultFlags; 395 | } 396 | 397 | bool ItemModel::canDropMimeData(const QMimeData* data, Qt::DropAction action, 398 | int row, int column, const QModelIndex& parent) const 399 | { 400 | Q_UNUSED(row); 401 | Q_UNUSED(column); 402 | Q_UNUSED(parent); 403 | 404 | if (action != Qt::CopyAction && action != Qt::MoveAction) 405 | { 406 | return false; 407 | } 408 | 409 | if (!data->hasUrls()) 410 | { 411 | return false; 412 | } 413 | 414 | auto urls = data->urls(); 415 | if (urls.count() == 1) 416 | { 417 | return urls.front().isLocalFile(); 418 | } 419 | 420 | return false; 421 | } 422 | 423 | bool ItemModel::dropMimeData(const QMimeData* data, Qt::DropAction action, 424 | int row, int column, const QModelIndex& parent) 425 | { 426 | if (!canDropMimeData(data, action, row, column, parent)) 427 | { 428 | return false; 429 | } 430 | 431 | QDir path = QDir(data->urls().front().toLocalFile()); 432 | Item* item = get(parent); 433 | 434 | emit drop(path, item->isFolder ? parent : parent.parent()); 435 | 436 | return false; 437 | } 438 | 439 | Item* ItemModel::get(const QModelIndex& index) const 440 | { 441 | return index.isValid() ? static_cast(index.internalPointer()) : mRoot; 442 | } 443 | 444 | void ItemModel::load(const QPersistentModelIndex& parentIndex, Item* parent) 445 | { 446 | auto lsd = new QProcess(this); 447 | auto lsl = new QProcess(this); 448 | 449 | auto cache = new QVector(); 450 | 451 | Item* loading = new Item(); 452 | loading->state = Item::Special; 453 | loading->name = "... loading [-]"; 454 | loading->parent = parent; 455 | 456 | QTimer* timer = new QTimer(this); 457 | 458 | QObject::connect(timer, &QTimer::timeout, this, [=]() 459 | { 460 | advanceSpinner(loading->name); 461 | auto loadingIndex = createIndex(loading->num(), 0, loading); 462 | emit dataChanged(loadingIndex, loadingIndex, QVector{Qt::DisplayRole}); 463 | }); 464 | 465 | auto rcloneFinished = [=]() 466 | { 467 | sender()->deleteLater(); 468 | 469 | parent->state = parent->state == Item::Loading1 ? Item::Loading2 : Item::Ready; 470 | if (parent->state != Item::Ready) 471 | { 472 | return; 473 | } 474 | 475 | timer->stop(); 476 | timer->deleteLater(); 477 | 478 | if (parent->isDeleted) 479 | { 480 | qDeleteAll(*cache); 481 | delete cache; 482 | delete parent; 483 | return; 484 | } 485 | 486 | QHash existing; 487 | for (int i=0; ichilds.count(); i++) 488 | { 489 | if (parent->childs[i] != loading) 490 | { 491 | existing.insert(parent->childs[i]->name, i); 492 | } 493 | } 494 | 495 | QVector todo; 496 | 497 | bool modified = false; 498 | for (auto& item : *cache) 499 | { 500 | auto it = existing.find(item->name); 501 | if (it == existing.end()) 502 | { 503 | item->path = parent->path.filePath(item->name); 504 | if (!item->isFolder && mFileIcons) 505 | { 506 | QString ext = QFileInfo(item->name).suffix(); 507 | if (!mLoadedIcons.contains(ext)) 508 | { 509 | item->state = Item::LoadingIcon; 510 | emit getIcon(item, parentIndex); 511 | } 512 | } 513 | todo.append(item); 514 | item = nullptr; 515 | } 516 | else 517 | { 518 | Item* old = parent->childs[it.value()]; 519 | if (old->isFolder != item->isFolder 520 | || old->modified != item->modified 521 | || old->size != item->size) 522 | { 523 | old->state = Item::Unknown; 524 | old->isFolder = item->isFolder; 525 | old->modified = item->modified; 526 | old->size = item->size; 527 | modified = true; 528 | emit dataChanged(createIndex(it.value(), 0, parent), 529 | createIndex(it.value(), 2, parent), 530 | QVector{Qt::DisplayRole}); 531 | } 532 | existing.erase(it); 533 | } 534 | } 535 | 536 | qDeleteAll(*cache); 537 | delete cache; 538 | 539 | for (int i=0; ichilds.count(); i++) 540 | { 541 | if (parent->childs[i] == loading || existing.contains(parent->childs[i]->name)) 542 | { 543 | emit beginRemoveRows(parentIndex, i, i); 544 | delete parent->childs.takeAt(i); 545 | emit endRemoveRows(); 546 | i--; 547 | } 548 | } 549 | 550 | if (!todo.isEmpty()) 551 | { 552 | modified = true; 553 | emit beginInsertRows(parentIndex, parent->childs.count(), parent->childs.count() + todo.count() - 1); 554 | parent->childs += todo; 555 | emit endInsertRows(); 556 | } 557 | 558 | if (modified) 559 | { 560 | sort(parentIndex, parent); 561 | } 562 | }; 563 | 564 | QObject::connect(lsd, static_cast(&QProcess::finished), this, rcloneFinished); 565 | QObject::connect(lsl, static_cast(&QProcess::finished), this, rcloneFinished); 566 | 567 | QObject::connect(lsd, &QProcess::readyRead, this, [=]() 568 | { 569 | while (lsd->canReadLine()) 570 | { 571 | if (mRegExpFolder.exactMatch(lsd->readLine().trimmed())) 572 | { 573 | QStringList cap = mRegExpFolder.capturedTexts(); 574 | 575 | Item* child = new Item(); 576 | child->isFolder = true; 577 | child->parent = parent; 578 | child->name = cap[2]; 579 | child->modified = cap[1]; 580 | 581 | cache->append(child); 582 | } 583 | } 584 | }); 585 | 586 | QObject::connect(lsl, &QProcess::readyRead, this, [=]() 587 | { 588 | while (lsl->canReadLine()) 589 | { 590 | if (mRegExpFile.exactMatch(lsl->readLine().trimmed())) 591 | { 592 | QStringList cap = mRegExpFile.capturedTexts(); 593 | 594 | Item* child = new Item(); 595 | child->parent = parent; 596 | child->name = cap[3]; 597 | child->modified = cap[2]; 598 | child->size = cap[1].toULongLong(); 599 | 600 | cache->append(child); 601 | } 602 | } 603 | }); 604 | 605 | parent->state = Item::Loading1; 606 | 607 | emit beginInsertRows(parentIndex, 0, 0); 608 | parent->childs.prepend(loading); 609 | emit endInsertRows(); 610 | 611 | timer->start(100); 612 | UseRclonePassword(lsd); 613 | UseRclonePassword(lsl); 614 | lsd->start(GetRclone(), QStringList() << "lsd" << GetRcloneConf() << mRemote + ":" + parent->path.path(), QIODevice::ReadOnly); 615 | lsl->start(GetRclone(), QStringList() << "lsl" << GetRcloneConf() << "--max-depth" << "1" << mRemote + ":" + parent->path.path(), QIODevice::ReadOnly); 616 | } 617 | 618 | void ItemModel::sortRecursive(Item* item, const ItemSorter& sorter) 619 | { 620 | qSort(item->childs.begin(), item->childs.end(), sorter); 621 | 622 | for (auto child : item->childs) 623 | { 624 | sortRecursive(child, sorter); 625 | } 626 | } 627 | 628 | void ItemModel::sort(const QModelIndex& parent, Item* item) 629 | { 630 | if (item->childs.isEmpty()) 631 | { 632 | return; 633 | } 634 | 635 | QList parents; 636 | parents << parent; 637 | emit layoutAboutToBeChanged(parents, QAbstractItemModel::VerticalSortHint); 638 | 639 | QModelIndexList oldList = persistentIndexList(); 640 | QVector > oldNodes; 641 | oldNodes.reserve(oldList.count()); 642 | for (const auto& index : oldList) 643 | { 644 | oldNodes.append(qMakePair(get(index), index.column())); 645 | } 646 | 647 | ItemSorter sorter(mSortColumn, mSortOrder); 648 | sortRecursive(item, sorter); 649 | 650 | QModelIndexList newList; 651 | newList.reserve(oldNodes.size()); 652 | for (const auto& node : oldNodes) 653 | { 654 | Item* child = node.first; 655 | int column = node.second; 656 | int row = child->num(); 657 | newList.append(createIndex(row, column, child)); 658 | } 659 | 660 | changePersistentIndexList(oldList, newList); 661 | 662 | emit layoutChanged(parents, QAbstractItemModel::VerticalSortHint); 663 | } 664 | -------------------------------------------------------------------------------- /src/item_model.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | 5 | struct Item 6 | { 7 | Item() 8 | { 9 | } 10 | 11 | ~Item() 12 | { 13 | for (auto child : childs) 14 | { 15 | if (child->isLoading() || state == LoadingIcon) 16 | { 17 | child->isDeleted = true; 18 | } 19 | else 20 | { 21 | delete child; 22 | } 23 | } 24 | } 25 | 26 | bool isLoading() const 27 | { 28 | return state == Loading1 || state == Loading2; 29 | } 30 | 31 | int num() const 32 | { 33 | Q_ASSERT(parent); 34 | return parent->childs.indexOf(const_cast(this)); 35 | } 36 | 37 | Item* parent = nullptr; 38 | 39 | enum State { Unknown, Loading1, Loading2, Ready, Special, LoadingIcon }; 40 | 41 | State state = Unknown; 42 | bool isFolder = false; 43 | bool isDeleted = false; 44 | QString name; 45 | QDir path; 46 | QString modified; 47 | quint64 size = 0; 48 | 49 | QVector childs; 50 | }; 51 | 52 | class IconCache; 53 | class ItemSorter; 54 | 55 | class ItemModel : public QAbstractItemModel 56 | { 57 | Q_OBJECT 58 | public: 59 | ItemModel(IconCache* icons, const QString& remote, QObject* parent); 60 | ~ItemModel(); 61 | 62 | const QDir& path(const QModelIndex& index) const; 63 | bool isLoading(const QModelIndex& index) const; 64 | void refresh(const QModelIndex& index); 65 | void rename(const QModelIndex& index, const QString& name); 66 | bool isTopLevel(const QModelIndex& index) const; 67 | bool isFolder(const QModelIndex& index) const; 68 | 69 | QModelIndex addRoot(const QString& name, const QString& path); 70 | 71 | QModelIndex index(int row, int column, const QModelIndex& parent) const override; 72 | QModelIndex parent(const QModelIndex& index) const override; 73 | bool hasChildren(const QModelIndex& parent) const override; 74 | int rowCount(const QModelIndex& parent) const override; 75 | int columnCount(const QModelIndex& parent) const override; 76 | void sort(int column, Qt::SortOrder order) override; 77 | QVariant data(const QModelIndex& index, int role) const override; 78 | QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 79 | 80 | bool removeRows(int row, int count, const QModelIndex& parent) override; 81 | 82 | Qt::ItemFlags flags(const QModelIndex& index) const override; 83 | 84 | bool canDropMimeData(const QMimeData* data, Qt::DropAction action, 85 | int row, int column, const QModelIndex& parent) const override; 86 | bool dropMimeData(const QMimeData* data, Qt::DropAction action, 87 | int row, int column, const QModelIndex& parent) override; 88 | 89 | signals: 90 | void getIcon(Item* item, const QPersistentModelIndex& index); 91 | void drop(const QDir& path, const QModelIndex& parent); 92 | 93 | private: 94 | Item* mRoot; 95 | 96 | QString mRemote; 97 | 98 | QHash mLoadedIcons; 99 | 100 | bool mFolderIcons; 101 | bool mFileIcons; 102 | 103 | QIcon mDriveIcon; 104 | QIcon mFolderIcon; 105 | QIcon mFileIcon; 106 | 107 | QFont mFixedFont; 108 | 109 | int mSortColumn; 110 | Qt::SortOrder mSortOrder; 111 | 112 | QRegExp mRegExpFolder; 113 | QRegExp mRegExpFile; 114 | 115 | Item* get(const QModelIndex& index) const; 116 | void load(const QPersistentModelIndex& parentIndex, Item* parent); 117 | 118 | void sortRecursive(Item* item, const ItemSorter& sorter); 119 | void sort(const QModelIndex& parent, Item* item); 120 | }; 121 | -------------------------------------------------------------------------------- /src/job_widget.cpp: -------------------------------------------------------------------------------- 1 | #include "job_widget.h" 2 | #include "utils.h" 3 | 4 | JobWidget::JobWidget(QProcess* process, const QString& info, const QStringList& args, const QString& source, const QString& dest, QWidget* parent) 5 | : QWidget(parent) 6 | , mProcess(process) 7 | { 8 | ui.setupUi(this); 9 | 10 | mArgs.append(QDir::toNativeSeparators(GetRclone())); 11 | mArgs.append(GetRcloneConf()); 12 | mArgs.append(args); 13 | 14 | ui.source->setText(source); 15 | ui.dest->setText(dest); 16 | ui.info->setText(info); 17 | 18 | ui.details->setVisible(false); 19 | 20 | ui.output->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); 21 | ui.output->setVisible(false); 22 | 23 | QObject::connect(ui.showDetails, &QToolButton::toggled, this, [=](bool checked) 24 | { 25 | ui.details->setVisible(checked); 26 | ui.showDetails->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 27 | }); 28 | 29 | QObject::connect(ui.showOutput, &QToolButton::toggled, this, [=](bool checked) 30 | { 31 | ui.output->setVisible(checked); 32 | ui.showOutput->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 33 | }); 34 | 35 | ui.cancel->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogCloseButton)); 36 | 37 | QObject::connect(ui.cancel, &QToolButton::clicked, this, [=]() 38 | { 39 | if (mRunning) 40 | { 41 | int button = QMessageBox::question( 42 | this, 43 | "Transfer", 44 | QString("rclone process is still running. Do you want to cancel it?"), 45 | QMessageBox::Yes | QMessageBox::No); 46 | if (button == QMessageBox::Yes) 47 | { 48 | cancel(); 49 | } 50 | } 51 | else 52 | { 53 | emit closed(); 54 | } 55 | }); 56 | 57 | ui.copy->setIcon(QApplication::style()->standardIcon(QStyle::SP_FileLinkIcon)); 58 | 59 | QObject::connect(ui.copy, &QToolButton::clicked, this, [=]() 60 | { 61 | QClipboard* clipboard = QGuiApplication::clipboard(); 62 | clipboard->setText(mArgs.join(" ")); 63 | }); 64 | 65 | QObject::connect(mProcess, &QProcess::readyRead, this, [=]() 66 | { 67 | QRegExp rxSize(R"(^Transferred:\s+(\S+ \S+) \(([^)]+)\)$)"); 68 | QRegExp rxErrors(R"(^Errors:\s+(\S+)$)"); 69 | QRegExp rxChecks(R"(^Checks:\s+(\S+)$)"); 70 | QRegExp rxTransferred(R"(^Transferred:\s+(\S+)$)"); 71 | QRegExp rxTime(R"(^Elapsed time:\s+(\S+)$)"); 72 | QRegExp rxProgress(R"(^\*([^:]+):\s*([^%]+)% done.+(ETA: [^)]+)$)"); 73 | 74 | while (mProcess->canReadLine()) 75 | { 76 | QString line = mProcess->readLine().trimmed(); 77 | if (++mLines == 10000) 78 | { 79 | ui.output->clear(); 80 | mLines = 1; 81 | } 82 | ui.output->appendPlainText(line); 83 | 84 | if (line.isEmpty()) 85 | { 86 | for (auto it = mActive.begin(), eit = mActive.end(); it != eit; /* empty */) 87 | { 88 | auto label = it.value(); 89 | if (mUpdated.contains(label)) 90 | { 91 | ++it; 92 | } 93 | else 94 | { 95 | it = mActive.erase(it); 96 | ui.progress->removeWidget(label->buddy()); 97 | ui.progress->removeWidget(label); 98 | delete label->buddy(); 99 | delete label; 100 | } 101 | } 102 | mUpdated.clear(); 103 | continue; 104 | } 105 | 106 | if (rxSize.exactMatch(line)) 107 | { 108 | ui.size->setText(rxSize.cap(1)); 109 | ui.bandwidth->setText(rxSize.cap(2)); 110 | } 111 | else if (rxErrors.exactMatch(line)) 112 | { 113 | ui.errors->setText(rxErrors.cap(1)); 114 | } 115 | else if (rxChecks.exactMatch(line)) 116 | { 117 | ui.checks->setText(rxChecks.cap(1)); 118 | } 119 | else if (rxTransferred.exactMatch(line)) 120 | { 121 | ui.transferred->setText(rxTransferred.cap(1)); 122 | } 123 | else if (rxTime.exactMatch(line)) 124 | { 125 | ui.elapsed->setText(rxTime.cap(1)); 126 | } 127 | else if (rxProgress.exactMatch(line)) 128 | { 129 | QString name = rxProgress.cap(1).trimmed(); 130 | 131 | auto it = mActive.find(name); 132 | 133 | QLabel* label; 134 | QProgressBar* bar; 135 | if (it == mActive.end()) 136 | { 137 | label = new QLabel(); 138 | label->setText(name); 139 | 140 | bar = new QProgressBar(); 141 | bar->setMinimum(0); 142 | bar->setMaximum(100); 143 | bar->setTextVisible(true); 144 | 145 | label->setBuddy(bar); 146 | 147 | ui.progress->addRow(label, bar); 148 | 149 | mActive.insert(name, label); 150 | } 151 | else 152 | { 153 | label = it.value(); 154 | bar = static_cast(label->buddy()); 155 | } 156 | 157 | bar->setValue(rxProgress.cap(2).toInt()); 158 | bar->setToolTip(rxProgress.cap(3)); 159 | 160 | mUpdated.insert(label); 161 | } 162 | } 163 | }); 164 | 165 | QObject::connect(mProcess, static_cast(&QProcess::finished), this, [=](int status) 166 | { 167 | mProcess->deleteLater(); 168 | for (auto label : mActive) 169 | { 170 | ui.progress->removeWidget(label->buddy()); 171 | ui.progress->removeWidget(label); 172 | delete label->buddy(); 173 | delete label; 174 | } 175 | 176 | mRunning = false; 177 | if (status == 0) 178 | { 179 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: black; }"); 180 | ui.showDetails->setText("Finished"); 181 | } 182 | else 183 | { 184 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: red; }"); 185 | ui.showDetails->setText("Error"); 186 | } 187 | 188 | ui.cancel->setToolTip("Close"); 189 | 190 | emit finished(ui.info->text()); 191 | }); 192 | 193 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: green; }"); 194 | ui.showDetails->setText("Running"); 195 | } 196 | 197 | JobWidget::~JobWidget() 198 | { 199 | } 200 | 201 | void JobWidget::showDetails() 202 | { 203 | ui.showDetails->setChecked(true); 204 | } 205 | 206 | void JobWidget::cancel() 207 | { 208 | if (!mRunning) 209 | { 210 | return; 211 | } 212 | 213 | mProcess->kill(); 214 | mProcess->waitForFinished(); 215 | 216 | emit closed(); 217 | } 218 | -------------------------------------------------------------------------------- /src/job_widget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | #include "ui_job_widget.h" 5 | 6 | class JobWidget : public QWidget 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | JobWidget(QProcess* process, const QString& info, const QStringList& args, const QString& source, const QString& dest, QWidget* parent = nullptr); 12 | ~JobWidget(); 13 | 14 | void showDetails(); 15 | 16 | public slots: 17 | void cancel(); 18 | 19 | signals: 20 | void finished(const QString& info); 21 | void closed(); 22 | 23 | private: 24 | Ui::JobWidget ui; 25 | 26 | bool mRunning = true; 27 | QProcess* mProcess; 28 | int mLines = 0; 29 | 30 | QStringList mArgs; 31 | QHash mActive; 32 | QSet mUpdated; 33 | }; 34 | -------------------------------------------------------------------------------- /src/job_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | JobWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 832 10 | 449 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | QToolButton { border: 0 } 39 | 40 | 41 | true 42 | 43 | 44 | Qt::ToolButtonTextBesideIcon 45 | 46 | 47 | Qt::RightArrow 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Copy command to clipboard 58 | 59 | 60 | QToolButton { border: 0 } 61 | 62 | 63 | 64 | 65 | 66 | 67 | Cancel 68 | 69 | 70 | QToolButton { border: 0 } 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 0 82 | 83 | 84 | 0 85 | 86 | 87 | 0 88 | 89 | 90 | 91 | 92 | Qt::Horizontal 93 | 94 | 95 | 96 | 40 97 | 20 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 0 107 | 0 108 | 109 | 110 | 111 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 112 | 113 | 114 | true 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 0 123 | 0 124 | 125 | 126 | 127 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 128 | 129 | 130 | true 131 | 132 | 133 | 134 | 135 | 136 | 137 | Errors: 138 | 139 | 140 | errors 141 | 142 | 143 | 144 | 145 | 146 | 147 | Checks: 148 | 149 | 150 | checks 151 | 152 | 153 | 154 | 155 | 156 | 157 | Bandwidth: 158 | 159 | 160 | bandwidth 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 0 169 | 0 170 | 171 | 172 | 173 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 174 | 175 | 176 | true 177 | 178 | 179 | 180 | 181 | 182 | 183 | true 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 0 192 | 0 193 | 194 | 195 | 196 | Size: 197 | 198 | 199 | size 200 | 201 | 202 | 203 | 204 | 205 | 206 | true 207 | 208 | 209 | 210 | 211 | 212 | 213 | QToolButton { border: 0 } 214 | 215 | 216 | Show Output 217 | 218 | 219 | true 220 | 221 | 222 | Qt::ToolButtonTextBesideIcon 223 | 224 | 225 | Qt::RightArrow 226 | 227 | 228 | 229 | 230 | 231 | 232 | Elapsed time: 233 | 234 | 235 | elapsed 236 | 237 | 238 | 239 | 240 | 241 | 242 | Transferred: 243 | 244 | 245 | transferred 246 | 247 | 248 | 249 | 250 | 251 | 252 | QPlainTextEdit::NoWrap 253 | 254 | 255 | true 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 0 264 | 0 265 | 266 | 267 | 268 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 269 | 270 | 271 | true 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 0 280 | 0 281 | 282 | 283 | 284 | Source: 285 | 286 | 287 | source 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 0 296 | 0 297 | 298 | 299 | 300 | Destination: 301 | 302 | 303 | dest 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 0 312 | 0 313 | 314 | 315 | 316 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 317 | 318 | 319 | true 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 0 328 | 0 329 | 330 | 331 | 332 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 333 | 334 | 335 | true 336 | 337 | 338 | 339 | 340 | 341 | 342 | QFormLayout::ExpandingFieldsGrow 343 | 344 | 345 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | showDetails 356 | cancel 357 | source 358 | dest 359 | size 360 | bandwidth 361 | elapsed 362 | showOutput 363 | output 364 | 365 | 366 | 367 | 368 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "main_window.h" 2 | 3 | int main(int argc, char* argv[]) 4 | { 5 | QApplication app(argc, argv); 6 | 7 | app.setApplicationDisplayName("Rclone Browser"); 8 | app.setApplicationName("rclone-browser"); 9 | app.setOrganizationName("rclone-browser"); 10 | app.setWindowIcon(QIcon(":/icons/icon.png")); 11 | 12 | MainWindow w; 13 | w.show(); 14 | 15 | return app.exec(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main_window.cpp: -------------------------------------------------------------------------------- 1 | #include "main_window.h" 2 | #include "remote_widget.h" 3 | #include "utils.h" 4 | #include "job_widget.h" 5 | #include "mount_widget.h" 6 | #include "stream_widget.h" 7 | #include "preferences_dialog.h" 8 | #ifdef Q_OS_OSX 9 | #include "osx_helper.h" 10 | #endif 11 | 12 | MainWindow::MainWindow() 13 | { 14 | ui.setupUi(this); 15 | 16 | mSystemTray.setIcon(qApp->windowIcon()); 17 | { 18 | auto settings = GetSettings(); 19 | if (settings->contains("MainWindow/geometry")) 20 | { 21 | restoreGeometry(settings->value("MainWindow/geometry").toByteArray()); 22 | } 23 | SetRclone(settings->value("Settings/rclone").toString()); 24 | SetRcloneConf(settings->value("Settings/rcloneConf").toString()); 25 | 26 | mAlwaysShowInTray = settings->value("Settings/alwaysShowInTray", false).toBool(); 27 | mCloseToTray = settings->value("Settings/closeToTray", false).toBool(); 28 | mNotifyFinishedTransfers = settings->value("Settings/notifyFinishedTransfers", true).toBool(); 29 | 30 | mSystemTray.setVisible(mAlwaysShowInTray); 31 | } 32 | 33 | QObject::connect(ui.preferences, &QAction::triggered, this, [=]() 34 | { 35 | PreferencesDialog dialog(this); 36 | if (dialog.exec() == QDialog::Accepted) 37 | { 38 | auto settings = GetSettings(); 39 | settings->setValue("Settings/rclone", dialog.getRclone().trimmed()); 40 | settings->setValue("Settings/rcloneConf", dialog.getRcloneConf().trimmed()); 41 | settings->setValue("Settings/stream", dialog.getStream()); 42 | #ifndef Q_OS_WIN32 43 | settings->setValue("Settings/mount", dialog.getMount()); 44 | #endif 45 | settings->setValue("Settings/alwaysShowInTray", dialog.getAlwaysShowInTray()); 46 | settings->setValue("Settings/closeToTray", dialog.getCloseToTray()); 47 | settings->setValue("Settings/notifyFinishedTransfers", dialog.getNotifyFinishedTransfers()); 48 | settings->setValue("Settings/showFolderIcons", dialog.getShowFolderIcons()); 49 | settings->setValue("Settings/showFileIcons", dialog.getShowFileIcons()); 50 | settings->setValue("Settings/rowColors", dialog.getRowColors()); 51 | SetRclone(dialog.getRclone()); 52 | SetRcloneConf(dialog.getRcloneConf()); 53 | mFirstTime = true; 54 | rcloneGetVersion(); 55 | 56 | mAlwaysShowInTray = dialog.getAlwaysShowInTray(); 57 | mCloseToTray = dialog.getCloseToTray(); 58 | mNotifyFinishedTransfers = dialog.getNotifyFinishedTransfers(); 59 | 60 | mSystemTray.setVisible(mAlwaysShowInTray); 61 | } 62 | }); 63 | 64 | QObject::connect(ui.quit, &QAction::triggered, this, [=]() 65 | { 66 | mCloseToTray = false; 67 | close(); 68 | }); 69 | 70 | QObject::connect(ui.about, &QAction::triggered, this, [=]() 71 | { 72 | QMessageBox::about( 73 | this, 74 | "Rclone Browser", 75 | QString( 76 | R"(

GUI for rclone, )" RCLONE_BROWSER_VERSION "

" 77 | R"(

Copyright © 2017 Martins Mozeiko

)" 78 | R"(

E-mail: martins.mozeiko@gmail.com

)" 79 | R"(

Web: https://mmozeiko.github.io/RcloneBrowser

)" 80 | ) 81 | ); 82 | }); 83 | QObject::connect(ui.aboutQt, &QAction::triggered, qApp, &QApplication::aboutQt); 84 | 85 | QObject::connect(ui.remotes, &QListWidget::currentItemChanged, this, [=](QListWidgetItem* current) 86 | { 87 | ui.open->setEnabled(current != NULL); 88 | }); 89 | QObject::connect(ui.remotes, &QListWidget::itemActivated, ui.open, &QPushButton::clicked); 90 | 91 | QObject::connect(ui.config, &QPushButton::clicked, this, &MainWindow::rcloneConfig); 92 | QObject::connect(ui.refresh, &QPushButton::clicked, this, &MainWindow::rcloneListRemotes); 93 | 94 | QObject::connect(ui.open, &QPushButton::clicked, this, [=]() 95 | { 96 | auto item = ui.remotes->selectedItems().front(); 97 | QString type = item->data(Qt::UserRole).toString(); 98 | QString name = item->text(); 99 | bool isLocal = type == "local"; 100 | 101 | auto remote = new RemoteWidget(&mIcons, name, isLocal, ui.tabs); 102 | QObject::connect(remote, &RemoteWidget::addMount, this, &MainWindow::addMount); 103 | QObject::connect(remote, &RemoteWidget::addStream, this, &MainWindow::addStream); 104 | QObject::connect(remote, &RemoteWidget::addTransfer, this, &MainWindow::addTransfer); 105 | 106 | int index = ui.tabs->addTab(remote, name); 107 | ui.tabs->setCurrentIndex(index); 108 | }); 109 | 110 | QObject::connect(ui.tabs, &QTabWidget::tabCloseRequested, ui.tabs, &QTabWidget::removeTab); 111 | 112 | ui.tabs->tabBar()->setTabButton(0, QTabBar::RightSide, nullptr); 113 | ui.tabs->tabBar()->setTabButton(0, QTabBar::LeftSide, nullptr); 114 | ui.tabs->tabBar()->setTabButton(1, QTabBar::RightSide, nullptr); 115 | ui.tabs->tabBar()->setTabButton(1, QTabBar::LeftSide, nullptr); 116 | ui.tabs->setCurrentIndex(0); 117 | 118 | QObject::connect(&mSystemTray, &QSystemTrayIcon::activated, this, [=](QSystemTrayIcon::ActivationReason reason) 119 | { 120 | if (reason == QSystemTrayIcon::DoubleClick || reason == QSystemTrayIcon::Trigger) 121 | { 122 | showNormal(); 123 | mSystemTray.setVisible(mAlwaysShowInTray); 124 | #ifdef Q_OS_OSX 125 | osxShowDockIcon(); 126 | #endif 127 | } 128 | }); 129 | 130 | QObject::connect(&mSystemTray, &QSystemTrayIcon::messageClicked, this, [=]() 131 | { 132 | showNormal(); 133 | mSystemTray.setVisible(mAlwaysShowInTray); 134 | #ifdef Q_OS_OSX 135 | osxShowDockIcon(); 136 | #endif 137 | 138 | ui.tabs->setCurrentIndex(1); 139 | if (mLastFinished) 140 | { 141 | mLastFinished->showDetails(); 142 | ui.jobsArea->ensureWidgetVisible(mLastFinished); 143 | } 144 | }); 145 | 146 | QMenu* trayMenu = new QMenu(this); 147 | QObject::connect(trayMenu->addAction("&Show"), &QAction::triggered, this, [=]() 148 | { 149 | showNormal(); 150 | mSystemTray.setVisible(mAlwaysShowInTray); 151 | #ifdef Q_OS_OSX 152 | osxShowDockIcon(); 153 | #endif 154 | }); 155 | QObject::connect(trayMenu->addAction("&Quit"), &QAction::triggered, this, &QWidget::close); 156 | mSystemTray.setContextMenu(trayMenu); 157 | 158 | mStatusMessage = new QLabel(); 159 | ui.statusBar->addWidget(mStatusMessage); 160 | ui.statusBar->setStyleSheet("QStatusBar::item { border: 0; }"); 161 | 162 | QTimer::singleShot(0, ui.remotes, SLOT(setFocus())); 163 | 164 | QString rclone = GetRclone(); 165 | if (rclone.isEmpty()) 166 | { 167 | rclone = QStandardPaths::findExecutable("rclone"); 168 | auto settings = GetSettings(); 169 | settings->setValue("Settings/rclone", rclone); 170 | SetRclone(rclone); 171 | } 172 | if (rclone.isEmpty()) 173 | { 174 | QMessageBox::information(this, "Error", "Cannot check rclone verison!\nPlease verify rclone location."); 175 | emit ui.preferences->trigger(); 176 | } 177 | else 178 | { 179 | rcloneGetVersion(); 180 | } 181 | } 182 | 183 | MainWindow::~MainWindow() 184 | { 185 | auto settings = GetSettings(); 186 | settings->setValue("MainWindow/geometry", saveGeometry()); 187 | } 188 | 189 | void MainWindow::rcloneGetVersion() 190 | { 191 | bool firstTime = mFirstTime; 192 | mFirstTime = false; 193 | 194 | QProcess* p = new QProcess(); 195 | 196 | QObject::connect(p, static_cast(&QProcess::finished), this, [=](int code) 197 | { 198 | if (code == 0) 199 | { 200 | QString version = p->readAllStandardOutput().trimmed(); 201 | mStatusMessage->setText(version + " in " + QDir::toNativeSeparators(GetRclone())); 202 | rcloneListRemotes(); 203 | } 204 | else 205 | { 206 | if (p->error() != QProcess::FailedToStart) 207 | { 208 | if (getConfigPassword(p)) 209 | { 210 | rcloneGetVersion(); 211 | } 212 | else 213 | { 214 | close(); 215 | } 216 | p->deleteLater(); 217 | return; 218 | } 219 | 220 | if (firstTime) 221 | { 222 | if (p->error() == QProcess::FailedToStart) 223 | { 224 | QMessageBox::information(this, "Error", "Wrong rclone executable or rclone not found!\nPlease select its location in next dialog."); 225 | } 226 | else 227 | { 228 | QMessageBox::information(this, "Error", "Cannot check rclone version!\nPlease verify rclone location."); 229 | } 230 | emit ui.preferences->trigger(); 231 | } 232 | } 233 | p->deleteLater(); 234 | }); 235 | 236 | UseRclonePassword(p); 237 | p->start(GetRclone(), QStringList() << "version" << "--ask-password=false", QIODevice::ReadOnly); 238 | } 239 | 240 | void MainWindow::rcloneConfig() 241 | { 242 | #if defined(Q_OS_WIN32) && (QT_VERSION < QT_VERSION_CHECK(5, 7, 0)) 243 | QProcess::startDetached(GetRclone(), QStringList() << "config" << GetRcloneConf()); 244 | return; 245 | #else 246 | 247 | QProcess* p = new QProcess(this); 248 | 249 | QObject::connect(p, static_cast(&QProcess::finished), this, [=](int code) 250 | { 251 | if (code == 0) 252 | { 253 | emit rcloneListRemotes(); 254 | } 255 | p->deleteLater(); 256 | }); 257 | #endif 258 | 259 | #if defined(Q_OS_WIN32) 260 | 261 | #if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) 262 | p->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments* args) 263 | { 264 | args->flags |= CREATE_NEW_CONSOLE; 265 | args->startupInfo->dwFlags &= ~STARTF_USESTDHANDLES; 266 | }); 267 | p->setProgram(GetRclone()); 268 | p->setArguments(QStringList() << "config" << GetRcloneConf()); 269 | #endif 270 | 271 | #elif defined(Q_OS_OSX) 272 | auto tmp = new QFile("/tmp/rclone_config.command"); 273 | tmp->open(QIODevice::WriteOnly); 274 | QTextStream(tmp) << "#!/bin/sh\n" << GetRclone() << " config" << GetRcloneConf().join(" ") << "\n"; 275 | tmp->close(); 276 | tmp->setPermissions( 277 | QFileDevice::ReadUser | QFileDevice::WriteUser | QFileDevice::ExeUser | 278 | QFileDevice::ReadGroup | QFileDevice::ExeGroup | 279 | QFileDevice::ReadOther | QFileDevice::ExeOther); 280 | p->setProgram("open"); 281 | p->setArguments(QStringList() << tmp->fileName()); 282 | #else 283 | QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); 284 | QString terminal = env.value("TERMINAL"); 285 | if (terminal.isEmpty()) 286 | { 287 | terminal = QStandardPaths::findExecutable("x-terminal-emulator"); 288 | if (terminal.isEmpty()) 289 | { 290 | QMessageBox::critical(this, "Error", 291 | "Not sure how to launch terminal!\n" 292 | "Please set path to terminal executable in $TERMINAL environment variable.", QMessageBox::Ok); 293 | return; 294 | } 295 | p->setArguments(QStringList() << "-e" << GetRclone() << "config" << GetRcloneConf()); 296 | } 297 | else 298 | { 299 | p->setArguments(QStringList() << "-e" << (GetRclone() + " config " + GetRcloneConf().join(" "))); 300 | } 301 | 302 | p->setProgram(terminal); 303 | #endif 304 | 305 | #if !defined(Q_OS_WIN32) || (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) 306 | UseRclonePassword(p); 307 | p->start(QIODevice::NotOpen); 308 | #endif 309 | } 310 | 311 | void MainWindow::rcloneListRemotes() 312 | { 313 | ui.remotes->clear(); 314 | 315 | QProcess* p = new QProcess(); 316 | 317 | QObject::connect(p, static_cast(&QProcess::finished), this, [=](int code) 318 | { 319 | if (code == 0) 320 | { 321 | QStyle* style = qApp->style(); 322 | int size = 2 * style->pixelMetric(QStyle::PM_ListViewIconSize); 323 | ui.remotes->setIconSize(QSize(size, size)); 324 | 325 | QString bytes = p->readAllStandardOutput().trimmed(); 326 | QStringList items = bytes.split('\n'); 327 | for (const QString& line : items) 328 | { 329 | if (line.isEmpty()) 330 | { 331 | continue; 332 | } 333 | 334 | QStringList parts = line.split(':'); 335 | if (parts.count() != 2) 336 | { 337 | continue; 338 | } 339 | 340 | QString name = parts[0].trimmed(); 341 | QString type = parts[1].trimmed(); 342 | QString tooltip = type; 343 | 344 | QString path = ":/remotes/images/" + type.replace(' ', '_') + ".png"; 345 | QIcon icon(QFile(path).exists() ? path : ":/remotes/images/unknown.png"); 346 | 347 | QListWidgetItem* item = new QListWidgetItem(icon, name); 348 | item->setData(Qt::UserRole, type); 349 | item->setToolTip(tooltip); 350 | ui.remotes->addItem(item); 351 | } 352 | } 353 | else 354 | { 355 | if (p->error() != QProcess::FailedToStart) 356 | { 357 | if (getConfigPassword(p)) 358 | { 359 | rcloneListRemotes(); 360 | } 361 | } 362 | } 363 | p->deleteLater(); 364 | }); 365 | 366 | UseRclonePassword(p); 367 | p->start(GetRclone(), QStringList() << "listremotes" << GetRcloneConf() << "-l" << "--ask-password=false", QIODevice::ReadOnly); 368 | } 369 | 370 | bool MainWindow::getConfigPassword(QProcess* p) 371 | { 372 | QString output = p->readAllStandardError().trimmed(); 373 | if (output.indexOf("RCLONE_CONFIG_PASS") > 0) 374 | { 375 | bool ok; 376 | QString password = QInputDialog::getText( 377 | this, qApp->applicationDisplayName(), 378 | "Enter password for .rclone.conf configuration file:", 379 | QLineEdit::Password, QString(), &ok); 380 | if (ok) 381 | { 382 | SetRclonePassword(password); 383 | return true; 384 | } 385 | } 386 | else if (output.indexOf("unknown command \"listremotes\"") > 0) 387 | { 388 | QMessageBox::critical(this, qApp->applicationDisplayName(), "It seems rclone version you are using is too old.\nPlease upgrade to at least version 1.34!"); 389 | return false; 390 | } 391 | return false; 392 | } 393 | 394 | bool MainWindow::canClose() 395 | { 396 | if (mJobCount == 0) 397 | { 398 | return true; 399 | } 400 | 401 | bool wasVisible = isVisible(); 402 | 403 | ui.tabs->setCurrentIndex(1); 404 | showNormal(); 405 | 406 | int button = QMessageBox::question( 407 | this, 408 | "Rclone Browser", 409 | QString("There are %1 job(s) running.\n" 410 | "Do you want to stop them and quit?").arg(mJobCount), 411 | QMessageBox::Yes | QMessageBox::No); 412 | 413 | if (!wasVisible) 414 | { 415 | hide(); 416 | } 417 | 418 | if (button == QMessageBox::Yes) 419 | { 420 | for (int i=0; icount(); i++) 421 | { 422 | QWidget* widget = ui.jobs->itemAt(i)->widget(); 423 | if (auto mount = qobject_cast(widget)) 424 | { 425 | mount->cancel(); 426 | } 427 | else if (auto transfer = qobject_cast(widget)) 428 | { 429 | transfer->cancel(); 430 | } 431 | else if (auto stream = qobject_cast(widget)) 432 | { 433 | stream->cancel(); 434 | } 435 | } 436 | return true; 437 | } 438 | 439 | return false; 440 | } 441 | 442 | void MainWindow::closeEvent(QCloseEvent* ev) 443 | { 444 | if (mCloseToTray && isVisible()) 445 | { 446 | #ifdef Q_OS_OSX 447 | osxHideDockIcon(); 448 | #endif 449 | mSystemTray.show(); 450 | hide(); 451 | ev->ignore(); 452 | return; 453 | } 454 | 455 | if (canClose()) 456 | { 457 | ev->accept(); 458 | } 459 | else 460 | { 461 | ev->ignore(); 462 | } 463 | } 464 | 465 | void MainWindow::addTransfer(const QString& message, const QString& source, const QString& dest, const QStringList& args) 466 | { 467 | QProcess* transfer = new QProcess(this); 468 | transfer->setProcessChannelMode(QProcess::MergedChannels); 469 | 470 | auto widget = new JobWidget(transfer, message, args, source, dest); 471 | 472 | auto line = new QFrame(); 473 | line->setFrameShape(QFrame::HLine); 474 | line->setFrameShadow(QFrame::Sunken); 475 | 476 | QObject::connect(widget, &JobWidget::finished, this, [=](const QString& info) 477 | { 478 | if (mNotifyFinishedTransfers) 479 | { 480 | qApp->alert(this); 481 | mLastFinished = widget; 482 | mSystemTray.showMessage("Transfer finished", info); 483 | } 484 | 485 | if (--mJobCount == 0) 486 | { 487 | ui.tabs->setTabText(1, "Jobs"); 488 | } 489 | else 490 | { 491 | ui.tabs->setTabText(1, QString("Jobs (%1)").arg(mJobCount)); 492 | } 493 | }); 494 | 495 | QObject::connect(widget, &JobWidget::closed, this, [=]() 496 | { 497 | if (widget == mLastFinished) 498 | { 499 | mLastFinished = nullptr; 500 | } 501 | ui.jobs->removeWidget(widget); 502 | ui.jobs->removeWidget(line); 503 | widget->deleteLater(); 504 | delete line; 505 | if (ui.jobs->count() == 2) 506 | { 507 | ui.noJobsAvailable->show(); 508 | } 509 | }); 510 | 511 | if (ui.jobs->count() == 2) 512 | { 513 | ui.noJobsAvailable->hide(); 514 | } 515 | 516 | ui.jobs->insertWidget(0, widget); 517 | ui.jobs->insertWidget(1, line); 518 | ui.tabs->setTabText(1, QString("Jobs (%1)").arg(++mJobCount)); 519 | 520 | UseRclonePassword(transfer); 521 | transfer->start(GetRclone(), GetRcloneConf() + args, QIODevice::ReadOnly); 522 | } 523 | 524 | void MainWindow::addMount(const QString& remote, const QString& folder) 525 | { 526 | QProcess* mount = new QProcess(this); 527 | mount->setProcessChannelMode(QProcess::MergedChannels); 528 | 529 | auto widget = new MountWidget(mount, remote, folder); 530 | 531 | auto line = new QFrame(); 532 | line->setFrameShape(QFrame::HLine); 533 | line->setFrameShadow(QFrame::Sunken); 534 | 535 | QObject::connect(widget, &MountWidget::finished, this, [=]() 536 | { 537 | if (--mJobCount == 0) 538 | { 539 | ui.tabs->setTabText(1, "Jobs"); 540 | } 541 | else 542 | { 543 | ui.tabs->setTabText(1, QString("Jobs (%1)").arg(mJobCount)); 544 | } 545 | }); 546 | 547 | QObject::connect(widget, &MountWidget::closed, this, [=]() 548 | { 549 | ui.jobs->removeWidget(widget); 550 | ui.jobs->removeWidget(line); 551 | widget->deleteLater(); 552 | delete line; 553 | if (ui.jobs->count() == 2) 554 | { 555 | ui.noJobsAvailable->show(); 556 | } 557 | }); 558 | 559 | if (ui.jobs->count() == 2) 560 | { 561 | ui.noJobsAvailable->hide(); 562 | } 563 | 564 | ui.jobs->insertWidget(0, widget); 565 | ui.jobs->insertWidget(1, line); 566 | ui.tabs->setTabText(1, QString("Jobs (%1)").arg(++mJobCount)); 567 | 568 | auto settings = GetSettings(); 569 | QString opt = settings->value("Settings/mount").toString(); 570 | 571 | QStringList args; 572 | args << "mount"; 573 | args.append(GetRcloneConf()); 574 | if (!opt.isEmpty()) 575 | { 576 | args.append(opt.split(' ')); 577 | } 578 | args << remote << folder; 579 | 580 | UseRclonePassword(mount); 581 | mount->start(GetRclone(), args, QIODevice::ReadOnly); 582 | } 583 | 584 | void MainWindow::addStream(const QString& remote, const QString& stream) 585 | { 586 | auto player = new QProcess(); 587 | auto rclone = new QProcess(); 588 | rclone->setStandardOutputProcess(player); 589 | 590 | QObject::connect(player, static_cast(&QProcess::finished), this, [=](int status) 591 | { 592 | player->deleteLater(); 593 | if (status != 0 && player->error() == QProcess::FailedToStart) 594 | { 595 | QMessageBox::critical(this, "Error", QString("Failed to start '%1' player process").arg(stream)); 596 | auto settings = GetSettings(); 597 | settings->remove("Settings/streamConfirmed"); 598 | } 599 | }); 600 | 601 | auto widget = new StreamWidget(rclone, player, remote, stream); 602 | 603 | auto line = new QFrame(); 604 | line->setFrameShape(QFrame::HLine); 605 | line->setFrameShadow(QFrame::Sunken); 606 | 607 | QObject::connect(widget, &StreamWidget::finished, this, [=]() 608 | { 609 | if (--mJobCount == 0) 610 | { 611 | ui.tabs->setTabText(1, "Jobs"); 612 | } 613 | else 614 | { 615 | ui.tabs->setTabText(1, QString("Jobs (%1)").arg(mJobCount)); 616 | } 617 | }); 618 | 619 | QObject::connect(widget, &StreamWidget::closed, this, [=]() 620 | { 621 | ui.jobs->removeWidget(widget); 622 | ui.jobs->removeWidget(line); 623 | widget->deleteLater(); 624 | delete line; 625 | if (ui.jobs->count() == 2) 626 | { 627 | ui.noJobsAvailable->show(); 628 | } 629 | }); 630 | 631 | if (ui.jobs->count() == 2) 632 | { 633 | ui.noJobsAvailable->hide(); 634 | } 635 | 636 | ui.jobs->insertWidget(0, widget); 637 | ui.jobs->insertWidget(1, line); 638 | ui.tabs->setTabText(1, QString("Jobs (%1)").arg(++mJobCount)); 639 | 640 | player->start(stream, QProcess::ReadOnly); 641 | UseRclonePassword(rclone); 642 | rclone->start(GetRclone(), QStringList() << "cat" << GetRcloneConf() << remote, QProcess::WriteOnly); 643 | } 644 | -------------------------------------------------------------------------------- /src/main_window.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | #include "ui_main_window.h" 5 | #include "icon_cache.h" 6 | 7 | class JobWidget; 8 | 9 | class MainWindow : public QMainWindow 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | MainWindow(); 15 | ~MainWindow(); 16 | 17 | private slots: 18 | void rcloneGetVersion(); 19 | void rcloneConfig(); 20 | void rcloneListRemotes(); 21 | 22 | void addTransfer(const QString& message, const QString& source, const QString& dest, const QStringList& args); 23 | void addMount(const QString& remote, const QString& folder); 24 | void addStream(const QString& remote, const QString& stream); 25 | 26 | private: 27 | Ui::MainWindow ui; 28 | 29 | QSystemTrayIcon mSystemTray; 30 | JobWidget* mLastFinished = nullptr; 31 | 32 | bool mAlwaysShowInTray; 33 | bool mCloseToTray; 34 | bool mNotifyFinishedTransfers; 35 | 36 | QLabel* mStatusMessage; 37 | 38 | IconCache mIcons; 39 | 40 | bool mFirstTime = true; 41 | int mJobCount = 0; 42 | 43 | bool canClose(); 44 | void closeEvent(QCloseEvent* ev) override; 45 | bool getConfigPassword(QProcess* p); 46 | 47 | void addEmptyJobsMessage(); 48 | }; 49 | -------------------------------------------------------------------------------- /src/main_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 658 10 | 411 11 | 12 | 13 | 14 | Rclone Browser 15 | 16 | 17 | 18 | 19 | 0 20 | 21 | 22 | 0 23 | 24 | 25 | 0 26 | 27 | 28 | 0 29 | 30 | 31 | 0 32 | 33 | 34 | 35 | 36 | 0 37 | 38 | 39 | true 40 | 41 | 42 | 43 | Remotes 44 | 45 | 46 | 47 | 48 | 49 | QAbstractItemView::NoEditTriggers 50 | 51 | 52 | QAbstractItemView::SelectRows 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 0 63 | 0 64 | 65 | 66 | 67 | &Config... 68 | 69 | 70 | 71 | 72 | 73 | 74 | Refresh 75 | 76 | 77 | 78 | 79 | 80 | 81 | Qt::Horizontal 82 | 83 | 84 | 85 | 40 86 | 20 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | false 95 | 96 | 97 | &Open 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | Jobs 108 | 109 | 110 | 111 | 112 | 113 | true 114 | 115 | 116 | 117 | 118 | 0 119 | 0 120 | 628 121 | 305 122 | 123 | 124 | 125 | 126 | 0 127 | 128 | 129 | 0 130 | 131 | 132 | 0 133 | 134 | 135 | 0 136 | 137 | 138 | 139 | 140 | 141 | 142 | No jobs are available 143 | 144 | 145 | 146 | 147 | 148 | 149 | Qt::Vertical 150 | 151 | 152 | 153 | 20 154 | 40 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 0 175 | 0 176 | 658 177 | 22 178 | 179 | 180 | 181 | 182 | &File 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | &Help 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | &About 202 | 203 | 204 | QAction::AboutRole 205 | 206 | 207 | 208 | 209 | About &Qt 210 | 211 | 212 | QAction::AboutQtRole 213 | 214 | 215 | 216 | 217 | &Preferences... 218 | 219 | 220 | QAction::PreferencesRole 221 | 222 | 223 | 224 | 225 | &Quit 226 | 227 | 228 | Ctrl+Q 229 | 230 | 231 | QAction::QuitRole 232 | 233 | 234 | 235 | 236 | tabs 237 | remotes 238 | config 239 | refresh 240 | open 241 | jobsArea 242 | 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /src/mount_widget.cpp: -------------------------------------------------------------------------------- 1 | #include "mount_widget.h" 2 | 3 | MountWidget::MountWidget(QProcess* process, const QString& remote, const QString& folder, QWidget* parent) 4 | : QWidget(parent) 5 | , mProcess(process) 6 | { 7 | ui.setupUi(this); 8 | 9 | ui.remote->setText(remote); 10 | ui.folder->setText(folder); 11 | ui.info->setText(QString("%1 on %2").arg(remote).arg(folder)); 12 | 13 | ui.details->setVisible(false); 14 | 15 | ui.output->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); 16 | ui.output->setVisible(false); 17 | 18 | QObject::connect(ui.showDetails, &QToolButton::toggled, this, [=](bool checked) 19 | { 20 | ui.details->setVisible(checked); 21 | ui.showDetails->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 22 | }); 23 | 24 | QObject::connect(ui.showOutput, &QToolButton::toggled, this, [=](bool checked) 25 | { 26 | ui.output->setVisible(checked); 27 | ui.showOutput->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 28 | }); 29 | 30 | ui.cancel->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogCloseButton)); 31 | 32 | QObject::connect(ui.cancel, &QToolButton::clicked, this, [=]() 33 | { 34 | if (mRunning) 35 | { 36 | int button = QMessageBox::question( 37 | this, 38 | "Unmount", 39 | QString("Do you want to umount %1 folder?").arg(folder), 40 | QMessageBox::Yes | QMessageBox::No); 41 | if (button == QMessageBox::Yes) 42 | { 43 | cancel(); 44 | } 45 | } 46 | else 47 | { 48 | emit closed(); 49 | } 50 | }); 51 | 52 | QObject::connect(mProcess, &QProcess::readyRead, this, [=]() 53 | { 54 | while (mProcess->canReadLine()) 55 | { 56 | ui.output->appendPlainText(mProcess->readLine().trimmed()); 57 | } 58 | }); 59 | 60 | QObject::connect(mProcess, static_cast(&QProcess::finished), this, [=](int status) 61 | { 62 | mProcess->deleteLater(); 63 | mRunning = false; 64 | if (status == 0) 65 | { 66 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: black; }"); 67 | ui.showDetails->setText("Finished"); 68 | } 69 | else 70 | { 71 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: red; }"); 72 | ui.showDetails->setText("Error"); 73 | } 74 | ui.cancel->setToolTip("Close"); 75 | emit finished(); 76 | }); 77 | 78 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: green; }"); 79 | ui.showDetails->setText("Mounted"); 80 | } 81 | 82 | MountWidget::~MountWidget() 83 | { 84 | } 85 | 86 | void MountWidget::cancel() 87 | { 88 | if (!mRunning) 89 | { 90 | return; 91 | } 92 | 93 | QString cmd; 94 | #ifdef Q_OS_OSX 95 | QProcess::startDetached("umount", QStringList() << ui.folder->text()); 96 | #else 97 | QProcess::startDetached("fusermount", QStringList() << "-u" << ui.folder->text()); 98 | #endif 99 | mProcess->waitForFinished(); 100 | 101 | emit closed(); 102 | } 103 | -------------------------------------------------------------------------------- /src/mount_widget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | #include "ui_mount_widget.h" 5 | 6 | class MountWidget : public QWidget 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | MountWidget(QProcess* process, const QString& remote, const QString& folder, QWidget* parent = nullptr); 12 | ~MountWidget(); 13 | 14 | public slots: 15 | void cancel(); 16 | 17 | signals: 18 | void finished(); 19 | void closed(); 20 | 21 | private: 22 | Ui::MountWidget ui; 23 | 24 | bool mRunning = true; 25 | QProcess* mProcess; 26 | }; 27 | -------------------------------------------------------------------------------- /src/mount_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MountWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 654 10 | 280 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | true 39 | 40 | 41 | Qt::ToolButtonTextBesideIcon 42 | 43 | 44 | Qt::RightArrow 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Unmount 55 | 56 | 57 | QToolButton { border: 0 } 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 0 72 | 73 | 74 | 0 75 | 76 | 77 | 0 78 | 79 | 80 | 81 | 82 | true 83 | 84 | 85 | 86 | 87 | 88 | 89 | true 90 | 91 | 92 | 93 | 94 | 95 | 96 | QToolButton { border: 0 } 97 | 98 | 99 | Show Output 100 | 101 | 102 | true 103 | 104 | 105 | Qt::ToolButtonTextBesideIcon 106 | 107 | 108 | Qt::RightArrow 109 | 110 | 111 | 112 | 113 | 114 | 115 | QPlainTextEdit::NoWrap 116 | 117 | 118 | true 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 0 127 | 0 128 | 129 | 130 | 131 | Folder: 132 | 133 | 134 | folder 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 0 143 | 0 144 | 145 | 146 | 147 | Remote: 148 | 149 | 150 | remote 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | showDetails 161 | cancel 162 | remote 163 | folder 164 | showOutput 165 | output 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/osx_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | 5 | QIcon osxGetIcon(const QString& extension); 6 | void osxHideDockIcon(); 7 | void osxShowDockIcon(); 8 | -------------------------------------------------------------------------------- /src/osx_helper.mm: -------------------------------------------------------------------------------- 1 | #include "osx_helper.h" 2 | #include 3 | #include 4 | 5 | QIcon osxGetIcon(const QString& extension) 6 | { 7 | QIcon icon; 8 | @autoreleasepool 9 | { 10 | NSImage* image = [[NSWorkspace sharedWorkspace] iconForFileType:extension.toNSString()]; 11 | NSRect rect = NSMakeRect(0, 0, image.size.width, image.size.height); 12 | CGImageRef imageRef = [image CGImageForProposedRect:&rect context:NULL hints:nil]; 13 | if (imageRef) 14 | { 15 | icon = QtMac::fromCGImageRef(imageRef); 16 | } 17 | } 18 | return icon; 19 | } 20 | 21 | void osxShowDockIcon() 22 | { 23 | ProcessSerialNumber psn = { 0, kCurrentProcess }; 24 | TransformProcessType(&psn, kProcessTransformToForegroundApplication); 25 | } 26 | 27 | void osxHideDockIcon() 28 | { 29 | ProcessSerialNumber psn = { 0, kCurrentProcess }; 30 | TransformProcessType(&psn, kProcessTransformToUIElementApplication); 31 | } 32 | -------------------------------------------------------------------------------- /src/pch.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | -------------------------------------------------------------------------------- /src/pch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef _MSC_VER 4 | #pragma warning(push, 0) 5 | #endif 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #ifdef Q_OS_WIN32 15 | #include 16 | #endif 17 | 18 | #ifdef Q_OS_OSX 19 | #include 20 | #endif 21 | 22 | #ifdef _MSC_VER 23 | #pragma warning pop 24 | #endif 25 | -------------------------------------------------------------------------------- /src/preferences_dialog.cpp: -------------------------------------------------------------------------------- 1 | #include "preferences_dialog.h" 2 | #include "utils.h" 3 | 4 | PreferencesDialog::PreferencesDialog(QWidget* parent) 5 | : QDialog(parent) 6 | { 7 | ui.setupUi(this); 8 | 9 | QObject::connect(ui.rcloneBrowse, &QPushButton::clicked, this, [=]() 10 | { 11 | QString rclone = QFileDialog::getOpenFileName(this, "Select rclone executable", ui.rclone->text()); 12 | if (rclone.isEmpty()) 13 | { 14 | return; 15 | } 16 | 17 | if (!QFileInfo(rclone).isExecutable()) 18 | { 19 | QMessageBox::critical(this, "Error", QString("File %1 is not executable").arg(rclone)); 20 | return; 21 | } 22 | 23 | if (QFileInfo(rclone) == QFileInfo(qApp->applicationFilePath())) 24 | { 25 | QMessageBox::critical(this, "Error", "You selected RcloneBrowser executable!\nPlease select rclone executable instead."); 26 | return; 27 | } 28 | 29 | ui.rclone->setText(rclone); 30 | }); 31 | 32 | QObject::connect(ui.rcloneConfBrowse, &QPushButton::clicked, this, [=]() 33 | { 34 | QString rcloneConf = QFileDialog::getOpenFileName(this, "Select .rclone.conf location", ui.rcloneConf->text()); 35 | if (rcloneConf.isEmpty()) 36 | { 37 | return; 38 | } 39 | 40 | ui.rcloneConf->setText(rcloneConf); 41 | }); 42 | 43 | auto settings = GetSettings(); 44 | ui.rclone->setText(QDir::toNativeSeparators(settings->value("Settings/rclone").toString())); 45 | ui.rcloneConf->setText(QDir::toNativeSeparators(settings->value("Settings/rcloneConf").toString())); 46 | ui.stream->setText(settings->value("Settings/stream").toString()); 47 | ui.showFolderIcons->setChecked(settings->value("Settings/showFolderIcons", true).toBool()); 48 | if (QSystemTrayIcon::isSystemTrayAvailable()) 49 | { 50 | ui.alwaysShowInTray->setChecked(settings->value("Settings/alwaysShowInTray", false).toBool()); 51 | ui.closeToTray->setChecked(settings->value("Settings/closeToTray", false).toBool()); 52 | ui.notifyFinishedTransfers->setChecked(settings->value("Settings/notifyFinishedTransfers", true).toBool()); 53 | } 54 | else 55 | { 56 | ui.alwaysShowInTray->setChecked(false); 57 | ui.alwaysShowInTray->setDisabled(true); 58 | ui.closeToTray->setChecked(false); 59 | ui.closeToTray->setDisabled(true); 60 | ui.notifyFinishedTransfers->setChecked(false); 61 | ui.notifyFinishedTransfers->setDisabled(true); 62 | } 63 | ui.showFileIcons->setChecked(settings->value("Settings/showFileIcons", true).toBool()); 64 | ui.rowColors->setChecked(settings->value("Settings/rowColors", false).toBool()); 65 | 66 | #ifdef Q_OS_WIN32 67 | ui.mount->hide(); 68 | ui.mountLabel->hide(); 69 | #else 70 | ui.mount->setText(settings->value("Settings/mount").toString()); 71 | #endif 72 | } 73 | 74 | PreferencesDialog::~PreferencesDialog() 75 | { 76 | } 77 | 78 | QString PreferencesDialog::getRclone() const 79 | { 80 | return QDir::fromNativeSeparators(ui.rclone->text()); 81 | } 82 | 83 | QString PreferencesDialog::getRcloneConf() const 84 | { 85 | return QDir::fromNativeSeparators(ui.rcloneConf->text()); 86 | } 87 | 88 | QString PreferencesDialog::getStream() const 89 | { 90 | return ui.stream->text(); 91 | } 92 | 93 | QString PreferencesDialog::getMount() const 94 | { 95 | return ui.mount->text(); 96 | } 97 | 98 | bool PreferencesDialog::getAlwaysShowInTray() const 99 | { 100 | return ui.alwaysShowInTray->isChecked(); 101 | } 102 | 103 | bool PreferencesDialog::getCloseToTray() const 104 | { 105 | return ui.closeToTray->isChecked(); 106 | } 107 | 108 | bool PreferencesDialog::getNotifyFinishedTransfers() const 109 | { 110 | return ui.notifyFinishedTransfers->isChecked(); 111 | } 112 | 113 | bool PreferencesDialog::getShowFolderIcons() const 114 | { 115 | return ui.showFolderIcons->isChecked(); 116 | } 117 | 118 | bool PreferencesDialog::getShowFileIcons() const 119 | { 120 | return ui.showFileIcons->isChecked(); 121 | } 122 | 123 | bool PreferencesDialog::getRowColors() const 124 | { 125 | return ui.rowColors->isChecked(); 126 | } 127 | -------------------------------------------------------------------------------- /src/preferences_dialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | #include "ui_preferences_dialog.h" 5 | 6 | class PreferencesDialog : public QDialog 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | PreferencesDialog(QWidget* parent = nullptr); 12 | ~PreferencesDialog(); 13 | 14 | QString getRclone() const; 15 | QString getRcloneConf() const; 16 | QString getStream() const; 17 | QString getMount() const; 18 | 19 | bool getAlwaysShowInTray() const; 20 | bool getCloseToTray() const; 21 | bool getNotifyFinishedTransfers() const; 22 | 23 | bool getShowFolderIcons() const; 24 | bool getShowFileIcons() const; 25 | bool getRowColors() const; 26 | 27 | private: 28 | Ui::PreferencesDialog ui; 29 | }; 30 | -------------------------------------------------------------------------------- /src/preferences_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PreferencesDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 475 10 | 407 11 | 12 | 13 | 14 | Preferences 15 | 16 | 17 | 18 | 19 | 20 | Settings 21 | 22 | 23 | 24 | 25 | 26 | ... 27 | 28 | 29 | 30 | 31 | 32 | 33 | rclone location: 34 | 35 | 36 | rclone 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Mount options: 53 | 54 | 55 | mount 56 | 57 | 58 | 59 | 60 | 61 | 62 | Stream command: 63 | 64 | 65 | stream 66 | 67 | 68 | 69 | 70 | 71 | 72 | .rclone.conf location: 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ... 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 0 94 | 0 95 | 96 | 97 | 98 | System Tray 99 | 100 | 101 | 102 | 103 | 104 | Always show in system tray 105 | 106 | 107 | 108 | 109 | 110 | 111 | Close to system tray 112 | 113 | 114 | 115 | 116 | 117 | 118 | Notify about finished transfers 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | User Interface 129 | 130 | 131 | 132 | 133 | 134 | Changing these options will require reopening remote tab. 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 0 143 | 0 144 | 145 | 146 | 147 | Show folder icons 148 | 149 | 150 | 151 | 152 | 153 | 154 | Alternating row colors 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 0 163 | 0 164 | 165 | 166 | 167 | Show file icons 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | Qt::Vertical 178 | 179 | 180 | 181 | 0 182 | 0 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | Qt::Horizontal 191 | 192 | 193 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 194 | 195 | 196 | 197 | 198 | 199 | 200 | rclone 201 | rcloneBrowse 202 | rcloneConf 203 | rcloneConfBrowse 204 | stream 205 | mount 206 | alwaysShowInTray 207 | closeToTray 208 | notifyFinishedTransfers 209 | showFolderIcons 210 | showFileIcons 211 | rowColors 212 | 213 | 214 | 215 | 216 | buttonBox 217 | accepted() 218 | PreferencesDialog 219 | accept() 220 | 221 | 222 | 256 223 | 319 224 | 225 | 226 | 157 227 | 274 228 | 229 | 230 | 231 | 232 | buttonBox 233 | rejected() 234 | PreferencesDialog 235 | reject() 236 | 237 | 238 | 324 239 | 319 240 | 241 | 242 | 286 243 | 274 244 | 245 | 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /src/progress_dialog.cpp: -------------------------------------------------------------------------------- 1 | #include "progress_dialog.h" 2 | 3 | ProgressDialog::ProgressDialog(const QString& title, const QString& operation, const QString& message, QProcess* process, QWidget* parent, bool close) 4 | : QDialog(parent) 5 | { 6 | ui.setupUi(this); 7 | resize(width(), 0); 8 | 9 | setWindowTitle(title); 10 | ui.labelOperation->setText(operation); 11 | ui.labelInfo->setText(message); 12 | 13 | ui.output->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); 14 | ui.output->setVisible(false); 15 | 16 | QObject::connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); 17 | 18 | QObject::connect(ui.buttonShowOutput, &QPushButton::toggled, this, [=](bool checked) 19 | { 20 | ui.output->setVisible(checked); 21 | ui.buttonShowOutput->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 22 | if (!checked) 23 | { 24 | adjustSize(); 25 | } 26 | }); 27 | 28 | QObject::connect(process, static_cast(&QProcess::finished), 29 | this, [=](int code, QProcess::ExitStatus status) 30 | { 31 | if (status == QProcess::NormalExit && code == 0) 32 | { 33 | if (close) 34 | { 35 | emit accept(); 36 | } 37 | } 38 | else 39 | { 40 | ui.buttonShowOutput->setChecked(true); 41 | ui.buttonBox->setEnabled(true); 42 | } 43 | }); 44 | 45 | QObject::connect(process, &QProcess::readyRead, this, [=]() 46 | { 47 | QString output = process->readAll(); 48 | ui.output->appendPlainText(output); 49 | emit outputAvailable(output); 50 | }); 51 | 52 | process->setProcessChannelMode(QProcess::MergedChannels); 53 | process->start(QIODevice::ReadOnly); 54 | } 55 | 56 | ProgressDialog::~ProgressDialog() 57 | { 58 | } 59 | 60 | void ProgressDialog::expand() 61 | { 62 | ui.buttonShowOutput->setChecked(true); 63 | } 64 | 65 | void ProgressDialog::allowToClose() 66 | { 67 | ui.buttonBox->setEnabled(true); 68 | } 69 | // 70 | //QString ProgressDialog::getOutput() const 71 | //{ 72 | // return ui.output->toPlainText(); 73 | //} 74 | -------------------------------------------------------------------------------- /src/progress_dialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | #include "ui_progress_dialog.h" 5 | 6 | class ProgressDialog : public QDialog 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | ProgressDialog(const QString& title, const QString& operation, const QString& message, QProcess* process, QWidget* parent = nullptr, bool close = true); 12 | ~ProgressDialog(); 13 | 14 | void expand(); 15 | void allowToClose(); 16 | 17 | signals: 18 | void outputAvailable(const QString& output) const; 19 | 20 | private: 21 | Ui::ProgressDialog ui; 22 | }; 23 | -------------------------------------------------------------------------------- /src/progress_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ProgressDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 618 10 | 291 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | 0 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 600 29 | 200 30 | 31 | 32 | 33 | QPlainTextEdit::NoWrap 34 | 35 | 36 | 37 | 38 | 39 | 40 | false 41 | 42 | 43 | QDialogButtonBox::Close 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 0 52 | 0 53 | 54 | 55 | 56 | QToolButton { border: none; } 57 | 58 | 59 | Show Output 60 | 61 | 62 | true 63 | 64 | 65 | Qt::ToolButtonTextBesideIcon 66 | 67 | 68 | Qt::RightArrow 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 0 77 | 0 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | buttonShowOutput 86 | output 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/remote_widget.cpp: -------------------------------------------------------------------------------- 1 | #include "remote_widget.h" 2 | #include "transfer_dialog.h" 3 | #include "export_dialog.h" 4 | #include "progress_dialog.h" 5 | #include "icon_cache.h" 6 | #include "item_model.h" 7 | #include "utils.h" 8 | 9 | RemoteWidget::RemoteWidget(IconCache* iconCache, const QString& remote, bool isLocal, QWidget* parent) 10 | : QWidget(parent) 11 | { 12 | ui.setupUi(this); 13 | 14 | QString root = isLocal ? "/" : QString(); 15 | 16 | #ifdef Q_OS_WIN32 17 | ui.mount->setVisible(false); 18 | ui.buttonMount->setVisible(false); 19 | #else 20 | isLocal = false; 21 | #endif 22 | auto settings = GetSettings(); 23 | ui.tree->setAlternatingRowColors(settings->value("Settings/rowColors", false).toBool()); 24 | 25 | QStyle* style = QApplication::style(); 26 | ui.refresh->setIcon(style->standardIcon(QStyle::SP_BrowserReload)); 27 | ui.mkdir->setIcon(style->standardIcon(QStyle::SP_FileDialogNewFolder)); 28 | ui.rename->setIcon(style->standardIcon(QStyle::SP_FileIcon)); 29 | ui.purge->setIcon(style->standardIcon(QStyle::SP_TrashIcon)); 30 | ui.mount->setIcon(style->standardIcon(QStyle::SP_DirLinkIcon)); 31 | ui.stream->setIcon(style->standardIcon(QStyle::SP_MediaPlay)); 32 | ui.upload->setIcon(style->standardIcon(QStyle::SP_ArrowUp)); 33 | ui.download->setIcon(style->standardIcon(QStyle::SP_ArrowDown)); 34 | ui.download->setIcon(style->standardIcon(QStyle::SP_ArrowDown)); 35 | ui.getSize->setIcon(style->standardIcon(QStyle::SP_FileDialogInfoView)); 36 | ui.export_->setIcon(style->standardIcon(QStyle::SP_FileDialogDetailedView)); 37 | 38 | ui.buttonRefresh->setDefaultAction(ui.refresh); 39 | ui.buttonMkdir->setDefaultAction(ui.mkdir); 40 | ui.buttonRename->setDefaultAction(ui.rename); 41 | ui.buttonPurge->setDefaultAction(ui.purge); 42 | ui.buttonMount->setDefaultAction(ui.mount); 43 | ui.buttonStream->setDefaultAction(ui.stream); 44 | ui.buttonUpload->setDefaultAction(ui.upload); 45 | ui.buttonDownload->setDefaultAction(ui.download); 46 | ui.buttonSize->setDefaultAction(ui.getSize); 47 | ui.buttonExport->setDefaultAction(ui.export_); 48 | 49 | ui.tree->sortByColumn(0, Qt::AscendingOrder); 50 | ui.tree->header()->setSectionsMovable(false); 51 | 52 | ItemModel* model = new ItemModel(iconCache, remote, this); 53 | ui.tree->setModel(model); 54 | QTimer::singleShot(0, ui.tree, SLOT(setFocus())); 55 | 56 | QObject::connect(model, &QAbstractItemModel::layoutChanged, this, [=]() 57 | { 58 | ui.tree->header()->setSectionResizeMode(0, QHeaderView::Stretch); 59 | ui.tree->resizeColumnToContents(1); 60 | ui.tree->resizeColumnToContents(2); 61 | }); 62 | 63 | QObject::connect(ui.tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, [=](const QItemSelection& selection) 64 | { 65 | for (auto child : findChildren()) 66 | { 67 | child->setDisabled(selection.isEmpty()); 68 | } 69 | 70 | if (selection.isEmpty()) 71 | { 72 | ui.path->clear(); 73 | return; 74 | } 75 | 76 | QModelIndex index = selection.indexes().front(); 77 | 78 | bool topLevel = model->isTopLevel(index); 79 | bool isFolder = model->isFolder(index); 80 | 81 | QDir path; 82 | if (model->isLoading(index)) 83 | { 84 | ui.refresh->setDisabled(true); 85 | ui.rename->setDisabled(true); 86 | ui.purge->setDisabled(true); 87 | ui.mount->setDisabled(true); 88 | ui.stream->setDisabled(true); 89 | path = model->path(model->parent(index)); 90 | } 91 | else 92 | { 93 | ui.refresh->setDisabled(false); 94 | ui.rename->setDisabled(topLevel); 95 | ui.purge->setDisabled(topLevel); 96 | ui.mount->setDisabled(!isFolder); 97 | ui.stream->setDisabled(isFolder); 98 | path = model->path(index); 99 | } 100 | 101 | ui.getSize->setDisabled(!isFolder); 102 | ui.export_->setDisabled(!isFolder); 103 | ui.path->setText(isLocal ? QDir::toNativeSeparators(path.path()) : path.path()); 104 | }); 105 | 106 | QObject::connect(ui.refresh, &QAction::triggered, this, [=]() 107 | { 108 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 109 | model->refresh(index); 110 | }); 111 | 112 | QObject::connect(ui.mkdir, &QAction::triggered, this, [=]() 113 | { 114 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 115 | if (!model->isFolder(index)) 116 | { 117 | index = index.parent(); 118 | } 119 | QDir path = model->path(index); 120 | QString pathMsg = isLocal ? QDir::toNativeSeparators(path.path()) : path.path(); 121 | 122 | QString name = QInputDialog::getText(this, "New Folder", QString("Create folder in %1").arg(pathMsg)); 123 | if (!name.isEmpty()) 124 | { 125 | QString folder = path.filePath(name); 126 | QString folderMsg = isLocal ? QDir::toNativeSeparators(folder) : folder; 127 | 128 | QProcess process; 129 | UseRclonePassword(&process); 130 | process.setProgram(GetRclone()); 131 | process.setArguments(QStringList() << "mkdir" << GetRcloneConf() << remote + ":" + folder); 132 | process.setReadChannelMode(QProcess::MergedChannels); 133 | 134 | ProgressDialog progress("New Folder", "Creating...", folderMsg, &process, this); 135 | if (progress.exec() == QDialog::Accepted) 136 | { 137 | model->refresh(index); 138 | } 139 | } 140 | }); 141 | 142 | QObject::connect(ui.rename, &QAction::triggered, this, [=]() 143 | { 144 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 145 | 146 | QString path = model->path(index).path(); 147 | QString pathMsg = isLocal ? QDir::toNativeSeparators(path) : path; 148 | 149 | QString name = model->data(index, Qt::DisplayRole).toString(); 150 | name = QInputDialog::getText(this, "Rename", QString("New name for %1").arg(pathMsg), QLineEdit::Normal, name); 151 | if (!name.isEmpty()) 152 | { 153 | QProcess process; 154 | UseRclonePassword(&process); 155 | process.setProgram(GetRclone()); 156 | process.setArguments(QStringList() 157 | << "move" 158 | << GetRcloneConf() 159 | << remote + ":" + path 160 | << remote + ":" + model->path(index.parent()).filePath(name)); 161 | process.setReadChannelMode(QProcess::MergedChannels); 162 | 163 | ProgressDialog progress("Rename", "Renaming...", pathMsg, &process, this); 164 | if (progress.exec() == QDialog::Accepted) 165 | { 166 | model->rename(index, name); 167 | } 168 | } 169 | }); 170 | 171 | QObject::connect(ui.purge, &QAction::triggered, this, [=]() 172 | { 173 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 174 | 175 | QString path = model->path(index).path(); 176 | QString pathMsg = isLocal ? QDir::toNativeSeparators(path) : path; 177 | 178 | int button = QMessageBox::question(this, "Delete", QString("Are you sure you want to delete %1 ?").arg(pathMsg), QMessageBox::Yes | QMessageBox::No); 179 | if (button == QMessageBox::Yes) 180 | { 181 | QProcess process; 182 | UseRclonePassword(&process); 183 | process.setProgram(GetRclone()); 184 | process.setArguments(QStringList() << (model->isFolder(index) ? "purge" : "delete") << GetRcloneConf() << remote + ":" + path); 185 | process.setReadChannelMode(QProcess::MergedChannels); 186 | 187 | ProgressDialog progress("Delete", "Deleting...", pathMsg, &process, this); 188 | if (progress.exec() == QDialog::Accepted) 189 | { 190 | QModelIndex parent = index.parent(); 191 | QModelIndex next = parent.child(index.row() + 1, 0); 192 | ui.tree->selectionModel()->select(next.isValid() ? next : parent, QItemSelectionModel::SelectCurrent); 193 | model->removeRow(index.row(), parent); 194 | } 195 | } 196 | }); 197 | 198 | QObject::connect(ui.mount, &QAction::triggered, this, [=]() 199 | { 200 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 201 | 202 | QString path = model->path(index).path(); 203 | QString pathMsg = isLocal ? QDir::toNativeSeparators(path) : path; 204 | 205 | QString folder = QFileDialog::getExistingDirectory(this, QString("Mount %1").arg(pathMsg)); 206 | if (!folder.isEmpty()) 207 | { 208 | emit addMount(remote + ":" + path, folder); 209 | } 210 | }); 211 | 212 | QObject::connect(ui.stream, &QAction::triggered, this, [=]() 213 | { 214 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 215 | QString path = model->path(index).path(); 216 | 217 | auto settings = GetSettings(); 218 | bool streamConfirmed = settings->value("Settings/streamConfirmed", false).toBool(); 219 | QString stream = settings->value("Settings/stream", "mpv -").toString(); 220 | if (!streamConfirmed) 221 | { 222 | QString result = QInputDialog::getText(this, "Stream", "Enter stream command (file will be passed in STDIN):", QLineEdit::Normal, stream); 223 | if (result.isEmpty()) 224 | { 225 | return; 226 | } 227 | 228 | stream = result; 229 | 230 | settings->setValue("Settings/stream", stream); 231 | settings->setValue("Settings/streamConfirmed", true); 232 | } 233 | 234 | emit addStream(remote + ":" + path, stream); 235 | }); 236 | 237 | QObject::connect(ui.upload, &QAction::triggered, this, [=]() 238 | { 239 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 240 | if (!model->isFolder(index)) 241 | { 242 | index = index.parent(); 243 | } 244 | QDir path = model->path(index); 245 | 246 | TransferDialog t(false, remote, path, true, this); 247 | if (t.exec() == QDialog::Accepted) 248 | { 249 | QString src = t.getSource(); 250 | QString dst = t.getDest(); 251 | 252 | QStringList args = t.getOptions(); 253 | emit addTransfer(QString("%1 from %2").arg(t.getMode()).arg(src), src, dst, args); 254 | } 255 | }); 256 | 257 | QObject::connect(ui.download, &QAction::triggered, this, [=]() 258 | { 259 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 260 | QDir path = model->path(index); 261 | 262 | TransferDialog t(true, remote, path, model->isFolder(index), this); 263 | if (t.exec() == QDialog::Accepted) 264 | { 265 | QString src = t.getSource(); 266 | QString dst = t.getDest(); 267 | 268 | QStringList args = t.getOptions(); 269 | emit addTransfer(QString("%1 %2").arg(t.getMode()).arg(src), src, dst, args); 270 | } 271 | }); 272 | 273 | QObject::connect(ui.getSize, &QAction::triggered, this, [=]() 274 | { 275 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 276 | 277 | QString path = model->path(index).path(); 278 | QString pathMsg = isLocal ? QDir::toNativeSeparators(path) : path; 279 | 280 | QProcess process; 281 | UseRclonePassword(&process); 282 | process.setProgram(GetRclone()); 283 | process.setArguments(QStringList() << "size" << GetRcloneConf() << remote + ":" + path); 284 | process.setReadChannelMode(QProcess::MergedChannels); 285 | 286 | ProgressDialog progress("Get Size", "Calculating...", pathMsg, &process, this, false); 287 | progress.expand(); 288 | progress.allowToClose(); 289 | progress.exec(); 290 | }); 291 | 292 | QObject::connect(ui.export_, &QAction::triggered, this, [=]() 293 | { 294 | QModelIndex index = ui.tree->selectionModel()->selectedRows().front(); 295 | QDir path = model->path(index); 296 | 297 | ExportDialog e(remote, path, this); 298 | if (e.exec() == QDialog::Accepted) 299 | { 300 | QString dst = e.getDestination(); 301 | bool txt = e.onlyFilenames(); 302 | 303 | QFile* file = new QFile(dst); 304 | if (!file->open(QFile::WriteOnly)) 305 | { 306 | QMessageBox::warning(this, "Error", QString("Cannot open file '%1' for writing!").arg(dst)); 307 | delete file; 308 | return; 309 | } 310 | 311 | QRegExp re(R"(^(\d+) (\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)\.\d+ (.+)$)"); 312 | 313 | QProcess process; 314 | UseRclonePassword(&process); 315 | process.setProgram(GetRclone()); 316 | process.setArguments(QStringList() << GetRcloneConf() << e.getOptions()); 317 | process.setReadChannelMode(QProcess::MergedChannels); 318 | 319 | ProgressDialog progress("Export", "Exporting...", dst, &process, this); 320 | file->setParent(&progress); 321 | 322 | QObject::connect(&progress, &ProgressDialog::outputAvailable, this, [=](const QString& output) 323 | { 324 | QTextStream out(file); 325 | 326 | for (const auto& line : output.split('\n')) 327 | { 328 | if (re.exactMatch(line.trimmed())) 329 | { 330 | QStringList cap = re.capturedTexts(); 331 | 332 | if (txt) 333 | { 334 | out << cap[3] << '\n'; 335 | } 336 | else 337 | { 338 | QString name = cap[3]; 339 | if (name.contains(' ') || name.contains(',') || name.contains('"')) 340 | { 341 | name = '"' + name.replace("\"", "\"\"") + '"'; 342 | } 343 | out << name << ',' << '"' << cap[2] << '"' << ',' << cap[1].toULongLong() << '\n'; 344 | } 345 | } 346 | } 347 | }); 348 | 349 | progress.exec(); 350 | } 351 | }); 352 | 353 | QObject::connect(model, &ItemModel::drop, this, [=](const QDir& path, const QModelIndex& parent) 354 | { 355 | qApp->setActiveWindow(this); 356 | QDir destPath = model->path(parent); 357 | QString dest = QFileInfo(path.path()).isDir() ? destPath.filePath(path.dirName()) : destPath.path(); 358 | TransferDialog t(false, remote, dest, true, this); 359 | t.setSource(path.path()); 360 | if (t.exec() == QDialog::Accepted) 361 | { 362 | QString src = t.getSource(); 363 | QString dst = t.getDest(); 364 | 365 | QStringList args = t.getOptions(); 366 | emit addTransfer(QString("%1 from %2").arg(t.getMode()).arg(src), src, dst, args); 367 | } 368 | }); 369 | 370 | QObject::connect(ui.tree, &QWidget::customContextMenuRequested, this, [=](const QPoint& pos) 371 | { 372 | QMenu menu; 373 | menu.addAction(ui.refresh); 374 | menu.addAction(ui.getSize); 375 | menu.addAction(ui.export_); 376 | menu.addSeparator(); 377 | menu.addAction(ui.mkdir); 378 | menu.addAction(ui.rename); 379 | menu.addAction(ui.purge); 380 | menu.addSeparator(); 381 | menu.addAction(ui.mount); 382 | menu.addAction(ui.stream); 383 | menu.addAction(ui.upload); 384 | menu.addAction(ui.download); 385 | menu.exec(ui.tree->viewport()->mapToGlobal(pos)); 386 | }); 387 | 388 | if (isLocal) 389 | { 390 | QHash drives; 391 | 392 | // QDir::drives is fast 393 | for (const auto& drive : QDir::drives()) 394 | { 395 | QString path = drive.path(); 396 | QModelIndex index = model->addRoot(QDir::toNativeSeparators(path), path); 397 | drives.insert(path, index); 398 | } 399 | 400 | #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) 401 | QThread* thread = new QThread(this); 402 | thread->start(); 403 | 404 | QObject* worker = new QObject(); 405 | worker->moveToThread(thread); 406 | 407 | QTimer::singleShot(0, worker, [=]() 408 | { 409 | QStorageInfo info; 410 | info.refresh(); 411 | 412 | // QStorageInfo::mountedVolumes is slow :( 413 | for (const auto& volume : info.mountedVolumes()) 414 | { 415 | QString name = volume.name(); 416 | if (!name.isEmpty()) 417 | { 418 | QString path = volume.rootPath(); 419 | QString item = QString("%1 (%2)").arg(QDir::toNativeSeparators(path)).arg(name); 420 | QTimer::singleShot(0, this, [=]() { 421 | model->rename(drives[path], item); 422 | }); 423 | } 424 | } 425 | 426 | thread->quit(); 427 | thread->deleteLater(); 428 | worker->deleteLater(); 429 | }); 430 | #endif 431 | 432 | ui.tree->selectionModel()->selectionChanged(QItemSelection(), QItemSelection()); 433 | } 434 | else 435 | { 436 | QModelIndex index = model->addRoot("/", root); 437 | ui.tree->selectionModel()->select(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); 438 | ui.tree->expand(index); 439 | } 440 | 441 | QShortcut* close = new QShortcut(QKeySequence::Close, this); 442 | QObject::connect(close, &QShortcut::activated, this, [=]() 443 | { 444 | auto tabs = qobject_cast(parent); 445 | tabs->removeTab(tabs->indexOf(this)); 446 | }); 447 | } 448 | 449 | RemoteWidget::~RemoteWidget() 450 | { 451 | } 452 | -------------------------------------------------------------------------------- /src/remote_widget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | #include "ui_remote_widget.h" 5 | 6 | class IconCache; 7 | 8 | class RemoteWidget : public QWidget 9 | { 10 | Q_OBJECT 11 | 12 | public: 13 | RemoteWidget(IconCache* icons, const QString& remote, bool isLocal, QWidget* parent = nullptr); 14 | ~RemoteWidget(); 15 | 16 | signals: 17 | void addTransfer(const QString& message, const QString& source, const QString& remote, const QStringList& args); 18 | void addMount(const QString& remote, const QString& folder); 19 | void addStream(const QString& remote, const QString& stream); 20 | 21 | private: 22 | Ui::RemoteWidget ui; 23 | }; 24 | -------------------------------------------------------------------------------- /src/remote_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RemoteWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 784 10 | 552 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Qt::Horizontal 21 | 22 | 23 | false 24 | 25 | 26 | 27 | 28 | 0 29 | 30 | 31 | 0 32 | 33 | 34 | 0 35 | 36 | 37 | 0 38 | 39 | 40 | 41 | 42 | 43 | 0 44 | 45 | 46 | 0 47 | 48 | 49 | 0 50 | 51 | 52 | 0 53 | 54 | 55 | 56 | 57 | &Refresh 58 | 59 | 60 | Qt::ToolButtonTextBesideIcon 61 | 62 | 63 | 64 | 65 | 66 | 67 | &New Folder 68 | 69 | 70 | Qt::ToolButtonTextBesideIcon 71 | 72 | 73 | 74 | 75 | 76 | 77 | R&ename 78 | 79 | 80 | Qt::ToolButtonTextBesideIcon 81 | 82 | 83 | 84 | 85 | 86 | 87 | De&lete 88 | 89 | 90 | Qt::ToolButtonTextBesideIcon 91 | 92 | 93 | 94 | 95 | 96 | 97 | &Mount 98 | 99 | 100 | Qt::ToolButtonTextBesideIcon 101 | 102 | 103 | 104 | 105 | 106 | 107 | &Stream 108 | 109 | 110 | Qt::ToolButtonTextBesideIcon 111 | 112 | 113 | 114 | 115 | 116 | 117 | &Upload... 118 | 119 | 120 | Qt::ToolButtonTextBesideIcon 121 | 122 | 123 | 124 | 125 | 126 | 127 | &Download... 128 | 129 | 130 | Qt::ToolButtonTextBesideIcon 131 | 132 | 133 | 134 | 135 | 136 | 137 | &Get Size... 138 | 139 | 140 | Qt::ToolButtonTextBesideIcon 141 | 142 | 143 | 144 | 145 | 146 | 147 | E&xport... 148 | 149 | 150 | Qt::ToolButtonTextBesideIcon 151 | 152 | 153 | 154 | 155 | 156 | 157 | Qt::Horizontal 158 | 159 | 160 | 161 | 40 162 | 20 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | true 174 | 175 | 176 | 177 | 178 | 179 | 180 | Qt::CustomContextMenu 181 | 182 | 183 | true 184 | 185 | 186 | QAbstractItemView::NoEditTriggers 187 | 188 | 189 | true 190 | 191 | 192 | Qt::CopyAction 193 | 194 | 195 | QAbstractItemView::SingleSelection 196 | 197 | 198 | QAbstractItemView::SelectRows 199 | 200 | 201 | true 202 | 203 | 204 | true 205 | 206 | 207 | false 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | &Refresh 219 | 220 | 221 | F5 222 | 223 | 224 | 225 | 226 | &Mount 227 | 228 | 229 | 230 | 231 | &Stream 232 | 233 | 234 | 235 | 236 | &New Folder 237 | 238 | 239 | F7 240 | 241 | 242 | 243 | 244 | R&ename 245 | 246 | 247 | F2 248 | 249 | 250 | 251 | 252 | De&lete 253 | 254 | 255 | Del 256 | 257 | 258 | 259 | 260 | &Upload 261 | 262 | 263 | 264 | 265 | &Download 266 | 267 | 268 | 269 | 270 | &Get Size 271 | 272 | 273 | 274 | 275 | E&xport 276 | 277 | 278 | 279 | 280 | buttonRefresh 281 | buttonMkdir 282 | buttonRename 283 | buttonPurge 284 | buttonMount 285 | buttonStream 286 | buttonUpload 287 | buttonDownload 288 | buttonSize 289 | path 290 | tree 291 | 292 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /src/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon.png 4 | 5 | 6 | images/amazon_cloud_drive.png 7 | images/b2.png 8 | images/crypt.png 9 | images/drive.png 10 | images/dropbox.png 11 | images/google_cloud_storage.png 12 | images/hubic.png 13 | images/local.png 14 | images/onedrive.png 15 | images/s3.png 16 | images/swift.png 17 | images/unknown.png 18 | images/yandex.png 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/resources.rc: -------------------------------------------------------------------------------- 1 | 1 ICON DISCARDABLE "icon.ico" 2 | -------------------------------------------------------------------------------- /src/stream_widget.cpp: -------------------------------------------------------------------------------- 1 | #include "stream_widget.h" 2 | 3 | StreamWidget::StreamWidget(QProcess* rclone, QProcess* player, const QString& remote, const QString& stream, QWidget* parent) 4 | : QWidget(parent) 5 | , mRclone(rclone) 6 | , mPlayer(player) 7 | { 8 | ui.setupUi(this); 9 | 10 | ui.remote->setText(remote); 11 | ui.stream->setText(stream); 12 | ui.info->setText(remote); 13 | 14 | ui.details->setVisible(false); 15 | 16 | ui.output->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); 17 | ui.output->setVisible(false); 18 | 19 | QObject::connect(ui.showDetails, &QToolButton::toggled, this, [=](bool checked) 20 | { 21 | ui.details->setVisible(checked); 22 | ui.showDetails->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 23 | }); 24 | 25 | QObject::connect(ui.showOutput, &QToolButton::toggled, this, [=](bool checked) 26 | { 27 | ui.output->setVisible(checked); 28 | ui.showOutput->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 29 | }); 30 | 31 | ui.cancel->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogCloseButton)); 32 | 33 | QObject::connect(ui.cancel, &QToolButton::clicked, this, [=]() 34 | { 35 | if (mRunning) 36 | { 37 | int button = QMessageBox::question( 38 | this, 39 | "Stop", 40 | QString("Do you want to stop %1 stream?").arg(remote), 41 | QMessageBox::Yes | QMessageBox::No); 42 | if (button == QMessageBox::Yes) 43 | { 44 | cancel(); 45 | } 46 | } 47 | else 48 | { 49 | emit closed(); 50 | } 51 | }); 52 | 53 | QObject::connect(mRclone, &QProcess::readyRead, this, [=]() 54 | { 55 | while (mRclone->canReadLine()) 56 | { 57 | ui.output->appendPlainText(mRclone->readLine().trimmed()); 58 | } 59 | }); 60 | 61 | QObject::connect(mRclone, static_cast(&QProcess::finished), this, [=]() 62 | { 63 | mRclone->deleteLater(); 64 | mRunning = false; 65 | emit finished(); 66 | emit closed(); 67 | }); 68 | 69 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: green; }"); 70 | ui.showDetails->setText("Streaming"); 71 | } 72 | 73 | StreamWidget::~StreamWidget() 74 | { 75 | } 76 | 77 | void StreamWidget::cancel() 78 | { 79 | if (!mRunning) 80 | { 81 | return; 82 | } 83 | 84 | mPlayer->terminate(); 85 | mRclone->kill(); 86 | mRclone->waitForFinished(); 87 | } 88 | -------------------------------------------------------------------------------- /src/stream_widget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | #include "ui_stream_widget.h" 5 | 6 | class StreamWidget : public QWidget 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | StreamWidget(QProcess* rclone, QProcess* player, const QString& remote, const QString& stream, QWidget* parent = nullptr); 12 | ~StreamWidget(); 13 | 14 | public slots: 15 | void cancel(); 16 | 17 | signals: 18 | void finished(); 19 | void closed(); 20 | 21 | private: 22 | Ui::StreamWidget ui; 23 | 24 | bool mRunning = true; 25 | QProcess* mRclone; 26 | QProcess* mPlayer; 27 | }; 28 | -------------------------------------------------------------------------------- /src/stream_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | StreamWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 654 10 | 280 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | true 39 | 40 | 41 | Qt::ToolButtonTextBesideIcon 42 | 43 | 44 | Qt::RightArrow 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Stop 55 | 56 | 57 | QToolButton { border: 0 } 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 0 72 | 73 | 74 | 0 75 | 76 | 77 | 0 78 | 79 | 80 | 81 | 82 | true 83 | 84 | 85 | 86 | 87 | 88 | 89 | true 90 | 91 | 92 | 93 | 94 | 95 | 96 | QToolButton { border: 0 } 97 | 98 | 99 | Show Output 100 | 101 | 102 | true 103 | 104 | 105 | Qt::ToolButtonTextBesideIcon 106 | 107 | 108 | Qt::RightArrow 109 | 110 | 111 | 112 | 113 | 114 | 115 | QPlainTextEdit::NoWrap 116 | 117 | 118 | true 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 0 127 | 0 128 | 129 | 130 | 131 | Folder: 132 | 133 | 134 | stream 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 0 143 | 0 144 | 145 | 146 | 147 | Remote: 148 | 149 | 150 | remote 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | showDetails 161 | cancel 162 | remote 163 | stream 164 | showOutput 165 | output 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/transfer_dialog.cpp: -------------------------------------------------------------------------------- 1 | #include "transfer_dialog.h" 2 | #include "utils.h" 3 | 4 | TransferDialog::TransferDialog(bool isDownload, const QString& remote, const QDir& path, bool isFolder, QWidget* parent) 5 | : QDialog(parent) 6 | , mIsDownload(isDownload) 7 | { 8 | ui.setupUi(this); 9 | resize(0, 0); 10 | setWindowTitle(isDownload ? "Download" : "Upload"); 11 | 12 | QStyle* style = qApp->style(); 13 | ui.buttonSourceFile->setIcon(style->standardIcon(QStyle::SP_FileIcon)); 14 | ui.buttonSourceFolder->setIcon(style->standardIcon(QStyle::SP_DirIcon)); 15 | ui.buttonDest->setIcon(style->standardIcon(QStyle::SP_DirIcon)); 16 | 17 | QPushButton* dryRun = ui.buttonBox->addButton("&Dry run", QDialogButtonBox::AcceptRole); 18 | ui.buttonBox->addButton("&Run", QDialogButtonBox::AcceptRole); 19 | 20 | QObject::connect(ui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [=]() 21 | { 22 | ui.cbSyncDelete->setCurrentIndex(0); 23 | ui.checkSkipNewer->setChecked(false); 24 | ui.checkSkipNewer->setChecked(false); 25 | ui.checkCompare->setChecked(true); 26 | ui.cbCompare->setCurrentIndex(0); 27 | ui.checkVerbose->setChecked(false); 28 | ui.checkSameFilesystem->setChecked(false); 29 | ui.checkDontUpdateModified->setChecked(false); 30 | ui.spinTransfers->setValue(4); 31 | ui.spinCheckers->setValue(8); 32 | ui.textBandwidth->clear(); 33 | ui.textMinSize->clear(); 34 | ui.textMinAge->clear(); 35 | ui.textMaxAge->clear(); 36 | ui.spinMaxDepth->setValue(0); 37 | ui.spinConnectTimeout->setValue(60); 38 | ui.spinIdleTimeout->setValue(300); 39 | ui.spinRetries->setValue(3); 40 | ui.spinLowLevelRetries->setValue(10); 41 | ui.checkDeleteExcluded->setChecked(false); 42 | ui.textExclude->clear(); 43 | ui.textExtra->clear(); 44 | }); 45 | ui.buttonBox->button(QDialogButtonBox::RestoreDefaults)->click(); 46 | 47 | QObject::connect(dryRun, &QPushButton::clicked, this, [=]() 48 | { 49 | mDryRun = true; 50 | }); 51 | QObject::connect(ui.buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); 52 | QObject::connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); 53 | 54 | QObject::connect(ui.buttonSourceFile, &QToolButton::clicked, this, [=]() 55 | { 56 | QString file = QFileDialog::getOpenFileName(this, "Choose file to upload"); 57 | if (!file.isEmpty()) 58 | { 59 | ui.textSource->setText(QDir::toNativeSeparators(file)); 60 | } 61 | }); 62 | 63 | QObject::connect(ui.buttonSourceFolder, &QToolButton::clicked, this, [=]() 64 | { 65 | QString folder = QFileDialog::getExistingDirectory(this, "Choose folder to upload"); 66 | if (!folder.isEmpty()) 67 | { 68 | ui.textSource->setText(QDir::toNativeSeparators(folder)); 69 | ui.textDest->setText(remote + ":" + path.filePath(QFileInfo(folder).fileName())); 70 | } 71 | }); 72 | 73 | QObject::connect(ui.buttonDest, &QToolButton::clicked, this, [=]() 74 | { 75 | QString folder = QFileDialog::getExistingDirectory(this, "Choose destination folder"); 76 | if (!folder.isEmpty()) 77 | { 78 | if (isFolder) 79 | { 80 | ui.textDest->setText(QDir::toNativeSeparators(folder + "/" + path.dirName())); 81 | } 82 | else 83 | { 84 | ui.textDest->setText(QDir::toNativeSeparators(folder)); 85 | } 86 | } 87 | }); 88 | 89 | auto settings = GetSettings(); 90 | settings->beginGroup("Transfer"); 91 | ReadSettings(settings.get(), this); 92 | settings->endGroup(); 93 | 94 | ui.buttonSourceFile->setVisible(!isDownload); 95 | ui.buttonSourceFolder->setVisible(!isDownload); 96 | ui.buttonDest->setVisible(isDownload); 97 | 98 | (isDownload ? ui.textSource : ui.textDest)->setText(remote + ":" + path.path()); 99 | } 100 | 101 | TransferDialog::~TransferDialog() 102 | { 103 | if (result() == QDialog::Accepted) 104 | { 105 | auto settings = GetSettings(); 106 | settings->beginGroup("Transfer"); 107 | WriteSettings(settings.get(), this); 108 | settings->remove("textSource"); 109 | settings->remove("textDest"); 110 | settings->endGroup(); 111 | } 112 | } 113 | 114 | void TransferDialog::setSource(const QString& path) 115 | { 116 | ui.textSource->setText(QDir::toNativeSeparators(path)); 117 | } 118 | 119 | QString TransferDialog::getMode() const 120 | { 121 | if (ui.rbCopy->isChecked()) 122 | { 123 | return "Copy"; 124 | } 125 | else if (ui.rbMove->isChecked()) 126 | { 127 | return "Move"; 128 | } 129 | else if (ui.rbSync->isChecked()) 130 | { 131 | return "Sync"; 132 | } 133 | 134 | return QString::null; 135 | } 136 | 137 | QString TransferDialog::getSource() const 138 | { 139 | return ui.textSource->text(); 140 | } 141 | 142 | QString TransferDialog::getDest() const 143 | { 144 | return ui.textDest->text(); 145 | } 146 | 147 | QStringList TransferDialog::getOptions() const 148 | { 149 | QString mode; 150 | 151 | QStringList list; 152 | if (ui.rbCopy->isChecked()) 153 | { 154 | list << "copy"; 155 | mode = "Copy"; 156 | } 157 | else if (ui.rbMove->isChecked()) 158 | { 159 | list << "move"; 160 | mode = "Move"; 161 | } 162 | else if (ui.rbSync->isChecked()) 163 | { 164 | list << "sync"; 165 | mode = "Sync"; 166 | } 167 | 168 | if (mDryRun) 169 | { 170 | list << "--dry-run"; 171 | } 172 | if (ui.rbSync->isChecked()) 173 | { 174 | switch (ui.cbSyncDelete->currentIndex()) 175 | { 176 | case 0: 177 | list << "--delete-during"; 178 | break; 179 | case 1: 180 | list << "--delete-after"; 181 | break; 182 | case 2: 183 | list << "--delete-before"; 184 | break; 185 | } 186 | } 187 | if (ui.checkSkipNewer->isChecked()) 188 | { 189 | list << "--update"; 190 | } 191 | if (ui.checkSkipExisting->isChecked()) 192 | { 193 | list << "--ignore-existing"; 194 | } 195 | if (ui.checkCompare->isChecked()) 196 | { 197 | switch (ui.cbCompare->currentIndex()) 198 | { 199 | case 1: 200 | list << "--checksum"; 201 | break; 202 | case 2: 203 | list << "--ignore-size"; 204 | break; 205 | case 3: 206 | list << "--size-only"; 207 | break; 208 | case 4: 209 | list << "--checksum" << "--ignore-size"; 210 | break; 211 | } 212 | } 213 | if (ui.checkVerbose->isChecked()) 214 | { 215 | list << "--verbose"; 216 | } 217 | if (ui.checkSameFilesystem->isChecked()) 218 | { 219 | list << "--one-file-system"; 220 | } 221 | if (ui.checkDontUpdateModified->isChecked()) 222 | { 223 | list << "--no-update-modtime"; 224 | } 225 | list << "--transfers" << ui.spinTransfers->text(); 226 | list << "--checkers" << ui.spinCheckers->text(); 227 | if (!ui.textBandwidth->text().isEmpty()) 228 | { 229 | list << "--bwlimit" << ui.textBandwidth->text(); 230 | } 231 | if (!ui.textMinSize->text().isEmpty()) 232 | { 233 | list << "--min-size" << ui.textMinSize->text(); 234 | } 235 | if (!ui.textMinAge->text().isEmpty()) 236 | { 237 | list << "--min-age" << ui.textMinAge->text(); 238 | } 239 | if (!ui.textMaxAge->text().isEmpty()) 240 | { 241 | list << "--max-age" << ui.textMaxAge->text(); 242 | } 243 | if (ui.spinMaxDepth->value() != 0) 244 | { 245 | list << "--max-depth" << ui.spinMaxDepth->text(); 246 | } 247 | list << "--contimeout" << (ui.spinConnectTimeout->text() + "s"); 248 | list << "--timeout" << (ui.spinIdleTimeout->text() + "s"); 249 | list << "--retries" << ui.spinRetries->text(); 250 | list << "--low-level-retries" << ui.spinLowLevelRetries->text(); 251 | 252 | if (ui.checkDeleteExcluded->isChecked()) 253 | { 254 | list << "--delete-excluded"; 255 | } 256 | 257 | QString excluded = ui.textExclude->toPlainText().trimmed(); 258 | if (!excluded.isEmpty()) 259 | { 260 | for (auto line : excluded.split('\n')) 261 | { 262 | list << "--exclude" << line; 263 | } 264 | } 265 | 266 | QString extra = ui.textExtra->text().trimmed(); 267 | if (!extra.isEmpty()) 268 | { 269 | for (auto arg : extra.split(' ')) 270 | { 271 | list << arg; 272 | } 273 | } 274 | 275 | list << "--stats" << "1s"; 276 | 277 | list << ui.textSource->text(); 278 | list << ui.textDest->text(); 279 | 280 | return list; 281 | } 282 | 283 | void TransferDialog::done(int r) 284 | { 285 | if (r == QDialog::Accepted) 286 | { 287 | if (mIsDownload) 288 | { 289 | if (ui.textDest->text().isEmpty()) 290 | { 291 | QMessageBox::warning(this, "Warning", "Please enter destination!"); 292 | return; 293 | } 294 | } 295 | else 296 | { 297 | if (ui.textSource->text().isEmpty()) 298 | { 299 | QMessageBox::warning(this, "Warning", "Please enter source!"); 300 | return; 301 | } 302 | } 303 | } 304 | QDialog::done(r); 305 | } 306 | -------------------------------------------------------------------------------- /src/transfer_dialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | #include "ui_transfer_dialog.h" 5 | 6 | class TransferDialog : public QDialog 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | TransferDialog(bool isDownload, const QString& remote, const QDir& path, bool isFolder, QWidget* parent = nullptr); 12 | ~TransferDialog(); 13 | 14 | void setSource(const QString& path); 15 | 16 | QString getMode() const; 17 | QString getSource() const; 18 | QString getDest() const; 19 | QStringList getOptions() const; 20 | 21 | private: 22 | Ui::TransferDialog ui; 23 | 24 | bool mIsDownload; 25 | bool mDryRun = false; 26 | 27 | void done(int r) override; 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | static QString gRclone; 4 | static QString gRcloneConf; 5 | static QString gRclonePassword; 6 | 7 | static QString GetIniFilename() 8 | { 9 | QFileInfo info = qApp->applicationFilePath(); 10 | return info.dir().filePath(info.baseName() + ".ini"); 11 | } 12 | 13 | static bool IsPortableMode() 14 | { 15 | QString ini = GetIniFilename(); 16 | return QFileInfo(ini).exists(); 17 | } 18 | 19 | std::unique_ptr GetSettings() 20 | { 21 | if (IsPortableMode()) 22 | { 23 | return std::unique_ptr(new QSettings(GetIniFilename(), QSettings::IniFormat)); 24 | } 25 | return std::unique_ptr(new QSettings); 26 | } 27 | 28 | void ReadSettings(QSettings* settings, QObject* widget) 29 | { 30 | QString name = widget->objectName(); 31 | if (!name.isEmpty() && settings->contains(name)) 32 | { 33 | if (QRadioButton* obj = qobject_cast(widget)) 34 | { 35 | obj->setChecked(settings->value(name).toBool()); 36 | return; 37 | } 38 | if (QCheckBox* obj = qobject_cast(widget)) 39 | { 40 | obj->setChecked(settings->value(name).toBool()); 41 | return; 42 | } 43 | if (QComboBox* obj = qobject_cast(widget)) 44 | { 45 | obj->setCurrentIndex(settings->value(name).toInt()); 46 | return; 47 | } 48 | if (QSpinBox* obj = qobject_cast(widget)) 49 | { 50 | obj->setValue(settings->value(name).toInt()); 51 | return; 52 | } 53 | if (QLineEdit* obj = qobject_cast(widget)) 54 | { 55 | obj->setText(settings->value(name).toString()); 56 | return; 57 | } 58 | if (QPlainTextEdit* obj = qobject_cast(widget)) 59 | { 60 | int count = settings->beginReadArray(name); 61 | QStringList lines; 62 | lines.reserve(count); 63 | for (int i=0; isetArrayIndex(i); 66 | lines.append(settings->value("value").toString()); 67 | } 68 | settings->endArray(); 69 | 70 | obj->setPlainText(lines.join('\n')); 71 | return; 72 | } 73 | } 74 | 75 | for (auto child : widget->children()) 76 | { 77 | ReadSettings(settings, child); 78 | } 79 | } 80 | 81 | void WriteSettings(QSettings* settings, QObject* widget) 82 | { 83 | QString name = widget->objectName(); 84 | if (QCheckBox* obj = qobject_cast(widget)) 85 | { 86 | settings->setValue(name, obj->isChecked()); 87 | return; 88 | } 89 | if (QComboBox* obj = qobject_cast(widget)) 90 | { 91 | settings->setValue(name, obj->currentIndex()); 92 | return; 93 | } 94 | if (QSpinBox* obj = qobject_cast(widget)) 95 | { 96 | settings->setValue(name, obj->value()); 97 | return; 98 | } 99 | if (QLineEdit* obj = qobject_cast(widget)) 100 | { 101 | if (obj->text().isEmpty()) 102 | { 103 | settings->remove(name); 104 | } 105 | else 106 | { 107 | settings->setValue(name, obj->text()); 108 | } 109 | return; 110 | } 111 | if (QPlainTextEdit* obj = qobject_cast(widget)) 112 | { 113 | QString text = obj->toPlainText().trimmed(); 114 | if (!text.isEmpty()) 115 | { 116 | QStringList lines = text.split('\n'); 117 | settings->beginWriteArray(name, lines.size()); 118 | for (int i=0; isetArrayIndex(i); 121 | settings->setValue("value", lines[i]); 122 | } 123 | settings->endArray(); 124 | } 125 | return; 126 | } 127 | 128 | for (auto child : widget->children()) 129 | { 130 | WriteSettings(settings, child); 131 | } 132 | } 133 | 134 | QStringList GetRcloneConf() 135 | { 136 | if (gRcloneConf.isEmpty()) 137 | { 138 | return QStringList(); 139 | } 140 | 141 | QString conf = gRcloneConf; 142 | if (IsPortableMode() && QFileInfo(conf).isRelative()) 143 | { 144 | conf = QDir(qApp->applicationDirPath()).filePath(conf); 145 | } 146 | return QStringList() << "--config" << conf; 147 | } 148 | 149 | void SetRcloneConf(const QString& rcloneConf) 150 | { 151 | gRcloneConf = rcloneConf; 152 | } 153 | 154 | QString GetRclone() 155 | { 156 | QString rclone = gRclone; 157 | if (IsPortableMode() && QFileInfo(rclone).isRelative()) 158 | { 159 | rclone = QDir(qApp->applicationDirPath()).filePath(rclone); 160 | } 161 | 162 | return rclone; 163 | } 164 | 165 | void SetRclone(const QString& rclone) 166 | { 167 | gRclone = rclone.trimmed(); 168 | } 169 | 170 | void UseRclonePassword(QProcess* process) 171 | { 172 | if (!gRclonePassword.isEmpty()) 173 | { 174 | QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); 175 | env.insert("RCLONE_CONFIG_PASS", gRclonePassword); 176 | process->setProcessEnvironment(env); 177 | } 178 | } 179 | 180 | void SetRclonePassword(const QString& rclonePassword) 181 | { 182 | gRclonePassword = rclonePassword; 183 | } 184 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | 5 | std::unique_ptr GetSettings(); 6 | 7 | void ReadSettings(QSettings* settings, QObject* widget); 8 | void WriteSettings(QSettings* settings, QObject* widget); 9 | 10 | QString GetRclone(); 11 | void SetRclone(const QString& rclone); 12 | 13 | QStringList GetRcloneConf(); 14 | void SetRcloneConf(const QString& rcloneConf); 15 | 16 | void UseRclonePassword(QProcess* process); 17 | void SetRclonePassword(const QString& rclonePassword); 18 | --------------------------------------------------------------------------------