├── .appveyor.yml ├── .github └── workflows │ └── windows-relaese.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── VERSION ├── _config.yml ├── assets ├── appdmg.json ├── background.png ├── background@2x.png ├── rclone-browser-128x128.png ├── rclone-browser-256x256.png ├── rclone-browser-32x32.png ├── rclone-browser-512x512.png ├── rclone-browser-64x64.png ├── rclone-browser.appdata.xml ├── rclone-browser.desktop └── rclone-browser.svg ├── scripts ├── images2ico.py ├── prepare_icons.sh ├── rclone-browser-win-installer.iss ├── release_AppImage.sh ├── release_macOS.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 ├── amazon_cloud_drive_inv.png ├── azureblob.png ├── azureblob_inv.png ├── b2.png ├── b2_inv.png ├── crypt.png ├── crypt_inv.png ├── drive.png ├── drive_inv.png ├── dropbox.png ├── dropbox_inv.png ├── ftp.png ├── ftp_inv.png ├── google_cloud_storage.png ├── google_cloud_storage_inv.png ├── google_photos.png ├── google_photos_inv.png ├── hubic.png ├── hubic_inv.png ├── local.png ├── local_inv.png ├── mega.png ├── mega_inv.png ├── onedrive.png ├── onedrive_inv.png ├── s3.png ├── s3_inv.png ├── s3_old.png ├── s3_old_inv.png ├── sftp.png ├── sftp_inv.png ├── swift.png ├── swift_inv.png ├── unknown.png ├── unknown_inv.png ├── yandex.png └── yandex_inv.png ├── item_model.cpp ├── item_model.h ├── job_options.cpp ├── job_options.h ├── job_widget.cpp ├── job_widget.h ├── job_widget.ui ├── list_of_job_options.cpp ├── list_of_job_options.h ├── 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 | image: 'Visual Studio 2019' 3 | branches: 4 | only: 5 | - master 6 | - kptsky_testing 7 | skip_tags: true 8 | clone_depth: 1 9 | 10 | environment: 11 | matrix: 12 | - configuration: x64 13 | BUILD_ARCH: x64 14 | BUILD_MSVC: msvc2017_64 15 | 16 | - configuration: Win32 17 | BUILD_ARCH: Win32 18 | BUILD_MSVC: msvc2017 19 | 20 | build_script: 21 | - mkdir build 22 | - cd build 23 | - cmake -G "Visual Studio 16 2019" -A %BUILD_ARCH% -DCMAKE_CONFIGURATION_TYPES="Release" -DCMAKE_PREFIX_PATH=C:\Qt\5.13\%BUILD_MSVC% .. 24 | - cmake --build . --config Release 25 | 26 | artifacts: 27 | - path: build/build/Release/RcloneBrowser.exe 28 | 29 | test: off 30 | deploy: off 31 | -------------------------------------------------------------------------------- /.github/workflows/windows-relaese.yml: -------------------------------------------------------------------------------- 1 | name: Windows Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths-ignore: 8 | - 'README.md' 9 | - 'LICENSE' 10 | 11 | pull_request: 12 | branches: 13 | - 'master' 14 | paths-ignore: 15 | - 'README.md' 16 | - 'LICENSE' 17 | 18 | env: 19 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 20 | VCINSTALLDIR: 'C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/' 21 | BUILD_TYPE: 'Release' 22 | TARGET_NAME: 'RcloneBrowser.exe' 23 | 24 | jobs: 25 | build: 26 | name: RcloneBrowser ${{ matrix.config.arch }} 27 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 28 | # You can convert this to a matrix build if you need cross-platform coverage. 29 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 30 | runs-on: windows-2019 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | qt_ver: ['5.15.2'] 35 | qt_target: ['desktop'] 36 | config: 37 | - { 38 | arch: x86, 39 | generator: "-G'Visual Studio 16 2019' -A Win32", 40 | vcpkg_triplet: x86-windows, 41 | qt_arch: win32_msvc2019, 42 | qt_arch_install: msvc2019, 43 | pak_arch: win32 44 | } 45 | - { 46 | arch: x64, 47 | generator: "-G'Visual Studio 16 2019' -A x64", 48 | vcpkg_triplet: x64-windows, 49 | qt_arch: win64_msvc2019_64, 50 | qt_arch_install: msvc2019_64, 51 | pak_arch: win64 52 | } 53 | steps: 54 | - name: Checkout Source code 55 | if: github.event_name == 'push' 56 | uses: actions/checkout@v3 57 | with: 58 | fetch-depth: 0 59 | 60 | - name: Checkout Source code 61 | if: github.event_name == 'pull_request' 62 | uses: actions/checkout@v3 63 | with: 64 | fetch-depth: 0 65 | ref: ${{ github.event.pull_request.head.sha }} 66 | 67 | - name: Cache Qt 68 | id: cache-qt 69 | uses: actions/cache@v3 70 | with: 71 | path: ./build/Qt/${{ matrix.qt_ver }}/${{ matrix.config.qt_arch_install }} 72 | key: ${{ runner.os }}-QtCache/${{ matrix.qt_ver }}/${{ matrix.config.qt_arch }} 73 | 74 | - name: Install Qt 75 | # Installs the Qt SDK 76 | uses: jurplel/install-qt-action@v3 77 | with: 78 | version: ${{ matrix.qt_ver }} 79 | target: ${{ matrix.qt_target }} 80 | arch: ${{ matrix.config.qt_arch }} 81 | dir: '${{ github.workspace }}/build/' 82 | # modules: 'qtscript' 83 | cache: ${{ steps.cache-qt.outputs.cache-hit }} 84 | 85 | - name: Configure CMake 86 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 87 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 88 | run: cmake ${{matrix.config.generator}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} 89 | 90 | - name: Build 91 | shell: cmd 92 | # Build your program with the given configuration 93 | run: | 94 | call "${{ env.VCINSTALLDIR }}\Auxiliary\Build\vcvarsall.bat" ${{ matrix.config.arch }} 95 | echo winSdkDir=%WindowsSdkDir% >> %GITHUB_ENV% 96 | echo winSdkVer=%WindowsSdkVersion% >> %GITHUB_ENV% 97 | echo vcToolsInstallDir=%VCToolsInstallDir% >> %GITHUB_ENV% 98 | echo vcToolsRedistDir=%VCToolsRedistDir% >> %GITHUB_ENV% 99 | 100 | cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 101 | 102 | - name: Package 103 | id: package 104 | shell: pwsh 105 | run: | 106 | $archiveName="RcloneBrowser-${{ matrix.config.pak_arch }}" 107 | New-Item -ItemType Directory $archiveName 108 | Copy-Item ${{ github.workspace }}\build\build\${{ env.BUILD_TYPE }}\${{ env.TARGET_NAME }} $archiveName\ 109 | windeployqt --qmldir . --plugindir $archiveName\ --no-translations --compiler-runtime $archiveName\${{ env.TARGET_NAME }} 110 | $excludeList = @("*.qmlc", "*.ilk", "*.exp", "*.lib", "*.pdb") 111 | Remove-Item -Path $archiveName -Include $excludeList -Recurse -Force 112 | $redistDll="{0}{1}\*.CRT\*.dll" -f $env:vcToolsRedistDir.Trim(), "${{ matrix.config.arch }}" 113 | Copy-Item $redistDll $archiveName\ 114 | # $sdkDll="{0}Redist\{1}ucrt\DLLs\{2}\*.dll" -f $env:winSdkDir.Trim(),$env:winSdkVer.Trim(), "${{ matrix.config.arch }}" 115 | # Copy-Item $sdkDll $archiveName\ 116 | Compress-Archive -Path $archiveName $archiveName'.zip' 117 | Write-Output "packageName=$archiveName" >> $env:GITHUB_OUTPUT 118 | 119 | - name: Artifact Upload 120 | uses: actions/upload-artifact@v3 121 | with: 122 | # name: Windows-artifact-${{ matrix.config.arch }} 123 | # path: ${{ github.workspace }}/build/build/${{ env.BUILD_TYPE }}/*\ 124 | name: ${{ steps.package.outputs.packageName }} 125 | path: ${{ steps.package.outputs.packageName }} 126 | -------------------------------------------------------------------------------- /.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 | /release 11 | /bs0.cmd 12 | /src/*.autosave 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 1 3 | 4 | matrix: 5 | include: 6 | 7 | - os: linux 8 | language: cpp 9 | sudo: false 10 | addons: 11 | apt: 12 | packages: 13 | - qttools5-dev 14 | script: 15 | - mkdir build && cd build 16 | - cmake .. 17 | - cmake --build . 18 | 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | ## [1.8.0][1.8.0] - 2020-02-17 3 | - NEW: http(s) proxy configuration for rclone 4 | - NEW: remotes icons size option selector 5 | - NEW: directories tree display for remotes 6 | - NEW: rclone extra default options for all operations (e.g. --fast-list) 7 | - NEW: added "Public Link" button to remote view 8 | - FIXED: option to show hidden files and folders was not always working as expected 9 | - FIXED: for sftp server default to home user directory (as normal sftp would do) 10 | - FIXED: an issue when on Windows local remote only allowed to browse drive C: 11 | - FIXED: problem using rclone and rclone.conf when path contained spaces 12 | - FIXED: bandwidth box on jobs tab is too small for fast connections 13 | - bunch of usual small tweaks and fixes 14 | 15 | ## [1.7.0][1.7.0] - 2019-11-27 16 | - NEW: built all releases with the latest Qt 5.13.2 17 | - NEW: changed Linux releases format to AppImage only 18 | - NEW: changed macOS release format to dmg image file 19 | - NEW: added installer for Windows releases - implemented using [Inno Setup](https://github.com/jrsoftware/issrc) 20 | - NEW: added Linux i386 release 21 | - NEW: changed macOS release compilation options to make it work on all macOS versions starting with 10.9 22 | - NEW: added portable mode for macOS and Linux 23 | - NEW: on Linux multiple terminals are tried for rclone config ($TERMINAL then gnome-terminal followed by xfce4-terminal, xterm, x-terminal-emulator and konsole) 24 | - NEW: enabled Qt HighDpiScaling - should help people with high DPI monitors 25 | - NEW: added dark mode - configurable via preferences or system setting (newer macOS) - thank you @noaione for initial PR 26 | - changed preferences window - added tabs to create more space for new options 27 | - fixed Windows portable mode 28 | - fixed mount/unmount on FreeBSD 29 | - disabled mount on OpenBSD and NetBSD (as not supported by rclone) 30 | - updated build and install for Linux - now all files will be installed in /usr/local root 31 | - fixed possible crashes when old rclone is used (with different version information output) 32 | - fixed an issue with long file names leading sometimes to inaccurate transfer progress bar display 33 | - added additional info to file progress bar tooltip - individual file stats 34 | - changed program icon 35 | - bunch of usual small tweaks and fixes 36 | 37 | ## [1.6.0][1.6.0] - 2019-10-27 38 | - fixed Windows mount/unmount (requires rclone v1.50+) 39 | - Rclone Browser checks now for used rclone version (mount is disabled in Windows if rclone 1.37 (by DinCahill) 85 | - Add a Public Link option to the right-click menu (by DinCahill) 86 | - Add preference: Show hidden files and folders (by DinCahill) 87 | - Add Mega icon (by DinCahill) 88 | - Refresh when Shared is toggled (by DinCahill) 89 | - Disable Upload button for Shared (by DinCahill) 90 | - Support for shared Google Drive files. Enable the checkbox when you open a remote, and all rclone commands will be passed --drive-shared-with-me (by DinCahill) 91 | - Set cache mode for mounts (by DinCahill) 92 | - Fixed missing leading / in path (required for some SFTP servers) (by DinCahill) 93 | 94 | ## [1.2][1.2] - 2017-03-11 95 | - Calculate size of folders, issue #4 96 | - Copy transfer command to clipboard, issue #20 97 | - Support custom .rclone.conf location, #21 98 | - Export list of files, issue #27 99 | - Bugfix for folder refresh not working after rename, issue #30 100 | - Remember empty text fields in transfer dialog, issue #32 101 | - Error message when too old rclone version is selected 102 | - Support portable mode, issue #28 103 | - Create .deb packages, issue #26 104 | 105 | ## [1.1][1.1] - 2017-01-31 106 | - Added `--transfer` option in UI, issue #1 107 | - Supports encrypted `.rclone.conf` configuration file, issue #2 108 | - Fixed crash when canceling active stream 109 | - Added ETA tooltip for transfer progress bars 110 | - Allow to specify extra arguments for rclone, issue #7 111 | - Fix for browsing Hubic remotes, issue #10 112 | - Support high-dpi mode for macOS 113 | 114 | ## [1.0.0][1.0.0] - 2017-01-29 115 | - Allows to browse and modify any rclone remote, including encrypted ones 116 | - Uses same configuration file as rclone, no extra configuration required 117 | - Simultaneously navigate multiple repositories in separate tabs 118 | - Lists files hierarchically with file name, size and modify date 119 | - All rclone commands are executed asynchronously, no freezing GUI 120 | - File hierarchy is lazily cached in memory, for faster traversal of folders 121 | - Allows to upload, download, create new folders, rename or delete files and folders 122 | - Can process multiple upload or download jobs in background 123 | - Drag & drop support for dragging files from local file explorer for uploading 124 | - Streaming media files for playback in player like mpv or similar 125 | - Mount and unmount folders on macOS and GNU/Linux 126 | - Optionally minimizes to tray, with notifications when upload/download finishes 127 | 128 | [1.8.0]: https://github.com/kapitainsky/RcloneBrowser/releases/tag/1.8.0 129 | [1.7.0]: https://github.com/kapitainsky/RcloneBrowser/releases/tag/1.7.0 130 | [1.6.0]: https://github.com/kapitainsky/RcloneBrowser/releases/tag/1.6.0 131 | [1.5.3]: https://github.com/kapitainsky/RcloneBrowser/releases/tag/1.5.3 132 | [1.5.2]: https://github.com/kapitainsky/RcloneBrowser/releases/tag/1.5.2 133 | [1.5.1]: https://github.com/kapitainsky/RcloneBrowser/releases/tag/1.5.1 134 | [1.5]: https://github.com/kapitainsky/RcloneBrowser/releases/tag/1.5 135 | [1.4.1]: https://github.com/kapitainsky/RcloneBrowser/releases/tag/1.4.1 136 | [1.4]: https://github.com/kapitainsky/RcloneBrowser/releases/tag/1.4 137 | [1.2]: https://github.com/mmozeiko/RcloneBrowser/releases/tag/1.2 138 | [1.1]: https://github.com/mmozeiko/RcloneBrowser/releases/tag/1.1 139 | [1.0.0]: https://github.com/mmozeiko/RcloneBrowser/releases/tag/1.0.0 140 | -------------------------------------------------------------------------------- /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 | MIT License 2 | 3 | Copyright (c) 2019-2020 kapitainsky (Rclone Browser) 4 | Copyright (c) 2017 Martins Mozeiko (Rclone Browser) 5 | Copyright (c) 2015 Mankalas (qcron) 6 | Copyright (c) 2013 Linus Unnebäck (node-appdmg) 7 | Copyright (c) 1997-2020 Jordan Russell (Inno Setup - Windows Installer) 8 | Portions Copyright (c) 2000-2020 Martijn (Inno Setup - Windows Installer) 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.8.0 -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /assets/appdmg.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Rclone Browser install", 3 | "icon": "../src/icon.icns", 4 | "icon-size": 128, 5 | "background": "background.png", 6 | "window": { 7 | "position": { 8 | "x": 500, 9 | "y": 300 10 | }, 11 | "size": { 12 | "width": 576, 13 | "height": 384 14 | } 15 | }, 16 | "contents": [ 17 | { "x": 436, "y": 215, "type": "link", "path": "/Applications" }, 18 | { "x": 140, "y": 215, "type": "file", "path": "../release/Rclone Browser.app" } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/assets/background.png -------------------------------------------------------------------------------- /assets/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/assets/background@2x.png -------------------------------------------------------------------------------- /assets/rclone-browser-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/assets/rclone-browser-128x128.png -------------------------------------------------------------------------------- /assets/rclone-browser-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/assets/rclone-browser-256x256.png -------------------------------------------------------------------------------- /assets/rclone-browser-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/assets/rclone-browser-32x32.png -------------------------------------------------------------------------------- /assets/rclone-browser-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/assets/rclone-browser-512x512.png -------------------------------------------------------------------------------- /assets/rclone-browser-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/assets/rclone-browser-64x64.png -------------------------------------------------------------------------------- /assets/rclone-browser.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.kapitainsky.rclone_browser 5 | MIT 6 | MIT 7 | Rclone Browser 8 | Simple cross platfrom GUI for rclone command line 9 | 10 |

Rclone Browser is a cross-platform Qt5 GUI for Rclone, a command line tool to synchronize (and mount) files from remote cloud storage services like Google Drive, OneDrive, Nextcloud, Dropbox, Amazon Drive and S3, Mega, and others. Use it to copy a file from one cloud storage service to another, from a cloud storage to your system or the other way around, and to mount some cloud storage on your system with a single click.

11 |

- Allows to browse and modify any rclone remote, including encrypted ones

12 |

- Uses same configuration file as rclone, no extra configuration required

13 |

- Supports custom location and encryption for .rclone.conf configuration file

14 |

- Simultaneously navigate multiple repositories in separate tabs

15 |

- Lists files hierarchically with file name, size and modify date

16 |

- All rclone commands are executed asynchronously, no freezing GUI

17 |

- File hierarchy is lazily cached in memory, for faster traversal of folders

18 |

- Allows to upload, download, create new folders, rename or delete files and folders

19 |

- Allows to calculate size of folder, export list of files and copy rclone command to clipboard

20 |

- Can process multiple upload or download jobs in background

21 |

- Drag and drop support for dragging files from local file explorer for uploading

22 |

- Streaming media files for playback in player like vlc or similar

23 |

- Mount and unmount folders on macOS, GNU/Linux and Windows (for Windows requires winfsp)

24 |

- Optionally minimizes to tray, with notifications when upload/download finishes

25 |

- Supports portable mode (create .ini file next to executable with same name), rclone and .rclone.conf path now can be relative to executable

26 |

- Supports drive-shared-with-me (Google Drive specific)

27 |

- For remotes supporting public link sharing has an option (right-click menu) to fetch it

28 |

- Supports tasks. Created jobs can be saved and run or edited later

29 |

- Configurable dark mode for all systems

30 |
31 | 32 | 33 | https://github.com/kapitainsky/RcloneBrowser/wiki/images/IMGdefault.png 34 | 35 | 36 | https://github.com/kapitainsky/RcloneBrowser/wiki/images/IMGtransfer.png 37 | 38 | 39 | https://github.com/kapitainsky/RcloneBrowser/ 40 | https://github.com/kapitainsky/RcloneBrowser/issues/ 41 | https://github.com/kapitainsky/RcloneBrowser/wiki 42 | https://github.com/kapitainsky/RcloneBrowser/issues/ 43 | 44 | 45 | 46 | Dariusz Bogdanski and contributors 47 | dariuszb@me.com 48 | 49 | rclone-browser 50 | 51 |
52 | -------------------------------------------------------------------------------- /assets/rclone-browser.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Rclone Browser 3 | Comment=Simple cross-platform GUI for rclone 4 | Exec=rclone-browser 5 | Icon=rclone-browser 6 | Terminal=false 7 | Type=Application 8 | Categories=Network; 9 | StartupNotify=false 10 | -------------------------------------------------------------------------------- /assets/rclone-browser.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 53 | 54 | 57 | 60 | 67 | 68 | 70 | 76 | 77 | 78 | 79 | 82 | 85 | 88 | 93 | 94 | 95 | 96 | 99 | 104 | 105 | 106 | 107 | 110 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /scripts/images2ico.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # to install "pip3 install Pillow" 4 | 5 | # packs multiple images (bmp/png/...) into ico file 6 | # width and height of images must be <= 256 7 | # pixel format of images must be 32-bit RGBA 8 | 9 | import argparse 10 | import struct 11 | import os 12 | from PIL import Image # https://python-pillow.org/ 13 | 14 | def pack(output, inp): 15 | count = len(inp) 16 | 17 | with open(output, "wb") as f: 18 | f.write(struct.pack("HHH", 0, 1, count)) 19 | offset = struct.calcsize("HHH") + struct.calcsize("BBBBHHII")*count 20 | 21 | for i in inp: 22 | size = os.stat(i).st_size 23 | img = Image.open(i) 24 | w = 0 if img.width == 256 else img.width 25 | h = 0 if img.height == 256 else img.height 26 | f.write(struct.pack("BBBBHHII", w, h, 0, 0, 1, 32, size, offset)) 27 | offset += size 28 | 29 | for i in inp: 30 | f.write(open(i, "rb").read()) 31 | 32 | if __name__ == "__main__": 33 | ap = argparse.ArgumentParser(description="pack multiple images into ico file") 34 | ap.add_argument("-o", "--out", help="output file") 35 | ap.add_argument("input", type=str, nargs='+', help="input images") 36 | args = ap.parse_args() 37 | pack(args.out, args.input) 38 | -------------------------------------------------------------------------------- /scripts/prepare_icons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # requires following programs: 4 | # brew install optipng 5 | # brew install imagemagick 6 | # brew install makeicns 7 | 8 | set -eu 9 | 10 | # input file: square canvas svg 11 | if [ ! -f "../assets/rclone-browser.svg" ]; then 12 | echo "Input file ../assets/rclone-browser.svg is missing" 13 | exit 14 | fi 15 | 16 | echo "Converting svg to png" 17 | convert -density 2400 -resize 512x512 -background none ../assets/rclone-browser.svg rclone-browser.png 18 | 19 | echo 20 | echo "Creating 512 256 128 64 32 16 image sizes" 21 | SIZES=(512 256 128 64 32 16) 22 | for s in "${SIZES[@]}" 23 | do 24 | convert rclone-browser.png -resize "$s" rclone-browser-"$s"x"$s".png 25 | optipng -o7 -strip all rclone-browser-"$s"x"$s".png 26 | done 27 | 28 | echo 29 | echo "Creating ico" 30 | ./images2ico.py -o ../src/icon.ico rclone-browser-256x256.png rclone-browser-128x128.png rclone-browser-64x64.png rclone-browser-32x32.png rclone-browser-16x16.png 31 | 32 | echo 33 | echo "Creating icns" 34 | makeicns -out ../src/icon.icns -512 rclone-browser-512x512.png -256 rclone-browser-256x256.png -128 rclone-browser-128x128.png -64 rclone-browser-64x64.png -32 rclone-browser-32x32.png -16 rclone-browser-16x16.png 35 | 36 | # clean temporary files 37 | rm rclone-browser-16x16.png 38 | rm rclone-browser.png 39 | 40 | # used for window icon 41 | cp rclone-browser-128x128.png ../src/icon.png 42 | 43 | # used for linux instalation 44 | mv rclone-browser-32x32.png ../assets/ 45 | mv rclone-browser-64x64.png ../assets/ 46 | mv rclone-browser-128x128.png ../assets/ 47 | mv rclone-browser-256x256.png ../assets/ 48 | mv rclone-browser-512x512.png ../assets/ 49 | -------------------------------------------------------------------------------- /scripts/rclone-browser-win-installer.iss: -------------------------------------------------------------------------------- 1 | 2 | #define MyAppName "Rclone Browser" 3 | #define MyAppPublisher "kapitainsky" 4 | #define MyAppURL "https://github.com/kapitainsky/RcloneBrowser" 5 | #define MyAppExeName "RcloneBrowser.exe" 6 | 7 | [Setup] 8 | ; We will use two different IDs - for 64bit and 32bit 9 | ;64bit: AppId={{0AF9BF43-8D44-4AFF-AE60-6CECF1BF0D31} 10 | ;32bit: AppId={{5644ED3A-6028-47C0-9796-29548EF7CEA3} 11 | AppId={#MyAppId} 12 | AppName={#MyAppName} 13 | AppVersion={#MyAppVersion} 14 | ;AppVerName={#MyAppName} {#MyAppVersion} 15 | AppPublisher={#MyAppPublisher} 16 | AppPublisherURL={#MyAppURL} 17 | AppSupportURL={#MyAppURL} 18 | AppUpdatesURL={#MyAppURL} 19 | DefaultDirName={autopf}\{#MyAppName} 20 | DefaultGroupName={#MyAppName} 21 | AllowNoIcons=yes 22 | ; The [Icons] "quicklaunchicon" entry uses {userappdata} but its [Tasks] entry has a proper IsAdminInstallMode Check. 23 | UsedUserAreasWarning=no 24 | LicenseFile=..\release\{#MyAppDir}\License.txt 25 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 26 | ;PrivilegesRequired=lowest 27 | PrivilegesRequiredOverridesAllowed=dialog 28 | SetupIconFile=..\src\icon.ico 29 | UninstallDisplayIcon={app}\{#MyAppExeName} 30 | Compression=lzma 31 | SolidCompression=yes 32 | WizardStyle=modern 33 | 34 | #if MyAppArch=="x64" 35 | ; "ArchitecturesAllowed=x64" specifies that Setup cannot run on 36 | ; anything but x64. 37 | ArchitecturesAllowed=x64 38 | ; "ArchitecturesInstallIn64BitMode=x64" requests that the install be 39 | ; done in "64-bit mode" on x64, meaning it should use the native 40 | ; 64-bit Program Files directory and the 64-bit view of the registry. 41 | ArchitecturesInstallIn64BitMode=x64 42 | #endif 43 | 44 | [Languages] 45 | Name: "english"; MessagesFile: "compiler:Default.isl" 46 | 47 | [Tasks] 48 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 49 | Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode 50 | 51 | [Files] 52 | Source: "..\release\{#MyAppDir}\RcloneBrowser.exe"; DestDir: "{app}"; Flags: ignoreversion 53 | Source: "..\release\{#MyAppDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 54 | 55 | [Icons] 56 | Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 57 | Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" 58 | Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" 59 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 60 | Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon 61 | 62 | [Run] 63 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 64 | 65 | -------------------------------------------------------------------------------- /scripts/release_AppImage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # x86_64 build on CentOS 7.7 6 | # gcc 7 installed 7 | # sudo yum install -y centos-release-scl 8 | # sudo yum install -y devtoolset-7-gcc* 9 | # run below command before build 10 | # scl enable devtoolset-7 bash 11 | 12 | # newer cmake is required than one included in CentOS 7 13 | # download from http://www.cmake.org/download 14 | # sudo mkdir /opt/cmake 15 | # sudo sh cmake-$version.$build-Linux-x86_64.sh --prefix=/opt/cmake 16 | 17 | if [ $(arch) = "x86_64" ]; then 18 | CMAKE="/opt/cmake/bin/cmake" 19 | fi 20 | 21 | # i686 build on Ubuntu 16.04 LTS 22 | 23 | # armv7l build on raspbian stretch 24 | 25 | # Qt path and flags set in env e.g.: 26 | # export PATH="/opt/Qt/5.14.0/bin/:$PATH" 27 | # export CPPFLAGS="-I/opt/Qt/5.14.0/bin/include/" 28 | # export LDFLAGS="-L/opt/Qt/5.14.0/bin/lib/" 29 | # export LD_LIBRARY_PATH="/opt/Qt/5.14.0/bin/lib/:$LD_LIBRARY_PATH" 30 | 31 | # for x86_64 and i686 platform 32 | # Qt 5.14.0 uses openssl 1.1 and some older distros still use 1.0 33 | # we build openssl 1.1.1d from source using following setup: 34 | # ./config shared --prefix=/opt/openssl-1.1.1/ && make --jobs=`nproc --all` && sudo make install 35 | # and add to build env 36 | # export LD_LIBRARY_PATH="/opt/openssl-1.1.1/lib/:$LD_LIBRARY_PATH" 37 | 38 | if [ "$1" = "SIGN" ]; then 39 | export SIGN="1" 40 | fi 41 | 42 | # check gcc version on Centos 43 | if [ $(arch) = "x86_64" ]; then 44 | currentver="$(gcc -dumpversion)" 45 | if [ "${currentver:0:1}" -lt "7" ]; then 46 | echo "gcc version 7 or newer required" 47 | echo "on Cetos 7 run" 48 | echo "scl enable devtoolset-7 bash" 49 | exit 50 | fi 51 | fi 52 | 53 | # building AppImage in temporary directory to keep system clean 54 | # use RAM disk if possible (as in: not building on CI system like Travis, and RAM disk is available) 55 | if [ "$CI" == "" ] && [ -d /dev/shm ]; then 56 | TEMP_BASE=/dev/shm 57 | else 58 | TEMP_BASE=/tmp 59 | fi 60 | 61 | # we run it from our project scripts folder 62 | ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"/.. 63 | VERSION=$(cat "$ROOT"/VERSION)-$(git rev-parse --short HEAD) 64 | # linuxdeploy uses $VERSION env variable for AppImage name 65 | export VERSION=$VERSION 66 | BUILD="$ROOT"/build 67 | TARGET=rclone-browser-$VERSION.AppImage 68 | 69 | # clean AppImage temporary folder 70 | if [ -d "$TEMP_BASE/$TARGET" ]; then 71 | rm -rf "$TEMP_BASE/$TARGET" 72 | fi 73 | mkdir "$TEMP_BASE/$TARGET" 74 | 75 | # clean build folder 76 | if [ -d "$BUILD" ]; then 77 | rm -rf "$BUILD" 78 | fi 79 | mkdir "$BUILD" 80 | 81 | # create release folder if does not exist 82 | mkdir -p "$ROOT"/release 83 | 84 | # clean current version previous build 85 | if [ $(arch) = "armv7l" ] && [ -f "$ROOT"/release/rclone-browser-"$VERSION"-armhf.AppImage ]; then 86 | rm "$ROOT"/release/rclone-browser-"$VERSION"-raspberrypi-armhf.AppImage 87 | fi 88 | 89 | if [ $(arch) = "i686" ] && [ -f "$ROOT"/release/rclone-browser-"$VERSION"-i386.AppImage ]; then 90 | rm "$ROOT"/release/rclone-browser-"$VERSION"-linux-i386.AppImage 91 | fi 92 | 93 | if [ $(arch) = "x86_64" ] && [ -f "$ROOT"/release/rclone-browser-"$VERSION"-x86_64.AppImage ]; then 94 | rm "$ROOT"/release/rclone-browser-"$VERSION"-linux-x86_64.AppImage 95 | fi 96 | 97 | # build and install to temporary AppDir folder 98 | cd "$BUILD" 99 | 100 | if [ $(arch) = "armv7l" ]; then 101 | # more threads need swap on 1GB RAM RPi 102 | cmake .. -DCMAKE_INSTALL_PREFIX=/usr 103 | make -j 2 104 | fi 105 | 106 | if [ $(arch) = "x86_64" ]; then 107 | "$CMAKE" .. -DCMAKE_INSTALL_PREFIX=/usr 108 | make --jobs=$(nproc --all) 109 | fi 110 | 111 | if [ $(arch) = "i686" ]; then 112 | cmake .. -DCMAKE_INSTALL_PREFIX=/usr 113 | make --jobs=$(nproc --all) 114 | fi 115 | 116 | make install DESTDIR="$TEMP_BASE"/"$TARGET"/AppDir 117 | 118 | # prepare AppImage 119 | cd "$TEMP_BASE/$TARGET" 120 | 121 | # metainfo file 122 | #mkdir $TEMP_BASE/$TARGET/AppDir/usr/share/metainfo 123 | #cp $ROOT/assets/rclone-browser.appdata.xml $TEMP_BASE/$TARGET/AppDir/usr/share/metainfo/ 124 | 125 | # copy info files to AppImage 126 | cp "$ROOT"/README.md "$TEMP_BASE"/"$TARGET"/AppDir/Readme.md 127 | cp "$ROOT"/CHANGELOG.md "$TEMP_BASE"/"$TARGET"/AppDir/Changelog.md 128 | cp "$ROOT"/LICENSE "$TEMP_BASE"/"$TARGET"/AppDir/License.txt 129 | 130 | # https://github.com/linuxdeploy/linuxdeploy 131 | # https://github.com/linuxdeploy/linuxdeploy-plugin-qt 132 | linuxdeploy --appdir AppDir --desktop-file=AppDir/usr/share/applications/rclone-browser.desktop --plugin qt 133 | #linuxdeploy-plugin-qt --appdir AppDir 134 | 135 | if [ $(arch) != "armv7l" ] 136 | then 137 | # we add openssl 1.1.1 libs needed for distros still using openssl 1.0 138 | cp /opt/openssl-1.1.1/lib/libssl.so.1.1 ./AppDir/usr/bin/ 139 | cp /opt/openssl-1.1.1/lib/libcrypto.so.1.1 ./AppDir/usr/bin/ 140 | fi 141 | 142 | # https://github.com/linuxdeploy/linuxdeploy-plugin-appimage 143 | linuxdeploy-plugin-appimage --appdir=AppDir 144 | 145 | # raspberry pi build 146 | if [ $(arch) = "armv7l" ]; then 147 | rename 's/armhf/raspberrypi-armhf/' Rclone_Browser* 148 | rename 's/Rclone_Browser/rclone-browser/' Rclone_Browser* 149 | fi 150 | 151 | # x86 build 152 | if [ $(arch) = "i686" ]; then 153 | rename 's/i386/linux-i386/' Rclone_Browser* 154 | rename 's/Rclone_Browser/rclone-browser/' Rclone_Browser* 155 | fi 156 | 157 | # x86_64 build 158 | if [ $(arch) = "x86_64" ]; then 159 | rename x86_64 linux-x86_64 Rclone_Browser* 160 | rename Rclone_Browser rclone-browser Rclone_Browser* 161 | fi 162 | 163 | cp ./*AppImage "$ROOT"/release/ 164 | 165 | # clean AppImage temporary folder 166 | cd .. 167 | rm -rf "$TARGET" 168 | -------------------------------------------------------------------------------- /scripts/release_macOS.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | QTDIR=/usr/local/opt/qt 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 | DMG=rclone-browser-$VERSION-macos 12 | APP="$TARGET"/"Rclone Browser.app" 13 | 14 | # clean from previous builds (if for the same version in releases) 15 | if [ -d "$BUILD" ]; then 16 | rm -rf "$BUILD" 17 | fi 18 | if [ -d "$ROOT"/release/"$TARGET" ]; then 19 | rm -rf "$ROOT"/release/"$TARGET"* 20 | fi 21 | if [ -f "$ROOT"/release/"$DMG".dmg ]; then 22 | rm "$ROOT"/release/"$DMG".dmg 23 | fi 24 | if [ -d "$ROOT"/release/"Rclone Browser.app" ]; then 25 | rm -rf "$ROOT"/release/"Rclone Browser.app" 26 | fi 27 | 28 | 29 | 30 | mkdir -p "$BUILD" 31 | cd "$BUILD" 32 | # brew install cmake qt5 33 | cmake .. -DCMAKE_PREFIX_PATH="$QTDIR" -DCMAKE_BUILD_TYPE=Release 34 | # brew install coreutils 35 | make --jobs=$(nproc --all) 36 | cd build 37 | "$QTDIR"/bin/macdeployqt rclone-browser.app -executable="rclone-browser.app/Contents/MacOS/rclone-browser" -qmldir=../src/ 38 | cd ../.. 39 | 40 | 41 | mkdir -p release 42 | cd release 43 | mkdir "$TARGET" 44 | cp -R "$BUILD"/build/rclone-browser.app "$APP" 45 | cp "$ROOT"/README.md "$APP"/Readme.md 46 | cp "$ROOT"/CHANGELOG.md "$APP"/Changelog.md 47 | cp "$ROOT"/LICENSE "$APP"/License.txt 48 | mv "$APP"/Contents/MacOS/rclone-browser "$APP"/Contents/MacOS/"Rclone Browser" 49 | 50 | sed -i .bak 's/rclone-browser/Rclone Browser/g' "$APP"/Contents/Info.plist 51 | rm "$APP"/Contents/*.bak 52 | 53 | cat >"$APP"/Contents/MacOS/qt.conf <nul 71 | 72 | copy "%ROOT%\README.md" "%TARGET%\Readme.md" 73 | copy "%ROOT%\CHANGELOG.md" "%TARGET%\Changelog.md" 74 | copy "%ROOT%\LICENSE" "%TARGET%\License.txt" 75 | copy "%BUILD%\RcloneBrowser.exe" "%TARGET%" 76 | 77 | windeployqt.exe --no-translations --no-angle --no-compiler-runtime --no-svg "%TARGET%\RcloneBrowser.exe" 78 | rd /s /q "%TARGET%\imageformats" 79 | 80 | rem include all MSVCruntime dlls 81 | copy "%VCToolsRedistDir%\%ARCH%\Microsoft.VC142.CRT\msvcp140.dll" "%TARGET%\" 82 | copy "%VCToolsRedistDir%\%ARCH%\Microsoft.VC142.CRT\vcruntime140*.dll" "%TARGET%\" 83 | 84 | rem for Windows 32 bits build include relevant openssl libraries 85 | if "%ARCH%" == "x86" ( 86 | copy "c:\Program Files (x86)\openssl-1.1.1d-win32\libssl-1_1.dll" "%TARGET%\" 87 | copy "c:\Program Files (x86)\openssl-1.1.1d-win32\libcrypto-1_1.dll" "%TARGET%\" 88 | ) 89 | 90 | ( 91 | echo [Paths] 92 | echo Prefix = . 93 | echo LibraryExecutables = . 94 | echo Plugins = . 95 | )>"%TARGET%\qt.conf" 96 | 97 | rem https://www.7-zip.org/ 98 | rem create zip archive of all files 99 | "c:\Program Files\7-Zip\7z.exe" a -mx=9 -r -tzip "%TARGET%.zip" "%TARGET%" 100 | 101 | rem create proper installer 102 | rem Inno Setup installer by https://github.com/jrsoftware/issrc 103 | rem in case user wants to install both 32bits and 64bits versions we need two AppId 104 | rem 64bits ;AppId={{0AF9BF43-8D44-4AFF-AE60-6CECF1BF0D31} 105 | rem 32bits ;AppId={{5644ED3A-6028-47C0-9796-29548EF7CEA3} 106 | if "%ARCH%" == "x86" ( 107 | "c:\Program Files (x86)\Inno Setup 6"\iscc "/dMyAppVersion=%VERSION%" "/dMyAppId={{5644ED3A-6028-47C0-9796-29548EF7CEA3}" "/dMyAppDir=rclone-browser-%VERSION_COMMIT%-windows-32-bit" "/dMyAppArch=x86" /O"../release" /F"rclone-browser-%VERSION_COMMIT%-windows-32-bit" rclone-browser-win-installer.iss 108 | ) else ( 109 | "c:\Program Files (x86)\Inno Setup 6"\iscc "/dMyAppVersion=%VERSION%" "/dMyAppId={{0AF9BF43-8D44-4AFF-AE60-6CECF1BF0D31}" "/dMyAppDir=rclone-browser-%VERSION_COMMIT%-windows-64-bit" "/dMyAppArch=x64" /O"../release" /F"rclone-browser-%VERSION_COMMIT%-windows-64-bit" rclone-browser-win-installer.iss 110 | ) 111 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8) 2 | 3 | if (NOT ${CMAKE_VERSION} VERSION_LESS "3.0.0") 4 | cmake_policy(SET CMP0028 NEW) 5 | endif() 6 | if (NOT ${CMAKE_VERSION} VERSION_LESS "2.8.11") 7 | cmake_policy(SET CMP0020 NEW) 8 | endif() 9 | 10 | if(WIN32) 11 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /WX /wd4100 /wd4189") 12 | else() 13 | add_definitions("-pedantic -Wall -Wextra -Werror -std=c++11") 14 | endif() 15 | 16 | if (APPLE) 17 | set(CMAKE_OSX_DEPLOYMENT_TARGET 10.9) 18 | endif() 19 | 20 | project(rclone-browser) 21 | FIND_PACKAGE(Qt5Core REQUIRED) 22 | FIND_PACKAGE(Qt5Gui REQUIRED) 23 | FIND_PACKAGE(Qt5Widgets REQUIRED) 24 | FIND_PACKAGE(Qt5Network REQUIRED) 25 | 26 | if (WIN32) 27 | FIND_PACKAGE(Qt5WinExtras REQUIRED) 28 | elseif(APPLE) 29 | FIND_PACKAGE(Qt5MacExtras REQUIRED) 30 | endif() 31 | 32 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 33 | 34 | set(UI 35 | main_window.ui 36 | remote_widget.ui 37 | transfer_dialog.ui 38 | export_dialog.ui 39 | progress_dialog.ui 40 | job_widget.ui 41 | mount_widget.ui 42 | stream_widget.ui 43 | preferences_dialog.ui 44 | ) 45 | 46 | set(MOC 47 | main_window.h 48 | remote_widget.h 49 | transfer_dialog.h 50 | export_dialog.h 51 | progress_dialog.h 52 | job_widget.h 53 | mount_widget.h 54 | stream_widget.h 55 | preferences_dialog.h 56 | icon_cache.h 57 | list_of_job_options.h 58 | item_model.h 59 | ) 60 | 61 | set(OTHER 62 | pch.h 63 | utils.h 64 | job_options.h 65 | ) 66 | 67 | set(SOURCE 68 | pch.cpp 69 | main.cpp 70 | main_window.cpp 71 | remote_widget.cpp 72 | transfer_dialog.cpp 73 | export_dialog.cpp 74 | progress_dialog.cpp 75 | job_widget.cpp 76 | mount_widget.cpp 77 | stream_widget.cpp 78 | preferences_dialog.cpp 79 | icon_cache.cpp 80 | item_model.cpp 81 | utils.cpp 82 | job_options.cpp 83 | list_of_job_options.cpp 84 | ) 85 | 86 | if(WIN32) 87 | set(OTHER ${OTHER} resources.rc) 88 | elseif(APPLE) 89 | set(OTHER ${OTHER} osx_helper.h) 90 | set(SOURCE ${SOURCE} osx_helper.mm) 91 | endif() 92 | 93 | set(QRC resources.qrc) 94 | 95 | add_definitions(-DRCLONE_BROWSER_VERSION="${RCLONE_BROWSER_VERSION}") 96 | 97 | qt5_wrap_ui(UI_OUT ${UI}) 98 | qt5_wrap_cpp(MOC_OUT ${MOC}) 99 | qt5_add_resources(QRC_OUT ${QRC} OPTIONS "-no-compress") 100 | 101 | source_group("" FILES ${SOURCE} ${MOC} ${UI} ${QRC} ${OTHER}) 102 | source_group("Generated" FILES ${MOC_OUT} ${UI_OUT} ${MOC_OUT} ${QRC_OUT}) 103 | 104 | MACRO(ADD_MSVC_PRECOMPILED_HEADER PrecompiledHeader PrecompiledSource SourcesVar) 105 | IF(MSVC) 106 | GET_FILENAME_COMPONENT(PrecompiledBasename ${PrecompiledHeader} NAME_WE) 107 | SET(PrecompiledBinary "${CMAKE_CURRENT_BINARY_DIR}/${PrecompiledBasename}.pch") 108 | SET(Sources ${${SourcesVar}}) 109 | 110 | SET_SOURCE_FILES_PROPERTIES(${PrecompiledSource} 111 | PROPERTIES COMPILE_FLAGS "/Yc\"${PrecompiledHeader}\" /Fp\"${PrecompiledBinary}\"" 112 | OBJECT_OUTPUTS "${PrecompiledBinary}") 113 | SET_SOURCE_FILES_PROPERTIES(${Sources} 114 | PROPERTIES COMPILE_FLAGS "/Yu\"${PrecompiledHeader}\" /FI\"${PrecompiledHeader}\" /Fp\"${PrecompiledBinary}\"" 115 | OBJECT_DEPENDS "${PrecompiledBinary}") 116 | # Add precompiled header to SourcesVar 117 | LIST(APPEND ${SourcesVar} ${PrecompiledSource}) 118 | ENDIF(MSVC) 119 | ENDMACRO(ADD_MSVC_PRECOMPILED_HEADER) 120 | 121 | ADD_MSVC_PRECOMPILED_HEADER(pch.h pch.cpp SOURCE) 122 | ADD_MSVC_PRECOMPILED_HEADER(pch.h pch.cpp MOC_OUT) 123 | 124 | if(WIN32) 125 | add_executable(RcloneBrowser WIN32 ${SOURCE} ${BACKEND} ${OTHER} ${MOC} ${MOC_OUT} ${UI_OUT} ${MOC_OUT} ${QRC_OUT}) 126 | target_link_libraries(RcloneBrowser Qt5::Widgets Qt5::Network Qt5::WinExtras) 127 | elseif(APPLE) 128 | set_source_files_properties(icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") 129 | add_executable(rclone-browser MACOSX_BUNDLE ${SOURCE} ${BACKEND} ${OTHER} ${MOC} ${MOC_OUT} ${UI_OUT} ${MOC_OUT} ${QRC_OUT} icon.icns) 130 | target_link_libraries(rclone-browser Qt5::Widgets Qt5::Network Qt5::MacExtras ${COCOA_LIB}) 131 | set_target_properties(rclone-browser PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist") 132 | 133 | else() 134 | add_executable(rclone-browser ${SOURCE} ${BACKEND} ${OTHER} ${MOC} ${MOC_OUT} ${UI_OUT} ${MOC_OUT} ${QRC_OUT}) 135 | target_link_libraries(rclone-browser Qt5::Widgets Qt5::Network) 136 | 137 | install(TARGETS rclone-browser RUNTIME DESTINATION bin) 138 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../assets/rclone-browser.svg" DESTINATION "share/icons/hicolor/scalable/apps" RENAME "rclone-browser.svg") 139 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../assets/rclone-browser-32x32.png" DESTINATION "share/icons/hicolor/32x32/apps" RENAME "rclone-browser.png") 140 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../assets/rclone-browser-64x64.png" DESTINATION "share/icons/hicolor/64x64/apps" RENAME "rclone-browser.png") 141 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../assets/rclone-browser-128x128.png" DESTINATION "share/icons/hicolor/128x128/apps" RENAME "rclone-browser.png") 142 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../assets/rclone-browser-256x256.png" DESTINATION "share/icons/hicolor/256x256/apps" RENAME "rclone-browser.png") 143 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../assets/rclone-browser-512x512.png" DESTINATION "share/icons/hicolor/512x512/apps" RENAME "rclone-browser.png") 144 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../assets/rclone-browser.desktop" DESTINATION "share/applications") 145 | endif() 146 | -------------------------------------------------------------------------------- /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, 5 | QWidget *parent) 6 | : QDialog(parent) { 7 | ui.setupUi(this); 8 | resize(0, 0); 9 | 10 | setWindowTitle("Export files list"); 11 | 12 | mTarget = remote + ":" + path.path(); 13 | 14 | QObject::connect(ui.buttonBox->button(QDialogButtonBox::RestoreDefaults), 15 | &QPushButton::clicked, this, [=]() { 16 | ui.rbText->setChecked(true); 17 | ui.checkSameFilesystem->setChecked(false); 18 | ui.textMinSize->clear(); 19 | ui.textMinAge->clear(); 20 | ui.textMaxAge->clear(); 21 | ui.spinMaxDepth->setValue(0); 22 | ui.textExclude->clear(); 23 | ui.textExtra->clear(); 24 | }); 25 | ui.buttonBox->button(QDialogButtonBox::RestoreDefaults)->click(); 26 | 27 | QObject::connect(ui.buttonBox, &QDialogButtonBox::accepted, this, 28 | &QDialog::accept); 29 | QObject::connect(ui.buttonBox, &QDialogButtonBox::rejected, this, 30 | &QDialog::reject); 31 | 32 | QObject::connect(ui.fileBrowse, &QToolButton::clicked, this, [=]() { 33 | QString file = 34 | QFileDialog::getSaveFileName(this, "Choose destination file"); 35 | if (!file.isEmpty()) { 36 | ui.textFile->setText(QDir::toNativeSeparators(file)); 37 | } 38 | }); 39 | 40 | auto settings = GetSettings(); 41 | settings->beginGroup("Export"); 42 | ReadSettings(settings.get(), this); 43 | settings->endGroup(); 44 | } 45 | 46 | ExportDialog::~ExportDialog() { 47 | if (result() == QDialog::Accepted) { 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 { return ui.textFile->text(); } 57 | 58 | bool ExportDialog::onlyFilenames() const { return ui.rbText->isChecked(); } 59 | 60 | QStringList ExportDialog::getOptions() const { 61 | QStringList list; 62 | list << "lsl"; 63 | if (ui.checkSameFilesystem->isChecked()) { 64 | list << "--one-file-system"; 65 | } 66 | if (!ui.textMinSize->text().isEmpty()) { 67 | list << "--min-size" << ui.textMinSize->text(); 68 | } 69 | if (!ui.textMinAge->text().isEmpty()) { 70 | list << "--min-age" << ui.textMinAge->text(); 71 | } 72 | if (!ui.textMaxAge->text().isEmpty()) { 73 | list << "--max-age" << ui.textMaxAge->text(); 74 | } 75 | if (ui.spinMaxDepth->value() != 0) { 76 | list << "--max-depth" << ui.spinMaxDepth->text(); 77 | } 78 | 79 | QString excluded = ui.textExclude->toPlainText().trimmed(); 80 | if (!excluded.isEmpty()) { 81 | for (auto line : excluded.split('\n')) { 82 | list << "--exclude" << line; 83 | } 84 | } 85 | 86 | QString extra = ui.textExtra->text().trimmed(); 87 | if (!extra.isEmpty()) { 88 | for (auto arg : extra.split(' ')) { 89 | list << arg; 90 | } 91 | } 92 | 93 | list << mTarget; 94 | 95 | return list; 96 | } 97 | 98 | void ExportDialog::done(int r) { 99 | if (r == QDialog::Accepted) { 100 | if (ui.textFile->text().isEmpty()) { 101 | QMessageBox::warning(this, "Warning", 102 | "Please enter destination filename!"); 103 | return; 104 | } 105 | } 106 | QDialog::done(r); 107 | } 108 | -------------------------------------------------------------------------------- /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 | Q_OBJECT 8 | 9 | public: 10 | ExportDialog(const QString &remote, const QDir &path, 11 | 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 | 226 | 227 | 228 | 229 | Exclude 230 | 231 | 232 | 233 | 234 | 235 | QPlainTextEdit::NoWrap 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | textFile 247 | fileBrowse 248 | tabWidget 249 | rbText 250 | rbCSV 251 | textMinSize 252 | textMinAge 253 | textMaxAge 254 | spinMaxDepth 255 | checkSameFilesystem 256 | textExtra 257 | textExclude 258 | 259 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /src/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/icon.icns -------------------------------------------------------------------------------- /src/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/icon.ico -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/icon.png -------------------------------------------------------------------------------- /src/icon_cache.cpp: -------------------------------------------------------------------------------- 1 | #include "icon_cache.h" 2 | #include "item_model.h" 3 | #if defined(Q_OS_MACOS) 4 | #include "osx_helper.h" 5 | #endif 6 | 7 | IconCache::IconCache(QObject *parent) : QObject(parent), mFileIcon(QFileIconProvider().icon(QFileIconProvider::File)) { 8 | #if defined(Q_OS_WIN32) 9 | CoInitializeEx(NULL, COINIT_MULTITHREADED); 10 | #endif 11 | 12 | mThread.start(); 13 | moveToThread(&mThread); 14 | } 15 | 16 | IconCache::~IconCache() { 17 | mThread.quit(); 18 | mThread.wait(); 19 | 20 | #if defined(Q_OS_WIN32) 21 | CoUninitialize(); 22 | #endif 23 | } 24 | 25 | void IconCache::getIcon(Item *item, const QPersistentModelIndex &parent) { 26 | QString ext = QFileInfo(item->name).suffix(); 27 | QIcon icon; 28 | auto it = mIcons.find(ext); 29 | if (it == mIcons.end()) { 30 | #if defined(Q_OS_WIN32) 31 | SHFILEINFOW info; 32 | if (SHGetFileInfoW(reinterpret_cast(("dummy." + ext).utf16()), 33 | FILE_ATTRIBUTE_NORMAL, &info, sizeof(info), 34 | SHGFI_ICON | SHGFI_USEFILEATTRIBUTES) && 35 | info.hIcon) { 36 | icon = QtWin::fromHICON(info.hIcon); 37 | DestroyIcon(info.hIcon); 38 | } 39 | #elif defined(Q_OS_MACOS) 40 | icon = osxGetIcon(ext.toUtf8().constData()); 41 | #else 42 | QMimeType mime = mMimeDatabase.mimeTypeForFile( 43 | item->name, QMimeDatabase::MatchExtension); 44 | if (mime.isValid()) { 45 | icon = QIcon::fromTheme(mime.iconName()); 46 | } 47 | #endif 48 | if (icon.isNull()) { 49 | icon = mFileIcon; 50 | } 51 | mIcons.insert(ext, icon); 52 | } else { 53 | icon = it.value(); 54 | } 55 | 56 | emit iconReady(item, parent, icon); 57 | } 58 | -------------------------------------------------------------------------------- /src/icon_cache.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | 5 | struct Item; 6 | 7 | class IconCache : public QObject { 8 | Q_OBJECT 9 | public: 10 | IconCache(QObject *parent = nullptr); 11 | ~IconCache(); 12 | 13 | public slots: 14 | void getIcon(Item *item, const QPersistentModelIndex &parent); 15 | 16 | signals: 17 | void iconReady(Item *item, const QPersistentModelIndex &parent, 18 | 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_MACOS) 27 | QMimeDatabase mMimeDatabase; 28 | #endif 29 | }; 30 | -------------------------------------------------------------------------------- /src/images/amazon_cloud_drive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/amazon_cloud_drive.png -------------------------------------------------------------------------------- /src/images/amazon_cloud_drive_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/amazon_cloud_drive_inv.png -------------------------------------------------------------------------------- /src/images/azureblob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/azureblob.png -------------------------------------------------------------------------------- /src/images/azureblob_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/azureblob_inv.png -------------------------------------------------------------------------------- /src/images/b2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/b2.png -------------------------------------------------------------------------------- /src/images/b2_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/b2_inv.png -------------------------------------------------------------------------------- /src/images/crypt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/crypt.png -------------------------------------------------------------------------------- /src/images/crypt_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/crypt_inv.png -------------------------------------------------------------------------------- /src/images/drive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/drive.png -------------------------------------------------------------------------------- /src/images/drive_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/drive_inv.png -------------------------------------------------------------------------------- /src/images/dropbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/dropbox.png -------------------------------------------------------------------------------- /src/images/dropbox_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/dropbox_inv.png -------------------------------------------------------------------------------- /src/images/ftp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/ftp.png -------------------------------------------------------------------------------- /src/images/ftp_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/ftp_inv.png -------------------------------------------------------------------------------- /src/images/google_cloud_storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/google_cloud_storage.png -------------------------------------------------------------------------------- /src/images/google_cloud_storage_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/google_cloud_storage_inv.png -------------------------------------------------------------------------------- /src/images/google_photos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/google_photos.png -------------------------------------------------------------------------------- /src/images/google_photos_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/google_photos_inv.png -------------------------------------------------------------------------------- /src/images/hubic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/hubic.png -------------------------------------------------------------------------------- /src/images/hubic_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/hubic_inv.png -------------------------------------------------------------------------------- /src/images/local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/local.png -------------------------------------------------------------------------------- /src/images/local_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/local_inv.png -------------------------------------------------------------------------------- /src/images/mega.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/mega.png -------------------------------------------------------------------------------- /src/images/mega_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/mega_inv.png -------------------------------------------------------------------------------- /src/images/onedrive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/onedrive.png -------------------------------------------------------------------------------- /src/images/onedrive_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/onedrive_inv.png -------------------------------------------------------------------------------- /src/images/s3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/s3.png -------------------------------------------------------------------------------- /src/images/s3_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/s3_inv.png -------------------------------------------------------------------------------- /src/images/s3_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/s3_old.png -------------------------------------------------------------------------------- /src/images/s3_old_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/s3_old_inv.png -------------------------------------------------------------------------------- /src/images/sftp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/sftp.png -------------------------------------------------------------------------------- /src/images/sftp_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/sftp_inv.png -------------------------------------------------------------------------------- /src/images/swift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/swift.png -------------------------------------------------------------------------------- /src/images/swift_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/swift_inv.png -------------------------------------------------------------------------------- /src/images/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/unknown.png -------------------------------------------------------------------------------- /src/images/unknown_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/unknown_inv.png -------------------------------------------------------------------------------- /src/images/yandex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/yandex.png -------------------------------------------------------------------------------- /src/images/yandex_inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holazt/RcloneBrowser/d4fa177c09de11b39517a402269424b964591e0d/src/images/yandex_inv.png -------------------------------------------------------------------------------- /src/item_model.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | 5 | struct Item { 6 | Item() {} 7 | 8 | ~Item() { 9 | for (auto child : childs) { 10 | if (child->isLoading() || state == LoadingIcon) { 11 | child->isDeleted = true; 12 | } else { 13 | delete child; 14 | } 15 | } 16 | } 17 | 18 | bool isLoading() const { return state == Loading1 || state == Loading2; } 19 | 20 | int num() const { 21 | Q_ASSERT(parent); 22 | return parent->childs.indexOf(const_cast(this)); 23 | } 24 | 25 | Item *parent = nullptr; 26 | 27 | enum State { Unknown, Loading1, Loading2, Ready, Special, LoadingIcon }; 28 | 29 | State state = Unknown; 30 | bool isFolder = false; 31 | bool isDeleted = false; 32 | QString name; 33 | QDir path; 34 | QString modified; 35 | quint64 size = 0; 36 | 37 | QVector childs; 38 | }; 39 | 40 | class IconCache; 41 | class ItemSorter; 42 | 43 | class ItemModel : public QAbstractItemModel { 44 | Q_OBJECT 45 | public: 46 | ItemModel(IconCache *icons, const QString &remote, QObject *parent); 47 | ~ItemModel(); 48 | 49 | const QDir &path(const QModelIndex &index) const; 50 | bool isLoading(const QModelIndex &index) const; 51 | void refresh(const QModelIndex &index); 52 | void rename(const QModelIndex &index, const QString &name); 53 | bool isTopLevel(const QModelIndex &index) const; 54 | bool isFolder(const QModelIndex &index) const; 55 | 56 | QModelIndex addRoot(const QString &name, const QString &path); 57 | 58 | QModelIndex index(int row, int column, 59 | const QModelIndex &parent) const override; 60 | QModelIndex parent(const QModelIndex &index) const override; 61 | bool hasChildren(const QModelIndex &parent) const override; 62 | int rowCount(const QModelIndex &parent) const override; 63 | int columnCount(const QModelIndex &parent) const override; 64 | void sort(int column, Qt::SortOrder order) override; 65 | QVariant data(const QModelIndex &index, int role) const override; 66 | QVariant headerData(int section, Qt::Orientation orientation, 67 | int role) const override; 68 | 69 | bool removeRows(int row, int count, const QModelIndex &parent) override; 70 | 71 | Qt::ItemFlags flags(const QModelIndex &index) const override; 72 | 73 | bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, 74 | int column, const QModelIndex &parent) const override; 75 | bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, 76 | int column, const QModelIndex &parent) override; 77 | 78 | signals: 79 | void getIcon(Item *item, const QPersistentModelIndex &index); 80 | void drop(const QDir &path, const QModelIndex &parent); 81 | 82 | private: 83 | Item *mRoot; 84 | 85 | QString mRemote; 86 | 87 | QHash mLoadedIcons; 88 | 89 | bool mFolderIcons; 90 | bool mFileIcons; 91 | 92 | QIcon mDriveIcon; 93 | QIcon mFolderIcon; 94 | QIcon mFileIcon; 95 | 96 | QFont mFixedFont; 97 | 98 | int mSortColumn; 99 | Qt::SortOrder mSortOrder; 100 | 101 | QRegExp mRegExpFolder; 102 | QRegExp mRegExpFile; 103 | 104 | Item *get(const QModelIndex &index) const; 105 | void load(const QPersistentModelIndex &parentIndex, Item *parent); 106 | 107 | void sortRecursive(Item *item, const ItemSorter &sorter); 108 | void sort(const QModelIndex &parent, Item *item); 109 | }; 110 | -------------------------------------------------------------------------------- /src/job_options.cpp: -------------------------------------------------------------------------------- 1 | #include "job_options.h" 2 | #include "utils.h" 3 | #include 4 | #include 5 | #ifdef _WIN32 6 | #pragma warning(disable : 4505) 7 | #endif 8 | JobOptions::JobOptions(bool isDownload) : JobOptions() { 9 | setJobType(isDownload); 10 | uniqueId = QUuid::createUuid(); 11 | } 12 | 13 | JobOptions::JobOptions() 14 | : jobType(UnknownJobType), operation(UnknownOp), dryRun(false), sync(false), 15 | syncTiming(UnknownTiming), skipNewer(false), skipExisting(false), 16 | compare(false), compareOption(), verbose(false), sameFilesystem(false), 17 | dontUpdateModified(false), maxDepth(0), deleteExcluded(false), 18 | isFolder(false), DriveSharedWithMe(false) {} 19 | 20 | const qint32 JobOptions::classVersion = 3; 21 | 22 | JobOptions::~JobOptions() {} 23 | 24 | /* 25 | * Turn the options held here into a string list for 26 | * use in the rclone command. 27 | * 28 | * This logic was originally in transfer_dialog.cpp. 29 | * 30 | * This needs to change whenever e.g. new options are 31 | * added to the dialog. 32 | */ 33 | QStringList JobOptions::getOptions() const { 34 | QStringList list; 35 | 36 | if (operation == Copy) { 37 | list << "copy"; 38 | } else if (operation == Move) { 39 | list << "move"; 40 | } else if (operation == Sync) { 41 | list << "sync"; 42 | } 43 | 44 | if (dryRun) { 45 | list << "--dry-run"; 46 | } 47 | 48 | if (sync) { 49 | switch (syncTiming) { 50 | case During: 51 | list << "--delete-during"; 52 | break; 53 | case After: 54 | list << "--delete-after"; 55 | break; 56 | case Before: 57 | list << "--delete-before"; 58 | break; 59 | default: 60 | break; 61 | ; 62 | } 63 | } 64 | 65 | if (skipNewer) { 66 | list << "--update"; 67 | } 68 | if (skipExisting) { 69 | list << "--ignore-existing"; 70 | } 71 | 72 | if (compare) { 73 | switch (compareOption) { 74 | case Checksum: 75 | list << "--checksum"; 76 | break; 77 | case IgnoreSize: 78 | list << "--ignore-size"; 79 | break; 80 | case SizeOnly: 81 | list << "--size-only"; 82 | break; 83 | case ChecksumIgnoreSize: 84 | list << "--checksum" 85 | << "--ignore-size"; 86 | break; 87 | default: 88 | break; 89 | } 90 | } 91 | 92 | // always verbose 93 | list << "--verbose"; 94 | if (sameFilesystem) { 95 | list << "--one-file-system"; 96 | } 97 | if (dontUpdateModified) { 98 | list << "--no-update-modtime"; 99 | } 100 | 101 | list << "--transfers" << transfers; 102 | list << "--checkers" << checkers; 103 | 104 | if (!bandwidth.isEmpty()) { 105 | list << "--bwlimit" << bandwidth; 106 | } 107 | if (!minSize.isEmpty()) { 108 | list << "--min-size" << minSize; 109 | } 110 | if (!minAge.isEmpty()) { 111 | list << "--min-age" << minAge; 112 | } 113 | if (!maxAge.isEmpty()) { 114 | list << "--max-age" << maxAge; 115 | } 116 | 117 | if (maxDepth != 0) { 118 | list << "--max-depth" << QString::number(maxDepth); 119 | } 120 | 121 | list << "--contimeout" << (connectTimeout + "s"); 122 | list << "--timeout" << (idleTimeout + "s"); 123 | list << "--retries" << retries; 124 | list << "--low-level-retries" << lowLevelRetries; 125 | 126 | if (deleteExcluded) { 127 | list << "--delete-excluded"; 128 | } 129 | 130 | if (!excluded.isEmpty()) { 131 | for (const auto &line : excluded.split('\n')) { 132 | list << "--exclude" << line; 133 | } 134 | } 135 | 136 | if (!extra.isEmpty()) { 137 | for (const auto &arg : extra.split(' ')) { 138 | list << arg; 139 | } 140 | } 141 | 142 | if (DriveSharedWithMe) { 143 | list << "--drive-shared-with-me"; 144 | } 145 | 146 | list << "--stats" 147 | << "1s"; 148 | 149 | list << "--stats-file-name-length" 150 | << "0"; 151 | 152 | QStringList defaultRcloneOptionsList = GetDefaultRcloneOptionsList(); 153 | if (!defaultRcloneOptionsList.isEmpty()) { 154 | list << defaultRcloneOptionsList; 155 | } 156 | 157 | list << source; 158 | list << dest; 159 | 160 | return list; 161 | } 162 | 163 | SerializationException::SerializationException(QString msg) 164 | : QException(), Message(msg) {} 165 | -------------------------------------------------------------------------------- /src/job_options.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | class JobOptions { 7 | public: 8 | explicit JobOptions(bool isDownload); 9 | JobOptions(); 10 | 11 | ~JobOptions(); 12 | 13 | enum Operation { UnknownOp, Copy, Move, Sync }; 14 | enum JobType { UnknownJobType, Upload, Download }; 15 | 16 | /* 17 | * The following enums have their int values synchronized with the 18 | * list indexes on the gui. Changes needed to be synchronized. 19 | */ 20 | enum SyncTiming { During, After, Before, UnknownTiming }; 21 | enum CompareOption { 22 | SizeAndModTime, 23 | Checksum, 24 | IgnoreSize, 25 | SizeOnly, 26 | ChecksumIgnoreSize 27 | }; 28 | 29 | QString description; 30 | 31 | JobType jobType; 32 | Operation operation; 33 | bool dryRun; // not persisted 34 | bool sync; 35 | SyncTiming syncTiming; 36 | bool skipNewer; 37 | bool skipExisting; 38 | bool compare; 39 | CompareOption compareOption; 40 | bool verbose; 41 | bool sameFilesystem; 42 | bool dontUpdateModified; 43 | QString transfers; 44 | QString checkers; 45 | QString bandwidth; 46 | QString minSize; 47 | QString minAge; 48 | QString maxAge; 49 | int maxDepth; 50 | QString connectTimeout; 51 | QString idleTimeout; 52 | QString retries; 53 | QString lowLevelRetries; 54 | bool deleteExcluded; 55 | QString excluded; 56 | QString extra; 57 | QString source; 58 | QString dest; 59 | bool isFolder; 60 | QUuid uniqueId; 61 | bool DriveSharedWithMe; 62 | 63 | void setJobType(bool isDownload) { 64 | jobType = (isDownload) ? Download : Upload; 65 | } 66 | 67 | QString myName() const { 68 | return "JobOptions"; // this->staticQtMetaObject.myName(); 69 | } 70 | QStringList getOptions() const; 71 | 72 | bool operator==(const JobOptions &other) const { 73 | return uniqueId == other.uniqueId; 74 | } 75 | 76 | /* 77 | * This allows the de-serialization method to accomodate changes 78 | * to the class structure, especially (most easily) added members. 79 | * 80 | * Increment the value each time a change is made, emit the new field(s) 81 | * in the operator<< function, and in operator>> add conditional logic 82 | * based on the version for reading in the new field(s) 83 | */ 84 | static const qint32 classVersion; 85 | }; 86 | 87 | class JobOptionsListWidgetItem : public QListWidgetItem { 88 | public: 89 | JobOptionsListWidgetItem(JobOptions *jo, const QIcon &icon, 90 | const QString &text) 91 | : QListWidgetItem(icon, text), mJobData(jo) {} 92 | 93 | void SetData(JobOptions *jo) { mJobData = jo; } 94 | JobOptions *GetData() { return mJobData; } 95 | 96 | private: 97 | JobOptions *mJobData; 98 | }; 99 | 100 | class SerializationException : public QException { 101 | public: 102 | QString Message; 103 | explicit SerializationException(QString msg); 104 | }; 105 | -------------------------------------------------------------------------------- /src/job_widget.cpp: -------------------------------------------------------------------------------- 1 | #include "job_widget.h" 2 | #include "utils.h" 3 | 4 | JobWidget::JobWidget(QProcess *process, const QString &info, 5 | const QStringList &args, const QString &source, 6 | const QString &dest, QWidget *parent) 7 | : QWidget(parent), mProcess(process) { 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( 24 | ui.showDetails, &QToolButton::toggled, this, [=](bool checked) { 25 | ui.details->setVisible(checked); 26 | ui.showDetails->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 27 | }); 28 | 29 | QObject::connect( 30 | ui.showOutput, &QToolButton::toggled, this, [=](bool checked) { 31 | ui.output->setVisible(checked); 32 | ui.showOutput->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 33 | }); 34 | 35 | ui.cancel->setIcon( 36 | QApplication::style()->standardIcon(QStyle::SP_DialogCloseButton)); 37 | 38 | QObject::connect(ui.cancel, &QToolButton::clicked, this, [=]() { 39 | if (mRunning) { 40 | int button = QMessageBox::question( 41 | this, "Transfer", 42 | QString("rclone process is still running. Do you want to cancel it?"), 43 | QMessageBox::Yes | QMessageBox::No); 44 | if (button == QMessageBox::Yes) { 45 | cancel(); 46 | } 47 | } else { 48 | emit closed(); 49 | } 50 | }); 51 | 52 | ui.copy->setIcon( 53 | QApplication::style()->standardIcon(QStyle::SP_FileLinkIcon)); 54 | 55 | QObject::connect(ui.copy, &QToolButton::clicked, this, [=]() { 56 | QClipboard *clipboard = QGuiApplication::clipboard(); 57 | clipboard->setText(mArgs.join(" ")); 58 | }); 59 | 60 | QObject::connect(mProcess, &QProcess::readyRead, this, [=]() { 61 | QRegExp rxSize( 62 | R"(^Transferred:\s+(\S+ \S+) \(([^)]+)\)$)"); // Until rclone 1.42 63 | QRegExp rxSize2( 64 | R"(^Transferred:\s+([0-9.]+)(\S*) \/ (\S+) (\S+), ([0-9%-]+), (\S+ \S+), (\S+) (\S+)$)"); // Starting with rclone 1.43 65 | QRegExp rxSize3( 66 | R"(^Transferred:\s+([0-9.]+ \w+) \/ ([0-9.]+ \w+), ([0-9%-]+), ([0-9.]+ \w+\/s), \w+ (\S+)$)"); // Starting with rclone 1.57 67 | QRegExp rxErrors(R"(^Errors:\s+(\S+))"); 68 | QRegExp rxChecks(R"(^Checks:\s+(\S+)$)"); // Until rclone 1.42 69 | QRegExp rxChecks2( 70 | R"(^Checks:\s+(\S+) \/ (\S+), ([0-9%-]+)$)"); // Starting with 71 | // rclone 1.43 72 | QRegExp rxTransferred(R"(^Transferred:\s+(\S+)$)"); // Until rclone 1.42 73 | QRegExp rxTransferred2( 74 | R"(^Transferred:\s+(\d+) \/ (\d+), ([0-9%-]+)$)"); // Starting with 75 | // rclone 1.43 76 | QRegExp rxTime(R"(^Elapsed time:\s+(\S+)$)"); 77 | QRegExp rxProgress( 78 | R"(^\*([^:]+):\s*([^%]+)% done.+(ETA: [^)]+)$)"); // Until rclone 1.38 79 | QRegExp rxProgress2( 80 | R"(\*([^:]+):\s*([^%]+)% \/[a-zA-z0-9.]+, [a-zA-z0-9.]+\/s, (\w+)$)"); // Starting with rclone 1.39 81 | QRegExp rxProgress3( 82 | R"(^\* ([^:]+):\s*([^%]+%) \/([0-9.]+\w+), ([0-9.]*[a-zA-Z\/]+s)*,)"); // Starting with rclone 1.56 83 | 84 | while (mProcess->canReadLine()) { 85 | QString line = mProcess->readLine().trimmed(); 86 | if (++mLines == 10000) { 87 | ui.output->clear(); 88 | mLines = 1; 89 | } 90 | ui.output->appendPlainText(line); 91 | 92 | if (line.isEmpty()) { 93 | for (auto it = mActive.begin(), eit = mActive.end(); it != eit; 94 | /* empty */) { 95 | auto label = it.value(); 96 | if (mUpdated.contains(label)) { 97 | ++it; 98 | } else { 99 | it = mActive.erase(it); 100 | ui.progress->removeWidget(label->buddy()); 101 | ui.progress->removeWidget(label); 102 | delete label->buddy(); 103 | delete label; 104 | } 105 | } 106 | mUpdated.clear(); 107 | continue; 108 | } 109 | 110 | if (rxSize.exactMatch(line)) { 111 | ui.size->setText(rxSize.cap(1)); 112 | ui.bandwidth->setText(rxSize.cap(2)); 113 | } else if (rxSize2.exactMatch(line)) { 114 | ui.size->setText(rxSize2.cap(1) + " " + rxSize2.cap(2) + "Byte" + ", " + 115 | rxSize2.cap(5)); 116 | ui.bandwidth->setText(rxSize2.cap(6)); 117 | ui.eta->setText(rxSize2.cap(8)); 118 | ui.totalsize->setText(rxSize2.cap(3) + " " + rxSize2.cap(4)); 119 | } 120 | else if (rxSize3.exactMatch(line)) { 121 | ui.size->setText(rxSize3.cap(1) + ", " + rxSize3.cap(3)); 122 | ui.bandwidth->setText(rxSize3.cap(4)); 123 | ui.eta->setText(rxSize3.cap(5)); 124 | ui.totalsize->setText(rxSize3.cap(2)); 125 | } else if (rxErrors.exactMatch(line)) { 126 | ui.errors->setText(rxErrors.cap(1)); 127 | } else if (rxChecks.exactMatch(line)) { 128 | ui.checks->setText(rxChecks.cap(1)); 129 | } else if (rxChecks2.exactMatch(line)) { 130 | ui.checks->setText(rxChecks2.cap(1) + " / " + rxChecks2.cap(2) + ", " + 131 | rxChecks2.cap(3)); 132 | } else if (rxTransferred.exactMatch(line)) { 133 | ui.transferred->setText(rxTransferred.cap(1)); 134 | } else if (rxTransferred2.exactMatch(line)) { 135 | ui.transferred->setText(rxTransferred2.cap(1) + " / " + 136 | rxTransferred2.cap(2) + ", " + 137 | rxTransferred2.cap(3)); 138 | } else if (rxTime.exactMatch(line)) { 139 | ui.elapsed->setText(rxTime.cap(1)); 140 | } else if (rxProgress.exactMatch(line)) { 141 | QString name = rxProgress.cap(1).trimmed(); 142 | 143 | auto it = mActive.find(name); 144 | 145 | QLabel *label; 146 | QProgressBar *bar; 147 | if (it == mActive.end()) { 148 | label = new QLabel(); 149 | label->setText(name); 150 | 151 | bar = new QProgressBar(); 152 | bar->setMinimum(0); 153 | bar->setMaximum(100); 154 | bar->setTextVisible(true); 155 | 156 | label->setBuddy(bar); 157 | 158 | ui.progress->addRow(label, bar); 159 | 160 | mActive.insert(name, label); 161 | } else { 162 | label = it.value(); 163 | bar = static_cast(label->buddy()); 164 | } 165 | 166 | bar->setValue(rxProgress.cap(2).toInt()); 167 | bar->setToolTip(rxProgress.cap(3)); 168 | 169 | mUpdated.insert(label); 170 | } else if (rxProgress2.exactMatch(line)) { 171 | QString name = rxProgress2.cap(1).trimmed(); 172 | 173 | auto it = mActive.find(name); 174 | 175 | QLabel *label; 176 | QProgressBar *bar; 177 | if (it == mActive.end()) { 178 | label = new QLabel(); 179 | 180 | QString nameTrimmed; 181 | 182 | if (name.length() > 47) { 183 | nameTrimmed = name.left(25) + "..." + name.right(19); 184 | } else { 185 | nameTrimmed = name; 186 | } 187 | 188 | label->setText(nameTrimmed); 189 | 190 | bar = new QProgressBar(); 191 | bar->setMinimum(0); 192 | bar->setMaximum(100); 193 | bar->setTextVisible(true); 194 | 195 | label->setBuddy(bar); 196 | 197 | ui.progress->addRow(label, bar); 198 | 199 | mActive.insert(name, label); 200 | } else { 201 | label = it.value(); 202 | bar = static_cast(label->buddy()); 203 | } 204 | 205 | bar->setValue(rxProgress2.cap(2).toInt()); 206 | bar->setToolTip("File name: " + name + "\nFile stats" + rxProgress2.cap(0).mid(rxProgress2.cap(0).indexOf(':'))); 207 | 208 | mUpdated.insert(label); 209 | } else if (rxProgress3.exactMatch(line)) { 210 | QString name = rxProgress3.cap(1).trimmed(); 211 | 212 | auto it = mActive.find(name); 213 | 214 | QLabel *label; 215 | QProgressBar *bar; 216 | if (it == mActive.end()) { 217 | label = new QLabel(); 218 | 219 | QString nameTrimmed; 220 | 221 | if (name.length() > 47) { 222 | nameTrimmed = name.left(25) + "..." + name.right(19); 223 | } else { 224 | nameTrimmed = name; 225 | } 226 | 227 | label->setText(nameTrimmed); 228 | 229 | bar = new QProgressBar(); 230 | bar->setMinimum(0); 231 | bar->setMaximum(100); 232 | bar->setTextVisible(true); 233 | 234 | label->setBuddy(bar); 235 | 236 | ui.progress->addRow(label, bar); 237 | 238 | mActive.insert(name, label); 239 | } else { 240 | label = it.value(); 241 | bar = static_cast(label->buddy()); 242 | } 243 | 244 | bar->setValue(rxProgress3.cap(2).toInt()); 245 | bar->setToolTip("File name: " + name + "\nFile stats" + rxProgress3.cap(0).mid(rxProgress3.cap(0).indexOf(':'))); 246 | 247 | mUpdated.insert(label); 248 | } 249 | } 250 | }); 251 | 252 | QObject::connect(mProcess, 253 | static_cast( 254 | &QProcess::finished), 255 | this, [=](int status, QProcess::ExitStatus) { 256 | mProcess->deleteLater(); 257 | for (auto label : mActive) { 258 | ui.progress->removeWidget(label->buddy()); 259 | ui.progress->removeWidget(label); 260 | delete label->buddy(); 261 | delete label; 262 | } 263 | 264 | mRunning = false; 265 | if (status == 0) { 266 | ui.showDetails->setStyleSheet( 267 | "QToolButton { border: 0; color: black; }"); 268 | ui.showDetails->setText("Finished"); 269 | } else { 270 | ui.showDetails->setStyleSheet( 271 | "QToolButton { border: 0; color: red; }"); 272 | ui.showDetails->setText("Error"); 273 | } 274 | 275 | ui.cancel->setToolTip("Close"); 276 | 277 | emit finished(ui.info->text()); 278 | }); 279 | 280 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: green; }"); 281 | ui.showDetails->setText("Running"); 282 | } 283 | 284 | JobWidget::~JobWidget() {} 285 | 286 | void JobWidget::showDetails() { ui.showDetails->setChecked(true); } 287 | 288 | void JobWidget::cancel() { 289 | if (!mRunning) { 290 | return; 291 | } 292 | 293 | mProcess->kill(); 294 | mProcess->waitForFinished(); 295 | 296 | emit closed(); 297 | } 298 | -------------------------------------------------------------------------------- /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 | Q_OBJECT 8 | 9 | public: 10 | JobWidget(QProcess *process, const QString &info, const QStringList &args, 11 | const QString &source, const QString &dest, 12 | QWidget *parent = nullptr); 13 | ~JobWidget(); 14 | 15 | void showDetails(); 16 | 17 | public slots: 18 | void cancel(); 19 | 20 | signals: 21 | void finished(const QString &info); 22 | void closed(); 23 | 24 | private: 25 | Ui::JobWidget ui; 26 | 27 | bool mRunning = true; 28 | QProcess *mProcess; 29 | int mLines = 0; 30 | 31 | QStringList mArgs; 32 | QHash mActive; 33 | QSet mUpdated; 34 | }; 35 | -------------------------------------------------------------------------------- /src/job_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | JobWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 966 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 | Total size: 106 | 107 | 108 | totalsize 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 0 117 | 0 118 | 119 | 120 | 121 | 122 | 140 123 | 0 124 | 125 | 126 | 127 | - 128 | 129 | 130 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 131 | 132 | 133 | true 134 | 135 | 136 | 137 | 138 | 139 | 140 | Remaining time: 141 | 142 | 143 | eta 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 0 152 | 0 153 | 154 | 155 | 156 | 157 | 140 158 | 0 159 | 160 | 161 | 162 | - 163 | 164 | 165 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 166 | 167 | 168 | true 169 | 170 | 171 | 172 | 173 | 174 | 175 | Errors: 176 | 177 | 178 | errors 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 0 187 | 0 188 | 189 | 190 | 191 | 192 | 140 193 | 0 194 | 195 | 196 | 197 | - 198 | 199 | 200 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 201 | 202 | 203 | true 204 | 205 | 206 | 207 | 208 | 209 | 210 | Checks: 211 | 212 | 213 | checks 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 0 222 | 0 223 | 224 | 225 | 226 | 227 | 200 228 | 0 229 | 230 | 231 | 232 | - 233 | 234 | 235 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 236 | 237 | 238 | true 239 | 240 | 241 | 242 | 243 | 244 | 245 | Elapsed time: 246 | 247 | 248 | elapsed 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 0 257 | 0 258 | 259 | 260 | 261 | 262 | 140 263 | 0 264 | 265 | 266 | 267 | - 268 | 269 | 270 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 271 | 272 | 273 | true 274 | 275 | 276 | 277 | 278 | 279 | 280 | true 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 0 289 | 0 290 | 291 | 292 | 293 | Size: 294 | 295 | 296 | size 297 | 298 | 299 | 300 | 301 | 302 | 303 | true 304 | 305 | 306 | 307 | 308 | 309 | 310 | QToolButton { border: 0 } 311 | 312 | 313 | Show Output 314 | 315 | 316 | true 317 | 318 | 319 | Qt::ToolButtonTextBesideIcon 320 | 321 | 322 | Qt::RightArrow 323 | 324 | 325 | 326 | 327 | 328 | 329 | Bandwidth: 330 | 331 | 332 | bandwidth 333 | 334 | 335 | 336 | 337 | 338 | 339 | Transfers: 340 | 341 | 342 | transferred 343 | 344 | 345 | 346 | 347 | 348 | 349 | QPlainTextEdit::NoWrap 350 | 351 | 352 | true 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 0 361 | 0 362 | 363 | 364 | 365 | 366 | 140 367 | 0 368 | 369 | 370 | 371 | - 372 | 373 | 374 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 375 | 376 | 377 | true 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 0 386 | 0 387 | 388 | 389 | 390 | Source: 391 | 392 | 393 | source 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 0 402 | 0 403 | 404 | 405 | 406 | Destination: 407 | 408 | 409 | dest 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 0 418 | 0 419 | 420 | 421 | 422 | 423 | 140 424 | 0 425 | 426 | 427 | 428 | - 429 | 430 | 431 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 432 | 433 | 434 | true 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 0 443 | 0 444 | 445 | 446 | 447 | 448 | 200 449 | 0 450 | 451 | 452 | 453 | - 454 | 455 | 456 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 457 | 458 | 459 | true 460 | 461 | 462 | 463 | 464 | 465 | 466 | QFormLayout::ExpandingFieldsGrow 467 | 468 | 469 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | showDetails 480 | cancel 481 | source 482 | dest 483 | size 484 | elapsed 485 | bandwidth 486 | showOutput 487 | output 488 | 489 | 490 | 491 | 492 | -------------------------------------------------------------------------------- /src/list_of_job_options.cpp: -------------------------------------------------------------------------------- 1 | #include "list_of_job_options.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static QDataStream &operator>>(QDataStream &dataStream, JobOptions &jo); 9 | static QDataStream &operator<<(QDataStream &dataStream, const JobOptions &jo); 10 | static QDataStream &operator>>(QDataStream &in, JobOptions::Operation &e); 11 | static QDataStream &operator>>(QDataStream &in, JobOptions::SyncTiming &e); 12 | static QDataStream &operator>>(QDataStream &in, JobOptions::CompareOption &e); 13 | static QDataStream &operator>>(QDataStream &in, JobOptions::JobType &e); 14 | 15 | ListOfJobOptions *ListOfJobOptions::SavedJobOptions = nullptr; 16 | const QString ListOfJobOptions::persistenceFileName = "tasks.bin"; 17 | 18 | ListOfJobOptions::ListOfJobOptions() {} 19 | 20 | ListOfJobOptions *ListOfJobOptions::getInstance() { 21 | if (SavedJobOptions == nullptr) { 22 | SavedJobOptions = new ListOfJobOptions(); 23 | RestoreFromUserData(*SavedJobOptions); 24 | } 25 | return SavedJobOptions; 26 | } 27 | 28 | bool ListOfJobOptions::Persist(JobOptions *jo) { 29 | bool isNew = !this->tasks.contains(jo); 30 | if (isNew) 31 | this->tasks.append(jo); 32 | else { 33 | // int ix = tasks.indexOf(jo); 34 | // JobOptions *old = tasks[ix]; 35 | // qDebug() << QString("old [%1] New [%2]") 36 | // .arg(old->description) 37 | // .arg(jo->description); 38 | } 39 | PersistToUserData(); 40 | return isNew; 41 | } 42 | 43 | bool ListOfJobOptions::Forget(JobOptions *jo) { 44 | bool isKnown = this->tasks.contains(jo); 45 | if (!isKnown) 46 | return false; 47 | int ix = tasks.indexOf(jo); 48 | tasks.removeAt(ix); 49 | // qDebug() << QString("removed [%1]").arg(jo->description); 50 | PersistToUserData(); 51 | return isKnown; 52 | } 53 | 54 | QFile *ListOfJobOptions::GetPersistenceFile(QIODevice::OpenModeFlag mode) { 55 | 56 | QDir outputDir; 57 | 58 | if (IsPortableMode()) { 59 | // in portable mode tasks' file will be saved in the same folder as 60 | // excecutable 61 | #ifdef Q_OS_MACOS 62 | // on macOS excecutable file is located in 63 | // ./rclone-browser.app/Contents/MasOS/ 64 | // to get actual bundle folder we have 65 | // to traverse three levels up 66 | outputDir = QDir(qApp->applicationDirPath() + "/../../.."); 67 | #else 68 | #ifdef Q_OS_WIN 69 | // not macOS 70 | outputDir = QDir(qApp->applicationDirPath()); 71 | #else 72 | QString xdg_config_home = qgetenv("XDG_CONFIG_HOME"); 73 | outputDir = QDir(xdg_config_home + "/rclone-browser"); 74 | #endif 75 | #endif 76 | 77 | } else { 78 | 79 | // get data location folder from Qt - OS dependend 80 | outputDir = 81 | QDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); 82 | } 83 | 84 | if (!outputDir.exists()) { 85 | outputDir.mkpath("."); 86 | } 87 | QString filePath = outputDir.absoluteFilePath(persistenceFileName); 88 | QFile *file = new QFile(filePath); 89 | 90 | if (!file->open(mode)) { 91 | // qDebug() << QString("Could not open ") << file->fileName(); 92 | delete file; 93 | file = nullptr; 94 | } 95 | return file; 96 | } 97 | 98 | bool ListOfJobOptions::RestoreFromUserData(ListOfJobOptions &dataIn) { 99 | QFile *file = GetPersistenceFile(QIODevice::ReadOnly); 100 | if (file == nullptr) 101 | return false; 102 | QDataStream instream(file); 103 | instream.setVersion(QDataStream::Qt_5_2); 104 | 105 | while (!instream.atEnd()) { 106 | try { 107 | JobOptions *jo = new JobOptions(); 108 | instream >> *jo; 109 | dataIn.tasks.append(jo); 110 | } catch (SerializationException &ex) { 111 | // qDebug() << QString("failed to restore tasks: ") << ex.Message; 112 | file->close(); 113 | delete file; 114 | return false; 115 | } 116 | } 117 | 118 | file->close(); 119 | delete file; 120 | 121 | return true; 122 | } 123 | 124 | bool ListOfJobOptions::PersistToUserData() { 125 | QFile *file = GetPersistenceFile( 126 | QIODevice::WriteOnly); // note this mode implies Truncate also 127 | if (file == nullptr) 128 | return false; 129 | QDataStream outstream(file); 130 | outstream.setVersion(QDataStream::Qt_5_2); 131 | 132 | for (JobOptions *it : tasks) { 133 | outstream << *it; 134 | } 135 | 136 | file->flush(); 137 | file->close(); 138 | 139 | emit tasksListUpdated(); 140 | 141 | delete file; 142 | 143 | return true; 144 | } 145 | 146 | QDataStream &operator<<(QDataStream &stream, const JobOptions &jo) { 147 | stream << jo.myName() << JobOptions::classVersion << jo.description 148 | << jo.jobType << jo.operation << /* jo.dryRun <<*/ jo.sync 149 | << jo.syncTiming << jo.skipNewer << jo.skipExisting << jo.compare 150 | << jo.compareOption << jo.verbose << jo.sameFilesystem 151 | << jo.dontUpdateModified << jo.transfers << jo.checkers << jo.bandwidth 152 | << jo.minSize << jo.minAge << jo.maxAge << jo.maxDepth 153 | << jo.connectTimeout << jo.idleTimeout << jo.retries 154 | << jo.lowLevelRetries << jo.deleteExcluded << jo.excluded << jo.extra 155 | << jo.DriveSharedWithMe << jo.source << jo.dest << jo.isFolder 156 | << jo.uniqueId; 157 | 158 | return stream; 159 | } 160 | 161 | QDataStream &operator>>(QDataStream &stream, JobOptions &jo) { 162 | QString actualName; 163 | qint32 actualVersion; 164 | 165 | stream >> actualName; 166 | if (QString::compare(actualName, jo.myName()) != 0) 167 | throw SerializationException("incorrect class"); 168 | 169 | stream >> actualVersion; 170 | if (actualVersion > JobOptions::classVersion) 171 | throw SerializationException("stored version is newer"); 172 | 173 | stream >> jo.description >> jo.jobType >> jo.operation >> 174 | /* jo.dryRun >> */ jo.sync >> jo.syncTiming >> jo.skipNewer >> 175 | jo.skipExisting >> jo.compare >> jo.compareOption >> jo.verbose >> 176 | jo.sameFilesystem >> jo.dontUpdateModified >> jo.transfers >> 177 | jo.checkers >> jo.bandwidth >> jo.minSize >> jo.minAge >> jo.maxAge >> 178 | jo.maxDepth >> jo.connectTimeout >> jo.idleTimeout >> jo.retries >> 179 | jo.lowLevelRetries >> jo.deleteExcluded >> jo.excluded >> jo.extra >> 180 | jo.DriveSharedWithMe >> jo.source >> jo.dest; 181 | 182 | // as fields are added in later revisions, check actualVersion here and 183 | // conditionally extract any new fields iff they are expected based on the 184 | // stream value 185 | if (actualVersion >= 2) { 186 | stream >> jo.isFolder; 187 | if (actualVersion >= 3) { 188 | stream >> jo.uniqueId; 189 | } 190 | } 191 | 192 | return stream; 193 | } 194 | 195 | QDataStream &operator>>(QDataStream &in, JobOptions::Operation &e) { 196 | in >> (quint32 &)e; 197 | return in; 198 | } 199 | 200 | QDataStream &operator>>(QDataStream &in, JobOptions::SyncTiming &e) { 201 | in >> (quint32 &)e; 202 | return in; 203 | } 204 | 205 | QDataStream &operator>>(QDataStream &in, JobOptions::CompareOption &e) { 206 | in >> (quint32 &)e; 207 | return in; 208 | } 209 | 210 | QDataStream &operator>>(QDataStream &in, JobOptions::JobType &e) { 211 | in >> (quint32 &)e; 212 | return in; 213 | } 214 | -------------------------------------------------------------------------------- /src/list_of_job_options.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "job_options.h" 3 | #include 4 | 5 | class ListOfJobOptions : public QObject { 6 | Q_OBJECT 7 | 8 | protected: 9 | ~ListOfJobOptions() = default; 10 | ListOfJobOptions(); 11 | 12 | public: 13 | static ListOfJobOptions *getInstance(); 14 | bool Persist(JobOptions *jo); 15 | bool Forget(JobOptions *jo); 16 | QList &getTasks() { return tasks; } 17 | 18 | signals: 19 | void tasksListUpdated(); 20 | 21 | private: 22 | static ListOfJobOptions *SavedJobOptions; 23 | static const QString persistenceFileName; 24 | static bool RestoreFromUserData(ListOfJobOptions &dataIn); 25 | static QFile *GetPersistenceFile(QIODevice::OpenModeFlag mode); 26 | 27 | QList tasks; 28 | bool PersistToUserData(); 29 | }; 30 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "main_window.h" 2 | #include "utils.h" 3 | 4 | int main(int argc, char *argv[]) { 5 | 6 | #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) 7 | static const char ENV_VAR_QT_DEVICE_PIXEL_RATIO[] = "QT_DEVICE_PIXEL_RATIO"; 8 | if (!qEnvironmentVariableIsSet(ENV_VAR_QT_DEVICE_PIXEL_RATIO) && 9 | !qEnvironmentVariableIsSet("QT_AUTO_SCREEN_SCALE_FACTOR") && 10 | !qEnvironmentVariableIsSet("QT_SCALE_FACTOR") && 11 | !qEnvironmentVariableIsSet("QT_SCREEN_SCALE_FACTORS")) { 12 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 13 | } 14 | #endif 15 | 16 | QApplication app(argc, argv); 17 | 18 | // app.setApplicationDisplayName("Rclone Browser"); 19 | app.setApplicationName("rclone-browser"); 20 | app.setOrganizationName("rclone-browser"); 21 | app.setWindowIcon(QIcon(":/icons/icon.png")); 22 | 23 | // initialize SSL libraries 24 | // see: https://github.com/linuxdeploy/linuxdeploy-plugin-qt/issues/57 25 | #if defined(Q_OS_LINUX) 26 | QString currentDir = QDir::currentPath(); 27 | QDir::setCurrent(QCoreApplication::applicationDirPath()); 28 | QSslSocket::supportsSsl(); 29 | QDir::setCurrent(currentDir); 30 | #endif 31 | 32 | auto settings = GetSettings(); 33 | 34 | // initialize proxy settings 35 | if (!(settings->contains("Settings/useProxy"))) { 36 | settings->setValue("Settings/useProxy", "false"); 37 | }; 38 | if (!(settings->contains("Settings/http_proxy"))) { 39 | settings->setValue("Settings/http_proxy", ""); 40 | }; 41 | if (!(settings->contains("Settings/https_proxy"))) { 42 | settings->setValue("Settings/https_proxy", ""); 43 | }; 44 | if (!(settings->contains("Settings/no_proxy"))) { 45 | settings->setValue("Settings/no_proxy", ""); 46 | }; 47 | 48 | if (settings->value("Settings/useProxy").toBool()) { 49 | qputenv("HTTP_PROXY", settings->value("Settings/http_proxy").toByteArray()); 50 | qputenv("http_proxy", settings->value("Settings/http_proxy").toByteArray()); 51 | qputenv("HTTPS_PROXY", 52 | settings->value("Settings/https_proxy").toByteArray()); 53 | qputenv("https_proxy", 54 | settings->value("Settings/https_proxy").toByteArray()); 55 | qputenv("NO_PROXY", settings->value("Settings/no_proxy").toByteArray()); 56 | qputenv("no_proxy", settings->value("Settings/no_proxy").toByteArray()); 57 | } 58 | 59 | // remmber darkMode state on app startup 60 | // during first run the darkModeIni key might not exist 61 | if (!(settings->contains("Settings/darkModeIni"))) { 62 | // if darkModeIni does not exist create new key 63 | settings->setValue("Settings/darkModeIni", "false"); 64 | }; 65 | 66 | // during first run the darkMode key might not exist 67 | if (!(settings->contains("Settings/darkMode"))) { 68 | // if darkMode does not exist create new key 69 | settings->setValue("Settings/darkMode", "false"); 70 | }; 71 | 72 | bool darkMode = settings->value("Settings/darkMode").toBool(); 73 | 74 | settings->setValue("Settings/darkModeIni", darkMode); 75 | 76 | // during first run the iconSize key might not exist 77 | if (!(settings->contains("Settings/iconSize"))) { 78 | // if iconSize does not exist create new key 79 | settings->setValue("Settings/iconSize", "medium"); 80 | }; 81 | 82 | // enforce one instance of Rclone Browser per user 83 | QString tmpDir; 84 | QString applicationNameBase; 85 | QFileInfo applicationPath; 86 | QFileInfo applicationUserPath; 87 | 88 | // QString xdg_config_home = qgetenv("XDG_CONFIG_HOME"); 89 | // qDebug() << QString("main.cpp $XDG_CONFIG_HOME: " + xdg_config_home); 90 | 91 | // QString APPIMAGE = qgetenv("APPIMAGE"); 92 | // qDebug() << QString("main.cpp $APPIMAGE: " + APPIMAGE); 93 | 94 | QFileInfo appBundlePath; 95 | 96 | if (IsPortableMode()) { 97 | 98 | // qDebug() << QString("isPortable is true"); 99 | // applicationPath = qApp->applicationFilePath(); 100 | #ifdef Q_OS_MACOS 101 | // on macOS excecutable file is located in 102 | // ./rclone-browser.app/Contents/MasOS/ 103 | // to get actual bundle folder we have 104 | // to traverse three levels up 105 | applicationPath = qApp->applicationFilePath(); 106 | tmpDir = applicationPath.absolutePath() + "/../../.."; 107 | 108 | // get bundle name 109 | QFileInfo MacOSPath = applicationPath.dir().path(); 110 | QFileInfo ContentsPath = MacOSPath.dir().path(); 111 | appBundlePath = ContentsPath.dir().path(); 112 | 113 | #else 114 | // not macOS 115 | #ifdef Q_OS_WIN 116 | applicationPath = qApp->applicationFilePath(); 117 | tmpDir = applicationPath.absolutePath(); 118 | #else 119 | QString xdg_config_home = qgetenv("XDG_CONFIG_HOME"); 120 | tmpDir = xdg_config_home + "/rclone-browser"; 121 | // create ./rclone-browser folder 122 | if (!QDir(tmpDir).exists()) { 123 | QDir().mkdir(tmpDir); 124 | } 125 | #endif 126 | #endif 127 | } else { 128 | // not portable mode 129 | // get tmp folder from Qt - OS dependend 130 | tmpDir = QDir::tempPath(); 131 | } 132 | 133 | // check if tmpDir writable 134 | // as isWritable does weird things on Windows 135 | // we do this old fashioned way by creating temp file 136 | QTemporaryFile tempfile(tmpDir + "/rclone-browserXXXXXX.test"); 137 | 138 | if (tempfile.open()) { 139 | tempfile.close(); 140 | tempfile.remove(); 141 | } else { 142 | // folder has no write access 143 | if (IsPortableMode()) { 144 | QMessageBox msgBox; 145 | msgBox.setIcon(QMessageBox::Warning); 146 | msgBox.setText("You need write " 147 | "access to this folder:\n\n" 148 | #ifdef Q_OS_MACOS 149 | + appBundlePath.absolutePath() + 150 | #else 151 | #ifdef Q_OS_WIN 152 | + tmpDir + 153 | #else 154 | + tmpDir.left(tmpDir.length() - 15) + 155 | #endif 156 | #endif 157 | "\n\n" 158 | #ifdef Q_OS_MACOS 159 | "Or remove file:\n\n" + 160 | appBundlePath.baseName() + 161 | ".ini \n\nfrom the above folder " 162 | #else 163 | #ifdef Q_OS_WIN 164 | "Or remove file:\n\n" + 165 | applicationPath.baseName() + 166 | ".ini \n\nfrom the above folder " 167 | #else 168 | "Or remove folder:\n\n" + 169 | tmpDir.left(tmpDir.length() - 15) + 170 | "\n\n" 171 | #endif 172 | #endif 173 | "to disable portable mode."); 174 | msgBox.exec(); 175 | } else { 176 | QMessageBox msgBox; 177 | msgBox.setIcon(QMessageBox::Warning); 178 | msgBox.setText("You need write " 179 | "access to this folder: \n\n" 180 | #ifdef Q_OS_MACOS 181 | + tmpDir 182 | #else 183 | 184 | #ifdef Q_OS_WIN 185 | + tmpDir 186 | #else 187 | 188 | + tmpDir.left(tmpDir.length() - 15) 189 | #endif 190 | #endif 191 | ); 192 | msgBox.exec(); 193 | } 194 | return static_cast( 195 | 0x80004004); // exit immediately if folder not writable 196 | } 197 | 198 | // qDebug() << QString("main.cpp tmpDir: " + tmpDir); 199 | 200 | // not most elegant as fixed name but in reality not big deal 201 | QLockFile lockFile(tmpDir + "/.RcloneBrowser_4q6RgLs2RpbJA.lock"); 202 | 203 | if (!lockFile.tryLock(100)) { 204 | // if already running display warning and quit 205 | QMessageBox msgBox; 206 | msgBox.setIcon(QMessageBox::Warning); 207 | msgBox.setText("Rclone Browser is already running." 208 | "\r\n\nOnly one instance is allowed."); 209 | msgBox.exec(); 210 | return static_cast( 211 | 0x80004004); // exit immediately if another instance is running 212 | } 213 | 214 | MainWindow w; 215 | w.show(); 216 | 217 | return app.exec(); 218 | } 219 | -------------------------------------------------------------------------------- /src/main_window.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "icon_cache.h" 4 | #include "job_options.h" 5 | #include "pch.h" 6 | #include "ui_main_window.h" 7 | 8 | class JobWidget; 9 | 10 | class MainWindow : public QMainWindow { 11 | Q_OBJECT 12 | 13 | public: 14 | MainWindow(); 15 | ~MainWindow(); 16 | 17 | private slots: 18 | void rcloneGetVersion(); 19 | void rcloneConfig(); 20 | void rcloneListRemotes(); 21 | void listTasks(); 22 | 23 | void addTransfer(const QString &message, const QString &source, 24 | const QString &dest, const QStringList &args); 25 | void addMount(const QString &remote, const QString &folder); 26 | void addStream(const QString &remote, const QString &stream); 27 | 28 | private: 29 | Ui::MainWindow ui; 30 | 31 | QSystemTrayIcon mSystemTray; 32 | JobWidget *mLastFinished = nullptr; 33 | 34 | bool mAlwaysShowInTray; 35 | bool mCloseToTray; 36 | bool mNotifyFinishedTransfers; 37 | 38 | QLabel *mStatusMessage; 39 | 40 | IconCache mIcons; 41 | 42 | bool mFirstTime = true; 43 | int mJobCount = 0; 44 | 45 | bool canClose(); 46 | void closeEvent(QCloseEvent *ev) override; 47 | bool getConfigPassword(QProcess *p); 48 | 49 | void addEmptyJobsMessage(); 50 | 51 | void runItem(JobOptionsListWidgetItem *item, bool dryrun = false); 52 | void editSelectedTask(); 53 | QIcon mUploadIcon; 54 | QIcon mDownloadIcon; 55 | }; 56 | -------------------------------------------------------------------------------- /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 | <html><head/><body><p>rclone config</p></body></html> 68 | 69 | 70 | &Config... 71 | 72 | 73 | 74 | 75 | 76 | 77 | Refresh 78 | 79 | 80 | 81 | 82 | 83 | 84 | Qt::Horizontal 85 | 86 | 87 | 88 | 40 89 | 20 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | false 98 | 99 | 100 | <html><head/><body><p>open remote</p></body></html> 101 | 102 | 103 | &Open 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Jobs 114 | 115 | 116 | 117 | 118 | 119 | true 120 | 121 | 122 | 123 | 124 | 0 125 | 0 126 | 610 127 | 299 128 | 129 | 130 | 131 | 132 | 0 133 | 134 | 135 | 0 136 | 137 | 138 | 0 139 | 140 | 141 | 0 142 | 143 | 144 | 145 | 146 | 147 | 148 | No jobs are available 149 | 150 | 151 | 152 | 153 | 154 | 155 | Qt::Vertical 156 | 157 | 158 | 159 | 20 160 | 40 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | Tasks 176 | 177 | 178 | 179 | 4 180 | 181 | 182 | 4 183 | 184 | 185 | 0 186 | 187 | 188 | 0 189 | 190 | 191 | 192 | 193 | true 194 | 195 | 196 | 197 | 198 | 0 199 | 0 200 | 646 201 | 327 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 0 216 | 0 217 | 218 | 219 | 220 | 221 | 200 222 | 20 223 | 224 | 225 | 226 | 227 | 16777215 228 | 32 229 | 230 | 231 | 232 | 233 | 4 234 | 235 | 236 | 4 237 | 238 | 239 | 4 240 | 241 | 242 | 4 243 | 244 | 245 | 4 246 | 247 | 248 | 249 | 250 | false 251 | 252 | 253 | 254 | 0 255 | 0 256 | 257 | 258 | 259 | 260 | 0 261 | 28 262 | 263 | 264 | 265 | -> Dry Run 266 | 267 | 268 | 269 | 270 | 271 | 272 | false 273 | 274 | 275 | 276 | 0 277 | 0 278 | 279 | 280 | 281 | 282 | 0 283 | 28 284 | 285 | 286 | 287 | Run 288 | 289 | 290 | 291 | 292 | 293 | 294 | false 295 | 296 | 297 | 298 | 0 299 | 0 300 | 301 | 302 | 303 | 304 | 0 305 | 28 306 | 307 | 308 | 309 | 310 | 100 311 | 16777215 312 | 313 | 314 | 315 | Qt::LeftToRight 316 | 317 | 318 | Edit 319 | 320 | 321 | 322 | 323 | 324 | 325 | false 326 | 327 | 328 | 329 | 0 330 | 0 331 | 332 | 333 | 334 | 335 | 0 336 | 28 337 | 338 | 339 | 340 | Qt::LeftToRight 341 | 342 | 343 | Delete 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 0 364 | 0 365 | 658 366 | 22 367 | 368 | 369 | 370 | 371 | &File 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | &Help 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | &About 391 | 392 | 393 | QAction::AboutRole 394 | 395 | 396 | 397 | 398 | About &Qt 399 | 400 | 401 | QAction::AboutQtRole 402 | 403 | 404 | 405 | 406 | &Preferences... 407 | 408 | 409 | QAction::PreferencesRole 410 | 411 | 412 | 413 | 414 | &Quit 415 | 416 | 417 | Ctrl+Q 418 | 419 | 420 | QAction::QuitRole 421 | 422 | 423 | 424 | 425 | tabs 426 | remotes 427 | config 428 | refresh 429 | open 430 | jobsArea 431 | 432 | 433 | 434 | 435 | -------------------------------------------------------------------------------- /src/mount_widget.cpp: -------------------------------------------------------------------------------- 1 | #include "mount_widget.h" 2 | #include "utils.h" 3 | 4 | MountWidget::MountWidget(QProcess *process, const QString &remote, 5 | const QString &folder, QWidget *parent) 6 | : QWidget(parent), mProcess(process) { 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( 19 | ui.showDetails, &QToolButton::toggled, this, [=](bool checked) { 20 | ui.details->setVisible(checked); 21 | ui.showDetails->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 22 | }); 23 | 24 | QObject::connect( 25 | ui.showOutput, &QToolButton::toggled, this, [=](bool checked) { 26 | ui.output->setVisible(checked); 27 | ui.showOutput->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 28 | }); 29 | 30 | ui.cancel->setIcon( 31 | QApplication::style()->standardIcon(QStyle::SP_DialogCloseButton)); 32 | 33 | QObject::connect(ui.cancel, &QToolButton::clicked, this, [=]() { 34 | if (mRunning) { 35 | int button = QMessageBox::question( 36 | this, "Unmount", 37 | #if defined(Q_OS_WIN) 38 | QString("Do you want to unmount %1 drive?").arg(folder), 39 | #else 40 | QString("Do you want to unmount %1 folder?").arg(folder), 41 | #endif 42 | QMessageBox::Yes | QMessageBox::No); 43 | if (button == QMessageBox::Yes) { 44 | cancel(); 45 | } 46 | } else { 47 | emit closed(); 48 | } 49 | }); 50 | 51 | QObject::connect(mProcess, &QProcess::readyRead, this, [=]() { 52 | while (mProcess->canReadLine()) { 53 | ui.output->appendPlainText(mProcess->readLine().trimmed()); 54 | } 55 | }); 56 | 57 | QObject::connect(mProcess, 58 | static_cast( 59 | &QProcess::finished), 60 | this, [=](int status, QProcess::ExitStatus) { 61 | mProcess->deleteLater(); 62 | mRunning = false; 63 | if (status == 0) { 64 | ui.showDetails->setStyleSheet( 65 | "QToolButton { border: 0; color: black; }"); 66 | ui.showDetails->setText("Finished"); 67 | } else { 68 | ui.showDetails->setStyleSheet( 69 | "QToolButton { border: 0; color: red; }"); 70 | ui.showDetails->setText("Error"); 71 | } 72 | ui.cancel->setToolTip("Close"); 73 | emit finished(); 74 | }); 75 | 76 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: green; }"); 77 | ui.showDetails->setText("Mounted"); 78 | } 79 | 80 | MountWidget::~MountWidget() {} 81 | 82 | void MountWidget::cancel() { 83 | if (!mRunning) { 84 | return; 85 | } 86 | 87 | QString cmd; 88 | 89 | #if defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) 90 | QProcess::startDetached("umount", QStringList() << ui.folder->text()); 91 | #else 92 | #if defined(Q_OS_WIN32) 93 | QProcess *p = new QProcess(); 94 | QStringList args; 95 | args << "rc"; 96 | // requires rlone version at least 1.50 97 | args << "core/quit"; 98 | 99 | args << "--rc-addr"; 100 | QString folder = ui.folder->text(); 101 | 102 | int port_offset = folder[0].toLatin1(); 103 | unsigned short int rclone_rc_port_base = 19000; 104 | unsigned short int rclone_rc_port = rclone_rc_port_base + port_offset; 105 | args << "localhost:" + QVariant(rclone_rc_port).toString(); 106 | 107 | UseRclonePassword(p); 108 | p->start(GetRclone(), args, QIODevice::ReadOnly); 109 | #else 110 | QProcess::startDetached("fusermount", QStringList() 111 | << "-u" << ui.folder->text()); 112 | #endif 113 | #endif 114 | 115 | mProcess->waitForFinished(); 116 | 117 | emit closed(); 118 | } 119 | -------------------------------------------------------------------------------- /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 | Q_OBJECT 8 | 9 | public: 10 | MountWidget(QProcess *process, const QString &remote, const QString &folder, 11 | 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 | Mount point: 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 | #include 14 | 15 | #if defined(Q_OS_WIN32) 16 | #include 17 | #endif 18 | 19 | #ifdef Q_OS_MACOS 20 | #include 21 | #endif 22 | 23 | #ifdef _MSC_VER 24 | #pragma warning pop 25 | #endif 26 | -------------------------------------------------------------------------------- /src/preferences_dialog.cpp: -------------------------------------------------------------------------------- 1 | #include "preferences_dialog.h" 2 | #include "utils.h" 3 | 4 | PreferencesDialog::PreferencesDialog(QWidget *parent) : QDialog(parent) { 5 | ui.setupUi(this); 6 | 7 | QObject::connect(ui.rcloneBrowse, &QPushButton::clicked, this, [=]() { 8 | QString rclone = QFileDialog::getOpenFileName( 9 | this, "Select rclone executable", ui.rclone->text()); 10 | if (rclone.isEmpty()) { 11 | return; 12 | } 13 | 14 | if (!QFileInfo(rclone).isExecutable()) { 15 | QMessageBox::critical(this, "Error", 16 | QString("File %1 is not executable").arg(rclone)); 17 | return; 18 | } 19 | 20 | if (QFileInfo(rclone) == QFileInfo(qApp->applicationFilePath())) { 21 | QMessageBox::critical(this, "Error", 22 | "You selected RcloneBrowser executable!\nPlease " 23 | "select rclone executable instead."); 24 | return; 25 | } 26 | 27 | ui.rclone->setText(rclone); 28 | }); 29 | 30 | QObject::connect(ui.rcloneConfBrowse, &QPushButton::clicked, this, [=]() { 31 | QString rcloneConf = QFileDialog::getOpenFileName( 32 | this, "Select .rclone.conf location", ui.rcloneConf->text()); 33 | if (rcloneConf.isEmpty()) { 34 | return; 35 | } 36 | 37 | ui.rcloneConf->setText(rcloneConf); 38 | }); 39 | 40 | QObject::connect( 41 | ui.defaultDownloadDirBrowse, &QPushButton::clicked, this, [=]() { 42 | QString defaultDownloadDir = QFileDialog::getExistingDirectory( 43 | this, "Select default download directory", 44 | ui.defaultDownloadDir->text()); 45 | 46 | if (defaultDownloadDir.isEmpty()) { 47 | return; 48 | } 49 | 50 | ui.defaultDownloadDir->setText(defaultDownloadDir); 51 | }); 52 | 53 | QObject::connect( 54 | ui.defaultUploadDirBrowse, &QPushButton::clicked, this, [=]() { 55 | QString defaultUploadDir = QFileDialog::getExistingDirectory( 56 | this, "Select default upload directory", 57 | ui.defaultUploadDir->text()); 58 | 59 | if (defaultUploadDir.isEmpty()) { 60 | return; 61 | } 62 | 63 | ui.defaultUploadDir->setText(defaultUploadDir); 64 | }); 65 | 66 | auto settings = GetSettings(); 67 | ui.rclone->setText( 68 | QDir::toNativeSeparators(settings->value("Settings/rclone").toString())); 69 | ui.rcloneConf->setText(QDir::toNativeSeparators( 70 | settings->value("Settings/rcloneConf").toString())); 71 | ui.stream->setText(settings->value("Settings/stream").toString()); 72 | 73 | #if defined(Q_OS_OPENBSD) || defined(Q_OS_NETBSD) 74 | ui.mount->setText( 75 | settings 76 | ->value("Settings/mount", 77 | "* mount is not supported by rclone on this system *") 78 | .toString()); 79 | ui.mount->setDisabled(true); 80 | #else 81 | ui.mount->setText( 82 | settings->value("Settings/mount", "--vfs-cache-mode writes").toString()); 83 | #endif 84 | 85 | ui.defaultDownloadDir->setText(QDir::toNativeSeparators( 86 | settings->value("Settings/defaultDownloadDir").toString())); 87 | ui.defaultUploadDir->setText(QDir::toNativeSeparators( 88 | settings->value("Settings/defaultUploadDir").toString())); 89 | ui.defaultDownloadOptions->setText( 90 | settings->value("Settings/defaultDownloadOptions").toString()); 91 | ui.defaultUploadOptions->setText( 92 | settings->value("Settings/defaultUploadOptions").toString()); 93 | ui.defaultRcloneOptions->setText( 94 | settings->value("Settings/defaultRcloneOptions").toString()); 95 | 96 | ui.checkRcloneBrowserUpdates->setChecked( 97 | settings->value("Settings/checkRcloneBrowserUpdates", true).toBool()); 98 | ui.checkRcloneUpdates->setChecked( 99 | settings->value("Settings/checkRcloneUpdates", true).toBool()); 100 | 101 | if (QSystemTrayIcon::isSystemTrayAvailable()) { 102 | ui.alwaysShowInTray->setChecked( 103 | settings->value("Settings/alwaysShowInTray", false).toBool()); 104 | ui.closeToTray->setChecked( 105 | settings->value("Settings/closeToTray", false).toBool()); 106 | ui.notifyFinishedTransfers->setChecked( 107 | settings->value("Settings/notifyFinishedTransfers", true).toBool()); 108 | } else { 109 | ui.alwaysShowInTray->setChecked(false); 110 | ui.alwaysShowInTray->setDisabled(true); 111 | ui.closeToTray->setChecked(false); 112 | ui.closeToTray->setDisabled(true); 113 | ui.notifyFinishedTransfers->setChecked(false); 114 | ui.notifyFinishedTransfers->setDisabled(true); 115 | } 116 | 117 | ui.showFolderIcons->setChecked( 118 | settings->value("Settings/showFolderIcons", true).toBool()); 119 | ui.showFileIcons->setChecked( 120 | settings->value("Settings/showFileIcons", true).toBool()); 121 | ui.rowColors->setChecked( 122 | settings->value("Settings/rowColors", true).toBool()); 123 | ui.showHidden->setChecked( 124 | settings->value("Settings/showHidden", true).toBool()); 125 | ui.darkMode->setChecked(settings->value("Settings/darkMode", true).toBool()); 126 | 127 | // dark mode option for all systems but latest macOS 128 | // on macOS Mojave or newer dark mode is managed by OS 129 | #if defined(Q_OS_MACOS) 130 | QString sysInfo = QSysInfo::productVersion(); 131 | if (sysInfo == "10.9" || sysInfo == "10.10" || sysInfo == "10.11" || 132 | sysInfo == "10.12" || sysInfo == "10.13") { 133 | } else { 134 | ui.darkMode->hide(); 135 | ui.darkMode_info->hide(); 136 | } 137 | #endif 138 | 139 | if ((settings->value("Settings/iconSize").toString()) == "small") { 140 | ui.cb_small->setChecked(true); 141 | } else { 142 | if (settings->value("Settings/iconSize").toString() == "large") { 143 | ui.cb_large->setChecked(true); 144 | } else { 145 | ui.cb_medium->setChecked(true); 146 | } 147 | } 148 | 149 | ui.info_2->setText( 150 | "See rclone FAQ for details."); 153 | ui.info_2->setTextFormat(Qt::RichText); 154 | ui.info_2->setTextInteractionFlags(Qt::TextBrowserInteraction); 155 | ui.info_2->setOpenExternalLinks(true); 156 | 157 | if (settings->value("Settings/useProxy").toBool()) { 158 | ui.useProxy->setChecked(true); 159 | } else { 160 | ui.useSystemSettings->setChecked(true); 161 | } 162 | ui.http_proxy->setText(settings->value("Settings/http_proxy").toString()); 163 | ui.https_proxy->setText(settings->value("Settings/https_proxy").toString()); 164 | ui.no_proxy->setText(settings->value("Settings/no_proxy").toString()); 165 | } 166 | 167 | PreferencesDialog::~PreferencesDialog() {} 168 | 169 | QString PreferencesDialog::getRclone() const { 170 | return QDir::fromNativeSeparators(ui.rclone->text()); 171 | } 172 | 173 | QString PreferencesDialog::getRcloneConf() const { 174 | return QDir::fromNativeSeparators(ui.rcloneConf->text()); 175 | } 176 | 177 | QString PreferencesDialog::getStream() const { return ui.stream->text(); } 178 | 179 | QString PreferencesDialog::getMount() const { return ui.mount->text(); } 180 | 181 | QString PreferencesDialog::getDefaultDownloadDir() const { 182 | return QDir::fromNativeSeparators(ui.defaultDownloadDir->text()); 183 | } 184 | 185 | QString PreferencesDialog::getDefaultUploadDir() const { 186 | return QDir::fromNativeSeparators(ui.defaultUploadDir->text()); 187 | } 188 | 189 | QString PreferencesDialog::getDefaultDownloadOptions() const { 190 | return ui.defaultDownloadOptions->text(); 191 | } 192 | 193 | QString PreferencesDialog::getDefaultUploadOptions() const { 194 | return ui.defaultUploadOptions->text(); 195 | } 196 | 197 | QString PreferencesDialog::getDefaultRcloneOptions() const { 198 | return ui.defaultRcloneOptions->text(); 199 | } 200 | 201 | bool PreferencesDialog::getCheckRcloneBrowserUpdates() const { 202 | return ui.checkRcloneBrowserUpdates->isChecked(); 203 | } 204 | 205 | bool PreferencesDialog::getCheckRcloneUpdates() const { 206 | return ui.checkRcloneUpdates->isChecked(); 207 | } 208 | 209 | bool PreferencesDialog::getAlwaysShowInTray() const { 210 | return ui.alwaysShowInTray->isChecked(); 211 | } 212 | 213 | bool PreferencesDialog::getCloseToTray() const { 214 | return ui.closeToTray->isChecked(); 215 | } 216 | 217 | bool PreferencesDialog::getNotifyFinishedTransfers() const { 218 | return ui.notifyFinishedTransfers->isChecked(); 219 | } 220 | 221 | bool PreferencesDialog::getShowFolderIcons() const { 222 | return ui.showFolderIcons->isChecked(); 223 | } 224 | 225 | bool PreferencesDialog::getShowFileIcons() const { 226 | return ui.showFileIcons->isChecked(); 227 | } 228 | 229 | bool PreferencesDialog::getRowColors() const { 230 | return ui.rowColors->isChecked(); 231 | } 232 | 233 | bool PreferencesDialog::getShowHidden() const { 234 | return ui.showHidden->isChecked(); 235 | } 236 | 237 | bool PreferencesDialog::getDarkMode() const { return ui.darkMode->isChecked(); } 238 | 239 | QString PreferencesDialog::getIconSize() const { 240 | if (ui.cb_small->isChecked()) { 241 | return "small"; 242 | } else { 243 | if (ui.cb_large->isChecked()) { 244 | return "large"; 245 | } else { 246 | return "medium"; 247 | } 248 | } 249 | } 250 | 251 | QString PreferencesDialog::getHttpProxy() const { 252 | return ui.http_proxy->text(); 253 | } 254 | 255 | QString PreferencesDialog::getHttpsProxy() const { 256 | return ui.https_proxy->text(); 257 | } 258 | 259 | QString PreferencesDialog::getNoProxy() const { return ui.no_proxy->text(); } 260 | 261 | bool PreferencesDialog::getUseProxy() const { 262 | if (ui.useSystemSettings->isChecked()) { 263 | return false; 264 | } else { 265 | return true; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /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 | Q_OBJECT 8 | 9 | public: 10 | PreferencesDialog(QWidget *parent = nullptr); 11 | ~PreferencesDialog(); 12 | 13 | QString getRclone() const; 14 | QString getRcloneConf() const; 15 | QString getStream() const; 16 | QString getMount() const; 17 | QString getDefaultDownloadDir() const; 18 | QString getDefaultUploadDir() const; 19 | QString getDefaultDownloadOptions() const; 20 | QString getDefaultUploadOptions() const; 21 | QString getDefaultRcloneOptions() const; 22 | 23 | bool getCheckRcloneBrowserUpdates() const; 24 | bool getCheckRcloneUpdates() const; 25 | 26 | bool getAlwaysShowInTray() const; 27 | bool getCloseToTray() const; 28 | bool getNotifyFinishedTransfers() const; 29 | 30 | bool getShowFolderIcons() const; 31 | bool getShowFileIcons() const; 32 | bool getRowColors() const; 33 | bool getShowHidden() const; 34 | bool getDarkMode() const; 35 | QString getIconSize() const; 36 | 37 | bool getUseProxy() const; 38 | QString getHttpProxy() const; 39 | QString getHttpsProxy() const; 40 | QString getNoProxy() const; 41 | 42 | private: 43 | Ui::PreferencesDialog ui; 44 | }; 45 | -------------------------------------------------------------------------------- /src/progress_dialog.cpp: -------------------------------------------------------------------------------- 1 | #include "progress_dialog.h" 2 | 3 | ProgressDialog::ProgressDialog(const QString &title, const QString &operation, 4 | const QString &message, QProcess *process, 5 | QWidget *parent, bool close, bool trim) 6 | : QDialog(parent) { 7 | ui.setupUi(this); 8 | resize(width(), 0); 9 | 10 | setWindowTitle(title); 11 | ui.labelOperation->setText(operation); 12 | ui.labelInfo->setText(message); 13 | 14 | ui.output->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); 15 | ui.output->setVisible(false); 16 | 17 | QObject::connect(ui.buttonBox, &QDialogButtonBox::rejected, this, 18 | &QDialog::reject); 19 | 20 | QObject::connect(ui.buttonShowOutput, &QPushButton::toggled, this, 21 | [=](bool checked) { 22 | ui.output->setVisible(checked); 23 | ui.buttonShowOutput->setArrowType( 24 | checked ? Qt::DownArrow : Qt::RightArrow); 25 | if (!checked) { 26 | adjustSize(); 27 | } 28 | }); 29 | 30 | QObject::connect(process, 31 | static_cast( 32 | &QProcess::finished), 33 | this, [=](int code, QProcess::ExitStatus status) { 34 | if (status == QProcess::NormalExit && code == 0) { 35 | if (close) { 36 | emit accept(); 37 | } 38 | } else { 39 | ui.buttonShowOutput->setChecked(true); 40 | ui.buttonBox->setEnabled(true); 41 | } 42 | }); 43 | 44 | QObject::connect(process, &QProcess::readyRead, this, [=]() { 45 | QString output = process->readAll(); 46 | if (trim) { 47 | output = output.trimmed(); 48 | } 49 | ui.output->appendPlainText(output); 50 | emit outputAvailable(output); 51 | }); 52 | 53 | process->setProcessChannelMode(QProcess::MergedChannels); 54 | process->start(QIODevice::ReadOnly); 55 | } 56 | 57 | ProgressDialog::~ProgressDialog() {} 58 | 59 | void ProgressDialog::expand() { ui.buttonShowOutput->setChecked(true); } 60 | 61 | void ProgressDialog::allowToClose() { ui.buttonBox->setEnabled(true); } 62 | // 63 | // QString ProgressDialog::getOutput() const 64 | //{ 65 | // return ui.output->toPlainText(); 66 | //} 67 | -------------------------------------------------------------------------------- /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 | Q_OBJECT 8 | 9 | public: 10 | ProgressDialog(const QString &title, const QString &operation, 11 | const QString &message, QProcess *process, 12 | QWidget *parent = nullptr, bool close = true, 13 | bool trim = false); 14 | ~ProgressDialog(); 15 | 16 | void expand(); 17 | void allowToClose(); 18 | 19 | signals: 20 | void outputAvailable(const QString &output) const; 21 | 22 | private: 23 | Ui::ProgressDialog ui; 24 | }; 25 | -------------------------------------------------------------------------------- /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.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 | Q_OBJECT 10 | 11 | public: 12 | RemoteWidget(IconCache *icons, const QString &remote, bool isLocal, 13 | bool isGoogle, QWidget *parent = nullptr); 14 | ~RemoteWidget(); 15 | 16 | signals: 17 | void addTransfer(const QString &message, const QString &source, 18 | const QString &remote, const QStringList &args); 19 | void addMount(const QString &remote, const QString &folder); 20 | void addStream(const QString &remote, const QString &stream); 21 | 22 | private: 23 | Ui::RemoteWidget ui; 24 | }; 25 | -------------------------------------------------------------------------------- /src/remote_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RemoteWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 912 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 | M&ove 88 | 89 | 90 | Qt::ToolButtonTextBesideIcon 91 | 92 | 93 | 94 | 95 | 96 | 97 | De&lete 98 | 99 | 100 | Qt::ToolButtonTextBesideIcon 101 | 102 | 103 | 104 | 105 | 106 | 107 | &Mount 108 | 109 | 110 | Qt::ToolButtonTextBesideIcon 111 | 112 | 113 | 114 | 115 | 116 | 117 | &Stream 118 | 119 | 120 | Qt::ToolButtonTextBesideIcon 121 | 122 | 123 | 124 | 125 | 126 | 127 | &Upload... 128 | 129 | 130 | Qt::ToolButtonTextBesideIcon 131 | 132 | 133 | 134 | 135 | 136 | 137 | &Download... 138 | 139 | 140 | Qt::ToolButtonTextBesideIcon 141 | 142 | 143 | 144 | 145 | 146 | 147 | &Get Size... 148 | 149 | 150 | Qt::ToolButtonTextBesideIcon 151 | 152 | 153 | 154 | 155 | 156 | 157 | &Tree 158 | 159 | 160 | Qt::ToolButtonTextBesideIcon 161 | 162 | 163 | 164 | 165 | 166 | 167 | &Link 168 | 169 | 170 | Qt::ToolButtonTextBesideIcon 171 | 172 | 173 | 174 | 175 | 176 | 177 | E&xport... 178 | 179 | 180 | Qt::ToolButtonTextBesideIcon 181 | 182 | 183 | 184 | 185 | 186 | 187 | Qt::Horizontal 188 | 189 | 190 | 191 | 40 192 | 20 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | Shared with Me (Google Drive) 201 | 202 | 203 | Shared 204 | 205 | 206 | 207 | 208 | 209 | 210 | Qt::Horizontal 211 | 212 | 213 | QSizePolicy::Minimum 214 | 215 | 216 | 217 | 10 218 | 20 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | true 230 | 231 | 232 | 233 | 234 | 235 | 236 | Qt::CustomContextMenu 237 | 238 | 239 | true 240 | 241 | 242 | QAbstractItemView::NoEditTriggers 243 | 244 | 245 | true 246 | 247 | 248 | Qt::CopyAction 249 | 250 | 251 | QAbstractItemView::SingleSelection 252 | 253 | 254 | QAbstractItemView::SelectRows 255 | 256 | 257 | true 258 | 259 | 260 | true 261 | 262 | 263 | false 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | &Refresh 275 | 276 | 277 | F5 278 | 279 | 280 | 281 | 282 | &Mount 283 | 284 | 285 | rclone mount 286 | 287 | 288 | 289 | 290 | &Stream 291 | 292 | 293 | 294 | 295 | &New Folder 296 | 297 | 298 | rclone mkdir 299 | 300 | 301 | F7 302 | 303 | 304 | 305 | 306 | R&ename 307 | 308 | 309 | rclone moveto 310 | 311 | 312 | F2 313 | 314 | 315 | 316 | 317 | De&lete 318 | 319 | 320 | rclone purge|delete 321 | 322 | 323 | Del 324 | 325 | 326 | 327 | 328 | &Upload 329 | 330 | 331 | 332 | 333 | &Download 334 | 335 | 336 | 337 | 338 | &Get Size 339 | 340 | 341 | rclone size 342 | 343 | 344 | 345 | 346 | DirTree 347 | 348 | 349 | rclone tree 350 | 351 | 352 | 353 | 354 | E&xport 355 | 356 | 357 | Export files list 358 | 359 | 360 | 361 | 362 | M&ove 363 | 364 | 365 | rclone move 366 | 367 | 368 | 369 | 370 | Shared 371 | 372 | 373 | 374 | 375 | Public Link 376 | 377 | 378 | rclone link 379 | 380 | 381 | 382 | 383 | buttonRefresh 384 | buttonMkdir 385 | buttonRename 386 | buttonPurge 387 | buttonMount 388 | buttonStream 389 | buttonUpload 390 | buttonDownload 391 | buttonSize 392 | buttonTree 393 | path 394 | tree 395 | 396 | 397 | 398 | 399 | -------------------------------------------------------------------------------- /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/mega.png 15 | images/onedrive.png 16 | images/s3.png 17 | images/ftp.png 18 | images/sftp.png 19 | images/swift.png 20 | images/unknown.png 21 | images/yandex.png 22 | images/azureblob.png 23 | images/google_photos.png 24 | images/amazon_cloud_drive_inv.png 25 | images/b2_inv.png 26 | images/crypt_inv.png 27 | images/drive_inv.png 28 | images/dropbox_inv.png 29 | images/google_cloud_storage_inv.png 30 | images/hubic_inv.png 31 | images/local_inv.png 32 | images/mega_inv.png 33 | images/onedrive_inv.png 34 | images/s3_inv.png 35 | images/ftp_inv.png 36 | images/sftp_inv.png 37 | images/swift_inv.png 38 | images/unknown_inv.png 39 | images/yandex_inv.png 40 | images/azureblob_inv.png 41 | images/google_photos_inv.png 42 | 43 | 44 | -------------------------------------------------------------------------------- /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, 4 | const QString &remote, const QString &stream, 5 | QWidget *parent) 6 | : QWidget(parent), mRclone(rclone), mPlayer(player) { 7 | ui.setupUi(this); 8 | 9 | ui.remote->setText(remote); 10 | ui.stream->setText(stream); 11 | ui.info->setText(remote); 12 | 13 | ui.details->setVisible(false); 14 | 15 | ui.output->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); 16 | ui.output->setVisible(false); 17 | 18 | QObject::connect( 19 | ui.showDetails, &QToolButton::toggled, this, [=](bool checked) { 20 | ui.details->setVisible(checked); 21 | ui.showDetails->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 22 | }); 23 | 24 | QObject::connect( 25 | ui.showOutput, &QToolButton::toggled, this, [=](bool checked) { 26 | ui.output->setVisible(checked); 27 | ui.showOutput->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); 28 | }); 29 | 30 | ui.cancel->setIcon( 31 | QApplication::style()->standardIcon(QStyle::SP_DialogCloseButton)); 32 | 33 | QObject::connect(ui.cancel, &QToolButton::clicked, this, [=]() { 34 | if (mRunning) { 35 | int button = QMessageBox::question( 36 | this, "Stop", QString("Do you want to stop %1 stream?").arg(remote), 37 | QMessageBox::Yes | QMessageBox::No); 38 | if (button == QMessageBox::Yes) { 39 | cancel(); 40 | } 41 | } else { 42 | emit closed(); 43 | } 44 | }); 45 | 46 | QObject::connect(mRclone, &QProcess::readyRead, this, [=]() { 47 | while (mRclone->canReadLine()) { 48 | ui.output->appendPlainText(mRclone->readLine().trimmed()); 49 | } 50 | }); 51 | 52 | QObject::connect(mRclone, 53 | static_cast( 54 | &QProcess::finished), 55 | this, [=]() { 56 | mRclone->deleteLater(); 57 | mRunning = false; 58 | emit finished(); 59 | emit closed(); 60 | }); 61 | 62 | ui.showDetails->setStyleSheet("QToolButton { border: 0; color: green; }"); 63 | ui.showDetails->setText("Streaming"); 64 | } 65 | 66 | StreamWidget::~StreamWidget() {} 67 | 68 | void StreamWidget::cancel() { 69 | if (!mRunning) { 70 | return; 71 | } 72 | 73 | mPlayer->terminate(); 74 | mRclone->kill(); 75 | mRclone->waitForFinished(); 76 | } 77 | -------------------------------------------------------------------------------- /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 | Q_OBJECT 8 | 9 | public: 10 | StreamWidget(QProcess *rclone, QProcess *player, const QString &remote, 11 | 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.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "job_options.h" 4 | #include "pch.h" 5 | #include "ui_transfer_dialog.h" 6 | 7 | class TransferDialog : public QDialog { 8 | Q_OBJECT 9 | 10 | public: 11 | TransferDialog(bool isDownload, bool isDrop, const QString &remote, 12 | const QDir &path, bool isFolder, QWidget *parent = nullptr, 13 | JobOptions *task = nullptr, bool editMode = false); 14 | ~TransferDialog(); 15 | 16 | void setSource(const QString &path); 17 | 18 | QString getMode() const; 19 | QString getSource() const; 20 | QString getDest() const; 21 | QStringList getOptions(); 22 | 23 | JobOptions *getJobOptions(); 24 | 25 | private: 26 | Ui::TransferDialog ui; 27 | 28 | bool mIsDownload; 29 | bool mDryRun = false; 30 | bool mIsFolder; 31 | bool mIsEditMode; 32 | 33 | JobOptions *mJobOptions; 34 | 35 | void putJobOptions(); 36 | 37 | void done(int r) override; 38 | 39 | signals: 40 | void tasksListChanged(); 41 | }; 42 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | static QString gRclone; 4 | static QString gRcloneConf; 5 | static QString gRclonePassword; 6 | 7 | // Software versions comparison 8 | // source: https://helloacm.com/how-to-compare-version-numbers-in-c/ 9 | std::vector split(const std::string &s, char d) { 10 | std::vector r; 11 | int j = 0; 12 | for (unsigned int i = 0; i < s.length(); i++) { 13 | if (s[i] == d) { 14 | r.push_back(s.substr(j, i - j)); 15 | j = i + 1; 16 | } 17 | } 18 | r.push_back(s.substr(j)); 19 | return r; 20 | } 21 | 22 | unsigned int compareVersion(const std::string &version1, const std::string &version2) { 23 | auto v1 = split(version1, '.'); 24 | auto v2 = split(version2, '.'); 25 | unsigned int max = v1.size() > v2.size() ? v1.size() : v2.size(); 26 | // pad the shorter version string 27 | if (v1.size() != max) { 28 | for (unsigned int i = max - v1.size(); i--;) { 29 | v1.push_back("0"); 30 | } 31 | } else { 32 | for (unsigned int i = max - v2.size(); i--;) { 33 | v2.push_back("0"); 34 | } 35 | } 36 | for (unsigned int i = 0; i < max; i++) { 37 | unsigned int n1 = stoi(v1[i]); 38 | unsigned int n2 = stoi(v2[i]); 39 | if (n1 > n2) { 40 | // version1 is higher than version2 41 | return 1; 42 | } else if (n1 < n2) { 43 | // version2 is higher than version1 44 | return 2; 45 | } 46 | } 47 | // the same versions 48 | return 0; 49 | } 50 | 51 | static QString GetIniFilename() { 52 | #ifdef Q_OS_MACOS 53 | QFileInfo applicationPath = qApp->applicationFilePath(); 54 | // qDebug() << QString(applicationPath.absolutePath()); 55 | // on macOS excecutable file is located in 56 | // ./rclone-browser.app/Contents/MasOS/ to get actual bundle folder we have to 57 | // traverse three levels up 58 | QFileInfo MacOSPath = applicationPath.dir().path(); 59 | QFileInfo ContentsPath = MacOSPath.dir().path(); 60 | QFileInfo appBundlePath = ContentsPath.dir().path(); 61 | // qDebug() << QString("utils.cpp appBundle.absolutePath: " + 62 | // appBundlePath.absolutePath()); 63 | // qDebug() << QString( 64 | // "utils.cpp ini file:" + 65 | // appBundlePath.dir().filePath(appBundlePath.baseName() + ".ini")); 66 | return appBundlePath.dir().filePath(appBundlePath.baseName() + ".ini"); 67 | #else 68 | #ifdef Q_OS_WIN 69 | QFileInfo applicationPath = qApp->applicationFilePath(); 70 | return applicationPath.dir().filePath(applicationPath.baseName() + ".ini"); 71 | #else 72 | QString xdg_config_home = qgetenv("XDG_CONFIG_HOME"); 73 | return xdg_config_home + "/rclone-browser/rclone-browser.ini"; 74 | #endif 75 | #endif 76 | } 77 | 78 | bool IsPortableMode() { 79 | QString ini = GetIniFilename(); 80 | QString xdg_config_home = qgetenv("XDG_CONFIG_HOME"); 81 | // qDebug() << QString("utils.cpp $XDG_CONFIG_HOME: " + xdg_config_home); 82 | QString appimage = qgetenv("APPIMAGE"); 83 | // qDebug() << QString("utils.cpp $APPIMAGE: " + appimage); 84 | 85 | // cat ".config" from $XDG_CONFIG_HOME 86 | // it should be the same as appimage if run from AppImage 87 | xdg_config_home = xdg_config_home.left(xdg_config_home.length() - 7); 88 | // qDebug() << QString("utils.cpp $XDG_CONFIG_HOME-7: " + xdg_config_home); 89 | 90 | if (!xdg_config_home.isEmpty() && !appimage.isEmpty() && 91 | xdg_config_home == appimage) { 92 | 93 | return true; 94 | } 95 | 96 | if (QFileInfo(ini).exists()) { 97 | 98 | return true; 99 | } else { 100 | return false; 101 | } 102 | 103 | // return QFileInfo(ini).exists(); 104 | } 105 | 106 | std::unique_ptr GetSettings() { 107 | if (IsPortableMode()) { 108 | return std::make_unique( 109 | GetIniFilename(), QSettings::IniFormat); 110 | } 111 | return std::make_unique(); 112 | } 113 | 114 | void ReadSettings(QSettings *settings, QObject *widget) { 115 | QString name = widget->objectName(); 116 | if (!name.isEmpty() && settings->contains(name)) { 117 | if (QRadioButton *obj = qobject_cast(widget)) { 118 | obj->setChecked(settings->value(name).toBool()); 119 | return; 120 | } 121 | if (QCheckBox *obj = qobject_cast(widget)) { 122 | obj->setChecked(settings->value(name).toBool()); 123 | return; 124 | } 125 | if (QComboBox *obj = qobject_cast(widget)) { 126 | obj->setCurrentIndex(settings->value(name).toInt()); 127 | return; 128 | } 129 | if (QSpinBox *obj = qobject_cast(widget)) { 130 | obj->setValue(settings->value(name).toInt()); 131 | return; 132 | } 133 | if (QLineEdit *obj = qobject_cast(widget)) { 134 | obj->setText(settings->value(name).toString()); 135 | return; 136 | } 137 | if (QPlainTextEdit *obj = qobject_cast(widget)) { 138 | int count = settings->beginReadArray(name); 139 | QStringList lines; 140 | lines.reserve(count); 141 | for (int i = 0; i < count; i++) { 142 | settings->setArrayIndex(i); 143 | lines.append(settings->value("value").toString()); 144 | } 145 | settings->endArray(); 146 | 147 | obj->setPlainText(lines.join('\n')); 148 | return; 149 | } 150 | } 151 | 152 | for (auto child : widget->children()) { 153 | ReadSettings(settings, child); 154 | } 155 | } 156 | 157 | void WriteSettings(QSettings *settings, QObject *widget) { 158 | QString name = widget->objectName(); 159 | if (QCheckBox *obj = qobject_cast(widget)) { 160 | settings->setValue(name, obj->isChecked()); 161 | return; 162 | } 163 | if (QComboBox *obj = qobject_cast(widget)) { 164 | settings->setValue(name, obj->currentIndex()); 165 | return; 166 | } 167 | if (QSpinBox *obj = qobject_cast(widget)) { 168 | settings->setValue(name, obj->value()); 169 | return; 170 | } 171 | if (QLineEdit *obj = qobject_cast(widget)) { 172 | if (obj->text().isEmpty()) { 173 | settings->remove(name); 174 | } else { 175 | settings->setValue(name, obj->text()); 176 | } 177 | return; 178 | } 179 | if (QPlainTextEdit *obj = qobject_cast(widget)) { 180 | QString text = obj->toPlainText().trimmed(); 181 | if (!text.isEmpty()) { 182 | QStringList lines = text.split('\n'); 183 | settings->beginWriteArray(name, lines.size()); 184 | for (int i = 0; i < lines.count(); i++) { 185 | settings->setArrayIndex(i); 186 | settings->setValue("value", lines[i]); 187 | } 188 | settings->endArray(); 189 | } 190 | return; 191 | } 192 | 193 | for (auto child : widget->children()) { 194 | WriteSettings(settings, child); 195 | } 196 | } 197 | 198 | QStringList GetRcloneConf() { 199 | if (gRcloneConf.isEmpty()) { 200 | return QStringList(); 201 | } 202 | 203 | QString conf = gRcloneConf; 204 | if (IsPortableMode() && QFileInfo(conf).isRelative()) { 205 | #ifdef Q_OS_MACOS 206 | // on macOS excecutable file is located in 207 | // ./rclone-browser.app/Contents/MasOS/rclone-browser to get actual bundle 208 | // folder we have to traverse three levels up 209 | conf = QDir(qApp->applicationDirPath() + "/../../..").filePath(conf); 210 | #else 211 | #ifdef Q_OS_WIN 212 | conf = QDir(qApp->applicationDirPath()).filePath(conf); 213 | #else 214 | QString xdg_config_home = qgetenv("XDG_CONFIG_HOME"); 215 | conf = QDir(xdg_config_home + "/..").filePath(conf); 216 | #endif 217 | #endif 218 | // qDebug() << QString("utils.cpp conf: " + conf); 219 | } 220 | return QStringList() << "--config" << conf; 221 | } 222 | 223 | void SetRcloneConf(const QString &rcloneConf) { gRcloneConf = rcloneConf; } 224 | 225 | QString GetRclone() { 226 | QString rclone = gRclone; 227 | if (IsPortableMode() && QFileInfo(rclone).isRelative()) { 228 | #ifdef Q_OS_MACOS 229 | // on macOS excecutable file is located in 230 | // ./rclone-browser.app/Contents/MasOS/rclone-browser to get actual bundle 231 | // folder we have to traverse three levels up 232 | rclone = QDir(qApp->applicationDirPath() + "/../../..").filePath(rclone); 233 | #else 234 | #ifdef Q_OS_WIN 235 | rclone = QDir(qApp->applicationDirPath()).filePath(rclone); 236 | #else 237 | QString xdg_config_home = qgetenv("XDG_CONFIG_HOME"); 238 | rclone = QDir(xdg_config_home + "/..").filePath(rclone); 239 | #endif 240 | #endif 241 | // qDebug() << QString("utils.cpp rclone portable: " + rclone); 242 | } 243 | 244 | return rclone; 245 | } 246 | 247 | void SetRclone(const QString &rclone) { gRclone = rclone.trimmed(); } 248 | 249 | void UseRclonePassword(QProcess *process) { 250 | if (!gRclonePassword.isEmpty()) { 251 | QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); 252 | env.insert("RCLONE_CONFIG_PASS", gRclonePassword); 253 | process->setProcessEnvironment(env); 254 | } 255 | } 256 | 257 | void SetRclonePassword(const QString &rclonePassword) { 258 | gRclonePassword = rclonePassword; 259 | } 260 | 261 | QStringList GetDriveSharedWithMe() { 262 | auto settings = GetSettings(); 263 | bool driveShared = settings->value("Settings/driveShared", false).toBool(); 264 | QStringList driveSharedOption; 265 | if (driveShared) { 266 | driveSharedOption << "--drive-shared-with-me"; 267 | } 268 | return driveSharedOption; 269 | } 270 | 271 | QStringList GetDefaultRcloneOptionsList() { 272 | auto settings = GetSettings(); 273 | QString defaultRcloneOptions = 274 | settings->value("Settings/defaultRcloneOptions").toString(); 275 | QStringList defaultRcloneOptionsList; 276 | if (!defaultRcloneOptions.isEmpty()) { 277 | for (const auto &arg : defaultRcloneOptions.split(' ')) { 278 | defaultRcloneOptionsList << arg; 279 | } 280 | } 281 | return defaultRcloneOptionsList; 282 | } 283 | 284 | QStringList GetShowHidden() { 285 | auto settings = GetSettings(); 286 | bool showHidden = settings->value("Settings/showHidden", true).toBool(); 287 | QStringList showHiddenOption; 288 | if (!showHidden) { 289 | showHiddenOption << "--exclude" 290 | << ".*/**" 291 | << "--exclude" 292 | << ".*"; 293 | } 294 | return showHiddenOption; 295 | } 296 | -------------------------------------------------------------------------------- /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 | bool IsPortableMode(); 11 | 12 | QString GetRclone(); 13 | void SetRclone(const QString &rclone); 14 | 15 | QStringList GetRcloneConf(); 16 | void SetRcloneConf(const QString &rcloneConf); 17 | 18 | void UseRclonePassword(QProcess *process); 19 | void SetRclonePassword(const QString &rclonePassword); 20 | 21 | QStringList GetDriveSharedWithMe(); 22 | QStringList GetDefaultRcloneOptionsList(); 23 | QStringList GetShowHidden(); 24 | 25 | unsigned int compareVersion(const std::string &version1, const std::string &version2); 26 | --------------------------------------------------------------------------------