Merge pull request #154 from moonlight-mod/kasimir/moonbase-changes
Various changes
Changed files
@@ -4,6 +4,7 @@ "private": true,"dependencies": {"@moonlight-mod/core": "workspace:*","@moonlight-mod/types": "workspace:*",+ "microdiff": "^1.5.0","nanotar": "^0.1.1"}}
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "appPanels","apiLevel": 2,"meta": {
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "common","apiLevel": 2,"meta": {
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "contextMenu","apiLevel": 2,"meta": {
@@ -13,6 +13,7 @@ ]},"settings": {"paths": {+ "advice": "restart","displayName": "Extension Paths","type": "list"}
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "disableSentry","apiLevel": 2,"meta": {
@@ -17,8 +17,6 @@ }},// Enable staff help menu- // FIXME: either make this actually work live (needs a state hook) or just- // wait for #122{find: ".HEADER_BAR)",replace: {@@ -29,7 +27,6 @@ }},// Enable further staff-locked options- // FIXME: #122, this doesn't work live{find: "shouldShowLurkerModeUpsellPopout:",replace: {
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "experiments","apiLevel": 2,"meta": {@@ -9,11 +10,13 @@ "tags": ["dangerZone"]},"settings": {"devtools": {+ "advice": "reload","displayName": "Enable staff help menu (DevTools)","type": "boolean","default": false},"staffSettings": {+ "advice": "reload","displayName": "Allow access to other staff settings elsewhere","type": "boolean","default": false
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "markdown","apiLevel": 2,"meta": {
@@ -0,0 +1,77 @@+import * as electron from "electron";+import * as fs from "node:fs/promises";+import * as path from "node:path";+import getNatives from "./native";++const natives = getNatives();++const confirm = (action: string) =>+ electron.dialog+ .showMessageBox({+ title: "Are you sure?",+ message: `Are you sure? This will ${action} and restart Discord.`,+ type: "warning",+ buttons: ["OK", "Cancel"]+ })+ .then((r) => r.response === 0);++async function updateAndRestart() {+ if (!(await confirm("update moonlight"))) return;+ const newVersion = await natives.checkForMoonlightUpdate();++ if (newVersion === null) {+ electron.dialog.showMessageBox({ message: "You are already on the latest version of moonlight." });+ return;+ }++ try {+ await natives.updateMoonlight();+ await electron.dialog.showMessageBox({ message: "Update successful, restarting Discord." });+ electron.app.relaunch();+ electron.app.exit(0);+ } catch {+ await electron.dialog.showMessageBox({+ message: "Failed to update moonlight. Please use the installer instead.",+ type: "error"+ });+ }+}++async function resetConfig() {+ if (!(await confirm("reset your configuration"))) return;++ const config = await moonlightHost.getConfigPath();+ const dir = path.dirname(config);+ const branch = path.basename(config, ".json");+ await fs.rename(config, path.join(dir, `${branch}-backup-${Math.floor(Date.now() / 1000)}.json`));++ await electron.dialog.showMessageBox({ message: "Configuration reset, restarting Discord." });+ electron.app.relaunch();+ electron.app.exit(0);+}++function showAbout() {+ electron.dialog.showMessageBox({+ title: "About moonlight",+ message: `moonlight ${moonlightHost.branch} ${moonlightHost.version}`+ });+}++electron.app.whenReady().then(() => {+ const original = electron.Menu.buildFromTemplate;+ electron.Menu.buildFromTemplate = function (entries) {+ const i = entries.findIndex((e) => e.label === "Check for Updates...");+ if (i === -1) return original.call(this, entries);++ entries.splice(i + 1, 0, {+ label: "moonlight",+ submenu: [+ { label: "Update and restart", click: updateAndRestart },+ { label: "Reset config", click: resetConfig },+ { label: "About", click: showAbout }+ ]+ });++ return original.call(this, entries);+ };+});
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "moonbase","apiLevel": 2,"meta": {@@ -9,26 +10,30 @@ },"dependencies": ["spacepack", "settings", "common", "notices"],"settings": {"sections": {+ "advice": "reload","displayName": "Split into sections","description": "Show the Moonbase tabs as separate sections","type": "boolean"},"saveFilter": {+ "advice": "none","displayName": "Persist filter","description": "Save extension filter in config","type": "boolean"},"updateChecking": {+ "advice": "none","displayName": "Automatic update checking","description": "Checks for updates to moonlight","type": "boolean",- "default": "true"+ "default": true},"updateBanner": {+ "advice": "none","displayName": "Show update banner","description": "Shows a banner for moonlight and extension updates","type": "boolean",- "default": "true"+ "default": true}},"cors": [
@@ -4,6 +4,8 @@ import extractAsar from "@moonlight-mod/core/asar";import { distDir, repoUrlFile, installedVersionFile } from "@moonlight-mod/types/constants";import { parseTarGzip } from "nanotar";+const moonlightGlobal = globalThis.moonlightHost ?? globalThis.moonlightNode;+const githubRepo = "moonlight-mod/moonlight";const githubApiUrl = `https://api.github.com/repos/${githubRepo}/releases/latest`;const artifactName = "dist.tar.gz";@@ -11,7 +13,7 @@const nightlyRefUrl = "https://moonlight-mod.github.io/moonlight/ref";const nightlyZipUrl = "https://moonlight-mod.github.io/moonlight/dist.tar.gz";-export const userAgent = `moonlight/${moonlightNode.version} (https://github.com/moonlight-mod/moonlight)`;+export const userAgent = `moonlight/${moonlightGlobal.version} (https://github.com/moonlight-mod/moonlight)`;async function getStableRelease(): Promise<{name: string;@@ -30,15 +32,15 @@ return await req.json();}export default function getNatives(): MoonbaseNatives {- const logger = moonlightNode.getLogger("moonbase/natives");+ const logger = moonlightGlobal.getLogger("moonbase/natives");return {async checkForMoonlightUpdate() {try {- if (moonlightNode.branch === MoonlightBranch.STABLE) {+ if (moonlightGlobal.branch === MoonlightBranch.STABLE) {const json = await getStableRelease();- return json.name !== moonlightNode.version ? json.name : null;- } else if (moonlightNode.branch === MoonlightBranch.NIGHTLY) {+ return json.name !== moonlightGlobal.version ? json.name : null;+ } else if (moonlightGlobal.branch === MoonlightBranch.NIGHTLY) {const req = await fetch(nightlyRefUrl, {cache: "no-store",headers: {@@ -46,7 +48,7 @@ "User-Agent": userAgent}});const ref = (await req.text()).split("\n")[0];- return ref !== moonlightNode.version ? ref : null;+ return ref !== moonlightGlobal.version ? ref : null;}return null;@@ -96,15 +98,15 @@ return [await zipReq.arrayBuffer(), ref];}const [tar, ref] =- moonlightNode.branch === MoonlightBranch.STABLE+ moonlightGlobal.branch === MoonlightBranch.STABLE? await downloadStable()- : moonlightNode.branch === MoonlightBranch.NIGHTLY+ : moonlightGlobal.branch === MoonlightBranch.NIGHTLY? await downloadNightly(): [null, null];if (!tar || !ref) return;- const dist = moonlightNodeSandboxed.fs.join(moonlightNode.getMoonlightDir(), distDir);+ const dist = moonlightNodeSandboxed.fs.join(moonlightGlobal.getMoonlightDir(), distDir);if (await moonlightNodeSandboxed.fs.exists(dist)) await moonlightNodeSandboxed.fs.rmdir(dist);await moonlightNodeSandboxed.fs.mkdir(dist);@@ -122,7 +124,7 @@ await moonlightNodeSandboxed.fs.writeFile(fullFile, file.data);}logger.debug("Writing version file:", ref);- const versionFile = moonlightNodeSandboxed.fs.join(moonlightNode.getMoonlightDir(), installedVersionFile);+ const versionFile = moonlightNodeSandboxed.fs.join(moonlightGlobal.getMoonlightDir(), installedVersionFile);await moonlightNodeSandboxed.fs.writeFileString(versionFile, ref.trim());logger.debug("Update extracted");@@ -157,7 +159,7 @@ "User-Agent": userAgent}});- const dir = moonlightNode.getExtensionDir(manifest.id);+ const dir = moonlightGlobal.getExtensionDir(manifest.id);// remake it in case of updatesif (await moonlightNodeSandboxed.fs.exists(dir)) await moonlightNodeSandboxed.fs.rmdir(dir);await moonlightNodeSandboxed.fs.mkdir(dir);@@ -176,7 +178,7 @@ await moonlightNodeSandboxed.fs.writeFileString(moonlightNodeSandboxed.fs.join(dir, repoUrlFile), repo);},async deleteExtension(id) {- const dir = moonlightNode.getExtensionDir(id);+ const dir = moonlightGlobal.getExtensionDir(id);await moonlightNodeSandboxed.fs.rmdir(dir);}};
@@ -7,6 +7,11 @@ .moonbase-settings > :first-child {margin-top: 0px;}+.moonbase-retry-button {+ padding: 8px;+ margin-right: 8px;+}+textarea.moonbase-resizeable {resize: vertical;}@@ -37,6 +42,13 @@ .moonbase-help-message {display: flex;flex-direction: row;justify-content: space-between;+}++.moonbase-help-message-sticky {+ position: sticky;+ top: 24px;+ z-index: 10;+ background-color: var(--background-primary);}.moonbase-extension-update-section {
@@ -38,3 +38,11 @@ Working,Installed,Failed}++// Ordered in terms of priority+export enum RestartAdvice {+ NotNeeded, // No action is needed+ ReloadSuggested, // A reload might be needed+ ReloadNeeded, // A reload is needed+ RestartNeeded // A restart is needed+}
@@ -1,12 +1,10 @@import settings from "@moonlight-mod/wp/settings_settings";import React from "@moonlight-mod/wp/react";import spacepack from "@moonlight-mod/wp/spacepack_spacepack";-import { Moonbase, pages } from "@moonlight-mod/wp/moonbase_ui";+import { Moonbase, pages, RestartAdviceMessage, Update } from "@moonlight-mod/wp/moonbase_ui";import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";import * as Components from "@moonlight-mod/wp/discord/components/common/index";--import Update from "./ui/update";const { MenuItem, Text, Breadcrumbs } = Components;@@ -72,6 +70,7 @@ >{page.name}</Breadcrumbs>+ <RestartAdviceMessage /><Update /><page.element />
@@ -1,5 +1,5 @@-import { Config, ExtensionLoadSource } from "@moonlight-mod/types";-import { ExtensionState, MoonbaseExtension, MoonbaseNatives, RepositoryManifest } from "../types";+import { Config, ExtensionEnvironment, ExtensionLoadSource, ExtensionSettingsAdvice } from "@moonlight-mod/types";+import { ExtensionState, MoonbaseExtension, MoonbaseNatives, RepositoryManifest, RestartAdvice } from "../types";import { Store } from "@moonlight-mod/wp/discord/packages/flux";import Dispatcher from "@moonlight-mod/wp/discord/Dispatcher";import getNatives from "../native";@@ -7,6 +7,7 @@ import { mainRepo } from "@moonlight-mod/types/constants";import { checkExtensionCompat, ExtensionCompat } from "@moonlight-mod/core/extension/loader";import { CustomComponent } from "@moonlight-mod/types/coreExtensions/moonbase";import { getConfigOption, setConfigOption } from "@moonlight-mod/core/util/config";+import diff from "microdiff";const logger = moonlight.getLogger("moonbase");@@ -14,7 +15,8 @@ let natives: MoonbaseNatives = moonlight.getNatives("moonbase");if (moonlightNode.isBrowser) natives = getNatives();class MoonbaseSettingsStore extends Store<any> {- private origConfig: Config;+ private initialConfig: Config;+ private savedConfig: Config;private config: Config;private extensionIndex: number;private configComponents: Record<string, Record<string, CustomComponent>> = {};@@ -35,6 +37,8 @@ get showOnlyUpdateable() {return this.#showOnlyUpdateable;}+ restartAdvice = RestartAdvice.NotNeeded;+extensions: { [id: number]: MoonbaseExtension };updates: {[id: number]: {@@ -47,8 +51,9 @@constructor() {super(Dispatcher);- this.origConfig = moonlightNode.config;- this.config = this.clone(this.origConfig);+ this.initialConfig = moonlightNode.config;+ this.savedConfig = moonlightNode.config;+ this.config = this.clone(this.savedConfig);this.extensionIndex = 0;this.modified = false;@@ -71,74 +76,70 @@ hasUpdate: false};}- natives!- .fetchRepositories(this.config.repositories)- .then((ret) => {- for (const [repo, exts] of Object.entries(ret)) {- try {- for (const ext of exts) {- const uniqueId = this.extensionIndex++;- const extensionData = {- id: ext.id,- uniqueId,- manifest: ext,- source: { type: ExtensionLoadSource.Normal, url: repo },- state: ExtensionState.NotDownloaded,- compat: ExtensionCompat.Compatible,- hasUpdate: false- };+ this.checkUpdates();+ }- // Don't present incompatible updates- if (checkExtensionCompat(ext) !== ExtensionCompat.Compatible) continue;+ async checkUpdates() {+ await Promise.all([this.checkExtensionUpdates(), this.checkMoonlightUpdates()]);+ this.shouldShowNotice = this.newVersion != null || Object.keys(this.updates).length > 0;+ this.emitChange();+ }- const existing = this.getExisting(extensionData);- if (existing != null) {- // Make sure the download URL is properly updated- for (const [id, e] of Object.entries(this.extensions)) {- if (e.id === ext.id && e.source.url === repo) {- this.extensions[parseInt(id)].manifest = {- ...e.manifest,- download: ext.download- };- break;- }- }+ private async checkExtensionUpdates() {+ const repositories = await natives!.fetchRepositories(this.savedConfig.repositories);- if (this.hasUpdate(extensionData)) {- this.updates[existing.uniqueId] = {- version: ext.version!,- download: ext.download,- updateManifest: ext- };- existing.hasUpdate = true;- existing.changelog = ext.meta?.changelog;- }+ // Reset update state+ for (const id in this.extensions) {+ const ext = this.extensions[id];+ ext.hasUpdate = false;+ ext.changelog = undefined;+ }+ this.updates = {};- continue;- }+ for (const [repo, exts] of Object.entries(repositories)) {+ for (const ext of exts) {+ const uniqueId = this.extensionIndex++;+ const extensionData = {+ id: ext.id,+ uniqueId,+ manifest: ext,+ source: { type: ExtensionLoadSource.Normal, url: repo },+ state: ExtensionState.NotDownloaded,+ compat: ExtensionCompat.Compatible,+ hasUpdate: false+ };- this.extensions[uniqueId] = extensionData;- }- } catch (e) {- logger.error(`Error processing repository ${repo}`, e);+ // Don't present incompatible updates+ if (checkExtensionCompat(ext) !== ExtensionCompat.Compatible) continue;++ const existing = this.getExisting(extensionData);+ if (existing != null) {+ // Make sure the download URL is properly updated+ existing.manifest = {+ ...existing.manifest,+ download: ext.download+ };++ if (this.hasUpdate(extensionData)) {+ this.updates[existing.uniqueId] = {+ version: ext.version!,+ download: ext.download,+ updateManifest: ext+ };+ existing.hasUpdate = true;+ existing.changelog = ext.meta?.changelog;}+ } else {+ this.extensions[uniqueId] = extensionData;}+ }+ }+ }- this.emitChange();- })- .then(() =>- this.getExtensionConfigRaw("moonbase", "updateChecking", true)- ? natives!.checkForMoonlightUpdate()- : new Promise<null>((resolve) => resolve(null))- )- .then((version) => {- this.newVersion = version;- this.emitChange();- })- .then(() => {- this.shouldShowNotice = this.newVersion != null || Object.keys(this.updates).length > 0;- this.emitChange();- });+ private async checkMoonlightUpdates() {+ this.newVersion = this.getExtensionConfigRaw("moonbase", "updateChecking", true)+ ? await natives!.checkForMoonlightUpdate()+ : null;}private getExisting(ext: MoonbaseExtension) {@@ -154,7 +155,7 @@ }// Jankprivate isModified() {- const orig = JSON.stringify(this.origConfig);+ const orig = JSON.stringify(this.savedConfig);const curr = JSON.stringify(this.config);return orig !== curr;}@@ -269,8 +270,12 @@ this.extensions[uniqueId].state = ExtensionState.Disabled;}if (update != null) {- this.extensions[uniqueId].settingsOverride = update.updateManifest.settings;- this.extensions[uniqueId].compat = checkExtensionCompat(update.updateManifest);+ const existing = this.extensions[uniqueId];+ existing.settingsOverride = update.updateManifest.settings;+ existing.compat = checkExtensionCompat(update.updateManifest);+ existing.manifest = update.updateManifest;+ existing.hasUpdate = false;+ existing.changelog = update.updateManifest.meta?.changelog;}delete this.updates[uniqueId];@@ -279,6 +284,7 @@ logger.error("Error installing extension:", e);}this.installing = false;+ this.restartAdvice = this.#computeRestartAdvice();this.emitChange();}@@ -310,8 +316,8 @@ deps[dep] = candidates.sort((a, b) => {const aRank = this.getRank(a);const bRank = this.getRank(b);if (aRank === bRank) {- const repoIndex = this.config.repositories.indexOf(a.source.url!);- const otherRepoIndex = this.config.repositories.indexOf(b.source.url!);+ const repoIndex = this.savedConfig.repositories.indexOf(a.source.url!);+ const otherRepoIndex = this.savedConfig.repositories.indexOf(b.source.url!);return repoIndex - otherRepoIndex;} else {return bRank - aRank;@@ -335,6 +341,7 @@ logger.error("Error deleting extension:", e);}this.installing = false;+ this.restartAdvice = this.#computeRestartAdvice();this.emitChange();}@@ -366,11 +373,100 @@ getExtensionConfigComponent(ext: string, name: string) {return this.configComponents[ext]?.[name];}+ #computeRestartAdvice() {+ const i = this.initialConfig; // Initial config, from startup+ const n = this.config; // New config about to be saved++ let returnedAdvice = RestartAdvice.NotNeeded;+ const updateAdvice = (r: RestartAdvice) => (returnedAdvice < r ? (returnedAdvice = r) : returnedAdvice);++ // Top-level keys, repositories is not needed here because Moonbase handles it.+ if (i.patchAll !== n.patchAll) updateAdvice(RestartAdvice.ReloadNeeded);+ if (i.loggerLevel !== n.loggerLevel) updateAdvice(RestartAdvice.ReloadNeeded);+ if (diff(i.devSearchPaths ?? [], n.devSearchPaths ?? [], { cyclesFix: false }).length !== 0)+ return updateAdvice(RestartAdvice.RestartNeeded);++ // Extension specific logic+ for (const id in n.extensions) {+ // Installed extension (might not be detected yet)+ const ext = Object.values(this.extensions).find((e) => e.id === id && e.state !== ExtensionState.NotDownloaded);+ // Installed and detected extension+ const detected = moonlightNode.extensions.find((e) => e.id === id);++ // If it's not installed at all, we don't care+ if (!ext) continue;++ const initState = i.extensions[id];+ const newState = n.extensions[id];++ const newEnabled = typeof newState === "boolean" ? newState : newState.enabled;+ // If it's enabled but not detected yet, restart.+ if (newEnabled && !detected) {+ return updateAdvice(RestartAdvice.RestartNeeded);+ continue;+ }++ // Toggling extensions specifically wants to rely on the initial state,+ // that's what was considered when loading extensions.+ const initEnabled = initState && (typeof initState === "boolean" ? initState : initState.enabled);+ if (initEnabled !== newEnabled) {+ // If we have the extension locally, we confidently know if it has host/preload scripts.+ // If not, we have to respect the environment specified in the manifest.+ // If that is the default, we can't know what's needed.++ if (detected?.scripts.hostPath || detected?.scripts.nodePath) {+ return updateAdvice(RestartAdvice.RestartNeeded);+ }++ switch (ext.manifest.environment) {+ case ExtensionEnvironment.Both:+ case ExtensionEnvironment.Web:+ updateAdvice(RestartAdvice.ReloadNeeded);+ continue;+ case ExtensionEnvironment.Desktop:+ return updateAdvice(RestartAdvice.RestartNeeded);+ default:+ updateAdvice(RestartAdvice.ReloadNeeded);+ continue;+ }+ }++ const initConfig = typeof initState === "boolean" ? {} : initState.config ?? {};+ const newConfig = typeof newState === "boolean" ? {} : newState.config ?? {};++ const def = ext.manifest.settings;+ if (!def) continue;++ const changedKeys = diff(initConfig, newConfig, { cyclesFix: false }).map((c) => c.path[0]);+ for (const key in def) {+ if (!changedKeys.includes(key)) continue;++ const advice = def[key].advice;+ switch (advice) {+ case ExtensionSettingsAdvice.None:+ updateAdvice(RestartAdvice.NotNeeded);+ continue;+ case ExtensionSettingsAdvice.Reload:+ updateAdvice(RestartAdvice.ReloadNeeded);+ continue;+ case ExtensionSettingsAdvice.Restart:+ updateAdvice(RestartAdvice.RestartNeeded);+ continue;+ default:+ updateAdvice(RestartAdvice.ReloadSuggested);+ }+ }+ }++ return returnedAdvice;+ }+writeConfig() {this.submitting = true;+ this.restartAdvice = this.#computeRestartAdvice();moonlightNode.writeConfig(this.config);- this.origConfig = this.clone(this.config);+ this.savedConfig = this.clone(this.config);this.submitting = false;this.modified = false;@@ -380,7 +476,7 @@reset() {this.submitting = false;this.modified = false;- this.config = this.clone(this.origConfig);+ this.config = this.clone(this.savedConfig);this.emitChange();}
@@ -11,16 +11,18 @@ export default function HelpMessage({className,text,icon,- children+ children,+ type = "info"}: {className?: string;text: string;icon: React.ComponentType<any>;+ type?: "warning" | "positive" | "error" | "info";children?: React.ReactNode;}) {return (<div- className={`${Margins.marginBottom20} ${HelpMessageClasses.info} ${HelpMessageClasses.container} moonbase-help-message ${className}`}+ className={`${Margins.marginBottom20} ${HelpMessageClasses[type]} ${HelpMessageClasses.container} moonbase-help-message ${className}`}><Flex direction={Flex.Direction.HORIZONTAL}><div
@@ -0,0 +1,47 @@+import { useStateFromStores } from "@moonlight-mod/wp/discord/packages/flux";+import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";+import * as Components from "@moonlight-mod/wp/discord/components/common/index";+import React from "@moonlight-mod/wp/react";+import { RestartAdvice } from "../../types";+import HelpMessage from "./HelpMessage";++const { Button } = Components;++const strings: Record<RestartAdvice, string> = {+ [RestartAdvice.NotNeeded]: "how did you even",+ [RestartAdvice.ReloadSuggested]: "A reload might be needed to apply some of the changed options.",+ [RestartAdvice.ReloadNeeded]: "A reload is needed to apply some of the changed options.",+ [RestartAdvice.RestartNeeded]: "A restart is needed to apply some of the changed options."+};++const buttonStrings: Record<RestartAdvice, string> = {+ [RestartAdvice.NotNeeded]: "huh?",+ [RestartAdvice.ReloadSuggested]: "Reload",+ [RestartAdvice.ReloadNeeded]: "Reload",+ [RestartAdvice.RestartNeeded]: "Restart"+};++const actions: Record<RestartAdvice, () => void> = {+ [RestartAdvice.NotNeeded]: () => {},+ [RestartAdvice.ReloadSuggested]: () => window.location.reload(),+ [RestartAdvice.ReloadNeeded]: () => window.location.reload(),+ [RestartAdvice.RestartNeeded]: () => MoonbaseSettingsStore.restartDiscord()+};++const { CircleWarningIcon } = Components;++export default function RestartAdviceMessage() {+ const restartAdvice = useStateFromStores([MoonbaseSettingsStore], () => MoonbaseSettingsStore.restartAdvice);++ if (restartAdvice === RestartAdvice.NotNeeded) return null;++ return (+ <div className="moonbase-help-message-sticky">+ <HelpMessage text={strings[restartAdvice]} icon={CircleWarningIcon} type="warning">+ <Button color={Button.Colors.YELLOW} size={Button.Sizes.TINY} onClick={actions[restartAdvice]}>+ {buttonStrings[restartAdvice]}+ </Button>+ </HelpMessage>+ </div>+ );+}
@@ -23,7 +23,7 @@ }import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";-const { BeakerIcon, DownloadIcon, TrashIcon, AngleBracketsIcon, CircleWarningIcon, Tooltip } = Components;+const { BeakerIcon, DownloadIcon, TrashIcon, AngleBracketsIcon, Tooltip } = Components;const PanelButton = spacepack.findByCode("Masks.PANEL_BUTTON")[0].exports.Z;const TabBarClasses = spacepack.findByExports("tabBar", "tabBarItem", "headerContentWrapper")[0].exports;@@ -40,14 +40,12 @@ const CONFLICTING_TEXT = "This extension is already installed from another source.";export default function ExtensionCard({ uniqueId }: { uniqueId: number }) {const [tab, setTab] = React.useState(ExtensionPage.Info);- const [restartNeeded, setRestartNeeded] = React.useState(false);- const { ext, enabled, busy, update, conflicting, showingNotice } = useStateFromStores([MoonbaseSettingsStore], () => {+ const { ext, enabled, busy, update, conflicting } = useStateFromStores([MoonbaseSettingsStore], () => {return {ext: MoonbaseSettingsStore.getExtension(uniqueId),enabled: MoonbaseSettingsStore.getExtensionEnabled(uniqueId),busy: MoonbaseSettingsStore.busy,- showingNotice: MoonbaseSettingsStore.showNotice(),update: MoonbaseSettingsStore.getExtensionUpdate(uniqueId),conflicting: MoonbaseSettingsStore.getExtensionConflicting(uniqueId)};@@ -111,14 +109,6 @@ }}/>)}- {restartNeeded && !showingNotice && (- <PanelButton- icon={() => <CircleWarningIcon color={Components.tokens.colors.STATUS_DANGER} />}- onClick={() => MoonbaseSettingsStore.restartDiscord()}- tooltipText="You will need to reload/restart your client for this extension to work properly."- />- )}-{ext.state === ExtensionState.NotDownloaded ? (<Tooltiptext={conflicting ? CONFLICTING_TEXT : COMPAT_TEXT_MAP[ext.compat]}@@ -141,8 +131,6 @@ // Don't auto enable dangerous extensionsif (!ext.manifest?.meta?.tags?.includes(ExtensionTag.DangerZone)) {MoonbaseSettingsStore.setExtensionEnabled(uniqueId, true);}-- setRestartNeeded(true);}}>Install@@ -157,7 +145,6 @@ icon={TrashIcon}tooltipText="Delete"onClick={() => {MoonbaseSettingsStore.deleteExtension(uniqueId);- setRestartNeeded(true);}}/>)}@@ -168,7 +155,6 @@ icon={DownloadIcon}tooltipText="Update"onClick={() => {MoonbaseSettingsStore.installExtension(uniqueId);- setRestartNeeded(true);}}/>)}@@ -190,7 +176,6 @@ }onChange={() => {const toggle = () => {MoonbaseSettingsStore.setExtensionEnabled(uniqueId, !enabled);- setRestartNeeded(true);};if (enabled && constants.builtinExtensions.includes(ext.id)) {
@@ -16,6 +16,7 @@ MenuCheckboxItem,MenuItem} from "@moonlight-mod/wp/discord/components/common/index";import * as Components from "@moonlight-mod/wp/discord/components/common/index";+import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";export enum Filter {Core = 1 << 0,@@ -44,7 +45,7 @@const TagItem = spacepack.findByCode('"forum-tag-"')[0].exports.Z;// FIXME: type component keys-const { ChevronSmallDownIcon, ChevronSmallUpIcon, ArrowsUpDownIcon } = Components;+const { ChevronSmallDownIcon, ChevronSmallUpIcon, ArrowsUpDownIcon, RetryIcon, Tooltip } = Components;function toggleTag(selectedTags: Set<string>, setSelectedTags: (tags: Set<string>) => void, tag: string) {const newState = new Set(selectedTags);@@ -211,7 +212,7 @@ offset = newOffset;}}setTagsButtonOffset(offset);- }, [windowSize]);+ }, [windowSize, tagsContainer.current, tagListInner.current, tagListInner.current?.getBoundingClientRect()?.width]);return (<div@@ -221,6 +222,20 @@ paddingTop: "12px"}}className={`${FilterBarClasses.tagsContainer} ${Margins.marginBottom8}`}>+ <Tooltip text="Refresh updates" position="top">+ {(props: any) => (+ <Button+ {...props}+ size={Button.Sizes.MIN}+ color={Button.Colors.CUSTOM}+ className={`${FilterBarClasses.sortDropdown} moonbase-retry-button`}+ innerClassName={FilterBarClasses.sortDropdownInner}+ onClick={() => MoonbaseSettingsStore.checkUpdates()}+ >+ <RetryIcon size={"custom"} width={16} />+ </Button>+ )}+ </Tooltip><PopoutrenderPopout={({ closePopout }: any) => (<FilterButtonPopout filter={filter} setFilter={setFilter} closePopout={closePopout} />@@ -305,6 +320,22 @@ )}</Button>)}</Popout>+ <Button+ size={Button.Sizes.MIN}+ color={Button.Colors.CUSTOM}+ className={`${FilterBarClasses.tagsButton} ${FilterBarClasses.tagsButtonPlaceholder}`}+ innerClassName={FilterBarClasses.tagsButtonInner}+ >+ {selectedTags.size > 0 ? (+ <div style={{ boxSizing: "content-box" }} className={FilterBarClasses.countContainer}>+ <Text className={FilterBarClasses.countText} color="none" variant="text-xs/medium">+ {selectedTags.size}+ </Text>+ </div>+ ) : null}++ <ChevronSmallUpIcon size={"custom"} width={20} />+ </Button></div>);}
@@ -13,7 +13,7 @@ import { ExtensionCompat } from "@moonlight-mod/core/extension/loader";import HelpMessage from "../HelpMessage";const SearchBar: any = Object.values(spacepack.findByCode("hideSearchIcon")[0].exports)[0];-const { CircleInformationIcon, XSmallIcon } = Components;+const { FormDivider, CircleInformationIcon, XSmallIcon } = Components;const PanelButton = spacepack.findByCode("Masks.PANEL_BUTTON")[0].exports.Z;export default function ExtensionsPage() {@@ -77,8 +77,8 @@ );// Prioritize extensions with updatesconst filteredWithUpdates = filtered.filter((ext) => ext!.hasUpdate);- const filteredWithoutUpdates = filtered.filter((ext) => !ext!.hasUpdate);- const { FormDivider } = Components;+ const filterUpdates = showOnlyUpdateable && filteredWithUpdates.length > 0;+ const filteredWithoutUpdates = filterUpdates ? [] : filtered.filter((ext) => !ext!.hasUpdate);return (<>@@ -97,7 +97,7 @@ }}/><FilterBar filter={filter} setFilter={setFilter} selectedTags={selectedTags} setSelectedTags={setSelectedTags} />- {showOnlyUpdateable && (+ {filterUpdates && (<HelpMessageicon={CircleInformationIcon}text="Only displaying updates"@@ -117,16 +117,12 @@{filteredWithUpdates.map((ext) => (<ExtensionCard uniqueId={ext.uniqueId} key={ext.uniqueId} />))}- {!showOnlyUpdateable && (- <>- {filteredWithUpdates.length > 0 && filteredWithoutUpdates.length > 0 && (- <FormDivider className="moonbase-update-divider" />- )}- {filteredWithoutUpdates.map((ext) => (- <ExtensionCard uniqueId={ext.uniqueId} key={ext.uniqueId} />- ))}- </>+ {filteredWithUpdates.length > 0 && filteredWithoutUpdates.length > 0 && (+ <FormDivider className="moonbase-update-divider" />)}+ {filteredWithoutUpdates.map((ext) => (+ <ExtensionCard uniqueId={ext.uniqueId} key={ext.uniqueId} />+ ))}</>);}
@@ -8,6 +8,7 @@ import ExtensionsPage from "./extensions";import ConfigPage from "./config";import Update from "./update";import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";+import RestartAdviceMessage from "./RestartAdvice";const { Divider } = spacepack.findByCode(".forumOrHome]:")[0].exports.Z;const TitleBarClasses = spacepack.findByCode("iconWrapper:", "children:")[0].exports;@@ -66,9 +67,12 @@ ))}</TabBar></div>+ <RestartAdviceMessage /><Update />{React.createElement(pages[subsection].element)}</>);}++export { RestartAdviceMessage, Update };
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "nativeFixes","meta": {"name": "Native Fixes",@@ -6,32 +7,38 @@ "tagline": "Various configurable fixes for Discord and Electron","authors": ["Cynosphere", "adryd"],"tags": ["fixes"]},+ "environment": "desktop","settings": {"devtoolsThemeFix": {+ "advice": "restart","displayName": "Devtools Theme Fix","description": "Temporary workaround for devtools defaulting to light theme on Electron 32","type": "boolean","default": true},"disableRendererBackgrounding": {+ "advice": "restart","displayName": "Disable Renderer Backgrounding","description": "This is enabled by default as a power saving measure, but it breaks screensharing and websocket connections fairly often","type": "boolean","default": true},"linuxAutoscroll": {+ "advice": "restart","displayName": "Enable middle click autoscroll on Linux","description": "Requires manual configuration of your system to disable middle click paste, has no effect on other operating systems","type": "boolean","default": false},"linuxSpeechDispatcher": {+ "advice": "restart","displayName": "Enable speech-dispatcher for TTS on Linux","description": "Fixes text-to-speech. Has no effect on other operating systems","type": "boolean","default": true},"vaapi": {+ "advice": "restart","displayName": "Enable VAAPI features on Linux","description": "Provides hardware accelerated video encode and decode. Has no effect on other operating systems","type": "boolean",
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "noHideToken","apiLevel": 2,"meta": {
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "noTrack","apiLevel": 2,"meta": {
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "notices","apiLevel": 2,"meta": {
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "quietLoggers","apiLevel": 2,"meta": {@@ -9,6 +10,7 @@ "tags": ["development"]},"settings": {"xssDefensesOnly": {+ "advice": "reload","displayName": "Only hide self-XSS","description": "Only disable self XSS prevention log","type": "boolean",
@@ -1,6 +1,8 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "rocketship","apiLevel": 2,+ "environment": "desktop","meta": {"name": "Rocketship","tagline": "Adds new features when using rocketship",
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "settings","apiLevel": 2,"meta": {
@@ -1,4 +1,5 @@{+ "$schema": "https://moonlight-mod.github.io/manifest.schema.json","id": "spacepack","apiLevel": 2,"meta": {@@ -9,6 +10,7 @@ "tags": ["library", "development"]},"settings": {"addToGlobalScope": {+ "advice": "reload","displayName": "Add to global scope","description": "Populates window.spacepack for easier usage in DevTools","type": "boolean",
MODIFIED
packages/core/src/patch.ts
MODIFIED
packages/core/src/patch.ts
@@ -101,7 +101,6 @@ moonlight.moonmap.parseScript(id, moduleCache[id]);}}- let modified = false;for (const [id, func] of Object.entries(entry)) {if (func.__moonlight === true) continue;@@ -110,6 +109,7 @@ const origModuleString = moduleCache[id];let moduleString = origModuleString;const patchedStr = [];const mappedName = moonlight.moonmap.modules[id];+ let modified = false;for (let i = 0; i < patches.length; i++) {const patch = patches[i];
MODIFIED
packages/injector/src/index.ts
MODIFIED
packages/injector/src/index.ts
@@ -15,7 +15,7 @@ import path from "node:path";import persist from "@moonlight-mod/core/persist";import createFS from "@moonlight-mod/core/fs";import { getConfigOption, getManifest, setConfigOption } from "@moonlight-mod/core/util/config";-import { getExtensionsPath, getMoonlightDir } from "@moonlight-mod/core/util/data";+import { getConfigPath, getExtensionsPath, getMoonlightDir } from "@moonlight-mod/core/util/data";const logger = new Logger("injector");@@ -223,7 +223,6 @@ const val = config.extensions[ext];if (val == null || typeof val === "boolean") return undefined;return val.config;}-global.moonlightHost = {get config() {return config;@@ -237,6 +236,7 @@ version: MOONLIGHT_VERSION,branch: MOONLIGHT_BRANCH as MoonlightBranch,getConfig,+ getConfigPath,getConfigOption(ext, name) {const manifest = getManifest(extensions, ext);return getConfigOption(ext, name, config, manifest?.settings);
MODIFIED
packages/types/src/config.ts
MODIFIED
packages/types/src/config.ts
@@ -81,9 +81,16 @@ type: ExtensionSettingType.Custom;default?: any;};+export enum ExtensionSettingsAdvice {+ None = "none",+ Reload = "reload",+ Restart = "restart"+}+export type ExtensionSettingsManifest = {displayName?: string;description?: string;+ advice?: ExtensionSettingsAdvice;} & (| BooleanSettingType| NumberSettingType
MODIFIED
packages/types/src/globals.ts
MODIFIED
packages/types/src/globals.ts
@@ -18,6 +18,7 @@ version: string;branch: MoonlightBranch;getConfig: (ext: string) => ConfigExtension["config"];+ getConfigPath: () => Promise<string>;getConfigOption: <T>(ext: string, name: string) => T | undefined;setConfigOption: <T>(ext: string, name: string, value: T) => void;writeConfig: (config: Config) => Promise<void>;
MODIFIED
pnpm-lock.yaml
MODIFIED
pnpm-lock.yaml
@@ -10,7 +10,7 @@ .:devDependencies:'@moonlight-mod/eslint-config':specifier: github:moonlight-mod/eslint-config- version: https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/7eb7bd7c51fe0e3ee9d2a0baf149212d2bb893af(eslint@9.12.0)(prettier@3.1.0)(typescript@5.3.2)+ version: https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/e262ac24e1a0955a9b3e0d66da247a0a8c0446c9(eslint@9.12.0)(prettier@3.1.0)(typescript@5.3.2)esbuild:specifier: ^0.19.3version: 0.19.3@@ -62,6 +62,9 @@ version: link:../core'@moonlight-mod/types':specifier: workspace:*version: link:../types+ microdiff:+ specifier: ^1.5.0+ version: 1.5.0nanotar:specifier: ^0.1.1version: 0.1.1@@ -311,9 +314,9 @@ '@humanwhocodes/retry@0.3.1':resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}engines: {node: '>=18.18'}- '@moonlight-mod/eslint-config@https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/7eb7bd7c51fe0e3ee9d2a0baf149212d2bb893af':- resolution: {tarball: https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/7eb7bd7c51fe0e3ee9d2a0baf149212d2bb893af}- version: 1.0.0+ '@moonlight-mod/eslint-config@https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/e262ac24e1a0955a9b3e0d66da247a0a8c0446c9':+ resolution: {tarball: https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/e262ac24e1a0955a9b3e0d66da247a0a8c0446c9}+ version: 1.0.1peerDependencies:eslint: '>= 9'typescript: '>= 5.3'@@ -1003,6 +1006,9 @@meriyah@6.0.1:resolution: {integrity: sha512-OyvYIOgpzXREySYJ1cqEb2pOKdeQMTfF9M8dRU6nC4hi/GXMmNpe9ssZCrSoTHazu05BSAoRBN/uYeco+ymfOg==}engines: {node: '>=18.0.0'}++ microdiff@1.5.0:+ resolution: {integrity: sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q==}micromatch@4.0.8:resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}@@ -1433,7 +1439,7 @@ '@humanwhocodes/module-importer@1.0.1': {}'@humanwhocodes/retry@0.3.1': {}- '@moonlight-mod/eslint-config@https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/7eb7bd7c51fe0e3ee9d2a0baf149212d2bb893af(eslint@9.12.0)(prettier@3.1.0)(typescript@5.3.2)':+ '@moonlight-mod/eslint-config@https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/e262ac24e1a0955a9b3e0d66da247a0a8c0446c9(eslint@9.12.0)(prettier@3.1.0)(typescript@5.3.2)':dependencies:'@eslint/js': 9.12.0eslint: 9.12.0@@ -2293,6 +2299,8 @@merge2@1.4.1: {}meriyah@6.0.1: {}++ microdiff@1.5.0: {}micromatch@4.0.8:dependencies: