diff --git a/index.html b/index.html index d3aceaf0..323b5c68 100755 --- a/index.html +++ b/index.html @@ -1,33 +1,35 @@ - - - - - - - - - ArcOS - - - - -
-
-
.
-
-
- - - - + + + + + + + + + ArcOS + + + + + + +
+
+
.
+
+
+ + + + + \ No newline at end of file diff --git a/src/apps/components/fsprogress/runtime.ts b/src/apps/components/fsprogress/runtime.ts index f4a0f45f..dd7f3501 100755 --- a/src/apps/components/fsprogress/runtime.ts +++ b/src/apps/components/fsprogress/runtime.ts @@ -1,5 +1,6 @@ import type { IFsProgressRuntime } from "$interfaces/runtimes/IFsProgressRuntime"; import { AppProcess } from "$ts/apps/process"; +import { IsElectron } from "$ts/electron"; import { Stack } from "$ts/env"; import { Store } from "$ts/writable"; import type { AppProcessData } from "$types/apps/app"; @@ -29,6 +30,10 @@ export class FsProgressRuntime extends AppProcess implements IFsProgressRuntime this.windowTitle.set(v.caption); this.windowIcon.set(v.icon); + if (IsElectron()) { + electron!.updateIconProgress(v.done, v.max); + } + if (v.done >= v.max && v.max > 0 && !v.errors.length) { await this.closeWindow(); // Close the window if pending operations are done @@ -48,6 +53,10 @@ export class FsProgressRuntime extends AppProcess implements IFsProgressRuntime async onClose(): Promise { if (this.parentPid) Stack.renderer?.focusedPid.set(this.parentPid); // Focus the parent PID upon close + if (IsElectron()) { + electron!.updateIconProgress(0, 0); + } + return true; } diff --git a/src/apps/core/loginapp/runtime.ts b/src/apps/core/loginapp/runtime.ts index a1599627..f0cf71fd 100755 --- a/src/apps/core/loginapp/runtime.ts +++ b/src/apps/core/loginapp/runtime.ts @@ -251,7 +251,7 @@ export class LoginAppRuntime extends AppProcess implements ILoginAppRuntime { broadcast("Stopping User Daemon"); await userDaemon.killSelf(); } - State?.loadState("turnedOff"); + await State?.loadState("turnedOff"); } async restart(userDaemon?: IUserDaemon) { diff --git a/src/css/apps/core/bootscreen.css b/src/css/apps/core/bootscreen.css index 2a879f79..a3e0f3cb 100755 --- a/src/css/apps/core/bootscreen.css +++ b/src/css/apps/core/bootscreen.css @@ -3,7 +3,7 @@ --bg: #000; } -div.window#bootScreen > div.body { +div.window#bootScreen>div.body { display: flex; justify-content: center; align-items: center; @@ -14,11 +14,11 @@ div.window#bootScreen > div.body { justify-content: space-around; } -div.window#bootScreen div.body > img { +div.window#bootScreen div.body>img { height: 200px; } -div.window#bootScreen div.body > img.gray { +div.window#bootScreen div.body>img.gray { filter: grayscale(50%); background-image: url("../../../assets/branding/glow-red.png"); } @@ -58,7 +58,7 @@ div.window#bootScreen div.bottom { } div.window#bootScreen div.top { - position: fixed; + position: absolute; top: 0; left: 0; width: 100%; @@ -83,4 +83,4 @@ div.window#bootScreen div.top div.keys div.row div.key { font-weight: bold; opacity: 0.5; width: 20px; -} +} \ No newline at end of file diff --git a/src/css/electron.css b/src/css/electron.css new file mode 100644 index 00000000..c1b7ff28 --- /dev/null +++ b/src/css/electron.css @@ -0,0 +1,108 @@ +@import url("./themes.css"); + +@property --electron-titlebar-height { + syntax: " | "; + inherits: false; + initial-value: 40px; +} + +:root { + --electron-titlebar-height: 40px; +} + +body.electron-app { + overflow: hidden; + transform: translateZ(0); + border-radius: 0px; + + @media (prefers-reduced-motion: no-preference) { + transition: border-radius .2s; + } + + &.transparent { + background-color: transparent; + } + +} + +/* Please, PLEASE don't judge me for this. it works. + * It's just a way for me to check if the titlebar is + * hidden or not and affect the apprenderer size + */ +body:has(#electron-titlebar.hidden) #appRenderer.electron-app { + height: 100%; +} + +body.electron-app.rounded { + border-radius: var(--win-border-rad) +} + +#appRenderer.electron-app { + top: unset; + bottom: 0; + height: calc(100% - var(--electron-titlebar-height)); +} + +#electron-titlebar { + display: flex; + position: relative; + top: 0; + + height: var(--electron-titlebar-height); + width: 100%; + + z-index: 10000 !important; + padding: .75rem; + + app-region: drag; + + user-select: none; + + background-color: var(--glass-bg); + backdrop-filter: blur(4px); + + &.hidden { + display: none; + + ~#appRenderer.electron-app { + height: 100%; + } + } + +} + +#electron-titlebar>.window-title { + flex: 1; + display: inline-flex; + gap: .5rem; + + &>img { + height: 100%; + } + + &>span { + align-self: center; + font-size: 1.15rem; + } +} + +#electron-titlebar>.controls { + align-self: center; + + & button { + margin: 0; + padding: .15rem; + font-size: 1.15rem; + border: none; + aspect-ratio: 1; + background-color: transparent; + color: var(--fg); + transition: opacity 0.2s; + + app-region: no-drag; + + &:hover { + color: var(--accent-light); + } + } +} \ No newline at end of file diff --git a/src/css/main.css b/src/css/main.css index 79140df3..c4ad78ae 100755 --- a/src/css/main.css +++ b/src/css/main.css @@ -23,6 +23,7 @@ @import url("./state/serverdown.css"); @import url("./state/turnedoff.css"); @import url("./state/arcterm.css"); +@import url("./electron.css"); html, body { @@ -85,7 +86,7 @@ main#main.hidden #appRenderer { font-family: var(--user-font, "Nunito Sans"), "Nunito Sans"; } -#appRenderer > div.window:not(.core) { +#appRenderer>div.window:not(.core) { pointer-events: initial; } @@ -99,6 +100,7 @@ main#main.hidden #appRenderer { opacity: 1; } } + .blink { animation: blink 1s step-start 0s infinite; } @@ -131,4 +133,4 @@ span.beta-pill { [class*="theme-"] { color: var(--fg); -} +} \ No newline at end of file diff --git a/src/lib/ElectronTitleBar.svelte b/src/lib/ElectronTitleBar.svelte new file mode 100644 index 00000000..e3ef75f8 --- /dev/null +++ b/src/lib/ElectronTitleBar.svelte @@ -0,0 +1,28 @@ + + +
+
+ ArcOS logo + {document.title} +
+
+ + + +
+
diff --git a/src/main.ts b/src/main.ts index 8368727d..35cc910d 100755 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,11 @@ import "dseg/css/dseg.css"; import "./css/main.css"; +import { handleElectronInit } from "$ts/electron"; // CODE EXECUTION STARTS HERE async function Main() { + await handleElectronInit(); + const { WaveKernel } = await import("$ts/kernel/wavekernel"); const kernel = new WaveKernel(); diff --git a/src/ts/apps/renderer.ts b/src/ts/apps/renderer.ts index 13307fd3..66265618 100755 --- a/src/ts/apps/renderer.ts +++ b/src/ts/apps/renderer.ts @@ -18,6 +18,7 @@ import { Store } from "../writable"; import { AppRendererError } from "./error"; import { BuiltinAppImportPathAbsolutes } from "./store"; import { Sleep } from "$ts/sleep"; +import { IsElectron } from "$ts/electron"; export class AppRenderer extends Process implements IAppRenderer { currentState: number[] = []; @@ -66,6 +67,10 @@ export class AppRenderer extends Process implements IAppRenderer { if (process._disposed) return; + if (IsElectron()) { + this.target.classList.add("electron-app"); + } + this.Log(`Rendering PID ${process.pid}`); renderTarget ||= this.target; diff --git a/src/ts/daemon/contexts/apprenderer.ts b/src/ts/daemon/contexts/apprenderer.ts index 10f9a110..ccc29eff 100644 --- a/src/ts/daemon/contexts/apprenderer.ts +++ b/src/ts/daemon/contexts/apprenderer.ts @@ -1,5 +1,6 @@ import type { IAppRendererUserContext } from "$interfaces/contexts/IAppRendererUserContext"; import type { IUserDaemon } from "$interfaces/IUserDaemon"; +import { IsElectron } from "$ts/electron"; import { Daemon, Stack } from "$ts/env"; import { bestForeground, darkenColor, hex3to6, invertColor, lightenColor } from "$ts/util/color"; import type { CustomStylePreferences, UserPreferences } from "$types/user"; @@ -59,6 +60,12 @@ export class AppRendererUserContext extends UserContext implements IAppRendererU renderer.classList.toggle("safe-mode", this.safeMode); renderer.classList.toggle("traffic-lights", v.shell.visuals.trafficLights); renderer.classList.toggle("hide-altmenus", v.shell.visuals.hideAltmenus); + + if (IsElectron()) { + renderer.classList.add("electron-app"); + const titleBar = document.getElementById("electron-titlebar")! as HTMLDivElement; + titleBar.setAttribute("style", style); + } } setUserStyleLoader(style: CustomStylePreferences) { diff --git a/src/ts/daemon/contexts/notifications.ts b/src/ts/daemon/contexts/notifications.ts index 4517164f..6f80d2ba 100644 --- a/src/ts/daemon/contexts/notifications.ts +++ b/src/ts/daemon/contexts/notifications.ts @@ -1,6 +1,9 @@ import type { INotificationsUserContext } from "$interfaces/contexts/INotificationsUserContext"; import type { IUserDaemon } from "$interfaces/IUserDaemon"; +import { Logo } from "$ts/branding"; +import { IsElectron } from "$ts/electron"; import { SysDispatch } from "$ts/env"; +import type { IconService } from "$ts/servicehost/services/IconService"; import type { Notification } from "$types/system/notification"; import { UserContext } from "../context"; @@ -24,6 +27,13 @@ export class NotificationsUserContext extends UserContext implements INotificati SysDispatch.dispatch("update-notifications", [this.notifications]); SysDispatch.dispatch("send-notification", [data]); + const iconService = this.serviceHost?.getService("IconService"); + + if (IsElectron()) { + electron!.sendNotification(data); + // if (iconService && data.icon) electron.sendNotification(data, await iconService.getIcon(data.icon)); + // else electron.sendNotification(data, Logo()); + } return id; } diff --git a/src/ts/electron.ts b/src/ts/electron.ts new file mode 100644 index 00000000..40c9acff --- /dev/null +++ b/src/ts/electron.ts @@ -0,0 +1,116 @@ +import type { LoginAppRuntime } from "$apps/core/loginapp/runtime"; +import ElectronTitleBar from "$lib/ElectronTitleBar.svelte"; +import { Daemon, Kernel, Stack } from "$ts/env"; +import type { ArcTerminal } from "$ts/terminal"; +import { mount } from "svelte"; + +export const IsElectron = () => !!(window as any)["electron"] && navigator.userAgent.includes("Electron"); + +function disableRoundedWindow(platform: NodeJS.Platform, disableRounding: boolean) { + if (platform === "win32") { + if (disableRounding) { + document.body.classList.remove("rounded"); + } else { + document.body.classList.add("rounded"); + } + } +} + +function hideTitleBar(titlebar: HTMLDivElement, hide: boolean) { + if (hide) { + titlebar.classList.add("hidden"); + } else { + titlebar.classList.remove("hidden"); + } +} + +export async function handleElectronInit() { + if (IsElectron()) { + // inform the electron process that native functions are available. + electron!.supportIntegration(); + + const platform = await electron!.getSystem(); + const isNative = await electron!.isNative(); + + mount(ElectronTitleBar, { target: document.body }); + const titleBar = document.getElementById("electron-titlebar")! as HTMLDivElement; + if (isNative) { + hideTitleBar(titleBar, true); + } + + document.body.classList.add("electron-app"); + if (platform === "win32") { + document.body.classList.add("transparent"); + } + + disableRoundedWindow(platform, false); + + electron!.handleMaximize(async (isMaximized) => { + disableRoundedWindow(platform, isMaximized); + }); + + electron!.handleFullscreen((isFullscreen) => { + disableRoundedWindow(platform, isFullscreen); + if (!isNative) { + hideTitleBar(titleBar, isFullscreen); + } + }); + + electron!.onPowerOff(async () => { + if (!Kernel || !Kernel.state) { + electron!.setCanClose(true); + electron!.closeWindow(); + return; + } + + switch (Kernel.state?.currentState) { + case "desktop": + await Daemon.power?.shutdown(); + break; + + case "boot": + case "turnedOff": + electron!.setCanClose(true); + electron!.closeWindow(); + break; + + case "arcterm": + function getSubProcesses(parentPid: number) { + const subProcesses = Stack.getSubProcesses(parentPid); + for (const [key, val] of subProcesses) { + if (val.name === "ArcTerminal") { + (val as ArcTerminal).processLine("exit"); + break; + } + + getSubProcesses(key); + } + } + + getSubProcesses(Kernel.initPid); + break; + + case "login": + function getSubProcessess(parentPid: number) { + const subProcesses = Stack.getSubProcesses(parentPid); + for (const [key, val] of subProcesses) { + if (val.name === "loginApp") { + if ((val as LoginAppRuntime).loadingStatus().length === 0) { + (val as LoginAppRuntime).shutdown(); + } + break; + } + + getSubProcessess(key); + } + } + + getSubProcessess(Kernel.initPid); + break; + default: + Kernel.Log("v7-electron", `Couldn't handle state "${Kernel.state?.currentState}"`); + break; + } + }); + } +} diff --git a/src/ts/kernel/wavekernel.ts b/src/ts/kernel/wavekernel.ts index 3e16f585..d907a7ac 100644 --- a/src/ts/kernel/wavekernel.ts +++ b/src/ts/kernel/wavekernel.ts @@ -1,14 +1,17 @@ +import type { LoginAppRuntime } from "$apps/core/loginapp/runtime"; import type { IStateHandler } from "$interfaces/IStateHandler"; import type { IWaveKernel } from "$interfaces/IWaveKernel"; import type { IProcessHandler } from "$interfaces/modules/IProcessHandler"; import type { ISystemDispatch } from "$interfaces/modules/ISystemDispatch"; import { __Console__ } from "$ts/console"; -import { ArcOSVersion, SetCurrentKernel, SetKernelExports } from "$ts/env"; +import { IsElectron } from "$ts/electron"; +import { ArcOSVersion, Daemon, Kernel, SetCurrentKernel, SetKernelExports, Stack } from "$ts/env"; import { JsExec } from "$ts/jsexec"; import { getBuild } from "$ts/metadata/build"; import { ChangeLogs } from "$ts/metadata/changelog"; import { getLicense } from "$ts/metadata/license"; import { getMode } from "$ts/metadata/mode"; +import type { ArcTerminal } from "$ts/terminal"; import { LogLevel, ShortLogLevelCaptions, type LogItem } from "../../types/shared/logging"; import { handleGlobalErrors } from "../error"; import { InitProcess } from "./init"; diff --git a/src/ts/state/index.ts b/src/ts/state/index.ts index a9845089..f3c257b0 100755 --- a/src/ts/state/index.ts +++ b/src/ts/state/index.ts @@ -1,6 +1,7 @@ import type { IAppProcess, IAppProcessConstructor } from "$interfaces/IAppProcess"; import type { IStateHandler } from "$interfaces/IStateHandler"; import type { IProcessHandler } from "$interfaces/modules/IProcessHandler"; +import { IsElectron } from "$ts/electron"; import { getKMod, Kernel, Stack } from "$ts/env"; import { LogLevel } from "$types/shared/logging"; import type { State } from "../../types/system/state"; @@ -94,6 +95,11 @@ export class StateHandler extends Process implements IStateHandler { this.currentState = id; + if (id == "turnedOff" && IsElectron()) { + electron!.setCanClose(true); + electron!.closeWindow(); + } + if (!data.appModule) { try { if (!data.render) { diff --git a/src/types/electron.ts b/src/types/electron.ts new file mode 100644 index 00000000..3696b0de --- /dev/null +++ b/src/types/electron.ts @@ -0,0 +1,16 @@ +import type { Notification } from "$types/system/notification"; + +export interface ElectronIPC { + closeWindow(): void; + minimizeWindow(): void; + maximizeWindow(): void; + sendNotification(data: Notification): void; + updateIconProgress(progress: number, max: number): void; + onPowerOff(callback: () => void): void; + setCanClose(canClose: boolean): void; + getSystem(): Promise; + handleMaximize(callback: (isMaximized: boolean) => void): void; + handleFullscreen(callback: (isFullscreen: boolean) => void): void; + supportIntegration(): void; + isNative(): Promise; +} diff --git a/src/window.d.ts b/src/window.d.ts index 9f5e8bab..394d5d16 100644 --- a/src/window.d.ts +++ b/src/window.d.ts @@ -1,8 +1,12 @@ +import type { ElectronIPC } from "$types/electron"; + declare global { interface Window { __DW_INIT__: boolean; __DW_STATUS__: string; } + + export const electron: ElectronIPC | undefined; } export {};