Skip to content

Commit ae2629b

Browse files
committed
feat: Tie FPFSS support to game owner / game metadata source
1 parent b07d162 commit ae2629b

23 files changed

Lines changed: 338 additions & 208 deletions

File tree

docs/configuration/preferences.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ For deployment it is recommended to not ship with a `preferences.json` file, but
6565
| `offlineManual` | Hidden | Relative or Absolute Path to the offline manual for the help page in situations without internet access. |
6666
| `gameDataSources` | Hidden | Specifies sources from which game data will be attempted to download from. Can support multiple mirrors. |
6767
| `gameMetadataSources` | Hidden | Specifies sources from which game metadata can be synced. (Shown on the home page, only 1 safely supported currently) |
68-
| `fpfssBaseUrl` | Hidden | Base URL for the FPFSS server, used for authoring actions on remote metadata. |
6968
| `server` | Config Page | The name of the server process to run. See [Services](services) |
7069
| `curateServer` | Config Page | The name of the server process to run when running curations on the Curate page. See [Services](services) |
7170
| `enablePlaytimeTracking` | Config Page | Whether to track playtime for games in the database. |

src/back/extensions/ApiImplementation.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -674,12 +674,16 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest,
674674
};
675675

676676
const extFpfss: typeof flashpoint.fpfss = {
677-
getAccessToken: async (): Promise<string> => {
677+
getAccessToken: async (sourceId: string): Promise<string> => {
678+
const source = state.preferences.gameMetadataSources.find(s => s.id === sourceId);
679+
if (!source) {
680+
throw 'No source found for ' + sourceId;
681+
}
678682
if (!state.socketServer.lastClient) {
679683
throw new Error('No connected client to handle FPFSS action.');
680684
}
681685
try {
682-
const user = await state.socketServer.request(state.socketServer.lastClient, BackOut.FPFSS_ACTION, extId);
686+
const { user } = await state.socketServer.request(state.socketServer.lastClient, BackOut.FPFSS_ACTION, source, extId);
683687
if (user && user.accessToken) {
684688
return user.accessToken;
685689
} else {
@@ -688,7 +692,7 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest,
688692
} catch (error) {
689693
const client = state.socketServer.lastClient;
690694
const openDialog = state.socketServer.showMessageBoxBack(state, client);
691-
await openDialog({
695+
openDialog({
692696
largeMessage: true,
693697
message: (error instanceof Error) ? error.message : String(error),
694698
buttons: [state.languageContainer.misc.ok]

src/back/responses.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import {
33
GameSearchOffset,
44
GameSearchSortable,
55
newSubfilter,
6-
PartialTagCategory
6+
PartialTagCategory,
7+
RemoteGamesRes
78
} from '@fparchive/flashpoint-archive';
89
import { LogLevel } from '@shared/Log/interface';
910
import { MetaEditFile, MetaEditMeta } from '@shared/MetaEdit';
10-
import { deepCopy, downloadFile, padEnd, sizeToString } from '@shared/Util';
11+
import { deepCopy, downloadFile, mapFpfssGameToLocal, padEnd, sizeToString } from '@shared/Util';
1112
import {
1213
BackIn,
1314
BackInit,
@@ -414,6 +415,49 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
414415
}
415416
});
416417

418+
state.socketServer.register(BackIn.UPDATE_GAME_FROM_SOURCE, async (event, gameId) => {
419+
const game = await fpDatabase.findGame(gameId);
420+
if (game) {
421+
const source = state.preferences.gameMetadataSources.find(s => s.id === game.owner);
422+
if (source) {
423+
const url = source.baseUrl + '/api/game/' + game.id;
424+
console.log(url);
425+
const res = await axios.get(url);
426+
console.log(JSON.stringify(res.data, undefined, 2));
427+
const newGame = mapFpfssGameToLocal(res.data, source.id);
428+
const tagRelations: Array<Array<string>> = [];
429+
const platformRelations: Array<Array<string>> = [];
430+
for (const tag of newGame.tags) {
431+
const t = await fpDatabase.findTag(tag);
432+
if (t) {
433+
tagRelations.push([game.id, String(t.id)]);
434+
}
435+
}
436+
for (const platform of newGame.platforms) {
437+
const p = await fpDatabase.findTag(platform);
438+
if (p) {
439+
platformRelations.push([game.id, String(p.id)]);
440+
}
441+
}
442+
const update: RemoteGamesRes = {
443+
games: [{
444+
...newGame,
445+
applicationPath: newGame.legacyApplicationPath,
446+
launchCommand: newGame.legacyLaunchCommand,
447+
platformName: newGame.primaryPlatform
448+
}],
449+
addApps: newGame.addApps ? newGame.addApps : [],
450+
gameData: newGame.gameData ? newGame.gameData : [],
451+
tagRelations,
452+
platformRelations,
453+
};
454+
console.log(JSON.stringify(update, undefined, 2));
455+
await fpDatabase.updateApplyGames(update, source.id);
456+
broadcastGameUpdate(state, game.id);
457+
}
458+
}
459+
});
460+
417461
state.socketServer.register(BackIn.SYNC_ALL, async (event, source) => {
418462
if (state.updateInProgress) {
419463
const openDialog = state.socketServer.showMessageBoxBack(state, event.client);

src/main/Main.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,15 @@ export function main(init: Init): void {
364364
const hostname = new URL(state.preferences.onlineManual).hostname;
365365
allowedHosts.push(hostname);
366366
}
367-
if (state.preferences && state.preferences.fpfssBaseUrl) {
368-
const hostname = new URL(state.preferences.fpfssBaseUrl).hostname;
369-
allowedHosts.push(hostname);
367+
if (state.preferences && state.preferences.gameMetadataSources) {
368+
for (const source of state.preferences.gameMetadataSources) {
369+
if (source.fpfssUrl) {
370+
const hostname = new URL(source.fpfssUrl).hostname;
371+
allowedHosts.push(hostname);
372+
}
373+
const hostname = new URL(source.baseUrl).hostname;
374+
allowedHosts.push(hostname);
375+
}
370376
}
371377
const allow = (
372378
url && (

src/renderer/components/GameGrid.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export type GameGridProps<T extends Content> = BrowsePageDisplayProps<T> & {
4242
/** Called when the user attempts to deselect a game. */
4343
onContentDeselect: (gameId?: string, col?: number, row?: number) => void;
4444
/** Called when the user attempts to open a context menu (at a game). */
45-
onContextMenu?: (event: React.MouseEvent, gameId: string, logoPath: string, screenshotPath: string) => void;
45+
onContextMenu?: (event: React.MouseEvent, sourceId: string, gameId: string, logoPath: string, screenshotPath: string) => void;
4646
/** Called when the user starts to drag a game. */
4747
onContentDragStart?: (event: React.DragEvent, dragEventData: GameDragEventData) => void;
4848
/** Called when the user stops dragging a game (when they release it). */
@@ -354,9 +354,9 @@ export class GameGrid<T extends Content> extends React.Component<GameGridProps<T
354354
* @param event React event
355355
* @param gameId ID of Game to open context menu for
356356
*/
357-
onGameContextMenu = (event: React.MouseEvent<HTMLDivElement>, gameId: string | undefined, logoPath: string, screenshotPath: string): void => {
357+
onGameContextMenu = (event: React.MouseEvent<HTMLDivElement>, sourceId: string, gameId: string, logoPath: string, screenshotPath: string): void => {
358358
if (this.props.onContextMenu) {
359-
if (gameId) { this.props.onContextMenu(event, gameId, logoPath, screenshotPath); }
359+
if (gameId) { this.props.onContextMenu(event, sourceId, gameId, logoPath, screenshotPath); }
360360
}
361361
};
362362

src/renderer/components/GameGridItem.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export function GameGridItem<T extends Content>(props: GameGridItemProps<T>) {
6565
if (isDragged) { className += ' game-grid-item--dragged'; }
6666

6767
const attributes: any = {};
68+
attributes[GameGridItem.sourceAttribute] = game?.owner;
6869
attributes[GameGridItem.idAttribute] = game?.id;
6970
attributes[GameGridItem.indexAttribute] = rowIndex;
7071
attributes[GameGridItem.logoPathAttribute] = props.game?.logoPath;
@@ -114,6 +115,7 @@ export function GameGridItem<T extends Content>(props: GameGridItemProps<T>) {
114115

115116
export namespace GameGridItem {
116117
/** ID of the attribute used to store the game's id. */
118+
export const sourceAttribute = 'data-source-id';
117119
export const idAttribute = 'data-game-id';
118120
export const indexAttribute = 'data-game-index';
119121
export const logoPathAttribute = 'data-game-logo-path';
@@ -125,12 +127,14 @@ export namespace GameGridItem {
125127
* @param element GameGridItem element.
126128
*/
127129
export function getDragEventData(element: Element): GameDragEventData {
128-
const gameId = element.getAttribute(GameGridItem.idAttribute);
130+
const sourceId = element.getAttribute(GameGridItem.sourceAttribute) || '';
131+
const gameId = element.getAttribute(GameGridItem.idAttribute) || '';
129132
const index = num(element.getAttribute(GameGridItem.indexAttribute));
130133
const logoPath = element.getAttribute(GameGridItem.logoPathAttribute) || '';
131134
const screenshotPath = element.getAttribute(GameGridItem.screenshotPathAttribute) || '';
132135
if (typeof gameId !== 'string') { throw new Error('Failed to get ID from GameListItem element. Attribute not found.'); }
133136
return {
137+
sourceId,
134138
gameId,
135139
index,
136140
logoPath,

src/renderer/components/GameItemContainer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type GameItemContainerProps = HTMLDivProps & {
1212
onContentDeselect?: (event: React.MouseEvent<HTMLDivElement>, gameId: string) => void;
1313
onContentLaunch?: (event: React.MouseEvent<HTMLDivElement>, gameId: string) => void;
1414
selectedGameId?: string;
15-
onGameContextMenu?: (event: React.MouseEvent<HTMLDivElement>, gameId: string, logoPath: string, screenshotPath: string) => void;
15+
onGameContextMenu?: (event: React.MouseEvent<HTMLDivElement>, sourceId: string, gameId: string, logoPath: string, screenshotPath: string) => void;
1616
onGameDragStart?: (event: React.DragEvent<HTMLDivElement>, dragEventData: GameDragEventData) => void;
1717
onGameDragEnd?: (event: React.DragEvent<HTMLDivElement>) => void;
1818
onGameDrop?: (event: React.DragEvent) => void;
@@ -106,7 +106,7 @@ export function GameItemContainer(props: GameItemContainerProps) {
106106
if (props.onContextMenu) { props.onContextMenu(event); }
107107
if (props.onGameContextMenu) {
108108
const dragData = findGameDragEventData(event.target);
109-
if (dragData?.gameId !== undefined) { props.onGameContextMenu(event, dragData?.gameId, dragData?.logoPath, dragData?.screenshotPath); }
109+
if (dragData?.gameId !== undefined) { props.onGameContextMenu(event, dragData?.sourceId, dragData?.gameId, dragData?.logoPath, dragData?.screenshotPath); }
110110
}
111111
};
112112

src/renderer/components/GameList.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export type GameListProps<T extends Content> = BrowsePageDisplayProps<T> & {
4040
/** Called when the user attempts to launch a game. */
4141
onContentLaunch: (gameId: string, override: GameLaunchOverride) => void;
4242
/** Called when the user attempts to open a context menu (at a game). */
43-
onContextMenu: (event: React.MouseEvent, gameId: string, logoPath: string, screenshotPath: string) => void;
43+
onContextMenu: (event: React.MouseEvent, sourceId: string, gameId: string, logoPath: string, screenshotPath: string) => void;
4444
/** Called when the user starts to drag a game. */
4545
onGameDragStart: (event: React.DragEvent, dragEventData: GameDragEventData) => void;
4646
/** Called when the user stops dragging a game (when they release it). */
@@ -265,8 +265,8 @@ export class GameList<T extends Content> extends React.Component<GameListProps<T
265265
* @param event React event
266266
* @param gameId ID of Game to open context meny for
267267
*/
268-
onGameContextMenu = (event: React.MouseEvent<HTMLDivElement>, gameId: string, logoPath: string, screenshotPath: string): void => {
269-
this.props.onContextMenu(event, gameId, logoPath, screenshotPath);
268+
onGameContextMenu = (event: React.MouseEvent<HTMLDivElement>, sourceId: string, gameId: string, logoPath: string, screenshotPath: string): void => {
269+
this.props.onContextMenu(event, sourceId, gameId, logoPath, screenshotPath);
270270
};
271271

272272
/**

src/renderer/components/GameListItem.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export function GameListItem(props: GameListItemProps) {
4141
if (isDragged) { className += ' game-list-item--dragged'; }
4242
// Set element attributes
4343
const attributes: any = {};
44+
attributes[GameListItem.sourceAttribute] = props.game?.owner;
4445
attributes[GameListItem.idAttribute] = props.game?.id;
4546
attributes[GameListItem.indexAttribute] = index;
4647
attributes[GameListItem.logoPathAttribute] = props.game?.logoPath;
@@ -92,6 +93,7 @@ export function GameListItem(props: GameListItemProps) {
9293

9394
export namespace GameListItem {
9495
/** ID of the attribute used to store the game's id. */
96+
export const sourceAttribute = 'data-source-id';
9597
export const idAttribute = 'data-game-id';
9698
export const indexAttribute = 'data-game-index';
9799
export const logoPathAttribute = 'data-game-logo-path';
@@ -103,12 +105,14 @@ export namespace GameListItem {
103105
* @param element GameListItem element.
104106
*/
105107
export function getDragEventData(element: Element): GameDragEventData {
106-
const gameId = element.getAttribute(GameListItem.idAttribute);
108+
const sourceId = element.getAttribute(GameListItem.sourceAttribute) || '';
109+
const gameId = element.getAttribute(GameListItem.idAttribute) || '';
107110
const index = num(element.getAttribute(GameListItem.indexAttribute));
108111
const logoPath = element.getAttribute(GameListItem.logoPathAttribute) || '';
109112
const screenshotPath = element.getAttribute(GameListItem.screenshotPathAttribute) || '';
110113
if (typeof gameId !== 'string') { throw new Error('Failed to get ID from GameListItem element. Attribute not found.'); }
111114
return {
115+
sourceId,
112116
gameId,
113117
index,
114118
logoPath,

src/renderer/components/Header.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import { useContextMenu } from '@renderer/hooks/useContextMenu';
88
import { useLocalization } from '@renderer/hooks/useLocalization';
99
import { logoutFpfss } from '@renderer/store/fpfss/slice';
1010
import { deleteStoredView, renameStoredView, updatePreferences } from '@renderer/store/preferences/slice';
11-
import { addViews, deleteView, duplicateView, renameView } from '@renderer/store/search/slice';
11+
import { addViews, deleteView, duplicateView, GENERAL_VIEW_ID, renameView } from '@renderer/store/search/slice';
1212
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';
1616
import { CustomHeaderItemProps, MenuItemType } from 'flashpoint-launcher-renderer';
1717
import { Link, useLocation, useNavigate } from 'react-router-dom';
1818
import { toast } from 'react-toastify';
19-
import { joinLibraryRoute, openUrlInWindow } from '../Util';
19+
import { getViewNameFpfss, joinLibraryRoute, openUrlInWindow } from '../Util';
2020
import { DynamicComponent } from './DynamicComponent';
2121
import { OpenIcon } from './OpenIcon';
2222

@@ -42,21 +42,24 @@ export function Header() {
4242
const loadViewsText = useAppSelector(state => state.preferences.loadViewsText);
4343
const hideNewViewButton = useAppSelector(state => state.preferences.hideNewViewButton);
4444
const enableEditing = useAppSelector(state => state.preferences.enableEditing);
45-
const fpfssBaseUrl = useAppSelector(state => state.preferences.fpfssBaseUrl);
4645
const onlineManual = useAppSelector(state => state.preferences.onlineManual);
4746
const offlineManual = useAppSelector(state => state.preferences.offlineManual);
48-
const fpfssUser = useAppSelector(state => state.fpfss.user);
4947
const fpfssEditsOpen = useAppSelector(state => Object.keys(state.search.views).filter(k => k.startsWith('!fpfss-')).length > 0);
5048
const playlists = useAppSelector(state => state.main.playlists);
5149
const customRoutes = useAppSelector(state => state.main.displaySettings.customRoutes);
5250
const { openMenu } = useContextMenu();
5351
const viewName = useViewName();
52+
const location = useLocation();
53+
const viewNameFpfss = getViewNameFpfss(location.pathname);
5454
const allStrings = useLocalization();
5555
const strings = allStrings.app;
5656
const viewNames = useAppSelector(selectViewNames);
5757
const dispatch = useAppDispatch();
5858
const navigate = useNavigate();
5959
const { confirmDialog, openConfirmDialog } = useConfirmDialog();
60+
const fpfssViewOwner = useAppSelector(state => (viewNameFpfss && viewNameFpfss !== GENERAL_VIEW_ID) ? state.search.views[viewNameFpfss].editingGame?.owner : '');
61+
const fpfssUser = useAppSelector(state => fpfssViewOwner ? state.fpfss.users[fpfssViewOwner] : undefined);
62+
const fpfssSource = useAppSelector(state => state.preferences.gameMetadataSources.find(s => s.id === fpfssViewOwner));
6063

6164
const onToggleLeftSidebarClick = () => {
6265
dispatch(updatePreferences({
@@ -310,13 +313,17 @@ export function Header() {
310313
{
311314
type: 'button',
312315
label: strings.fpfssProfile,
316+
enabled: fpfssSource !== undefined && !!fpfssSource.fpfssUrl,
313317
onClick: () => {
314-
openUrlInWindow(`${fpfssBaseUrl}/web/profile`);
318+
if (fpfssSource && fpfssSource.fpfssUrl) {
319+
openUrlInWindow(`${fpfssSource.fpfssUrl}/web/profile`);
320+
}
315321
}
316322
},
317323
{
318324
type: 'button',
319325
label: strings.fpfssLogout,
326+
enabled: fpfssSource !== undefined && !!fpfssSource.fpfssUrl,
320327
onClick: () => {
321328
dispatch(logoutFpfss()).unwrap()
322329
.then(() => {

0 commit comments

Comments
 (0)