Compare commits
14 Commits
7a0bb14df4
...
v0.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 806d701535 | |||
| 3644001dbc | |||
| f4fd1b1b07 | |||
| a150958960 | |||
| 819d349bae | |||
| 0ae6c3f593 | |||
| 945e3212cf | |||
| 3547207eca | |||
| 6669e6722a | |||
| 7524745b18 | |||
| a9c5139d55 | |||
| 7cc6056e7e | |||
| 7516eb4444 | |||
| ef2403b3cd |
95
.drone.yml
Normal file
95
.drone.yml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build-deb-amd64
|
||||||
|
image: cache.coadcorp.com/library/buildpack-deps:jammy
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
commands:
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y --no-install-recommends build-essential qt5-qmake qtbase5-dev qtbase5-dev-tools libexif-dev qt5-image-formats-plugins libmosquitto-dev dpkg-dev fakeroot ca-certificates
|
||||||
|
- ARCH=amd64 BUILD_DIR=build-amd64 bash sbin/build_deb.sh "${DRONE_TAG:-${DRONE_COMMIT_SHA:0:8}}"
|
||||||
|
- ls -la dist
|
||||||
|
|
||||||
|
- name: build-deb-armhf
|
||||||
|
image: cache.coadcorp.com/library/buildpack-deps:bullseye
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
ARM_CFLAGS: -march=armv6 -mfpu=vfp -mfloat-abi=hard -marm
|
||||||
|
commands:
|
||||||
|
- dpkg --add-architecture armhf
|
||||||
|
- |
|
||||||
|
cat > /etc/apt/sources.list <<'EOF'
|
||||||
|
deb [arch=amd64] http://deb.debian.org/debian bullseye main contrib non-free
|
||||||
|
deb [arch=amd64] http://deb.debian.org/debian bullseye-updates main contrib non-free
|
||||||
|
deb [arch=amd64] http://security.debian.org/debian-security bullseye-security main contrib non-free
|
||||||
|
deb [arch=armhf] http://deb.debian.org/debian bullseye main contrib non-free
|
||||||
|
deb [arch=armhf] http://deb.debian.org/debian bullseye-updates main contrib non-free
|
||||||
|
deb [arch=armhf] http://security.debian.org/debian-security bullseye-security main contrib non-free
|
||||||
|
EOF
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y --no-install-recommends build-essential qt5-qmake qtbase5-dev qtbase5-dev-tools libexif-dev qt5-image-formats-plugins libmosquitto-dev dpkg-dev fakeroot ca-certificates gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf qtbase5-dev:armhf libqt5core5a:armhf libqt5gui5:armhf libqt5widgets5:armhf libqt5network5:armhf libexif-dev:armhf libmosquitto-dev:armhf qt5-image-formats-plugins:armhf
|
||||||
|
- ARCH=armhf BUILD_DIR=build-armhf QMAKESPEC=$PWD/mkspecs/linux-armhf-g++ QMAKE_QTCONF=$PWD/mkspecs/qt-armhf.conf QMAKE_CFLAGS="$ARM_CFLAGS" QMAKE_CXXFLAGS="$ARM_CFLAGS" bash sbin/build_deb.sh "${DRONE_TAG:-${DRONE_COMMIT_SHA:0:8}}"
|
||||||
|
- ls -la dist
|
||||||
|
|
||||||
|
- name: build-deb-arm64
|
||||||
|
image: cache.coadcorp.com/library/buildpack-deps:bullseye
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
ARM64_CFLAGS: -march=armv8-a
|
||||||
|
commands:
|
||||||
|
- dpkg --add-architecture arm64
|
||||||
|
- |
|
||||||
|
cat > /etc/apt/sources.list <<'EOF'
|
||||||
|
deb [arch=amd64] http://deb.debian.org/debian bullseye main contrib non-free
|
||||||
|
deb [arch=amd64] http://deb.debian.org/debian bullseye-updates main contrib non-free
|
||||||
|
deb [arch=amd64] http://security.debian.org/debian-security bullseye-security main contrib non-free
|
||||||
|
deb [arch=arm64] http://deb.debian.org/debian bullseye main contrib non-free
|
||||||
|
deb [arch=arm64] http://deb.debian.org/debian bullseye-updates main contrib non-free
|
||||||
|
deb [arch=arm64] http://security.debian.org/debian-security bullseye-security main contrib non-free
|
||||||
|
EOF
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y --no-install-recommends build-essential qt5-qmake qtbase5-dev qtbase5-dev-tools libexif-dev qt5-image-formats-plugins libmosquitto-dev dpkg-dev fakeroot ca-certificates gcc-aarch64-linux-gnu g++-aarch64-linux-gnu qtbase5-dev:arm64 libqt5core5a:arm64 libqt5gui5:arm64 libqt5widgets5:arm64 libqt5network5:arm64 libexif-dev:arm64 libmosquitto-dev:arm64 qt5-image-formats-plugins:arm64
|
||||||
|
- ARCH=arm64 BUILD_DIR=build-arm64 QMAKESPEC=$PWD/mkspecs/linux-arm64-g++ QMAKE_QTCONF=$PWD/mkspecs/qt-arm64.conf QMAKE_CFLAGS="$ARM64_CFLAGS" QMAKE_CXXFLAGS="$ARM64_CFLAGS" bash sbin/build_deb.sh "${DRONE_TAG:-${DRONE_COMMIT_SHA:0:8}}"
|
||||||
|
- ls -la dist
|
||||||
|
|
||||||
|
- name: build-deps-image
|
||||||
|
image: cache.coadcorp.com/plugins/docker
|
||||||
|
settings:
|
||||||
|
registry: registry.coadcorp.com
|
||||||
|
repo: slide/build-deps
|
||||||
|
dockerfile: Dockerfile.builddeps
|
||||||
|
username:
|
||||||
|
from_secret: REGISTRY_USER
|
||||||
|
password:
|
||||||
|
from_secret: REGISTRY_PASS
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- ${DRONE_COMMIT_SHA}
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- promote
|
||||||
|
target:
|
||||||
|
- build-deps
|
||||||
|
|
||||||
|
- name: gitea-release
|
||||||
|
image: cache.coadcorp.com/plugins/gitea-release
|
||||||
|
depends_on:
|
||||||
|
- build-deb-amd64
|
||||||
|
- build-deb-armhf
|
||||||
|
- build-deb-arm64
|
||||||
|
settings:
|
||||||
|
api_key:
|
||||||
|
from_secret: GITEA_TOKEN
|
||||||
|
base_url: https://git.coadcorp.com
|
||||||
|
files:
|
||||||
|
- dist/*.deb
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
title: ${DRONE_TAG}
|
||||||
|
note: Automated release for ${DRONE_TAG}
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
17
Dockerfile.builddeps
Normal file
17
Dockerfile.builddeps
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM cache.coadcorp.com/library/ubuntu:22.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
qt5-qmake \
|
||||||
|
qtbase5-dev \
|
||||||
|
qtbase5-dev-tools \
|
||||||
|
libexif-dev \
|
||||||
|
qt5-image-formats-plugins \
|
||||||
|
libmosquitto-dev \
|
||||||
|
dpkg-dev \
|
||||||
|
fakeroot \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
21
INSTALL.md
21
INSTALL.md
@@ -6,6 +6,8 @@ This project depends on the following dynamically linked libraries:
|
|||||||
|
|
||||||
* qt5
|
* qt5
|
||||||
* libexif
|
* libexif
|
||||||
|
* qt5-image-formats-plugins
|
||||||
|
* libmosquitto1
|
||||||
|
|
||||||
### OSX
|
### OSX
|
||||||
|
|
||||||
@@ -13,28 +15,29 @@ This project depends on the following dynamically linked libraries:
|
|||||||
brew install qt libexif
|
brew install qt libexif
|
||||||
```
|
```
|
||||||
|
|
||||||
### Raspbian Stretch
|
### Debian / Ubuntu / Raspberry Pi OS (runtime dependencies)
|
||||||
|
|
||||||
```
|
```
|
||||||
brew install qt5 libexif12
|
sudo apt-get install -y qt5-image-formats-plugins libmosquitto1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Extract binaries
|
### Raspberry Pi Zero / Raspbian (additional image format support)
|
||||||
|
|
||||||
```
|
```
|
||||||
tar xf slide_<arch>_<version>.tar.gz
|
sudo apt-get install -y qt5-image-formats-plugins libmosquitto1 libmng1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Move binary to executable folder
|
## Install .deb package
|
||||||
|
|
||||||
### OSX
|
Use apt so dependencies are resolved automatically:
|
||||||
|
|
||||||
```
|
```
|
||||||
mv slide_<version>/slide.app/Contents/MacOS/slide /usr/local/bin/
|
sudo apt-get install ./slide_<version>_<arch>.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
### Linux
|
If you must use dpkg, install runtime dependencies first:
|
||||||
|
|
||||||
```
|
```
|
||||||
mv slide_<version>/slide /usr/bin/
|
sudo apt-get install -y qt5-image-formats-plugins libmosquitto1
|
||||||
|
sudo dpkg -i slide_<version>_<arch>.deb
|
||||||
```
|
```
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -7,7 +7,7 @@ all: build
|
|||||||
|
|
||||||
.PHONY: install-deps-deb
|
.PHONY: install-deps-deb
|
||||||
install-deps-deb:
|
install-deps-deb:
|
||||||
apt install qt5-qmake libexif12 qt5-default libexif-dev qt5-image-formats-plugins
|
apt install qt5-qmake libexif12 qt5-default libexif-dev qt5-image-formats-plugins libmosquitto-dev
|
||||||
|
|
||||||
check-deps-deb:
|
check-deps-deb:
|
||||||
dpkg -l | grep qt5-qmake
|
dpkg -l | grep qt5-qmake
|
||||||
@@ -15,6 +15,7 @@ check-deps-deb:
|
|||||||
dpkg -l | grep libexif-dev
|
dpkg -l | grep libexif-dev
|
||||||
dpkg -l | grep qt5-default
|
dpkg -l | grep qt5-default
|
||||||
dpkg -l | grep qt5-image-formats-plugins
|
dpkg -l | grep qt5-image-formats-plugins
|
||||||
|
dpkg -l | grep libmosquitto-dev
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -117,6 +117,7 @@ Supported keys and values in the JSON configuration are:
|
|||||||
* `opacity` : the same as the command line `-o` argument
|
* `opacity` : the same as the command line `-o` argument
|
||||||
* `blur` : the same as the command line `-b` argument
|
* `blur` : the same as the command line `-b` argument
|
||||||
* `debug` : set to true to enable verbose output from the program
|
* `debug` : set to true to enable verbose output from the program
|
||||||
|
* `mqtt` : MQTT playback control (see below)
|
||||||
* `immich` : connect to an Immich server instead of a local path (see below)
|
* `immich` : connect to an Immich server instead of a local path (see below)
|
||||||
* `scheduler` : this entry is an array of possible path values and associated settings. This key lets you manage display times/settings for a collection of paths. In the example above the top entry shows ONLY files from a Redit feed between 2 and 4pm, ONLY files from the `show_peak_times` folder from 8am to 10am and then 4pm to 7pm. At all other times it alternates displaying files in the `always_show_1` and `always_show_2` folder.
|
* `scheduler` : this entry is an array of possible path values and associated settings. This key lets you manage display times/settings for a collection of paths. In the example above the top entry shows ONLY files from a Redit feed between 2 and 4pm, ONLY files from the `show_peak_times` folder from 8am to 10am and then 4pm to 7pm. At all other times it alternates displaying files in the `always_show_1` and `always_show_2` folder.
|
||||||
* `exclusive` : When set to `true` only this entry will be used when it is in its valid time window.
|
* `exclusive` : When set to `true` only this entry will be used when it is in its valid time window.
|
||||||
@@ -124,9 +125,47 @@ Supported keys and values in the JSON configuration are:
|
|||||||
* `path` : the path to image files
|
* `path` : the path to image files
|
||||||
* `stretch` : as above
|
* `stretch` : as above
|
||||||
|
|
||||||
|
### MQTT control
|
||||||
|
|
||||||
|
Add an `mqtt` block to control playback remotely. Publish one of the commands below to the configured topic.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"mqtt": {
|
||||||
|
"host": "mqtt.local",
|
||||||
|
"port": 1883,
|
||||||
|
"topic": "slide/control",
|
||||||
|
"immichTopic": "slide/immich",
|
||||||
|
"clientId": "slide-frame",
|
||||||
|
"username": "slide",
|
||||||
|
"password": "secret",
|
||||||
|
"keepAlive": 30,
|
||||||
|
"qos": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
* `play` / `resume` — resume slideshow
|
||||||
|
* `pause` — pause slideshow
|
||||||
|
* `next` / `next-image` — advance to next image
|
||||||
|
* `next-folder` — jump to next configured path (if multiple paths are configured)
|
||||||
|
* `restart` / `reset` — recreate the selector and restart playback
|
||||||
|
If `immichTopic` is not set, it defaults to `<topic>/immich`.
|
||||||
|
|
||||||
|
Immich control topic (`immichTopic`):
|
||||||
|
* `album:<id>` or `albumIds:id1,id2` — filter to one or more album IDs
|
||||||
|
* `person:<id>` or `personIds:id1,id2` — filter to one or more person IDs
|
||||||
|
* `reset` / `clear` — clear album/person filters
|
||||||
|
* JSON payloads are also accepted, for example:
|
||||||
|
```
|
||||||
|
{"albumIds":["..."],"personIds":["..."],"order":"desc","size":"fullsize"}
|
||||||
|
```
|
||||||
|
|
||||||
### Immich configuration (lightweight + low power)
|
### Immich configuration (lightweight + low power)
|
||||||
|
|
||||||
Immich uses an API key and a `/api` base path. This integration requests the asset search endpoint and downloads thumbnail images into a local cache before displaying them. That keeps bandwidth and power usage low while still letting `slide` do its normal scaling and transitions.
|
Immich uses an API key and a `/api` base path. This integration requests the asset search endpoint and downloads the configured image size into a local cache before displaying them. That keeps bandwidth and power usage low while still letting `slide` do its normal scaling and transitions.
|
||||||
|
|
||||||
Example (single source):
|
Example (single source):
|
||||||
```
|
```
|
||||||
@@ -167,11 +206,12 @@ Immich settings:
|
|||||||
* `url`: base Immich server URL (the integration appends `/api` automatically if missing).
|
* `url`: base Immich server URL (the integration appends `/api` automatically if missing).
|
||||||
* `apiKey`: Immich API key (needs `asset.view`, and `asset.download` if `size` is `original`).
|
* `apiKey`: Immich API key (needs `asset.view`, and `asset.download` if `size` is `original`).
|
||||||
* `albumId` or `albumIds`: optional album filters.
|
* `albumId` or `albumIds`: optional album filters.
|
||||||
|
* `personId` or `personIds`: optional person filters.
|
||||||
* `size`: `"fullsize"`, `"preview"`, `"thumbnail"`, or `"original"` (original uses the download endpoint).
|
* `size`: `"fullsize"`, `"preview"`, `"thumbnail"`, or `"original"` (original uses the download endpoint).
|
||||||
* `order`: `"asc"` or `"desc"` ordering for asset search.
|
* `order`: `"asc"` or `"desc"` ordering for asset search.
|
||||||
* `pageSize`: assets fetched per page.
|
* `pageSize`: assets fetched per page.
|
||||||
* `maxAssets`: cap on total assets fetched (0 means no cap).
|
* `maxAssets`: cap on total assets fetched (0 means no cap).
|
||||||
* `cachePath`: local cache directory for downloaded thumbnails.
|
* `cachePath`: local cache directory for downloaded images.
|
||||||
* `cacheMaxMB`: maximum cache size in MB (0 disables cleanup).
|
* `cacheMaxMB`: maximum cache size in MB (0 disables cleanup).
|
||||||
* `includeArchived`: include archived assets in search results.
|
* `includeArchived`: include archived assets in search results.
|
||||||
When `immich` is set on an entry, `path` and `imageList` are ignored.
|
When `immich` is set on an entry, `path` and `imageList` are ignored.
|
||||||
@@ -205,6 +245,7 @@ See the `Configuration File` section for details of each setting.
|
|||||||
* qt5
|
* qt5
|
||||||
* qt5-image-formats-plugins
|
* qt5-image-formats-plugins
|
||||||
* libexif
|
* libexif
|
||||||
|
* libmosquitto-dev
|
||||||
|
|
||||||
Ubuntu/Raspbian:
|
Ubuntu/Raspbian:
|
||||||
|
|
||||||
|
|||||||
21
mkspecs/linux-arm64-g++/qmake.conf
Normal file
21
mkspecs/linux-arm64-g++/qmake.conf
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
exists(/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf) {
|
||||||
|
include(/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf)
|
||||||
|
} else: exists(/usr/share/qt5/mkspecs/linux-g++/qmake.conf) {
|
||||||
|
include(/usr/share/qt5/mkspecs/linux-g++/qmake.conf)
|
||||||
|
} else: exists($$[QT_HOST_DATA]/mkspecs/linux-g++/qmake.conf) {
|
||||||
|
include($$[QT_HOST_DATA]/mkspecs/linux-g++/qmake.conf)
|
||||||
|
} else {
|
||||||
|
include($$[QT_INSTALL_DATA]/mkspecs/linux-g++/qmake.conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
QMAKE_CC = aarch64-linux-gnu-gcc
|
||||||
|
QMAKE_CXX = aarch64-linux-gnu-g++
|
||||||
|
QMAKE_LINK = aarch64-linux-gnu-g++
|
||||||
|
QMAKE_AR = aarch64-linux-gnu-ar cqs
|
||||||
|
QMAKE_STRIP = aarch64-linux-gnu-strip
|
||||||
|
|
||||||
|
QMAKE_INCDIR_QT = /usr/include/aarch64-linux-gnu/qt5
|
||||||
|
QMAKE_LIBDIR_QT = /usr/lib/aarch64-linux-gnu
|
||||||
|
QMAKE_INCDIR += /usr/include/aarch64-linux-gnu/qt5
|
||||||
|
QMAKE_LIBDIR += /usr/lib/aarch64-linux-gnu
|
||||||
|
QMAKE_LFLAGS += -Wl,-rpath-link,/usr/lib/aarch64-linux-gnu
|
||||||
1
mkspecs/linux-arm64-g++/qplatformdefs.h
Normal file
1
mkspecs/linux-arm64-g++/qplatformdefs.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qplatformdefs.h"
|
||||||
21
mkspecs/linux-armhf-g++/qmake.conf
Normal file
21
mkspecs/linux-armhf-g++/qmake.conf
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
exists(/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf) {
|
||||||
|
include(/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf)
|
||||||
|
} else: exists(/usr/share/qt5/mkspecs/linux-g++/qmake.conf) {
|
||||||
|
include(/usr/share/qt5/mkspecs/linux-g++/qmake.conf)
|
||||||
|
} else: exists($$[QT_HOST_DATA]/mkspecs/linux-g++/qmake.conf) {
|
||||||
|
include($$[QT_HOST_DATA]/mkspecs/linux-g++/qmake.conf)
|
||||||
|
} else {
|
||||||
|
include($$[QT_INSTALL_DATA]/mkspecs/linux-g++/qmake.conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
QMAKE_CC = arm-linux-gnueabihf-gcc
|
||||||
|
QMAKE_CXX = arm-linux-gnueabihf-g++
|
||||||
|
QMAKE_LINK = arm-linux-gnueabihf-g++
|
||||||
|
QMAKE_AR = arm-linux-gnueabihf-ar cqs
|
||||||
|
QMAKE_STRIP = arm-linux-gnueabihf-strip
|
||||||
|
|
||||||
|
QMAKE_INCDIR_QT = /usr/include/arm-linux-gnueabihf/qt5
|
||||||
|
QMAKE_LIBDIR_QT = /usr/lib/arm-linux-gnueabihf
|
||||||
|
QMAKE_INCDIR += /usr/include/arm-linux-gnueabihf/qt5
|
||||||
|
QMAKE_LIBDIR += /usr/lib/arm-linux-gnueabihf
|
||||||
|
QMAKE_LFLAGS += -Wl,-rpath-link,/usr/lib/arm-linux-gnueabihf
|
||||||
1
mkspecs/linux-armhf-g++/qplatformdefs.h
Normal file
1
mkspecs/linux-armhf-g++/qplatformdefs.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qplatformdefs.h"
|
||||||
11
mkspecs/qt-arm64.conf
Normal file
11
mkspecs/qt-arm64.conf
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Paths]
|
||||||
|
Prefix=/usr
|
||||||
|
Headers=/usr/include/aarch64-linux-gnu/qt5
|
||||||
|
Libraries=/usr/lib/aarch64-linux-gnu
|
||||||
|
ArchData=/usr/lib/aarch64-linux-gnu/qt5
|
||||||
|
Data=/usr/lib/aarch64-linux-gnu/qt5
|
||||||
|
Plugins=/usr/lib/aarch64-linux-gnu/qt5/plugins
|
||||||
|
HostPrefix=/usr
|
||||||
|
HostData=/usr/lib/x86_64-linux-gnu/qt5
|
||||||
|
HostBinaries=/usr/lib/qt5/bin
|
||||||
|
HostLibraries=/usr/lib/x86_64-linux-gnu
|
||||||
11
mkspecs/qt-armhf.conf
Normal file
11
mkspecs/qt-armhf.conf
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Paths]
|
||||||
|
Prefix=/usr
|
||||||
|
Headers=/usr/include/arm-linux-gnueabihf/qt5
|
||||||
|
Libraries=/usr/lib/arm-linux-gnueabihf
|
||||||
|
ArchData=/usr/lib/arm-linux-gnueabihf/qt5
|
||||||
|
Data=/usr/lib/arm-linux-gnueabihf/qt5
|
||||||
|
Plugins=/usr/lib/arm-linux-gnueabihf/qt5/plugins
|
||||||
|
HostPrefix=/usr
|
||||||
|
HostData=/usr/lib/x86_64-linux-gnu/qt5
|
||||||
|
HostBinaries=/usr/lib/qt5/bin
|
||||||
|
HostLibraries=/usr/lib/x86_64-linux-gnu
|
||||||
@@ -5,15 +5,49 @@ set -euo pipefail
|
|||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
VERSION="${1:-${VERSION:-0.0.0}}"
|
VERSION="${1:-${VERSION:-0.0.0}}"
|
||||||
VERSION="${VERSION#v}"
|
VERSION="${VERSION#v}"
|
||||||
|
# Debian versions must start with a digit; fall back to 0.0.0+<sha/tag>.
|
||||||
|
if [[ ! "$VERSION" =~ ^[0-9] ]]; then
|
||||||
|
VERSION="0.0.0+${VERSION}"
|
||||||
|
fi
|
||||||
ARCH="${ARCH:-$(dpkg --print-architecture)}"
|
ARCH="${ARCH:-$(dpkg --print-architecture)}"
|
||||||
|
BUILD_DIR="${BUILD_DIR:-$ROOT_DIR/build-$ARCH}"
|
||||||
PACKAGE_NAME="slide"
|
|
||||||
BUILD_DIR="$ROOT_DIR/build"
|
|
||||||
DIST_DIR="$ROOT_DIR/dist"
|
DIST_DIR="$ROOT_DIR/dist"
|
||||||
STAGE_DIR="$BUILD_DIR/deb"
|
STAGE_DIR="$BUILD_DIR/deb"
|
||||||
|
QMAKE_BIN="${QMAKE_BIN:-qmake}"
|
||||||
|
MAKE_JOBS="${MAKE_JOBS:-2}"
|
||||||
|
|
||||||
|
QMAKE_ARGS=()
|
||||||
|
if [[ -n "${QMAKESPEC:-}" ]]; then
|
||||||
|
QMAKE_ARGS+=("-spec" "$QMAKESPEC")
|
||||||
|
fi
|
||||||
|
if [[ -n "${QMAKE_QTCONF:-}" ]]; then
|
||||||
|
QMAKE_ARGS+=("-qtconf" "$QMAKE_QTCONF")
|
||||||
|
fi
|
||||||
|
if [[ -n "${QMAKE_CC:-}" ]]; then
|
||||||
|
QMAKE_ARGS+=("QMAKE_CC=$QMAKE_CC")
|
||||||
|
fi
|
||||||
|
if [[ -n "${QMAKE_CXX:-}" ]]; then
|
||||||
|
QMAKE_ARGS+=("QMAKE_CXX=$QMAKE_CXX")
|
||||||
|
fi
|
||||||
|
if [[ -n "${QMAKE_LINK:-}" ]]; then
|
||||||
|
QMAKE_ARGS+=("QMAKE_LINK=$QMAKE_LINK")
|
||||||
|
fi
|
||||||
|
if [[ -n "${QMAKE_CFLAGS:-}" ]]; then
|
||||||
|
QMAKE_ARGS+=("QMAKE_CFLAGS=$QMAKE_CFLAGS")
|
||||||
|
fi
|
||||||
|
if [[ -n "${QMAKE_CXXFLAGS:-}" ]]; then
|
||||||
|
QMAKE_ARGS+=("QMAKE_CXXFLAGS=$QMAKE_CXXFLAGS")
|
||||||
|
fi
|
||||||
|
if [[ -n "${QMAKE_LFLAGS:-}" ]]; then
|
||||||
|
QMAKE_ARGS+=("QMAKE_LFLAGS=$QMAKE_LFLAGS")
|
||||||
|
fi
|
||||||
|
|
||||||
|
PACKAGE_NAME="slide"
|
||||||
|
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
make build
|
mkdir -p "$BUILD_DIR"
|
||||||
|
"$QMAKE_BIN" "${QMAKE_ARGS[@]}" "$ROOT_DIR/src/slide.pro" -o "$BUILD_DIR/Makefile"
|
||||||
|
make -C "$BUILD_DIR" -j"$MAKE_JOBS"
|
||||||
|
|
||||||
rm -rf "$STAGE_DIR"
|
rm -rf "$STAGE_DIR"
|
||||||
mkdir -p "$STAGE_DIR/DEBIAN" "$STAGE_DIR/usr/local/bin" "$DIST_DIR"
|
mkdir -p "$STAGE_DIR/DEBIAN" "$STAGE_DIR/usr/local/bin" "$DIST_DIR"
|
||||||
@@ -27,9 +61,9 @@ Section: graphics
|
|||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: ${ARCH}
|
Architecture: ${ARCH}
|
||||||
Maintainer: slide build
|
Maintainer: slide build
|
||||||
Depends: libqt5core5a, libqt5gui5, libqt5widgets5, libqt5network5, libexif12, qt5-image-formats-plugins
|
Depends: libqt5core5a, libqt5gui5, libqt5widgets5, libqt5network5, libexif12, qt5-image-formats-plugins, libmosquitto1
|
||||||
Description: Lightweight slideshow for photo frames
|
Description: Lightweight slideshow for photo frames
|
||||||
Simple, lightweight slideshow designed for low power devices.
|
Simple, lightweight slideshow designed for low power devices.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
dpkg-deb --build "$STAGE_DIR" "$DIST_DIR/${PACKAGE_NAME}_${VERSION}_${ARCH}.deb"
|
dpkg-deb --build -Zgzip "$STAGE_DIR" "$DIST_DIR/${PACKAGE_NAME}_${VERSION}_${ARCH}.deb"
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
|
|||||||
if(albumIds.size() > 0)
|
if(albumIds.size() > 0)
|
||||||
config.albumIds = albumIds;
|
config.albumIds = albumIds;
|
||||||
|
|
||||||
|
std::string personId = ParseJSONString(immichJson, "personId");
|
||||||
|
if(!personId.empty())
|
||||||
|
config.personIds.push_back(personId);
|
||||||
|
|
||||||
|
std::vector<std::string> personIds = ParseJSONStrings(immichJson, "personIds");
|
||||||
|
if(personIds.size() > 0)
|
||||||
|
config.personIds = personIds;
|
||||||
|
|
||||||
SetJSONInt(config.pageSize, immichJson, "pageSize");
|
SetJSONInt(config.pageSize, immichJson, "pageSize");
|
||||||
SetJSONInt(config.maxAssets, immichJson, "maxAssets");
|
SetJSONInt(config.maxAssets, immichJson, "maxAssets");
|
||||||
SetJSONInt(config.cacheMaxMB, immichJson, "cacheMaxMB");
|
SetJSONInt(config.cacheMaxMB, immichJson, "cacheMaxMB");
|
||||||
@@ -112,6 +120,46 @@ ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MqttConfig ParseMqttConfigObject(QJsonObject mqttJson) {
|
||||||
|
MqttConfig config;
|
||||||
|
|
||||||
|
std::string host = ParseJSONString(mqttJson, "host");
|
||||||
|
if(!host.empty())
|
||||||
|
config.host = host;
|
||||||
|
|
||||||
|
std::string topic = ParseJSONString(mqttJson, "topic");
|
||||||
|
if(!topic.empty())
|
||||||
|
config.topic = topic;
|
||||||
|
|
||||||
|
std::string immichTopic = ParseJSONString(mqttJson, "immichTopic");
|
||||||
|
if(!immichTopic.empty())
|
||||||
|
config.immichTopic = immichTopic;
|
||||||
|
|
||||||
|
std::string clientId = ParseJSONString(mqttJson, "clientId");
|
||||||
|
if(!clientId.empty())
|
||||||
|
config.clientId = clientId;
|
||||||
|
|
||||||
|
std::string username = ParseJSONString(mqttJson, "username");
|
||||||
|
if(!username.empty())
|
||||||
|
config.username = username;
|
||||||
|
|
||||||
|
std::string password = ParseJSONString(mqttJson, "password");
|
||||||
|
if(!password.empty())
|
||||||
|
config.password = password;
|
||||||
|
|
||||||
|
SetJSONInt(config.port, mqttJson, "port");
|
||||||
|
SetJSONInt(config.keepAlive, mqttJson, "keepAlive");
|
||||||
|
SetJSONInt(config.qos, mqttJson, "qos");
|
||||||
|
|
||||||
|
if(config.immichTopic.empty() && !config.topic.empty())
|
||||||
|
config.immichTopic = config.topic + "/immich";
|
||||||
|
|
||||||
|
if(!config.host.empty() && !config.topic.empty())
|
||||||
|
config.enabled = true;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
Config loadConfiguration(const std::string &configFilePath, const Config ¤tConfig) {
|
Config loadConfiguration(const std::string &configFilePath, const Config ¤tConfig) {
|
||||||
if(configFilePath.empty())
|
if(configFilePath.empty())
|
||||||
{
|
{
|
||||||
@@ -327,6 +375,11 @@ AppConfig loadAppConfiguration(const AppConfig &commandLineConfig) {
|
|||||||
loadedConfig.overlay = overlayString;
|
loadedConfig.overlay = overlayString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(jsonDoc.contains("mqtt") && jsonDoc["mqtt"].isObject())
|
||||||
|
{
|
||||||
|
loadedConfig.mqtt = ParseMqttConfigObject(jsonDoc["mqtt"].toObject());
|
||||||
|
}
|
||||||
|
|
||||||
loadedConfig.paths = parsePathEntry(jsonDoc, baseRecursive, baseShuffle, baseSorted);
|
loadedConfig.paths = parsePathEntry(jsonDoc, baseRecursive, baseShuffle, baseSorted);
|
||||||
if(loadedConfig.paths.count() <= 0)
|
if(loadedConfig.paths.count() <= 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ struct ImmichConfig {
|
|||||||
std::string url = "";
|
std::string url = "";
|
||||||
std::string apiKey = "";
|
std::string apiKey = "";
|
||||||
std::vector<std::string> albumIds;
|
std::vector<std::string> albumIds;
|
||||||
|
std::vector<std::string> personIds;
|
||||||
std::string size = "fullsize";
|
std::string size = "fullsize";
|
||||||
std::string order = "desc";
|
std::string order = "desc";
|
||||||
int pageSize = 200;
|
int pageSize = 200;
|
||||||
@@ -40,6 +41,13 @@ struct ImmichConfig {
|
|||||||
if (albumIds[i] != b.albumIds[i])
|
if (albumIds[i] != b.albumIds[i])
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (personIds.size() != b.personIds.size())
|
||||||
|
return false;
|
||||||
|
for (size_t i = 0; i < personIds.size(); ++i)
|
||||||
|
{
|
||||||
|
if (personIds[i] != b.personIds[i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +57,38 @@ struct ImmichConfig {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct MqttConfig {
|
||||||
|
bool enabled = false;
|
||||||
|
std::string host = "localhost";
|
||||||
|
int port = 1883;
|
||||||
|
std::string topic = "slide/control";
|
||||||
|
std::string immichTopic = "";
|
||||||
|
std::string clientId = "slide";
|
||||||
|
std::string username = "";
|
||||||
|
std::string password = "";
|
||||||
|
int keepAlive = 30;
|
||||||
|
int qos = 0;
|
||||||
|
|
||||||
|
bool operator==(const MqttConfig &b) const
|
||||||
|
{
|
||||||
|
return enabled == b.enabled &&
|
||||||
|
host == b.host &&
|
||||||
|
port == b.port &&
|
||||||
|
topic == b.topic &&
|
||||||
|
immichTopic == b.immichTopic &&
|
||||||
|
clientId == b.clientId &&
|
||||||
|
username == b.username &&
|
||||||
|
password == b.password &&
|
||||||
|
keepAlive == b.keepAlive &&
|
||||||
|
qos == b.qos;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator!=(const MqttConfig &b) const
|
||||||
|
{
|
||||||
|
return !operator==(b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// configuration options that apply to an image/folder of images
|
// configuration options that apply to an image/folder of images
|
||||||
struct Config {
|
struct Config {
|
||||||
public:
|
public:
|
||||||
@@ -106,6 +146,7 @@ struct AppConfig : public Config {
|
|||||||
std::string overlay = "";
|
std::string overlay = "";
|
||||||
QString overlayHexRGB = "#FFFFFF";
|
QString overlayHexRGB = "#FFFFFF";
|
||||||
QVector<PathEntry> paths;
|
QVector<PathEntry> paths;
|
||||||
|
MqttConfig mqtt;
|
||||||
|
|
||||||
bool debugMode = false;
|
bool debugMode = false;
|
||||||
|
|
||||||
|
|||||||
@@ -371,4 +371,4 @@ const ImageDetails ListImageSelector::getNextImage(const ImageDisplayOptions& ba
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
while(true);
|
while(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ ImageSwitcher::ImageSwitcher(MainWindow& w, unsigned int timeoutMsec, std::uniqu
|
|||||||
|
|
||||||
void ImageSwitcher::updateImage()
|
void ImageSwitcher::updateImage()
|
||||||
{
|
{
|
||||||
|
if (paused)
|
||||||
|
return;
|
||||||
if(reloadConfigIfNeeded)
|
if(reloadConfigIfNeeded)
|
||||||
{
|
{
|
||||||
reloadConfigIfNeeded(window, this);
|
reloadConfigIfNeeded(window, this);
|
||||||
@@ -60,10 +62,64 @@ void ImageSwitcher::setConfigFileReloader(std::function<void(MainWindow &w, Imag
|
|||||||
void ImageSwitcher::setRotationTime(unsigned int timeoutMsecIn)
|
void ImageSwitcher::setRotationTime(unsigned int timeoutMsecIn)
|
||||||
{
|
{
|
||||||
timeout = timeoutMsecIn;
|
timeout = timeoutMsecIn;
|
||||||
timer.start(timeout);
|
if (!paused)
|
||||||
|
timer.start(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImageSwitcher::setImageSelector(std::unique_ptr<ImageSelector>& selectorIn)
|
void ImageSwitcher::setImageSelector(std::unique_ptr<ImageSelector>& selectorIn)
|
||||||
{
|
{
|
||||||
selector = std::move(selectorIn);
|
selector = std::move(selectorIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ImageSwitcher::pause()
|
||||||
|
{
|
||||||
|
paused = true;
|
||||||
|
timer.stop();
|
||||||
|
timerNoContent.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageSwitcher::resume()
|
||||||
|
{
|
||||||
|
if (!paused)
|
||||||
|
return;
|
||||||
|
paused = false;
|
||||||
|
timer.start(timeout);
|
||||||
|
scheduleImageUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageSwitcher::stepOnce()
|
||||||
|
{
|
||||||
|
bool wasPaused = paused;
|
||||||
|
if (wasPaused)
|
||||||
|
paused = false;
|
||||||
|
updateImage();
|
||||||
|
if (wasPaused)
|
||||||
|
{
|
||||||
|
paused = true;
|
||||||
|
timer.stop();
|
||||||
|
timerNoContent.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageSwitcher::restart(std::unique_ptr<ImageSelector>& selectorIn)
|
||||||
|
{
|
||||||
|
paused = false;
|
||||||
|
timerNoContent.stop();
|
||||||
|
setImageSelector(selectorIn);
|
||||||
|
timer.start(timeout);
|
||||||
|
scheduleImageUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageSwitcher::skipToNextFolder()
|
||||||
|
{
|
||||||
|
auto *listSelector = dynamic_cast<ListImageSelector*>(selector.get());
|
||||||
|
if (!listSelector)
|
||||||
|
return false;
|
||||||
|
stepOnce();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageSwitcher::isPaused() const
|
||||||
|
{
|
||||||
|
return paused;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ public:
|
|||||||
void setConfigFileReloader(std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeededIn);
|
void setConfigFileReloader(std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeededIn);
|
||||||
void setRotationTime(unsigned int timeoutMsec);
|
void setRotationTime(unsigned int timeoutMsec);
|
||||||
void setImageSelector(std::unique_ptr<ImageSelector>& selector);
|
void setImageSelector(std::unique_ptr<ImageSelector>& selector);
|
||||||
|
void pause();
|
||||||
|
void resume();
|
||||||
|
void stepOnce();
|
||||||
|
void restart(std::unique_ptr<ImageSelector>& selector);
|
||||||
|
bool skipToNextFolder();
|
||||||
|
bool isPaused() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void updateImage();
|
void updateImage();
|
||||||
@@ -30,6 +36,7 @@ private:
|
|||||||
const unsigned int timeoutNoContent = 5 * 1000; // 5 sec
|
const unsigned int timeoutNoContent = 5 * 1000; // 5 sec
|
||||||
QTimer timerNoContent;
|
QTimer timerNoContent;
|
||||||
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeeded;
|
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeeded;
|
||||||
|
bool paused = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // IMAGESWITCHER_H
|
#endif // IMAGESWITCHER_H
|
||||||
|
|||||||
@@ -105,6 +105,14 @@ QVector<ImmichAsset> ImmichClient::fetchAssets()
|
|||||||
bool triedZero = false;
|
bool triedZero = false;
|
||||||
int page = 1;
|
int page = 1;
|
||||||
|
|
||||||
|
if (ShouldLog())
|
||||||
|
{
|
||||||
|
Log("Immich search: size=", config.size, ", order=", config.order,
|
||||||
|
", pageSize=", pageSize, ", maxAssets=", maxAssets,
|
||||||
|
", albumIds=", config.albumIds.size(),
|
||||||
|
", personIds=", config.personIds.size());
|
||||||
|
}
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
QJsonObject body;
|
QJsonObject body;
|
||||||
@@ -121,6 +129,13 @@ QVector<ImmichAsset> ImmichClient::fetchAssets()
|
|||||||
ids.append(QString::fromStdString(id));
|
ids.append(QString::fromStdString(id));
|
||||||
body["albumIds"] = ids;
|
body["albumIds"] = ids;
|
||||||
}
|
}
|
||||||
|
if (config.personIds.size() > 0)
|
||||||
|
{
|
||||||
|
QJsonArray ids;
|
||||||
|
for (const auto &id : config.personIds)
|
||||||
|
ids.append(QString::fromStdString(id));
|
||||||
|
body["personIds"] = ids;
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray response = postJson(apiUrl("/search/metadata"), body, nullptr, kMetadataTimeoutMs);
|
QByteArray response = postJson(apiUrl("/search/metadata"), body, nullptr, kMetadataTimeoutMs);
|
||||||
if (response.isEmpty())
|
if (response.isEmpty())
|
||||||
@@ -134,6 +149,7 @@ QVector<ImmichAsset> ImmichClient::fetchAssets()
|
|||||||
QJsonObject assetsObj = root["assets"].toObject();
|
QJsonObject assetsObj = root["assets"].toObject();
|
||||||
QJsonArray items = assetsObj["items"].toArray();
|
QJsonArray items = assetsObj["items"].toArray();
|
||||||
int total = assetsObj["total"].toInt();
|
int total = assetsObj["total"].toInt();
|
||||||
|
Log("Immich page ", page, ": ", items.size(), " assets (total ", total, ")");
|
||||||
if (items.isEmpty())
|
if (items.isEmpty())
|
||||||
{
|
{
|
||||||
if (total > 0 && page == 1 && !triedZero)
|
if (total > 0 && page == 1 && !triedZero)
|
||||||
@@ -194,6 +210,7 @@ bool ImmichClient::downloadAsset(const QString &assetId, QByteArray &data, QStri
|
|||||||
url.setQuery(query);
|
url.setQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log("Immich download asset ", assetId.toStdString(), " (", size.toStdString(), ")");
|
||||||
QByteArray payload = getBytes(url, &contentType, kAssetTimeoutMs);
|
QByteArray payload = getBytes(url, &contentType, kAssetTimeoutMs);
|
||||||
if (payload.isEmpty())
|
if (payload.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
@@ -297,7 +314,10 @@ QString ImmichAssetCache::getCachedPath(const QString &assetId, const QString &a
|
|||||||
|
|
||||||
QString existing = findExisting(assetId);
|
QString existing = findExisting(assetId);
|
||||||
if (!existing.isEmpty())
|
if (!existing.isEmpty())
|
||||||
|
{
|
||||||
|
Log("Immich cache hit: ", assetId.toStdString());
|
||||||
return existing;
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray data;
|
QByteArray data;
|
||||||
QString contentType;
|
QString contentType;
|
||||||
@@ -318,6 +338,8 @@ QString ImmichAssetCache::getCachedPath(const QString &assetId, const QString &a
|
|||||||
if (!file.commit())
|
if (!file.commit())
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
|
Log("Immich cached asset: ", assetId.toStdString(), " -> ", filePath.toStdString());
|
||||||
|
|
||||||
if (cacheMaxBytes > 0)
|
if (cacheMaxBytes > 0)
|
||||||
{
|
{
|
||||||
if (!cacheSizeKnown)
|
if (!cacheSizeKnown)
|
||||||
|
|||||||
258
src/main.cpp
258
src/main.cpp
@@ -3,6 +3,7 @@
|
|||||||
#include "imageswitcher.h"
|
#include "imageswitcher.h"
|
||||||
#include "pathtraverser.h"
|
#include "pathtraverser.h"
|
||||||
#include "immichpathtraverser.h"
|
#include "immichpathtraverser.h"
|
||||||
|
#include "mqttcontroller.h"
|
||||||
#include "overlay.h"
|
#include "overlay.h"
|
||||||
#include "appconfig.h"
|
#include "appconfig.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
@@ -18,6 +19,9 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
void usage(std::string programName) {
|
void usage(std::string programName) {
|
||||||
std::cerr << "Usage: " << programName << " [-t rotation_seconds] [-T transition_seconds] [-h/--overlay-color #rrggbb] [-a aspect('l','p','a', 'm')] [-o background_opacity(0..255)] [-b blur_radius] -p image_folder [-r] [-s] [-S] [-v] [--verbose] [--stretch] [-c config_file_path]" << std::endl;
|
std::cerr << "Usage: " << programName << " [-t rotation_seconds] [-T transition_seconds] [-h/--overlay-color #rrggbb] [-a aspect('l','p','a', 'm')] [-o background_opacity(0..255)] [-b blur_radius] -p image_folder [-r] [-s] [-S] [-v] [--verbose] [--stretch] [-c config_file_path]" << std::endl;
|
||||||
@@ -242,6 +246,216 @@ void ReloadConfigIfNeeded(AppConfig &appConfig, MainWindow &w, ImageSwitcher *sw
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool ParseBooleanString(const QString &value, bool &outValue)
|
||||||
|
{
|
||||||
|
QString v = value.trimmed().toLower();
|
||||||
|
if (v == "true" || v == "1" || v == "yes" || v == "on")
|
||||||
|
{
|
||||||
|
outValue = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (v == "false" || v == "0" || v == "no" || v == "off")
|
||||||
|
{
|
||||||
|
outValue = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> SplitCsv(const QString &value)
|
||||||
|
{
|
||||||
|
std::vector<std::string> output;
|
||||||
|
QStringList parts = value.split(',', Qt::SkipEmptyParts);
|
||||||
|
for (const auto &part : parts)
|
||||||
|
{
|
||||||
|
QString trimmed = part.trimmed();
|
||||||
|
if (!trimmed.isEmpty())
|
||||||
|
output.push_back(trimmed.toStdString());
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
QString trimmed = payload.trimmed();
|
||||||
|
if (trimmed.isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (trimmed.startsWith("{"))
|
||||||
|
{
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(trimmed.toUtf8());
|
||||||
|
if (!doc.isObject())
|
||||||
|
return false;
|
||||||
|
QJsonObject obj = doc.object();
|
||||||
|
|
||||||
|
if (obj.contains("albumId") && obj["albumId"].isString())
|
||||||
|
{
|
||||||
|
config.albumIds = { obj["albumId"].toString().toStdString() };
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (obj.contains("albumIds") && obj["albumIds"].isArray())
|
||||||
|
{
|
||||||
|
config.albumIds.clear();
|
||||||
|
QJsonArray arr = obj["albumIds"].toArray();
|
||||||
|
for (const auto &value : arr)
|
||||||
|
{
|
||||||
|
if (value.isString())
|
||||||
|
config.albumIds.push_back(value.toString().toStdString());
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (obj.contains("personId") && obj["personId"].isString())
|
||||||
|
{
|
||||||
|
config.personIds = { obj["personId"].toString().toStdString() };
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (obj.contains("personIds") && obj["personIds"].isArray())
|
||||||
|
{
|
||||||
|
config.personIds.clear();
|
||||||
|
QJsonArray arr = obj["personIds"].toArray();
|
||||||
|
for (const auto &value : arr)
|
||||||
|
{
|
||||||
|
if (value.isString())
|
||||||
|
config.personIds.push_back(value.toString().toStdString());
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (obj.contains("order") && obj["order"].isString())
|
||||||
|
{
|
||||||
|
config.order = obj["order"].toString().toStdString();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (obj.contains("size") && obj["size"].isString())
|
||||||
|
{
|
||||||
|
config.size = obj["size"].toString().toStdString();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (obj.contains("pageSize") && obj["pageSize"].isDouble())
|
||||||
|
{
|
||||||
|
config.pageSize = (int)obj["pageSize"].toDouble();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (obj.contains("maxAssets") && obj["maxAssets"].isDouble())
|
||||||
|
{
|
||||||
|
config.maxAssets = (int)obj["maxAssets"].toDouble();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (obj.contains("includeArchived"))
|
||||||
|
{
|
||||||
|
if (obj["includeArchived"].isBool())
|
||||||
|
{
|
||||||
|
config.includeArchived = obj["includeArchived"].toBool();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
else if (obj["includeArchived"].isString())
|
||||||
|
{
|
||||||
|
bool parsed = false;
|
||||||
|
bool boolValue = false;
|
||||||
|
parsed = ParseBooleanString(obj["includeArchived"].toString(), boolValue);
|
||||||
|
if (parsed)
|
||||||
|
{
|
||||||
|
config.includeArchived = boolValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.contains("reset") && obj["reset"].isBool() && obj["reset"].toBool())
|
||||||
|
{
|
||||||
|
config.albumIds.clear();
|
||||||
|
config.personIds.clear();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString key;
|
||||||
|
QString value;
|
||||||
|
int idx = trimmed.indexOf('=');
|
||||||
|
if (idx < 0)
|
||||||
|
idx = trimmed.indexOf(':');
|
||||||
|
if (idx < 0)
|
||||||
|
idx = trimmed.indexOf(' ');
|
||||||
|
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
key = trimmed.left(idx).trimmed().toLower();
|
||||||
|
value = trimmed.mid(idx + 1).trimmed();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
key = trimmed.toLower();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == "reset" || key == "clear" || key == "all")
|
||||||
|
{
|
||||||
|
config.albumIds.clear();
|
||||||
|
config.personIds.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key == "album" || key == "albumid")
|
||||||
|
{
|
||||||
|
config.albumIds = { value.toStdString() };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key == "albums" || key == "albumids")
|
||||||
|
{
|
||||||
|
config.albumIds = SplitCsv(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key == "person" || key == "personid")
|
||||||
|
{
|
||||||
|
config.personIds = { value.toStdString() };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key == "persons" || key == "personids")
|
||||||
|
{
|
||||||
|
config.personIds = SplitCsv(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key == "order")
|
||||||
|
{
|
||||||
|
config.order = value.toStdString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key == "size")
|
||||||
|
{
|
||||||
|
config.size = value.toStdString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key == "includearchived")
|
||||||
|
{
|
||||||
|
bool boolValue = false;
|
||||||
|
if (ParseBooleanString(value, boolValue))
|
||||||
|
{
|
||||||
|
config.includeArchived = boolValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key == "pagesize")
|
||||||
|
{
|
||||||
|
bool ok = false;
|
||||||
|
int parsed = value.toInt(&ok);
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
config.pageSize = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key == "maxassets")
|
||||||
|
{
|
||||||
|
bool ok = false;
|
||||||
|
int parsed = value.toInt(&ok);
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
config.maxAssets = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
QApplication a(argc, argv);
|
QApplication a(argc, argv);
|
||||||
@@ -275,6 +489,50 @@ int main(int argc, char *argv[])
|
|||||||
w.setImageSwitcher(&switcher);
|
w.setImageSwitcher(&switcher);
|
||||||
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloader = [&appConfig](MainWindow &w, ImageSwitcher *switcher) { ReloadConfigIfNeeded(appConfig, w, switcher); };
|
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloader = [&appConfig](MainWindow &w, ImageSwitcher *switcher) { ReloadConfigIfNeeded(appConfig, w, switcher); };
|
||||||
switcher.setConfigFileReloader(reloader);
|
switcher.setConfigFileReloader(reloader);
|
||||||
|
|
||||||
|
std::unique_ptr<MqttController> mqttController;
|
||||||
|
if (appConfig.mqtt.enabled)
|
||||||
|
{
|
||||||
|
mqttController = std::unique_ptr<MqttController>(new MqttController(appConfig.mqtt, &a));
|
||||||
|
QObject::connect(mqttController.get(), &MqttController::play, [&switcher]() { switcher.resume(); });
|
||||||
|
QObject::connect(mqttController.get(), &MqttController::pause, [&switcher]() { switcher.pause(); });
|
||||||
|
QObject::connect(mqttController.get(), &MqttController::nextImage, [&switcher]() { switcher.stepOnce(); });
|
||||||
|
QObject::connect(mqttController.get(), &MqttController::nextFolder, [&switcher]() {
|
||||||
|
if (!switcher.skipToNextFolder())
|
||||||
|
switcher.stepOnce();
|
||||||
|
});
|
||||||
|
QObject::connect(mqttController.get(), &MqttController::restart, [&appConfig, &switcher]() {
|
||||||
|
std::unique_ptr<ImageSelector> newSelector = GetSelectorForApp(appConfig);
|
||||||
|
switcher.restart(newSelector);
|
||||||
|
});
|
||||||
|
QObject::connect(mqttController.get(), &MqttController::immichControl, [&appConfig, &switcher](const QString &payload) {
|
||||||
|
bool updated = false;
|
||||||
|
for (int i = 0; i < appConfig.paths.count(); ++i)
|
||||||
|
{
|
||||||
|
if (!appConfig.paths[i].immich.enabled)
|
||||||
|
continue;
|
||||||
|
ImmichConfig newConfig = appConfig.paths[i].immich;
|
||||||
|
if (ApplyImmichPayload(newConfig, payload))
|
||||||
|
{
|
||||||
|
appConfig.paths[i].immich = newConfig;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated)
|
||||||
|
{
|
||||||
|
Log("MQTT immich update applied.");
|
||||||
|
std::unique_ptr<ImageSelector> newSelector = GetSelectorForApp(appConfig);
|
||||||
|
switcher.setImageSelector(newSelector);
|
||||||
|
if (!switcher.isPaused())
|
||||||
|
switcher.scheduleImageUpdate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log("MQTT immich update ignored: ", payload.toStdString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mqttController->start();
|
||||||
|
}
|
||||||
switcher.start();
|
switcher.start();
|
||||||
return a.exec();
|
return a.exec();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <QGraphicsPixmapItem>
|
#include <QGraphicsPixmapItem>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
|
#include <QTransform>
|
||||||
|
|
||||||
MainWindow::MainWindow(QWidget *parent) :
|
MainWindow::MainWindow(QWidget *parent) :
|
||||||
QMainWindow(parent),
|
QMainWindow(parent),
|
||||||
@@ -170,11 +171,11 @@ void MainWindow::updateImage()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
QLabel *label = this->findChild<QLabel*>("image");
|
QLabel *label = this->findChild<QLabel*>("image");
|
||||||
const QPixmap* oldImage = label->pixmap();
|
QPixmap oldImage = label->pixmap(Qt::ReturnByValue);
|
||||||
if (oldImage != NULL && transitionSeconds > 0)
|
if (!oldImage.isNull() && transitionSeconds > 0)
|
||||||
{
|
{
|
||||||
QPalette palette;
|
QPalette palette;
|
||||||
palette.setBrush(QPalette::Background, *oldImage);
|
palette.setBrush(QPalette::Window, oldImage);
|
||||||
this->setPalette(palette);
|
this->setPalette(palette);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +214,7 @@ void MainWindow::updateImage()
|
|||||||
|
|
||||||
label->setPixmap(background);
|
label->setPixmap(background);
|
||||||
|
|
||||||
if (oldImage != NULL && transitionSeconds > 0)
|
if (!oldImage.isNull() && transitionSeconds > 0)
|
||||||
{
|
{
|
||||||
auto effect = new QGraphicsOpacityEffect(label);
|
auto effect = new QGraphicsOpacityEffect(label);
|
||||||
effect->setOpacity(0.0);
|
effect->setOpacity(0.0);
|
||||||
@@ -273,9 +274,9 @@ QPixmap MainWindow::getBlurredBackground(const QPixmap& originalSize, const QPix
|
|||||||
|
|
||||||
QPixmap MainWindow::getRotatedPixmap(const QPixmap& p)
|
QPixmap MainWindow::getRotatedPixmap(const QPixmap& p)
|
||||||
{
|
{
|
||||||
QMatrix matrix;
|
QTransform transform;
|
||||||
matrix.rotate(currentImage.rotation);
|
transform.rotate(currentImage.rotation);
|
||||||
return p.transformed(matrix);
|
return p.transformed(transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap MainWindow::getScaledPixmap(const QPixmap& p)
|
QPixmap MainWindow::getScaledPixmap(const QPixmap& p)
|
||||||
@@ -325,12 +326,12 @@ void MainWindow::drawBackground(const QPixmap& originalSize, const QPixmap& scal
|
|||||||
QPixmap background = blur(originalSize.scaledToHeight(height()));
|
QPixmap background = blur(originalSize.scaledToHeight(height()));
|
||||||
QRect rect((background.width() - width())/2, 0, width(), height());
|
QRect rect((background.width() - width())/2, 0, width(), height());
|
||||||
background = background.copy(rect);
|
background = background.copy(rect);
|
||||||
palette.setBrush(QPalette::Background, background);
|
palette.setBrush(QPalette::Window, background);
|
||||||
} else {
|
} else {
|
||||||
QPixmap background = blur(originalSize.scaledToHeight(height()));
|
QPixmap background = blur(originalSize.scaledToHeight(height()));
|
||||||
QRect rect((background.width() - width())/2, 0, width(), height());
|
QRect rect((background.width() - width())/2, 0, width(), height());
|
||||||
background = background.copy(rect);
|
background = background.copy(rect);
|
||||||
palette.setBrush(QPalette::Background, background);
|
palette.setBrush(QPalette::Window, background);
|
||||||
}
|
}
|
||||||
this->setPalette(palette);
|
this->setPalette(palette);
|
||||||
}
|
}
|
||||||
|
|||||||
177
src/mqttcontroller.cpp
Normal file
177
src/mqttcontroller.cpp
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#include "mqttcontroller.h"
|
||||||
|
#include "logger.h"
|
||||||
|
|
||||||
|
#include <QMetaObject>
|
||||||
|
|
||||||
|
#include <mosquitto.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool g_mqttInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MqttController::MqttController(const MqttConfig &configIn, QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
config(configIn)
|
||||||
|
{
|
||||||
|
if (!g_mqttInitialized)
|
||||||
|
{
|
||||||
|
mosquitto_lib_init();
|
||||||
|
g_mqttInitialized = true;
|
||||||
|
}
|
||||||
|
controlTopic = QString::fromStdString(config.topic);
|
||||||
|
immichTopic = QString::fromStdString(config.immichTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
MqttController::~MqttController()
|
||||||
|
{
|
||||||
|
if (client)
|
||||||
|
{
|
||||||
|
mosquitto_disconnect(client);
|
||||||
|
mosquitto_loop_stop(client, true);
|
||||||
|
mosquitto_destroy(client);
|
||||||
|
client = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttController::start()
|
||||||
|
{
|
||||||
|
if (!config.enabled)
|
||||||
|
{
|
||||||
|
Log("MQTT disabled or missing host/topic.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString clientId = QString::fromStdString(config.clientId);
|
||||||
|
if (clientId.isEmpty())
|
||||||
|
clientId = "slide";
|
||||||
|
|
||||||
|
client = mosquitto_new(clientId.toUtf8().constData(), true, this);
|
||||||
|
if (!client)
|
||||||
|
{
|
||||||
|
Log("MQTT: failed to create client.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mosquitto_connect_callback_set(client, &MqttController::HandleConnect);
|
||||||
|
mosquitto_message_callback_set(client, &MqttController::HandleMessage);
|
||||||
|
|
||||||
|
if (!config.username.empty())
|
||||||
|
{
|
||||||
|
mosquitto_username_pw_set(client, config.username.c_str(),
|
||||||
|
config.password.empty() ? nullptr : config.password.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
mosquitto_reconnect_delay_set(client, 2, 30, true);
|
||||||
|
|
||||||
|
int rc = mosquitto_connect_async(client, config.host.c_str(), config.port, config.keepAlive);
|
||||||
|
if (rc != MOSQ_ERR_SUCCESS)
|
||||||
|
{
|
||||||
|
Log("MQTT connect failed: ", mosquitto_strerror(rc));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = mosquitto_loop_start(client);
|
||||||
|
if (rc != MOSQ_ERR_SUCCESS)
|
||||||
|
{
|
||||||
|
Log("MQTT loop start failed: ", mosquitto_strerror(rc));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttController::HandleConnect(struct mosquitto *mosq, void *userdata, int rc)
|
||||||
|
{
|
||||||
|
auto *self = static_cast<MqttController *>(userdata);
|
||||||
|
if (!self || !mosq)
|
||||||
|
return;
|
||||||
|
if (rc != 0)
|
||||||
|
{
|
||||||
|
Log("MQTT connect error: ", mosquitto_strerror(rc));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->connected = true;
|
||||||
|
self->subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttController::subscribe()
|
||||||
|
{
|
||||||
|
if (!client || !connected)
|
||||||
|
return;
|
||||||
|
int rc = mosquitto_subscribe(client, nullptr, config.topic.c_str(), config.qos);
|
||||||
|
if (rc != MOSQ_ERR_SUCCESS)
|
||||||
|
{
|
||||||
|
Log("MQTT subscribe failed: ", mosquitto_strerror(rc));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log("MQTT subscribed to ", config.topic);
|
||||||
|
}
|
||||||
|
if (!config.immichTopic.empty() && config.immichTopic != config.topic)
|
||||||
|
{
|
||||||
|
rc = mosquitto_subscribe(client, nullptr, config.immichTopic.c_str(), config.qos);
|
||||||
|
if (rc != MOSQ_ERR_SUCCESS)
|
||||||
|
{
|
||||||
|
Log("MQTT subscribe (immich) failed: ", mosquitto_strerror(rc));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log("MQTT subscribed to ", config.immichTopic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttController::HandleMessage(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *message)
|
||||||
|
{
|
||||||
|
Q_UNUSED(mosq);
|
||||||
|
auto *self = static_cast<MqttController *>(userdata);
|
||||||
|
if (!self || !message || !message->payload)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString payload = QString::fromUtf8(static_cast<const char *>(message->payload), message->payloadlen);
|
||||||
|
QString topic = QString::fromUtf8(message->topic);
|
||||||
|
QMetaObject::invokeMethod(self, "handleMessage", Qt::QueuedConnection,
|
||||||
|
Q_ARG(QString, topic),
|
||||||
|
Q_ARG(QString, payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttController::handleMessage(const QString &topic, const QString &payload)
|
||||||
|
{
|
||||||
|
QString cmd = payload.trimmed().toLower();
|
||||||
|
if (cmd.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Log("MQTT message on ", topic.toStdString(), ": ", cmd.toStdString());
|
||||||
|
|
||||||
|
if (!immichTopic.isEmpty() && topic == immichTopic)
|
||||||
|
{
|
||||||
|
emit immichControl(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd == "play" || cmd == "resume")
|
||||||
|
{
|
||||||
|
emit play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cmd == "pause")
|
||||||
|
{
|
||||||
|
emit pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cmd == "next" || cmd == "skip" || cmd == "next-image")
|
||||||
|
{
|
||||||
|
emit nextImage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cmd == "next-folder" || cmd == "folder-next" || cmd == "skip-folder")
|
||||||
|
{
|
||||||
|
emit nextFolder();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cmd == "restart" || cmd == "reset")
|
||||||
|
{
|
||||||
|
emit restart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log("MQTT unknown command: ", cmd.toStdString());
|
||||||
|
}
|
||||||
45
src/mqttcontroller.h
Normal file
45
src/mqttcontroller.h
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#ifndef MQTTCONTROLLER_H
|
||||||
|
#define MQTTCONTROLLER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "appconfig.h"
|
||||||
|
|
||||||
|
struct mosquitto;
|
||||||
|
struct mosquitto_message;
|
||||||
|
|
||||||
|
class MqttController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit MqttController(const MqttConfig &config, QObject *parent = nullptr);
|
||||||
|
~MqttController();
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void play();
|
||||||
|
void pause();
|
||||||
|
void nextImage();
|
||||||
|
void nextFolder();
|
||||||
|
void restart();
|
||||||
|
void immichControl(const QString &payload);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleMessage(const QString &topic, const QString &payload);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void HandleConnect(struct mosquitto *mosq, void *userdata, int rc);
|
||||||
|
static void HandleMessage(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *message);
|
||||||
|
|
||||||
|
void subscribe();
|
||||||
|
|
||||||
|
MqttConfig config;
|
||||||
|
QString controlTopic;
|
||||||
|
QString immichTopic;
|
||||||
|
struct mosquitto *client = nullptr;
|
||||||
|
bool connected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // MQTTCONTROLLER_H
|
||||||
@@ -37,6 +37,7 @@ SOURCES += \
|
|||||||
pathtraverser.cpp \
|
pathtraverser.cpp \
|
||||||
immichpathtraverser.cpp \
|
immichpathtraverser.cpp \
|
||||||
immichclient.cpp \
|
immichclient.cpp \
|
||||||
|
mqttcontroller.cpp \
|
||||||
overlay.cpp \
|
overlay.cpp \
|
||||||
imageselector.cpp \
|
imageselector.cpp \
|
||||||
appconfig.cpp \
|
appconfig.cpp \
|
||||||
@@ -49,6 +50,7 @@ HEADERS += \
|
|||||||
pathtraverser.h \
|
pathtraverser.h \
|
||||||
immichpathtraverser.h \
|
immichpathtraverser.h \
|
||||||
immichclient.h \
|
immichclient.h \
|
||||||
|
mqttcontroller.h \
|
||||||
overlay.h \
|
overlay.h \
|
||||||
imageswitcher.h \
|
imageswitcher.h \
|
||||||
imagestructs.h \
|
imagestructs.h \
|
||||||
@@ -62,3 +64,4 @@ target.path = /usr/local/bin/
|
|||||||
INSTALLS += target
|
INSTALLS += target
|
||||||
|
|
||||||
unix|win32: LIBS += -lexif
|
unix|win32: LIBS += -lexif
|
||||||
|
unix|win32: LIBS += -lmosquitto
|
||||||
|
|||||||
Reference in New Issue
Block a user