diff --git a/src/apps/components/masteroptions/MasterOptions.svelte b/src/apps/components/masteroptions/MasterOptions.svelte new file mode 100644 index 00000000..072e876d --- /dev/null +++ b/src/apps/components/masteroptions/MasterOptions.svelte @@ -0,0 +1,43 @@ + + +
+ +

ArcOS Master Options

+
+
+

Choose one of the following options to continue:

+
+ {#each MasterOptionStore as option (option.caption)} + + {/each} +
+
+

Logged in as {Daemon.userInfo.username}

+

{Server.hostname ?? "No server"}

+
+
+ + {#snippet leftContent()} + + Daemon.power?.restart(true)} disabled={$loading} /> + Daemon.power?.shutdown(true)} disabled={$loading} /> + Daemon.power?.logoff(true)} disabled={$loading} /> + + {/snippet} + {#snippet rightContent()} + process.closeWindow()} disabled={$loading}>Close + {/snippet} + diff --git a/src/apps/components/masteroptions/MasterOptions.ts b/src/apps/components/masteroptions/MasterOptions.ts new file mode 100644 index 00000000..a0017feb --- /dev/null +++ b/src/apps/components/masteroptions/MasterOptions.ts @@ -0,0 +1,43 @@ +import "$css/apps/components/masteroptions.css"; +import type { App } from "$types/app"; +import MasterOptions from "./MasterOptions.svelte"; +import { MasterOptionsRuntime } from "./runtime"; + +const MasterOptionsApp: App = { + metadata: { + name: "Master Options", + version: "1.0.0", + author: "Izaak Kuipers", + icon: "ElevationIcon", + }, + position: { centered: true }, + size: { w: 400, h: 490 }, + minSize: { w: 400, h: 490 }, + maxSize: { w: 400, h: 490 }, + state: { + maximized: false, + minimized: false, + resizable: false, + fullscreen: false, + headless: true, + }, + controls: { + minimize: false, + maximize: false, + close: true, + }, + assets: { + runtime: MasterOptionsRuntime, + component: MasterOptions as any, + }, + glass: false, + elevated: false, + hidden: true, + core: false, + overlay: false, + noSafeMode: false, + vital: true, + id: "MasterOptions", +}; + +export default MasterOptionsApp; diff --git a/src/apps/components/masteroptions/MasterOptions/MasterOptionButton.svelte b/src/apps/components/masteroptions/MasterOptions/MasterOptionButton.svelte new file mode 100644 index 00000000..2ae85ed3 --- /dev/null +++ b/src/apps/components/masteroptions/MasterOptions/MasterOptionButton.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/apps/components/masteroptions/runtime.ts b/src/apps/components/masteroptions/runtime.ts new file mode 100644 index 00000000..88e935e6 --- /dev/null +++ b/src/apps/components/masteroptions/runtime.ts @@ -0,0 +1,66 @@ +import type { IAppProcess } from "$interfaces/app"; +import { AppProcess } from "$ts/apps/process"; +import { Daemon } from "$ts/daemon"; +import { Stack } from "$ts/env"; +import { Plural } from "$ts/util"; +import { Store } from "$ts/writable"; +import type { AppProcessData } from "$types/app"; + +export class MasterOptionsRuntime extends AppProcess { + loading = Store(false); + + //#region LIFECYCLE + + constructor(pid: number, parentPid: number, app: AppProcessData) { + super(pid, parentPid, app); + + this.setSource(__SOURCE__); + } + + async render() { + if (await this.closeIfSecondInstance()) return; + } + + //#endregion LIFECYCLE + + async killGhosts() { + const state = Stack.renderer?.currentState || []; + const ghosts = []; + + for (const pid of state) { + const proc = Stack.getProcess(pid); + if (!proc) { + await Stack.renderer?.remove(pid); + ghosts.push(pid); + } + } + + await Daemon.getShell()?.ShowToast( + { + content: `Removed ${ghosts.length} ${Plural("ghost", ghosts.length)} from the renderer.`, + icon: "ghost", + }, + 4000 + ); + } + + async killUserApps() { + const userApps: IAppProcess[] = [...Stack.store()] + .map(([_, v]) => v as IAppProcess) + .filter( + (proc) => proc instanceof AppProcess && !proc.app.data.core && proc.app.id !== "arcShell" && proc.app.id !== "wallpaper" + ); + + for (const proc of userApps) { + await Stack.kill(proc.pid, true); + } + + await Daemon.getShell()?.ShowToast( + { + content: `Forcefully terminated ${userApps.length} ${Plural("application", userApps.length)}.`, + icon: "power", + }, + 4000 + ); + } +} diff --git a/src/apps/components/masteroptions/store.ts b/src/apps/components/masteroptions/store.ts new file mode 100644 index 00000000..c6f96c04 --- /dev/null +++ b/src/apps/components/masteroptions/store.ts @@ -0,0 +1,35 @@ +import { Env } from "$ts/env"; +import type { MasterOption } from "./types"; + +export const MasterOptionStore: MasterOption[] = [ + { + caption: "Manage Processes", + image: "ProcessManagerIcon", + action: async (runtime) => await runtime.spawnOverlayApp("processManager", +Env.get("shell_pid"), "Processes"), + }, + { + caption: "Remove any ghost windows", + image: "RestartIcon", + action: async (runtime) => await runtime.killGhosts(), + }, + { + caption: "Kill all user applications", + image: "ErrorIcon", + action: async (runtime) => await runtime.killUserApps(), + }, + { + caption: "Launch ArcTerm", + image: "ArcTermIcon", + action: async (runtime) => await runtime.spawnApp("ArcTerm", +Env.get("shell_pid")), + }, + { + caption: "Advanced System Settings", + image: "WindowSettingsIcon", + action: async (runtime) => await runtime.spawnApp("AdvSystemSettings", +Env.get("shell_pid")), + }, + { + caption: "Report a bug", + image: "BugReportIcon", + action: async (runtime) => await runtime.spawnOverlayApp("BugHuntCreator", +Env.get("shell_pid")), + }, +]; diff --git a/src/apps/components/masteroptions/types.ts b/src/apps/components/masteroptions/types.ts new file mode 100644 index 00000000..5edbfce8 --- /dev/null +++ b/src/apps/components/masteroptions/types.ts @@ -0,0 +1,7 @@ +import type { MasterOptionsRuntime } from "./runtime"; + +export interface MasterOption { + caption: string; + image: string; + action: (process: MasterOptionsRuntime) => Promise; +} diff --git a/src/apps/components/systemshortcuts/runtime.ts b/src/apps/components/systemshortcuts/runtime.ts index 1829b108..18cf13ba 100755 --- a/src/apps/components/systemshortcuts/runtime.ts +++ b/src/apps/components/systemshortcuts/runtime.ts @@ -56,7 +56,7 @@ export class SystemShortcutsRuntime extends AppProcess { alt: true, key: "Backspace", action: () => { - this.spawnOverlayApp("processManager", +Env.get("shell_pid"), "Processes"); + this.spawnOverlayApp("MasterOptions", +Env.get("shell_pid")); }, global: true, } diff --git a/src/css/apps/admin/adminportal/page/scopes.css b/src/css/apps/admin/adminportal/page/scopes.css index 5d993164..c0bf9b2c 100755 --- a/src/css/apps/admin/adminportal/page/scopes.css +++ b/src/css/apps/admin/adminportal/page/scopes.css @@ -1,3 +1,8 @@ +#AdminPortal .container.scopes div.scope-list { + height: 100%; + overflow-y: scroll; +} + #AdminPortal .container.scopes div.row { min-height: 36px; height: fit-content; @@ -14,6 +19,9 @@ #AdminPortal .container.scopes div.row.header { background-color: var(--button-bg); font-weight: bold; + position: sticky; + top: 0; + z-index: 1000; } #AdminPortal .container.scopes div.row div.segment { diff --git a/src/css/apps/components/masteroptions.css b/src/css/apps/components/masteroptions.css new file mode 100644 index 00000000..987b8ec3 --- /dev/null +++ b/src/css/apps/components/masteroptions.css @@ -0,0 +1,85 @@ +.shade-MasterOptions { + background-position: center; + background-size: cover; + background-image: linear-gradient(to bottom, #0006 0%, #0006 100%), var(--wallpaper); + z-index: 10000000000; +} + +#MasterOptions > div.body { + padding: 0; + display: flex; + flex-direction: column; +} + +#MasterOptions > div.body div.header { + padding: 10px; + border-bottom: var(--win-border); + display: flex; + align-items: center; + gap: 10px; +} + +#MasterOptions > div.body div.header h1 { + font-size: 14px; + font-weight: bold; +} + +#MasterOptions > div.body div.content { + height: 0; + flex-grow: 1; + display: flex; + flex-direction: column; + padding: 15px; +} + +#MasterOptions > div.body div.options { + height: 0; + flex-grow: 1; + margin: 15px 0; + padding: 10px; + border-radius: 15px; + border: var(--win-border); +} + +#MasterOptions > div.body div.options button { + height: 40px; + width: 100%; + display: flex; + align-items: center; + gap: 15px; + padding: 0 10px; +} + +#MasterOptions > div.body div.options button img { + height: 24px; +} + +#MasterOptions > div.body div.options button:not(:hover):not(:active) { + outline: none; + background-color: transparent; +} + +#MasterOptions > div.body div.authorization { + display: flex; + justify-content: space-between; +} + +#MasterOptions > div.body div.authorization p.hostname { + opacity: 0.5; +} + +#MasterOptions > div.body div.window-action-bar button.lucide { + color: var(--win-bg); +} + +#MasterOptions > div.body div.window-action-bar button.restart { + background-color: var(--clr-orange-fg); +} + +#MasterOptions > div.body div.window-action-bar button.shutdown { + background-color: var(--clr-red-fg); +} + +#MasterOptions > div.body div.window-action-bar button.logout { + background-color: var(--clr-green-fg); +} diff --git a/src/interfaces/contexts/power.ts b/src/interfaces/contexts/power.ts index f992cb82..aac5f132 100644 --- a/src/interfaces/contexts/power.ts +++ b/src/interfaces/contexts/power.ts @@ -4,10 +4,10 @@ import type { ReadableStore } from "$types/writable"; export interface IPowerUserContext extends IUserContext { battery: ReadableStore; - logoff(): Promise; - shutdown(): Promise; - restart(): Promise; - logoffSafeMode(): Promise; + logoff(force?: boolean): Promise; + shutdown(force?: boolean): Promise; + restart(force?: boolean): Promise; + logoffSafeMode(force?: boolean): Promise; toLogin(type: string, props?: Record, force?: boolean): Promise; closeOpenedApps(type: string, props?: Record, force?: boolean): Promise; batteryInfo(): Promise; diff --git a/src/interfaces/modules/server.ts b/src/interfaces/modules/server.ts index 5a34f390..35b64802 100644 --- a/src/interfaces/modules/server.ts +++ b/src/interfaces/modules/server.ts @@ -6,6 +6,7 @@ export interface IServerManager { previewBranch?: string; servers: ServerOption[]; url?: string; + hostname?: string; authCode?: string; checkUsernameAvailability(username: string): Promise; checkEmailAvailability(username: string): Promise; diff --git a/src/ts/apps/store.ts b/src/ts/apps/store.ts index 951ce696..e6de8ac8 100755 --- a/src/ts/apps/store.ts +++ b/src/ts/apps/store.ts @@ -44,6 +44,7 @@ export const BuiltinAppImportPathAbsolutes = import.meta.glob([ "$apps/components/serviceinfo/ServiceInfo", "$apps/components/iconeditdialog/IconEditDialog", "$apps/components/arctermcolors/ArcTermColors", + "$apps/components/masteroptions/MasterOptions", "$apps/user/advsystemsettings/AdvSystemSettings", "$apps/user/appstore/AppStore", "$apps/user/arcterm/ArcTerm", diff --git a/src/ts/daemon/contexts/power.ts b/src/ts/daemon/contexts/power.ts index 24ace64b..b4e5b0b4 100644 --- a/src/ts/daemon/contexts/power.ts +++ b/src/ts/daemon/contexts/power.ts @@ -14,36 +14,36 @@ export class PowerUserContext extends UserContext implements IPowerUserContext { super(id, daemon); } - async logoff() { + async logoff(force = false) { if (this._disposed) return; this.Log(`Logging off NOW`); - await this.toLogin("logoff"); + await this.toLogin("logoff", {}, force); } - async shutdown() { + async shutdown(force = false) { if (this._disposed) return; this.Log(`Shutting down NOW`); - await this.toLogin("shutdown"); + await this.toLogin("shutdown", {}, force); } - async restart() { + async restart(force = false) { if (this._disposed) return; this.Log(`Restarting NOW`); - await this.toLogin("restart"); + await this.toLogin("restart", {}, force); } - async logoffSafeMode() { + async logoffSafeMode(force = false) { this.Log(`Logging off NOW (safe mode)`); Env.set("safemode", true); - await this.toLogin("logoff", { safeMode: true }); + await this.toLogin("logoff", { safeMode: true }, force); } async toLogin(type: string, props: Record = {}, force = false) { diff --git a/src/ts/kernel/mods/server/index.ts b/src/ts/kernel/mods/server/index.ts index 6c408a34..6b09f2de 100644 --- a/src/ts/kernel/mods/server/index.ts +++ b/src/ts/kernel/mods/server/index.ts @@ -28,6 +28,11 @@ export class ServerManager extends KernelModule implements IServerManager { return this.currentServer?.url; } + get hostname() { + if (!this.url) return undefined; + return new URL(this.url).hostname; + } + get authCode() { return this.currentServer?.authCode; } diff --git a/src/ts/terminal/commands/arcfetch.ts b/src/ts/terminal/commands/arcfetch.ts index db8103b9..c065eaf2 100755 --- a/src/ts/terminal/commands/arcfetch.ts +++ b/src/ts/terminal/commands/arcfetch.ts @@ -41,7 +41,7 @@ export class ArcFetchCommand extends TerminalProcess { return Object.entries({ OS: `ArcOS ${ArcOSVersion}-${ArcMode()} (${ArcBuild()})`, - Host: `${server?.url} ${BRBLACK}(${authcode() ? "Protected" : "Open"})${RESET}`, + Host: `${server?.hostname ?? server?.url} ${BRBLACK}(${authcode() ? "Protected" : "Open"})${RESET}`, Username: `${Env.get("currentuser")} ${BRBLACK}(${Env.get("administrator") ? "Administrator" : "Regular User"})${RESET}`, Mode: `Browser ${BRBLACK}(on state ${state?.toUpperCase()})${RESET}`, Terminal: `PID ${term.pid} (${term.name}) on parent PID ${term.parentPid}`, diff --git a/src/ts/terminal/mode/index.ts b/src/ts/terminal/mode/index.ts index c941bdc2..308ec8cf 100755 --- a/src/ts/terminal/mode/index.ts +++ b/src/ts/terminal/mode/index.ts @@ -1,7 +1,7 @@ import type { IUserDaemon } from "$interfaces/daemon"; import type { IArcTerminal } from "$interfaces/terminal"; import { UserDaemon } from "$ts/daemon"; -import { ArcOSVersion, Env, Stack, SysDispatch } from "$ts/env"; +import { ArcOSVersion, Env, Server, Stack, SysDispatch } from "$ts/env"; import { Backend } from "$ts/kernel/mods/server/axios"; import { Process } from "$ts/kernel/mods/stack/process/instance"; import { ArcBuild } from "$ts/metadata/build"; @@ -234,7 +234,7 @@ export class TerminalMode extends Process { async loginPrompt(): Promise { this.rl?.println(`ArcTerm ${ArcOSVersion}-${ArcMode()}_${ArcBuild()}\n`); - const username = await this.rl?.read(`arcapi.nl login: `); + const username = await this.rl?.read(`${Server.hostname ?? "ArcOS"} login: `); const password = await this.rl?.read(`Password:`); if (!username || !password) {