Migrate from personal repo

This commit is contained in:
2026-01-09 18:24:20 +01:00
commit 4696420c0a
23 changed files with 4737 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use nix

12
.gitattributes vendored Normal file
View File

@@ -0,0 +1,12 @@
res/icon/macos.icns filter=lfs diff=lfs merge=lfs -text
res/banner.png filter=lfs diff=lfs merge=lfs -text
res/icon/linux/128x128.png filter=lfs diff=lfs merge=lfs -text
res/icon/linux/16x16.png filter=lfs diff=lfs merge=lfs -text
res/icon/linux/256x256.png filter=lfs diff=lfs merge=lfs -text
res/icon filter=lfs diff=lfs merge=lfs -text
res/icon/appicon.ico filter=lfs diff=lfs merge=lfs -text
res/icon/linux filter=lfs diff=lfs merge=lfs -text
res/icon/linux/32x32.png filter=lfs diff=lfs merge=lfs -text
res/icon/linux/48x48.png filter=lfs diff=lfs merge=lfs -text
res/icon/linux/512x512.png filter=lfs diff=lfs merge=lfs -text
res/icon/linux/64x64.png filter=lfs diff=lfs merge=lfs -text

47
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Electron CI
on:
push:
branches: ['**']
pull_request:
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Check version tag
id: version_guard
if: github.ref == 'refs/heads/master'
shell: bash
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Build (no publish)
if: github.ref != 'refs/heads/master'
run: npx electron-builder --publish=never
- name: Build & publish
if: github.ref == 'refs/heads/master' && steps.version_guard.outputs.exists == 'false'
run: npx electron-builder --publish=always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish skipped
if: github.ref == 'refs/heads/master' && steps.version_guard.outputs.exists == 'true'
run: echo "Version already released — skipping publish"

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
/target
/.idea
/.vscode
/dist
/node_modules
*.iml
local/
*.log

14
.prettierrc Normal file
View File

@@ -0,0 +1,14 @@
{
"plugins": ["prettier-plugin-java"],
"printWidth":120,
"tabWidth":4,
"useTabs":false,
"semi":true,
"singleQuote":false,
"trailingComma":"all",
"bracketSpacing":true,
"arrowParens":"always",
"requirePragma":false,
"insertPragma":false,
"proseWrap":"always"
}

31
Makefile Normal file
View File

@@ -0,0 +1,31 @@
.ONESHELL:
.POSIX:
.DEFAULT_GOAL := run
ELECTRON := $(shell \
if [ -f /etc/os-release ] && grep -q '^ID=nixos' /etc/os-release; then \
echo electron; \
else \
echo "npx electron"; \
fi)
node_modules: package-lock.json
npm install
.PHONY: run
run:
$(ELECTRON) src/main.js
.PHONY: run-x
run-x:
./util/run-xorg $(ELECTRON)
.PHONY: run-wl
run-wl:
./util/run-wl $(ELECTRON)
.PHONY: fix
fix: node_modules
./node_modules/.bin/prettier --write src

1
README.md Normal file
View File

@@ -0,0 +1 @@
# TODO

4303
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

68
package.json Normal file
View File

@@ -0,0 +1,68 @@
{
"name": "imgfloat-client",
"version": "1.0.1",
"description": "Electron wrapper for the Imgfloat overlay",
"main": "src/main.js",
"build": {
"appId": "dev.kruhlmann.imgfloat",
"productName": "Imgfloat",
"files": [
"src/main.js",
"res/**"
],
"publish": {
"provider": "github",
"owner": "Kruhlmann",
"repo": "imgfloat-j"
},
"asar": false,
"directories": {
"output": "dist"
},
"linux": {
"target": [
"AppImage"
],
"category": "Utility",
"icon": "res/icon/linux"
},
"win": {
"target": [
"nsis"
],
"icon": "res/icon/appicon.ico"
},
"nsis": {
"oneClick": true,
"perMachine": true,
"allowElevation": true,
"allowToChangeInstallationDirectory": false,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Imgfloat",
"installerIcon": "res/icon/appicon.ico",
"uninstallerIcon": "res/icon/appicon.ico"
},
"mac": {
"category": "public.app-category.productivity",
"icon": "res/icon/macos.icns",
"target": [
{
"target": "dmg",
"arch": [
"arm64"
]
}
]
}
},
"dependencies": {
"electron-updater": "^6.7.3"
},
"devDependencies": {
"electron": "^35.7.5",
"electron-builder": "^24.13.3",
"prettier": "^3.7.4",
"prettier-plugin-java": "^2.7.7"
}
}

BIN
res/banner.png LFS Normal file

Binary file not shown.

BIN
res/icon/appicon.ico LFS Normal file

Binary file not shown.

BIN
res/icon/linux/128x128.png LFS Normal file

Binary file not shown.

BIN
res/icon/linux/16x16.png LFS Normal file

Binary file not shown.

BIN
res/icon/linux/256x256.png LFS Normal file

Binary file not shown.

BIN
res/icon/linux/32x32.png LFS Normal file

Binary file not shown.

BIN
res/icon/linux/48x48.png LFS Normal file

Binary file not shown.

BIN
res/icon/linux/512x512.png LFS Normal file

Binary file not shown.

BIN
res/icon/linux/64x64.png LFS Normal file

Binary file not shown.

BIN
res/icon/macos.icns LFS Normal file

Binary file not shown.

16
shell.nix Normal file
View File

@@ -0,0 +1,16 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
packages = [
pkgs.electron
pkgs.mesa
pkgs.nodejs
pkgs.openbox
pkgs.vulkan-loader
pkgs.wayland
pkgs.wayland-protocols
pkgs.weston
pkgs.xorg.xorgserver
pkgs.xwayland
];
}

112
src/main.js Normal file
View File

@@ -0,0 +1,112 @@
const path = require("node:path");
const { app, BrowserWindow } = require("electron");
const { autoUpdater } = require("electron-updater");
const initialWindowWidthPx = 960;
const initialWindowHeightPx = 640;
let canvasSizeInterval;
function clearCanvasSizeInterval() {
if (canvasSizeInterval) {
clearInterval(canvasSizeInterval);
canvasSizeInterval = undefined;
}
}
async function autoResizeWindow(win, lastSize) {
if (win.isDestroyed()) {
return lastSize;
}
const newSize = await win.webContents.executeJavaScript(`(() => {
const canvas = document.getElementById('broadcast-canvas');
if (!canvas) {
return null;
}
const rect = canvas.getBoundingClientRect();
return {
width: Math.round(rect.width),
height: Math.round(rect.height),
};
})();`);
if (!newSize?.width || !newSize?.height) {
return lastSize;
}
if (lastSize.width === newSize.width && lastSize.height === newSize.height) {
return lastSize;
}
console.info(
`Window size did not match canvas old: ${lastSize.width}x${lastSize.height} new: ${newSize.width}x${newSize.height}. Resizing.`,
);
win.setContentSize(newSize.width, newSize.height, false);
win.setResizable(false);
return newSize;
}
function onPostNavigationLoad(win, url, broadcastRect) {
url = url || win.webContents.getURL();
let pathname;
try {
pathname = new URL(url).pathname;
} catch (e) {
console.error(`Failed to parse URL: ${url}`, e);
return;
}
const isBroadcast = /\/view\/[^/]+\/broadcast\/?$/.test(pathname);
console.info(`Navigation to ${url} detected. Is broadcast: ${isBroadcast}`);
if (isBroadcast) {
clearCanvasSizeInterval();
console.info("Setting up auto-resize for broadcast window.");
canvasSizeInterval = setInterval(() => {
autoResizeWindow(win, broadcastRect).then((newSize) => {
broadcastRect = newSize;
});
}, 750);
autoResizeWindow(win, broadcastRect).then((newSize) => {
broadcastRect = newSize;
});
} else {
clearCanvasSizeInterval();
win.setSize(initialWindowWidthPx, initialWindowHeightPx, false);
}
}
function createWindow(version) {
const win = new BrowserWindow({
width: initialWindowWidthPx,
height: initialWindowHeightPx,
transparent: true,
frame: true,
backgroundColor: "#00000000",
alwaysOnTop: false,
icon: path.join(__dirname, "../resources/assets/icon/appicon.ico"),
webPreferences: { backgroundThrottling: false },
});
win.setMenu(null);
win.setTitle(`Imgfloat Client v${version}`);
return win;
}
app.whenReady().then(() => {
if (process.env.CI) {
process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
app.exit(1);
});
setTimeout(() => app.quit(), 3000);
}
autoUpdater.checkForUpdatesAndNotify();
let broadcastRect = { width: 0, height: 0 };
const version = app.getVersion();
const win = createWindow(version);
win.loadURL(process.env["IMGFLOAT_CHANNELS_URL"] || "https://imgfloat.kruhlmann.dev/channels");
win.webContents.on("did-finish-load", () => onPostNavigationLoad(win, undefined, broadcastRect));
win.webContents.on("did-navigate", (_, url) => onPostNavigationLoad(win, url, broadcastRect));
win.webContents.on("did-navigate-in-page", (_, url) => onPostNavigationLoad(win, url, broadcastRect));
win.on("page-title-updated", (e) => e.preventDefault());
win.on("closed", clearCanvasSizeInterval);
});

36
util/run-wl Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env sh
set -eu
ELECTRON="$1"
APP_ENTRY="src/main.js"
SIZE="1280x800"
cleanup() {
kill "$ELECTRON_PID" "$WESTON_PID" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
weston \
--backend=x11-backend.so \
--width="${SIZE%x*}" \
--height="${SIZE#*x}" \
--xwayland &
WESTON_PID=$!
# Wait for Weston to create its socket
for i in $(seq 1 50); do
SOCKET=$(ls "$XDG_RUNTIME_DIR"/wayland-* 2>/dev/null | head -n1 || true)
[ -n "$SOCKET" ] && break
sleep 0.1
done
[ -n "${SOCKET:-}" ] || exit 1
export WAYLAND_DISPLAY="$(basename "$SOCKET")"
export ELECTRON_OZONE_PLATFORM_HINT=wayland
export NIXOS_OZONE_WL=1
"$ELECTRON" "$APP_ENTRY" &
ELECTRON_PID=$!
wait

58
util/run-xorg Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env sh
set -eu
ELECTRON="$1"
APP_ENTRY="src/main.js"
SCREEN="1280x800"
for d in 99 98 97 96 95; do
if ! xdpyinfo -display ":$d" >/dev/null 2>&1; then
DISP="$d"
break
fi
done
[ -n "${DISP:-}" ] || {
echo "No free DISPLAY found" >&2
exit 1
}
echo "Using DISPLAY=:$DISP"
cleanup() {
kill "$ELECTRON_PID" "$OPENBOX_PID" "$XEPHYR_PID" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
Xephyr ":$DISP" -screen "$SCREEN" -resizeable -ac &
XEPHYR_PID=$!
sleep 1
DISPLAY=":$DISP" openbox &
OPENBOX_PID=$!
sleep 0.5
DISPLAY=":$DISP" "${ELECTRON}" "$APP_ENTRY" &
ELECTRON_PID=$!
DISPLAY=":$DISP" xsetroot -solid "#009999"
while :; do
if ! kill -0 "$ELECTRON_PID" 2>/dev/null; then
echo "Electron exited — killing Xephyr"
kill "$XEPHYR_PID" 2>/dev/null || true
break
fi
if ! kill -0 "$XEPHYR_PID" 2>/dev/null; then
echo "Xephyr exited — killing Electron"
kill "$ELECTRON_PID" 2>/dev/null || true
break
fi
sleep 0.5
done
wait "$ELECTRON_PID" 2>/dev/null || true
wait "$XEPHYR_PID" 2>/dev/null || true