From cd033502c2cfad4dac98f76aca1db8222453ad51 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 30 Jul 2024 06:46:18 +0100 Subject: [PATCH] feat: Create playlist from results fix: Playlist ordering --- src/back/responses.ts | 6 ++ src/renderer/components/app.tsx | 2 +- src/renderer/components/pages/BrowsePage.tsx | 83 +++++++++++++++----- src/renderer/store/search/slice.ts | 47 ++++++++--- src/shared/back/types.ts | 3 + 5 files changed, 110 insertions(+), 31 deletions(-) diff --git a/src/back/responses.ts b/src/back/responses.ts index 1a69abc84..31a58129d 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -1088,6 +1088,12 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise { + search.limit = 99999999999; + + return await fpDatabase.searchGames(search); + }); + state.socketServer.register(BackIn.BROWSE_VIEW_PAGE, async (event, search) => { search.slim = true; diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index f596709c2..10871b372 100644 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1312,7 +1312,7 @@ export class App extends React.Component { if (this.props.currentView.selectedPlaylist && this.props.currentView.advancedFilter.playlistOrder && (sourceGameId !== destGameId)) { // Send swap to backend, reflect on frontend immediately const library = getViewName(this.props.location.pathname); - this.props.searchActions.swapPlaylistGame({ + this.props.searchActions.movePlaylistGame({ view: library, sourceGameId, destGameId, diff --git a/src/renderer/components/pages/BrowsePage.tsx b/src/renderer/components/pages/BrowsePage.tsx index a86b23eaa..7268fdca1 100644 --- a/src/renderer/components/pages/BrowsePage.tsx +++ b/src/renderer/components/pages/BrowsePage.tsx @@ -1,6 +1,6 @@ import * as remote from '@electron/remote'; import { WithTagCategoriesProps } from '@renderer/containers/withTagCategories'; -import { BackIn } from '@shared/back/types'; +import { BackIn, BackOut } from '@shared/back/types'; import { BrowsePageLayout } from '@shared/BrowsePageLayout'; import { ExtensionContribution } from '@shared/extensions/interfaces'; import { LangContainer } from '@shared/lang'; @@ -24,8 +24,8 @@ import { RequestState } from '@renderer/store/search/slice'; import { WithSearchProps } from '@renderer/containers/withSearch'; import { WithViewProps } from '@renderer/containers/withView'; import { SearchBar } from '@renderer/components/SearchBar'; -import path = require('path'); import { delayedThrottle } from '@shared/utils/throttle'; +import path = require('path'); type Pick = { [P in K]: T[P]; }; @@ -388,6 +388,10 @@ export class BrowsePage extends React.Component { + this.props.searchActions.selectPlaylist({ + view: this.props.currentView.id, + playlist: data + }); this.props.onUpdatePlaylist(data); }); this.setState({ @@ -412,24 +416,63 @@ export class BrowsePage extends React.Component { - this.setState({ - currentPlaylist: { - filePath: '', - id: uuid(), - games: [], - title: '', - description: '', - author: '', - icon: '', - library: this.props.currentView.id, - extreme: false - }, - isEditingPlaylist: true, - isNewPlaylist: true, - }); - if (this.props.currentView.selectedPlaylist) { - this.onSelectPlaylist(null); - } + const contextButtons: MenuItemConstructorOptions[] = [{ + label: 'Create Empty Playlist', + click: () => { + this.setState({ + currentPlaylist: { + filePath: '', + id: uuid(), + games: [], + title: '', + description: '', + author: '', + icon: '', + library: this.props.currentView.id, + extreme: false + }, + isEditingPlaylist: true, + isNewPlaylist: true, + }); + if (this.props.currentView.selectedPlaylist) { + this.onSelectPlaylist(null); + } + } + }, + { + label: 'Create From Search Results', + click: () => { + window.Shared.back.request(BackIn.BROWSE_ALL_RESULTS, { + ...this.props.currentView.searchFilter, + slim: true, + }) + .then((games) => { + this.setState({ + currentPlaylist: { + filePath: '', + id: uuid(), + games: games.map(g => ({ + gameId: g.id, + notes: '', + })), + title: '', + description: '', + author: '', + icon: '', + library: this.props.currentView.id, + extreme: false + }, + isEditingPlaylist: true, + isNewPlaylist: true, + }); + if (this.props.currentView.selectedPlaylist) { + this.onSelectPlaylist(null); + } + }); + } + }]; + const menu = remote.Menu.buildFromTemplate(contextButtons); + menu.popup({ window: remote.getCurrentWindow() }); }; onDiscardPlaylistClick = (): void => { diff --git a/src/renderer/store/search/slice.ts b/src/renderer/store/search/slice.ts index c0755b5a1..79d5dc412 100644 --- a/src/renderer/store/search/slice.ts +++ b/src/renderer/store/search/slice.ts @@ -21,7 +21,7 @@ export enum RequestState { RECEIVED, } -export type SwapPlaylistGameData = { +export type MovePlaylistGameData = { view: string; sourceGameId: string; destGameId: string; @@ -419,7 +419,7 @@ const searchSlice = createSlice({ } } }, - swapPlaylistGame(state: SearchState, { payload }: PayloadAction) { + movePlaylistGame(state: SearchState, { payload }: PayloadAction) { const view = state.views[payload.view]; const { sourceGameId, destGameId } = payload; @@ -427,18 +427,44 @@ const searchSlice = createSlice({ const sourceIdx = view.selectedPlaylist.games.findIndex(g => g.gameId === sourceGameId); const destIdx = view.selectedPlaylist.games.findIndex(g => g.gameId === destGameId); if (sourceIdx > -1 && destIdx > -1) { - const replacedGame = view.selectedPlaylist.games[destIdx]; - view.selectedPlaylist.games[destIdx] = view.selectedPlaylist.games[sourceIdx]; - view.selectedPlaylist.games[sourceIdx] = replacedGame; + // Remove existing source game + const sourceGame = view.selectedPlaylist.games.splice(sourceIdx, 1)[0]; + if (sourceIdx < destIdx) { + const destIdx = view.selectedPlaylist.games.findIndex(g => g.gameId === destGameId); + if (destIdx < view.selectedPlaylist.games.length) { + view.selectedPlaylist.games.splice(destIdx + 1, 0, sourceGame); + } else { + view.selectedPlaylist.games.push(sourceGame); + } + } else { + const destIdx = view.selectedPlaylist.games.findIndex(g => g.gameId === destGameId); + view.selectedPlaylist.games.splice(destIdx, 0, sourceGame); + } - // Try and swap them in the current results - const games = Object.entries(view.data.games); + + + // Try and move them in the results view + const games = Object.entries(view.data.games).map(([key, value]) => [Number(key), value]); const sourceGameEntry = games.find((g) => g[1].id === sourceGameId); const destGameEntry = games.find((g) => g[1].id === destGameId); if (sourceGameEntry && destGameEntry) { - view.data.games[num(sourceGameEntry[0])] = destGameEntry[1]; - view.data.games[num(destGameEntry[0])] = sourceGameEntry[1]; + const sourceIndex = games.indexOf(sourceGameEntry); + const destIndex = games.indexOf(destGameEntry); + if (sourceIdx < destIndex) { + games[sourceIndex][0] = games[destIndex][0]; + // Moving down (Shift games between up) + for (let i = sourceIdx + 1; i < destIndex + 1; i++) { + games[i][0]--; + } + } else { + games[sourceIndex][0] = games[destIndex][0]; + // Moving up (Shift games between down) + for (let i = destIndex; i < sourceIndex; i++) { + games[i][0]++; + } + } } + view.data.games = Object.fromEntries(games); // Update the playlist file window.Shared.back.send(BackIn.SAVE_PLAYLIST, view.selectedPlaylist); @@ -523,6 +549,7 @@ const searchSlice = createSlice({ }, }); +type GameRecordsArray = [number, Game]; export const { actions: searchActions } = searchSlice; export const { @@ -537,7 +564,7 @@ export const { setOrderBy, setOrderReverse, setAdvancedFilter, - swapPlaylistGame, + movePlaylistGame, requestRange, updateGame, addData } = searchSlice.actions; diff --git a/src/shared/back/types.ts b/src/shared/back/types.ts index 75baed4aa..244b1d422 100644 --- a/src/shared/back/types.ts +++ b/src/shared/back/types.ts @@ -116,6 +116,8 @@ export enum BackIn { /** Returns a query object for the given data */ PARSE_QUERY_DATA, + /** Returns all results for a search */ + BROWSE_ALL_RESULTS, /** Get a page of a browse view. */ BROWSE_VIEW_PAGE, /** Get the first page of a browse view */ @@ -362,6 +364,7 @@ export type BackInTemplate = SocketTemplate boolean; [BackIn.PARSE_QUERY_DATA]: (query: QueryData) => SearchQuery; + [BackIn.BROWSE_ALL_RESULTS]: (searchQuery: SearchQuery) => Game[]; [BackIn.BROWSE_VIEW_PAGE]: (search: SearchQuery) => void; [BackIn.BROWSE_VIEW_FIRST_PAGE]: (search: SearchQuery) => BrowseViewFirstPageResponseData; [BackIn.BROWSE_VIEW_KEYSET]: (search: SearchQuery) => BrowseViewKeysetResponse;