Skip to content

Commit 2d7a9c6

Browse files
committed
feat: Uninstall extensions
fix: Make sure requiring ext module for unload also uses webpack bypass fix: Flipped incorrect unload and load in SET_EXTENSION_ENABLED fix: Only show menu seperator when there are extension context buttons present
1 parent 2b20280 commit 2d7a9c6

File tree

13 files changed

+97
-15
lines changed

13 files changed

+97
-15
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export const DownloadExtCommand = 'core-manager.download-extension';
2+
export const UninstallExtCommand = 'core-manager.uninstall-extension';

extensions/core-manager/src/components/ExtensionSubsection.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useAppSelector } from 'flashpoint-launcher-renderer-ext/hooks';
33
import { getExtensionFileURL, runCommand, setExtensionEnabled } from 'flashpoint-launcher-renderer-ext/utils';
44
import { useEffect, useState } from 'react';
55
import { List, RowComponentProps, useDynamicRowHeight } from 'react-window';
6-
import { DownloadExtCommand } from '../commands';
6+
import { DownloadExtCommand, UninstallExtCommand } from '../commands';
77
import { loadExtIndexUrl, ManagerExtensionInfo } from '../extensionLoader';
88

99
export type ExtensionRowProps = {
@@ -14,6 +14,7 @@ export type ExtensionRowProps = {
1414
export function ExtensionSubsection() {
1515
const [availableExtensions, setAvailableExtensions] = useState<ManagerExtensionInfo[]>([]);
1616
const installedExtensions = useAppSelector(state => state.main.extensions);
17+
console.log(installedExtensions);
1718
const disabledExtensions = useAppSelector(state => state.preferences.disabledExtensions);
1819

1920
useEffect(() => {
@@ -66,7 +67,7 @@ export function ExtensionSubsection() {
6667
}
6768

6869
export function ExtensionRow({ items, disabledExtensions, index, style }: RowComponentProps<ExtensionRowProps>) {
69-
const { id, title, description, installed, newestVersion, iconUrl, getDownloadUrl, } = items[index];
70+
const { id, title, description, installed, newestVersion, iconUrl, getDownloadUrl } = items[index];
7071
const [busy, setBusy] = useState(false);
7172
const canInstall = getDownloadUrl !== undefined;
7273
const enabled = !disabledExtensions.includes(id);
@@ -97,7 +98,16 @@ export function ExtensionRow({ items, disabledExtensions, index, style }: RowCom
9798
{ !busy ? (
9899
<>
99100
{ installed && (
100-
<SimpleButton value={'Remove'}/>
101+
<SimpleButton value={'Remove'} onClick={() => {
102+
setBusy(true);
103+
runCommand(UninstallExtCommand, id)
104+
.catch((error) => {
105+
const errorString = `Failed to uninstall extension: ${error}`;
106+
alert(errorString);
107+
log.error('Manager', errorString);
108+
})
109+
.finally(() => setBusy(false));
110+
}}/>
101111
)}
102112
{ canInstall && (
103113
<SimpleButton value={'Install'} onClick={() => {

extensions/core-manager/src/extension.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import axios from 'axios';
2-
import { commands, Disposable, ExtensionContext, installExtension, log, registerDisposable } from 'flashpoint-launcher';
2+
import { commands, Disposable, ExtensionContext, installExtension, log, registerDisposable, uninstallExtension } from 'flashpoint-launcher';
33
import fs from 'node:fs';
44
import os from 'node:os';
55
import path from 'node:path';
6-
import { DownloadExtCommand } from './commands';
6+
import { DownloadExtCommand, UninstallExtCommand } from './commands';
77

88
export async function activate(context: ExtensionContext): Promise<void> {
99
const register = (disp: Disposable) => {
@@ -32,4 +32,12 @@ export async function activate(context: ExtensionContext): Promise<void> {
3232
await installExtension(tempFilepath);
3333
})
3434
);
35+
36+
register(
37+
commands.registerCommand(UninstallExtCommand, async (extId: string) => {
38+
log.info('Uninstalling Extension ' + extId);
39+
40+
await uninstallExtension(extId);
41+
})
42+
);
3543
}

src/back/extensions/ApiImplementation.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { loadCurationIndexImage } from '@back/curate/parse';
55
import { duplicateCuration, genCurationWarnings, makeCurationFromGame, refreshCurationContent } from '@back/curate/util';
66
import { saveCuration } from '@back/curate/write';
77
import { downloadGameData } from '@back/download';
8-
import { installExtension as installExtensionUtil, unzipFile as unzipFileUtil } from '@back/extensions/util';
8+
import { installExtension as installExtensionUtil, uninstallExtension as uninstallExtensionUtil, unzipFile as unzipFileUtil } from '@back/extensions/util';
99
import { genContentTree } from '@back/rust';
1010
import { BackState, StatusState } from '@back/types';
1111
import { awaitDialog } from '@back/util/dialog';
@@ -96,6 +96,10 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest,
9696
return installExtensionUtil(state, filePath);
9797
};
9898

99+
const uninstallExtension = (extId: string) => {
100+
return uninstallExtensionUtil(state, extId);
101+
};
102+
99103
const registerDataProvider = (provider: flashpoint.GameDataProvider): void => {
100104
console.log(`Registered ${provider.id}`);
101105
state.registry.dataSources.set(provider.id, provider);
@@ -686,6 +690,7 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest,
686690
getExtensionFileURL: getExtensionFileURL,
687691
unzipFile,
688692
installExtension,
693+
uninstallExtension,
689694
getExtConfigValue: getExtConfigValue,
690695
setExtConfigValue: setExtConfigValue,
691696
onExtConfigChange: state.apiEmitters.ext.onExtConfigChange.extEvent(extManifest.displayName || extManifest.name),

src/back/extensions/ExtensionService.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export class ExtensionService {
162162
subscriptions: extData.subscriptions
163163
};
164164
if (entryPath) {
165+
console.log('loading ' + entryPath);
165166
const extModule: ExtensionModule = this.require(entryPath);
166167
if (!extModule.activate) {
167168
throw new Error('No "activate" export found in extension module!');
@@ -203,6 +204,18 @@ export class ExtensionService {
203204
}
204205
}
205206

207+
public async removeExtension(id: string): Promise<void> {
208+
await this.unloadExtension(id);
209+
if (this.installedExtensionsReady.isOpen()) {
210+
const extIdx = this._extensions.findIndex(e => e.id == id);
211+
if (extIdx > -1) {
212+
this._extensions.splice(extIdx);
213+
} else {
214+
log.error('Extensions', `Attempted removal of extension ${id}, but no extension with this ID found`);
215+
}
216+
}
217+
}
218+
206219
private async _unloadExtension(ext: IExtension): Promise<void> {
207220
const extData = this._extensionData[ext.id];
208221

@@ -214,7 +227,7 @@ export class ExtensionService {
214227
const entryPath = getExtensionEntry(ext);
215228
if (entryPath) {
216229
try {
217-
const extModule: ExtensionModule = await import(entryPath);
230+
const extModule: ExtensionModule = this.require(entryPath);
218231
if (extModule.deactivate) {
219232
try {
220233
await extModule.deactivate.apply(global);

src/back/extensions/util.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IExtension } from '@shared/extensions/interfaces';
55
import { fixSlashes } from '@shared/Util';
66
import { parseVariableString } from '@shared/utils/VariableString';
77
import { ZipExtractOptions } from 'flashpoint-launcher';
8+
import * as fs from 'fs-extra';
89
import { extractFull } from 'node-7z';
910
import * as path from 'node:path';
1011

@@ -31,6 +32,16 @@ export async function parseAppVar(extId: string, appPath: string, launchCommand:
3132
});
3233
}
3334

35+
export async function uninstallExtension(state: BackState, extId: string) {
36+
console.log('finding ' + extId);
37+
const ext = await state.extensionsService.getExtension(extId);
38+
if (ext) {
39+
await state.extensionsService.removeExtension(extId);
40+
await fs.remove(ext.extensionPath);
41+
}
42+
state.socketServer.broadcast(BackOut.REMOVED_EXTENSION, extId);
43+
}
44+
3445
export async function installExtension(state: BackState, filePath: string) {
3546
const extensionsPath = path.join(state.config.flashpointPath, state.preferences.extensionsPath);
3647

src/back/responses.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,11 +1445,11 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
14451445
if (newState) {
14461446
// Enable ext
14471447
state.preferences.disabledExtensions = state.preferences.disabledExtensions.filter(c => c !== extId);
1448-
await state.extensionsService.unloadExtension(extId);
1448+
await state.extensionsService.loadExtension(extId);
14491449
} else {
14501450
// Disable ext
14511451
state.preferences.disabledExtensions.push(extId);
1452-
await state.extensionsService.loadExtension(extId);
1452+
await state.extensionsService.unloadExtension(extId);
14531453
}
14541454
state.prefsQueue.push(() => {
14551455
PreferencesFile.saveFile(path.join(state.config.flashpointPath, PREFERENCES_FILENAME), state.preferences, state);

src/renderer/components/app.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { setDownloaderState, updateDownloaderStatus, updateDownloaderTask, updat
99
import { setFpfssUser } from '@renderer/store/fpfss/slice';
1010
import { pushHistory } from '@renderer/store/history/slice';
1111
import { addLogEntries, setEntries } from '@renderer/store/logs/slice';
12-
import { addCustomRoute, addGameSidebarComponent, addLoaded, addNewExtension, cancelDialog, changeService, createDialog, openDynamicPage, removeCustomRoute, removeGameSidebarComponent, removeService, setExtOrderablesFromCallback, setMainState, setUnrecoverableError, setUpdateInfo, updateDialog, updateDialogField, updateMetadataSource } from '@renderer/store/main/slice';
12+
import { addCustomRoute, addGameSidebarComponent, addLoaded, addNewExtension, cancelDialog, changeService, createDialog, openDynamicPage, removeCustomRoute, removeExtension, removeGameSidebarComponent, removeService, setExtOrderablesFromCallback, setMainState, setUnrecoverableError, setUpdateInfo, updateDialog, updateDialogField, updateMetadataSource } from '@renderer/store/main/slice';
1313
import { setExtState, setPreferences, updatePreferences } from '@renderer/store/preferences/slice';
1414
import { addData, createViews, GENERAL_VIEW_ID, resetDropdownData, updateGame } from '@renderer/store/search/slice';
1515
import store, { AppDispatch, RootState } from '@renderer/store/store';
@@ -958,6 +958,10 @@ function registerWebsocketListeners(dispatch: AppDispatch) {
958958
}));
959959
});
960960

961+
window.Shared.back.register(BackOut.REMOVED_EXTENSION, async (event, extId) => {
962+
dispatch(removeExtension(extId));
963+
});
964+
961965
window.Shared.back.register(BackOut.ADDED_EXTENSION, async (event, ext) => {
962966
dispatch(addNewExtension(ext));
963967
});

src/renderer/context/MenuContext.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function MenuProvider({ children }: MenuContextProps) {
4343
const htdocsFolderPath = useAppSelector(state => state.preferences.htdocsFolderPath);
4444
const dataPacksFolderPath = useAppSelector(state => state.preferences.dataPacksFolderPath);
4545
const imageFolderPath = useAppSelector(state => state.preferences.imageFolderPath);
46-
const extContextButtons = useAppSelector(state => state.main.contextButtons);
46+
const extContextButtonContribs = useAppSelector(state => state.main.contextButtons);
4747
const scale = useAppSelector(state => state.preferences.scaleValues.menuItem);
4848
const menuItemHeight = Math.floor(calcScale(menuDefHeight, scale));
4949
const navigate = useNavigate();
@@ -211,16 +211,17 @@ export function MenuProvider({ children }: MenuContextProps) {
211211
});
212212
});
213213
}
214-
}, ...fpfssButtons, { type: 'separator' }
214+
}, ...fpfssButtons
215215
];
216216
contextButtons = contextButtons.concat(editingButtons);
217217
}
218218

219219
// Add extension contexts
220-
for (const contribution of extContextButtons) {
220+
const extContextButtons: MenuItemType[] = [];
221+
for (const contribution of extContextButtonContribs) {
221222
for (const contextButton of contribution.value) {
222223
if (contextButton.context === 'game') {
223-
contextButtons.push({
224+
extContextButtons.push({
224225
type: 'button',
225226
label: contextButton.name,
226227
onClick: () => {
@@ -237,6 +238,10 @@ export function MenuProvider({ children }: MenuContextProps) {
237238
}
238239
}
239240

241+
if (extContextButtons.length > 0) {
242+
contextButtons.push({ type: 'separator' });
243+
return contextButtons.concat(extContextButtons);
244+
}
240245
return contextButtons;
241246
};
242247

src/renderer/store/main/middleware.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { startAppListening } from '@renderer/store/listenerMiddleware';
33
import { BackIn } from '@shared/back/types';
44
import { selectGame, selectPlaylist } from '../search/slice';
55
import store from '../store';
6-
import { removePlaylistGame, RemovePlaylistGameAction, resolveDialog, ResolveDialogActionData } from './slice';
6+
import { addNewExtension, removeExtension, removePlaylistGame, RemovePlaylistGameAction, resolveDialog, ResolveDialogActionData, setMainState } from './slice';
77

88
export function addMainMiddleware() {
99
// Send dialog state to event handlers after reducer has finished
@@ -19,6 +19,17 @@ export function addMainMiddleware() {
1919
}
2020
});
2121

22+
startAppListening({
23+
matcher: isAnyOf(removeExtension, addNewExtension),
24+
effect: async () => {
25+
// Refetch extension contributions
26+
window.Shared.back.request(BackIn.GET_RENDERER_EXTENSION_INFO)
27+
.then((data) => {
28+
store.dispatch(setMainState(data));
29+
});
30+
}
31+
});
32+
2233
startAppListening({
2334
matcher: isAnyOf(removePlaylistGame),
2435
effect: async (action: PayloadAction<RemovePlaylistGameAction>, listenerApi) => {

0 commit comments

Comments
 (0)