21 Commits

Author SHA1 Message Date
78ae23519f Remove nix ci 2026-01-15 17:55:57 +01:00
0fe0691ac1 Add nix package 2026-01-15 17:49:04 +01:00
2de091567c Update icon for APPIMAGE 2026-01-15 14:32:29 +01:00
dc19fb7c14 Merge branch 'master' of github.com:imgfloat/client 2026-01-15 14:29:43 +01:00
941ac7ee19 Update domain and bump version 2026-01-15 14:29:39 +01:00
38aa8acf03 Remove API test page 2026-01-14 02:00:38 +01:00
516f991ae8 Add dev console button 2026-01-13 23:48:10 +01:00
4959965429 Bump version 2026-01-13 18:42:49 +01:00
3940f06cd0 Add fake frame 2026-01-13 18:40:36 +01:00
c2a90fdb64 Rename domain var 2026-01-13 15:10:57 +01:00
bfbc5a9fe1 Remove banner 2026-01-13 14:54:07 +01:00
c90e8cf54e Move client back to server 2026-01-13 14:51:28 +01:00
c333884cdf Ignore race condition 2026-01-13 10:44:43 +01:00
b5dda31360 Add custom domain option 2026-01-13 10:44:05 +01:00
2cd2c265a3 Merge branch 'master' of github.com:imgfloat/client 2026-01-12 18:12:01 +01:00
1c69bf461c Bump version 2026-01-12 17:17:10 +01:00
5f77890fff Migrate client code to local repo 2026-01-12 17:16:45 +01:00
1fcde694dc Add browser api test 2026-01-12 17:16:32 +01:00
c3234914c6 Update package.json 2026-01-11 16:25:33 +01:00
99d84dbda1 Upgrade to version 1.0.1 2026-01-11 15:54:28 +01:00
477c11f163 Upgrade electron and remove java dependency 2026-01-11 15:53:34 +01:00
15 changed files with 568 additions and 209 deletions

1
.gitattributes vendored
View File

@@ -10,3 +10,4 @@ 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/48x48.png filter=lfs diff=lfs merge=lfs -text
res/icon/linux/512x512.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 res/icon/linux/64x64.png filter=lfs diff=lfs merge=lfs -text
res/icon/brand.png filter=lfs diff=lfs merge=lfs -text

View File

@@ -1,5 +1,5 @@
{ {
"plugins": ["prettier-plugin-java"], "plugins": [],
"printWidth":120, "printWidth":120,
"tabWidth":4, "tabWidth":4,
"useTabs":false, "useTabs":false,

View File

@@ -15,17 +15,16 @@ node_modules: package-lock.json
.PHONY: run .PHONY: run
run: run:
$(ELECTRON) src/main.js LOCAL_DOMAIN=1 $(ELECTRON) src/main.js
.PHONY: run-x .PHONY: run-x
run-x: run-x:
./util/run-xorg $(ELECTRON) LOCAL_DOMAIN=1 ./util/run-xorg $(ELECTRON)
.PHONY: run-wl .PHONY: run-wl
run-wl: run-wl:
./util/run-wl $(ELECTRON) LOCAL_DOMAIN=1 ./util/run-wl $(ELECTRON)
.PHONY: fix .PHONY: fix
fix: node_modules fix: node_modules
./node_modules/.bin/prettier --write src ./node_modules/.bin/prettier --write src

View File

@@ -1 +1,3 @@
# TODO # Client
Electron based desktop client for viewing the imgfloat broadcast dashboard.

3
nix/default.nix Normal file
View File

@@ -0,0 +1,3 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.callPackage ./imgfloat-client.nix { }

48
nix/imgfloat-client.nix Normal file
View File

@@ -0,0 +1,48 @@
{ lib
, buildNpmPackage
, fetchFromGitHub
, makeWrapper
, electron
}:
buildNpmPackage rec {
pname = "imgfloat-client";
version = "1.0.5";
src = fetchFromGitHub {
owner = "imgfloat";
repo = "client";
rev = "v${version}";
hash = lib.fakeSha256;
};
npmDepsHash = lib.fakeSha256;
npmBuild = false;
npmFlags = [ "--ignore-scripts" ];
nativeBuildInputs = [ makeWrapper ];
buildInputs = [ electron ];
installPhase = ''
runHook preInstall
mkdir -p $out/share/imgfloat
cp -R src res package.json node_modules $out/share/imgfloat/
mkdir -p $out/bin
makeWrapper ${lib.getExe electron} $out/bin/imgfloat \
--add-flags $out/share/imgfloat/src/main.js \
--set-default ELECTRON_IS_DEV 0
runHook postInstall
'';
meta = with lib; {
description = "Electron based desktop client for viewing the imgfloat broadcast dashboard";
homepage = "https://github.com/imgfloat/client";
license = licenses.mit;
mainProgram = "imgfloat";
platforms = platforms.linux;
maintainers = [];
};
}

120
package-lock.json generated
View File

@@ -1,66 +1,21 @@
{ {
"name": "imgfloat-client", "name": "imgfloat-client",
"version": "1.0.1", "version": "1.0.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imgfloat-client", "name": "imgfloat-client",
"version": "1.0.1", "version": "1.0.5",
"dependencies": { "dependencies": {
"electron-updater": "^6.7.3" "electron-updater": "^6.7.3"
}, },
"devDependencies": { "devDependencies": {
"electron": "^35.7.5", "electron": "^39.2.7",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"prettier": "^3.7.4", "prettier": "^3.7.4"
"prettier-plugin-java": "^2.7.7"
} }
}, },
"node_modules/@chevrotain/cst-dts-gen": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/gast": "11.0.3",
"@chevrotain/types": "11.0.3",
"lodash-es": "4.17.21"
}
},
"node_modules/@chevrotain/gast": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/types": "11.0.3",
"lodash-es": "4.17.21"
}
},
"node_modules/@chevrotain/regexp-to-ast": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@chevrotain/types": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@chevrotain/utils": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@develar/schema-utils": { "node_modules/@develar/schema-utils": {
"version": "2.6.5", "version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -1282,34 +1237,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chevrotain": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
"@chevrotain/regexp-to-ast": "11.0.3",
"@chevrotain/types": "11.0.3",
"@chevrotain/utils": "11.0.3",
"lodash-es": "4.17.21"
}
},
"node_modules/chevrotain-allstar": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
"integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash-es": "^4.17.21"
},
"peerDependencies": {
"chevrotain": "^11.0.0"
}
},
"node_modules/chownr": { "node_modules/chownr": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@@ -1872,12 +1799,11 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "35.7.5", "version": "39.2.7",
"resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.7.tgz",
"integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==", "integrity": "sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^22.7.7", "@types/node": "^22.7.7",
@@ -2909,18 +2835,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/java-parser": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/java-parser/-/java-parser-3.0.1.tgz",
"integrity": "sha512-sDIR7u9b7O2JViNUxiZRhnRz7URII/eE7g2B+BmGxDeS6Ex3OYAcCyz5oh0H4LQ+hL/BS8OJTz8apMy9xtGmrQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"chevrotain": "11.0.3",
"chevrotain-allstar": "0.3.1",
"lodash": "4.17.21"
}
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -3051,13 +2965,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.defaults": { "node_modules/lodash.defaults": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@@ -3440,19 +3347,6 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/prettier-plugin-java": {
"version": "2.7.7",
"resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-2.7.7.tgz",
"integrity": "sha512-K3N2lrdKzx2FAi67E0UOTLKybX6iitAxYGuiv/emY8v6TzzGzoaKjmhaAyDKIH5iakFqdN+xUwWoauXnE2JZPA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"java-parser": "3.0.1"
},
"peerDependencies": {
"prettier": "^3.0.0"
}
},
"node_modules/process-nextick-args": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",

View File

@@ -1,13 +1,13 @@
{ {
"name": "imgfloat-client", "name": "imgfloat-client",
"version": "1.0.0", "version": "1.0.5",
"description": "Electron wrapper for the Imgfloat overlay", "description": "Electron wrapper for the Imgfloat overlay",
"main": "src/main.js", "main": "src/main.js",
"build": { "build": {
"appId": "dev.kruhlmann.imgfloat", "appId": "dev.kruhlmann.imgfloat",
"productName": "Imgfloat", "productName": "Imgfloat",
"files": [ "files": [
"src/main.js", "src/**",
"res/**" "res/**"
], ],
"publish": { "publish": {
@@ -28,10 +28,12 @@
}, },
"win": { "win": {
"target": [ "target": [
"nsis" "nsis",
"portable"
], ],
"icon": "res/icon/appicon.ico" "icon": "res/icon/appicon.ico"
}, },
"portable": { "artifactName": "Imgfloat.exe" },
"nsis": { "nsis": {
"oneClick": true, "oneClick": true,
"perMachine": true, "perMachine": true,
@@ -60,9 +62,8 @@
"electron-updater": "^6.7.3" "electron-updater": "^6.7.3"
}, },
"devDependencies": { "devDependencies": {
"electron": "^35.7.5", "electron": "^39.2.7",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"prettier": "^3.7.4", "prettier": "^3.7.4"
"prettier-plugin-java": "^2.7.7"
} }
} }

BIN
res/banner.png LFS

Binary file not shown.

BIN
res/icon/brand.png LFS Normal file

Binary file not shown.

238
src/css/index.css Normal file
View File

@@ -0,0 +1,238 @@
* {
box-sizing: border-box;
color: white;
}
:root {
--window-frame-height: 36px;
--window-control-size: 28px;
}
p {
margin: 0;
}
.hidden {
display: none !important;
}
body {
font-family: Arial, sans-serif;
background: #0f172a;
color: #e2e8f0;
margin: 0;
padding: 0;
}
.channels-body {
min-height: 100vh;
background:
radial-gradient(circle at 10% 20%, rgba(124, 58, 237, 0.16), transparent 30%),
radial-gradient(circle at 85% 10%, rgba(59, 130, 246, 0.18), transparent 28%), #0f172a;
display: flex;
align-items: center;
justify-content: center;
padding: clamp(24px, 4vw, 48px);
}
.window-frame {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: var(--window-frame-height);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
background: rgba(15, 23, 42, 0.9);
border-bottom: 1px solid rgba(30, 41, 59, 0.7);
-webkit-app-region: drag;
}
.window-frame-title {
font-size: 12px;
letter-spacing: 0.3px;
color: #cbd5f5;
text-transform: uppercase;
}
.window-frame-controls {
display: flex;
align-items: center;
gap: 6px;
-webkit-app-region: no-drag;
}
.window-control {
width: var(--window-control-size);
height: var(--window-control-size);
padding: 0;
border-radius: 6px;
background: rgba(148, 163, 184, 0.2);
border: 1px solid rgba(148, 163, 184, 0.25);
color: #e2e8f0;
font-size: 14px;
line-height: 1;
display: grid;
place-items: center;
box-shadow: none;
}
.window-control:hover {
background: rgba(148, 163, 184, 0.35);
}
.window-control:active {
transform: none;
}
.window-control-close {
background: rgba(239, 68, 68, 0.3);
border-color: rgba(239, 68, 68, 0.4);
}
.window-control-close:hover {
background: rgba(239, 68, 68, 0.5);
}
.channels-shell {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
max-width: 700px;
}
.channels-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.channels-main {
display: flex;
justify-content: center;
}
.channel-card {
width: 100%;
background: rgba(11, 18, 32, 0.95);
border: 1px solid #1f2937;
border-radius: 16px;
padding: clamp(20px, 3vw, 32px);
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.45);
display: flex;
flex-direction: column;
gap: 10px;
}
.channel-card h1 {
margin: 6px 0 4px;
}
.channel-form {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 6px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand-mark {
width: 40px;
height: 40px;
display: grid;
place-items: center;
font-weight: 700;
letter-spacing: 0.5px;
}
.brand-title {
font-weight: 700;
font-size: 18px;
}
.brand-subtitle {
color: #94a3b8;
font-size: 13px;
}
.text-input {
width: 100%;
padding: 12px;
border-radius: 10px;
border: 1px solid #1f2937;
background: #0f172a;
color: #e2e8f0;
font-size: 15px;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.text-input:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.25);
}
.text-input:disabled,
.text-input[aria-disabled="true"] {
background: #020617;
border-color: #334155;
color: #64748b;
cursor: not-allowed;
box-shadow: none;
}
.text-input:disabled::placeholder {
color: #475569;
}
.button,
button {
background: #7c3aed;
color: white;
padding: 10px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-weight: 600;
box-shadow: 0 10px 30px rgba(124, 58, 237, 0.25);
}
.button:disabled,
button:disabled,
.button[aria-disabled="true"] {
background: #a78bfa;
color: #e5e7eb;
cursor: not-allowed;
box-shadow: none;
opacity: 0.7;
}
.button:disabled:hover,
button:disabled:hover {
transform: none;
}
.button.block {
width: 100%;
}
.muted {
color: #94a3b8;
font-size: 0.9em;
}

129
src/index.html Normal file
View File

@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Browse channels - Imgfloat</title>
<link rel="stylesheet" href="./css/index.css" />
</head>
<body class="channels-body">
<div class="window-frame" role="presentation">
<div class="window-frame-title">Imgfloat</div>
<div class="window-frame-controls">
<button class="window-control" type="button" data-window-action="minimize" aria-label="Minimize">
&minus;
</button>
<button class="window-control" type="button" data-window-action="devtools" aria-label="Toggle dev tools">
&#9881;
</button>
<button
class="window-control window-control-close"
type="button"
data-window-action="close"
aria-label="Close"
>
&times;
</button>
</div>
</div>
<div class="channels-shell">
<header class="channels-header">
<div class="brand">
<img class="brand-mark" alt="brand" src="../res/icon/brand.png" />
<div>
<div class="brand-title">Imgfloat</div>
<div class="brand-subtitle">Twitch overlay manager</div>
</div>
</div>
</header>
<main class="channels-main">
<section class="channel-card">
<p class="eyebrow subtle">Broadcast overlay</p>
<h1>Open a channel</h1>
<p class="muted">Type the channel name to jump straight to their overlay.</p>
<form id="channel-search-form" class="channel-form">
<label class="sr-only" for="channel-search">Channel name</label>
<input
id="channel-search"
name="channel"
class="text-input"
type="text"
list="channel-suggestions"
placeholder="Type a channel name"
autocomplete="off"
autofocus
spellcheck="false"
/>
<label class="sr-only" for="domain-input">Server domain</label>
<input
id="domain-input"
name="domain"
class="text-input"
type="url"
autocomplete="off"
spellcheck="false"
/>
<datalist id="channel-suggestions"></datalist>
<button type="submit" class="button block">Open overlay</button>
</form>
</section>
</main>
</div>
<script>
const form = document.getElementById("channel-search-form");
const input = document.getElementById("channel-search");
const domainInput = document.getElementById("domain-input");
const windowActionButtons = document.querySelectorAll("[data-window-action]");
window.store.loadBroadcaster().then((value) => {
if (value && input.value === "") {
input.value = value;
}
});
Promise.all([window.store.loadDomain(), window.store.loadDefaultDomain()]).then(
([savedDomain, defaultDomain]) => {
domainInput.value = savedDomain || defaultDomain;
domainInput.placeholder = defaultDomain;
},
);
domainInput.addEventListener("change", () => {
const trimmedDomain = domainInput.value.trim();
if (trimmedDomain) {
window.store.saveDomain(trimmedDomain);
}
});
form.addEventListener("submit", (e) => {
e.preventDefault();
const channel = input.value.trim();
const fallbackDomain = domainInput.placeholder || "";
const domain = domainInput.value.trim() || fallbackDomain;
if (!channel) {
return;
}
const params = new URLSearchParams({ broadcaster: channel });
window.store.saveDomain(domain);
window.location.href = `${domain}/view/${encodeURIComponent(channel)}/broadcast`;
});
windowActionButtons.forEach((button) => {
button.addEventListener("click", () => {
const action = button.dataset.windowAction;
if (action === "minimize") {
window.store.minimizeWindow();
}
if (action === "devtools") {
window.store.toggleDevTools();
}
if (action === "close") {
window.store.closeWindow();
}
});
});
</script>
</body>
</html>

View File

@@ -1,95 +1,109 @@
const path = require("node:path"); const path = require("node:path");
const { app, BrowserWindow } = require("electron"); const { app, BrowserWindow, ipcMain } = require("electron");
const { autoUpdater } = require("electron-updater"); const { autoUpdater } = require("electron-updater");
const { readStore, writeStore } = require("./store.js");
const initialWindowWidthPx = 960; const STORE_PATH = path.join(app.getPath("userData"), "settings.json");
const initialWindowHeightPx = 640; const INITIAL_WINDOW_WIDTH_PX = 960;
const INITIAL_WINDOW_HEIGHT_PX = 640;
const LOCAL_DOMAIN = "http://localhost:8080";
const DEFAULT_DOMAIN = "https://imgflo.at";
const RUNTIME_DOMAIN = resolveDefaultDomain();
let ELECTRON_WINDOW;
let canvasSizeInterval; function normalizeDomain(domain) {
function clearCanvasSizeInterval() { return domain?.trim()?.replace(/\/+$/, "");
if (canvasSizeInterval) {
clearInterval(canvasSizeInterval);
canvasSizeInterval = undefined;
}
} }
async function autoResizeWindow(win, lastSize) { function resolveDefaultDomain() {
if (win.isDestroyed()) { if (process.env.LOCAL_DOMAIN) {
return lastSize; return normalizeDomain(LOCAL_DOMAIN);
} }
const newSize = await win.webContents.executeJavaScript(`(() => { const buildTimeDomain = process.env.IMGFLOAT_DOMAIN || DEFAULT_DOMAIN;
const canvas = document.getElementById('broadcast-canvas'); return normalizeDomain(buildTimeDomain);
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) { function createWindowOptions() {
url = url || win.webContents.getURL(); return {
let pathname; width: INITIAL_WINDOW_WIDTH_PX,
try { height: INITIAL_WINDOW_HEIGHT_PX,
pathname = new URL(url).pathname; transparent: true,
} catch (e) { frame: false,
console.error(`Failed to parse URL: ${url}`, e); backgroundColor: "#00000000",
return; alwaysOnTop: false,
} icon: path.join(__dirname, "../res/icon/brand.png"),
const isBroadcast = /\/view\/[^/]+\/broadcast\/?$/.test(pathname); webPreferences: {
backgroundThrottling: false,
console.info(`Navigation to ${url} detected. Is broadcast: ${isBroadcast}`); preload: path.join(__dirname, "preload.js"),
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) { function createWindow(version) {
const win = new BrowserWindow({ const windowOptions = createWindowOptions();
width: initialWindowWidthPx, const win = new BrowserWindow(windowOptions);
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.setMenu(null);
win.setFullScreenable(false);
win.setFullScreen(false);
win.setResizable(false);
win.setTitle(`Imgfloat Client v${version}`); win.setTitle(`Imgfloat Client v${version}`);
return win; return win;
} }
ipcMain.handle("set-window-size", (_, width, height) => {
if (ELECTRON_WINDOW && !ELECTRON_WINDOW.isDestroyed()) {
ELECTRON_WINDOW.setContentSize(width, height, false);
}
});
ipcMain.handle("minimize-window", () => {
if (ELECTRON_WINDOW && !ELECTRON_WINDOW.isDestroyed()) {
ELECTRON_WINDOW.minimize();
}
});
ipcMain.handle("close-window", () => {
if (ELECTRON_WINDOW && !ELECTRON_WINDOW.isDestroyed()) {
ELECTRON_WINDOW.close();
}
});
ipcMain.handle("toggle-devtools", () => {
if (ELECTRON_WINDOW && !ELECTRON_WINDOW.isDestroyed()) {
if (ELECTRON_WINDOW.webContents.isDevToolsOpened()) {
ELECTRON_WINDOW.webContents.closeDevTools();
} else {
ELECTRON_WINDOW.webContents.openDevTools({ mode: "detach" });
}
}
});
ipcMain.handle("save-broadcaster", (_, broadcaster) => {
const store = readStore(STORE_PATH);
store.lastBroadcaster = broadcaster;
writeStore(STORE_PATH, store);
});
ipcMain.handle("load-broadcaster", () => {
const store = readStore(STORE_PATH);
return store.lastBroadcaster ?? "";
});
ipcMain.handle("save-domain", (_, domain) => {
const store = readStore(STORE_PATH);
store.lastDomain = normalizeDomain(domain);
writeStore(STORE_PATH, store);
});
ipcMain.handle("load-domain", () => {
const store = readStore(STORE_PATH);
return normalizeDomain(store.lastDomain) || RUNTIME_DOMAIN;
});
ipcMain.handle("load-default-domain", () => RUNTIME_DOMAIN);
app.whenReady().then(() => { app.whenReady().then(() => {
if (process.env.CI) { if (process.env.CI) {
process.on("uncaughtException", (err) => { process.on("uncaughtException", (err) => {
@@ -100,13 +114,12 @@ app.whenReady().then(() => {
} }
autoUpdater.checkForUpdatesAndNotify(); autoUpdater.checkForUpdatesAndNotify();
let broadcastRect = { width: 0, height: 0 };
const version = app.getVersion(); const version = app.getVersion();
const win = createWindow(version); ELECTRON_WINDOW = createWindow(version);
win.loadURL(process.env["IMGFLOAT_CHANNELS_URL"] || "https://imgfloat.kruhlmann.dev/channels"); ELECTRON_WINDOW.loadFile(path.join(__dirname, "index.html"));
win.webContents.on("did-finish-load", () => onPostNavigationLoad(win, undefined, broadcastRect)); ELECTRON_WINDOW.on("page-title-updated", (e) => e.preventDefault());
win.webContents.on("did-navigate", (_, url) => onPostNavigationLoad(win, url, broadcastRect));
win.webContents.on("did-navigate-in-page", (_, url) => onPostNavigationLoad(win, url, broadcastRect)); if (process.env.DEVTOOLS) {
win.on("page-title-updated", (e) => e.preventDefault()); ELECTRON_WINDOW.webContents.openDevTools({ mode: "detach" });
win.on("closed", clearCanvasSizeInterval); }
}); });

13
src/preload.js Normal file
View File

@@ -0,0 +1,13 @@
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("store", {
saveBroadcaster: (value) => ipcRenderer.invoke("save-broadcaster", value),
loadBroadcaster: () => ipcRenderer.invoke("load-broadcaster"),
saveDomain: (value) => ipcRenderer.invoke("save-domain", value),
loadDomain: () => ipcRenderer.invoke("load-domain"),
loadDefaultDomain: () => ipcRenderer.invoke("load-default-domain"),
setWindowSize: (width, height) => ipcRenderer.invoke("set-window-size", width, height),
minimizeWindow: () => ipcRenderer.invoke("minimize-window"),
closeWindow: () => ipcRenderer.invoke("close-window"),
toggleDevTools: () => ipcRenderer.invoke("toggle-devtools"),
});

18
src/store.js Normal file
View File

@@ -0,0 +1,18 @@
const fs = require("node:fs");
function readStore(store_path) {
try {
return JSON.parse(fs.readFileSync(store_path, "utf8"));
} catch {
return {};
}
}
function writeStore(store_path, data) {
fs.writeFileSync(store_path, JSON.stringify(data, null, 2));
}
module.exports = {
readStore,
writeStore,
};