Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4bbfd49ad | ||
|
|
142b79ee27 | ||
|
|
a43e9fe122 | ||
|
|
ce84b1d062 | ||
|
|
0e6113729d | ||
|
|
27d7b346c0 | ||
|
|
ea9c73826b | ||
|
|
5a9605c87b | ||
|
|
db151c2920 | ||
|
|
c7fca31653 | ||
|
|
081c01ada9 | ||
|
|
613d4e7f61 | ||
|
|
79daae6796 | ||
|
|
b1e751a6b7 | ||
|
|
068e4c2aa5 | ||
|
|
f1144c483e | ||
|
|
e5b8ef6a00 | ||
|
|
b200d997b0 | ||
|
|
11e8e3241c | ||
|
|
f5e40ab5b2 | ||
|
|
3231bc5df2 | ||
|
|
0ce230de3c | ||
|
|
252b358baa | ||
|
|
dd76a7c4bf | ||
|
|
9bd8cb0676 | ||
|
|
48a8c36135 | ||
|
|
19c946c705 | ||
|
|
1828353d52 | ||
|
|
2a54663f76 | ||
|
|
6942cbb459 | ||
|
|
df4711d1c1 | ||
|
|
7b71f5dc9a | ||
|
|
0ed8c005c5 | ||
|
|
1c81365247 |
@@ -0,0 +1,194 @@
|
||||
name: linux appimage (gitea self-hosted)
|
||||
|
||||
# Runs ONLY on Gitea (GitHub ignores .gitea/workflows), in the private Builds
|
||||
# repository. Gitea is master; GitHub is a mirror and a release surface.
|
||||
#
|
||||
# Unlike the Windows lane, which builds a static toolchain of its own through
|
||||
# contrib/depends, the x86_64 AppImage links against the host's Qt6. Those
|
||||
# dependencies are owned by such-fleet (roles/wowlet_build_host) rather than
|
||||
# installed here: a workflow that apt-installs needs sudo on a shared runner and
|
||||
# makes the build host's contents a property of whichever job ran last.
|
||||
#
|
||||
# Dispatch-only. The runner is capacity 1 and shared with the fleet gates.
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
appimage:
|
||||
name: "Linux x86_64 (host Qt6 AppImage)"
|
||||
runs-on: ubuntu-latest # gitea label ubuntu-latest:host -> deb, host mode
|
||||
steps:
|
||||
- name: verify the build host has its dependencies
|
||||
run: |
|
||||
missing=0
|
||||
for t in cmake make curl bzip2 git patchelf pkg-config qmake6; do
|
||||
command -v "$t" >/dev/null || { echo "MISSING TOOL: $t"; missing=1; }
|
||||
done
|
||||
for h in /usr/include/boost/version.hpp; do
|
||||
[ -f "$h" ] || { echo "MISSING HEADER: $h"; missing=1; }
|
||||
done
|
||||
pkg-config --exists libsodium || { echo "MISSING: libsodium"; missing=1; }
|
||||
[ "$missing" = 0 ] || {
|
||||
echo "Run playbooks/wowlet-build-host.yml in such-fleet; do not apt-install from CI."
|
||||
exit 1; }
|
||||
echo "qt $(qmake6 -query QT_VERSION)"; nproc; free -g | head -2
|
||||
|
||||
- name: configure git
|
||||
run: git config --global --add safe.directory '*'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: fetch wownerod to embed
|
||||
run: |
|
||||
# ubuntu-latest and windows-latest both resolve to the same host in
|
||||
# host mode, so a fixed /tmp path lets a concurrently dispatched job's
|
||||
# `rm -rf` delete this job's binary between fetch and configure. Scope
|
||||
# every extract to the run.
|
||||
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
|
||||
rm -rf "$EMBED/wownerod-embed" && mkdir -p "$EMBED/wownerod-embed"
|
||||
curl -fL -o "$EMBED/wownero.tar.bz2" \
|
||||
https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/wownero-x86_64-linux-gnu-v0.11.4.0.tar.bz2
|
||||
rm -rf "$EMBED/wow-x" && mkdir -p "$EMBED/wow-x"
|
||||
tar xjf "$EMBED/wownero.tar.bz2" -C "$EMBED/wow-x"
|
||||
find "$EMBED/wow-x" -name wownerod -type f -exec cp {} "$EMBED/wownerod-embed/" \;
|
||||
chmod +x "$EMBED/wownerod-embed/wownerod"
|
||||
ls -la "$EMBED/wownerod-embed"
|
||||
|
||||
- name: fetch tor to embed
|
||||
run: |
|
||||
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
|
||||
rm -rf "$EMBED/tor-embed" && mkdir -p "$EMBED/tor-embed"
|
||||
curl -fL -o "$EMBED/teb.tgz" \
|
||||
https://archive.torproject.org/tor-package-archive/torbrowser/15.0/tor-expert-bundle-linux-x86_64-15.0.tar.gz
|
||||
rm -rf "$EMBED/teb" && mkdir -p "$EMBED/teb" && tar xzf "$EMBED/teb.tgz" -C "$EMBED/teb"
|
||||
cp "$EMBED/teb/tor/tor" "$EMBED/teb/tor/libssl.so.3" "$EMBED/teb/tor/libevent-2.1.so.7" \
|
||||
"$EMBED/teb/tor/libcrypto.so.3" "$EMBED/tor-embed/"
|
||||
# The expert bundle's tor carries no rpath; point it and its libs at
|
||||
# $ORIGIN so the embedded copy resolves them from the extract dir on
|
||||
# any distro.
|
||||
for f in tor libssl.so.3 libevent-2.1.so.7 libcrypto.so.3; do
|
||||
patchelf --set-rpath '$ORIGIN' "$EMBED/tor-embed/$f"
|
||||
done
|
||||
chmod +x "$EMBED/tor-embed/tor"
|
||||
|
||||
- name: build
|
||||
run: |
|
||||
# TOR_VERSION must EXACTLY match `tor --version` of the embedded
|
||||
# binary, or TorManager::unpackBins() sees embedded > unpacked and
|
||||
# rewrites Tor on every launch. The bundle's release number (15.0) is
|
||||
# not its tor version, so derive it.
|
||||
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
|
||||
TORVER=$("$EMBED/tor-embed/tor" --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1)
|
||||
echo "embedded tor version: $TORVER"
|
||||
# ARCH=x86-64, never native: the build host's CPU is newer than many
|
||||
# users' and native bakes in unsupported instructions -> SIGILL.
|
||||
cmake -S . -B build \
|
||||
-DWITH_SCANNER=OFF \
|
||||
-DARCH=x86-64 \
|
||||
-DWOWNEROD_DIR="$EMBED/wownerod-embed" \
|
||||
-DTOR_DIR="$EMBED/tor-embed" \
|
||||
-DTOR_VERSION="$TORVER"
|
||||
cmake --build build -j6
|
||||
ls -la build/bin/
|
||||
|
||||
- name: package AppImage
|
||||
run: |
|
||||
rm -rf AppDir
|
||||
mkdir -p AppDir/usr/bin AppDir/usr/share/applications \
|
||||
AppDir/usr/share/icons/hicolor/256x256/apps
|
||||
cp build/bin/wowlet AppDir/usr/bin/wowlet
|
||||
chmod +x AppDir/usr/bin/wowlet
|
||||
cp src/assets/wowlet.desktop AppDir/usr/share/applications/wowlet.desktop
|
||||
cp src/assets/images/appicons/256x256.png \
|
||||
AppDir/usr/share/icons/hicolor/256x256/apps/wowlet.png
|
||||
|
||||
curl -sL -o linuxdeploy https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||
curl -sL -o linuxdeploy-plugin-qt https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
|
||||
curl -sL -o appimagetool https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x linuxdeploy linuxdeploy-plugin-qt appimagetool
|
||||
|
||||
export APPIMAGE_EXTRACT_AND_RUN=1
|
||||
export QMAKE="$(command -v qmake6)"
|
||||
export EXTRA_QT_PLUGINS="svg;multimedia;tls"
|
||||
export QML_SOURCES_PATHS="$PWD/src/assets/qml"
|
||||
export PATH="$PWD:$PATH"
|
||||
|
||||
./linuxdeploy --appdir AppDir --plugin qt \
|
||||
-d AppDir/usr/share/applications/wowlet.desktop \
|
||||
-i AppDir/usr/share/icons/hicolor/256x256/apps/wowlet.png
|
||||
|
||||
# qmlimportscanner reads only the `import` lines in our QML; it does
|
||||
# not follow a module's own qmldir depends/default-import chain, so
|
||||
# transitive modules (QtQuick -> QtQml.WorkerScript/Models) are
|
||||
# silently skipped. Blanket-copy the trees, then resolve each copied
|
||||
# plugin's Qt libs ourselves -- linuxdeploy only does that for modules
|
||||
# it deployed, not ones we copied by hand.
|
||||
QMLDIR=$(qmake6 -query QT_INSTALL_QML)
|
||||
mkdir -p AppDir/usr/qml AppDir/usr/lib
|
||||
for m in QtQuick QtQml; do cp -rn "$QMLDIR/$m" AppDir/usr/qml/ 2>/dev/null || true; done
|
||||
find AppDir/usr/qml -name '*.so' -print0 | while IFS= read -r -d '' so; do
|
||||
ldd "$so" 2>/dev/null | awk '/=> \//{print $3}' \
|
||||
| grep -E '/(libQt6|libicu|libpcre2|libdouble-conversion|libzstd|libb2|libpng|libharfbuzz|libmd4c|libglib|libgraphite)' \
|
||||
| while read -r lib; do cp -L -n "$lib" AppDir/usr/lib/ 2>/dev/null || true; done
|
||||
done
|
||||
echo "=== qt.conf ==="; cat AppDir/usr/bin/qt.conf
|
||||
|
||||
# Guard: mining.qml must not pull in QtQuick.Controls. If a future
|
||||
# edit re-adds a Controls type this fails loudly instead of shipping
|
||||
# a white mining tab.
|
||||
if grep -q 'import QtQuick.Controls' src/assets/qml/mining.qml; then
|
||||
echo "ERROR: mining.qml imports QtQuick.Controls again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH=x86_64 ./appimagetool AppDir wowlet-x86_64.AppImage
|
||||
ls -la wowlet-x86_64.AppImage
|
||||
|
||||
- name: upload wowlet AppImage
|
||||
# v3: Gitea implements the older artifact protocol and rejects
|
||||
# @actions/artifact v2 clients (upload-artifact@v4+) as unsupported.
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wowlet-x86_64-linux-AppImage
|
||||
path: wowlet-x86_64.AppImage
|
||||
if-no-files-found: error
|
||||
|
||||
- name: publish the AppImage to the tag's release
|
||||
# The artifact above uploads fine but this Gitea's REST listing reports
|
||||
# zero artifacts for every run, so a tag build's output cannot be fetched
|
||||
# back through the API. Release assets can, so publish there as well.
|
||||
# Fail-safe by design: with no usable token this says why and exits 0,
|
||||
# it never turns a green build red.
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -eu
|
||||
FILE=wowlet-x86_64.AppImage
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
API="${GITHUB_SERVER_URL:-https://git.such.software}/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
||||
echo "no token available to this runner, skipping the release upload" >&2
|
||||
exit 0
|
||||
fi
|
||||
H="Authorization: token $RELEASE_TOKEN"
|
||||
# Single-line python on purpose: a continuation line at column 0 would
|
||||
# end this block scalar and break the workflow's YAML. `|| true` keeps
|
||||
# a malformed response from aborting the step under `set -e`.
|
||||
rid=$(curl -sS -H "$H" "$API/releases/tags/$TAG" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
if [ -z "$rid" ]; then
|
||||
rid=$(curl -sS -H "$H" -H "Content-Type: application/json" -X POST \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"wowlet $TAG\"}" "$API/releases" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
fi
|
||||
if [ -z "$rid" ]; then
|
||||
echo "could not resolve or create a release for $TAG, skipping the upload" >&2
|
||||
exit 0
|
||||
fi
|
||||
code=$(curl -sS -H "$H" -o /dev/null -w '%{http_code}' -X POST \
|
||||
-F "attachment=@$FILE" "$API/releases/$rid/assets?name=$(basename "$FILE")")
|
||||
echo "release asset upload returned HTTP $code"
|
||||
if [ "$code" != "201" ]; then
|
||||
echo "the release upload did not return 201; the run artifact still holds the build" >&2
|
||||
fi
|
||||
@@ -103,8 +103,39 @@ jobs:
|
||||
# for the dormant `installed/` layout)
|
||||
mkdir -p dist/Wowlet.app/Contents/Resources
|
||||
cp ../src/assets/images/appicons/appicon.icns dist/Wowlet.app/Contents/Resources/
|
||||
# bundle Qt frameworks/plugins so the app runs off-machine
|
||||
/opt/homebrew/opt/qt/bin/macdeployqt dist/Wowlet.app
|
||||
# bundle Qt frameworks/plugins so the app runs off-machine.
|
||||
# -qmldir is not optional: our QML lives in .qrc resources, which
|
||||
# macdeployqt cannot scan, so without it the bundle ships no QML
|
||||
# modules at all and every page importing anything beyond QtQuick
|
||||
# fails to load. That is how 5.1.2 shipped a grey mining tab on mac
|
||||
# while Linux was fine: Contents/Resources held no qml directory, and
|
||||
# the app logged 'module "QtQuick.Layouts" is not installed'.
|
||||
/opt/homebrew/opt/qt/bin/macdeployqt dist/Wowlet.app -qmldir=../src/assets/qml
|
||||
|
||||
# qmlimportscanner reads only the `import` lines in our QML and does
|
||||
# not follow a module's own qmldir depends chain, so transitive
|
||||
# modules (QtQuick -> QtQml.WorkerScript/Models) are silently skipped.
|
||||
# The AppImage build hit this twice and blanket-copies the trees; do
|
||||
# the same here, before the dylib passes below so their deps resolve.
|
||||
# Ask Qt where its QML modules live rather than assuming a brew
|
||||
# layout; the AppImage build asks qmake the same question.
|
||||
QTPREFIX="$(brew --prefix qt)"
|
||||
QMLSRC="$("$QTPREFIX/bin/qmake6" -query QT_INSTALL_QML 2>/dev/null || echo "$QTPREFIX/share/qt/qml")"
|
||||
[ -d "$QMLSRC" ] || { echo "ERROR: Qt QML modules not found at $QMLSRC" >&2; exit 1; }
|
||||
mkdir -p dist/Wowlet.app/Contents/Resources/qml
|
||||
for m in QtQuick QtQml; do
|
||||
cp -Rn "$QMLSRC/$m" dist/Wowlet.app/Contents/Resources/qml/ 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Guard: the module the mining tab needs has to be in the bundle. A
|
||||
# missing QML module is invisible until someone opens the tab, so it
|
||||
# fails the build instead of shipping a page that paints grey.
|
||||
for m in QtQuick QtQuick/Layouts; do
|
||||
[ -d "dist/Wowlet.app/Contents/Resources/qml/$m" ] || {
|
||||
echo "ERROR: $m missing from the bundle; QML pages will not load" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
# macdeployqt bundles Qt frameworks but not the plain-dylib brew deps
|
||||
# of Qt's image plugins (libwebp/libsharpyuv/libbrotli...), which are
|
||||
@@ -216,3 +247,47 @@ jobs:
|
||||
name: wowlet-macos
|
||||
path: build/wowlet-macos-*.zip
|
||||
retention-days: 14
|
||||
# Without this an empty upload passes silently, which is how a signed
|
||||
# build can appear to publish while shipping nothing.
|
||||
if-no-files-found: error
|
||||
|
||||
- name: publish the signed zip to the tag's release
|
||||
# Same reason as the Linux job: run artifacts upload but this Gitea's
|
||||
# REST listing returns zero for every run, so the notarized zip cannot
|
||||
# be fetched back through the API. Release assets can be. Fail-safe:
|
||||
# with no usable token this says why and exits 0.
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -eu
|
||||
FILE=$(ls build/wowlet-macos-*.zip 2>/dev/null | head -1)
|
||||
if [ -z "$FILE" ]; then
|
||||
echo "no macOS zip to publish; the packaging step should have failed first" >&2
|
||||
exit 0
|
||||
fi
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
API="${GITHUB_SERVER_URL:-https://git.such.software}/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
||||
echo "no token available to this runner, skipping the release upload" >&2
|
||||
exit 0
|
||||
fi
|
||||
H="Authorization: token $RELEASE_TOKEN"
|
||||
# Single-line python on purpose: a continuation line at column 0 would
|
||||
# end this block scalar and break the workflow's YAML. `|| true` keeps
|
||||
# a malformed response from aborting the step under `set -e`.
|
||||
rid=$(curl -sS -H "$H" "$API/releases/tags/$TAG" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
if [ -z "$rid" ]; then
|
||||
rid=$(curl -sS -H "$H" -H "Content-Type: application/json" -X POST \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"wowlet $TAG\"}" "$API/releases" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
fi
|
||||
if [ -z "$rid" ]; then
|
||||
echo "could not resolve or create a release for $TAG, skipping the upload" >&2
|
||||
exit 0
|
||||
fi
|
||||
code=$(curl -sS -H "$H" -o /dev/null -w '%{http_code}' -X POST \
|
||||
-F "attachment=@$FILE" "$API/releases/$rid/assets?name=$(basename "$FILE")")
|
||||
echo "release asset upload returned HTTP $code"
|
||||
if [ "$code" != "201" ]; then
|
||||
echo "the release upload did not return 201; the run artifact still holds the build" >&2
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
name: windows build + sign (gitea self-hosted)
|
||||
|
||||
# Runs ONLY on Gitea (GitHub ignores .gitea/workflows), in the private Builds
|
||||
# repository. Gitea is master; GitHub is a mirror and a release surface.
|
||||
#
|
||||
# Two jobs on two different machines, and that split is the whole point:
|
||||
#
|
||||
# build -> `deb`, which advertises windows-latest:host because it carries the
|
||||
# MinGW cross-compiler. Produces an UNSIGNED wowlet.exe.
|
||||
# sign -> `desktop-iim2fs2` (windows:host), the only machine with the EV
|
||||
# code-signing token plugged into it.
|
||||
#
|
||||
# The signing host does not sign in this job. The runner there is
|
||||
# NT SERVICE\act_runner, which cannot use the token: SafeNet holds the token
|
||||
# login per user, and the login is established interactively by an operator.
|
||||
# So the job queues the artifact for the signing broker that Fleet installs
|
||||
# (roles/windows_code_signing) and waits for it. CI never sees the key and never
|
||||
# sees the token password, so it can never spend one of the token's three
|
||||
# password attempts.
|
||||
#
|
||||
# Dispatch-only. Both runners are capacity 1 and shared with other lanes.
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
env:
|
||||
HOST: x86_64-w64-mingw32
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Windows x86_64 (mingw cross, depends)"
|
||||
runs-on: windows-latest # gitea label windows-latest:host -> deb, host mode
|
||||
steps:
|
||||
- name: verify the cross toolchain is present
|
||||
# Host mode on a shared fleet runner: this job installs nothing and
|
||||
# needs no sudo. Fleet owns the toolchain; if something is missing the
|
||||
# job says exactly what rather than half-building.
|
||||
run: |
|
||||
missing=0
|
||||
for t in curl bzip2 unzip zip git cmake make ccache python3 perl pkg-config \
|
||||
x86_64-w64-mingw32-gcc-posix x86_64-w64-mingw32-g++-posix; do
|
||||
command -v "$t" >/dev/null || { echo "MISSING: $t"; missing=1; }
|
||||
done
|
||||
# mingw32.mk needs the posix threading variant (std::thread/mutex/cv).
|
||||
x86_64-w64-mingw32-g++ -v 2>&1 | grep -q 'Thread model: posix' || {
|
||||
echo "MISSING: x86_64-w64-mingw32-g++ is not the posix threading variant"; missing=1; }
|
||||
[ "$missing" = 0 ] || { echo "Fix the build host in such-fleet, not here."; exit 1; }
|
||||
echo "toolchain ok"; nproc; free -g | head -2
|
||||
|
||||
- name: configure git
|
||||
run: git config --global --add safe.directory '*'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# depends is the slow part (static Qt6/boost/openssl/tor from source).
|
||||
# Cache the source tarballs and the compiled packages so only a cold run
|
||||
# pays the ~1h cost.
|
||||
- name: restore depends sources cache
|
||||
id: cache-sources
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: contrib/depends/sources
|
||||
key: depends-sources-${{ hashFiles('contrib/depends/packages/*') }}
|
||||
restore-keys: |
|
||||
depends-sources-
|
||||
|
||||
- name: restore depends built cache (mingw)
|
||||
id: cache-built
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: contrib/depends/built
|
||||
key: depends-built-mingw32-${{ hashFiles('contrib/depends/packages/*', 'contrib/depends/hosts/*', 'contrib/depends/toolchain.cmake.in') }}
|
||||
restore-keys: |
|
||||
depends-built-mingw32-
|
||||
|
||||
- name: build depends (static Qt6 + tor + boost + openssl for mingw)
|
||||
# -j4, not the -j2 the GitHub lane needs: that limit existed because a
|
||||
# parallel static Qt6 compile OOMs a 16G hosted runner. deb has 30G.
|
||||
run: make -C contrib/depends HOST="$HOST" -j4
|
||||
|
||||
# Save with if:always() so an expensive toolchain banks even when the
|
||||
# later wowlet.exe build fails; the automatic post-save is skipped on
|
||||
# job failure, which otherwise re-pays hours every time.
|
||||
- name: save depends sources cache
|
||||
if: always() && steps.cache-sources.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: contrib/depends/sources
|
||||
key: depends-sources-${{ hashFiles('contrib/depends/packages/*') }}
|
||||
|
||||
- name: save depends built cache (mingw)
|
||||
if: always() && steps.cache-built.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: contrib/depends/built
|
||||
key: depends-built-mingw32-${{ hashFiles('contrib/depends/packages/*', 'contrib/depends/hosts/*', 'contrib/depends/toolchain.cmake.in') }}
|
||||
|
||||
- name: fetch wownerod.exe to embed
|
||||
run: |
|
||||
# This job and the Linux one land on the same host in host mode, and a
|
||||
# fixed /tmp path let each wipe the other's binary mid-build. Scope the
|
||||
# extract to the run.
|
||||
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
|
||||
rm -rf "$EMBED/wownerod-embed" && mkdir -p "$EMBED/wownerod-embed"
|
||||
curl -fL -o "$EMBED/wownero-win.zip" \
|
||||
https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/wownero-x86_64-w64-mingw32-v0.11.4.0.zip
|
||||
# -j flattens the versioned subdir so wownerod.exe is a DIRECT child
|
||||
# (WownerodQrcGenerator globs ${WOWNEROD_DIR}/* non-recursively).
|
||||
unzip -o -j "$EMBED/wownero-win.zip" 'wownero-x86_64-w64-mingw32-v0.11.4.0/wownerod.exe' -d "$EMBED/wownerod-embed"
|
||||
ls -la "$EMBED/wownerod-embed"
|
||||
|
||||
- name: build wowlet.exe
|
||||
run: |
|
||||
rm -rf build && mkdir build && cd build
|
||||
# ARCH=x86-64: portable baseline. wownero's CMake defaults ARCH=native,
|
||||
# which under a cross compiler bakes the BUILD HOST's instructions into
|
||||
# the Windows binary -> SIGILL on older CPUs.
|
||||
cmake --toolchain "$GITHUB_WORKSPACE/contrib/depends/$HOST/share/toolchain.cmake" \
|
||||
-DARCH=x86-64 \
|
||||
-DWITH_SCANNER=OFF \
|
||||
-DCHECK_UPDATES=OFF \
|
||||
-DTOR_INSTALLED=On \
|
||||
-DFEATHER_TARGET_TRIPLET="$HOST" \
|
||||
-DWOWNEROD_DIR="/tmp/wowlet-${GITHUB_RUN_ID:-local}/wownerod-embed" \
|
||||
..
|
||||
cmake --build . -j6
|
||||
ls -la bin/
|
||||
|
||||
- name: stage the unsigned exe for signing
|
||||
# The signing job needs the bare exe, not the zip: the broker signs one
|
||||
# PE file at a time. The zip is assembled after signing so the published
|
||||
# archive contains the signed binary.
|
||||
run: |
|
||||
mkdir -p stage/tor
|
||||
cp build/bin/wowlet.exe stage/wowlet.exe
|
||||
cp "contrib/depends/$HOST/Tor/tor.exe" stage/tor/tor.exe
|
||||
(cp LICENSE stage/LICENSE.txt 2>/dev/null || cp COPYING stage/LICENSE.txt 2>/dev/null) || true
|
||||
ls -la stage stage/tor
|
||||
|
||||
- name: upload unsigned staging tree
|
||||
# v3, not v4: Gitea Actions implements the older artifact protocol and
|
||||
# rejects @actions/artifact v2 clients with
|
||||
# "GHESNotSupportedError ... not currently supported on GHES".
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wowlet-x86_64-windows-unsigned
|
||||
path: stage/
|
||||
if-no-files-found: error
|
||||
|
||||
sign:
|
||||
name: "Authenticode sign (hardware token broker)"
|
||||
needs: build
|
||||
runs-on: windows # gitea label windows:host -> desktop-iim2fs2
|
||||
steps:
|
||||
- name: download unsigned staging tree
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: wowlet-x86_64-windows-unsigned
|
||||
path: stage
|
||||
|
||||
- name: keep the unsigned tree for the reproducible archive
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Copy-Item 'stage' 'unsigned' -Recurse -Force
|
||||
|
||||
- name: queue wowlet.exe for the signing broker and wait
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$queued = "wowlet-${env:GITHUB_RUN_ID}.exe"
|
||||
$inPath = "C:\signing\in\$queued"
|
||||
$outPath = "C:\signing\out\$queued"
|
||||
|
||||
if (Test-Path 'C:\signing\SIGNING_LOCKED') {
|
||||
Get-Content 'C:\signing\SIGNING_LOCKED'
|
||||
throw "The signing broker's circuit breaker is tripped. An operator must clear it."
|
||||
}
|
||||
|
||||
Copy-Item 'stage\wowlet.exe' $inPath -Force
|
||||
Set-Content -LiteralPath "$inPath.ready" -Value $env:GITHUB_RUN_ID
|
||||
Write-Host "queued $queued"
|
||||
|
||||
$deadline = (Get-Date).AddMinutes(10)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path "$outPath.done") { break }
|
||||
if (Test-Path 'C:\signing\SIGNING_LOCKED') {
|
||||
Get-Content 'C:\signing\SIGNING_LOCKED'
|
||||
throw "The broker tripped its circuit breaker while signing this artifact."
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
if (-not (Test-Path "$outPath.done")) { throw "Timed out waiting for the signing broker." }
|
||||
|
||||
Move-Item $outPath 'stage\wowlet.exe' -Force
|
||||
Remove-Item "$outPath.done" -Force -ErrorAction SilentlyContinue
|
||||
|
||||
- name: verify the published binary is signed and timestamped
|
||||
# The broker verifies before releasing; this verifies again on the
|
||||
# artifact that is actually about to ship. A signature or timestamp
|
||||
# failure blocks distribution.
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sig = Get-AuthenticodeSignature 'stage\wowlet.exe'
|
||||
$sig | Format-List Status, StatusMessage,
|
||||
@{n='Signer';e={$_.SignerCertificate.Subject}},
|
||||
@{n='TimeStamper';e={$_.TimeStamperCertificate.Subject}}
|
||||
if ($sig.Status -ne 'Valid') { throw "Authenticode status is $($sig.Status)" }
|
||||
if (-not $sig.TimeStamperCertificate) { throw "Signature carries no RFC 3161 timestamp" }
|
||||
if ($sig.SignerCertificate.Subject -notmatch 'Such Software LLC') {
|
||||
throw "Unexpected signer: $($sig.SignerCertificate.Subject)"
|
||||
}
|
||||
Write-Host "SHA256 $((Get-FileHash 'stage\wowlet.exe' -Algorithm SHA256).Hash)"
|
||||
|
||||
- name: package both zips
|
||||
# Two archives ship, deliberately. Authenticode mutates the exe, so a
|
||||
# signed binary can never byte-match what someone reproduces from
|
||||
# source. The unsigned archive stays reproducible and is what verifiers
|
||||
# check; the signed one is what a normal user should download. Both
|
||||
# hashes go in the PGP-signed hashes.txt.
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
|
||||
# Not Compress-Archive: Windows PowerShell writes the OS separator
|
||||
# into entry names, producing "tor\tor.exe". The ZIP format mandates
|
||||
# forward slashes, and extractors that take the backslash literally
|
||||
# leave a file called "tor\tor.exe" in the root instead of a tor
|
||||
# directory -- at which point the bundled Tor is not where wowlet
|
||||
# looks for it, and this wallet gives its daemon an exclusive
|
||||
# --tx-proxy, so no Tor means no transaction relay at all.
|
||||
# v5.1.0 shipped tor/tor.exe from `zip -X`; keep that shape.
|
||||
function New-Archive([string] $Source, [string] $Destination) {
|
||||
if (Test-Path $Destination) { Remove-Item $Destination -Force }
|
||||
$root = (Resolve-Path $Source).Path.TrimEnd('\')
|
||||
$zip = [System.IO.Compression.ZipFile]::Open($Destination, 'Create')
|
||||
try {
|
||||
foreach ($file in Get-ChildItem $root -Recurse -File | Sort-Object FullName) {
|
||||
$entry = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
|
||||
[void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
|
||||
$zip, $file.FullName, $entry, 'Optimal')
|
||||
}
|
||||
} finally { $zip.Dispose() }
|
||||
}
|
||||
|
||||
New-Archive 'unsigned' 'wowlet-x86_64-windows-unsigned.zip'
|
||||
New-Archive 'stage' 'wowlet-x86_64-windows.zip'
|
||||
|
||||
# Prove the shape rather than trusting it: a backslash in any entry
|
||||
# name fails the build.
|
||||
foreach ($archive in @('wowlet-x86_64-windows.zip', 'wowlet-x86_64-windows-unsigned.zip')) {
|
||||
$zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $archive))
|
||||
try {
|
||||
$names = $zip.Entries | ForEach-Object { $_.FullName }
|
||||
if ($names -match '\\') { throw "$archive contains a backslash entry name: $($names -join ', ')" }
|
||||
if ($names -notcontains 'tor/tor.exe') { throw "$archive is missing tor/tor.exe (got: $($names -join ', '))" }
|
||||
} finally { $zip.Dispose() }
|
||||
"{0} {1} {2}" -f $archive, (Get-Item $archive).Length,
|
||||
(Get-FileHash $archive -Algorithm SHA256).Hash
|
||||
}
|
||||
|
||||
- name: upload Windows zips
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wowlet-x86_64-windows
|
||||
path: |
|
||||
wowlet-x86_64-windows.zip
|
||||
wowlet-x86_64-windows-unsigned.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: publish the Windows zips to the tag's release
|
||||
# Same reason as the Linux and macOS jobs: artifacts upload fine, but
|
||||
# this Gitea's REST listing reports zero for every run, so the signed exe
|
||||
# cannot be fetched back through the API. Release assets can be.
|
||||
#
|
||||
# This runs after the Authenticode, timestamp and signer checks above, so
|
||||
# an unsigned or untimestamped binary never reaches a release. curl.exe,
|
||||
# not Invoke-WebRequest: `shell: powershell` is Windows PowerShell 5.1,
|
||||
# whose Invoke-WebRequest has no -Form for multipart uploads.
|
||||
#
|
||||
# Fail-safe: a build that produced a correctly signed binary must not go
|
||||
# red because publishing failed, so every bad path reports and exits 0.
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
shell: powershell
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Continue'
|
||||
if (-not $env:RELEASE_TOKEN) {
|
||||
Write-Host "no token available to this runner, skipping the release upload"
|
||||
exit 0
|
||||
}
|
||||
$tag = $env:GITHUB_REF -replace '^refs/tags/', ''
|
||||
$server = if ($env:GITHUB_SERVER_URL) { $env:GITHUB_SERVER_URL } else { 'https://git.such.software' }
|
||||
$api = "$server/api/v1/repos/$env:GITHUB_REPOSITORY"
|
||||
$auth = "Authorization: token $env:RELEASE_TOKEN"
|
||||
$rid = ''
|
||||
try { $rid = ((curl.exe -sS -H $auth "$api/releases/tags/$tag") | ConvertFrom-Json).id } catch { $rid = '' }
|
||||
if (-not $rid) {
|
||||
try {
|
||||
$body = "{""tag_name"":""$tag"",""name"":""wowlet $tag""}"
|
||||
$rid = ((curl.exe -sS -H $auth -H "Content-Type: application/json" -X POST -d $body "$api/releases") | ConvertFrom-Json).id
|
||||
} catch { $rid = '' }
|
||||
}
|
||||
if (-not $rid) {
|
||||
Write-Host "could not resolve or create a release for $tag, skipping the upload"
|
||||
exit 0
|
||||
}
|
||||
foreach ($f in @('wowlet-x86_64-windows.zip', 'wowlet-x86_64-windows-unsigned.zip')) {
|
||||
if (-not (Test-Path $f)) { Write-Host "missing $f, nothing to publish for it"; continue }
|
||||
$code = curl.exe -sS -o NUL -w '%{http_code}' -H $auth -F "attachment=@$f" "$api/releases/$rid/assets?name=$f"
|
||||
Write-Host "release asset upload for $f returned HTTP $code"
|
||||
}
|
||||
@@ -14,9 +14,11 @@ feather_autogen/
|
||||
feather.cbp
|
||||
src/assets_tor.qrc
|
||||
src/assets_docs.qrc
|
||||
src/assets_wownerod.qrc
|
||||
feather.AppDir/*
|
||||
src/assets/tor/*
|
||||
src/assets/docs/*
|
||||
src/assets/wownerod/*
|
||||
!src/assets/tor/.gitkeep
|
||||
!src/assets/docs/.gitkeep
|
||||
contrib/installers/windows/setup.nsi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
|
||||
project(feather
|
||||
VERSION "5.1.0"
|
||||
VERSION "5.1.4"
|
||||
DESCRIPTION "A free Monero desktop wallet"
|
||||
LANGUAGES CXX C ASM
|
||||
)
|
||||
|
||||
@@ -5,4 +5,4 @@ echo $HERE
|
||||
|
||||
# Tails 5.8 sets QT_QPA_PLATFORM to wayland which breaks our app
|
||||
export QT_QPA_PLATFORM=xcb
|
||||
${APPDIR}/usr/bin/feather $*
|
||||
${APPDIR}/usr/bin/wowlet $*
|
||||
|
||||
@@ -5,29 +5,29 @@ unset SOURCE_DATE_EPOCH
|
||||
|
||||
# Manually create the AppImage (reproducibly) since linuxdeployqt is not able to create cross-compiled AppImages
|
||||
|
||||
APPDIR="$PWD/feather.AppDir"
|
||||
APPDIR="$PWD/wowlet.AppDir"
|
||||
|
||||
mkdir -p "$APPDIR"
|
||||
mkdir -p "$APPDIR/usr/share/applications/"
|
||||
mkdir -p "$APPDIR/usr/bin"
|
||||
|
||||
cp "src/assets/feather.desktop" "$APPDIR/usr/share/applications/feather.desktop"
|
||||
cp "src/assets/feather.desktop" "$APPDIR/feather.desktop"
|
||||
cp "src/assets/images/appicons/64x64.png" "$APPDIR/feather.png"
|
||||
cp "build/bin/feather" "$APPDIR/usr/bin/feather"
|
||||
chmod +x "$APPDIR/usr/bin/feather"
|
||||
cp "src/assets/wowlet.desktop" "$APPDIR/usr/share/applications/wowlet.desktop"
|
||||
cp "src/assets/wowlet.desktop" "$APPDIR/wowlet.desktop"
|
||||
cp "src/assets/images/appicons/64x64.png" "$APPDIR/wowlet.png"
|
||||
cp "build/bin/wowlet" "$APPDIR/usr/bin/wowlet"
|
||||
chmod +x "$APPDIR/usr/bin/wowlet"
|
||||
|
||||
cp "contrib/AppImage/AppRun" "$APPDIR/"
|
||||
chmod +x "$APPDIR/AppRun"
|
||||
|
||||
find feather.AppDir/ -exec touch -h -a -m -t 202101010100.00 {} \;
|
||||
find wowlet.AppDir/ -exec touch -h -a -m -t 202101010100.00 {} \;
|
||||
|
||||
mksquashfs feather.AppDir feather.squashfs -comp zstd -info -root-owned -no-xattrs -noappend -fstime 0
|
||||
mksquashfs wowlet.AppDir wowlet.squashfs -comp zstd -info -root-owned -no-xattrs -noappend -fstime 0
|
||||
# mksquashfs writes a timestamp to the header
|
||||
printf '\x00\x00\x00\x00' | dd conv=notrunc of=feather.squashfs bs=1 seek=$((0x8))
|
||||
printf '\x00\x00\x00\x00' | dd conv=notrunc of=wowlet.squashfs bs=1 seek=$((0x8))
|
||||
|
||||
rm -f feather.AppImage
|
||||
rm -f wowlet.AppImage
|
||||
|
||||
cat "${GITHUB_WORKSPACE:-/feather}/contrib/depends/${HOST}/runtime" >> feather.AppImage
|
||||
cat feather.squashfs >> feather.AppImage
|
||||
chmod a+x feather.AppImage
|
||||
cat "${GITHUB_WORKSPACE:-/feather}/contrib/depends/${HOST}/runtime" >> wowlet.AppImage
|
||||
cat wowlet.squashfs >> wowlet.AppImage
|
||||
chmod a+x wowlet.AppImage
|
||||
|
||||
@@ -43,6 +43,15 @@ SET(absl_ROOT @prefix@)
|
||||
SET(NATIVE_BIN_PATH @prefix@/native/bin CACHE FILEPATH "path to native binaries" FORCE)
|
||||
SET(LRELEASE_PATH @prefix@/native/bin CACHE FILEPATH "path to lrelease" FORCE)
|
||||
|
||||
# A cross Qt that carries qtdeclarative makes find_package(Qt6) demand a HOST Qt,
|
||||
# because QML needs host-side tools (qmlcachegen, qmlimportscanner,
|
||||
# qmltyperegistrar). depends already builds one: native_qt installs to
|
||||
# native/qt-host and qt.mk hands that path to the cross Qt's own configure. Our
|
||||
# build never heard about it, so Qt6Config refused with "please set the
|
||||
# QT_HOST_PATH cache variable" and the Windows cross build has failed since
|
||||
# qtdeclarative landed. Same path, stated once more for the consuming project.
|
||||
SET(QT_HOST_PATH @prefix@/native/qt-host CACHE PATH "host Qt for cross QML tooling")
|
||||
|
||||
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Android")
|
||||
SET(LIBUNWIND_INCLUDE_DIR @prefix@/include)
|
||||
SET(LIBUNWIND_LIBRARIES @prefix@/lib/libunwind.a)
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
<string>????</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>feather</string>
|
||||
<string>wowlet</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>Feather</string>
|
||||
<string>Wowlet</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.monero-project.feather</string>
|
||||
<string>org.wownero.wowlet</string>
|
||||
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Camera is used to scan QR codes on demand.</string>
|
||||
|
||||
@@ -34,7 +34,7 @@ if (CHECK_UPDATES)
|
||||
add_subdirectory(openpgp)
|
||||
endif()
|
||||
|
||||
qt_add_resources(RESOURCES assets.qrc assets_tor.qrc assets_wownerod.qrc assets_docs.qrc mining.qrc)
|
||||
qt_add_resources(RESOURCES assets.qrc assets_tor.qrc assets_wownerod.qrc assets_docs.qrc mining.qrc silly.qrc scene.qrc)
|
||||
|
||||
# Compile source files (.h/.cpp)
|
||||
file(GLOB SOURCE_FILES
|
||||
@@ -148,7 +148,10 @@ add_executable(feather ${EXECUTABLE_FLAG} main.cpp
|
||||
${ASSETS_TOR}
|
||||
)
|
||||
|
||||
# The CMake target keeps its upstream name so the target_* calls below stay put;
|
||||
# only what we ship is renamed.
|
||||
set_target_properties(feather PROPERTIES
|
||||
OUTPUT_NAME wowlet
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
||||
LINK_FLAGS_RELEASE -s
|
||||
)
|
||||
@@ -370,14 +373,14 @@ else()
|
||||
)
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/feather.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/32x32.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/32x32/apps RENAME "feather.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/48x48.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/48x48/apps RENAME "feather.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/64x64.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/64x64/apps RENAME "feather.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/96x96.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/96x96/apps RENAME "feather.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/128x128.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/128x128/apps RENAME "feather.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/256x256.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/256x256/apps RENAME "feather.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/512x512.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/512x512/apps RENAME "feather.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/wowlet.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/32x32.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/32x32/apps RENAME "wowlet.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/48x48.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/48x48/apps RENAME "wowlet.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/64x64.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/64x64/apps RENAME "wowlet.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/96x96.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/96x96/apps RENAME "wowlet.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/128x128.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/128x128/apps RENAME "wowlet.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/256x256.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/256x256/apps RENAME "wowlet.png")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/512x512.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/512x512/apps RENAME "wowlet.png")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -385,13 +388,14 @@ if(APPLE)
|
||||
configure_file(${CMAKE_SOURCE_DIR}/contrib/macdeploy/Info.plist.in ${CMAKE_SOURCE_DIR}/contrib/macdeploy/Info.plist @ONLY)
|
||||
|
||||
set_target_properties(feather PROPERTIES
|
||||
OUTPUT_NAME wowlet
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/contrib/macdeploy/Info.plist"
|
||||
LINK_FLAGS_RELEASE -s
|
||||
)
|
||||
|
||||
file(COPY "${CMAKE_SOURCE_DIR}/src/assets/images/appicons/appicon.icns" DESTINATION "${CMAKE_SOURCE_DIR}/installed/feather.app/Contents/Resources/" )
|
||||
file(COPY "${CMAKE_SOURCE_DIR}/src/assets/images/appicons/appicon.icns" DESTINATION "${CMAKE_SOURCE_DIR}/installed/wowlet.app/Contents/Resources/" )
|
||||
endif()
|
||||
|
||||
qt_finalize_executable(feather)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "utils/config.h"
|
||||
#include "utils/Icons.h"
|
||||
#include "WebsocketNotifier.h"
|
||||
#include "widgets/SceneWidget.h"
|
||||
|
||||
HistoryWidget::HistoryWidget(Wallet *wallet, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
@@ -24,6 +25,13 @@ HistoryWidget::HistoryWidget(Wallet *wallet, QWidget *parent)
|
||||
, m_model(wallet->historyModel())
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// wowlet: History is "home" — the full ambient scene as a top banner.
|
||||
auto *scene = new SceneWidget(this);
|
||||
scene->setMinimumHeight(120);
|
||||
scene->setMaximumHeight(150);
|
||||
ui->verticalLayout->insertWidget(0, scene);
|
||||
|
||||
m_contextMenu->addMenu(m_copyMenu);
|
||||
m_contextMenu->addAction(icons()->icon("info2.svg"), "Show details", this, &HistoryWidget::showTxDetails);
|
||||
m_contextMenu->addAction("View on block explorer", this, &HistoryWidget::onViewOnBlockExplorer);
|
||||
|
||||
@@ -252,7 +252,7 @@ void MainWindow::initWidgets() {
|
||||
});
|
||||
|
||||
// [Mining] — wowlet: the pixel-art mining tab (mining.qml + embedded-node solo miner)
|
||||
m_miningWidget = new MiningWidget(m_wallet, this);
|
||||
m_miningWidget = new MiningWidget(m_wallet, m_nodes, this);
|
||||
ui->tabWidget->addTab(m_miningWidget, "Mining");
|
||||
|
||||
// [Notes]
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "utils/config.h"
|
||||
#include "utils/Icons.h"
|
||||
#include "utils/Utils.h"
|
||||
#include "widgets/SceneWidget.h"
|
||||
|
||||
ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
@@ -20,6 +21,13 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// wowlet: ambient scene as a short banner under the address table. A layout
|
||||
// item, never an overlay over the table.
|
||||
auto *scene = new SceneWidget(this, SceneWidget::Dawn, SceneWidget::Sit);
|
||||
scene->setMinimumHeight(110);
|
||||
scene->setMaximumHeight(130);
|
||||
ui->verticalLayout->addWidget(scene);
|
||||
|
||||
m_model = m_wallet->subaddressModel();
|
||||
m_proxyModel = new SubaddressProxyModel(this, m_wallet->subaddress());
|
||||
m_proxyModel->setSourceModel(m_model);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "Icons.h"
|
||||
#include "libwalletqt/Wallet.h"
|
||||
#include "libwalletqt/WalletManager.h"
|
||||
#include "widgets/SceneWidget.h"
|
||||
|
||||
#if defined(WITH_SCANNER)
|
||||
#include "wizard/offline_tx_signing/OfflineTxSigningWizard.h"
|
||||
@@ -25,6 +26,21 @@ SendWidget::SendWidget(Wallet *wallet, QWidget *parent)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// wowlet: the form only needs the top of the tab — give the rest of it to
|
||||
// the ambient scene. A layout item under the form, never an overlay: the
|
||||
// send form itself stays untouched.
|
||||
//
|
||||
// Centred in the space below the form rather than stacked straight under
|
||||
// it: butted up against the Clear/Send row the scene reads as part of the
|
||||
// form, and a band of night sky touching a button looks like a mistake.
|
||||
auto *scene = new SceneWidget(this, SceneWidget::Night, SceneWidget::Money);
|
||||
scene->setMinimumHeight(160);
|
||||
scene->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
ui->verticalLayoutOuter->addSpacing(14); // never touches the Send row
|
||||
ui->verticalLayoutOuter->addStretch(1);
|
||||
ui->verticalLayoutOuter->addWidget(scene);
|
||||
ui->verticalLayoutOuter->addStretch(1);
|
||||
|
||||
QString amount_rx = R"(^\d{0,8}[\.,]\d{0,12}|(all)$)";
|
||||
QRegularExpression rx;
|
||||
rx.setPattern(amount_rx);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -19,7 +19,24 @@
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<layout class="QVBoxLayout" name="verticalLayoutOuter">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SizeConstraint::SetMinimumSize</enum>
|
||||
</property>
|
||||
@@ -274,6 +291,8 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
|
||||
@@ -139,6 +139,12 @@ void Settings::setupAppearanceTab() {
|
||||
emit updateBalance();
|
||||
});
|
||||
|
||||
// [Prefer reduced motion] wowlet: gate playful animations (mining tab, loaders, ...)
|
||||
ui->checkBox_reducedMotion->setChecked(conf()->get(Config::reducedMotion).toBool());
|
||||
connect(ui->checkBox_reducedMotion, &QCheckBox::toggled, [this](bool toggled){
|
||||
conf()->set(Config::reducedMotion, toggled);
|
||||
});
|
||||
|
||||
// [Preferred fiat currency]
|
||||
QStringList availableFiatCurrencies = appData()->prices.rates.keys();
|
||||
for (const auto ¤cy : availableFiatCurrencies) {
|
||||
|
||||
@@ -159,6 +159,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_reducedMotion">
|
||||
<property name="text">
|
||||
<string>Prefer reduced motion (disable playful animations)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -35,6 +35,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
playing: !mining.reducedMotion // wowlet: respect reduced-motion
|
||||
//visible: mining.daemonMiningState === 0
|
||||
source: "qrc:/mining/bg1.gif"
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
@@ -91,7 +92,9 @@ Rectangle {
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: false
|
||||
Layout.preferredHeight: 128
|
||||
Layout.maximumHeight: 128
|
||||
|
||||
Image {
|
||||
source: "qrc:/mining/topleft.png"
|
||||
@@ -195,6 +198,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
playing: !mining.reducedMotion // wowlet: respect reduced-motion
|
||||
visible: mining.daemonMiningState !== 0
|
||||
source: "qrc:/mining/mining.webp"
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
@@ -210,7 +214,9 @@ Rectangle {
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: false
|
||||
Layout.preferredHeight: 128
|
||||
Layout.maximumHeight: 128
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
@@ -231,128 +237,98 @@ Rectangle {
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
Layout.fillHeight: true
|
||||
// Pinned: with fillHeight the console absorbed any slack in the
|
||||
// top bar, stretching the bezel and dropping the metrics out of
|
||||
// the bottom of the panel.
|
||||
Layout.fillHeight: false
|
||||
Layout.preferredHeight: 102
|
||||
Layout.minimumHeight: 102
|
||||
Layout.maximumHeight: 102
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 5
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredHeight: 102 // console bezel is a fixed 102px tall
|
||||
source: "qrc:/mining/topright_left.png"
|
||||
smooth: false
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredHeight: 102 // console bezel is a fixed 102px tall
|
||||
source: "qrc:/mining/topright_middle.png"
|
||||
fillMode: Image.TileHorizontally
|
||||
smooth: false
|
||||
|
||||
RowLayout {
|
||||
// The node metrics live inside the console's dark interior
|
||||
// band (rows 7..94 of the 102px topright_middle bezel).
|
||||
// Anchored at a fixed offset with fixed row heights so a
|
||||
// long value (a full block height) can never be pushed
|
||||
// out of the bottom of the console.
|
||||
Row {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 16
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 6
|
||||
anchors.leftMargin: 14
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
anchors.topMargin: 4
|
||||
height: 52
|
||||
spacing: 24
|
||||
|
||||
height: 60
|
||||
spacing: 16
|
||||
Column {
|
||||
width: Math.min(260, (parent.width - parent.spacing) / 2)
|
||||
spacing: 2
|
||||
|
||||
ColumnLayout {
|
||||
Layout.minimumWidth: 200
|
||||
Layout.maximumWidth: 260
|
||||
Layout.fillHeight: true
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
text: "Block Height"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 8
|
||||
font.pointSize: 14
|
||||
font.family: comicMonoBold.name;
|
||||
color: "#41FF00"
|
||||
antialiasing: false
|
||||
}
|
||||
Text {
|
||||
text: "Block Height"
|
||||
font.pointSize: 14
|
||||
font.family: comicMonoBold.name;
|
||||
color: "#41FF00"
|
||||
antialiasing: false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
id: heightText
|
||||
text: "-"
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
fontSizeMode: Text.HorizontalFit
|
||||
minimumPointSize: 9
|
||||
font.pointSize: 14
|
||||
font.family: comicMonoBold.name;
|
||||
color: "#41FF00"
|
||||
antialiasing: false
|
||||
}
|
||||
Text {
|
||||
id: heightText
|
||||
text: "-"
|
||||
width: parent.width
|
||||
fontSizeMode: Text.HorizontalFit
|
||||
minimumPointSize: 9
|
||||
font.pointSize: 14
|
||||
font.family: comicMonoBold.name;
|
||||
color: "#41FF00"
|
||||
antialiasing: false
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.minimumWidth: 200
|
||||
Layout.maximumWidth: 260
|
||||
Layout.fillHeight: true
|
||||
Column {
|
||||
width: Math.min(260, (parent.width - parent.spacing) / 2)
|
||||
spacing: 2
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
text: "Sync Progress"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 8
|
||||
font.pointSize: 14
|
||||
font.family: comicMonoBold.name;
|
||||
color: "#41FF00"
|
||||
antialiasing: false
|
||||
}
|
||||
Text {
|
||||
text: "Sync Progress"
|
||||
font.pointSize: 14
|
||||
font.family: comicMonoBold.name;
|
||||
color: "#41FF00"
|
||||
antialiasing: false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
id: syncPctText
|
||||
text: "-"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 8
|
||||
font.pointSize: 14
|
||||
font.family: comicMonoBold.name;
|
||||
color: "#41FF00"
|
||||
antialiasing: false
|
||||
}
|
||||
Text {
|
||||
id: syncPctText
|
||||
text: "-"
|
||||
width: parent.width
|
||||
fontSizeMode: Text.HorizontalFit
|
||||
minimumPointSize: 9
|
||||
font.pointSize: 14
|
||||
font.family: comicMonoBold.name;
|
||||
color: "#41FF00"
|
||||
antialiasing: false
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 5
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredHeight: 102 // console bezel is a fixed 102px tall
|
||||
source: "qrc:/mining/topright_right.png"
|
||||
smooth: false
|
||||
}
|
||||
@@ -434,6 +410,7 @@ Rectangle {
|
||||
smooth: false
|
||||
|
||||
AnimatedImage {
|
||||
playing: !mining.reducedMotion // wowlet: respect reduced-motion
|
||||
source: mining.daemonMiningState === 0 ? "qrc:/assets/images/dog_sitting.gif" : "qrc:/assets/images/dog_running.gif"
|
||||
width: 80
|
||||
height: 60
|
||||
@@ -535,6 +512,10 @@ Rectangle {
|
||||
Image {
|
||||
id: imgAxe
|
||||
visible: mining.daemonMiningState === 0
|
||||
// Greyed out while the wallet is on someone else's node:
|
||||
// solo mining is an RPC to the daemon we are connected
|
||||
// to, and that daemon has to be ours.
|
||||
opacity: mining.canMine ? 1.0 : 0.35
|
||||
source: "qrc:/mining/axe.png"
|
||||
width: 73
|
||||
height: 75
|
||||
@@ -544,6 +525,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
playing: !mining.reducedMotion // wowlet: respect reduced-motion
|
||||
visible: mining.daemonMiningState !== 0
|
||||
source: "qrc:/mining/elmo.gif"
|
||||
width: 106
|
||||
@@ -573,7 +555,11 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if(mining.daemonMiningState === 0) {
|
||||
if (mining.daemonMiningState === 0) {
|
||||
if (!mining.canMine) {
|
||||
remoteNodeNotice.flash();
|
||||
return;
|
||||
}
|
||||
root.startMining();
|
||||
root.calcAvailableHeightConsoleLines();
|
||||
}
|
||||
@@ -581,14 +567,56 @@ Rectangle {
|
||||
root.stopMining();
|
||||
}
|
||||
onEntered: {
|
||||
imgAxe.height = 64
|
||||
imgAxe.width = 64
|
||||
if (mining.canMine) {
|
||||
imgAxe.height = 64
|
||||
imgAxe.width = 64
|
||||
} else {
|
||||
remoteNodeNotice.show();
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
imgAxe.height = 75
|
||||
imgAxe.width = 73
|
||||
remoteNodeNotice.hideSoon();
|
||||
}
|
||||
}
|
||||
|
||||
// Why the axe is greyed out. Shown on hover, and flashed if
|
||||
// the axe is clicked anyway.
|
||||
Rectangle {
|
||||
id: remoteNodeNotice
|
||||
z: parent.z + 5
|
||||
visible: opacity > 0.01
|
||||
opacity: 0.0
|
||||
width: 250
|
||||
height: noticeText.implicitHeight + 16
|
||||
radius: 6
|
||||
color: "#231a10"
|
||||
border.color: "#e0b062"
|
||||
border.width: 1
|
||||
anchors.right: parent.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
id: noticeText
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 16
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#f2dcb4"
|
||||
font.pointSize: 9
|
||||
text: "Mining needs your own node.\nYou are connected to a remote one — " +
|
||||
"start the built-in node or point the wallet at a local daemon " +
|
||||
"under Settings to mine."
|
||||
}
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 160 } }
|
||||
Timer { id: noticeHide; interval: 2600; onTriggered: remoteNodeNotice.opacity = 0.0 }
|
||||
function show() { noticeHide.stop(); opacity = 1.0 }
|
||||
function hideSoon() { noticeHide.restart() }
|
||||
function flash() { show(); noticeHide.restart() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
#include "SplashDialog.h"
|
||||
#include "ui_SplashDialog.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QMovie>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include "utils/Icons.h"
|
||||
|
||||
SplashDialog::SplashDialog(QWidget *parent)
|
||||
@@ -14,8 +18,25 @@ SplashDialog::SplashDialog(QWidget *parent)
|
||||
|
||||
this->setWindowIcon(icons()->icon("appicon/64x64"));
|
||||
|
||||
QPixmap pixmap = QPixmap(":/assets/images/key.png");
|
||||
ui->icon->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation));
|
||||
// wowlet: a random silly loading GIF (wow-lite-wallet tradition) instead of
|
||||
// a static key icon. Auto-discovers whatever loaders are bundled under
|
||||
// :/silly so the set can grow without touching this code; falls back to the
|
||||
// key pixmap if none are present.
|
||||
const QStringList loaders = QDir(":/silly").entryList(QStringList() << "*.gif", QDir::Files);
|
||||
if (!loaders.isEmpty()) {
|
||||
const QString pick = ":/silly/" + loaders.at(QRandomGenerator::global()->bounded(loaders.size()));
|
||||
auto *movie = new QMovie(pick, QByteArray(), this);
|
||||
movie->jumpToFrame(0);
|
||||
const QSize frame = movie->currentImage().size();
|
||||
const int h = 56;
|
||||
if (frame.height() > 0)
|
||||
movie->setScaledSize(QSize(frame.width() * h / frame.height(), h));
|
||||
ui->icon->setMovie(movie);
|
||||
movie->start();
|
||||
} else {
|
||||
QPixmap pixmap = QPixmap(":/assets/images/key.png");
|
||||
ui->icon->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
this->setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE RCC><RCC version="1.0">
|
||||
<qresource prefix="/">
|
||||
<file alias="qml/scene.qml">assets/qml/scene.qml</file>
|
||||
<file alias="scene/dog2.gif">assets/images/scene/dog2.gif</file>
|
||||
<file alias="scene/dog.gif">assets/images/scene/dog.gif</file>
|
||||
<file alias="scene/flyingmoney.gif">assets/images/scene/flyingmoney.gif</file>
|
||||
<file alias="scene/goldcoin.gif">assets/images/scene/goldcoin.gif</file>
|
||||
<file alias="scene/grass.png">assets/images/scene/grass.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE RCC><RCC version="1.0">
|
||||
<qresource prefix="/">
|
||||
<file alias="silly/l11.gif">assets/images/silly/l11.gif</file>
|
||||
<file alias="silly/l12.gif">assets/images/silly/l12.gif</file>
|
||||
<file alias="silly/l19.gif">assets/images/silly/l19.gif</file>
|
||||
<file alias="silly/l1.gif">assets/images/silly/l1.gif</file>
|
||||
<file alias="silly/l21.gif">assets/images/silly/l21.gif</file>
|
||||
<file alias="silly/l22.gif">assets/images/silly/l22.gif</file>
|
||||
<file alias="silly/l26.gif">assets/images/silly/l26.gif</file>
|
||||
<file alias="silly/l27.gif">assets/images/silly/l27.gif</file>
|
||||
<file alias="silly/l3.gif">assets/images/silly/l3.gif</file>
|
||||
<file alias="silly/l5.gif">assets/images/silly/l5.gif</file>
|
||||
<file alias="silly/l6.gif">assets/images/silly/l6.gif</file>
|
||||
<file alias="silly/l7.gif">assets/images/silly/l7.gif</file>
|
||||
<file alias="silly/loading.gif">assets/images/silly/loading.gif</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -30,6 +30,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
||||
{Config::homeWidget,{QS("homeWidget"), "ccs"}},
|
||||
{Config::donateBeg,{QS("donateBeg"), 1}},
|
||||
{Config::showHistorySyncNotice, {QS("showHistorySyncNotice"), true}},
|
||||
{Config::reducedMotion, {QS("reducedMotion"), false}},
|
||||
|
||||
{Config::geometry, {QS("geometry"), {}}},
|
||||
{Config::windowState, {QS("windowState"), {}}},
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
homeWidget,
|
||||
donateBeg,
|
||||
showHistorySyncNotice,
|
||||
reducedMotion, // wowlet: disable playful animations (accessibility / preference)
|
||||
|
||||
geometry,
|
||||
windowState,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "MiningWidget.h"
|
||||
|
||||
#include "utils/nodes.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QVBoxLayout>
|
||||
@@ -11,6 +13,8 @@
|
||||
#include <QThread>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "utils/config.h"
|
||||
|
||||
static QString formatUptime(qint64 secs) {
|
||||
return QString("%1:%2:%3")
|
||||
.arg(secs / 3600, 2, 10, QChar('0'))
|
||||
@@ -18,9 +22,10 @@ static QString formatUptime(qint64 secs) {
|
||||
.arg(secs % 60, 2, 10, QChar('0'));
|
||||
}
|
||||
|
||||
MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent)
|
||||
MiningWidget::MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_wallet(wallet)
|
||||
, m_nodes(nodes)
|
||||
, m_quickWidget(new QQuickWidget(this))
|
||||
, m_backend(new MiningBackend(this))
|
||||
, m_timer(new QTimer(this))
|
||||
@@ -29,6 +34,7 @@ MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent)
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(m_quickWidget);
|
||||
|
||||
m_backend->setReducedMotion(conf()->get(Config::reducedMotion).toBool());
|
||||
m_quickWidget->rootContext()->setContextProperty("mining", m_backend);
|
||||
m_quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
m_quickWidget->setSource(QUrl("qrc:/qml/mining.qml"));
|
||||
@@ -43,6 +49,10 @@ MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent)
|
||||
}
|
||||
|
||||
void MiningWidget::onStartMining() {
|
||||
// The QML disables the control, but the signal is not a promise: refuse
|
||||
// here too rather than send start_mining to a stranger's daemon.
|
||||
if (!m_backend->canMine())
|
||||
return;
|
||||
int threads = std::max(1, QThread::idealThreadCount() / 2);
|
||||
if (m_wallet->startMining(threads))
|
||||
m_backend->setState(3);
|
||||
@@ -54,6 +64,8 @@ void MiningWidget::onStopMining() {
|
||||
}
|
||||
|
||||
void MiningWidget::poll() {
|
||||
m_backend->setCanMine(m_nodes && m_nodes->connection().isLocal());
|
||||
|
||||
bool mining = m_wallet->isMining();
|
||||
m_backend->setState(mining ? 3 : 0);
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#define WOWLET_MININGWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class Nodes;
|
||||
#include <QTimer>
|
||||
#include <QQuickWidget>
|
||||
|
||||
@@ -15,13 +17,25 @@
|
||||
class MiningBackend : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int daemonMiningState READ daemonMiningState NOTIFY daemonMiningStateChanged)
|
||||
Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
|
||||
Q_PROPERTY(bool canMine READ canMine NOTIFY canMineChanged)
|
||||
public:
|
||||
explicit MiningBackend(QObject *parent = nullptr) : QObject(parent) {}
|
||||
int daemonMiningState() const { return m_state; }
|
||||
void setState(int s) { if (m_state != s) { m_state = s; emit daemonMiningStateChanged(); } }
|
||||
bool reducedMotion() const { return m_reducedMotion; }
|
||||
void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } }
|
||||
// Solo mining is a start_mining RPC to whichever daemon the wallet is
|
||||
// connected to. Against a remote node that is either refused outright or —
|
||||
// worse, if the operator left the RPC open — it puts someone else's machine
|
||||
// to work on our address. It is only ever ours to ask of our own node.
|
||||
bool canMine() const { return m_canMine; }
|
||||
void setCanMine(bool c) { if (m_canMine != c) { m_canMine = c; emit canMineChanged(); } }
|
||||
|
||||
signals:
|
||||
void daemonMiningStateChanged();
|
||||
void reducedMotionChanged();
|
||||
void canMineChanged();
|
||||
void daemonOutput(const QString &line);
|
||||
void syncStatus(int from, int to, int pct);
|
||||
void uptimeChanged(const QString &uptime);
|
||||
@@ -29,6 +43,8 @@ signals:
|
||||
|
||||
private:
|
||||
int m_state = 0; // 0:idle 1:startup 2:syncing 3:mining
|
||||
bool m_reducedMotion = false;
|
||||
bool m_canMine = false;
|
||||
};
|
||||
|
||||
// wowlet: the pixel-art mining tab. Hosts mining.qml and wires its pick-axe to the wallet's solo
|
||||
@@ -36,7 +52,7 @@ private:
|
||||
class MiningWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MiningWidget(Wallet *wallet, QWidget *parent = nullptr);
|
||||
explicit MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onStartMining();
|
||||
@@ -45,6 +61,7 @@ private slots:
|
||||
|
||||
private:
|
||||
Wallet *m_wallet;
|
||||
Nodes *m_nodes;
|
||||
QQuickWidget *m_quickWidget;
|
||||
MiningBackend *m_backend;
|
||||
QTimer *m_timer;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// SPDX-FileCopyrightText: The Monero Project
|
||||
|
||||
#include "SceneWidget.h"
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "utils/config.h"
|
||||
|
||||
SceneWidget::SceneWidget(QWidget *parent, Variant variant, Mode mode)
|
||||
: QQuickWidget(parent)
|
||||
, m_backend(new SceneBackend(this))
|
||||
{
|
||||
m_backend->setReducedMotion(conf()->get(Config::reducedMotion).toBool());
|
||||
m_backend->setVariant(static_cast<int>(variant));
|
||||
m_backend->setMode(mode == Sit ? QStringLiteral("sit")
|
||||
: mode == Money ? QStringLiteral("money")
|
||||
: QStringLiteral("run"));
|
||||
this->rootContext()->setContextProperty("sceneCtx", m_backend);
|
||||
this->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
this->setSource(QUrl("qrc:/qml/scene.qml"));
|
||||
this->callScene("suspend");
|
||||
}
|
||||
|
||||
void SceneWidget::showEvent(QShowEvent *event) {
|
||||
QQuickWidget::showEvent(event);
|
||||
this->callScene("restart");
|
||||
}
|
||||
|
||||
void SceneWidget::hideEvent(QHideEvent *event) {
|
||||
this->callScene("suspend");
|
||||
QQuickWidget::hideEvent(event);
|
||||
}
|
||||
|
||||
void SceneWidget::resizeEvent(QResizeEvent *event) {
|
||||
QQuickWidget::resizeEvent(event);
|
||||
if (this->isVisible())
|
||||
this->callScene("restart");
|
||||
}
|
||||
|
||||
void SceneWidget::callScene(const char *method) {
|
||||
if (auto *root = this->rootObject())
|
||||
QMetaObject::invokeMethod(root, method);
|
||||
}
|
||||
|
||||
void SceneWidget::refreshReducedMotion() {
|
||||
m_backend->setReducedMotion(conf()->get(Config::reducedMotion).toBool());
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// SPDX-FileCopyrightText: The Monero Project
|
||||
|
||||
#ifndef WOWLET_SCENEWIDGET_H
|
||||
#define WOWLET_SCENEWIDGET_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QQuickWidget>
|
||||
|
||||
// Context object exposed to scene.qml as `sceneCtx`. Carries the reduced-motion
|
||||
// setting so the scene freezes into its still (reduced-motion) form, and the
|
||||
// sky variant so the tabs are not all showing the same field.
|
||||
class SceneBackend : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
|
||||
Q_PROPERTY(int variant READ variant NOTIFY variantChanged)
|
||||
Q_PROPERTY(QString mode READ mode NOTIFY modeChanged)
|
||||
public:
|
||||
explicit SceneBackend(QObject *parent = nullptr) : QObject(parent) {}
|
||||
bool reducedMotion() const { return m_reducedMotion; }
|
||||
void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } }
|
||||
int variant() const { return m_variant; }
|
||||
void setVariant(int v) { if (m_variant != v) { m_variant = v; emit variantChanged(); } }
|
||||
QString mode() const { return m_mode; }
|
||||
void setMode(const QString &m) { if (m_mode != m) { m_mode = m; emit modeChanged(); } }
|
||||
signals:
|
||||
void reducedMotionChanged();
|
||||
void variantChanged();
|
||||
void modeChanged();
|
||||
private:
|
||||
bool m_reducedMotion = false;
|
||||
int m_variant = 0;
|
||||
QString m_mode = QStringLiteral("run");
|
||||
};
|
||||
|
||||
// wowlet: reusable ambient-scene surface. Hosts scene.qml and reads the
|
||||
// reduced-motion preference from Config. Drop it behind a form, into a banner,
|
||||
// or over the window as an idle screensaver.
|
||||
class SceneWidget : public QQuickWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Sky variants, one per host, so History/Send/Receive do not all show the
|
||||
// same field. Must match the `skies` table in scene.qml.
|
||||
enum Variant { Dusk = 0, Dawn = 1, Night = 2 };
|
||||
|
||||
// How much scene a surface wants. History has room for the whole run;
|
||||
// Receive gets a doge sitting in the field; Send gets money blowing past and
|
||||
// nothing else, so the form it sits under is not competing with a dog.
|
||||
enum Mode { Run, Sit, Money };
|
||||
|
||||
explicit SceneWidget(QWidget *parent = nullptr, Variant variant = Dusk,
|
||||
Mode mode = Run);
|
||||
void refreshReducedMotion();
|
||||
|
||||
protected:
|
||||
// A scene on a hidden tab is parked rather than left running: an unshown
|
||||
// QQuickWidget is 0px wide, and a lap run against a 0px field collects every
|
||||
// coin at once and leaves its litter (+1s, barks) to surface on the real lap
|
||||
// when the tab is finally shown. Parking also keeps a background tab from
|
||||
// burning CPU on animation nobody is looking at.
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
// Belt and braces: a scene that is on screen and has just been given a real
|
||||
// size is running, whether or not a show event was the thing that got it
|
||||
// there. `restart` only sets a flag, so re-arming an already-running scene
|
||||
// costs nothing and does not interrupt its lap.
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private:
|
||||
void callScene(const char *method);
|
||||
|
||||
SceneBackend *m_backend;
|
||||
};
|
||||
|
||||
#endif //WOWLET_SCENEWIDGET_H
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "utils/Utils.h"
|
||||
#include "libwalletqt/Wallet.h"
|
||||
#include "widgets/SceneWidget.h"
|
||||
|
||||
WalletUnlockWidget::WalletUnlockWidget(QWidget *parent, Wallet *wallet)
|
||||
: QWidget(parent)
|
||||
@@ -18,6 +19,14 @@ WalletUnlockWidget::WalletUnlockWidget(QWidget *parent, Wallet *wallet)
|
||||
ui->setupUi(this);
|
||||
this->reset();
|
||||
|
||||
// wowlet: the lock screen shows the full ambient scene above the unlock form
|
||||
// (this is the idle "screensaver" — the wallet auto-locks here on inactivity).
|
||||
// It freezes into a still when "Prefer reduced motion" is set.
|
||||
auto *scene = new SceneWidget(this);
|
||||
scene->setMinimumHeight(200);
|
||||
scene->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
ui->verticalLayout->insertWidget(0, scene, 1);
|
||||
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setAutoDefault(true);
|
||||
|
||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &WalletUnlockWidget::tryUnlock);
|
||||
|
||||
@@ -27,6 +27,13 @@ PageMenu::PageMenu(WizardFields *fields, WalletKeysFilesModel *wallets, QWidget
|
||||
});
|
||||
|
||||
QString settingsSkin = conf()->get(Config::skin).toString();
|
||||
|
||||
// wowlet: reduced-motion opt-out, surfaced on first run (unchecked = playful
|
||||
// animations on, the default). Also lives in Settings → Appearance.
|
||||
ui->checkBox_reducedMotion->setChecked(conf()->get(Config::reducedMotion).toBool());
|
||||
connect(ui->checkBox_reducedMotion, &QCheckBox::toggled, [](bool toggled){
|
||||
conf()->set(Config::reducedMotion, toggled);
|
||||
});
|
||||
}
|
||||
|
||||
void PageMenu::initializePage() {
|
||||
|
||||
@@ -192,6 +192,13 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_reducedMotion">
|
||||
<property name="text">
|
||||
<string>Prefer reduced motion (disable playful animations)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
|
||||