├── .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 |  | 
40 |
41 |
42 | ### Settings window
43 |
44 | Light | Dark
45 | :-------------------------------------------------:|:---------------------------------------------------------:
46 |  | 
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 |
48 |
--------------------------------------------------------------------------------
/res/themes/dark/status_active.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/res/themes/dark/status_connecting.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/res/themes/dark/status_error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/res/themes/dark/status_inactive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
46 |
--------------------------------------------------------------------------------
/res/themes/light/status_active.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/res/themes/light/status_connecting.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/res/themes/light/status_error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/res/themes/light/status_inactive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
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 |
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: 
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 |
--------------------------------------------------------------------------------