From f64e355907181f61aa653b54be063de5164aa806 Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 15 Dec 2024 14:11:36 +0000 Subject: [PATCH 01/10] chore: Add 13.1.0 upcoming strings for crowdin --- lang/en.json | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lang/en.json b/lang/en.json index 40ad22978..1ffd1ab81 100644 --- a/lang/en.json +++ b/lang/en.json @@ -14,6 +14,12 @@ "searchLimitDesc": "Limit the number of results returned in any search", "searchLimitUnlimited": "Unlimited", "searchLimitValue": "{0} Results", + "useCustomViews": "Use Custom Search Views", + "useCustomViewsDesc": "Replaces the Library search tabs with custom named search tabs and adds a Library option to the advanced filters.", + "defaultOpeningPage": "Default Opening Page", + "defaultOpeningPageDesc": "Default page to open to, either the Home Page or a chosen Search Page", + "restoreSearchViews": "Restore Search Results", + "restoreSearchViewsDesc": "Saves and restore a copy of each search page (text, filters, selected game and playlist) whenever you open the application.", "enableEditing": "Enable Editing", "enableEditingDesc": "Enable editing of games and additional applications. Also shows tabs related to editing.", "symlinkCuration": "Symlink Curation Content", @@ -189,7 +195,10 @@ "fpfssProfile": "Open Profile", "fpfssLogout": "Logout from FPFSS", "softwareUpdateRequired": "Software updated required. Do this now?", - "noLauncherUpdateReady": "Metadata update requires a newer Launcher version, but the software update is not currently available." + "noLauncherUpdateReady": "Metadata update requires a newer Launcher version, but the software update is not currently available.", + "deleteView": "Delete Search View", + "deleteOnlyBrowseView": "Delete Search View (Must have another custom view)", + "createNewView": "Create new search view" }, "filter": { "dateAdded": "Date Added", @@ -362,8 +371,9 @@ "allGames": "All Games", "newPlaylist": "New Playlist", "importPlaylist": "Import Playlist", + "noGamesFoundInsidePlaylist": "No Results Found in Playlist!", "emptyPlaylist": "Empty Playlist", - "noGamesFound": "No Games Found!", + "noGamesFound": "No Results Found!", "dropGameOnLeft": "Drop a game on this playlist in the {0} to add it.", "leftSidebar": "left sidebar", "setFlashpointPathQuestion": "Have you set the path to the {0} at the {1} page?", @@ -379,6 +389,8 @@ "showExtremeScreenshot": "Show Extreme Screenshot", "busy": "Please Wait...", "openGameDataBrowser": "Open Game Data Browser", + "allGenericEntries": "All Entries", + "usePlaylistOrder": "Use Playlist Order", "notArchived": "Not Archived", "archived": "Archived", "playOnline": "Play Online" @@ -533,7 +545,8 @@ "showImage": "Show Image", "searching": "Searching...", "loading": "Loading...", - "ok": "OK" + "ok": "OK", + "allow": "Allow" }, "menu": { "viewThumbnailInFolder": "View Thumbnail in Folder", @@ -627,7 +640,10 @@ "badAntiVirus": "You appear to be using '{0}'.\nThis particular Antivirus is known to cause issues, and we recommend adding an exception to the Flashpoint folder.\n\nIf you only see a white screen when running Flash games you may need to reinstall Flashpoint to restore deleted files.\n\nSee the Wiki or Help tab for detailed instructions.", "openWiki": "Open Wiki", "openDiscord": "Join Discord Server", - "doNotShowAgain": "Do not show again" + "doNotShowAgain": "Do not show again", + "extFpfssConsent": "Extension with ID {0} is requesting access to your FPFSS token. Allow?\nThis means the extension will be able access your FPFSS account and perform actions on your behalf.", + "gameDataUpdateReady": "A game data update is ready. Do you want to upgrade now?", + "gameDataUpdateReadyLcDifferent": "A game data update is ready. The launch command has changed and this may break existing save files. Do you want to upgrade now?" }, "libraries": { "arcade": "Games", @@ -647,5 +663,9 @@ "techDesc": "Adds support for all other tech - Shockwave, Unity, Java, HTML5, etc.", "screenshots": "Logos & Screenshots", "screenshotsDesc": "Adds logos for Grid view and screenshots for all games." + }, + "extensions": { + "fpssConsentRevokeTitle": "Revoke access to FPFSS", + "fpssConsentRevokeDesc": "Withdraw this extension's access to FPFSS" } } From 28c974e043e28be994909f0b71e67de70e6ffa7d Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 10 Feb 2025 16:00:07 +0000 Subject: [PATCH 02/10] chore: Final push of 13.1.0 strings for crowdin --- lang/en.json | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lang/en.json b/lang/en.json index 1ffd1ab81..7c658998f 100644 --- a/lang/en.json +++ b/lang/en.json @@ -10,6 +10,8 @@ "hideExtremeScreenshotsDesc": "Hides screenshots of Extreme tagged content, can be unhidden with a click on the image box.", "fancyAnimations": "Fancy Animations", "fancyAnimationsDesc": "Enable fancy animations in the launcher.", + "hideNewViewButton": "Hide New View Button", + "hideNewViewButtonDesc": "When using custom views, hide the new view (+) button. You can instead press 'Create New View' under the context menu for tabs.", "searchLimit": "Search Limit", "searchLimitDesc": "Limit the number of results returned in any search", "searchLimitUnlimited": "Unlimited", @@ -89,8 +91,10 @@ "screenshotPreviewDelay": "Screenshot Preview Delay", "screenshotPreviewDelayDesc": "Delay (in milliseconds) before showing the screenshot, when Screenshot Preview is set to on. (when hovering)", "advancedHeader": "Advanced", + "clearWininetCache": "Clear WinINet Cache", + "clearWininetCacheDesc": "Clears Windows web cache, which may help resolve loading issues in curations and other rare cases.", "optimizeDatabase": "Optimize Database", - "optimizeDatabaseDesc": "Run maintenance tasks to increase database performance and reduce size", + "optimizeDatabaseDesc": "Run maintenance tasks to increase database performance and reduce size.", "showDeveloperTab": "Show Developer Tab", "showDeveloperTabDesc": "Show the 'Developer' tab. This is most likely only useful for developers and curators.", "registerProtocol": "Register As Protocol Handler", @@ -180,6 +184,7 @@ "curate": "Curate", "developer": "Developer", "searchPlaceholder": "Search...", + "searchPlaceholderCountable": "Search... ({0} items)", "hideRightSidebar": "Hide right sidebar", "showRightSidebar": "Show right sidebar", "hideLeftSidebar": "Hide left sidebar", @@ -198,7 +203,8 @@ "noLauncherUpdateReady": "Metadata update requires a newer Launcher version, but the software update is not currently available.", "deleteView": "Delete Search View", "deleteOnlyBrowseView": "Delete Search View (Must have another custom view)", - "createNewView": "Create new search view" + "createNewView": "Create new search view", + "errorLoadingServices": "An error occurred while loading services. Please check the logs for more details." }, "filter": { "dateAdded": "Date Added", @@ -386,14 +392,18 @@ "noGameMatchedSearch": "Try searching for something less restrictive.", "mountParameters": "Mount Parameters", "noMountParameters": "No Mount Parameters", + "ruffleSupport": "Ruffle Support", "showExtremeScreenshot": "Show Extreme Screenshot", "busy": "Please Wait...", "openGameDataBrowser": "Open Game Data Browser", "allGenericEntries": "All Entries", "usePlaylistOrder": "Use Playlist Order", + "hideFilters": "Hide Filters", + "showFilters": "Show Filters", "notArchived": "Not Archived", "archived": "Archived", - "playOnline": "Play Online" + "playOnline": "Play Online", + "tagFilterIcon": "Tag Filter Icon" }, "tags": { "name": "Name", @@ -416,7 +426,8 @@ "makeAliasWhenMerged": "Make this an alias of the tag?", "deleteTag": "Delete Tag", "deleteTagCategory": "Delete Tag Category", - "locked": "Locked while processing..." + "locked": "Locked while processing...", + "filterIcon": "Icon Thumbnail" }, "curate": { "noCurationSelected": "No Curation Selected", @@ -533,6 +544,7 @@ "misc": { "noBlankFound": "No {0} Found", "addBlank": "Add {0}", + "removeBlank": "Remove {0}", "deleteAllBlankImages": "Delete ALL {0} images for this game", "yes": "Yes", "no": "No", From fa54aad3d81908d0fe6fd2114928c1df2e9ef72a Mon Sep 17 00:00:00 2001 From: dot-mike <586280+dot-mike@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:05:49 +0200 Subject: [PATCH 03/10] Fix not allowed to set user-agent with axios --- src/renderer/Util.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/Util.ts b/src/renderer/Util.ts index 5f6ba121e..dd5453db3 100644 --- a/src/renderer/Util.ts +++ b/src/renderer/Util.ts @@ -12,6 +12,7 @@ import { ViewQuery } from '@shared/library/util'; import { getGameDataFilename } from '@shared/utils/misc'; import { GENERAL_VIEW_ID } from '@renderer/store/search/slice'; import _axios from 'axios'; +import { VERSION } from '@shared/version'; export const gameDragDataType = 'json/game-drag'; @@ -314,6 +315,6 @@ export function wrapSearchTerm(text: string): string { export const axios = _axios.create({ headers: { - 'User-Agent': 'Flashpoint Launcher' + 'x-launcher-version': VERSION, } }); From 4ab2b97660e54821fa77dc65118a308c790621ee Mon Sep 17 00:00:00 2001 From: dot-mike <586280+dot-mike@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:06:22 +0200 Subject: [PATCH 04/10] Add launcher version to backend requests --- src/back/dns.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/back/dns.ts b/src/back/dns.ts index d7bdb3ae5..449bbdd60 100644 --- a/src/back/dns.ts +++ b/src/back/dns.ts @@ -3,6 +3,7 @@ import * as dns from 'node:dns'; import * as dnsPacket from 'dns-packet'; import _axios from 'axios'; import { Agent } from 'node:https'; +import { VERSION } from '@shared/version'; let id = 0; @@ -88,6 +89,7 @@ const agent = new Agent({ export const axios = _axios.create({ headers: { - 'User-Agent': 'Flashpoint Launcher' + 'User-Agent': 'Flashpoint Launcher/' + VERSION, + 'x-launcher-version': VERSION } }) \ No newline at end of file From 58ec1b5470f52901360ffd3c473e9c1878f1bd6a Mon Sep 17 00:00:00 2001 From: dot-mike <586280+dot-mike@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:50:08 +0200 Subject: [PATCH 05/10] feat: Enhance curation from game with task tracking --- src/back/curate/util.ts | 70 ++++++++++++++++++++++++++++++--- src/back/responses.ts | 4 +- src/renderer/components/app.tsx | 18 ++++++++- src/shared/back/types.ts | 2 +- 4 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/back/curate/util.ts b/src/back/curate/util.ts index 0873c124f..a8a99417f 100644 --- a/src/back/curate/util.ts +++ b/src/back/curate/util.ts @@ -322,15 +322,32 @@ function isValidDate(str: string): boolean { return (/^\d{4}(-(0?[1-9]|1[012])(-(0?[1-9]|[12][0-9]|3[01]))?)?$/).test(str); } -export async function makeCurationFromGame(state: BackState, gameId: string, skipDataPack?: boolean): Promise { - const game = await fpDatabase.findGame(gameId); - const folder = uuid(); - if (game) { - const curPath = path.join(state.config.flashpointPath, CURATIONS_FOLDER_WORKING, folder); - await fs.promises.mkdir(curPath, { recursive: true }); +export async function makeCurationFromGame(state: BackState, gameId: string, skipDataPack?: boolean, taskId?: string): Promise { + try { + const game = await fpDatabase.findGame(gameId); + const folder = uuid(); + if (game) { + if (taskId) { + state.socketServer.broadcast(BackOut.UPDATE_TASK, { + id: taskId, + status: 'Preparing curation...', + progress: 0.1 + }); + } + + const curPath = path.join(state.config.flashpointPath, CURATIONS_FOLDER_WORKING, folder); + await fs.promises.mkdir(curPath, { recursive: true }); const contentFolder = path.join(curPath, 'content'); await fs.promises.mkdir(contentFolder, { recursive: true }); + if (taskId) { + state.socketServer.broadcast(BackOut.UPDATE_TASK, { + id: taskId, + status: 'Copying existing data...', + progress: 0.3 + }); + } + const imagesRoot = path.join(state.config.flashpointPath, state.preferences.imageFolderPath); // Copy images (download from remote if does not exist) const logoRelPath = path.join('Logos', gameId.substring(0, 2), gameId.substring(2, 4), `${gameId}.png`); @@ -381,6 +398,14 @@ export async function makeCurationFromGame(state: BackState, gameId: string, ski // Extract active data pack if exists if (game.activeDataId) { + if (taskId) { + state.socketServer.broadcast(BackOut.UPDATE_TASK, { + id: taskId, + status: 'Extracting data pack...', + progress: 0.5 + }); + } + await checkAndDownloadGameData(game.activeDataId); const activeData = await fpDatabase.findGameDataById(game.activeDataId); if (activeData && activeData.path && !skipDataPack) { @@ -430,6 +455,15 @@ export async function makeCurationFromGame(state: BackState, gameId: string, ski alreadyImported: true, warnings: await genCurationWarnings(data, state.config.flashpointPath, state.suggestions, state.languageContainer.curate, state.apiEmitters.curations.onWillGenCurationWarnings), }; + + if (taskId) { + state.socketServer.broadcast(BackOut.UPDATE_TASK, { + id: taskId, + status: 'Finalizing curation...', + progress: 0.9 + }); + } + await saveCuration(curPath, curation); state.loadedCurations.push(curation); @@ -445,8 +479,32 @@ export async function makeCurationFromGame(state: BackState, gameId: string, ski // Send back responses state.socketServer.broadcast(BackOut.CURATE_LIST_CHANGE, [curation]); + + // Mark curation task as finished + if (taskId) { + state.socketServer.broadcast(BackOut.UPDATE_TASK, { + id: taskId, + status: '', + progress: 1, + finished: true + }); + } + return curation.folder; } + } catch (error) { + log.error('Launcher', `Make Curation From Game failed: ${error instanceof Error ? error.message : String(error)}`); + + if (taskId) { + state.socketServer.broadcast(BackOut.UPDATE_TASK, { + id: taskId, + status: 'Failed to create curation', + finished: true, + error: error instanceof Error ? error.message : String(error) + }); + } + throw error; + } } export async function refreshCurationContent(state: BackState, folder: string) { diff --git a/src/back/responses.ts b/src/back/responses.ts index ee529f42f..c307e0407 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -2356,8 +2356,8 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise { - return makeCurationFromGame(state, gameId); + state.socketServer.register(BackIn.CURATE_FROM_GAME, async (event, gameId, taskId) => { + return makeCurationFromGame(state, gameId, false, taskId); }); state.socketServer.register(BackIn.CURATE_CREATE_CURATION, async (event, folder, meta) => { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 6e3c90a13..927ef613c 100644 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1311,7 +1311,9 @@ export class App extends React.Component { label: strings.menu.makeCurationFromGame, enabled: this.props.preferencesData.enableEditing, click: () => { - window.Shared.back.request(BackIn.CURATE_FROM_GAME, gameId) + const task = newCurateTask('Make Curation from Game', 'Preparing curation...', this.props.addTask); + + window.Shared.back.request(BackIn.CURATE_FROM_GAME, gameId, task.id) .then((folder) => { if (folder) { // Select the new curation @@ -1321,6 +1323,13 @@ export class App extends React.Component { // Redirect to Curate once it's been made this.props.history.push(Paths.CURATE); } else { + this.props.setTask({ + id: task.id, + status: 'Failed to create curation', + finished: true, + error: 'No error provided.' + }); + ipcRenderer.invoke(CustomIPC.SHOW_MESSAGE_BOX, { title: 'Failed to create curation', message: 'Failed to create curation from this game. No error provided.' @@ -1328,6 +1337,13 @@ export class App extends React.Component { } }) .catch((err: any) => { + this.props.setTask({ + id: task.id, + status: 'Failed to create curation', + finished: true, + error: err.toString() + }); + ipcRenderer.invoke(CustomIPC.SHOW_MESSAGE_BOX, { title: 'Failed to create curation', message: `Failed to create curation from this game.\nError: ${err.toString()}` diff --git a/src/shared/back/types.ts b/src/shared/back/types.ts index a4d89cf45..d771cf7af 100644 --- a/src/shared/back/types.ts +++ b/src/shared/back/types.ts @@ -429,7 +429,7 @@ export type BackInTemplate = SocketTemplate void; [BackIn.CURATE_EXPORT]: (curations: LoadedCuration[], taskId?: string) => void; [BackIn.CURATE_EXPORT_DATA_PACK]: (curations: LoadedCuration[], taskId?: string) => void; - [BackIn.CURATE_FROM_GAME]: (gameId: string) => string | undefined; + [BackIn.CURATE_FROM_GAME]: (gameId: string, taskId?: string) => string | undefined; [BackIn.CURATE_REFRESH_CONTENT]: (folder: string) => void; [BackIn.CURATE_GEN_WARNINGS]: (curation: CurationState) => CurationWarnings; [BackIn.CURATE_DUPLICATE]: (folders: string[]) => void; From e5dc4e2ec9d08bffd6bae9935c89dc43dc7df67f Mon Sep 17 00:00:00 2001 From: dot-mike <586280+dot-mike@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:07:12 +0200 Subject: [PATCH 06/10] fix: Correct updated games count in metadata update message --- src/renderer/components/pages/HomePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/pages/HomePage.tsx b/src/renderer/components/pages/HomePage.tsx index 750dd8227..abe38f20b 100644 --- a/src/renderer/components/pages/HomePage.tsx +++ b/src/renderer/components/pages/HomePage.tsx @@ -471,7 +471,7 @@ export function HomePage(props: HomePageProps) { { props.main.metadataUpdate.ready && props.main.metadataUpdate.total > 0 && (
- {formatString(strings.updatedGamesReady, (props.main.metadataUpdate.total + 1).toString())} + {formatString(strings.updatedGamesReady, (props.main.metadataUpdate.total).toString())}
)}
From a94ddb5db51814f0f71ee54227aa4c47efe84167 Mon Sep 17 00:00:00 2001 From: dot-mike <586280+dot-mike@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:40:57 +0200 Subject: [PATCH 07/10] feat: Download game from context menu on right-click --- lang/en.json | 1 + src/renderer/components/app.tsx | 38 +++++++++++++++++++++++++++++++++ src/shared/lang.ts | 1 + 3 files changed, 40 insertions(+) diff --git a/lang/en.json b/lang/en.json index 9d7cb74ec..dd7c415f3 100644 --- a/lang/en.json +++ b/lang/en.json @@ -592,6 +592,7 @@ "exportPlaylist": "Export Playlist", "makeCurationFromGame": "Make Curation from Game", "copyShortcutURL": "Copy Shortcut URL", + "downloadGame": "Download Game", "downloadPlaylistContent": "Download Playlist Entries" }, "dialog": { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 6e3c90a13..1a90c7b84 100644 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1208,6 +1208,44 @@ export class App extends React.Component { click: () => { clipboard.writeText(gameId); } + }, + { + /* Download Game */ + label: strings.menu.downloadGame, + enabled: !window.Shared.isBackRemote, // (Local "back" only) + click: () => { + window.Shared.back.request(BackIn.GET_GAME, gameId) + .then((game) => { + if (game && game.activeDataId) { + window.Shared.back.request(BackIn.DOWNLOAD_GAME_DATA, game.activeDataId) + .then(() => { + // Refresh the game data in the sidebar after successful download + this.onUpdateActiveGameData(true, game.activeDataId); + }) + .catch((error) => { + console.error('Failed to download game:', error); + const opts: Electron.MessageBoxOptions = { + type: 'error', + title: 'Download Failed', + message: `Failed to download game: ${game.title}\nError: ${error.toString()}`, + buttons: ['Ok'], + }; + ipcRenderer.invoke(CustomIPC.SHOW_MESSAGE_BOX, opts); + }); + } else { + const opts: Electron.MessageBoxOptions = { + type: 'warning', + title: 'Cannot Download', + message: 'This game does not have downloadable content or the game data is not available.', + buttons: ['Ok'], + }; + ipcRenderer.invoke(CustomIPC.SHOW_MESSAGE_BOX, opts); + } + }) + .catch((error) => { + console.error('Failed to get game info:', error); + }); + } }, { type: 'separator' }, { /* File Location */ label: strings.menu.openFileLocation, diff --git a/src/shared/lang.ts b/src/shared/lang.ts index 474f5a94f..098f67148 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -605,6 +605,7 @@ const langTemplate = { 'exportPlaylist', 'makeCurationFromGame', 'copyShortcutURL', + 'downloadGame', 'downloadPlaylistContent', ] as const, dialog: [ From f8d25f2d0a985f58b1d2e2200afdbd55e8b9493b Mon Sep 17 00:00:00 2001 From: dot-mike <586280+dot-mike@users.noreply.github.com> Date: Wed, 30 Jul 2025 02:15:36 +0200 Subject: [PATCH 08/10] feat: Implement game de-selection Click on the same game to hide the sidebar. --- src/renderer/components/pages/BrowsePage.tsx | 48 ++++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/renderer/components/pages/BrowsePage.tsx b/src/renderer/components/pages/BrowsePage.tsx index 2187c09d1..b9614bbf9 100644 --- a/src/renderer/components/pages/BrowsePage.tsx +++ b/src/renderer/components/pages/BrowsePage.tsx @@ -403,19 +403,28 @@ export class BrowsePage extends React.Component => { const { currentView } = this.props; - if (currentView.selectedGame?.id !== gameId && gameId) { - const game = await window.Shared.back.request(BackIn.GET_GAME, gameId); - if (game) { - if (col !== undefined && row !== undefined) { - this.props.searchActions.setGridScroll({ + if (gameId) { + if (currentView.selectedGame?.id !== gameId) { + // Select a different game + const game = await window.Shared.back.request(BackIn.GET_GAME, gameId); + if (game) { + if (col !== undefined && row !== undefined) { + this.props.searchActions.setGridScroll({ + view: currentView.id, + col, + row + }); + } + this.props.searchActions.selectGame({ view: currentView.id, - col, - row + game, }); } + } else { + // Deselect the same game that's already selected this.props.searchActions.selectGame({ view: currentView.id, - game, + game: undefined }); } } @@ -424,18 +433,27 @@ export class BrowsePage extends React.Component => { const { currentView } = this.props; - if (currentView.selectedGame?.id !== gameId && gameId) { - const game = await window.Shared.back.request(BackIn.GET_GAME, gameId); - if (game) { - if (row !== undefined) { - this.props.searchActions.setListScroll({ + if (gameId) { + if (currentView.selectedGame?.id !== gameId) { + // Select a different game + const game = await window.Shared.back.request(BackIn.GET_GAME, gameId); + if (game) { + if (row !== undefined) { + this.props.searchActions.setListScroll({ + view: currentView.id, + row + }); + } + this.props.searchActions.selectGame({ view: currentView.id, - row + game, }); } + } else { + // Deselect the same game that's already selected this.props.searchActions.selectGame({ view: currentView.id, - game, + game: undefined, }); } } From 7bdf79314ee715e7f24ecbec9224362e1eaa1689 Mon Sep 17 00:00:00 2001 From: dot-mike <586280+dot-mike@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:55:58 +0200 Subject: [PATCH 09/10] fix: UX bug related to double-click. Prevents the sidebar from hiding / unhiding on double-click --- src/renderer/components/pages/BrowsePage.tsx | 78 ++++++++++++++++++-- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/src/renderer/components/pages/BrowsePage.tsx b/src/renderer/components/pages/BrowsePage.tsx index b9614bbf9..4d0b0323c 100644 --- a/src/renderer/components/pages/BrowsePage.tsx +++ b/src/renderer/components/pages/BrowsePage.tsx @@ -84,6 +84,13 @@ export class BrowsePage extends React.Component = React.createRef(); + /** Timeouts for delayed selection/deselection to allow double-clicks */ + private gridSelectionTimeout?: NodeJS.Timeout; + private listSelectionTimeout?: NodeJS.Timeout; + + /** Timestamp of last game launch to prevent immediate deselection after launch */ + private lastGameLaunchTime: number = 0; + /** Time it takes before the current "quick search" string to reset after a change was made (in milliseconds). */ static readonly quickSearchTimeout: number = 1500; @@ -104,6 +111,16 @@ export class BrowsePage extends React.Component => { const { currentView } = this.props; + + // Clear any pending selection timeout + if (this.gridSelectionTimeout) { + clearTimeout(this.gridSelectionTimeout); + this.gridSelectionTimeout = undefined; + } + if (gameId) { if (currentView.selectedGame?.id !== gameId) { // Select a different game @@ -421,11 +445,20 @@ export class BrowsePage extends React.Component { + this.props.searchActions.selectGame({ + view: currentView.id, + game: undefined + }); + this.gridSelectionTimeout = undefined; + }, 250); } } } @@ -433,6 +466,13 @@ export class BrowsePage extends React.Component => { const { currentView } = this.props; + + // Clear any pending selection timeout + if (this.listSelectionTimeout) { + clearTimeout(this.listSelectionTimeout); + this.listSelectionTimeout = undefined; + } + if (gameId) { if (currentView.selectedGame?.id !== gameId) { // Select a different game @@ -450,16 +490,38 @@ export class BrowsePage extends React.Component { + this.props.searchActions.selectGame({ + view: currentView.id, + game: undefined, + }); + this.listSelectionTimeout = undefined; + }, 250); } } }; onGameLaunch = async (gameId: string, override: GameLaunchOverride): Promise => { + // Record the launch time to prevent immediate deselection + this.lastGameLaunchTime = Date.now(); + + // Clear any pending deselection timeouts since we're launching the game + if (this.gridSelectionTimeout) { + clearTimeout(this.gridSelectionTimeout); + this.gridSelectionTimeout = undefined; + } + if (this.listSelectionTimeout) { + clearTimeout(this.listSelectionTimeout); + this.listSelectionTimeout = undefined; + } + await window.Shared.back.request(BackIn.LAUNCH_GAME, gameId, override); }; From e1fc4ec1f7edab0d6102d303850b100d44bcb203 Mon Sep 17 00:00:00 2001 From: dot-mike <586280+dot-mike@users.noreply.github.com> Date: Fri, 1 Aug 2025 23:49:11 +0200 Subject: [PATCH 10/10] fix: Whitespace filter search not working --- src/renderer/components/SearchBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/SearchBar.tsx b/src/renderer/components/SearchBar.tsx index 49548efbd..d1a59dac6 100644 --- a/src/renderer/components/SearchBar.tsx +++ b/src/renderer/components/SearchBar.tsx @@ -679,7 +679,7 @@ function SearchableSelectDropdown(props: Searcha // Split the items into 2 halves - Selected and not selected, then merge const filteredItems = React.useMemo(() => { - const lowerSearch = search.toLowerCase().replace(' ', ''); + const lowerSearch = search.toLowerCase().replace(/\s+/g, ''); const selectedItems = storedItems.filter((item) => item.value in selected); selectedItems.sort((a, b) => { if (selected[a.value] === 'whitelist' && selected[b.value] === 'blacklist') { @@ -693,7 +693,7 @@ function SearchableSelectDropdown(props: Searcha return [ ...selectedItems, - ...storedItems.filter((item) => !(item.value in selected) && item.orderVal.toLowerCase().includes(lowerSearch)), + ...storedItems.filter((item) => !(item.value in selected) && item.orderVal.toLowerCase().replace(/\s+/g, '').includes(lowerSearch)), ]; }, [search, storedItems]);