├── .github └── workflows │ ├── develop.yml │ └── release.yml ├── .gitignore ├── CMakeLists.txt ├── README.md ├── desktop └── gotify-tray-cpp.desktop ├── gotify-tray++.rc ├── images ├── settings-dark.png ├── settings-light.png ├── window-dark.png └── window-light.png ├── res ├── icons │ ├── gotify-tray++.ico │ ├── qt.png │ ├── tray-error.png │ ├── tray-unread.png │ └── tray.png └── themes │ ├── dark │ ├── ServerInfoDialog.qss │ ├── refresh.svg │ ├── status_active.svg │ ├── status_connecting.svg │ ├── status_error.svg │ ├── status_inactive.svg │ └── trashcan.svg │ └── light │ ├── ServerInfoDialog.qss │ ├── refresh.svg │ ├── status_active.svg │ ├── status_connecting.svg │ ├── status_error.svg │ ├── status_inactive.svg │ └── trashcan.svg ├── resources.qrc ├── scripts ├── build-win.ps1 └── gotify-tray++.iss └── src ├── applicationitem.cpp ├── applicationitem.h ├── applicationitemmodel.cpp ├── applicationitemmodel.h ├── appversion.cpp ├── appversion.h ├── cache.cpp ├── cache.h ├── clickablelabel.cpp ├── clickablelabel.h ├── gotifyapi.cpp ├── gotifyapi.h ├── gotifymodels.cpp ├── gotifymodels.h ├── imagepopup.cpp ├── imagepopup.h ├── listener.cpp ├── listener.h ├── main.cpp ├── mainapplication.cpp ├── mainapplication.h ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui ├── messageitem.cpp ├── messageitem.h ├── messageitemmodel.cpp ├── messageitemmodel.h ├── messagewidget.cpp ├── messagewidget.h ├── messagewidget.ui ├── processthread.cpp ├── processthread.h ├── requesthandler.cpp ├── requesthandler.h ├── serverinfodialog.cpp ├── serverinfodialog.h ├── serverinfodialog.ui ├── settings.cpp ├── settings.h ├── settingsdialog.cpp ├── settingsdialog.h ├── settingsdialog.ui ├── statuswidget.cpp ├── statuswidget.h ├── tray.cpp ├── tray.h ├── utils.cpp └── utils.h /.github/workflows/develop.yml: -------------------------------------------------------------------------------- 1 | name: develop 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: windows-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | fetch-depth: 0 12 | - name: Install Qt 13 | uses: jurplel/install-qt-action@v3 14 | with: 15 | version: '6.5.2' 16 | host: 'windows' 17 | target: 'desktop' 18 | arch: 'win64_mingw' 19 | modules: 'qtwebsockets qtimageformats' 20 | dir: ${{ github.workspace }}/qt 21 | - uses: seanmiddleditch/gha-setup-ninja@master 22 | - name: build-package 23 | run: powershell -File scripts/build-win.ps1 24 | - name: create-installer 25 | run: iscc scripts/gotify-tray++.iss 26 | shell: cmd 27 | - uses: actions/upload-artifact@v4 28 | with: 29 | name: gotify-tray++-setup.exe 30 | path: dist/gotify-tray++-setup.exe 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - name: Install Qt 16 | uses: jurplel/install-qt-action@v3 17 | with: 18 | version: '6.5.2' 19 | host: 'windows' 20 | target: 'desktop' 21 | arch: 'win64_mingw' 22 | modules: 'qtwebsockets qtimageformats' 23 | dir: ${{ github.workspace }}/qt 24 | - uses: seanmiddleditch/gha-setup-ninja@master 25 | - name: build-package 26 | run: powershell -File scripts/build-win.ps1 27 | - name: create-installer 28 | run: iscc scripts/gotify-tray++.iss 29 | shell: cmd 30 | - uses: actions/upload-artifact@v4 31 | with: 32 | name: gotify-tray++-setup.exe 33 | path: dist/gotify-tray++-setup.exe 34 | 35 | release: 36 | runs-on: ubuntu-latest 37 | needs: build 38 | steps: 39 | - uses: actions/download-artifact@v4 40 | with: 41 | name: gotify-tray++-setup.exe 42 | - name: Rename installer 43 | run: | 44 | mv gotify-tray++-setup.exe gotify-tray++-setup_${{github.ref_name}}.exe 45 | - name: Release 46 | uses: marvinpinto/action-automatic-releases@latest 47 | with: 48 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 49 | prerelease: false 50 | files: | 51 | gotify-tray++-setup_${{github.ref_name}}.exe 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | /.qmake.cache 35 | /.qmake.stash 36 | 37 | # qtcreator generated files 38 | *.pro.user* 39 | CMakeLists.txt.user* 40 | 41 | # xemacs temporary files 42 | *.flc 43 | 44 | # Vim temporary files 45 | .*.swp 46 | 47 | # Visual Studio generated files 48 | *.ib_pdb_index 49 | *.idb 50 | *.ilk 51 | *.pdb 52 | *.sln 53 | *.suo 54 | *.vcproj 55 | *vcproj.*.*.user 56 | *.ncb 57 | *.sdf 58 | *.opensdf 59 | *.vcxproj 60 | *vcxproj.* 61 | 62 | # MinGW generated files 63 | *.Debug 64 | *.Release 65 | 66 | # Python byte code 67 | *.pyc 68 | 69 | # Binaries 70 | # -------- 71 | *.dll 72 | *.exe 73 | 74 | dist 75 | build 76 | package 77 | .vscode 78 | compile_commands.json 79 | .cache 80 | *.pem -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(gotify-tray++ VERSION 0.1 LANGUAGES CXX) 4 | 5 | set(CMAKE_AUTOUIC ON) 6 | set(CMAKE_AUTOMOC ON) 7 | set(CMAKE_AUTORCC ON) 8 | 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | find_package(KF6Notifications QUIET) 13 | if(KF6Notifications_FOUND) 14 | add_definitions(-DUSE_KDE) 15 | endif() 16 | 17 | find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network WebSockets Sql) 18 | find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network WebSockets Sql) 19 | 20 | if (WIN32) 21 | set(APP_ICON_RESOURCE_WINDOWS "${CMAKE_CURRENT_SOURCE_DIR}/gotify-tray++.rc") 22 | endif() 23 | 24 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) 25 | 26 | set(PROJECT_SOURCES 27 | src/main.cpp 28 | ) 29 | 30 | qt_add_executable(gotify-tray++ 31 | MANUAL_FINALIZATION 32 | ${PROJECT_SOURCES} 33 | ${APP_ICON_RESOURCE_WINDOWS} 34 | 35 | resources.qrc 36 | README.md 37 | src/tray.h src/tray.cpp 38 | src/mainapplication.h src/mainapplication.cpp 39 | src/statuswidget.h src/statuswidget.cpp 40 | src/settings.h src/settings.cpp 41 | src/messageitemmodel.h src/messageitemmodel.cpp 42 | src/gotifyapi.h src/gotifyapi.cpp 43 | src/requesthandler.h src/requesthandler.cpp 44 | src/gotifymodels.h src/gotifymodels.cpp 45 | src/messageitem.h src/messageitem.cpp 46 | src/listener.h src/listener.cpp 47 | src/applicationitem.h src/applicationitem.cpp 48 | src/utils.h src/utils.cpp 49 | src/applicationitemmodel.h src/applicationitemmodel.cpp 50 | src/cache.h src/cache.cpp 51 | src/mainwindow.h src/mainwindow.cpp src/mainwindow.ui 52 | src/messagewidget.h src/messagewidget.cpp src/messagewidget.ui 53 | src/serverinfodialog.h src/serverinfodialog.cpp src/serverinfodialog.ui 54 | src/settingsdialog.h src/settingsdialog.cpp src/settingsdialog.ui 55 | src/appversion.h src/appversion.cpp 56 | src/clickablelabel.h src/clickablelabel.cpp 57 | src/processthread.h src/processthread.cpp 58 | src/imagepopup.h src/imagepopup.cpp 59 | 60 | ) 61 | 62 | if(KF6Notifications_FOUND) 63 | target_link_libraries(gotify-tray++ PRIVATE 64 | KF6::Notifications 65 | ) 66 | endif() 67 | 68 | target_link_libraries(gotify-tray++ PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::WebSockets Qt${QT_VERSION_MAJOR}::Sql) 69 | 70 | # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. 71 | # If you are developing for iOS or macOS you should consider setting an 72 | # explicit, fixed bundle identifier manually though. 73 | if(${QT_VERSION} VERSION_LESS 6.1.0) 74 | set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.gotify-tray++) 75 | endif() 76 | set_target_properties(gotify-tray++ PROPERTIES 77 | ${BUNDLE_ID_OPTION} 78 | MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} 79 | MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 80 | MACOSX_BUNDLE TRUE 81 | WIN32_EXECUTABLE TRUE 82 | ) 83 | 84 | include(GNUInstallDirs) 85 | install(TARGETS gotify-tray++ 86 | BUNDLE DESTINATION . 87 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 88 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 89 | ) 90 | 91 | if(QT_VERSION_MAJOR EQUAL 6) 92 | qt_finalize_executable(gotify-tray++) 93 | endif() 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gotify Tray++ 2 | 3 | 4 | A tray notification application for receiving messages from a [Gotify server](https://github.com/gotify/server). 5 | 6 | 7 | ## Features 8 | 9 | - Receive gotify messages in the native notification area. 10 | - Reconnect after wake from sleep or losing network connection. 11 | - Disable notification banners for low priority messages. 12 | - Manually delete received messages. 13 | - Go through a history of all previously received messages. 14 | - Receive missed messages after losing network connection. 15 | 16 | 17 | ## Installation 18 | 19 | ### Windows 20 | 21 | [Download the latest release here.](https://github.com/seird/gotify-tray-cpp/releases/latest) 22 | 23 | ### Arch Linux 24 | 25 | Install from AUR package: 26 | 27 | ``` 28 | gotify-tray-cpp 29 | ``` 30 | 31 | 32 | ## Images 33 | 34 | 35 | ### Main window 36 | 37 | Light | Dark 38 | :-------------------------------------------------:|:---------------------------------------------------------: 39 | ![window-light](https://raw.githubusercontent.com/seird/gotify-tray-cpp/master/images/window-light.png) | ![window-dark](https://raw.githubusercontent.com/seird/gotify-tray-cpp/master/images/window-dark.png) 40 | 41 | 42 | ### Settings window 43 | 44 | Light | Dark 45 | :-------------------------------------------------:|:---------------------------------------------------------: 46 | ![settings-light](https://raw.githubusercontent.com/seird/gotify-tray-cpp/master/images/settings-light.png) | ![settings-dark](https://raw.githubusercontent.com/seird/gotify-tray-cpp/master/images/settings-dark.png) 47 | -------------------------------------------------------------------------------- /desktop/gotify-tray-cpp.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Gotify Tray++ 3 | Comment=A tray notification application for receiving messages from a Gotify server 4 | Exec=gotify-tray-cpp 5 | Icon=gotify-tray-cpp 6 | Type=Application 7 | Categories=Network; 8 | StartupWMClass=Gotify-Tray++ 9 | -------------------------------------------------------------------------------- /gotify-tray++.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON DISCARDABLE "res/icons/gotify-tray++.ico" 2 | 3 | 4 | 1 VERSIONINFO 5 | FILEVERSION 0,0,12,0 6 | PRODUCTVERSION 0,0,12,0 7 | { 8 | BLOCK "StringFileInfo" 9 | { 10 | BLOCK "000004B0" 11 | { 12 | VALUE "CompanyName", "Gotify Tray++\0" 13 | VALUE "FileDescription", "Gotify Tray++\0" 14 | VALUE "FileVersion", "0.0.12\0" 15 | VALUE "OriginalFilename", "gotify-tray++.exe\0" 16 | VALUE "ProductName", "Gotify Tray++\0" 17 | VALUE "ProductVersion", "0.0.12\0" 18 | } 19 | } 20 | BLOCK "VarFileInfo" 21 | { 22 | VALUE "Translation", 0x0000, 0x04B0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /images/settings-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seird/gotify-tray-cpp/1081cb6ffbb9da18f6f7d4a26023895c55d076ba/images/settings-dark.png -------------------------------------------------------------------------------- /images/settings-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seird/gotify-tray-cpp/1081cb6ffbb9da18f6f7d4a26023895c55d076ba/images/settings-light.png -------------------------------------------------------------------------------- /images/window-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seird/gotify-tray-cpp/1081cb6ffbb9da18f6f7d4a26023895c55d076ba/images/window-dark.png -------------------------------------------------------------------------------- /images/window-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seird/gotify-tray-cpp/1081cb6ffbb9da18f6f7d4a26023895c55d076ba/images/window-light.png -------------------------------------------------------------------------------- /res/icons/gotify-tray++.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seird/gotify-tray-cpp/1081cb6ffbb9da18f6f7d4a26023895c55d076ba/res/icons/gotify-tray++.ico -------------------------------------------------------------------------------- /res/icons/qt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seird/gotify-tray-cpp/1081cb6ffbb9da18f6f7d4a26023895c55d076ba/res/icons/qt.png -------------------------------------------------------------------------------- /res/icons/tray-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seird/gotify-tray-cpp/1081cb6ffbb9da18f6f7d4a26023895c55d076ba/res/icons/tray-error.png -------------------------------------------------------------------------------- /res/icons/tray-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seird/gotify-tray-cpp/1081cb6ffbb9da18f6f7d4a26023895c55d076ba/res/icons/tray-unread.png -------------------------------------------------------------------------------- /res/icons/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seird/gotify-tray-cpp/1081cb6ffbb9da18f6f7d4a26023895c55d076ba/res/icons/tray.png -------------------------------------------------------------------------------- /res/themes/dark/ServerInfoDialog.qss: -------------------------------------------------------------------------------- 1 | ServerInfoDialog QPushButton[state="success"] { 2 | background-color: #960b7a0b; 3 | color: white; 4 | } 5 | 6 | ServerInfoDialog QPushButton[state="success"]:!default:hover { 7 | background: #960b7a0b; 8 | } 9 | 10 | ServerInfoDialog QPushButton[state="failed"] { 11 | background-color: #8ebb2929; 12 | color: white; 13 | } 14 | 15 | ServerInfoDialog QPushButton[state="failed"]:!default:hover { 16 | background: #8ebb2929; 17 | } 18 | 19 | ServerInfoDialog QLineEdit[state="success"] {} 20 | 21 | ServerInfoDialog QLineEdit[state="failed"] { 22 | border: 1px solid red; 23 | } 24 | -------------------------------------------------------------------------------- /res/themes/dark/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /res/themes/dark/status_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /res/themes/dark/status_connecting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /res/themes/dark/status_error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /res/themes/dark/status_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /res/themes/dark/trashcan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/themes/light/ServerInfoDialog.qss: -------------------------------------------------------------------------------- 1 | ServerInfoDialog QPushButton[state="success"] { 2 | background-color: #6400FF00; 3 | color: black; 4 | } 5 | 6 | ServerInfoDialog QPushButton[state="success"]:!default:hover { 7 | background: #6400FF00; 8 | } 9 | 10 | ServerInfoDialog QPushButton[state="failed"] { 11 | background-color: #64FF0000; 12 | color: black; 13 | } 14 | 15 | ServerInfoDialog QPushButton[state="failed"]:!default:hover { 16 | background: #64FF0000; 17 | } 18 | 19 | ServerInfoDialog QLineEdit[state="success"] {} 20 | 21 | ServerInfoDialog QLineEdit[state="failed"] { 22 | border: 1px solid red; 23 | } 24 | -------------------------------------------------------------------------------- /res/themes/light/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /res/themes/light/status_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /res/themes/light/status_connecting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /res/themes/light/status_error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /res/themes/light/status_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /res/themes/light/trashcan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | res/icons/gotify-tray++.ico 4 | res/icons/qt.png 5 | res/icons/tray.png 6 | res/icons/tray-error.png 7 | res/icons/tray-unread.png 8 | res/themes/dark/refresh.svg 9 | res/themes/dark/status_active.svg 10 | res/themes/dark/status_connecting.svg 11 | res/themes/dark/status_error.svg 12 | res/themes/dark/status_inactive.svg 13 | res/themes/dark/ServerInfoDialog.qss 14 | res/themes/dark/trashcan.svg 15 | res/themes/light/refresh.svg 16 | res/themes/light/status_active.svg 17 | res/themes/light/status_connecting.svg 18 | res/themes/light/status_error.svg 19 | res/themes/light/status_inactive.svg 20 | res/themes/light/ServerInfoDialog.qss 21 | res/themes/light/trashcan.svg 22 | 23 | 24 | -------------------------------------------------------------------------------- /scripts/build-win.ps1: -------------------------------------------------------------------------------- 1 | $Qt_ROOT_DIR=$env:Qt6_DIR 2 | $SRC_DIR=(get-item $PSScriptRoot).parent.FullName 3 | $BUILD_DIR=$SRC_DIR + "/build" 4 | $PACKAGE_DIR=$SRC_DIR + "/package" 5 | Write-Host -BackgroundColor Yellow "Qt_ROOT_DIR: $Qt_ROOT_DIR" 6 | Write-Host -BackgroundColor Yellow "BUILD_DIR: $BUILD_DIR" 7 | Write-Host -BackgroundColor Yellow "PACKAGE_DIR: $PACKAGE_DIR" 8 | 9 | # ------------------------------------------------------------------------------ 10 | Write-Host -BackgroundColor Yellow "CLEANING" 11 | if (Test-Path $BUILD_DIR) {Remove-Item -LiteralPath $BUILD_DIR -Force -Recurse} 12 | if (Test-PATH $PACKAGE_DIR) {Remove-Item -LiteralPath $PACKAGE_DIR -Force -Recurse} 13 | 14 | # ------------------------------------------------------------------------------ 15 | Write-Host -BackgroundColor Yellow "BUILDING" 16 | cmake -S $SRC_DIR -B $BUILD_DIR -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$Qt_ROOT_DIR 17 | ninja -C $BUILD_DIR 18 | 19 | # ------------------------------------------------------------------------------ 20 | Write-Host -BackgroundColor Yellow "PACKAGING" 21 | # 0 - prepare dir 22 | cd $SRC_DIR 23 | New-Item -ItemType "directory" -Path $PACKAGE_DIR 24 | 25 | # 1 - copy gotify-tray++ build 26 | Copy-Item $BUILD_DIR/gotify-tray++.exe -Destination $PACKAGE_DIR 27 | 28 | # 2 - copy dlls 29 | # windeployqt $PACKAGE_DIR/gotify-tray++.exe --no-translations --no-system-d3d-compiler --no-opengl-sw 30 | Copy-Item $Qt_ROOT_DIR/bin/Qt6Core.dll -Destination $PACKAGE_DIR 31 | Copy-Item $Qt_ROOT_DIR/bin/Qt6Gui.dll -Destination $PACKAGE_DIR 32 | Copy-Item $Qt_ROOT_DIR/bin/Qt6Network.dll -Destination $PACKAGE_DIR 33 | Copy-Item $Qt_ROOT_DIR/bin/Qt6Sql.dll -Destination $PACKAGE_DIR 34 | Copy-Item $Qt_ROOT_DIR/bin/Qt6Svg.dll -Destination $PACKAGE_DIR 35 | Copy-Item $Qt_ROOT_DIR/bin/Qt6WebSockets.dll -Destination $PACKAGE_DIR 36 | Copy-Item $Qt_ROOT_DIR/bin/Qt6Widgets.dll -Destination $PACKAGE_DIR 37 | Copy-Item $Qt_ROOT_DIR/bin/libgcc_s_seh-1.dll -Destination $PACKAGE_DIR 38 | Copy-Item $Qt_ROOT_DIR/bin/libstdc++-6.dll -Destination $PACKAGE_DIR 39 | Copy-Item $Qt_ROOT_DIR/bin/libwinpthread-1.dll -Destination $PACKAGE_DIR 40 | 41 | Copy-Item -Recurse $Qt_ROOT_DIR/plugins/iconengines -Exclude *.debug -Destination $PACKAGE_DIR 42 | Copy-Item -Recurse $Qt_ROOT_DIR/plugins/imageformats -Exclude *.debug -Destination $PACKAGE_DIR 43 | Copy-Item -Recurse $Qt_ROOT_DIR/plugins/styles -Exclude *.debug -Destination $PACKAGE_DIR 44 | Copy-Item -Recurse $Qt_ROOT_DIR/plugins/tls -Exclude *.debug -Destination $PACKAGE_DIR 45 | 46 | New-Item -ItemType "directory" -Path $PACKAGE_DIR/platforms 47 | Copy-Item $Qt_ROOT_DIR/plugins/platforms/qwindows.dll -Destination $PACKAGE_DIR/platforms 48 | New-Item -ItemType "directory" -Path $PACKAGE_DIR/sqldrivers 49 | Copy-Item $Qt_ROOT_DIR/plugins/sqldrivers/qsqlite.dll -Destination $PACKAGE_DIR/sqldrivers 50 | 51 | Get-ChildItem $PACKAGE_DIR 52 | Write-Host -BackgroundColor Green "PACKAGING DONE" 53 | -------------------------------------------------------------------------------- /scripts/gotify-tray++.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "Gotify Tray++" 5 | #define MyAppVersion "0.0.12" 6 | #define MyAppURL "https://github.com/seird/gotify-tray-cpp" 7 | #define MyAppExeName "gotify-tray++.exe" 8 | 9 | [Setup] 10 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 11 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 12 | AppId={{4F505F0F-12A9-49E0-B5DB-43ADC28EBF36} 13 | AppName={#MyAppName} 14 | AppVersion={#MyAppVersion} 15 | ;AppVerName={#MyAppName} {#MyAppVersion} 16 | AppPublisherURL={#MyAppURL} 17 | AppSupportURL={#MyAppURL} 18 | AppUpdatesURL={#MyAppURL} 19 | DefaultDirName={autopf}\{#MyAppName} 20 | DisableProgramGroupPage=yes 21 | ; Remove the following line to run in administrative install mode (install for all users.) 22 | PrivilegesRequired=lowest 23 | OutputDir=..\dist 24 | OutputBaseFilename=gotify-tray++-setup 25 | Compression=lzma 26 | SolidCompression=yes 27 | WizardStyle=modern 28 | UninstallDisplayIcon={app}\{#MyAppExeName} 29 | 30 | [Languages] 31 | Name: "english"; MessagesFile: "compiler:Default.isl" 32 | 33 | [Tasks] 34 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 35 | 36 | [Files] 37 | Source: "..\package\gotify-tray++.exe"; DestDir: "{app}"; Flags: ignoreversion 38 | Source: "..\package\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 39 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 40 | 41 | [Icons] 42 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 43 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 44 | 45 | [Run] 46 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent unchecked 47 | 48 | -------------------------------------------------------------------------------- /src/applicationitem.cpp: -------------------------------------------------------------------------------- 1 | #include "applicationitem.h" 2 | #include "settings.h" 3 | 4 | 5 | ApplicationItem::ApplicationItem(const QString &text, GotifyModel::Application * application) 6 | : QStandardItem(text) 7 | { 8 | if (application) { 9 | setData(application->id, ApplicationRole::Id); 10 | setData(application->token, ApplicationRole::Token); 11 | setData(application->name, ApplicationRole::Name); 12 | setData(application->description, ApplicationRole::Description); 13 | setData(application->internal, ApplicationRole::Internal); 14 | setData(application->image, ApplicationRole::Image); 15 | setData(false, ApplicationRole::AllMessages); 16 | } else { 17 | setData(true, ApplicationRole::AllMessages); 18 | } 19 | setFont(settings->applicationFont()); 20 | } 21 | 22 | 23 | int ApplicationItem::id() 24 | { 25 | return data(ApplicationRole::Id).toInt(); 26 | } 27 | 28 | 29 | QString ApplicationItem::token() 30 | { 31 | return data(ApplicationRole::Token).toString(); 32 | } 33 | 34 | 35 | QString ApplicationItem::name() 36 | { 37 | return data(ApplicationRole::Name).toString(); 38 | } 39 | 40 | 41 | QString ApplicationItem::description() 42 | { 43 | return data(ApplicationRole::Description).toString(); 44 | } 45 | 46 | 47 | QString ApplicationItem::image() 48 | { 49 | return data(ApplicationRole::Image).toString(); 50 | } 51 | 52 | 53 | bool ApplicationItem::internal() 54 | { 55 | return data(ApplicationRole::Internal).toBool(); 56 | } 57 | 58 | 59 | bool ApplicationItem::allMessages() 60 | { 61 | return data(ApplicationRole::AllMessages).toBool(); 62 | } 63 | -------------------------------------------------------------------------------- /src/applicationitem.h: -------------------------------------------------------------------------------- 1 | #ifndef APPLICATIONITEM_H 2 | #define APPLICATIONITEM_H 3 | 4 | 5 | #include 6 | 7 | #include "gotifymodels.h" 8 | 9 | 10 | class ApplicationItem : public QStandardItem 11 | { 12 | 13 | public: 14 | explicit ApplicationItem(const QString &text, GotifyModel::Application * application = nullptr); 15 | int id(); 16 | QString token(); 17 | QString name(); 18 | QString description(); 19 | bool internal(); 20 | QString image(); 21 | bool allMessages(); 22 | 23 | 24 | private: 25 | enum ApplicationRole { 26 | Id = Qt::UserRole + 1, 27 | Token = Qt::UserRole + 2, 28 | Name = Qt::UserRole + 3, 29 | Description = Qt::UserRole + 4, 30 | Internal = Qt::UserRole + 5, 31 | Image = Qt::UserRole + 6, 32 | AllMessages = Qt::UserRole + 7 33 | }; 34 | }; 35 | 36 | 37 | #endif // APPLICATIONITEM_H 38 | -------------------------------------------------------------------------------- /src/applicationitemmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "applicationitemmodel.h" 2 | #include "settings.h" 3 | 4 | 5 | ApplicationItemModel::ApplicationItemModel(QObject * parent) 6 | : QStandardItemModel(parent) 7 | { 8 | 9 | } 10 | 11 | 12 | ApplicationItem * ApplicationItemModel::itemFromIndex(const QModelIndex &index) 13 | { 14 | return static_cast(QStandardItemModel::itemFromIndex(index)); 15 | } 16 | 17 | 18 | ApplicationItem * ApplicationItemModel::itemFromId(int id) 19 | { 20 | for (int r=0; r(item(r, 0)); 22 | if (id == applicationItem->id() && !applicationItem->allMessages()) { 23 | return applicationItem; 24 | } 25 | } 26 | return nullptr; 27 | } 28 | 29 | 30 | ApplicationProxyModel::ApplicationProxyModel(ApplicationItemModel * applicationItemModel) 31 | : QSortFilterProxyModel() 32 | { 33 | setSourceModel(applicationItemModel); 34 | setSortCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); 35 | if (settings->sortApplications()) { 36 | sort(0, Qt::SortOrder::AscendingOrder); 37 | } 38 | } 39 | 40 | 41 | bool ApplicationProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const 42 | { 43 | ApplicationItemModel * source = static_cast(sourceModel()); 44 | 45 | if (source->itemFromIndex(source_left)->allMessages()) { 46 | return true; 47 | } else if (source->itemFromIndex(source_right)->allMessages()) { 48 | return false; 49 | } 50 | 51 | return QSortFilterProxyModel::lessThan(source_left, source_right); 52 | } 53 | -------------------------------------------------------------------------------- /src/applicationitemmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef APPLICATIONITEMMODEL_H 2 | #define APPLICATIONITEMMODEL_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "applicationitem.h" 10 | 11 | 12 | class ApplicationItemModel : public QStandardItemModel 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit ApplicationItemModel(QObject * parent = nullptr); 18 | ApplicationItem * itemFromIndex(const QModelIndex &index); 19 | ApplicationItem * itemFromId(int id); 20 | 21 | private: 22 | 23 | }; 24 | 25 | 26 | class ApplicationProxyModel : public QSortFilterProxyModel 27 | { 28 | Q_OBJECT 29 | 30 | public: 31 | explicit ApplicationProxyModel(ApplicationItemModel * applicationItemModel); 32 | 33 | protected: 34 | bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const; 35 | 36 | private: 37 | 38 | }; 39 | 40 | 41 | #endif // APPLICATIONITEMMODEL_H 42 | -------------------------------------------------------------------------------- /src/appversion.cpp: -------------------------------------------------------------------------------- 1 | #include "appversion.h" 2 | 3 | QVersionNumber appVersion(0, 0, 12); 4 | -------------------------------------------------------------------------------- /src/appversion.h: -------------------------------------------------------------------------------- 1 | #ifndef APPVERSION_H 2 | #define APPVERSION_H 3 | 4 | 5 | #include 6 | 7 | 8 | extern QVersionNumber appVersion; 9 | 10 | 11 | #endif // APPVERSION_H 12 | -------------------------------------------------------------------------------- /src/cache.cpp: -------------------------------------------------------------------------------- 1 | #include "cache.h" 2 | #include "utils.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | Cache * cache = nullptr; 10 | 11 | 12 | Cache::Cache(QObject * parent) 13 | : QObject(parent) 14 | { 15 | directory = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/"; 16 | if (!QDir().mkpath(directory)) { 17 | qWarning() << "Cache directory could not be created: " + directory; 18 | directory = "./cache"; 19 | if (!QDir().mkpath(directory)) { 20 | qFatal() << "Cache directory could not be created: " + directory; 21 | } 22 | } 23 | 24 | filesDirectory = directory + "files/"; 25 | if (!QDir().mkpath(filesDirectory)) { 26 | qFatal() << "Cache files directory could not be created: " + filesDirectory; 27 | } 28 | 29 | database = QSqlDatabase::addDatabase("QSQLITE"); 30 | database.setDatabaseName(directory + "cache.db.sqlite3"); 31 | database.open(); 32 | QSqlQuery query(database); 33 | query.prepare("CREATE TABLE IF NOT EXISTS cache (" 34 | "id INTEGER PRIMARY KEY AUTOINCREMENT," 35 | "key TEXT UNIQUE," 36 | "value TEXT)"); 37 | query.exec(); 38 | } 39 | 40 | 41 | Cache::~Cache() 42 | { 43 | database.removeDatabase("QSQLITE"); 44 | database.close(); 45 | } 46 | 47 | 48 | Cache * Cache::getInstance() 49 | { 50 | if(!cache) { 51 | cache = new Cache(); 52 | } 53 | return cache; 54 | } 55 | 56 | 57 | qint64 Cache::size() 58 | { 59 | return Utils::dirSize(getDir()); 60 | } 61 | 62 | 63 | void Cache::clear() 64 | { 65 | QSqlQuery query(database); 66 | query.prepare("DELETE FROM cache"); 67 | query.exec(); 68 | 69 | QDir dir(filesDirectory); 70 | dir.removeRecursively(); 71 | dir.mkdir(filesDirectory); 72 | } 73 | 74 | 75 | bool Cache::store(QVariant key, QString value) 76 | { 77 | QSqlQuery query(database); 78 | query.prepare("INSERT OR REPLACE INTO cache (key, value)" 79 | "VALUES(:key, :value)"); 80 | query.bindValue(":key", key); 81 | query.bindValue(":value", value); 82 | bool result = query.exec(); 83 | return result; 84 | } 85 | 86 | 87 | QString Cache::lookup(QVariant key) 88 | { 89 | QSqlQuery query(database); 90 | query.prepare("SELECT value FROM cache WHERE key=:key"); 91 | query.bindValue(":key", key); 92 | query.exec(); 93 | // QSqlQuery's internal pointer is located one position before the first record. 94 | return query.next() ? query.value(0).toString() : QString(); 95 | } 96 | 97 | 98 | QString Cache::getFile(QVariant key) 99 | { 100 | QString result = lookup(key); 101 | return result.isNull() ? QString() : filesDirectory + result; 102 | } 103 | -------------------------------------------------------------------------------- /src/cache.h: -------------------------------------------------------------------------------- 1 | #ifndef CACHE_H 2 | #define CACHE_H 3 | 4 | #include 5 | #include 6 | 7 | class Cache : QObject 8 | { 9 | Q_OBJECT 10 | public: 11 | Cache(QObject * parent = nullptr); 12 | ~Cache(); 13 | static Cache * getInstance(); 14 | 15 | QString getDir() const { return directory; }; 16 | QString getFilesDir() const { return filesDirectory; }; 17 | 18 | QString lookup(QVariant key); 19 | bool store(QVariant key, QString value); 20 | void clear(); 21 | /** 22 | * @brief Get the cache size in bytes 23 | * @return 24 | */ 25 | qint64 size(); 26 | 27 | /** 28 | * @brief getFile Returns the associated cache value prepended with the filesDirectory 29 | * @param key 30 | * @return 31 | */ 32 | QString getFile(QVariant key); 33 | 34 | private: 35 | QString directory; 36 | QString filesDirectory; 37 | QSqlDatabase database; 38 | }; 39 | 40 | 41 | extern Cache * cache; 42 | 43 | 44 | #endif // CACHE_H 45 | -------------------------------------------------------------------------------- /src/clickablelabel.cpp: -------------------------------------------------------------------------------- 1 | #include "clickablelabel.h" 2 | 3 | 4 | ClickableLabel::ClickableLabel(QWidget * parent, Qt::WindowFlags f) 5 | : QLabel(parent, f) 6 | { 7 | } 8 | 9 | 10 | void ClickableLabel::mousePressEvent(QMouseEvent* event) 11 | { 12 | emit clicked(); 13 | } 14 | -------------------------------------------------------------------------------- /src/clickablelabel.h: -------------------------------------------------------------------------------- 1 | #ifndef CLICKABLELABEL_H 2 | #define CLICKABLELABEL_H 3 | 4 | 5 | #include 6 | #include 7 | 8 | 9 | class ClickableLabel : public QLabel 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | explicit ClickableLabel(QWidget * parent = nullptr, Qt::WindowFlags f=Qt::WindowFlags()); 15 | 16 | signals: 17 | void clicked(); 18 | 19 | protected: 20 | void mousePressEvent(QMouseEvent* event); 21 | 22 | }; 23 | 24 | 25 | #endif // CLICKABLELABEL_H 26 | -------------------------------------------------------------------------------- /src/gotifyapi.cpp: -------------------------------------------------------------------------------- 1 | #include "gotifyapi.h" 2 | #include "utils.h" 3 | 4 | GotifyApi::GotifyApi(QUrl severUrl, QByteArray clientToken, QString certPath, QObject* parent) 5 | : QNetworkAccessManager(parent) 6 | { 7 | updateAuth(severUrl, clientToken, certPath); 8 | 9 | request.setRawHeader("Content-Type", "application/json"); 10 | request.setRawHeader("Accept", "application/json"); 11 | } 12 | 13 | 14 | void GotifyApi::updateAuth(QUrl severUrl, QByteArray clientToken, QString certPath) 15 | { 16 | this->serverUrl = severUrl; 17 | this->clientToken = clientToken; 18 | this->certPath = certPath; 19 | request.setRawHeader("X-Gotify-Key", clientToken); 20 | } 21 | 22 | 23 | QNetworkReply * GotifyApi::get(QString endpoint, QUrlQuery query) 24 | { 25 | QUrl url(serverUrl); 26 | url.setPath(endpoint); 27 | url.setQuery(query); 28 | request.setUrl(url); 29 | 30 | QNetworkReply* reply = QNetworkAccessManager::get(request); 31 | 32 | if (serverUrl.scheme() == "https" && !certPath.isEmpty()) 33 | reply->ignoreSslErrors(Utils::getSelfSignedExpectedErrors(certPath)); 34 | 35 | return reply; 36 | } 37 | 38 | QNetworkReply* 39 | GotifyApi::deleteResource(QString endpoint) 40 | { 41 | QUrl url(serverUrl); 42 | url.setPath(endpoint); 43 | request.setUrl(url); 44 | QNetworkReply* reply = QNetworkAccessManager::deleteResource(request); 45 | 46 | if (serverUrl.scheme() == "https" && !certPath.isEmpty()) 47 | reply->ignoreSslErrors(Utils::getSelfSignedExpectedErrors(certPath)); 48 | 49 | return reply; 50 | } 51 | 52 | QNetworkReply* 53 | GotifyApi::applications() 54 | { 55 | return get("/application"); 56 | } 57 | 58 | 59 | QNetworkReply * GotifyApi::applicationMessages(int applicationId, int limit, int since) 60 | { 61 | return get("/application/" + QString::number(applicationId) + "/message"); 62 | } 63 | 64 | 65 | QNetworkReply * GotifyApi::messages(int limit, int since) 66 | { 67 | QUrlQuery query; 68 | query.addQueryItem("limit", QString::number(limit)); 69 | query.addQueryItem("since", QString::number(since)); 70 | return get("/message", query); 71 | } 72 | 73 | 74 | QNetworkReply * GotifyApi::deleteMessage(int messageId) 75 | { 76 | return deleteResource("/message/" + QString::number(messageId)); 77 | } 78 | 79 | 80 | QNetworkReply * GotifyApi::deleteMessages() 81 | { 82 | return deleteResource("/message"); 83 | } 84 | 85 | 86 | QNetworkReply * GotifyApi::deleteApplicaitonMessages(int applicationId) 87 | { 88 | return deleteResource("/application/" + QString::number(applicationId) + "/message"); 89 | } 90 | 91 | 92 | QNetworkReply * GotifyApi::health() 93 | { 94 | return get("/health"); 95 | } 96 | 97 | 98 | QNetworkReply * GotifyApi::version() 99 | { 100 | return get("/version"); 101 | } 102 | -------------------------------------------------------------------------------- /src/gotifyapi.h: -------------------------------------------------------------------------------- 1 | #ifndef GOTIFYAPI_H 2 | #define GOTIFYAPI_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class GotifyApi : public QNetworkAccessManager 9 | { 10 | Q_OBJECT 11 | public: 12 | GotifyApi(QUrl severUrl, QByteArray clientToken, QString certPath = "", QObject* parent = nullptr); 13 | void updateAuth(QUrl serverUrl, QByteArray clientToken, QString certPath = ""); 14 | QNetworkReply * applications(); 15 | QNetworkReply * applicationMessages(int applicationId, int limit = 100, int since = 0); 16 | QNetworkReply * messages(int limit = 100, int since = 0); 17 | QNetworkReply * deleteMessage(int messageId); 18 | QNetworkReply * deleteMessages(); 19 | QNetworkReply * deleteApplicaitonMessages(int applicationId); 20 | QNetworkReply * health(); 21 | QNetworkReply * version(); 22 | 23 | private: 24 | QUrl serverUrl; 25 | QByteArray clientToken; 26 | QString certPath; 27 | QNetworkRequest request; 28 | 29 | QNetworkReply * get(QString endpoint, QUrlQuery query = QUrlQuery()); 30 | QNetworkReply * deleteResource(QString endpoint); 31 | }; 32 | 33 | #endif // GOTIFYAPI_H 34 | -------------------------------------------------------------------------------- /src/gotifymodels.cpp: -------------------------------------------------------------------------------- 1 | #include "gotifymodels.h" 2 | 3 | 4 | namespace GotifyModel { 5 | 6 | 7 | Message::Message(QJsonObject object, QObject * parent) : QObject(parent) 8 | { 9 | id = object["id"].toInt(); 10 | appId = object["appid"].toInt(); 11 | priority = object["priority"].toInt(); 12 | date = QDateTime::fromString(object["date"].toString(), Qt::DateFormat::ISODate).toLocalTime(); 13 | title = object["title"].toString(); 14 | message = object.value("message").toString(); 15 | markdown = object["extras"].toObject()["client::display"].toObject()["contentType"].toString() == "text/markdown"; 16 | } 17 | 18 | 19 | Messages::Messages(QJsonArray array, QObject * parent) : QObject(parent) 20 | { 21 | for (auto x: array) { 22 | QJsonObject object = x.toObject(); 23 | 24 | GotifyModel::Message * messageModel = new GotifyModel::Message(object); 25 | messages.append(messageModel); 26 | } 27 | } 28 | 29 | 30 | Application::Application(QJsonObject object, QObject * parent) : QObject(parent) 31 | { 32 | id = object["id"].toInt(); 33 | image = object["image"].toString(); 34 | internal = object["internal"].toBool(); 35 | name = object["name"].toString(); 36 | token = object["token"].toString(); 37 | } 38 | 39 | 40 | Applications::Applications(QJsonArray array, QObject * parent) : QObject(parent) 41 | { 42 | for (auto x: array) { 43 | QJsonObject object = x.toObject(); 44 | 45 | GotifyModel::Application * applicationModel = new GotifyModel::Application(object); 46 | applications.append(applicationModel); 47 | } 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/gotifymodels.h: -------------------------------------------------------------------------------- 1 | #ifndef GOTIFYMODELS_H 2 | #define GOTIFYMODELS_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | namespace GotifyModel { 12 | 13 | 14 | class Message : public QObject 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit Message(QJsonObject object, QObject * parent = nullptr); 20 | 21 | int id; 22 | int appId; 23 | int priority; 24 | QDateTime date; 25 | QString title; 26 | QString message; 27 | bool markdown; 28 | }; 29 | 30 | 31 | class Messages : public QObject 32 | { 33 | Q_OBJECT 34 | 35 | public: 36 | explicit Messages(QJsonArray array, QObject * parent = nullptr); 37 | 38 | QList messages; 39 | }; 40 | 41 | 42 | class Application : public QObject 43 | { 44 | Q_OBJECT 45 | 46 | public: 47 | explicit Application(QJsonObject object, QObject * parent = nullptr); 48 | 49 | int id; 50 | QString image; 51 | bool internal; 52 | QString description; 53 | QString name; 54 | QString token; 55 | }; 56 | 57 | 58 | class Applications : public QObject 59 | { 60 | Q_OBJECT 61 | 62 | public: 63 | explicit Applications(QJsonArray array, QObject * parent = nullptr); 64 | 65 | QList applications; 66 | }; 67 | 68 | } 69 | 70 | #endif // GOTIFYMODELS_H 71 | -------------------------------------------------------------------------------- /src/imagepopup.cpp: -------------------------------------------------------------------------------- 1 | #include "imagepopup.h" 2 | #include "settings.h" 3 | 4 | #include 5 | 6 | 7 | ImagePopup::ImagePopup(QWidget * parent) 8 | : QLabel(parent, Qt::Popup), 9 | url(QString()) 10 | { 11 | installEventFilter(this); 12 | connect(&timer, &QTimer::timeout, this, &ImagePopup::checkMouse); 13 | } 14 | 15 | 16 | void ImagePopup::display(const QString& filePath, const QUrl& url, QPoint pos) 17 | { 18 | close(); 19 | 20 | this->url = url; 21 | 22 | QPixmap pixmap(filePath); 23 | int W = settings->popupWidth(); 24 | int H = settings->popupHeight(); 25 | if (pixmap.height() > H || pixmap.width() > W) 26 | pixmap = pixmap.scaled(W, H, Qt::KeepAspectRatio, Qt::SmoothTransformation); 27 | setPixmap(pixmap); 28 | move(pos - QPoint(25, 25)); 29 | 30 | timer.start(500); 31 | 32 | show(); 33 | } 34 | 35 | 36 | void ImagePopup::checkMouse() 37 | { 38 | if (!underMouse()) 39 | close(); 40 | } 41 | 42 | 43 | void ImagePopup::close() 44 | { 45 | timer.stop(); 46 | url = QString(); 47 | QLabel::close(); 48 | } 49 | 50 | 51 | void ImagePopup::mousePressEvent(QMouseEvent * event) 52 | { 53 | if (!url.isEmpty() && event->button() == Qt::MouseButton::LeftButton) 54 | QDesktopServices::openUrl(url); 55 | 56 | QLabel::mousePressEvent(event); 57 | } 58 | 59 | 60 | bool ImagePopup::eventFilter(QObject * obj, QEvent * event) 61 | { 62 | switch (event->type()) { 63 | #ifndef Q_OS_MAC 64 | case QEvent::Leave: 65 | close(); 66 | break; 67 | #endif 68 | case QEvent::KeyPress: { 69 | QKeyEvent * keyEvent = static_cast(event); 70 | if (keyEvent->key() == Qt::Key_Escape) { 71 | close(); 72 | } 73 | break; 74 | } 75 | default: 76 | break; 77 | } 78 | 79 | return QLabel::eventFilter(obj, event); 80 | } 81 | -------------------------------------------------------------------------------- /src/imagepopup.h: -------------------------------------------------------------------------------- 1 | #ifndef IMAGEPOPUP_H 2 | #define IMAGEPOPUP_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | class ImagePopup : public QLabel 13 | { 14 | Q_OBJECT 15 | public: 16 | ImagePopup(QWidget * parent=nullptr); 17 | void display(const QString& filePath, const QUrl& url, QPoint pos); 18 | 19 | virtual bool eventFilter(QObject * obj, QEvent * event); 20 | void mousePressEvent(QMouseEvent * event); 21 | 22 | public slots: 23 | void close(); 24 | 25 | private: 26 | QTimer timer; 27 | QUrl url; 28 | void checkMouse(); 29 | }; 30 | 31 | 32 | #endif // IMAGEPOPUP_H 33 | -------------------------------------------------------------------------------- /src/listener.cpp: -------------------------------------------------------------------------------- 1 | #include "listener.h" 2 | #include "settings.h" 3 | #include "utils.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #define MIN(x, y) ((x) < (y) ? (x) : (y)) 10 | 11 | Listener::Listener(QUrl serverUrl, QByteArray clientToken, QString certPath, QObject* parent) 12 | : QWebSocket(QString(), QWebSocketProtocol::VersionLatest, parent) 13 | , secDelay(0.1) 14 | { 15 | updateAuth(serverUrl, clientToken, certPath); 16 | connect(this, &QWebSocket::connected, this, [this]{secDelay = 0.1;}); 17 | connect(this, &QWebSocket::textMessageReceived, this, &Listener::textMessageReceivedHandler); 18 | } 19 | 20 | void 21 | Listener::updateAuth(QUrl serverUrl, QByteArray clientToken, QString certPath) 22 | { 23 | this->serverUrl = serverUrl; 24 | this->clientToken = clientToken; 25 | this->certPath = certPath; 26 | 27 | if (serverUrl.scheme() == "https" && !certPath.isEmpty()) 28 | ignoreSslErrors(Utils::getSelfSignedExpectedErrors(certPath)); 29 | } 30 | 31 | 32 | QUrl Listener::buildUrl() 33 | { 34 | QUrl url(serverUrl); 35 | url.setScheme(url.scheme().compare("https") ? "ws" : "wss"); 36 | url.setPath(url.path() + "/stream"); 37 | QUrlQuery query; 38 | query.addQueryItem("token", clientToken); 39 | url.setQuery(query); 40 | return url; 41 | } 42 | 43 | 44 | void Listener::startListening() 45 | { 46 | qDebug() << "Opening listener"; 47 | QTimer::singleShot(1000 * secDelay, this, [this]{ 48 | if (!isConnected()) 49 | open(buildUrl()); 50 | }); 51 | secDelay = qBound(0.1f, secDelay * 2, 120.0f); 52 | } 53 | 54 | 55 | bool Listener::isConnected() 56 | { 57 | return state() == QAbstractSocket::SocketState::ConnectedState; 58 | } 59 | 60 | 61 | void Listener::textMessageReceivedHandler(const QString &message) 62 | { 63 | QJsonDocument document = QJsonDocument::fromJson(message.toUtf8()); 64 | if (document.isNull()) { 65 | qWarning() << "json document is null; message: " << message; 66 | return; 67 | } 68 | 69 | QJsonObject object = document.object(); 70 | GotifyModel::Message * gotifyMessage = new GotifyModel::Message(object); 71 | emit messageReceived(gotifyMessage); 72 | } 73 | -------------------------------------------------------------------------------- /src/listener.h: -------------------------------------------------------------------------------- 1 | #ifndef LISTENER_H 2 | #define LISTENER_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "gotifymodels.h" 11 | 12 | 13 | class Listener : public QWebSocket 14 | { 15 | Q_OBJECT 16 | public: 17 | Listener(QUrl serverUrl, QByteArray clientToken, QString certPath = "", QObject* parent = nullptr); 18 | 19 | void updateAuth(QUrl serverUrl, QByteArray clientToken, QString certPath = ""); 20 | void startListening(); 21 | bool isConnected(); 22 | 23 | signals: 24 | void messageReceived(GotifyModel::Message * message); 25 | 26 | private slots: 27 | void textMessageReceivedHandler(const QString &message); 28 | 29 | private: 30 | QUrl serverUrl; 31 | QByteArray clientToken; 32 | QString certPath; 33 | 34 | QTimer * timer; 35 | float secDelay; 36 | 37 | QUrl buildUrl(); 38 | }; 39 | 40 | 41 | #endif // LISTENER_H 42 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "mainapplication.h" 2 | 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | MainApplication app(argc, argv); 7 | 8 | if (app.acquireLock() && app.verifyServer()) { 9 | app.init(); 10 | return app.exec(); 11 | } else { 12 | qFatal() << "Failed to acquire lock."; 13 | return EXIT_FAILURE; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mainapplication.cpp: -------------------------------------------------------------------------------- 1 | #include "mainapplication.h" 2 | #include "applicationitem.h" 3 | #include "serverinfodialog.h" 4 | #include "settingsdialog.h" 5 | #include "settings.h" 6 | #include "cache.h" 7 | #include "appversion.h" 8 | #include "processthread.h" 9 | #include "requesthandler.h" 10 | #include "utils.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #ifdef USE_KDE 19 | #include 20 | #endif 21 | 22 | MainApplication::MainApplication(int& argc, char* argv[]) 23 | : QApplication(argc, argv) 24 | , firstConnect(true) 25 | , lastHeartbeat(QDateTime::currentMSecsSinceEpoch()) 26 | { 27 | setApplicationName("Gotify Tray++"); 28 | setApplicationVersion(appVersion.toString()); 29 | setDesktopFileName(applicationName()); 30 | setQuitOnLastWindowClosed(false); 31 | setWindowIcon(QIcon(":/res/icons/gotify-tray++.ico")); 32 | applyStyle(); 33 | 34 | settings = Settings::getInstance(); 35 | cache = Cache::getInstance(); 36 | requestHandler = RequestHandler::getInstance(); 37 | 38 | lockfile = new QLockFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + applicationName() + ".lock"); 39 | } 40 | 41 | 42 | MainApplication::~MainApplication() 43 | { 44 | delete lockfile; 45 | if (gotifyApi) delete gotifyApi; 46 | if (listener) delete listener; 47 | if (applicationProxyModel) delete applicationProxyModel; 48 | if (mainWindow) delete mainWindow; 49 | if (tray) delete tray; 50 | } 51 | 52 | 53 | void MainApplication::init() 54 | { 55 | initComponents(); 56 | initGui(); 57 | connectComponents(); 58 | initShortcuts(); 59 | 60 | QNetworkReply * reply = gotifyApi->applications(); 61 | connect(reply, &QNetworkReply::finished, requestHandler, &RequestHandler::applications); 62 | 63 | listener->startListening(); 64 | 65 | heartbeatTimer->start(settings->heartbeatInterval()); 66 | } 67 | 68 | 69 | void MainApplication::initComponents() 70 | { 71 | QUrl serverUrl = settings->serverUrl(); 72 | QByteArray clientToken = settings->clientToken(); 73 | QString certPath = settings->selfSignedCertificatePath(); 74 | gotifyApi = new GotifyApi(serverUrl, clientToken, certPath); 75 | listener = new Listener(serverUrl, clientToken, certPath); 76 | applicationProxyModel = new ApplicationProxyModel(&applicationItemModel); 77 | mainWindow = new MainWindow(&messageItemModel, &applicationItemModel, applicationProxyModel); 78 | tray = new Tray(); 79 | imagePopup = new ImagePopup(mainWindow); 80 | heartbeatTimer = new QTimer(this); 81 | } 82 | 83 | 84 | void MainApplication::initGui() 85 | { 86 | mainWindow->show(); 87 | QTimer::singleShot(0, this, [this]{mainWindow->hide();}); 88 | 89 | tray->setError(); 90 | tray->show(); 91 | } 92 | 93 | 94 | bool MainApplication::acquireLock() 95 | { 96 | #ifdef QT_DEBUG 97 | return true; 98 | #else 99 | lockfile->setStaleLockTime(0); 100 | return lockfile->tryLock(); 101 | #endif 102 | } 103 | 104 | bool MainApplication::verifyServer(bool forceNew) 105 | { 106 | QUrl url = settings->serverUrl(); 107 | QByteArray clientToken = settings->clientToken(); 108 | 109 | if (forceNew || url.isEmpty() || clientToken.isEmpty()) { 110 | ServerInfoDialog dialog(NULL, url, clientToken, settings->selfSignedCertificatePath()); 111 | return dialog.exec(); 112 | } else { 113 | return true; 114 | } 115 | } 116 | 117 | 118 | void MainApplication::connectComponents() 119 | { 120 | connect(mainWindow, &MainWindow::activated, tray, &Tray::revertIcon); 121 | connect(mainWindow, &MainWindow::hidden, this, &MainApplication::mainWindowHidden); 122 | connect(mainWindow, &MainWindow::refresh, this, &MainApplication::refreshCallback); 123 | connect(mainWindow, &MainWindow::deleteAll, this, &MainApplication::deleteAllCallback); 124 | connect(mainWindow, &MainWindow::deleteMessage, this, &MainApplication::deleteMessageCallback); 125 | connect(mainWindow, &MainWindow::applicationChanged, this, &MainApplication::applicationChangedCallback); 126 | 127 | connect(tray->actionShowWindow, &QAction::triggered, mainWindow, &MainWindow::bringToFront); 128 | connect(tray->actionSettings, &QAction::triggered, this, &MainApplication::showSettings); 129 | connect(tray->actionReconnect, &QAction::triggered, this, &MainApplication::reconnectCallback); 130 | connect(tray->actionQuit, &QAction::triggered, this, &MainApplication::quit); 131 | connect(tray, &Tray::activated, this, &MainApplication::trayActivated); 132 | connect(tray, &Tray::messageClicked, mainWindow, [this]{if (settings->notificationClick()) mainWindow->bringToFront();}); 133 | 134 | connect(styleHints(), &QStyleHints::colorSchemeChanged, this, &MainApplication::themeChangedCallback); 135 | 136 | connect(requestHandler, &RequestHandler::finishedMessages, this, &MainApplication::messagesCallback); 137 | connect(requestHandler, &RequestHandler::finishedMissedMessages, this, &MainApplication::missedMessagesCallback); 138 | connect(requestHandler, &RequestHandler::finishedApplications, this, &MainApplication::applicationsCallback); 139 | connect(requestHandler, &RequestHandler::finishedImagePopup, this, &MainApplication::showImagePopup); 140 | 141 | connect(&processApplicationsThread, &ProcessThread::Applications::processed, this, &MainApplication::insertApplications); 142 | 143 | connect(listener, &Listener::connected, this, &MainApplication::listenerConnectedCallback); 144 | connect(listener, &Listener::disconnected, this, &MainApplication::listenerDisconnectedCallback); 145 | connect(listener, &Listener::messageReceived, this, &MainApplication::messageReceivedCallback); 146 | 147 | connect(settings, &Settings::serverChanged, this, &MainApplication::serverChangedCallback); 148 | connect(settings, &Settings::quitRequested, this, &MainApplication::quit); 149 | 150 | connect(heartbeatTimer, &QTimer::timeout, this, &MainApplication::heartbeat); 151 | } 152 | 153 | 154 | void MainApplication::initShortcuts() 155 | { 156 | new QShortcut(Qt::CTRL | Qt::Key_Q, mainWindow, this, &MainApplication::quit); 157 | new QShortcut(Qt::Key_F5, mainWindow, this, &MainApplication::refreshCallback); 158 | } 159 | 160 | 161 | void MainApplication::showSettings() 162 | { 163 | SettingsDialog dialog(mainWindow); 164 | dialog.exec(); 165 | tray->revertIcon(); 166 | } 167 | 168 | 169 | void MainApplication::serverChangedCallback() 170 | { 171 | QUrl url = settings->serverUrl(); 172 | QByteArray token = settings->clientToken(); 173 | QString certPath = settings->selfSignedCertificatePath(); 174 | gotifyApi->updateAuth(url, token, certPath); 175 | listener->updateAuth(url, token, certPath); 176 | reconnectCallback(); 177 | } 178 | 179 | 180 | void MainApplication::reconnectCallback() 181 | { 182 | if (listener->isConnected()) 183 | listener->close(); 184 | else 185 | listener->startListening(); 186 | } 187 | 188 | 189 | void MainApplication::trayActivated(QSystemTrayIcon::ActivationReason reason) 190 | { 191 | if (reason == QSystemTrayIcon::ActivationReason::Trigger) { 192 | mainWindow->bringToFront(); 193 | } 194 | } 195 | 196 | 197 | void MainApplication::mainWindowHidden() 198 | { 199 | imagePopup->close(); 200 | } 201 | 202 | 203 | void MainApplication::refreshCallback() 204 | { 205 | themeChangedCallback(Qt::ColorScheme::Unknown); 206 | 207 | mainWindow->disableApplications(); 208 | mainWindow->disableButtons(); 209 | messageItemModel.clear(); 210 | applicationItemModel.clear(); 211 | 212 | QNetworkReply * reply = gotifyApi->applications(); 213 | connect(reply, &QNetworkReply::finished, requestHandler, &RequestHandler::applications); 214 | } 215 | 216 | 217 | void MainApplication::messagesCallback(GotifyModel::Messages * messages) 218 | { 219 | messageItemModel.clear(); 220 | for (auto message : messages->messages) { 221 | messageItemModel.appendMessage(message); 222 | message->deleteLater(); 223 | } 224 | messages->deleteLater(); 225 | mainWindow->enableButtons(); 226 | mainWindow->enableApplications(false); 227 | } 228 | 229 | 230 | void MainApplication::missedMessagesCallback(GotifyModel::Messages * messages) 231 | { 232 | int lastId = settings->lastId(); 233 | QListIterator it(messages->messages); 234 | it.toBack(); 235 | while (it.hasPrevious()) { 236 | GotifyModel::Message * message = it.previous(); 237 | if (message->id > lastId) { 238 | if (settings->notifyMissed()) { 239 | messageReceivedCallback(message); 240 | } else { 241 | addMessageToModel(message); 242 | } 243 | } 244 | message->deleteLater(); 245 | } 246 | messages->deleteLater(); 247 | } 248 | 249 | 250 | void MainApplication::applicationsCallback(GotifyModel::Applications * applications) 251 | { 252 | applicationItemModel.clear(); 253 | processApplicationsThread.process(applications); 254 | } 255 | 256 | 257 | void MainApplication::insertApplications(GotifyModel::Applications * applications) 258 | { 259 | ApplicationItem * item = new ApplicationItem("ALL MESSAGES"); 260 | applicationItemModel.insertRow(0, item); 261 | 262 | for (auto application : applications->applications) { 263 | ApplicationItem * item = new ApplicationItem(application->name, application); 264 | item->setIcon(QIcon(cache->getFile(application->id))); 265 | applicationItemModel.appendRow(item); 266 | application->deleteLater(); 267 | } 268 | 269 | mainWindow->enableApplications(); 270 | applications->deleteLater(); 271 | } 272 | 273 | 274 | void MainApplication::listenerConnectedCallback() 275 | { 276 | qDebug() << "Listener connected"; 277 | mainWindow->setActive(); 278 | tray->setActive(); 279 | 280 | if (firstConnect) { 281 | firstConnect = false; 282 | return; 283 | } 284 | 285 | QNetworkReply * reply = gotifyApi->messages(); 286 | connect(reply, &QNetworkReply::finished, requestHandler, [this]{requestHandler->messages(true);}); 287 | } 288 | 289 | 290 | void MainApplication::listenerDisconnectedCallback() 291 | { 292 | qDebug() << "Listener disconnected"; 293 | mainWindow->setConnecting(); 294 | tray->setError(); 295 | listener->startListening(); 296 | } 297 | 298 | 299 | void MainApplication::addMessageToModel(GotifyModel::Message * message) 300 | { 301 | // Check if the message's appId is in the applicationItemModel 302 | if (!applicationItemModel.itemFromId(message->appId)) { 303 | qWarning() << "Application " << message->appId << " is not in applicationItemModel."; 304 | return; 305 | } 306 | 307 | // Get the selected ApplicationItem 308 | ApplicationItem * applicationItem = applicationItemModel.itemFromIndex(applicationProxyModel->mapToSource(mainWindow->selectedApplication())); 309 | 310 | // If the selected ApplicationItem is the 'All messages' item, or if the appIds match 311 | // --> insert the message 312 | if (applicationItem->allMessages() || (message->appId == applicationItem->id())) { 313 | messageItemModel.insertMessage(0, message); 314 | } 315 | } 316 | 317 | 318 | void MainApplication::messageReceivedCallback(GotifyModel::Message * message) 319 | { 320 | addMessageToModel(message); 321 | 322 | // Don't show a notification if it's low priority or the window is active 323 | if (message->priority < settings->notificationPriority() || mainWindow->isActiveWindow()) { 324 | return; 325 | } 326 | 327 | // Change the tray icon to show there are unread notifications 328 | if (settings->trayUnreadEnabled() && !mainWindow->isActiveWindow()) { 329 | tray->setUnread(); 330 | } 331 | 332 | #ifdef USE_KDE 333 | // KDE KNotification -- https://api.kde.org/frameworks/knotifications/html/classKNotification.html 334 | KNotification* notification = new KNotification(QStringLiteral("notification")); 335 | notification->setComponentName(QStringLiteral("plasma_workspace")); 336 | 337 | QString text = message->message; 338 | if (!Utils::containsHtml(message->message) && !message->markdown) 339 | text = Utils::replaceLinks(message->message); 340 | notification->setText(text); 341 | notification->setTitle(message->title); 342 | notification->setIconName(cache->getFile(message->appId)); 343 | notification->setUrgency(Utils::priorityToUrgency(message->priority)); 344 | 345 | // Set image url on the notification 346 | QList urls; 347 | QString imageUrl = Utils::extractImage(message->message); 348 | QString filePath = cache->getFile(imageUrl); 349 | if (!filePath.isNull()) 350 | urls.append(QUrl::fromLocalFile(filePath)); 351 | notification->setUrls(urls); 352 | 353 | if (settings->notificationClick()) { 354 | KNotificationAction* action = notification->addDefaultAction(QStringLiteral("Open")); // default action -> triggered when clicking the popup 355 | QObject::connect(action, &KNotificationAction::activated, this, [this] { mainWindow->bringToFront(); }); 356 | } 357 | notification->sendEvent(); 358 | #else 359 | // QSystemTrayIcon Notification 360 | tray->showMessage( 361 | message->title, 362 | message->message, 363 | QIcon(cache->getFile(message->appId)), 364 | settings->notificationDurationMs()); 365 | #endif 366 | 367 | message->deleteLater(); 368 | } 369 | 370 | 371 | void MainApplication::deleteAllCallback(ApplicationItem * applicationItem) 372 | { 373 | if (applicationItem->allMessages()) 374 | gotifyApi->deleteMessages(); 375 | else 376 | gotifyApi->deleteApplicaitonMessages(applicationItem->id()); 377 | 378 | messageItemModel.clear(); 379 | } 380 | 381 | 382 | void MainApplication::deleteMessageCallback(MessageItem * item) 383 | { 384 | gotifyApi->deleteMessage(item->id()); 385 | messageItemModel.removeRow(item->row()); 386 | } 387 | 388 | 389 | void MainApplication::applicationChangedCallback(ApplicationItem * item) 390 | { 391 | mainWindow->disableButtons(); 392 | mainWindow->disableApplications(); 393 | messageItemModel.clear(); 394 | QNetworkReply * reply = item->allMessages() ? gotifyApi->messages() : gotifyApi->applicationMessages(item->id()); 395 | connect(reply, &QNetworkReply::finished, requestHandler, [this]{requestHandler->messages();}); 396 | } 397 | 398 | 399 | void MainApplication::showImagePopup(const QString& fileName, const QUrl& url, QPoint pos) 400 | { 401 | imagePopup->display(fileName, url, pos); 402 | } 403 | 404 | 405 | void MainApplication::themeChangedCallback(Qt::ColorScheme colorScheme) 406 | { 407 | mainWindow->setIcons(); 408 | applyStyle(); 409 | } 410 | 411 | 412 | void MainApplication::applyStyle() 413 | { 414 | // Check if this is necessary on other DEs as well 415 | #ifdef Q_OS_WIN 416 | setStyle("fusion"); 417 | setPalette(style()->standardPalette()); 418 | #endif 419 | } 420 | 421 | void 422 | MainApplication::heartbeat() 423 | { 424 | if (QDateTime::currentMSecsSinceEpoch() > (lastHeartbeat + 2 * settings->heartbeatInterval())) 425 | reconnectCallback(); 426 | 427 | lastHeartbeat = QDateTime::currentMSecsSinceEpoch(); 428 | } 429 | 430 | void MainApplication::quit(){ 431 | qDebug() << "Quit requested"; 432 | 433 | tray->hide(); 434 | mainWindow->storeWindowState(); 435 | settings->sync(); 436 | 437 | if (cache) 438 | delete cache; 439 | 440 | disconnect(listener, &Listener::disconnected, this, &MainApplication::listenerDisconnectedCallback); 441 | listener->close(); 442 | 443 | lockfile->unlock(); 444 | 445 | QApplication::quit(); 446 | } 447 | -------------------------------------------------------------------------------- /src/mainapplication.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINAPPLICATION_H 2 | #define MAINAPPLICATION_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "mainwindow.h" 16 | #include "tray.h" 17 | #include "gotifyapi.h" 18 | #include "messageitemmodel.h" 19 | #include "applicationitemmodel.h" 20 | #include "listener.h" 21 | #include "processthread.h" 22 | #include "imagepopup.h" 23 | 24 | 25 | class MainApplication : public QApplication 26 | { 27 | public: 28 | MainApplication(int &argc, char *argv[]); 29 | ~MainApplication(); 30 | void init(); 31 | bool acquireLock(); 32 | bool verifyServer(bool forceNew = false); 33 | void quit(); 34 | 35 | private: 36 | MainWindow * mainWindow; 37 | Tray * tray; 38 | MessageItemModel messageItemModel; 39 | ApplicationItemModel applicationItemModel; 40 | ApplicationProxyModel * applicationProxyModel; 41 | QLockFile * lockfile; 42 | QShortcut * shortcut_quit; 43 | GotifyApi * gotifyApi; 44 | Listener * listener; 45 | ImagePopup * imagePopup; 46 | bool firstConnect; 47 | ProcessThread::Applications processApplicationsThread; 48 | QTimer* heartbeatTimer; 49 | qint64 lastHeartbeat; 50 | 51 | void initGui(); 52 | void initComponents(); 53 | void connectComponents(); 54 | void applyStyle(); 55 | void initShortcuts(); 56 | void addMessageToModel(GotifyModel::Message * message); 57 | 58 | private slots: 59 | void showSettings(); 60 | void reconnectCallback(); 61 | void trayActivated(QSystemTrayIcon::ActivationReason reason); 62 | void mainWindowHidden(); 63 | void refreshCallback(); 64 | void deleteAllCallback(ApplicationItem * applicationItem); 65 | void deleteMessageCallback(MessageItem * item); 66 | void applicationChangedCallback(ApplicationItem * item); 67 | void showImagePopup(const QString& fileName, const QUrl& url, QPoint pos); 68 | void themeChangedCallback(Qt::ColorScheme colorScheme); 69 | void serverChangedCallback(); 70 | 71 | void listenerConnectedCallback(); 72 | void listenerDisconnectedCallback(); 73 | void messageReceivedCallback(GotifyModel::Message * message); 74 | 75 | void messagesCallback(GotifyModel::Messages * messages); 76 | void missedMessagesCallback(GotifyModel::Messages * messages); 77 | void applicationsCallback(GotifyModel::Applications * applications); 78 | 79 | void insertApplications(GotifyModel::Applications* applications); 80 | 81 | void heartbeat(); 82 | }; 83 | 84 | #endif // MAINAPPLICATION_H 85 | -------------------------------------------------------------------------------- /src/mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include "settings.h" 3 | #include "ui_mainwindow.h" 4 | #include "messagewidget.h" 5 | #include "cache.h" 6 | #include "utils.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | 14 | MainWindow::MainWindow(MessageItemModel * messageItemModel, ApplicationItemModel * applicationItemModel, ApplicationProxyModel * applicationProxyModel, QWidget *parent) 15 | : QMainWindow(parent) 16 | , ui(new Ui::MainWindow) 17 | { 18 | ui->setupUi(this); 19 | 20 | setWindowTitle(qApp->applicationName()); 21 | 22 | this->messageItemModel = messageItemModel; 23 | ui->listView_messages->setModel(messageItemModel); 24 | 25 | this->applicationItemModel = applicationItemModel; 26 | this->applicationProxyModel = applicationProxyModel; 27 | ui->listView_applications->setModel(applicationProxyModel); 28 | 29 | // Do not expand the applications listview when resizing 30 | ui->splitter->setStretchFactor(0, 0); 31 | ui->splitter->setStretchFactor(1, 1); 32 | 33 | // Do not collapse the message list 34 | ui->splitter->setCollapsible(1, false); 35 | 36 | setFonts(); 37 | setIcons(); 38 | restoreWindowState(); 39 | connectComponents(); 40 | 41 | installEventFilter(this); 42 | } 43 | 44 | 45 | MainWindow::~MainWindow() 46 | { 47 | delete ui; 48 | } 49 | 50 | 51 | void MainWindow::connectComponents() 52 | { 53 | connect(messageItemModel, &MessageItemModel::rowsInserted, this, &MainWindow::displayMessageWidgets); 54 | connect(ui->listView_applications->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::currentChangedCallback); 55 | connect(settings, &Settings::fontChanged, this, &MainWindow::setFonts); 56 | connect(settings, &Settings::sizeChanged, this, &MainWindow::setIcons); 57 | connect(settings, &Settings::showPriorityChanged, this, &MainWindow::showPriority); 58 | } 59 | 60 | 61 | void MainWindow::setFonts() 62 | { 63 | ui->label_application->setFont(settings->selectedApplicationFont()); 64 | 65 | QFont font = settings->applicationFont(); 66 | for (int r=0; rrowCount(); ++r) 67 | applicationItemModel->item(r)->setFont(font); 68 | 69 | for (int r=0; rrowCount(); ++r) { 70 | MessageWidget * messageWidget = static_cast(ui->listView_messages->indexWidget(messageItemModel->index(r, 0))); 71 | messageWidget->setFonts(); 72 | } 73 | } 74 | 75 | 76 | void MainWindow::setIcons() 77 | { 78 | QString theme = Utils::getTheme(); 79 | ui->pb_refresh->setIcon(QIcon("://res/themes/" + theme + "/refresh.svg")); 80 | ui->pb_delete_all->setIcon(QIcon("://res/themes/" + theme + "/trashcan.svg")); 81 | 82 | QSize labelSize = settings->statusLabelSize(); 83 | ui->statusWidget->setFixedSize(labelSize); 84 | ui->statusWidget->refresh(); 85 | 86 | QSize buttonSize = settings->mainButtonSize(); 87 | ui->pb_refresh->setFixedSize(buttonSize); 88 | ui->pb_delete_all->setFixedSize(buttonSize); 89 | ui->pb_refresh->setIconSize(0.7*buttonSize); 90 | ui->pb_delete_all->setIconSize(0.9*buttonSize); 91 | 92 | ui->listView_applications->setIconSize(settings->applicationIconSize()); 93 | 94 | for (int r=0; rrowCount(); ++r) { 95 | MessageWidget * messageWidget = static_cast(ui->listView_messages->indexWidget(messageItemModel->index(r, 0))); 96 | messageWidget->setIcons(); 97 | } 98 | } 99 | 100 | 101 | void MainWindow::showPriority(bool enabled) 102 | { 103 | for (int r=0; rrowCount(); ++r) { 104 | MessageWidget * messageWidget = static_cast(ui->listView_messages->indexWidget(messageItemModel->index(r, 0))); 105 | messageWidget->showPriority(enabled); 106 | } 107 | } 108 | 109 | 110 | QModelIndex MainWindow::selectedApplication() 111 | { 112 | return ui->listView_applications->selectionModel()->currentIndex(); 113 | } 114 | 115 | 116 | void MainWindow::bringToFront() 117 | { 118 | ensurePolished(); 119 | show(); 120 | setWindowState((windowState() & ~Qt::WindowState::WindowMinimized) | Qt::WindowState::WindowActive); 121 | activateWindow(); 122 | raise(); 123 | } 124 | 125 | 126 | void MainWindow::enableButtons() 127 | { 128 | ui->pb_delete_all->setEnabled(true); 129 | ui->pb_refresh->setEnabled(true); 130 | } 131 | 132 | 133 | void MainWindow::disableButtons() 134 | { 135 | ui->pb_delete_all->setDisabled(true); 136 | ui->pb_refresh->setDisabled(true); 137 | } 138 | 139 | 140 | void MainWindow::enableApplications(bool select) 141 | { 142 | ui->listView_applications->setEnabled(true); 143 | ui->listView_applications->setFocus(); 144 | if (select) 145 | ui->listView_applications->setCurrentIndex(applicationProxyModel->index(0, 0)); 146 | } 147 | 148 | 149 | void MainWindow::disableApplications() 150 | { 151 | ui->listView_applications->clearSelection(); 152 | ui->listView_applications->setDisabled(true); 153 | } 154 | 155 | 156 | void MainWindow::setActive() 157 | { 158 | ui->statusWidget->setActive(); 159 | } 160 | 161 | 162 | void MainWindow::setConnecting() 163 | { 164 | ui->statusWidget->setConnecting(); 165 | } 166 | 167 | 168 | void MainWindow::setError() 169 | { 170 | ui->statusWidget->setError(); 171 | } 172 | 173 | 174 | void MainWindow::displayMessageWidgets(const QModelIndex &parent, int first, int last) 175 | { 176 | QString theme = Utils::getTheme(); 177 | QApplication * app = qApp; 178 | 179 | for (int i=first; i<=last; ++i) { 180 | QModelIndex index = messageItemModel->index(i, 0, parent); 181 | if (!index.isValid()) 182 | continue; 183 | MessageItem * item = messageItemModel->itemFromIndex(index); 184 | MessageWidget * messageWidget = new MessageWidget(item, QIcon(cache->getFile(item->appId())), ui->listView_messages); 185 | connect(messageWidget, &MessageWidget::deletionRequested, this, [this, item]{emit deleteMessage(item);}); 186 | ui->listView_messages->setIndexWidget(index, messageWidget); 187 | 188 | app->processEvents(); 189 | } 190 | } 191 | 192 | 193 | void MainWindow::currentChangedCallback(const QModelIndex ¤t, const QModelIndex &previous) 194 | { 195 | ApplicationItem * item = applicationItemModel->itemFromIndex(applicationProxyModel->mapToSource(current)); 196 | if (item) { 197 | ui->label_application->setText(item->text()); 198 | emit applicationChanged(item); 199 | } 200 | } 201 | 202 | 203 | void MainWindow::refreshCallback() 204 | { 205 | emit refresh(); 206 | } 207 | 208 | 209 | void MainWindow::deleteAllCallback() 210 | { 211 | if (!messageItemModel->rowCount()) 212 | return; 213 | 214 | ApplicationItem * item = applicationItemModel->itemFromIndex(applicationProxyModel->mapToSource(selectedApplication())); 215 | 216 | QString text = item->allMessages() ? "Delete ALL messages?" : "Delete all '" + item->name() + "' messages?"; 217 | if (QMessageBox::warning(this, "Are you sure?", text, 218 | QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, 219 | QMessageBox::StandardButton::Cancel) 220 | != QMessageBox::StandardButton::Ok) { 221 | return; 222 | } 223 | 224 | emit deleteAll(item); 225 | } 226 | 227 | 228 | void MainWindow::storeWindowState() 229 | { 230 | settings->setWindowGeometry(saveGeometry()); 231 | settings->setWindowState(saveState()); 232 | settings->setSplitterState(ui->splitter->saveState()); 233 | } 234 | 235 | void MainWindow::restoreWindowState() 236 | { 237 | restoreGeometry(settings->windowGeometry()); 238 | restoreState(settings->windowState()); 239 | ui->splitter->restoreState(settings->splitterState()); 240 | } 241 | 242 | 243 | void MainWindow::closeEvent(QCloseEvent *event) 244 | { 245 | hide(); 246 | event->ignore(); 247 | emit hidden(); 248 | } 249 | 250 | 251 | bool MainWindow::eventFilter(QObject * watched, QEvent * event) 252 | { 253 | switch (event->type()) { 254 | case QEvent::Type::WindowActivate: 255 | emit activated(); 256 | break; 257 | default: 258 | break; 259 | } 260 | 261 | return QMainWindow::eventFilter(watched, event); 262 | } 263 | -------------------------------------------------------------------------------- /src/mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "statuswidget.h" 10 | #include "messageitemmodel.h" 11 | #include "applicationitemmodel.h" 12 | 13 | 14 | QT_BEGIN_NAMESPACE 15 | namespace Ui { class MainWindow; } 16 | QT_END_NAMESPACE 17 | 18 | class MainWindow : public QMainWindow 19 | { 20 | Q_OBJECT 21 | 22 | public: 23 | MainWindow(MessageItemModel * messageItemModel, ApplicationItemModel * applicationItemModel, ApplicationProxyModel * applicationProxyModel, QWidget *parent = nullptr); 24 | ~MainWindow(); 25 | void bringToFront(); 26 | void enableButtons(); 27 | void disableButtons(); 28 | void enableApplications(bool select=true); 29 | void disableApplications(); 30 | void setActive(); 31 | void setConnecting(); 32 | void setError(); 33 | void storeWindowState(); 34 | void restoreWindowState(); 35 | void setFonts(); 36 | void setIcons(); 37 | void showPriority(bool enabled); 38 | QModelIndex selectedApplication(); 39 | bool eventFilter(QObject * watched, QEvent * event); 40 | 41 | signals: 42 | void refresh(); 43 | void deleteAll(ApplicationItem * applicationItem); 44 | void deleteMessage(MessageItem * item); 45 | void applicationChanged(ApplicationItem * item); 46 | void hidden(); 47 | void activated(); 48 | 49 | private slots: 50 | void displayMessageWidgets(const QModelIndex &parent, int first, int last); 51 | void currentChangedCallback(const QModelIndex ¤t, const QModelIndex &previous); 52 | void refreshCallback(); 53 | void deleteAllCallback(); 54 | 55 | protected: 56 | void closeEvent(QCloseEvent *event); 57 | 58 | private: 59 | MessageItemModel * messageItemModel; 60 | ApplicationItemModel * applicationItemModel; 61 | ApplicationProxyModel * applicationProxyModel; 62 | Ui::MainWindow * ui; 63 | 64 | void connectComponents(); 65 | }; 66 | 67 | 68 | #endif // MAINWINDOW_H 69 | -------------------------------------------------------------------------------- /src/mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 809 10 | 647 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 4 20 | 21 | 22 | 4 23 | 24 | 25 | 4 26 | 27 | 28 | 4 29 | 30 | 31 | 32 | 33 | Qt::Orientation::Horizontal 34 | 35 | 36 | 37 | QAbstractItemView::EditTrigger::NoEditTriggers 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Qt::Orientation::Horizontal 48 | 49 | 50 | QSizePolicy::Policy::Fixed 51 | 52 | 53 | 54 | 5 55 | 20 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Qt::Orientation::Horizontal 67 | 68 | 69 | 70 | 40 71 | 20 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Application 80 | 81 | 82 | 83 | 84 | 85 | 86 | Qt::Orientation::Horizontal 87 | 88 | 89 | 90 | 40 91 | 20 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Refresh 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | Delete all messages 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | 123 | 124 | QAbstractItemView::EditTrigger::NoEditTriggers 125 | 126 | 127 | QAbstractItemView::ScrollMode::ScrollPerPixel 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | StatusWidget 141 | QWidget 142 |
statuswidget.h
143 | 1 144 |
145 |
146 | 147 | listView_applications 148 | listView_messages 149 | pb_refresh 150 | pb_delete_all 151 | 152 | 153 | 154 | 155 | pb_refresh 156 | clicked() 157 | MainWindow 158 | refreshCallback() 159 | 160 | 161 | 744 162 | 20 163 | 164 | 165 | 834 166 | 173 167 | 168 | 169 | 170 | 171 | pb_delete_all 172 | clicked() 173 | MainWindow 174 | deleteAllCallback() 175 | 176 | 177 | 793 178 | 21 179 | 180 | 181 | 861 182 | 149 183 | 184 | 185 | 186 | 187 | 188 | deleteAllCallback() 189 | refreshCallback() 190 | 191 |
192 | -------------------------------------------------------------------------------- /src/messageitem.cpp: -------------------------------------------------------------------------------- 1 | #include "messageitem.h" 2 | 3 | 4 | MessageItem::MessageItem(GotifyModel::Message * message) 5 | : QStandardItem() 6 | { 7 | setData(message->id, MessageRole::Id); 8 | setData(message->appId, MessageRole::AppId); 9 | setData(message->priority, MessageRole::Priority); 10 | setData(message->title, MessageRole::Title); 11 | setData(message->date, MessageRole::Date); 12 | setData(message->message, MessageRole::Message); 13 | setData(message->markdown, MessageRole::Markdown); 14 | } 15 | 16 | 17 | int MessageItem::id() 18 | { 19 | return data(MessageRole::Id).toInt(); 20 | } 21 | 22 | 23 | int MessageItem::appId() 24 | { 25 | return data(MessageRole::AppId).toInt(); 26 | } 27 | 28 | 29 | int MessageItem::priority() 30 | { 31 | return data(MessageRole::Priority).toInt(); 32 | } 33 | 34 | 35 | QString MessageItem::title() 36 | { 37 | return data(MessageRole::Title).toString(); 38 | } 39 | 40 | 41 | QDateTime MessageItem::date() 42 | { 43 | return data(MessageRole::Date).toDateTime(); 44 | } 45 | 46 | 47 | QString MessageItem::message() 48 | { 49 | return data(MessageRole::Message).toString(); 50 | } 51 | 52 | 53 | bool MessageItem::markdown() 54 | { 55 | return data(MessageRole::Markdown).toBool(); 56 | } 57 | -------------------------------------------------------------------------------- /src/messageitem.h: -------------------------------------------------------------------------------- 1 | #ifndef MESSAGEITEM_H 2 | #define MESSAGEITEM_H 3 | 4 | 5 | #include 6 | 7 | #include "gotifymodels.h" 8 | 9 | 10 | class MessageItem : public QStandardItem 11 | { 12 | 13 | public: 14 | explicit MessageItem(GotifyModel::Message * message); 15 | int id(); 16 | int appId(); 17 | int priority(); 18 | QString title(); 19 | QDateTime date(); 20 | QString message(); 21 | bool markdown(); 22 | 23 | 24 | private: 25 | enum MessageRole { 26 | Id = Qt::UserRole + 1, 27 | AppId = Qt::UserRole + 2, 28 | Priority = Qt::UserRole + 3, 29 | Title = Qt::UserRole + 4, 30 | Date = Qt::UserRole + 5, 31 | Message = Qt::UserRole + 6, 32 | Markdown = Qt::UserRole + 7, 33 | }; 34 | }; 35 | 36 | 37 | #endif // MESSAGEITEM_H 38 | -------------------------------------------------------------------------------- /src/messageitemmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "messageitemmodel.h" 2 | #include "settings.h" 3 | 4 | 5 | MessageItemModel::MessageItemModel(QObject *parent) 6 | : QStandardItemModel(parent) 7 | { 8 | 9 | } 10 | 11 | 12 | void MessageItemModel::updateLastId(int id) 13 | { 14 | if (id > settings->lastId()) 15 | settings->setLastId(id); 16 | } 17 | 18 | 19 | void MessageItemModel::insertMessage(int row, GotifyModel::Message * message) 20 | { 21 | updateLastId(message->id); 22 | MessageItem * item = new MessageItem(message); 23 | insertRow(row, item); 24 | } 25 | 26 | 27 | void MessageItemModel::appendMessage(GotifyModel::Message * message) 28 | { 29 | updateLastId(message->id); 30 | MessageItem * item = new MessageItem(message); 31 | appendRow(item); 32 | } 33 | 34 | 35 | MessageItem * MessageItemModel::itemFromIndex(const QModelIndex &index) 36 | { 37 | 38 | return static_cast(QStandardItemModel::itemFromIndex(index)); 39 | } 40 | -------------------------------------------------------------------------------- /src/messageitemmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef MESSAGEITEMMODEL_H 2 | #define MESSAGEITEMMODEL_H 3 | 4 | 5 | #include 6 | #include 7 | 8 | #include "messageitem.h" 9 | 10 | 11 | class MessageItemModel : public QStandardItemModel 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit MessageItemModel(QObject * parent = nullptr); 17 | void insertMessage(int row, GotifyModel::Message * message); 18 | void appendMessage(GotifyModel::Message * message); 19 | MessageItem * itemFromIndex(const QModelIndex &index); 20 | 21 | private: 22 | void updateLastId(int id); 23 | 24 | }; 25 | 26 | #endif // MESSAGEITEMMODEL_H 27 | -------------------------------------------------------------------------------- /src/messagewidget.cpp: -------------------------------------------------------------------------------- 1 | #include "messagewidget.h" 2 | #include "cache.h" 3 | #include "requesthandler.h" 4 | #include "settings.h" 5 | #include "ui_messagewidget.h" 6 | #include "utils.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define MAX(x, y) ((x) > (y) ? (x) : (y)) 18 | 19 | 20 | MessageWidget::MessageWidget(MessageItem * item, QIcon icon, QWidget *parent) : 21 | QWidget(parent), 22 | ui(new Ui::MessageWidget), 23 | manager(nullptr) 24 | { 25 | ui->setupUi(this); 26 | 27 | setFonts(); 28 | setIcons(); 29 | setPriorityColor(item->priority()); 30 | 31 | // Application Icon 32 | QPixmap pixmap = icon.pixmap(settings->messageWidgetImageSize()); 33 | ui->label_image->setPixmap(pixmap); 34 | 35 | // Title 36 | ui->label_title->setText(item->title()); 37 | 38 | // Date 39 | ui->label_date->setText((settings->useLocale() ? QLocale::system().toString(item->date(), QLocale::FormatType::ShortFormat) : item->date().toString("yyyy-MM-dd, hh:mm")) + " "); 40 | 41 | // Message contents 42 | ui->frame_content_image->hide(); 43 | ui->label_message_fallback->hide(); 44 | if (settings->forcePlainText()) { 45 | ui->label_message->setText(item->message()); 46 | ui->label_message->setTextFormat(Qt::PlainText); 47 | } else { 48 | // Message image: if the text contains an image url, display it in the content_image label 49 | QString image = Utils::extractImage(item->message()); 50 | if (settings->showImageUrl() && !image.isNull()) 51 | setImage(image); 52 | 53 | // Message text 54 | int allowedWidth = parentWidget()->width() - ui->label_image->width() - ui->label_priority->width() - 40; 55 | QString text = item->message(); 56 | if (settings->showImageUrl() && imageUrl == text) 57 | ui->label_message->hide(); 58 | else if (settings->messageFallback() && Utils::violatesWidth(text, ui->label_message->font(), allowedWidth)) { 59 | ui->label_message_fallback->setText(text); 60 | ui->label_message_fallback->show(); 61 | ui->label_message->hide(); 62 | } else { 63 | if (!Utils::containsHtml(item->message())) 64 | text = Utils::replaceLinks(item->message()); 65 | ui->label_message->setText(text.replace("\n", "
")); 66 | } 67 | 68 | if (settings->renderMarkdown() && item->markdown()) 69 | ui->label_message->setTextFormat(Qt::MarkdownText); 70 | } 71 | 72 | // Size 73 | adjustSize(); 74 | int minHeight = settings->messageWidgetHeightMin(); 75 | item->setSizeHint(QSize(item->sizeHint().width(), MAX(minHeight, height()))); 76 | 77 | connect(ui->label_message, &QLabel::linkHovered, this, &MessageWidget::linkHoveredCallback); 78 | } 79 | 80 | MessageWidget::~MessageWidget() 81 | { 82 | delete ui; 83 | if (manager) delete manager; 84 | } 85 | 86 | 87 | void MessageWidget::setFonts() 88 | { 89 | ui->label_title->setFont(settings->titleFont()); 90 | ui->label_date->setFont(settings->dateFont()); 91 | ui->label_message->setFont(settings->messageFont()); 92 | } 93 | 94 | 95 | void MessageWidget::setIcons() 96 | { 97 | ui->pb_delete->setIcon(QIcon(":/res/themes/" + Utils::getTheme() + "/trashcan.svg")); 98 | ui->label_image->setFixedSize(settings->messageApplicationIconSize()); 99 | ui->pb_delete->setIconSize(0.9*settings->messageButtonSize()); 100 | ui->pb_delete->setFixedSize(settings->messageButtonSize()); 101 | } 102 | 103 | bool 104 | MessageWidget::setImage(QString url) 105 | { 106 | QString filePath = cache->getFile(url); 107 | if (filePath.isNull()) { 108 | // Make a get request 109 | if (!manager) manager = new QNetworkAccessManager(); 110 | QNetworkRequest request; 111 | QEventLoop eventLoop; 112 | request.setUrl(QUrl(url)); 113 | QNetworkReply* reply = manager->get(request); 114 | if (QUrl(url).scheme() == "https" && !settings->selfSignedCertificatePath().isEmpty()) 115 | reply->ignoreSslErrors(Utils::getSelfSignedExpectedErrors(settings->selfSignedCertificatePath())); 116 | connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); 117 | eventLoop.exec(); // TODO: this should run in a non-blocking way. Note that adjustSize() must be called AFTER setting the content_image label 118 | 119 | if (reply->error() != QNetworkReply::NetworkError::NoError) { 120 | qDebug() << reply->errorString(); 121 | reply->deleteLater(); 122 | return false; 123 | } 124 | 125 | // Write the file and store in cache 126 | QString fileName = Utils::getUuid(); 127 | filePath = cache->getFilesDir() + fileName; 128 | Utils::writeFile(filePath, reply->readAll()); 129 | cache->store(url, fileName); 130 | reply->deleteLater(); 131 | } 132 | 133 | // Set the image 134 | QPixmap pixmap(filePath); 135 | float W = settings->messageWidgetContentImageWidth(); 136 | float H = settings->messageWidgetContentImageHeight(); 137 | QListView* lv = static_cast(parent()); 138 | W *= lv->width() - ui->label_content_image->width(); 139 | H *= lv->height(); 140 | 141 | if (pixmap.width() > W || pixmap.height() > H) 142 | ui->label_content_image->setPixmap(pixmap.scaled(W, H, Qt::KeepAspectRatio, Qt::SmoothTransformation)); 143 | else 144 | ui->label_content_image->setPixmap(pixmap); 145 | 146 | ui->frame_content_image->show(); 147 | imageUrl = url; 148 | 149 | return true; 150 | } 151 | 152 | 153 | void MessageWidget::deleteCallback() 154 | { 155 | emit deletionRequested(); 156 | } 157 | 158 | void 159 | MessageWidget::clickedContentImage() 160 | { 161 | QGuiApplication::clipboard()->setText(imageUrl); 162 | } 163 | 164 | void MessageWidget::linkHoveredCallback(const QString& link) 165 | { 166 | if (!settings->popupEnabled()) 167 | return; 168 | 169 | QUrl url(link); 170 | if (Utils::isImage(url.fileName())) { 171 | QPoint pos = QCursor::pos(); 172 | 173 | QString filePath = cache->getFile(link); 174 | if (!filePath.isNull()) { 175 | emit requestHandler->finishedImagePopup(filePath, url, pos); 176 | } else { 177 | if (!manager) manager = new QNetworkAccessManager(); 178 | QNetworkRequest request; 179 | request.setUrl(url); 180 | QNetworkReply* reply = manager->get(request); 181 | if (url.scheme() == "https" && !settings->selfSignedCertificatePath().isEmpty()) 182 | reply->ignoreSslErrors(Utils::getSelfSignedExpectedErrors(settings->selfSignedCertificatePath())); 183 | connect(reply, &QNetworkReply::finished, requestHandler, [this, pos] { requestHandler->imagePopup(pos); }); 184 | } 185 | } 186 | } 187 | 188 | 189 | void MessageWidget::showPriority(bool enabled) 190 | { 191 | ui->label_priority->setFixedWidth(settings->priorityColorWidth()*enabled); 192 | } 193 | 194 | 195 | void MessageWidget::setPriorityColor(int priority) 196 | { 197 | showPriority(settings->priorityColor()); 198 | 199 | if (priority >= 4 && priority <= 7) 200 | ui->label_priority->setStyleSheet("background-color: #b3e67e22;"); 201 | else if (priority > 7) 202 | ui->label_priority->setStyleSheet("background-color: #e74c3c;"); 203 | } 204 | -------------------------------------------------------------------------------- /src/messagewidget.h: -------------------------------------------------------------------------------- 1 | #ifndef MESSAGEWIDGET_H 2 | #define MESSAGEWIDGET_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "messageitem.h" 9 | 10 | 11 | namespace Ui { 12 | class MessageWidget; 13 | } 14 | 15 | class MessageWidget : public QWidget 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | explicit MessageWidget(MessageItem * item, QIcon icon, QWidget *parent = nullptr); 21 | ~MessageWidget(); 22 | void setFonts(); 23 | void setIcons(); 24 | void showPriority(bool enabled); 25 | 26 | signals: 27 | void deletionRequested(); 28 | 29 | private slots: 30 | void deleteCallback(); 31 | void clickedContentImage(); 32 | void linkHoveredCallback(const QString& link); 33 | 34 | private: 35 | Ui::MessageWidget * ui; 36 | QNetworkAccessManager * manager; 37 | 38 | bool setImage(QString url); 39 | void setPriorityColor(int priority); 40 | QString imageUrl; 41 | }; 42 | 43 | #endif // MESSAGEWIDGET_H 44 | -------------------------------------------------------------------------------- /src/messagewidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MessageWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 454 10 | 171 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | true 18 | 19 | 20 | 21 | QLayout::SizeConstraint::SetMinimumSize 22 | 23 | 24 | 4 25 | 26 | 27 | 5 28 | 29 | 30 | 4 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | 40 | 41 | QFrame::Shape::Panel 42 | 43 | 44 | QFrame::Shadow::Sunken 45 | 46 | 47 | 48 | QLayout::SizeConstraint::SetMinimumSize 49 | 50 | 51 | 0 52 | 53 | 54 | 0 55 | 56 | 57 | 5 58 | 59 | 60 | 0 61 | 62 | 63 | 0 64 | 65 | 66 | 67 | 68 | 69 | 0 70 | 0 71 | 72 | 73 | 74 | 75 | 11 76 | 77 | 78 | 79 | TextLabel 80 | 81 | 82 | Qt::TextFormat::RichText 83 | 84 | 85 | true 86 | 87 | 88 | true 89 | 90 | 91 | Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextSelectableByMouse 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 24 103 | 24 104 | 105 | 106 | 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 6 116 | 16777215 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | false 128 | 129 | 130 | QFrame::Shadow::Plain 131 | 132 | 133 | true 134 | 135 | 136 | 137 | 138 | 139 | 140 | Qt::Orientation::Horizontal 141 | 142 | 143 | QSizePolicy::Policy::Maximum 144 | 145 | 146 | 147 | 40 148 | 20 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | Qt::Orientation::Vertical 157 | 158 | 159 | QSizePolicy::Policy::Fixed 160 | 161 | 162 | 163 | 0 164 | 2 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 11 174 | 175 | 176 | 177 | Date 178 | 179 | 180 | Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextSelectableByMouse 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 0 189 | 190 | 191 | 0 192 | 193 | 194 | 0 195 | 196 | 197 | 0 198 | 199 | 200 | 0 201 | 202 | 203 | 204 | 205 | Click to copy URL to clipboard 206 | 207 | 208 | Content Image 209 | 210 | 211 | 212 | 213 | 214 | 215 | Qt::Orientation::Horizontal 216 | 217 | 218 | 219 | 40 220 | 20 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 0 233 | 0 234 | 235 | 236 | 237 | 238 | 17 239 | false 240 | 241 | 242 | 243 | Title 244 | 245 | 246 | true 247 | 248 | 249 | Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextSelectableByMouse 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | true 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | ClickableLabel 271 | QLabel 272 |
clickablelabel.h
273 | 274 | clicked() 275 | 276 |
277 |
278 | 279 | 280 | 281 | pb_delete 282 | clicked() 283 | MessageWidget 284 | deleteCallback() 285 | 286 | 287 | 427 288 | 21 289 | 290 | 291 | 491 292 | 68 293 | 294 | 295 | 296 | 297 | 298 | deleteCallback() 299 | clickedContentImage() 300 | 301 |
302 | -------------------------------------------------------------------------------- /src/processthread.cpp: -------------------------------------------------------------------------------- 1 | #include "processthread.h" 2 | #include "utils.h" 3 | #include "cache.h" 4 | #include "settings.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | 13 | namespace ProcessThread { 14 | 15 | 16 | void Applications::process(GotifyModel::Applications * applications) 17 | { 18 | this->applications = applications; 19 | start(); 20 | } 21 | 22 | 23 | void Applications::run() 24 | { 25 | QNetworkAccessManager manager; 26 | QNetworkRequest request; 27 | QEventLoop eventLoop; 28 | QUrl url = settings->serverUrl(); 29 | 30 | for (auto application : applications->applications) { 31 | // Check the cache 32 | QString file = cache->getFile(application->id); 33 | if (!file.isNull()) { 34 | continue; 35 | } 36 | 37 | // Make a get request 38 | url.setPath("/" + application->image); 39 | request.setUrl(url); 40 | QNetworkReply * reply = manager.get(request); 41 | if (url.scheme() == "https" && !settings->selfSignedCertificatePath().isEmpty()) 42 | reply->ignoreSslErrors(Utils::getSelfSignedExpectedErrors(settings->selfSignedCertificatePath())); 43 | connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); 44 | eventLoop.exec(); 45 | 46 | // Write the file and store in cache 47 | QFileInfo fi(application->image); 48 | QString filePath = cache->getFilesDir() + fi.fileName(); 49 | Utils::writeFile(filePath, reply->readAll()); 50 | cache->store(application->id, fi.fileName()); 51 | 52 | reply->deleteLater(); 53 | } 54 | emit processed(applications); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/processthread.h: -------------------------------------------------------------------------------- 1 | #ifndef PROCESSTHREAD_H 2 | #define PROCESSTHREAD_H 3 | 4 | 5 | #include "gotifymodels.h" 6 | 7 | #include 8 | #include 9 | 10 | 11 | namespace ProcessThread { 12 | 13 | class Applications : public QThread 14 | { 15 | Q_OBJECT 16 | 17 | public: 18 | void process(GotifyModel::Applications * applications); 19 | 20 | signals: 21 | void processed(GotifyModel::Applications * applications); 22 | 23 | protected: 24 | virtual void run(); 25 | 26 | private: 27 | GotifyModel::Applications * applications; 28 | }; 29 | 30 | } 31 | 32 | #endif // PROCESSTHREAD_H 33 | -------------------------------------------------------------------------------- /src/requesthandler.cpp: -------------------------------------------------------------------------------- 1 | #include "requesthandler.h" 2 | #include "utils.h" 3 | #include "cache.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | RequestHandler * requestHandler = nullptr; 11 | 12 | 13 | RequestHandler * RequestHandler::getInstance() 14 | { 15 | if(!requestHandler) { 16 | requestHandler = new RequestHandler(); 17 | } 18 | return requestHandler; 19 | } 20 | 21 | 22 | void RequestHandler::applications() 23 | { 24 | QNetworkReply * reply = qobject_cast(sender()); 25 | if(!reply) { 26 | emit finished(); 27 | return; 28 | } 29 | 30 | if (reply->error() != QNetworkReply::NetworkError::NoError) { 31 | emit replyError(reply->error(), reply->errorString()); 32 | emit finished(); 33 | reply->deleteLater(); 34 | return; 35 | } 36 | 37 | QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); 38 | if (document.isNull()) { 39 | qDebug() << "json document is null"; 40 | emit parseError(); 41 | emit finished(); 42 | reply->deleteLater(); 43 | return; 44 | } 45 | 46 | QJsonArray applicationsArray = document.array(); 47 | emit finishedApplications(new GotifyModel::Applications(applicationsArray)); 48 | emit finished(); 49 | reply->deleteLater(); 50 | } 51 | 52 | 53 | void RequestHandler::messages(bool missed) 54 | { 55 | QNetworkReply * reply = qobject_cast(sender()); 56 | if(!reply) { 57 | emit finished(); 58 | return; 59 | } 60 | 61 | if (reply->error() != QNetworkReply::NetworkError::NoError) { 62 | emit replyError(reply->error(), reply->errorString()); 63 | emit finished(); 64 | reply->deleteLater(); 65 | return; 66 | } 67 | 68 | QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); 69 | if (document.isNull()) { 70 | qDebug() << "json document is null"; 71 | emit parseError(); 72 | emit finished(); 73 | reply->deleteLater(); 74 | return; 75 | } 76 | 77 | QJsonObject object = document.object(); 78 | QJsonArray messagesArray = object["messages"].toArray(); 79 | 80 | if (missed) { 81 | emit finishedMissedMessages(new GotifyModel::Messages(messagesArray)); 82 | } else { 83 | emit finishedMessages(new GotifyModel::Messages(messagesArray)); 84 | } 85 | 86 | 87 | emit finished(); 88 | reply->deleteLater(); 89 | } 90 | 91 | 92 | void RequestHandler::testServer() 93 | { 94 | QNetworkReply * reply = qobject_cast(sender()); 95 | if(!reply) { 96 | emit finished(); 97 | return; 98 | } 99 | 100 | if (reply->error() != QNetworkReply::NetworkError::NoError) { 101 | emit replyError(reply->error(), reply->errorString()); 102 | emit finished(); 103 | reply->deleteLater(); 104 | return; 105 | } 106 | 107 | QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); 108 | if (document.isNull()) { 109 | qDebug() << "json document is null"; 110 | emit parseError(); 111 | emit finished(); 112 | reply->deleteLater(); 113 | return; 114 | } 115 | 116 | emit serverOk(); 117 | emit finished(); 118 | reply->deleteLater(); 119 | } 120 | 121 | 122 | void RequestHandler::imagePopup(QPoint pos) 123 | { 124 | QNetworkReply * reply = qobject_cast(sender()); 125 | if(!reply) { 126 | emit finished(); 127 | return; 128 | } 129 | 130 | if (reply->error() != QNetworkReply::NetworkError::NoError) { 131 | emit replyError(reply->error(), reply->errorString()); 132 | emit finished(); 133 | reply->deleteLater(); 134 | return; 135 | } 136 | 137 | QString fileName = Utils::getUuid(); 138 | QString filePath = cache->getFilesDir() + fileName; 139 | Utils::writeFile(filePath, reply->readAll()); 140 | QUrl url = reply->request().url(); 141 | cache->store(url.toString(), fileName); 142 | 143 | emit finishedImagePopup(filePath, url, pos); 144 | emit finished(); 145 | reply->deleteLater(); 146 | } 147 | -------------------------------------------------------------------------------- /src/requesthandler.h: -------------------------------------------------------------------------------- 1 | #ifndef REQUESTHANDLER_H 2 | #define REQUESTHANDLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "gotifymodels.h" 9 | 10 | 11 | /** 12 | * @brief Handle QNetworkReply from QNetworkManager get/post/... requests. 13 | * 14 | * Each handler: 15 | * - corresponds to a GotifyApi method. 16 | * - emits a GotifyModel upon successful handling 17 | * - emits a QNetworkReply::NetworkError and QString when a handler error occurs 18 | * 19 | * Connect the corresponding handler to the GotifyApi::<> finished signal. 20 | * Connect to the finished<> signals to receive the GotifyModel. 21 | */ 22 | class RequestHandler : public QObject 23 | { 24 | Q_OBJECT 25 | public: 26 | explicit RequestHandler(QObject * parent = nullptr) : QObject(parent) {}; 27 | static RequestHandler * getInstance(); 28 | 29 | void applications(); 30 | void messages(bool missed=false); 31 | void testServer(); 32 | void imagePopup(QPoint pos); 33 | 34 | signals: 35 | /* Finished - successful or not */ 36 | void finished(); 37 | /* Finished successfully */ 38 | void finishedMessages(GotifyModel::Messages * messagesModel); 39 | void finishedMissedMessages(GotifyModel::Messages * messagesModel); 40 | void finishedApplications(GotifyModel::Applications * applicationsModel); 41 | void finishedImagePopup(const QString& fileName, const QUrl& url, QPoint pos); 42 | void serverOk(); 43 | /* Finished with an error */ 44 | void replyError(QNetworkReply::NetworkError error, QString errorString); 45 | void parseError(); 46 | }; 47 | 48 | 49 | extern RequestHandler * requestHandler; 50 | 51 | 52 | #endif // REQUESTHANDLER_H 53 | -------------------------------------------------------------------------------- /src/serverinfodialog.cpp: -------------------------------------------------------------------------------- 1 | #include "serverinfodialog.h" 2 | #include "requesthandler.h" 3 | #include "settings.h" 4 | #include "ui_serverinfodialog.h" 5 | #include "utils.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | ServerInfoDialog::ServerInfoDialog(QWidget* parent, QUrl url, QByteArray clientToken, QString certPath) 12 | : QDialog(parent) 13 | , ui(new Ui::ServerInfoDialog) 14 | { 15 | this->certPath = certPath; 16 | 17 | ui->setupUi(this); 18 | ui->pb_certificate->hide(); 19 | ui->line_url->setText(url.toString()); 20 | ui->line_token->setText(clientToken); 21 | if (url.scheme() == "https" && !certPath.isEmpty()) 22 | ui->label_status->setText("Certificate: " + certPath); 23 | ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setDisabled(true); 24 | setStyleSheet(Utils::readFile(":/res/themes/" + Utils::getTheme() + "/ServerInfoDialog.qss")); 25 | adjustSize(); 26 | 27 | gotifyApi = new GotifyApi(QUrl(), "", ""); 28 | 29 | connect(requestHandler, &RequestHandler::serverOk, this, &ServerInfoDialog::testSuccessCallback); 30 | connect(requestHandler, &RequestHandler::replyError, this, &ServerInfoDialog::testErrorCallback); 31 | connect(requestHandler, &RequestHandler::finished, this, [this] { ui->pb_test->setEnabled(true); }); 32 | connect(this, &QDialog::accepted, this, &ServerInfoDialog::acceptedCallback); 33 | } 34 | 35 | ServerInfoDialog::~ServerInfoDialog() 36 | { 37 | delete ui; 38 | delete gotifyApi; 39 | } 40 | 41 | 42 | void ServerInfoDialog::acceptedCallback() 43 | { 44 | QUrl serverUrl(ui->line_url->text()); 45 | settings->setServerUrl(serverUrl); 46 | settings->setClientToken(ui->line_token->text().toUtf8()); 47 | settings->setSelfSignedCertificatePath(serverUrl.scheme() == "https" ? certPath : ""); 48 | 49 | // Copy the self-signed certificate to application data directory 50 | if (serverUrl.scheme() == "https" && !certPath.isEmpty()) { 51 | QString directory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/"; 52 | if (!QDir().mkpath(directory)) 53 | qFatal() << "AppDataLocation directory could not be created:" << directory; 54 | 55 | QString destination = directory + "certificate"; 56 | 57 | if (certPath != destination) { 58 | QFile sFile(certPath); 59 | QFile dFile(destination); 60 | 61 | if (dFile.exists()) 62 | dFile.remove(); 63 | 64 | if (sFile.copy(destination)) 65 | settings->setSelfSignedCertificatePath(destination); 66 | else 67 | qFatal() << "Failed to copy certificate to AppDataLocation:" << destination; 68 | } 69 | } else { 70 | settings->setSelfSignedCertificatePath(""); 71 | } 72 | 73 | emit settings->serverChanged(); 74 | } 75 | 76 | 77 | void ServerInfoDialog::enableInputs() 78 | { 79 | ui->line_url->setEnabled(true); 80 | ui->line_token->setEnabled(true); 81 | ui->pb_test->setEnabled(true); 82 | } 83 | 84 | 85 | void ServerInfoDialog::disableInputs() 86 | { 87 | ui->line_url->setDisabled(true); 88 | ui->line_token->setDisabled(true); 89 | ui->pb_test->setDisabled(true); 90 | } 91 | 92 | 93 | void ServerInfoDialog::inputChangedCallback() 94 | { 95 | ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setDisabled(true); 96 | Utils::updateWidgetProperty(ui->pb_test, "state", ""); 97 | } 98 | 99 | void 100 | ServerInfoDialog::urlChangedCallback(QString text) 101 | { 102 | ui->pb_certificate->setVisible(QUrl(text).scheme() == "https"); 103 | } 104 | 105 | void 106 | ServerInfoDialog::certificateCallback() 107 | { 108 | QString path = QFileDialog::getOpenFileName(this, "Import self-signed server certificate", QDir::homePath(), "Certificates (*.pem *.crt);;*"); 109 | if (path.isEmpty()) 110 | return; 111 | 112 | QList expectedErrors = Utils::getSelfSignedExpectedErrors(path); 113 | if (expectedErrors.size()) { 114 | certPath = path; 115 | ui->label_status->setText("Certificate: " + certPath); 116 | Utils::updateWidgetProperty(ui->pb_certificate, "state", ""); 117 | } else { 118 | certPath = ""; 119 | ui->label_status->setText("ERROR: Invalid certificate"); 120 | Utils::updateWidgetProperty(ui->pb_certificate, "state", "failed"); 121 | } 122 | 123 | ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setDisabled(true); 124 | Utils::updateWidgetProperty(ui->pb_test, "state", ""); 125 | } 126 | 127 | void 128 | ServerInfoDialog::testCallback() 129 | { 130 | Utils::updateWidgetProperty(ui->pb_test, "state", ""); 131 | Utils::updateWidgetProperty(ui->line_url, "state", ""); 132 | Utils::updateWidgetProperty(ui->line_token, "state", ""); 133 | Utils::updateWidgetProperty(ui->pb_certificate, "state", ""); 134 | 135 | // Clean the url 136 | QString text = ui->line_url->text(); 137 | while (text.size() > 0 && text.at(text.size()-1) == '/') 138 | text.chop(1); 139 | ui->line_url->setText(text); 140 | 141 | QUrl url(text); 142 | QByteArray clientToken(ui->line_token->text().toUtf8()); 143 | if (url.isEmpty() || clientToken.isEmpty()) { 144 | return; 145 | } 146 | 147 | delete gotifyApi; 148 | gotifyApi = new GotifyApi(url, clientToken, certPath); 149 | 150 | disableInputs(); 151 | ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setDisabled(true); 152 | 153 | // Verify url and clientToken 154 | QNetworkReply * reply = gotifyApi->messages(1); 155 | connect(reply, &QNetworkReply::finished, requestHandler, &RequestHandler::testServer); 156 | } 157 | 158 | void 159 | ServerInfoDialog::testSuccessCallback() 160 | { 161 | Utils::updateWidgetProperty(ui->pb_test, "state", "success"); 162 | Utils::updateWidgetProperty(ui->line_token, "state", "success"); 163 | Utils::updateWidgetProperty(ui->line_url, "state", "success"); 164 | 165 | ui->label_status->setText("Server information verified"); 166 | 167 | setMinimumWidth(width()); 168 | adjustSize(); 169 | 170 | enableInputs(); 171 | ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled(true); 172 | ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setFocus(); 173 | } 174 | 175 | void 176 | ServerInfoDialog::testErrorCallback(QNetworkReply::NetworkError error, QString errorString) 177 | { 178 | switch (error) { 179 | case QNetworkReply::AuthenticationRequiredError: 180 | Utils::updateWidgetProperty(ui->pb_test, "state", "failed"); 181 | Utils::updateWidgetProperty(ui->line_token, "state", "failed"); 182 | Utils::updateWidgetProperty(ui->line_url, "state", "success"); 183 | ui->line_token->setFocus(); 184 | ui->label_status->setText(errorString); 185 | break; 186 | default: 187 | Utils::updateWidgetProperty(ui->pb_test, "state", "failed"); 188 | Utils::updateWidgetProperty(ui->line_token, "state", ""); 189 | Utils::updateWidgetProperty(ui->line_url, "state", "failed"); 190 | ui->line_url->setFocus(); 191 | ui->label_status->setText(errorString); 192 | break; 193 | } 194 | 195 | // Resize the widget to fit the error string 196 | setMinimumWidth(width()); 197 | adjustSize(); 198 | 199 | enableInputs(); 200 | } 201 | -------------------------------------------------------------------------------- /src/serverinfodialog.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVERINFODIALOG_H 2 | #define SERVERINFODIALOG_H 3 | 4 | #include 5 | #include 6 | 7 | #include "gotifyapi.h" 8 | 9 | 10 | namespace Ui { 11 | class ServerInfoDialog; 12 | } 13 | 14 | class ServerInfoDialog : public QDialog 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit ServerInfoDialog(QWidget* parent = nullptr, QUrl url = QUrl(), QByteArray clientToken = nullptr, QString certPath = ""); 20 | ~ServerInfoDialog(); 21 | 22 | private slots: 23 | void acceptedCallback(); 24 | void inputChangedCallback(); 25 | void urlChangedCallback(QString text); 26 | void certificateCallback(); 27 | void testCallback(); 28 | void testSuccessCallback(); 29 | void testErrorCallback(QNetworkReply::NetworkError error, QString errorString); 30 | 31 | private: 32 | GotifyApi * gotifyApi; 33 | Ui::ServerInfoDialog *ui; 34 | QString certPath; 35 | 36 | void enableInputs(); 37 | void disableInputs(); 38 | 39 | }; 40 | 41 | #endif // SERVERINFODIALOG_H 42 | -------------------------------------------------------------------------------- /src/serverinfodialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ServerInfoDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 351 10 | 169 11 | 12 | 13 | 14 | Server info 15 | 16 | 17 | 18 | 19 | 20 | Import self-signed server certificate 21 | 22 | 23 | Certificate 24 | 25 | 26 | 27 | 28 | 29 | 30 | Qt::Orientation::Horizontal 31 | 32 | 33 | QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok 34 | 35 | 36 | 37 | 38 | 39 | 40 | Qt::Orientation::Horizontal 41 | 42 | 43 | 44 | 40 45 | 20 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Server url: 56 | 57 | 58 | 59 | 60 | 61 | 62 | https://gotify.example.com 63 | 64 | 65 | 66 | 67 | 68 | 69 | Client token: 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | Test 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Qt::AlignmentFlag::AlignCenter 99 | 100 | 101 | true 102 | 103 | 104 | Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextSelectableByMouse 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | buttonBox 114 | accepted() 115 | ServerInfoDialog 116 | accept() 117 | 118 | 119 | 290 120 | 120 121 | 122 | 123 | 157 124 | 168 125 | 126 | 127 | 128 | 129 | buttonBox 130 | rejected() 131 | ServerInfoDialog 132 | reject() 133 | 134 | 135 | 290 136 | 120 137 | 138 | 139 | 286 140 | 168 141 | 142 | 143 | 144 | 145 | line_url 146 | textChanged(QString) 147 | ServerInfoDialog 148 | inputChangedCallback() 149 | 150 | 151 | 123 152 | 26 153 | 154 | 155 | 8 156 | 98 157 | 158 | 159 | 160 | 161 | line_token 162 | textChanged(QString) 163 | ServerInfoDialog 164 | inputChangedCallback() 165 | 166 | 167 | 168 168 | 44 169 | 170 | 171 | 28 172 | 104 173 | 174 | 175 | 176 | 177 | pb_test 178 | clicked() 179 | ServerInfoDialog 180 | testCallback() 181 | 182 | 183 | 224 184 | 83 185 | 186 | 187 | 67 188 | 119 189 | 190 | 191 | 192 | 193 | pb_certificate 194 | clicked() 195 | ServerInfoDialog 196 | certificateCallback() 197 | 198 | 199 | 117 200 | 85 201 | 202 | 203 | 101 204 | 157 205 | 206 | 207 | 208 | 209 | line_url 210 | textChanged(QString) 211 | ServerInfoDialog 212 | urlChangedCallback(QString) 213 | 214 | 215 | 219 216 | 29 217 | 218 | 219 | 310 220 | 168 221 | 222 | 223 | 224 | 225 | 226 | inputChangedCallback() 227 | testCallback() 228 | certificateCallback() 229 | urlChangedCallback(QString) 230 | 231 | 232 | -------------------------------------------------------------------------------- /src/settings.cpp: -------------------------------------------------------------------------------- 1 | #include "settings.h" 2 | 3 | 4 | Settings * settings = nullptr; 5 | 6 | 7 | //------------------------------------------------------------------------------ 8 | Settings * Settings::getInstance() { 9 | if(!settings) { 10 | settings = new Settings(); 11 | } 12 | return settings; 13 | } 14 | 15 | 16 | //------------------------------------------------------------------------------ 17 | void Settings::setServerUrl(QUrl url) 18 | { 19 | setValue("serverUrl", url.toString()); 20 | } 21 | 22 | QUrl Settings::serverUrl() 23 | { 24 | return QUrl(value("serverUrl", "").toString()); 25 | } 26 | 27 | 28 | //------------------------------------------------------------------------------ 29 | void Settings::setClientToken(QByteArray token) 30 | { 31 | setValue("clientToken", QString(token)); 32 | } 33 | 34 | QByteArray Settings::clientToken() 35 | { 36 | return value("clientToken", "").toByteArray(); 37 | } 38 | 39 | 40 | //------------------------------------------------------------------------------ 41 | void Settings::setLastId(int id) 42 | { 43 | setValue("lastId", id); 44 | } 45 | 46 | int Settings::lastId() 47 | { 48 | return value("lastId", 0).toInt(); 49 | } 50 | 51 | 52 | //------------------------------------------------------------------------------ 53 | void Settings::setNotifyMissed(bool mode) 54 | { 55 | setValue("notifyMissed", mode); 56 | } 57 | 58 | 59 | bool Settings::notifyMissed() 60 | { 61 | return value("notifyMissed", true).toBool(); 62 | } 63 | 64 | 65 | //------------------------------------------------------------------------------ 66 | void Settings::setStatusLabelSize(QSize size) 67 | { 68 | setValue("size/statusLabelSize", size); 69 | } 70 | 71 | QSize Settings::statusLabelSize() 72 | { 73 | return value("size/statusLabelSize", QSize(25, 25)).toSize(); 74 | } 75 | 76 | 77 | //------------------------------------------------------------------------------ 78 | void Settings::setMainButtonSize(QSize size) 79 | { 80 | setValue("size/mainButtonSize", size); 81 | } 82 | 83 | QSize Settings::mainButtonSize() 84 | { 85 | return value("size/mainButtonSize", QSize(33, 33)).toSize(); 86 | } 87 | 88 | 89 | //------------------------------------------------------------------------------ 90 | void Settings::setMessageButtonSize(QSize size) 91 | { 92 | setValue("size/messageButtonSize", size); 93 | } 94 | 95 | QSize Settings::messageButtonSize() 96 | { 97 | return value("size/messageButtonSize", QSize(25, 25)).toSize(); 98 | } 99 | 100 | 101 | //------------------------------------------------------------------------------ 102 | void Settings::setApplicationIconSize(QSize size) 103 | { 104 | setValue("size/applicationIconSize", size); 105 | } 106 | 107 | QSize Settings::applicationIconSize() 108 | { 109 | return value("size/applicationIconSize", QSize(40, 40)).toSize(); 110 | } 111 | 112 | 113 | //------------------------------------------------------------------------------ 114 | void Settings::setMessageApplicationIconSize(QSize size) 115 | { 116 | setValue("size/messageApplicationIconSize", size); 117 | } 118 | 119 | QSize Settings::messageApplicationIconSize() 120 | { 121 | return value("size/messageApplicationIconSize", QSize(33, 33)).toSize(); 122 | } 123 | 124 | 125 | //------------------------------------------------------------------------------ 126 | void Settings::setWindowGeometry(QByteArray geometry) 127 | { 128 | setValue("state/geometry", geometry); 129 | } 130 | 131 | QByteArray Settings::windowGeometry() 132 | { 133 | return value("state/geometry").toByteArray(); 134 | } 135 | 136 | 137 | //------------------------------------------------------------------------------ 138 | void Settings::setWindowState(QByteArray state) 139 | { 140 | setValue("state/state", state); 141 | } 142 | 143 | QByteArray Settings::windowState() 144 | { 145 | return value("state/state").toByteArray(); 146 | } 147 | 148 | 149 | //------------------------------------------------------------------------------ 150 | void Settings::setSplitterState(QByteArray state) 151 | { 152 | setValue("state/splitterState", state); 153 | } 154 | 155 | QByteArray Settings::splitterState() 156 | { 157 | return value("state/splitterState").toByteArray(); 158 | } 159 | 160 | 161 | //------------------------------------------------------------------------------ 162 | void Settings::setMessageWidgetHeightMin(int height) 163 | { 164 | setValue("messageWidgetHeightMin", height); 165 | } 166 | 167 | int Settings::messageWidgetHeightMin() 168 | { 169 | return value("messageWidgetHeightMin", 100).toInt(); 170 | } 171 | 172 | 173 | //------------------------------------------------------------------------------ 174 | void Settings::setMessageWidgetImageSize(QSize size) 175 | { 176 | setValue("size/messageWidgetImageSizeMax", size); 177 | } 178 | 179 | QSize Settings::messageWidgetImageSize() 180 | { 181 | return value("size/messageWidgetImageSizeMax", QSize(33, 33)).toSize(); 182 | } 183 | 184 | 185 | //------------------------------------------------------------------------------ 186 | void Settings::setMessageWidgetContentImageWidth(float width) 187 | { 188 | setValue("messageWidgetContentImageWidth", width); 189 | } 190 | 191 | float Settings::messageWidgetContentImageWidth() 192 | { 193 | return value("messageWidgetContentImageWidth", 0.8f).toFloat(); 194 | } 195 | 196 | 197 | //------------------------------------------------------------------------------ 198 | void Settings::setMessageWidgetContentImageHeight(float height) 199 | { 200 | setValue("messageWidgetContentImageHeight", height); 201 | } 202 | 203 | float Settings::messageWidgetContentImageHeight() 204 | { 205 | return value("messageWidgetContentImageHeight", 0.3f).toFloat(); 206 | } 207 | 208 | //------------------------------------------------------------------------------ 209 | void 210 | Settings::setMessageFallback(bool mode) 211 | { 212 | setValue("messageFallback", mode); 213 | } 214 | 215 | bool 216 | Settings::messageFallback() 217 | { 218 | return value("messageFallback", false).toBool(); 219 | } 220 | 221 | //------------------------------------------------------------------------------ 222 | void 223 | Settings::setRenderMarkdown(bool mode) 224 | { 225 | setValue("renderMarkdown", mode); 226 | } 227 | 228 | bool 229 | Settings::renderMarkdown() 230 | { 231 | return value("renderMarkdown", false).toBool(); 232 | } 233 | 234 | //------------------------------------------------------------------------------ 235 | void Settings::setShowImageUrl(bool mode) 236 | { 237 | setValue("showImageUrl", mode); 238 | } 239 | 240 | 241 | bool Settings::showImageUrl() 242 | { 243 | return value("showImageUrl", true).toBool(); 244 | } 245 | 246 | //------------------------------------------------------------------------------ 247 | void 248 | Settings::setForcePlainText(bool mode) 249 | { 250 | setValue("forcePlainText", mode); 251 | } 252 | 253 | bool 254 | Settings::forcePlainText() 255 | { 256 | return value("forcePlainText", false).toBool(); 257 | } 258 | 259 | //------------------------------------------------------------------------------ 260 | void Settings::setPriorityColor(bool mode) 261 | { 262 | setValue("priorityColor", mode); 263 | } 264 | 265 | 266 | bool Settings::priorityColor() 267 | { 268 | return value("priorityColor", true).toBool(); 269 | } 270 | 271 | 272 | //------------------------------------------------------------------------------ 273 | void Settings::setPriorityColorWidth(int w) 274 | { 275 | setValue("size/priorityColorWidth", w); 276 | } 277 | 278 | 279 | int Settings::priorityColorWidth() 280 | { 281 | return value("size/priorityColorWidth", 6).toInt(); 282 | } 283 | 284 | 285 | //------------------------------------------------------------------------------ 286 | void Settings::setNotificationPriority(int priority) 287 | { 288 | setValue("notificationPriority", priority); 289 | } 290 | 291 | int Settings::notificationPriority() 292 | { 293 | return value("notificationPriority", 0).toInt(); 294 | } 295 | 296 | 297 | //------------------------------------------------------------------------------ 298 | void Settings::setNotificationDurationMs(int ms) 299 | { 300 | setValue("notificationDuration", ms); 301 | } 302 | 303 | int Settings::notificationDurationMs() 304 | { 305 | return value("notificationDuration", 5000).toInt(); 306 | } 307 | 308 | 309 | //------------------------------------------------------------------------------ 310 | void Settings::setNotificationShowIcon(bool mode) 311 | { 312 | setValue("notificationShowIcon", mode); 313 | } 314 | 315 | bool Settings::notificationShowIcon() 316 | { 317 | return value("notificationShowIcon", true).toBool(); 318 | } 319 | 320 | 321 | //------------------------------------------------------------------------------ 322 | void Settings::setNotificationClick(bool mode) 323 | { 324 | setValue("notificationClick", mode); 325 | } 326 | 327 | bool Settings::notificationClick() 328 | { 329 | return value("notificationClick", true).toBool(); 330 | } 331 | 332 | 333 | //------------------------------------------------------------------------------ 334 | void Settings::setTrayUnreadEnabled(bool mode) 335 | { 336 | setValue("trayUnread", mode); 337 | } 338 | 339 | bool Settings::trayUnreadEnabled() 340 | { 341 | return value("trayUnread", false).toBool(); 342 | } 343 | 344 | 345 | //------------------------------------------------------------------------------ 346 | void Settings::setPopupEnabled(bool mode) 347 | { 348 | setValue("popupEnabled", mode); 349 | } 350 | 351 | bool Settings::popupEnabled() 352 | { 353 | return value("popupEnabled", false).toBool(); 354 | } 355 | 356 | 357 | //------------------------------------------------------------------------------ 358 | void Settings::setPopupHeight(int height) 359 | { 360 | setValue("popupHeight", height); 361 | } 362 | 363 | int Settings::popupHeight() 364 | { 365 | return value("popupHeight", 400).toInt(); 366 | } 367 | 368 | 369 | //------------------------------------------------------------------------------ 370 | void Settings::setPopupWidth(int width) 371 | { 372 | setValue("popupWidth", width); 373 | } 374 | 375 | int Settings::popupWidth() 376 | { 377 | return value("popupWidth", 400).toInt(); 378 | } 379 | 380 | 381 | //------------------------------------------------------------------------------ 382 | void 383 | Settings::setSelectedApplicationFont(QFont font) 384 | { 385 | setValue("font/selectedApplication", font.toString()); 386 | } 387 | 388 | 389 | QFont Settings::selectedApplicationFont() 390 | { 391 | QFont font; 392 | QString s = value("font/selectedApplication", "").toString(); 393 | if (s.isEmpty()) { 394 | font.setPointSize(font.pointSize()+3); 395 | font.setBold(true); 396 | } else { 397 | font.fromString(s); 398 | } 399 | return font; 400 | } 401 | 402 | 403 | //------------------------------------------------------------------------------ 404 | void Settings::setApplicationFont(QFont font) 405 | { 406 | setValue("font/application", font.toString()); 407 | } 408 | 409 | 410 | QFont Settings::applicationFont() 411 | { 412 | QFont font; 413 | QString s = value("font/application", "").toString(); 414 | if (!s.isEmpty()) { 415 | font.fromString(s); 416 | } 417 | return font; 418 | } 419 | 420 | 421 | //------------------------------------------------------------------------------ 422 | void Settings::setTitleFont(QFont font) 423 | { 424 | setValue("font/title", font.toString()); 425 | } 426 | 427 | 428 | QFont Settings::titleFont() 429 | { 430 | QFont font; 431 | QString s = value("font/title", "").toString(); 432 | if (s.isEmpty()) { 433 | font.setBold(true); 434 | } else { 435 | font.fromString(s); 436 | } 437 | return font; 438 | } 439 | 440 | 441 | //------------------------------------------------------------------------------ 442 | void Settings::setDateFont(QFont font) 443 | { 444 | setValue("font/date", font.toString()); 445 | } 446 | 447 | 448 | QFont Settings::dateFont() 449 | { 450 | QFont font; 451 | QString s = value("font/date", "").toString(); 452 | if (s.isEmpty()) { 453 | font.setItalic(true); 454 | } else { 455 | font.fromString(s); 456 | } 457 | return font; 458 | } 459 | 460 | 461 | //------------------------------------------------------------------------------ 462 | void Settings::setMessageFont(QFont font) 463 | { 464 | setValue("font/message", font.toString()); 465 | } 466 | 467 | 468 | QFont Settings::messageFont() 469 | { 470 | QFont font; 471 | QString s = value("font/message", "").toString(); 472 | if (!s.isEmpty()) { 473 | font.fromString(s); 474 | } 475 | return font; 476 | } 477 | 478 | 479 | //------------------------------------------------------------------------------ 480 | void Settings::setUseLocale(bool mode) 481 | { 482 | setValue("useLocale", mode); 483 | } 484 | 485 | 486 | bool Settings::useLocale() 487 | { 488 | return value("useLocale", true).toBool(); 489 | } 490 | 491 | 492 | //------------------------------------------------------------------------------ 493 | void Settings::setSortApplications(bool mode) 494 | { 495 | setValue("sortApplications", mode); 496 | } 497 | 498 | 499 | bool Settings::sortApplications() 500 | { 501 | return value("sortApplications", true).toBool(); 502 | } 503 | 504 | //------------------------------------------------------------------------------ 505 | int 506 | Settings::heartbeatInterval() 507 | { 508 | return value("heartbeatInterval", 60000).toInt(); 509 | } 510 | 511 | //------------------------------------------------------------------------------ 512 | void 513 | Settings::setSelfSignedCertificatePath(QString path) 514 | { 515 | setValue("selfSignedCertificatePath", path); 516 | } 517 | 518 | QString 519 | Settings::selfSignedCertificatePath() 520 | { 521 | return value("selfSignedCertificatePath", "").toString(); 522 | } 523 | 524 | //------------------------------------------------------------------------------ 525 | void 526 | Settings::setCustomTray(bool mode) 527 | { 528 | setValue("customTray", mode); 529 | } 530 | 531 | bool 532 | Settings::customTray() 533 | { 534 | return value("customTray", false).toBool(); 535 | } 536 | 537 | //------------------------------------------------------------------------------ 538 | void 539 | Settings::setCustomTrayUnread(bool mode) 540 | { 541 | setValue("customTrayUnread", mode); 542 | } 543 | 544 | bool 545 | Settings::customTrayUnread() 546 | { 547 | return value("customTrayUnread", false).toBool(); 548 | } 549 | 550 | //------------------------------------------------------------------------------ 551 | void 552 | Settings::setCustomTrayError(bool mode) 553 | { 554 | setValue("customTrayError", mode); 555 | } 556 | 557 | bool 558 | Settings::customTrayError() 559 | { 560 | return value("customTrayError", true).toBool(); 561 | } 562 | 563 | //------------------------------------------------------------------------------ 564 | void 565 | Settings::setCustomTrayPath(QString path) 566 | { 567 | setValue("customTrayPath", path); 568 | } 569 | QString 570 | Settings::customTrayPath() 571 | { 572 | return value("customTrayPath", ":/res/icons/tray.png").toString(); 573 | } 574 | 575 | //------------------------------------------------------------------------------ 576 | void 577 | Settings::setCustomTrayUnreadPath(QString path) 578 | { 579 | setValue("customTrayUnreadPath", path); 580 | } 581 | QString 582 | Settings::customTrayUnreadPath() 583 | { 584 | return value("customTrayUnreadPath", ":/res/icons/tray-unread.png").toString(); 585 | } 586 | 587 | //------------------------------------------------------------------------------ 588 | void 589 | Settings::setCustomTrayErrorPath(QString path) 590 | { 591 | setValue("customTrayErrorPath", path); 592 | } 593 | QString 594 | Settings::customTrayErrorPath() 595 | { 596 | return value("customTrayErrorPath", ":/res/icons/tray-error.png").toString(); 597 | } 598 | -------------------------------------------------------------------------------- /src/settings.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGS_H 2 | #define SETTINGS_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | class Settings : public QSettings 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | Settings() : QSettings(qApp->applicationName()) {}; 18 | 19 | static Settings * getInstance(); 20 | 21 | void setServerUrl(QUrl url); 22 | QUrl serverUrl(); 23 | 24 | void setClientToken(QByteArray token); 25 | QByteArray clientToken(); 26 | 27 | void setLastId(int id); 28 | int lastId(); 29 | 30 | void setNotifyMissed(bool mode); 31 | bool notifyMissed(); 32 | 33 | void setStatusLabelSize(QSize size); 34 | QSize statusLabelSize(); 35 | 36 | void setMainButtonSize(QSize size); 37 | QSize mainButtonSize(); 38 | 39 | void setMessageButtonSize(QSize size); 40 | QSize messageButtonSize(); 41 | 42 | void setApplicationIconSize(QSize size); 43 | QSize applicationIconSize(); 44 | 45 | void setMessageApplicationIconSize(QSize size); 46 | QSize messageApplicationIconSize(); 47 | 48 | void setWindowGeometry(QByteArray geometry); 49 | QByteArray windowGeometry(); 50 | 51 | void setWindowState(QByteArray state); 52 | QByteArray windowState(); 53 | 54 | void setSplitterState(QByteArray state); 55 | QByteArray splitterState(); 56 | 57 | void setMessageWidgetHeightMin(int height); 58 | int messageWidgetHeightMin(); 59 | 60 | void setMessageWidgetImageSize(QSize size); 61 | QSize messageWidgetImageSize(); 62 | 63 | void setMessageWidgetContentImageWidth(float width); 64 | float messageWidgetContentImageWidth(); 65 | 66 | void setMessageWidgetContentImageHeight(float height); 67 | float messageWidgetContentImageHeight(); 68 | 69 | void setMessageFallback(bool mode); 70 | bool messageFallback(); 71 | 72 | void setRenderMarkdown(bool mode); 73 | bool renderMarkdown(); 74 | 75 | void setShowImageUrl(bool mode); 76 | bool showImageUrl(); 77 | 78 | void setForcePlainText(bool mode); 79 | bool forcePlainText(); 80 | 81 | void setPriorityColor(bool mode); 82 | bool priorityColor(); 83 | 84 | void setPriorityColorWidth(int w); 85 | int priorityColorWidth(); 86 | 87 | void setNotificationPriority(int priority); 88 | int notificationPriority(); 89 | 90 | void setNotificationDurationMs(int ms); 91 | int notificationDurationMs(); 92 | 93 | void setNotificationShowIcon(bool mode); 94 | bool notificationShowIcon(); 95 | 96 | void setNotificationClick(bool mode); 97 | bool notificationClick(); 98 | 99 | void setTrayUnreadEnabled(bool mode); 100 | bool trayUnreadEnabled(); 101 | 102 | void setPopupEnabled(bool mode); 103 | bool popupEnabled(); 104 | 105 | void setPopupHeight(int height); 106 | int popupHeight(); 107 | 108 | void setPopupWidth(int width); 109 | int popupWidth(); 110 | 111 | void setSelectedApplicationFont(QFont font); 112 | QFont selectedApplicationFont(); 113 | 114 | void setApplicationFont(QFont font); 115 | QFont applicationFont(); 116 | 117 | void setTitleFont(QFont font); 118 | QFont titleFont(); 119 | 120 | void setDateFont(QFont font); 121 | QFont dateFont(); 122 | 123 | void setMessageFont(QFont font); 124 | QFont messageFont(); 125 | 126 | void setUseLocale(bool mode); 127 | bool useLocale(); 128 | 129 | void setSortApplications(bool mode); 130 | bool sortApplications(); 131 | 132 | int heartbeatInterval(); 133 | 134 | void setSelfSignedCertificatePath(QString path); 135 | QString selfSignedCertificatePath(); 136 | 137 | void setCustomTray(bool mode); 138 | bool customTray(); 139 | 140 | void setCustomTrayUnread(bool mode); 141 | bool customTrayUnread(); 142 | 143 | void setCustomTrayError(bool mode); 144 | bool customTrayError(); 145 | 146 | void setCustomTrayPath(QString path); 147 | QString customTrayPath(); 148 | 149 | void setCustomTrayUnreadPath(QString path); 150 | QString customTrayUnreadPath(); 151 | 152 | void setCustomTrayErrorPath(QString path); 153 | QString customTrayErrorPath(); 154 | 155 | signals: 156 | void serverChanged(); 157 | void sizeChanged(); 158 | void fontChanged(); 159 | void showPriorityChanged(bool mode); 160 | void quitRequested(); 161 | 162 | }; 163 | 164 | 165 | extern Settings * settings; 166 | 167 | 168 | #endif // SETTINGS_H 169 | -------------------------------------------------------------------------------- /src/settingsdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "settingsdialog.h" 2 | #include "cache.h" 3 | #include "serverinfodialog.h" 4 | #include "settings.h" 5 | #include "ui_settingsdialog.h" 6 | #include "utils.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | SettingsDialog::SettingsDialog(QWidget *parent) : 18 | QDialog(parent), 19 | ui(new Ui::SettingsDialog), 20 | bChanged(false), 21 | bFontChanged(false), 22 | bSizeChanged(false), 23 | bShowPriorityChanged(false) 24 | { 25 | ui->setupUi(this); 26 | setWindowTitle("Preferences — " + qApp->applicationName()); 27 | 28 | ui->label_app_version->setText(QApplication::applicationVersion()); 29 | ui->label_qt_version->setText(qVersion()); 30 | ui->label_app_icon->setPixmap(QIcon(":/res/icons/gotify-tray++.ico").pixmap(22, 22)); 31 | ui->label_qt_icon->setPixmap(QIcon(":/res/icons/qt.png").pixmap(22, 16)); 32 | 33 | QString theme = Utils::getTheme(); 34 | ui->label_status->setPixmap(QPixmap(":/res/themes/" + theme + "/status_active.svg")); 35 | ui->label_delete_all->setPixmap(QPixmap(":/res/themes/" + theme + "/trashcan.svg")); 36 | ui->label_delete->setPixmap(QPixmap(":/res/themes/" + theme + "/trashcan.svg")); 37 | ui->label_refresh->setPixmap(QPixmap(":/res/themes/" + theme + "/refresh.svg")); 38 | ui->label_priority->setStyleSheet("background-color: #b3e67e22;"); 39 | 40 | cacheThread = QThread::create([this]{ui->label_cache->setText(QString::number(cache->size()/1e6, 'f', 2) + " MB");}); 41 | cacheThread->start(); 42 | 43 | #ifdef Q_OS_WIN 44 | ui->label_notification_duration->hide(); 45 | ui->label_notification_duration_ms->hide(); 46 | ui->spin_duration->hide(); 47 | #endif 48 | 49 | readSettings(); 50 | connectComponents(); 51 | 52 | ui->buttonBox->button(QDialogButtonBox::Apply)->setDisabled(true); 53 | } 54 | 55 | 56 | SettingsDialog::~SettingsDialog() 57 | { 58 | delete ui; 59 | cacheThread->deleteLater(); 60 | } 61 | 62 | 63 | void SettingsDialog::serverInfo() 64 | { 65 | QUrl url = settings->serverUrl(); 66 | QByteArray clientToken = settings->clientToken(); 67 | QString certPath = settings->selfSignedCertificatePath(); 68 | 69 | ServerInfoDialog dialog(this, url, clientToken, certPath); 70 | dialog.exec(); 71 | } 72 | 73 | 74 | void SettingsDialog::applicationIcon() 75 | { 76 | int currentSize = ui->label_application_icon1->width(); 77 | int size = QInputDialog::getInt(this, "Application Icon Size", "Size in pixels", currentSize, 10, 100); 78 | if (currentSize != size) { 79 | ui->label_application_icon1->setFixedSize(QSize(size, size)); 80 | ui->label_application_icon2->setFixedSize(QSize(size, size)); 81 | sizeChanged(); 82 | } 83 | } 84 | 85 | 86 | void SettingsDialog::messageApplicationIcon() 87 | { 88 | int currentSize = ui->label_icon->width(); 89 | int size = QInputDialog::getInt(this, "Message Application Icon Size", "Size in pixels", currentSize, 10, 100); 90 | if (currentSize != size) { 91 | ui->label_icon->setFixedSize(QSize(size, size)); 92 | sizeChanged(); 93 | } 94 | } 95 | 96 | 97 | void SettingsDialog::mainButton() 98 | { 99 | int currentSize = ui->label_refresh->width()/0.7; // TODO 100 | int size = QInputDialog::getInt(this, "Main Button Size", "Size in pixels", currentSize, 10, 100); 101 | if (currentSize != size) { 102 | ui->label_refresh->setFixedSize(0.7*QSize(size, size)); 103 | ui->label_delete_all->setFixedSize(0.9*QSize(size, size)); 104 | sizeChanged(); 105 | } 106 | } 107 | 108 | 109 | void SettingsDialog::statusLabel() 110 | { 111 | int currentSize = ui->label_status->width(); 112 | int size = QInputDialog::getInt(this, "Status Label Size", "Size in pixels", currentSize, 10, 100); 113 | if (currentSize != size) { 114 | ui->label_status->setFixedSize(QSize(size, size)); 115 | sizeChanged(); 116 | } 117 | } 118 | 119 | 120 | void SettingsDialog::messageButton() 121 | { 122 | int currentSize = ui->label_delete->width(); 123 | int size = QInputDialog::getInt(this, "Message Button Size", "Size in pixels", currentSize, 10, 100); 124 | if (currentSize != size) { 125 | ui->label_delete->setFixedSize(QSize(size, size)); 126 | sizeChanged(); 127 | } 128 | } 129 | 130 | 131 | void SettingsDialog::titleFont() 132 | { 133 | bool accepted = false; 134 | QFont font = QFontDialog::QFontDialog::getFont(&accepted, ui->label_title->font(), this, "Choose a title font"); 135 | if (accepted) { 136 | ui->label_title->setFont(font); 137 | fontChanged(); 138 | } 139 | } 140 | 141 | 142 | void SettingsDialog::dateFont() 143 | { 144 | bool accepted = false; 145 | QFont font = QFontDialog::QFontDialog::getFont(&accepted, ui->label_date->font(), this, "Choose a date font"); 146 | if (accepted) { 147 | ui->label_date->setFont(font); 148 | fontChanged(); 149 | } 150 | } 151 | 152 | 153 | void SettingsDialog::messageFont() 154 | { 155 | bool accepted = false; 156 | QFont font = QFontDialog::QFontDialog::getFont(&accepted, ui->label_message->font(), this, "Choose a message font"); 157 | if (accepted) { 158 | ui->label_message->setFont(font); 159 | fontChanged(); 160 | } 161 | } 162 | 163 | 164 | void SettingsDialog::applicationFont() 165 | { 166 | bool accepted = false; 167 | QFont font = QFontDialog::QFontDialog::getFont(&accepted, ui->label_application1->font(), this, "Choose an application font"); 168 | if (accepted) { 169 | ui->label_application1->setFont(font); 170 | ui->label_application2->setFont(font); 171 | fontChanged(); 172 | } 173 | } 174 | 175 | 176 | void SettingsDialog::selectedApplicationFont() 177 | { 178 | bool accepted = false; 179 | QFont font = QFontDialog::QFontDialog::getFont(&accepted, ui->label_selected_application->font(), this, "Choose a selected application font"); 180 | if (accepted) { 181 | ui->label_selected_application->setFont(font); 182 | fontChanged(); 183 | } 184 | } 185 | 186 | 187 | void SettingsDialog::messagePriority() 188 | { 189 | int currentWidth = ui->label_priority->width(); 190 | int width = QInputDialog::getInt(this, "Priority Color Width", "Width in pixels", currentWidth, 1, 50); 191 | if (currentWidth != width) { 192 | ui->label_priority->setFixedWidth(width); 193 | showPriorityChanged(); 194 | } 195 | } 196 | 197 | 198 | void SettingsDialog::loadSizes() 199 | { 200 | ui->label_application_icon1->setFixedSize(settings->applicationIconSize()); 201 | ui->label_application_icon2->setFixedSize(settings->applicationIconSize()); 202 | ui->label_refresh->setFixedSize(0.7*settings->mainButtonSize()); 203 | ui->label_delete_all->setFixedSize(0.9*settings->mainButtonSize()); 204 | ui->label_delete->setFixedSize(settings->messageButtonSize()); 205 | ui->label_status->setFixedSize(settings->statusLabelSize()); 206 | ui->label_icon->setFixedSize(settings->messageApplicationIconSize()); 207 | ui->label_priority->setFixedWidth(settings->priorityColorWidth()); 208 | } 209 | 210 | 211 | void SettingsDialog::loadFonts() 212 | { 213 | ui->label_title->setFont(settings->titleFont()); 214 | ui->label_date->setFont(settings->dateFont()); 215 | ui->label_message->setFont(settings->messageFont()); 216 | ui->label_application1->setFont(settings->applicationFont()); 217 | ui->label_application2->setFont(settings->applicationFont()); 218 | ui->label_selected_application->setFont(settings->selectedApplicationFont()); 219 | } 220 | 221 | 222 | void SettingsDialog::resetFontsSizes() 223 | { 224 | if (QMessageBox::warning(this, "Are you sure?", "Reset all fonts and sizes?", 225 | QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, 226 | QMessageBox::StandardButton::Cancel) 227 | == QMessageBox::StandardButton::Ok) { 228 | settings->remove("font"); 229 | settings->remove("size"); 230 | loadFonts(); 231 | loadSizes(); 232 | emit settings->fontChanged(); 233 | emit settings->sizeChanged(); 234 | emit settings->showPriorityChanged(settings->priorityColor()); 235 | } 236 | } 237 | 238 | 239 | void SettingsDialog::changed() 240 | { 241 | bChanged = true; 242 | ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); 243 | } 244 | 245 | 246 | void SettingsDialog::fontChanged() 247 | { 248 | bFontChanged = true; 249 | changed(); 250 | } 251 | 252 | 253 | void SettingsDialog::sizeChanged() 254 | { 255 | bSizeChanged = true; 256 | changed(); 257 | } 258 | 259 | 260 | void SettingsDialog::showPriorityChanged() 261 | { 262 | bShowPriorityChanged = true; 263 | changed(); 264 | } 265 | 266 | 267 | void SettingsDialog::clearCache() 268 | { 269 | cache->clear(); 270 | ui->label_cache->setText("0 MB"); 271 | } 272 | 273 | 274 | void SettingsDialog::openCache() 275 | { 276 | QDesktopServices::openUrl(QUrl::fromLocalFile(cache->getDir())); 277 | } 278 | 279 | void 280 | SettingsDialog::openCustomTray() 281 | { 282 | QString path = QFileDialog::getOpenFileName(this, "Select a custom tray icon", QDir::homePath(), "Images (*.png *.jpg);;*"); 283 | if (path.isEmpty()) 284 | return; 285 | ui->line_custom_tray_normal->setText(path); 286 | } 287 | 288 | void 289 | SettingsDialog::openCustomTrayUnread() 290 | { 291 | QString path = QFileDialog::getOpenFileName(this, "Select a custom tray icon (unread)", QDir::homePath(), "Images (*.png *.jpg);;*"); 292 | if (path.isEmpty()) 293 | return; 294 | ui->line_custom_tray_unread->setText(path); 295 | } 296 | 297 | void 298 | SettingsDialog::openCustomTrayError() 299 | { 300 | QString path = QFileDialog::getOpenFileName(this, "Select a custom tray icon (error)", QDir::homePath(), "Images (*.png *.jpg);;*"); 301 | if (path.isEmpty()) 302 | return; 303 | ui->line_custom_tray_error->setText(path); 304 | } 305 | 306 | void 307 | SettingsDialog::trayPathChanged(QString path) 308 | { 309 | QImage image(path); 310 | if (image.isNull()) 311 | return; 312 | ui->label_tray_preview_normal->setPixmap(QPixmap::fromImage(image).scaled(25, 25, Qt::KeepAspectRatio, Qt::SmoothTransformation)); 313 | changed(); 314 | } 315 | 316 | void 317 | SettingsDialog::trayPathUnreadChanged(QString path) 318 | { 319 | QImage image(path); 320 | if (image.isNull()) 321 | return; 322 | ui->label_tray_preview_unread->setPixmap(QPixmap::fromImage(image).scaled(25, 25, Qt::KeepAspectRatio, Qt::SmoothTransformation)); 323 | changed(); 324 | } 325 | 326 | void 327 | SettingsDialog::trayPathErrorChanged(QString path) 328 | { 329 | QImage image(path); 330 | if (image.isNull()) 331 | return; 332 | ui->label_tray_preview_error->setPixmap(QPixmap::fromImage(image).scaled(25, 25, Qt::KeepAspectRatio, Qt::SmoothTransformation)); 333 | changed(); 334 | } 335 | 336 | void 337 | SettingsDialog::connectComponents() 338 | { 339 | connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &SettingsDialog::saveSettings); 340 | } 341 | 342 | 343 | void SettingsDialog::readSettings() 344 | { 345 | // -------------------------- General -------------------------- 346 | ui->spin_priority->setValue(settings->notificationPriority()); 347 | ui->spin_duration->setValue(settings->notificationDurationMs()); 348 | ui->cb_notify->setChecked(settings->notifyMissed()); 349 | ui->cb_notification_click->setChecked(settings->notificationClick()); 350 | ui->cb_tray_icon_unread->setChecked(settings->trayUnreadEnabled()); 351 | ui->cb_priority_colors->setChecked(settings->priorityColor()); 352 | ui->cb_locale->setChecked(settings->useLocale()); 353 | ui->cb_sort_applications->setChecked(settings->sortApplications()); 354 | ui->cb_image_urls->setChecked(settings->showImageUrl()); 355 | 356 | // --------------------------- Fonts --------------------------- 357 | loadFonts(); 358 | loadSizes(); 359 | 360 | // --------------------------- Custom Tray Icon --------------------------- 361 | ui->cb_custom_tray_normal->setChecked(settings->customTray()); 362 | ui->cb_custom_tray_unread->setChecked(settings->customTrayUnread()); 363 | ui->cb_custom_tray_error->setChecked(settings->customTrayError()); 364 | ui->line_custom_tray_normal->setText(settings->customTrayPath()); 365 | ui->line_custom_tray_unread->setText(settings->customTrayUnreadPath()); 366 | ui->line_custom_tray_error->setText(settings->customTrayErrorPath()); 367 | ui->line_custom_tray_normal->setEnabled(settings->customTray()); 368 | ui->line_custom_tray_unread->setEnabled(settings->customTrayUnread()); 369 | ui->line_custom_tray_error->setEnabled(settings->customTrayError()); 370 | ui->pb_custom_tray_normal->setEnabled(settings->customTray()); 371 | ui->pb_custom_tray_unread->setEnabled(settings->customTrayUnread()); 372 | ui->pb_custom_tray_error->setEnabled(settings->customTrayError()); 373 | 374 | // -------------------------- Advanced ------------------------- 375 | ui->groupbox_image_popup->setChecked(settings->popupEnabled()); 376 | ui->spin_popup_w->setValue(settings->popupWidth()); 377 | ui->spin_popup_h->setValue(settings->popupHeight()); 378 | 379 | ui->spin_content_h->setValue(settings->messageWidgetContentImageHeight() * 100.0f); 380 | ui->spin_content_w->setValue(settings->messageWidgetContentImageWidth() * 100.0f); 381 | 382 | ui->cb_force_plaintext->setChecked(settings->forcePlainText()); 383 | ui->cb_markdown->setChecked(settings->renderMarkdown()); 384 | ui->cb_message_fallback->setChecked(settings->messageFallback()); 385 | } 386 | 387 | 388 | void SettingsDialog::saveSettings() 389 | { 390 | ui->buttonBox->button(QDialogButtonBox::Apply)->setDisabled(true); 391 | 392 | // -------------------------- General -------------------------- 393 | settings->setNotificationPriority(ui->spin_priority->value()); 394 | settings->setNotificationDurationMs(ui->spin_duration->value()); 395 | settings->setNotifyMissed(ui->cb_notify->isChecked()); 396 | settings->setNotificationClick(ui->cb_notification_click->isChecked()); 397 | settings->setTrayUnreadEnabled(ui->cb_tray_icon_unread->isChecked()); 398 | settings->setUseLocale(ui->cb_locale->isChecked()); 399 | settings->setSortApplications(ui->cb_sort_applications->isChecked()); 400 | settings->setShowImageUrl(ui->cb_image_urls->isChecked()); 401 | if (bShowPriorityChanged) { 402 | bool mode = ui->cb_priority_colors->isChecked(); 403 | settings->setPriorityColorWidth(ui->label_priority->width()); 404 | settings->setPriorityColor(mode); 405 | emit settings->showPriorityChanged(mode); 406 | } 407 | 408 | // --------------------------- Fonts --------------------------- 409 | if (bFontChanged) { 410 | settings->setTitleFont(ui->label_title->font()); 411 | settings->setDateFont(ui->label_date->font()); 412 | settings->setMessageFont(ui->label_message->font()); 413 | settings->setApplicationFont(ui->label_application1->font()); 414 | settings->setSelectedApplicationFont(ui->label_selected_application->font()); 415 | emit settings->fontChanged(); 416 | } 417 | 418 | // --------------------------- Custom Tray Icon --------------------------- 419 | settings->setCustomTray(ui->cb_custom_tray_normal->isChecked()); 420 | settings->setCustomTrayUnread(ui->cb_custom_tray_unread->isChecked()); 421 | settings->setCustomTrayError(ui->cb_custom_tray_error->isChecked()); 422 | settings->setCustomTrayPath(ui->line_custom_tray_normal->text()); 423 | settings->setCustomTrayUnreadPath(ui->line_custom_tray_unread->text()); 424 | settings->setCustomTrayErrorPath(ui->line_custom_tray_error->text()); 425 | 426 | // --------------------------- Sizes --------------------------- 427 | if (bSizeChanged) { 428 | settings->setApplicationIconSize(ui->label_application_icon1->size()); 429 | settings->setMessageApplicationIconSize(ui->label_icon->size()); 430 | settings->setMainButtonSize(ui->label_refresh->size() / 0.7); 431 | settings->setStatusLabelSize(ui->label_status->size()); 432 | settings->setMessageButtonSize(ui->label_delete->size()); 433 | 434 | emit settings->sizeChanged(); 435 | } 436 | 437 | // -------------------------- Advanced ------------------------- 438 | settings->setPopupEnabled(ui->groupbox_image_popup->isChecked()); 439 | settings->setPopupWidth(ui->spin_popup_w->value()); 440 | settings->setPopupHeight(ui->spin_popup_h->value()); 441 | settings->setMessageWidgetContentImageHeight(ui->spin_content_h->value() / 100.0f); 442 | settings->setMessageWidgetContentImageWidth(ui->spin_content_w->value() / 100.0f); 443 | 444 | settings->setForcePlainText(ui->cb_force_plaintext->isChecked()); 445 | settings->setRenderMarkdown(ui->cb_markdown->isChecked()); 446 | settings->setMessageFallback(ui->cb_message_fallback->isChecked()); 447 | } 448 | 449 | 450 | void SettingsDialog::resetSettings() 451 | { 452 | if (QMessageBox::warning(this, "Are you sure?", "Reset all settings?", 453 | QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, 454 | QMessageBox::StandardButton::Cancel) 455 | == QMessageBox::StandardButton::Ok) { 456 | settings->clear(); 457 | emit settings->quitRequested(); 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /src/settingsdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGSDIALOG_H 2 | #define SETTINGSDIALOG_H 3 | 4 | #include 5 | #include 6 | 7 | 8 | namespace Ui { 9 | class SettingsDialog; 10 | } 11 | 12 | class SettingsDialog : public QDialog 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit SettingsDialog(QWidget * parent = nullptr); 18 | ~SettingsDialog(); 19 | 20 | private: 21 | Ui::SettingsDialog * ui; 22 | QThread * cacheThread; 23 | 24 | bool bChanged; 25 | bool bFontChanged; 26 | bool bSizeChanged; 27 | bool bShowPriorityChanged; 28 | 29 | void readSettings(); 30 | void loadSizes(); 31 | void loadFonts(); 32 | void connectComponents(); 33 | 34 | private slots: 35 | void saveSettings(); 36 | void resetSettings(); 37 | 38 | void changed(); 39 | void fontChanged(); 40 | void sizeChanged(); 41 | void showPriorityChanged(); 42 | 43 | void serverInfo(); 44 | void resetFontsSizes(); 45 | void titleFont(); 46 | void dateFont(); 47 | void messageFont(); 48 | void applicationFont(); 49 | void selectedApplicationFont(); 50 | void applicationIcon(); 51 | void messageApplicationIcon(); 52 | void mainButton(); 53 | void statusLabel(); 54 | void messageButton(); 55 | void messagePriority(); 56 | 57 | void clearCache(); 58 | void openCache(); 59 | 60 | void openCustomTray(); 61 | void openCustomTrayUnread(); 62 | void openCustomTrayError(); 63 | 64 | void trayPathChanged(QString path); 65 | void trayPathUnreadChanged(QString path); 66 | void trayPathErrorChanged(QString path); 67 | }; 68 | 69 | #endif // SETTINGSDIALOG_H 70 | -------------------------------------------------------------------------------- /src/statuswidget.cpp: -------------------------------------------------------------------------------- 1 | #include "statuswidget.h" 2 | #include "utils.h" 3 | 4 | 5 | StatusWidget::StatusWidget(QWidget * parent) : QLabel(parent) 6 | { 7 | setFixedSize(QSize(20, 20)); 8 | setScaledContents(true); 9 | image = ""; 10 | } 11 | 12 | 13 | void 14 | StatusWidget::setStatus(QString image) 15 | { 16 | this->image = image; 17 | QString resource = ":/res/themes/" + Utils::getTheme() + "/" + image; 18 | setPixmap(QPixmap(resource)); 19 | } 20 | 21 | 22 | void 23 | StatusWidget::setActive() 24 | { 25 | setToolTip("Listening for new messages"); 26 | setStatus("status_active.svg"); 27 | } 28 | 29 | 30 | void 31 | StatusWidget::setConnecting() 32 | { 33 | setToolTip("Connecting..."); 34 | setStatus("status_connecting.svg"); 35 | } 36 | 37 | 38 | void 39 | StatusWidget::setInactive() 40 | { 41 | setToolTip("Listener inactive."); 42 | setStatus("status_inactive.svg"); 43 | } 44 | 45 | 46 | void 47 | StatusWidget::setError() 48 | { 49 | setToolTip("Listener error"); 50 | setStatus("status_error.svg"); 51 | } 52 | 53 | 54 | void 55 | StatusWidget::refresh() 56 | { 57 | if (image != "") { 58 | setStatus(image); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/statuswidget.h: -------------------------------------------------------------------------------- 1 | #ifndef STATUSWIDGET_H 2 | #define STATUSWIDGET_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | class StatusWidget : public QLabel 12 | { 13 | public: 14 | StatusWidget(QWidget * parent = nullptr); 15 | void setActive(); 16 | void setConnecting(); 17 | void setInactive(); 18 | void setError(); 19 | void refresh(); 20 | 21 | private: 22 | QString image; 23 | void setStatus(QString image); 24 | }; 25 | 26 | 27 | #endif // STATUSWIDGET_H 28 | -------------------------------------------------------------------------------- /src/tray.cpp: -------------------------------------------------------------------------------- 1 | #include "tray.h" 2 | #include "settings.h" 3 | 4 | #include 5 | #include 6 | 7 | 8 | Tray::Tray(QObject * parent) : QSystemTrayIcon(parent) 9 | { 10 | if (!isSystemTrayAvailable()) { 11 | qFatal() << "System tray is not available."; 12 | } 13 | 14 | if (!supportsMessages()) { 15 | qFatal() << "System does not support notifications."; 16 | } 17 | 18 | setToolTip(qApp->applicationName()); 19 | setIcon(QIcon(":/res/icons/tray.png")); 20 | 21 | actionShowWindow = new QAction("Show Window"); 22 | actionSettings = new QAction("Settings"); 23 | actionReconnect = new QAction("Reconnect"); 24 | actionQuit = new QAction("Quit"); 25 | 26 | menu.addAction(actionShowWindow); 27 | menu.addSeparator(); 28 | menu.addAction(actionSettings); 29 | menu.addSeparator(); 30 | menu.addAction(actionReconnect); 31 | menu.addSeparator(); 32 | menu.addAction(actionQuit); 33 | 34 | setContextMenu(&menu); 35 | } 36 | 37 | 38 | Tray::~Tray() 39 | { 40 | delete actionShowWindow; 41 | delete actionSettings; 42 | delete actionReconnect; 43 | delete actionQuit; 44 | } 45 | 46 | void 47 | Tray::setActive() 48 | { 49 | iconError = false; 50 | 51 | QString path = ":/res/icons/tray.png"; 52 | if (settings->customTray() && QFile(settings->customTrayPath()).exists()) 53 | path = settings->customTrayPath(); 54 | setIcon(QIcon(path)); 55 | } 56 | 57 | void 58 | Tray::setError() 59 | { 60 | iconError = true; 61 | 62 | QString path = ":/res/icons/tray-error.png"; 63 | if (settings->customTrayError() && QFile(settings->customTrayErrorPath()).exists()) 64 | path = settings->customTrayErrorPath(); 65 | setIcon(QIcon(path)); 66 | } 67 | 68 | void 69 | Tray::setUnread() 70 | { 71 | QString path = ":/res/icons/tray-unread.png"; 72 | if (settings->customTrayUnread() && QFile(settings->customTrayUnreadPath()).exists()) 73 | path = settings->customTrayUnreadPath(); 74 | setIcon(QIcon(path)); 75 | } 76 | 77 | void 78 | Tray::revertIcon() 79 | { 80 | if (iconError) { 81 | setError(); 82 | } else { 83 | setActive(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/tray.h: -------------------------------------------------------------------------------- 1 | #ifndef TRAY_H 2 | #define TRAY_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | class Tray : public QSystemTrayIcon 13 | { 14 | public: 15 | Tray(QObject * parent = nullptr); 16 | ~Tray(); 17 | 18 | void setActive(); 19 | void setError(); 20 | void setUnread(); 21 | void revertIcon(); 22 | 23 | QAction * actionShowWindow; 24 | QAction * actionSettings; 25 | QAction * actionReconnect; 26 | QAction * actionQuit; 27 | private: 28 | QMenu menu; 29 | bool iconError = false; 30 | }; 31 | 32 | 33 | #endif // TRAY_H 34 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #ifdef USE_KDE 16 | #include 17 | #endif 18 | 19 | namespace Utils { 20 | 21 | QString 22 | getTheme() 23 | { 24 | switch (qApp->styleHints()->colorScheme()) { 25 | case Qt::ColorScheme::Dark: 26 | return QStringLiteral("dark"); 27 | case Qt::ColorScheme::Light: 28 | return QStringLiteral("light"); 29 | default: 30 | // fallback to custom method 31 | return (qApp->palette().color(QPalette::Active, QPalette::Base).lightness() < 127) ? QStringLiteral("dark") : QStringLiteral("light"); 32 | } 33 | } 34 | 35 | void 36 | updateWidgetProperty(QWidget* widget, const char* name, const QVariant& value) 37 | { 38 | widget->setProperty(name, value); 39 | QStyle* style = widget->style(); 40 | style->unpolish(widget); 41 | style->polish(widget); 42 | widget->update(); 43 | } 44 | 45 | QString 46 | readFile(QString fileName) 47 | { 48 | QFile file; 49 | file.setFileName(fileName); 50 | if (!file.open(QFile::ReadOnly)) { 51 | qWarning() << "Could not open " << fileName; 52 | return QString(); 53 | } 54 | QString text = file.readAll(); 55 | file.close(); 56 | return text; 57 | } 58 | 59 | bool 60 | writeFile(QString fileName, QByteArray data) 61 | { 62 | QFile file(fileName); 63 | if (file.open(QFile::WriteOnly)) { 64 | file.write(data); 65 | file.close(); 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | QString 72 | extractImage(QString text) 73 | { 74 | text = text.trimmed(); 75 | 76 | // Check for markdown image format: ![alt text](image_url) 77 | QRegularExpression reMD(R"(!\[\]\((https?://[^\s\)]+)\))"); 78 | QRegularExpressionMatch matchMD = reMD.match(text); 79 | if (matchMD.hasMatch()) { 80 | return matchMD.captured(1).trimmed(); // Return the image URL 81 | } 82 | 83 | // Check for plain image URL format 84 | static QRegularExpression rePlain(R"(\s*(https?://[^\s]+\.(jpg|jpeg|png|gif|svg|webp|ico|tiff|bmp))\s*)"); 85 | QRegularExpressionMatch matchPlain = rePlain.match(text); 86 | if (matchPlain.hasMatch()) { 87 | return matchPlain.captured(0).trimmed(); // Return the image URL 88 | } 89 | 90 | return QString(); 91 | } 92 | 93 | 94 | QString replaceLinks(QString text) 95 | { 96 | static QRegularExpression re("(https?)(://[^\\s)]+)(?=\\)?)"); 97 | return text.replace(re, "\\1\\2"); 98 | } 99 | 100 | 101 | bool containsHtml(QString text) 102 | { 103 | static QRegularExpression regex(R"(<([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>.*<\/\1>)"); 104 | QRegularExpressionMatch matchHTML = regex.match(text); 105 | return matchHTML.hasMatch(); 106 | } 107 | 108 | bool 109 | violatesWidth(const QString& text, const QFont& font, int allowedWidth) 110 | { 111 | QFontMetrics metrics(font); 112 | // Characters which will allow wrapping in QLabel 113 | QRegularExpression pattern("[ \\n,/]"); 114 | 115 | // Split the text using the pattern 116 | QStringList words = text.split(pattern, Qt::SkipEmptyParts); 117 | 118 | // Return true if any word is found which doesn't autowrap 119 | // https://doc.qt.io/qt-6/qfontmetrics.html#horizontalAdvance-1 120 | for (const QString& word : words) 121 | if (metrics.horizontalAdvance(word) > allowedWidth) 122 | return true; 123 | 124 | return false; 125 | } 126 | 127 | QString 128 | getUuid() 129 | { 130 | return QUuid::createUuid().toString(QUuid::Id128); 131 | } 132 | 133 | bool 134 | isImage(const QString& fileName) 135 | { 136 | QFileInfo fi(fileName); 137 | QString ext = fi.suffix(); 138 | static QStringList imageExts = { "jpg", "jpeg", "png", "gif", "svg", "webp", "ico", "tiff", "bmp" }; 139 | return imageExts.contains(ext); 140 | } 141 | 142 | qint64 143 | dirSize(const QString& dirName) 144 | { 145 | QDirIterator it(dirName, QDirIterator::Subdirectories); 146 | qint64 size = 0; 147 | while (it.hasNext()) { 148 | it.next(); 149 | size += it.fileInfo().size(); 150 | } 151 | return size; 152 | } 153 | 154 | QList 155 | getSelfSignedExpectedErrors(QString certPath) 156 | { 157 | QList cert = QSslCertificate::fromPath(certPath); 158 | if (!cert.size()) { 159 | qDebug() << "Self-signed server certificate not found:" << certPath; 160 | return QList{}; 161 | } 162 | 163 | QSslError error(QSslError::SelfSignedCertificate, cert.at(0)); 164 | QList expectedSslErrors; 165 | expectedSslErrors.append(error); 166 | return expectedSslErrors; 167 | } 168 | 169 | #ifdef USE_KDE 170 | KNotification::Urgency 171 | priorityToUrgency(int priority) 172 | { 173 | switch (priority) { 174 | case 0 ... 3: 175 | return KNotification::Urgency::LowUrgency; 176 | case 4 ... 6: 177 | return KNotification::Urgency::NormalUrgency; 178 | case 7 ... 9: 179 | return KNotification::Urgency::HighUrgency; 180 | case 10: 181 | return KNotification::Urgency::CriticalUrgency; 182 | default: 183 | return KNotification::Urgency::NormalUrgency; 184 | } 185 | } 186 | #endif // USE_KDE 187 | 188 | } 189 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #include 5 | #include 6 | 7 | #ifdef USE_KDE 8 | #include 9 | #endif 10 | 11 | namespace Utils { 12 | 13 | QString 14 | getTheme(); 15 | 16 | void 17 | updateWidgetProperty(QWidget* widget, const char* name, const QVariant& value); 18 | 19 | QString 20 | readFile(QString fileName); 21 | 22 | bool 23 | writeFile(QString fileName, QByteArray data); 24 | 25 | QString 26 | extractImage(QString text); 27 | 28 | QString 29 | replaceLinks(QString text); 30 | 31 | bool 32 | containsHtml(QString text); 33 | 34 | bool 35 | violatesWidth(const QString& text, const QFont& font, int allowedWidth); 36 | 37 | QString 38 | getUuid(); 39 | 40 | bool 41 | isImage(const QString& fileName); 42 | 43 | qint64 44 | dirSize(const QString& dirName); 45 | 46 | QList 47 | getSelfSignedExpectedErrors(QString certPath); 48 | 49 | #ifdef USE_KDE 50 | KNotification::Urgency 51 | priorityToUrgency(int priority); 52 | #endif 53 | 54 | } 55 | 56 | #endif // UTILS_H 57 | --------------------------------------------------------------------------------