diff --git a/package.json b/package.json index e19caa9..24b6b23 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,30 @@ "name": "neofunkin-1", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "dist/index.js", + "build": { + "appId": "asper.games.neofunkin", + "productName": "NeoFunkin [EARLY ALPHA]", + "files": [ + "dist/**/*" + ], + "win": { + "target": "portable" + }, + "linux": { + "target": "AppImage" + } + }, "scripts": { "build": "tsc && node scripts/finalizeBuild.js", - "electron": "electron dist" + "electron": "electron dist", + "bundle": "electron-builder" }, "keywords": [], "author": "", "license": "AGPL-3.0-only", "packageManager": "pnpm@10.4.1", "dependencies": { - "electron": "^35.0.0", "express": "^4.21.2" }, "pnpm": { @@ -21,8 +34,10 @@ ] }, "devDependencies": { - "@types/electron": "^1.6.12", + "electron": "^35.0.0", + "@types/express": "^5.0.0", "@types/node": "^22.13.9", + "electron-builder": "^25.1.8", "fs-extra": "^11.3.0", "typescript": "^5.8.2" } diff --git a/src/index.ts b/src/index.ts index 05522af..7a08f04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,60 +1,72 @@ import wm from './lib/windowManager'; -import { easeInOutQuad } from './lib/animations'; +import { wait } from './lib/utils'; import { app } from 'electron'; +import displayCrashScreen from './lib/crash'; +import express from 'express'; +import path from 'path'; +import { createServer } from 'http'; -app.on('ready', () => { - try { - const mw = wm.create({ - w: 10, - h: 10, - whp: true, - onCreate: (win: any) => { - win.loadURL('https://example.com'); - }, - }); - - const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - - (async () => { - await wait(200); - wm.resize({ id: mw, w: 25, h: 25, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad}); - await wait(1000); - wm.resize({ id: mw, w: 50, h: 50, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad}); - await wait(650); - wm.move({ id: mw, x: 0, y: 50, p: true, fromCenter: true}); - wm.resize({ id: mw, w: 33, h: 100, p: true }); - wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'}); - await wait(500); - wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true}); - wm.resize({ id: mw, w: 33, h: 100, p: true }); - wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'}); - await wait(500); - wm.move({ id: mw, x: 100, y: 50, p: true, fromCenter: true}); - wm.resize({ id: mw, w: 33, h: 100, p: true }); - wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'}); - await wait(500); - wm.resize({ id: mw, w: 100, h: 100, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad}); - await wait(550); - wm.resize({ id: mw, w: 100, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'}); - await wait(550); - wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad, anchor: 'bottom'}); - await wait(500); - wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true, ease: easeInOutQuad, smooth: true}); - await wait(500); - wm.resize({ id: mw, w: 50, h: 50, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad}); - await wait(510); - wm.eval(mw, `document.querySelector('body').innerHTML = '

NeoFunkin

'`); - await wait(500); - wm.eval(mw, `document.querySelector('body').innerHTML = '

MrpGimlom

'`); - await wait(100); - wm.eval(mw, `document.querySelector('body').innerHTML = '

NeoFunkin: Window manager

'`); - await wait(500); - wm.eval(mw, `document.querySelector('body').innerHTML = '

NeoFunkin: Window manager


DEMO BY @TRUE1ANN, NEOFUNKIN IS PROPERTY OF ASPER'`); - await wait(1500); - wm.eval(mw, `document.querySelector('body').innerHTML = '

NeoFunkin: Window manager


DEMO BY @TRUE1ANN, NEOFUNKIN IS PROPERTY OF ASPER
this took so much to do omfg'`); - })(); - - } catch (e) { - console.error(e); +declare global { + namespace NodeJS { + interface Process { + nfenv?: any; + } } -}); \ No newline at end of file +} + +process.nfenv = { + serverPort: 65034 as number, // todo: pick a random one instead, not for now bcuz easier to debug and use /eval endpoint + crash: function (reason?: string) { + console.error('[nfcrash] Simulating a crash with reason:', reason || '*no reason*'); + displayCrashScreen(); + } +} + +async function startWebRoot() { + const app = express(); + const server = createServer(app); + + app.use(express.static(path.join(__dirname, 'webroot'))); + + app.get('/eval', (req: express.Request, res: express.Response): any => { + const code = req.headers['x-code'] as string; + + if (!code) return res.status(400).send('No code provided'); + + try { + const result = (function(require) { + return eval(code); + })(require); + res.status(200).send(result as string); + } catch (error: any) { + res.status(500).send(error.message as string); + } + }); + + // to-do: add echo webhook endpoint and endpoints to create more webhook endpoints (and also delete those) + + server.listen(process.nfenv.serverPort, 'localhost'); +}; + +startWebRoot().then(() => { + app.on('ready', async () => { + try { + const mw = wm.create({ + w: 20, + h: 20, + whp: true, + onCreate: (win: any) => { + win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/mainmenu/test.html`); + }, + }); + + wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true }) + await wait(500); + wm.resize({ id: mw, w: 40, h: 40, smooth: true, p: true}); + + } catch (e) { + console.error(e); + process.nfenv.crash(); + } + }); +}) \ No newline at end of file diff --git a/src/lib/crash.ts b/src/lib/crash.ts new file mode 100644 index 0000000..67272f2 --- /dev/null +++ b/src/lib/crash.ts @@ -0,0 +1,34 @@ +import { wm, wdata } from './windowManager'; +import { easeInOutQuad } from './animations'; +import { wait, toP } from './utils' +import { app, screen } from 'electron'; + +export default async function displayCrashScreen() { + const bwdata = { ...wdata }; + const win = wm.create({ + w: 40, + h: 40, + whp: true, + onCreate: (win: any) => { + win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/crash/index.html`); + }, + }); + const button = wm.create({ + w: 10, + h: 5, + whp: true, + noBorder: true, + noBackground: true, + onCreate: (win: any) => { + win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/crash/exit.html`); + }, + }); + await wait(300); + console.log('[nfcrash] Deleted all windows'); + Object.keys(bwdata).forEach(async (id) => { + bwdata[id].win.destroy(); // Note: We dont use wm.destroy(...) so ...onDestroy wont get called and cause trouble + }); + const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds; + wm.move({ id: button, y: 75, x: 50, p: true, fromCenter: true }); + wm.follow.start(button, win); +}; \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..e962f8b --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,4 @@ +export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +export function toP(value: number, percentage: number) { + return value * (percentage / 100) as number; +} \ No newline at end of file diff --git a/src/lib/windowManager.ts b/src/lib/windowManager.ts index 3b3fdae..450dfbc 100644 --- a/src/lib/windowManager.ts +++ b/src/lib/windowManager.ts @@ -1,17 +1,16 @@ -import { BrowserWindow, screen } from 'electron'; +import { BrowserWindow, Rectangle, screen } from 'electron'; import { wm_create_conf, wm_move_conf, wm_resize_conf } from './../types/wm'; +import { toP } from './utils'; interface WindowData { win: BrowserWindow; conf: wm_create_conf; + follows?: string; + followStuff?: any; } let wdata: { [key: string]: WindowData } = {}; -function toP(value: number, percentage: number) { - return value * (percentage / 100) as number; -} - const wm = { create: function (c: wm_create_conf) { const wid = Array.from({ length: 8 }, () => '0123456789abcdef'[Math.floor(Math.random() * 16)]).join(''); @@ -21,7 +20,7 @@ const wm = { width: c.whp ? toP(sw, c.w!) : c.w, height: c.whp ? toP(sh, c.h!) : c.h, x: c.xyp ? toP(sw, c.x!) : c.x, - y: c.xyp ? toP(sh, c.y!) : c.x, + y: c.xyp ? toP(sh, c.y!) : c.y, frame: !c.noBorder, transparent: c.noBackground, webPreferences: { @@ -30,7 +29,7 @@ const wm = { }, }); - wdata[wid] = { win: win, conf: c }; // for further pullin + wdata[wid] = { win: win, conf: c }; win.setMenuBarVisibility(false); // tell me WHY should i NOT make this the default if (typeof c.onMinimize === 'function') win.on('minimize', () => c.onMinimize!(win)); @@ -43,7 +42,7 @@ const wm = { if (c.onCreate) c.onCreate(win); return wid // just so you can reference it later on }, - destroy: async function (id: number) { + destroy: async function (id: string) { console.log('[wm] destroy', id); const win = wdata[id]; if (typeof win.conf.onDestroy === 'function') await win.conf.onDestroy!(win.win); @@ -108,86 +107,85 @@ const wm = { resize: function (c: wm_resize_conf) { console.log('[wm] resize', c.id); const win = wdata[c.id]; - let wb = win.win.getBounds(); + if (!win || !win.win) { + console.error('Window data not found for id:', c.id); + return; + } + const wb = win.win.getBounds(); const sW = wb.width, sH = wb.height; + // Use the given dimensions if provided; otherwise fall back to the current window size c.w = c.w === undefined ? sW : c.w; c.h = c.h === undefined ? sH : c.h; const duration = c.duration || 500; const tick = c.tick || 16; const totalSteps = Math.floor(duration / tick); + // Get primary display bounds – make sure that screen is imported from Electron const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds; - let targetW = toP(c.p ? sw : 1, c.w); - let targetH = toP(c.p ? sh : 1, c.h); + // Calculate target width and height using percentage conversion if needed. + // Assuming toP converts a percentage value to pixels + const targetW = toP(c.p ? sw : 1, c.w); + const targetH = toP(c.p ? sh : 1, c.h); + + // Compute window center for centering if needed const centerX = wb.x + wb.width / 2; const centerY = wb.y + wb.height / 2; + + // Adjust position based on the chosen anchor. const getAnchoredPosition = (curWidth: number, curHeight: number) => { let newX = wb.x; let newY = wb.y; - if (!c.fromCenter && c.anchor) { - switch (c.anchor) { - case 'top': - newX = wb.x; - newY = wb.y; - break; - case 'bottom': - newX = wb.x; - newY = wb.y + wb.height - curHeight; - break; - case 'left': - newX = wb.x; - newY = wb.y; - break; - case 'right': - newX = wb.x + wb.width - curWidth; - newY = wb.y; - break; - default: - newX = wb.x; - newY = wb.y; - } + switch (c.anchor) { + case 'top': + // Align to top – x remains same + newY = wb.y; + break; + case 'bottom': + // Align to bottom + newY = wb.y + wb.height - curHeight; + break; + case 'left': + // Align to left – y remains same + newX = wb.x; + break; + case 'right': + // Align to right + newX = wb.x + wb.width - curWidth; + break; + default: + // Default: center the resized window relative to the original bounds + newX = centerX - curWidth / 2; + newY = centerY - curHeight / 2; } return { newX, newY }; }; + + // Animation function to smooth the resize const animate = () => { let currentStep = 0; const startW = wb.width; const startH = wb.height; const step = () => { currentStep++; - const t = currentStep / totalSteps; + const t = Math.min(currentStep / totalSteps, 1); const eT = c.ease ? c.ease(t) : t; const newW = startW + (targetW - startW) * eT; const newH = startH + (targetH - startH) * eT; - let newX: number, newY: number; - if (c.fromCenter) { - newX = Math.round(centerX - newW / 2); - newY = Math.round(centerY - newH / 2); - } else { - const pos = getAnchoredPosition(Math.round(newW), Math.round(newH)); - newX = pos.newX; - newY = pos.newY; - } + // Get new positions based on the current width and height + const pos = getAnchoredPosition(Math.round(newW), Math.round(newH)); win.win.setBounds({ - x: newX, - y: newY, + x: pos.newX, + y: pos.newY, width: Math.round(newW), height: Math.round(newH) }); if (currentStep < totalSteps) { setTimeout(step, tick); } else { - let finalX: number, finalY: number; - if (c.fromCenter) { - finalX = Math.round(centerX - targetW / 2); - finalY = Math.round(centerY - targetH / 2); - } else { - const pos = getAnchoredPosition(Math.round(targetW), Math.round(targetH)); - finalX = pos.newX; - finalY = pos.newY; - } + // Make sure to set final bounds exactly + const finalPos = getAnchoredPosition(Math.round(targetW), Math.round(targetH)); win.win.setBounds({ - x: finalX, - y: finalY, + x: finalPos.newX, + y: finalPos.newY, width: Math.round(targetW), height: Math.round(targetH) }); @@ -195,21 +193,15 @@ const wm = { }; step(); }; + + // Use animate if smooth animation is requested, otherwise set bounds immediately. if (c.smooth) { animate(); } else { - let finalX: number, finalY: number; - if (c.fromCenter) { - finalX = Math.round(centerX - targetW / 2); - finalY = Math.round(centerY - targetH / 2); - } else { - const pos = getAnchoredPosition(Math.round(targetW), Math.round(targetH)); - finalX = pos.newX; - finalY = pos.newY; - } + const finalPos = getAnchoredPosition(Math.round(targetW), Math.round(targetH)); win.win.setBounds({ - x: finalX, - y: finalY, + x: finalPos.newX, + y: finalPos.newY, width: Math.round(targetW), height: Math.round(targetH) }); @@ -222,6 +214,42 @@ const wm = { } catch (error) { console.error('Error executing JavaScript:', error); } + }, + follow: { + INTERNAL_follow: function (id: string) { + const leadWin = wdata[wdata[id].follows!].win; + const followWin = wdata[id].win; + const leadBounds = leadWin.getBounds(); + const followBounds = followWin.getBounds(); + + if (wdata[id].followStuff === undefined) { + wdata[id].followStuff = {}; + } + + if (wdata[id].followStuff.offsetX === undefined || wdata[id].followStuff.offsetY === undefined) { + wdata[id].followStuff.offsetX = followBounds.x - leadBounds.x; + wdata[id].followStuff.offsetY = followBounds.y - leadBounds.y; + } + + const newX = leadBounds.x + wdata[id].followStuff.offsetX; + const newY = leadBounds.y + wdata[id].followStuff.offsetY; + + followWin.setPosition(newX, newY); + followWin.show(); + }, + INTERNAL_followfocus: function (id: string) { + wdata[id].win.show(); + }, + start: function (id: string, follows: string) { + console.log('[wm]', id, 'follows', follows); + wdata[id].follows = follows; + wdata[follows].win.on('move', this.INTERNAL_follow.bind(this, id)); + wdata[follows].win.on('focus', this.INTERNAL_followfocus.bind(this, id)); + }, + stop: function (id: string) { + wdata[wdata[id].follows!].win.removeListener('move', this.INTERNAL_follow.bind(this, id)); + wdata[wdata[id].follows!].win.removeListener('focus', this.INTERNAL_followfocus.bind(this, id)); + } } }; diff --git a/src/types/wm.d.ts b/src/types/wm.d.ts deleted file mode 100644 index e3e2a55..0000000 --- a/src/types/wm.d.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { BrowserWindow } from 'electron'; - -export type wm_create_conf = { - w?: number; // Width - h?: number; // Height - x?: number; // X start pos - y?: number; // Y start pos - whp?: boolean; // is w and h in percentages - xyp?: boolean; // is x and y in percentages - noBorder?: boolean; // Hide OS window decorations - noBackground?: boolean; // Hide window background - onCreate?: Function; // wm.create(...) - onMinimize?: Function; // USER pressed '_' - onMaximize?: Function; // USER pressed '[]' - onRestore?: Function; // USER pressed '[]]' - onUnfocus?: Function; // USER unfocused on the window - onFocus?: Function; // USER focused on the window - onClose?: Function; // USER pressed 'x' - onDestroy?: Function; // USER pressed 'x' OR wm.destroy(...) -}; - -export type wm_move_conf = { - id: string; // ID of the window to perform everythin on - x?: number; // New X - y?: number; // New Y - p?: boolean; // Use percentages - fromCenter?: boolean; // Should be performed from the center of the window? - smooth?: boolean; // Should it be smooth? - ease?: Function; // Calculates the easing (lib/animations.ts) - duration?: number; // (ms) The total duration of the animation - tick?: number; // (ms) Delay between window movenments -} - -export type wm_resize_conf = { - id: string; // ID of the window to perform everythin on - w?: number; // New width - h?: number; // New height - p?: boolean; // Use percentages - fromCenter?: boolean; // Should be performed from the center of the window? - smooth?: boolean; // Should it be smooth? - ease?: Function; // Calculates the easing (lib/animations.ts) - duration?: number; // (ms) The total duration of the animation - tick?: number; // (ms) Delay between window resizings -} - -interface WindowData { - win: BrowserWindow; - conf: wm_create_conf; -} - -declare const wm: { - create: (c: wm_create_conf) => string; - destroy: (id: string) => Promise; - move: (c: wm_move_conf) => void; - resize: (c: wm_resize_conf) => void; - eval: (id: string, code: Function) => Promise; -}; - -export default wm; -export { WindowData, wm }; \ No newline at end of file diff --git a/src/types/wm.ts b/src/types/wm.ts index 05ac8d2..a2868c1 100644 --- a/src/types/wm.ts +++ b/src/types/wm.ts @@ -1,6 +1,6 @@ export type wm_create_conf = { - w?: number; // Width - h?: number; // Height + w: number; // Width + h: number; // Height x?: number; // X start pos y?: number; // Y start pos whp?: boolean; // is w and h in percentages @@ -34,10 +34,9 @@ export type wm_resize_conf = { w?: number; // New width h?: number; // New height p?: boolean; // Use percentages - fromCenter?: boolean; // Should be performed from the center of the window? smooth?: boolean; // Should it be smooth? ease?: Function; // Calculates the easing (lib/animations.ts) duration?: number; // (ms) The total duration of the animation tick?: number; // (ms) Delay between window resizings - anchor?: string; // bottom, top, left, right + anchor?: string; // bottom, top, left, right, center } \ No newline at end of file diff --git a/src/webroot/screens/crash/exit.html b/src/webroot/screens/crash/exit.html new file mode 100644 index 0000000..5e664ad --- /dev/null +++ b/src/webroot/screens/crash/exit.html @@ -0,0 +1,18 @@ + + + + + + NeoFunkin + + + + + + \ No newline at end of file diff --git a/src/screens/crash/index.html b/src/webroot/screens/crash/index.html similarity index 67% rename from src/screens/crash/index.html rename to src/webroot/screens/crash/index.html index 885f66a..00e76ae 100644 --- a/src/screens/crash/index.html +++ b/src/webroot/screens/crash/index.html @@ -5,5 +5,6 @@

Uh oh.

+ NeoFunkin crashed. Check logs for more details. diff --git a/src/screens/mainmenu/test.html b/src/webroot/screens/mainmenu/test.html similarity index 50% rename from src/screens/mainmenu/test.html rename to src/webroot/screens/mainmenu/test.html index f22a6d9..7beefc9 100644 --- a/src/screens/mainmenu/test.html +++ b/src/webroot/screens/mainmenu/test.html @@ -5,5 +5,7 @@

Hello, World!

+ +