mirror of
https://github.com/imgfloat/client.git
synced 2026-02-05 12:09:27 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2de091567c | |||
| dc19fb7c14 | |||
| 941ac7ee19 | |||
| 38aa8acf03 | |||
| 516f991ae8 | |||
| 4959965429 | |||
| 3940f06cd0 | |||
| c2a90fdb64 | |||
| bfbc5a9fe1 | |||
| c90e8cf54e | |||
| c333884cdf | |||
| b5dda31360 | |||
| 2cd2c265a3 | |||
| 1c69bf461c | |||
| 5f77890fff | |||
| 1fcde694dc | |||
| c3234914c6 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["prettier-plugin-java"],
|
"plugins": [],
|
||||||
"printWidth":120,
|
"printWidth":120,
|
||||||
"tabWidth":4,
|
"tabWidth":4,
|
||||||
"useTabs":false,
|
"useTabs":false,
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
# TODO
|
# Client
|
||||||
|
|
||||||
|
Electron based desktop client for viewing the imgfloat broadcast dashboard.
|
||||||
|
|
||||||
|
## "Why not use a web source?"
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "imgfloat-client",
|
"name": "imgfloat-client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "imgfloat-client",
|
"name": "imgfloat-client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-updater": "^6.7.3"
|
"electron-updater": "^6.7.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "imgfloat-client",
|
"name": "imgfloat-client",
|
||||||
"version": "1.0.1",
|
"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,
|
||||||
|
|||||||
BIN
res/banner.png
LFS
BIN
res/banner.png
LFS
Binary file not shown.
BIN
res/icon/brand.png
LFS
Normal file
BIN
res/icon/brand.png
LFS
Normal file
Binary file not shown.
238
src/css/index.css
Normal file
238
src/css/index.css
Normal 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
129
src/index.html
Normal 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">
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
<button class="window-control" type="button" data-window-action="devtools" aria-label="Toggle dev tools">
|
||||||
|
⚙
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="window-control window-control-close"
|
||||||
|
type="button"
|
||||||
|
data-window-action="close"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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>
|
||||||
175
src/main.js
175
src/main.js
@@ -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
13
src/preload.js
Normal 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
18
src/store.js
Normal 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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user