Skip to content

Commit 66d9b20

Browse files
committed
feat: Custom route support in extensions
1 parent 1614cf0 commit 66d9b20

File tree

6 files changed

+128
-19
lines changed

6 files changed

+128
-19
lines changed

docs/extensions/overview.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,9 @@ However, Applications can also provide `url` or `command` instead. The full deta
291291
- **type** - Type of this configuration prop's value (string, boolean or object)
292292
- **enum** - *(Optional)* - Array of enums to use when you want a Select config box instead of an Input config box.
293293
- **default** - Default value of this configuration prop
294+
295+
# Frontend Extensions
296+
297+
Recommend installing `@reduxjs/toolkit` for type support of dispatch actions and the selector
298+
299+
TODO

src/renderer/components/ActivityRoutes.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { Paths } from '@shared/Paths';
2+
import { CustomRoute } from 'flashpoint-launcher-renderer';
23
import { Activity, ReactNode } from 'react';
34
import { useLocation } from 'react-router-dom';
5+
import { DynamicComponent } from './DynamicComponent';
46
import { CuratePage } from './pages/CuratePage';
7+
import { FpfssPage } from './pages/FpfssPage';
58
import { HomePage } from './pages/HomePage';
69
import { IFramePage } from './pages/IFramePage';
710
import { LogsPage } from './pages/LogsPage';
811
import { TagsPage } from './pages/TagsPage';
9-
import { FpfssPage } from './pages/FpfssPage';
1012

1113
export type ActivityRoutesProps = {
1214
manualUrl: string;
15+
customRoutes: CustomRoute[];
1316
}
1417

15-
export function ActivityRoutes({ manualUrl }: ActivityRoutesProps) {
18+
export function ActivityRoutes({ manualUrl, customRoutes }: ActivityRoutesProps) {
1619
return (
1720
<>
1821
<ActivityRoute path={Paths.HOME} exact>
@@ -33,6 +36,11 @@ export function ActivityRoutes({ manualUrl }: ActivityRoutesProps) {
3336
<ActivityRoute path={Paths.FPFSS}>
3437
<FpfssPage/>
3538
</ActivityRoute>
39+
{ customRoutes.filter(r => r.keepLoaded).map(route =>
40+
<ActivityRoute key={route.path} path={route.path}>
41+
<DynamicComponent name={route.component} props={{}}/>
42+
</ActivityRoute>
43+
)}
3644
</>
3745
);
3846
}

src/renderer/components/Header.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import { RootState } from '@renderer/store/store';
1313
import { getLibraryItemTitle } from '@shared/library/util';
1414
import { Paths } from '@shared/Paths';
1515
import { DialogFieldProps, DialogState, DialogStateTemplate } from 'flashpoint-launcher';
16-
import { MenuItemType } from 'flashpoint-launcher-renderer';
16+
import { CustomHeaderItemProps, MenuItemType } from 'flashpoint-launcher-renderer';
1717
import { Link, useLocation, useNavigate } from 'react-router-dom';
1818
import { toast } from 'react-toastify';
1919
import { joinLibraryRoute, openUrlInWindow } from '../Util';
20+
import { DynamicComponent } from './DynamicComponent';
2021
import { OpenIcon } from './OpenIcon';
2122

2223
const viewDragType = 'text/plain';
@@ -47,6 +48,7 @@ export function Header() {
4748
const fpfssUser = useAppSelector(state => state.fpfss.user);
4849
const fpfssEditsOpen = useAppSelector(state => Object.keys(state.search.views).filter(k => k.startsWith('!fpfss-')).length > 0);
4950
const playlists = useAppSelector(state => state.main.playlists);
51+
const customRoutes = useAppSelector(state => state.main.displaySettings.customRoutes);
5052
const { openMenu } = useContextMenu();
5153
const viewName = useViewName();
5254
const allStrings = useLocalization();
@@ -426,6 +428,22 @@ export function Header() {
426428
title={'FPFSS'}
427429
link={Paths.FPFSS} />
428430
)}
431+
{ customRoutes.filter(r => r.headerItem !== undefined).map(route => {
432+
if (route.headerItem && route.headerItem.component !== undefined) {
433+
const routeProps: CustomHeaderItemProps = {
434+
id: route.headerItem.id,
435+
title: route.headerItem.title,
436+
link: route.path
437+
};
438+
return <DynamicComponent name={route.headerItem.component} props={routeProps} />;
439+
} else {
440+
return <HeaderMenuItem
441+
key={route.path}
442+
id={'header__' + route.headerItem!.id}
443+
link={route.path}
444+
title={route.headerItem!.title}/>;
445+
}
446+
})}
429447
</ul>
430448
</div>
431449
{/* Right-most portion */}

src/renderer/components/app.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ 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 { addLoaded, cancelDialog, changeService, createDialog, openDynamicPage, removeService, setDisplaySettingsFromCallback, setExtOrderablesFromCallback, setMainState, setUpdateInfo, updateDialog, updateDialogField, updateMetadataSource } from '@renderer/store/main/slice';
12+
import { addLoaded, cancelDialog, changeService, createDialog, dsAddCustomRoute, openDynamicPage, removeService, setDisplaySettingsFromCallback, setExtOrderablesFromCallback, setMainState, setUpdateInfo, updateDialog, updateDialogField, updateMetadataSource } from '@renderer/store/main/slice';
1313
import { setPreferences, updatePreferences } from '@renderer/store/preferences/slice';
1414
import { addData, createViews, GENERAL_VIEW_ID, resetDropdownData } from '@renderer/store/search/slice';
1515
import store, { AppDispatch, RootState } from '@renderer/store/store';
1616
import { setTagCategories } from '@renderer/store/tagCategories/slice';
1717
import { addTask, setTask, setTaskBarOpen } from '@renderer/store/tasks/slice';
18+
import { idToGame } from '@renderer/util/async';
1819
import * as extUtils from '@renderer/util/ext';
1920
import { BackIn, BackInit, BackOut, FpfssUser } from '@shared/back/types';
2021
import { APP_TITLE } from '@shared/constants';
@@ -34,6 +35,7 @@ import { LangContext } from '../util/lang';
3435
import { ActivityRoutes } from './ActivityRoutes';
3536
import { Dialog } from './Dialog';
3637
import { GameComponentDropdownSelectField, GameComponentInputField } from './DisplayComponent';
38+
import { DynamicComponent } from './DynamicComponent';
3739
import { DynamicComponentProvider, RemoteModule } from './DynamicComponentProvider';
3840
import { DynamicThemeProvider } from './DynamicThemeProvider';
3941
import { FloatingContainer } from './FloatingContainer';
@@ -134,6 +136,7 @@ export function App() {
134136
const manualUrl = useAppSelector(state => state.preferences.onlineManual || pathToFileUrl(path.join(window.Shared.config.fullFlashpointPath, state.preferences.offlineManual)));
135137
const dynamicThemeFileList = useAppSelector(selectDynamicThemes);
136138
const remoteModules = useAppSelector(selectRemoteModules);
139+
const customRoutes = useAppSelector(state => state.main.displaySettings.customRoutes);
137140
const currentView = useView();
138141
const firstBrowsePageViewName = useAppSelector(state => Object.keys(state.search.views).find(v => v !== GENERAL_VIEW_ID));
139142
const showRightSidebar = currentView?.selectedGame !== undefined && browsePageShowRightSidebar && !hiddenRightSidebarPages.reduce((prev, cur) => prev || location.pathname.startsWith(cur), false);
@@ -219,12 +222,10 @@ export function App() {
219222
<DynamicThemeProvider fileList={dynamicThemeFileList} >
220223
<DynamicComponentProvider manifests={remoteModules}>
221224
<MenuProvider>
222-
223225
<ToastContainer
224226
theme='dark'
225227
className='toast-container'
226228
progressClassName='toast-container-progress'
227-
hideProgressBar={true}
228229
position='bottom-center'/>
229230
{!stopRender ? (
230231
<>
@@ -280,7 +281,8 @@ export function App() {
280281
<>
281282
{ useActivityRoutes && (
282283
<ActivityRoutes
283-
manualUrl={manualUrl} />
284+
manualUrl={manualUrl}
285+
customRoutes={customRoutes} />
284286
)}
285287
<Routes>
286288
<Route
@@ -322,6 +324,12 @@ export function App() {
322324
<Route
323325
path={Paths.DYNAMIC}
324326
element={<DynamicPage name={dynamicPage?.name || ''} props={dynamicPage?.props}/>}/>
327+
{ customRoutes.map(route =>
328+
<Route
329+
key={route.path}
330+
path={route.path}
331+
element={useActivityRoutes ? <></> : <DynamicComponent name={route.component} props={{}}/>}/>
332+
)}
325333
<Route element={<NotFoundPage/>}/>
326334
</Routes>
327335
<Activity mode={isBrowsePage ? 'visible' : 'hidden'}>
@@ -406,10 +414,14 @@ function initApp(dispatch: AppDispatch) {
406414
window.ext = {
407415
utils: {
408416
getPointer,
409-
getFileServerURL: getFileServerURL,
417+
getFileServerURL,
410418
getExtensionFileURL: (extId, filePath) => {
411419
return `${getFileServerURL()}/extdata/${extId}/${filePath}`;
412420
},
421+
idToGame,
422+
runCommand: (command, args) => {
423+
return window.Shared.back.request(BackIn.RUN_COMMAND, command, args);
424+
},
413425
search: {
414426
onWhitelistFactory: extUtils.onWhitelistFactory,
415427
onBlacklistFactory: extUtils.onBlacklistFactory,
@@ -427,12 +439,17 @@ function initApp(dispatch: AppDispatch) {
427439
RandomGames,
428440
},
429441
hooks: {
430-
useNavigate: () => useNavigate(),
442+
useNavigate,
431443
useAppDispatch,
432444
useAppSelector,
433445
useContextMenu,
434446
useLocalization,
435447
},
448+
actions: {
449+
main: {
450+
dsAddCustomRoute,
451+
}
452+
}
436453
};
437454
window.setDisplaySettings = ((cb) => {
438455
dispatch(setDisplaySettingsFromCallback(cb));

src/renderer/store/main/slice.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createLangContainer } from '@shared/lang';
66
import { deepCopy, recursiveReplace } from '@shared/Util';
77
import * as axiosImport from 'axios';
88
import { AppExtConfigData, ComponentStatus, CreditsData, DialogFieldProps, DialogState, ExtensionContribution, Game, GameData, GameMetadataSource, GameOfTheDay, IExtensionDescription, ILogoSet, IService, ITheme, LangContainer, LangFile, MetaUpdateState, PlatformAppPathSuggestions, Playlist, PlaylistGame, ViewGame } from 'flashpoint-launcher';
9-
import { DisplaySettings, DynamicPageProps, ExtOrderable } from 'flashpoint-launcher-renderer';
9+
import { CustomRoute, DisplaySettings, DynamicPageProps, ExtOrderable } from 'flashpoint-launcher-renderer';
1010

1111
export const RANDOM_GAME_ROW_COUNT = 6;
1212

@@ -191,7 +191,8 @@ const DEFAULT_DISPLAYS: DisplaySettings = {
191191
'homePage_extras'
192192
],
193193
searchComponents: [],
194-
browseDisplays: {}
194+
browseDisplays: {},
195+
customRoutes: [],
195196
};
196197

197198
const initialState: MainState = {
@@ -419,6 +420,12 @@ const mainSlice = createSlice({
419420
state.services.splice(serviceIdx, 1);
420421
}
421422
},
423+
dsAddCustomRoute(state: MainState, { payload }: PayloadAction<CustomRoute>) {
424+
const existingIdx = state.displaySettings.customRoutes.findIndex(r => r.path === payload.path);
425+
if (existingIdx === -1) {
426+
state.displaySettings.customRoutes.push(payload);
427+
}
428+
},
422429
setDisplaySettingsFromCallback(state: MainState, { payload }: PayloadAction<DisplaySettingsCallback>) {
423430
try {
424431
state.displaySettings = payload(state.displaySettings);
@@ -478,6 +485,7 @@ export const { setMainState,
478485
openDynamicPage,
479486
changeService,
480487
removeService,
488+
dsAddCustomRoute,
481489
setDisplaySettingsFromCallback,
482490
setExtOrderablesFromCallback,
483491
setUpdateInfo,

typings/flashpoint-launcher.d.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
// tslint:disable:no-declare-current-package
2222

2323
declare module 'flashpoint-launcher' {
24-
import { Readable } from 'stream';
2524
import { FlashpointArchive, GameSearch } from '@fparchive/flashpoint-archive';
25+
import { Readable } from 'stream';
2626

2727
/** Version of the Flashpoint Launcher */
2828
const version: string;
@@ -2864,14 +2864,36 @@ declare module 'flashpoint-launcher' {
28642864
}
28652865

28662866
declare module 'flashpoint-launcher-renderer' {
2867-
import { AppPreferencesData, GameOrderBy, GameOrderReverse, Game, Tag, ViewGame,
2868-
ExtOrder, PlaylistGame, AdvancedFilter, ResultsView, CurationState, CurateGroup,
2869-
GameOfTheDay, GameData, Playlist, DialogState, ComponentStatus, MetaUpdateState,
2870-
IService, AppExtConfigData, LangContainer, LangFile, CreditsData, ITheme,
2871-
ILogoSet, PlatformAppPathSuggestions, IExtensionDescription,
2872-
ExtensionContribution } from 'flashpoint-launcher';
2873-
import { TypedUseSelectorHook } from 'react-redux';
2867+
import { ActionCreatorWithPayload } from '@reduxjs/toolkit';
28742868
import { CancelToken } from 'axios';
2869+
import {
2870+
AdvancedFilter,
2871+
AppExtConfigData,
2872+
AppPreferencesData,
2873+
ComponentStatus,
2874+
CreditsData,
2875+
CurateGroup,
2876+
CurationState,
2877+
DialogState,
2878+
ExtensionContribution,
2879+
ExtOrder,
2880+
Game,
2881+
GameData,
2882+
GameOfTheDay,
2883+
GameOrderBy, GameOrderReverse,
2884+
IExtensionDescription,
2885+
ILogoSet,
2886+
IService,
2887+
ITheme,
2888+
LangContainer, LangFile,
2889+
MetaUpdateState,
2890+
PlatformAppPathSuggestions,
2891+
Playlist,
2892+
PlaylistGame,
2893+
ResultsView,
2894+
Tag, ViewGame
2895+
} from 'flashpoint-launcher';
2896+
import { TypedUseSelectorHook } from 'react-redux';
28752897

28762898
/** Game properties that will have suggestions gathered and displayed. */
28772899
type SuggestionProps = (
@@ -2995,6 +3017,23 @@ declare module 'flashpoint-launcher-renderer' {
29953017

29963018
type GameListColumnInfo = GameListColumnInfoIcon | GameListColumnInfoNormal
29973019

3020+
type CustomHeaderItemProps = {
3021+
id?: string;
3022+
title: string;
3023+
link: string;
3024+
}
3025+
3026+
type CustomRoute = {
3027+
headerItem?: {
3028+
id: string;
3029+
title: string;
3030+
component?: string,
3031+
};
3032+
path: string;
3033+
keepLoaded?: boolean;
3034+
component: string;
3035+
}
3036+
29983037
type DisplaySettings = {
29993038
gameSidebar: {
30003039
middle: string[],
@@ -3009,6 +3048,7 @@ declare module 'flashpoint-launcher-renderer' {
30093048
browseDisplays: {
30103049
[k: string]: any
30113050
},
3051+
customRoutes: CustomRoute[],
30123052
}
30133053

30143054
type SearchableSelectItem = {
@@ -3290,11 +3330,18 @@ declare module 'flashpoint-launcher-renderer' {
32903330
rollRandomGames: () => void;
32913331
};
32923332

3333+
type RunCommandResponse = {
3334+
success: boolean;
3335+
res: any;
3336+
}
3337+
32933338
interface IExtensionWindow {
32943339
utils: {
32953340
getExtensionFileURL: (extId: string, filePath: string) => string;
32963341
getFileServerURL: () => string;
32973342
getPointer: (event: React.MouseEvent<any>) => Pointer;
3343+
idToGame: (gameId: string) => Promise<Game | null>;
3344+
runCommand: (command: string, args?: any[]) => Promise<RunCommandResponse>;
32983345
search: {
32993346
onWhitelistFactory: (extId: string, key: string, filter: AdvancedFilter, setAdvancedFilter: (advFilter: AdvancedFilter) => void) => (value: string) => void,
33003347
onBlacklistFactory: (extId: string, key: string, filter: AdvancedFilter, setAdvancedFilter: (advFilter: AdvancedFilter) => void) => (value: string) => void,
@@ -3318,6 +3365,11 @@ declare module 'flashpoint-launcher-renderer' {
33183365
useContextMenu: () => MenuContextStateProps,
33193366
useLocalization:() => LangContainer,
33203367
},
3368+
actions: {
3369+
main: {
3370+
dsAddCustomRoute: ActionCreatorWithPayload<CustomRoute>
3371+
}
3372+
}
33213373
}
33223374

33233375
declare global {

0 commit comments

Comments
 (0)