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
View File
@@ -0,0 +1 @@
use nix
+12
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
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
View File
@@ -0,0 +1,8 @@
/target
/.idea
/.vscode
/dist
/node_modules
*.iml
local/
*.log
+14
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
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
View File
@@ -0,0 +1 @@
# TODO
+4303
View File
File diff suppressed because it is too large Load Diff
+68
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
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+16
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
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);
});
Executable
+36
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
Executable
+58
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