Skip to content

Commit

Permalink
feat: Screenshot Preview Mode
Browse files Browse the repository at this point in the history
  • Loading branch information
colin969 committed Mar 7, 2024
1 parent 0c582ef commit 227e175
Show file tree
Hide file tree
Showing 15 changed files with 180 additions and 29 deletions.
7 changes: 7 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@
"logoSet": "Logo Set",
"noLogoSet": "No Logo Set",
"logoSetDesc": "ID of the logo set to use.",
"screenshotPreviewMode": "Screenshot Preview",
"screenshotPreviewModeDesc": "Preview the screenshot of a Game in Grid mode",
"screenshotPreviewModeOff": "Off",
"screenshotPreviewModeOn": "On (When Hovering)",
"screenshotPreviewModeAlways": "Always",
"screenshotPreviewDelay": "Screenshot Preview Delay",
"screenshotPreviewDelayDesc": "Delay (in milliseconds) before showing the screenshot, when Screenshot Preview is set to on. (when hovering)",
"advancedHeader": "Advanced",
"optimizeDatabase": "Optimize Database",
"optimizeDatabaseDesc": "Run maintenance tasks to increase database performance and reduce size",
Expand Down
19 changes: 10 additions & 9 deletions src/back/extensions/ApiImplementation.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { ExtConfigFile } from '@back/ExtConfigFile';
import { DisposableChildProcess, ManagedChildProcess } from '@back/ManagedChildProcess';
import { EXT_CONFIG_FILENAME, PREFERENCES_FILENAME } from '@back/constants';
import { loadCurationIndexImage } from '@back/curate/parse';
import { duplicateCuration, genCurationWarnings, makeCurationFromGame, refreshCurationContent } from '@back/curate/util';
import { saveCuration } from '@back/curate/write';
import { ExtConfigFile } from '@back/ExtConfigFile';
import { DisposableChildProcess, ManagedChildProcess } from '@back/ManagedChildProcess';
import { downloadGameData } from '@back/download';
import { genContentTree } from '@back/rust';
import { BackState, StatusState } from '@back/types';
import { pathTo7zBack } from '@back/util/SevenZip';
import { awaitDialog } from '@back/util/dialog';
import { clearDisposable, dispose, newDisposable, registerDisposable } from '@back/util/lifecycle';
import { addPlaylistGame, deletePlaylist, deletePlaylistGame, filterPlaylists, findPlaylist, findPlaylistByName, getPlaylistGame, savePlaylistGame, updatePlaylist } from '../playlist';
import {
deleteCuration,
getOpenMessageBoxFunc,
Expand All @@ -17,31 +19,29 @@ import {
runService,
setStatus
} from '@back/util/misc';
import { pathTo7zBack } from '@back/util/SevenZip';
import { BrowsePageLayout, ScreenshotPreviewMode } from '@shared/BrowsePageLayout';
import { ILogEntry, LogLevel } from '@shared/Log/interface';
import { BackOut } from '@shared/back/types';
import { BrowsePageLayout } from '@shared/BrowsePageLayout';
import { CURATIONS_FOLDER_WORKING } from '@shared/constants';
import { CurationMeta, LoadedCuration } from '@shared/curate/types';
import { getContentFolderByKey } from '@shared/curate/util';
import { CurationTemplate, IExtensionManifest } from '@shared/extensions/interfaces';
import { ProcessState, Task } from '@shared/interfaces';
import { ILogEntry, LogLevel } from '@shared/Log/interface';
import { PreferencesFile } from '@shared/preferences/PreferencesFile';
import { overwritePreferenceData } from '@shared/preferences/util';
import { formatString } from '@shared/utils/StringFormatter';
import * as flashpoint from 'flashpoint-launcher';
import { Game } from 'flashpoint-launcher';
import * as fs from 'fs';
import * as fsExtra from 'fs-extra';
import { extractFull } from 'node-7z';
import * as path from 'path';
import { fpDatabase, loadCurationArchive } from '..';
import { addPlaylistGame, deletePlaylist, deletePlaylistGame, filterPlaylists, findPlaylist, findPlaylistByName, getPlaylistGame, savePlaylistGame, updatePlaylist } from '../playlist';
import { newExtLog } from './ExtensionUtils';
import { Command, RegisteredMiddleware } from './types';
import uuid = require('uuid');
import { awaitDialog } from '@back/util/dialog';
import stream = require('stream');
import { Game } from 'flashpoint-launcher';
import { downloadGameData } from '@back/download';

/**
* Create a Flashpoint API implementation specific to an extension, used during module load interception
Expand Down Expand Up @@ -661,6 +661,7 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest,
ProcessState: ProcessState,
BrowsePageLayout: BrowsePageLayout,
LogLevel: LogLevel,
ScreenshotPreviewMode: ScreenshotPreviewMode,

// Disposable funcs
dispose: dispose,
Expand Down
2 changes: 1 addition & 1 deletion src/back/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2577,7 +2577,7 @@ function createCommand(filename: string, useWine: boolean, noshell: boolean): st
case 'darwin':
case 'linux':
if (useWine) {
return `wine start /wait /unix "${filename}"`;
return `flatpak --env="WINEPREFIX=/home/colin/fpwinepfx" run org.winehq.Wine start /wait /unix "${filename}"`;
}
return noshell ? filename : `"${filename}"`;
default:
Expand Down
14 changes: 7 additions & 7 deletions src/renderer/components/ConfigBoxSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { memoizeOne } from '@shared/memoize';
import * as React from 'react';
import { ConfigBox, ConfigBoxProps } from './ConfigBox';

export type SelectItem = {
value: string;
export type SelectItem<T> = {
value: T;
display?: string;
}

export type ConfigBoxSelectProps = ConfigBoxProps & {
value: string;
export type ConfigBoxSelectProps<T extends string | number> = ConfigBoxProps & {
value: T;
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
items: SelectItem[];
items: SelectItem<T>[];
};

export function ConfigBoxSelect(props: ConfigBoxSelectProps) {
export function ConfigBoxSelect<T extends string | number>(props: ConfigBoxSelectProps<T>) {
return (
<ConfigBox
{...props}
Expand All @@ -30,7 +30,7 @@ export function ConfigBoxSelect(props: ConfigBoxSelectProps) {
);
}

const renderSelectItemsMemo = memoizeOne((selectItems: SelectItem[]): JSX.Element[] => {
const renderSelectItemsMemo = memoizeOne(<T extends string | number>(selectItems: SelectItem<T>[]): JSX.Element[] => {
return selectItems.map((item, idx)=> (
<option
key={idx}
Expand Down
10 changes: 9 additions & 1 deletion src/renderer/components/GameGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BackOut, BackOutTemplate } from '@shared/back/types';
import { LOGOS, VIEW_PAGE_SIZE } from '@shared/constants';
import { LOGOS, SCREENSHOTS, VIEW_PAGE_SIZE } from '@shared/constants';
import { memoizeOne } from '@shared/memoize';
import * as React from 'react';
import { ArrowKeyStepper, AutoSizer, Grid, GridCellProps, ScrollIndices } from 'react-virtualized-reactv17';
Expand All @@ -8,6 +8,7 @@ import { findElementAncestor, getExtremeIconURL, getGameImageURL } from '../Util
import { GameGridItem } from './GameGridItem';
import { GameItemContainer } from './GameItemContainer';
import { GameDragEventData } from './pages/BrowsePage';
import { ScreenshotPreviewMode } from '@shared/BrowsePageLayout';

const RENDERER_OVERSCAN = 5;

Expand Down Expand Up @@ -50,6 +51,10 @@ export type GameGridProps = {
gridRef?: RefFunc<HTMLDivElement>;
/** Updates to clear platform icon cache */
logoVersion: number;
/** Screenshot Preview Mode */
screenshotPreviewMode: ScreenshotPreviewMode;
/** Screenshot Preview Delay */
screenshotPreviewDelay: number;
};

/** A grid of cells, where each cell displays a game. */
Expand Down Expand Up @@ -194,6 +199,9 @@ export class GameGrid extends React.Component<GameGridProps> {
extreme={game ? game.tags.findIndex(t => this.props.extremeTags.includes(t.trim())) !== -1 : false}
extremeIconPath={extremeIconPath}
thumbnail={game ? getGameImageURL(LOGOS, game.id) : ''}
screenshot={game ? getGameImageURL(SCREENSHOTS, game.id) : ''}
screenshotPreviewMode={this.props.screenshotPreviewMode}
screenshotPreviewDelay={this.props.screenshotPreviewDelay}
logoVersion={this.props.logoVersion}
isDraggable={true}
isSelected={game ? game.id === selectedGameId : false}
Expand Down
34 changes: 31 additions & 3 deletions src/renderer/components/GameGridItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from 'react';
import { GridCellProps } from 'react-virtualized';
import { getPlatformIconURL } from '../Util';
import { GameDragEventData } from './pages/BrowsePage';
import { ScreenshotPreviewMode } from '@shared/BrowsePageLayout';

export type GameGridItemProps = Partial<GridCellProps> & {
id: string;
Expand All @@ -13,6 +14,8 @@ export type GameGridItemProps = Partial<GridCellProps> & {
logoVersion: number;
/** Path to the game's thumbnail. */
thumbnail: string;
/** Path to the game's screenshot */
screenshot: string;
/** If the cell can be dragged (defaults to false). */
isDraggable?: boolean;
/** If the cell is selected. */
Expand All @@ -23,11 +26,34 @@ export type GameGridItemProps = Partial<GridCellProps> & {
extremeIconPath: string;
/** On Drop event */
onDrop?: (event: React.DragEvent) => void;
/** Screenshot Preview Mode */
screenshotPreviewMode: ScreenshotPreviewMode;
/** Screenshot Preview Delay */
screenshotPreviewDelay: number;
};

// Displays a single game. Meant to be rendered inside a grid.
export function GameGridItem(props: GameGridItemProps) {
const { rowIndex, id, title, platforms, thumbnail, extreme, isDraggable, isSelected, isDragged, extremeIconPath, style, onDrop } = props;
const [isHovered, setIsHovered] = React.useState(false);
const [showScreenshot, setShowScreenshot] = React.useState(props.screenshotPreviewMode === ScreenshotPreviewMode.ALWAYS);

React.useEffect(() => {
if (props.screenshotPreviewMode === ScreenshotPreviewMode.ON) {
let timeoutId: any; // It's a timeout
if (isHovered) {
timeoutId = setTimeout(() => {
setShowScreenshot(true);
console.log('screenshot on');
}, props.screenshotPreviewDelay); // Delay in milliseconds
} else {
setShowScreenshot(false);
console.log('screenshot off');
}
return () => clearTimeout(timeoutId); // Cleanup timeout on component unmount or if hover state changes
}
}, [isHovered]);

const { rowIndex, id, title, platforms, thumbnail, screenshot, extreme, isDraggable, isSelected, isDragged, extremeIconPath, style, onDrop } = props;
// Get the platform icon path
const platformIcons = React.useMemo(() =>
platforms.slice(0, 5).map(p => getPlatformIconURL(p, props.logoVersion))
Expand All @@ -52,11 +78,13 @@ export function GameGridItem(props: GameGridItemProps) {
className={className}
draggable={isDraggable}
onDrop={onDrop}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
{ ...attributes }>
<div className='game-grid-item__thumb'>
<div
className='game-grid-item__thumb__image'
style={{ backgroundImage: `url('${thumbnail}')` }}>
style={{ backgroundImage: `url('${ showScreenshot ? screenshot : thumbnail }')` }}>
{(extreme) ? (
<div className='game-grid-item__thumb__icons--upper'>
<div
Expand All @@ -79,7 +107,7 @@ export function GameGridItem(props: GameGridItemProps) {
</div>
</li>
);
}, [style, className, isDraggable, id, title, platformIcons, thumbnail]);
}, [style, className, isDraggable, id, title, platformIcons, thumbnail, screenshot, showScreenshot]);
}

export namespace GameGridItem {
Expand Down
10 changes: 9 additions & 1 deletion src/renderer/components/RandomGames.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/indent */
import { LangContext } from '@renderer/util/lang';
import { LOGOS } from '@shared/constants';
import { LOGOS, SCREENSHOTS } from '@shared/constants';
import * as React from 'react';
import { findGameDragEventDataGrid, getExtremeIconURL, getGameImageURL } from '../Util';
import { GameGridItem } from './GameGridItem';
import { GameItemContainer } from './GameItemContainer';
import { HomePageBox } from './HomePageBox';
import { SimpleButton } from './SimpleButton';
import { ViewGame } from 'flashpoint-launcher';
import { ScreenshotPreviewMode } from '@shared/BrowsePageLayout';

type RandomGamesProps = {
games: ViewGame[];
Expand All @@ -22,6 +23,10 @@ type RandomGamesProps = {
logoVersion: number;
minimized: boolean;
onToggleMinimize: () => void;
/** Screenshot Preview Mode */
screenshotPreviewMode: ScreenshotPreviewMode;
/** Screenshot Preview Delay */
screenshotPreviewDelay: number;
};

// A small "grid" of randomly selected games.
Expand Down Expand Up @@ -52,6 +57,9 @@ export function RandomGames(props: RandomGamesProps) {
extreme={game ? game.tags.findIndex(t => props.extremeTags.includes(t.trim())) !== -1 : false}
extremeIconPath={getExtremeIconURL(props.logoVersion)}
thumbnail={getGameImageURL(LOGOS, game.id)}
screenshot={getGameImageURL(SCREENSHOTS, game.id)}
screenshotPreviewMode={props.screenshotPreviewMode}
screenshotPreviewDelay={props.screenshotPreviewDelay}
logoVersion={props.logoVersion}
isSelected={props.selectedGameId === game.id}
isDragged={false} />
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/components/pages/BrowsePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ export class BrowsePage extends React.Component<BrowsePageProps, BrowsePageState
cellWidth={width}
cellHeight={height}
logoVersion={this.props.logoVersion}
screenshotPreviewMode={this.props.preferencesData.screenshotPreviewMode}
screenshotPreviewDelay={this.props.preferencesData.screenshotPreviewDelay}
gridRef={this.gameGridOrListRefFunc} />
);
} else {
Expand Down
53 changes: 48 additions & 5 deletions src/renderer/components/pages/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import * as Coerce from '@shared/utils/Coerce';
import { Spinner } from '../Spinner';
import { SimpleButton } from '../SimpleButton';
import { ScreenshotPreviewMode } from '@shared/BrowsePageLayout';

const { num } = Coerce;

Expand Down Expand Up @@ -326,6 +327,23 @@ export class ConfigPage extends React.Component<ConfigPageProps, ConfigPageState
onChange={this.onCurrentLogoSetChange}
onItemSelect={this.onCurrentLogoSetSelect}
bottomChildren={logoSetPreviewRows}/>
{/* Screenshot Preview Mode */}
<ConfigBoxSelect
title={strings.screenshotPreviewMode}
description={strings.screenshotPreviewModeDesc}
value={this.props.preferencesData.screenshotPreviewMode}
items={this.itemizeScreenshotPreviewModes(strings)}
onChange={this.onScreenshotPreviewModeChange}
/>
<ConfigBoxSelectInput
title={strings.screenshotPreviewDelay}
description={strings.screenshotPreviewDelayDesc}
editable={true}
text={this.props.preferencesData.screenshotPreviewDelay.toString()}
placeholder='250'
onChange={this.onScreenshotPreviewDelayChange}
onItemSelect={this.onScreenshotPreviewDelayChange}
items={['0', '150', '250', '350', '500', '750', '1000']}/>
</div>
</div>

Expand Down Expand Up @@ -429,8 +447,8 @@ export class ConfigPage extends React.Component<ConfigPageProps, ConfigPageState
);
}

itemizeLangOptionsMemo = memoizeOne((langs: LangFile[], autoString: string): SelectItem[] => {
const items: SelectItem[] = langs.map((lang) => {
itemizeLangOptionsMemo = memoizeOne((langs: LangFile[], autoString: string): SelectItem<string>[] => {
const items: SelectItem<string>[] = langs.map((lang) => {
return {
value: lang.code,
display: lang.data.name ? `${lang.data.name} (${lang.code})` : lang.code
Expand All @@ -441,15 +459,15 @@ export class ConfigPage extends React.Component<ConfigPageProps, ConfigPageState
return items;
});

itemizeServerOptionsMemo = memoizeOne((serverNames: string[]): SelectItem[] =>
itemizeServerOptionsMemo = memoizeOne((serverNames: string[]): SelectItem<string>[] =>
serverNames.map((name) => {
return {
value: name
};
})
);

itemizeSearchLimitOptionsMemo = memoizeOne( (strings: LangContainer['config']): SelectItem[] => {
itemizeSearchLimitOptionsMemo = memoizeOne( (strings: LangContainer['config']): SelectItem<string>[] => {
return [
{
value: '0',
Expand Down Expand Up @@ -486,6 +504,23 @@ export class ConfigPage extends React.Component<ConfigPageProps, ConfigPageState
];
});

itemizeScreenshotPreviewModes = memoizeOne( (strings: LangContainer['config']): SelectItem<number>[] => {
return [
{
value: ScreenshotPreviewMode.OFF,
display: strings.screenshotPreviewModeOff
},
{
value: ScreenshotPreviewMode.ON,
display: strings.screenshotPreviewModeOn
},
{
value: ScreenshotPreviewMode.ALWAYS,
display: strings.screenshotPreviewModeAlways
}
];
});

itemizeLibraryOptionsMemo = memoizeOne((libraries: string[], excludedRandomLibraries: string[], libraryStrings: LangContainer['libraries']): MultiSelectItem[] => {
return libraries.map(library => {
return {
Expand Down Expand Up @@ -1052,6 +1087,14 @@ export class ConfigPage extends React.Component<ConfigPageProps, ConfigPageState
updatePreferencesDataAsync({ currentLogoSet: logoSet ? logoSet.id : undefined });
};

onScreenshotPreviewModeChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
updatePreferencesData({ screenshotPreviewMode: num(event.target.value) });
};

onScreenshotPreviewDelayChange = (value: string): void => {
updatePreferencesData({ screenshotPreviewDelay: num(value) });
};

getThemeName(id: string) {
const theme = this.props.themeList.find(t => t.id === id);
if (theme) { return theme.meta.name || theme.id; }
Expand Down Expand Up @@ -1183,7 +1226,7 @@ function formatLogoSetName(item: ILogoSet): string {
return `${item.name} (${item.id})`;
}

function itemizeExtEnums(enums: string[]): SelectItem[] {
function itemizeExtEnums(enums: string[]): SelectItem<string>[] {
return enums.map(e => {
return {
value: e
Expand Down
Loading

0 comments on commit 227e175

Please sign in to comment.