Skip to content

Commit

Permalink
UI rework
Browse files Browse the repository at this point in the history
  • Loading branch information
Namaneo committed Aug 26, 2023
1 parent bf58ddd commit 876e667
Show file tree
Hide file tree
Showing 16 changed files with 238 additions and 326 deletions.
61 changes: 6 additions & 55 deletions cores/cores.json
Original file line number Diff line number Diff line change
@@ -1,83 +1,34 @@
{
"2048": {
"name": "2048",
"repository": "libretro-2048",
"makefile": "Makefile.libretro",
"systems": [
{
"name": "Get to the 2048 tile!",
"standalone": true
}
]
},
"gambatte": {
"name": "Gambatte",
"repository": "gambatte-libretro",
"systems": [
{
"name": "Game Boy",
"extension": "gb"
},
{
"name": "Game Boy Color",
"extension": "gbc"
}
]
"systems": ["Game Boy", "Game Boy Color"]
},
"vbam": {
"name": "VBA-M",
"repository": "vbam-libretro",
"directory": "src/libretro",
"systems": [
{
"name": "Game Boy Advance",
"extension": "gba"
}
]
"systems": ["Game Boy Advance"]
},
"desmume": {
"name": "DeSmuME",
"directory": "desmume/src/frontend/libretro",
"systems": [
{
"name": "Nintendo DS",
"extension": "nds"
}
]
"systems": ["Nintendo DS"]
},
"nestopia": {
"name": "Nestopia",
"directory": "libretro",
"systems": [
{
"name": "NES",
"extension": "nes"
}
]
"systems": ["NES"]
},
"snes9x": {
"name": "Snes9x",
"directory": "libretro",
"systems": [
{
"name": "SNES",
"extension": "smc"
}
]
"systems": ["SNES"]
},
"genesis": {
"name": "Genesis Plus GX",
"repository": "Genesis-Plus-GX",
"makefile": "Makefile.libretro",
"systems": [
{
"name": "Master System",
"extension": "sms"
},
{
"name": "Mega Drive",
"extension": "bin"
}
]
"systems": ["Master System", "Mega Drive"]
}
}
Binary file removed cores/lib2048.a
Binary file not shown.
9 changes: 5 additions & 4 deletions ui/sources/entities/game.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Path from "../services/path";
import { System } from "./system";

export class Game {
Expand All @@ -16,12 +17,12 @@ export class Game {
/**
* @param {System} system
* @param {string} rom
* @param {boolean} installed
*/
constructor(system, rom) {
constructor(system, rom, installed) {
this.system = system.name;
this.rom = rom;
this.name = rom.lastIndexOf('.') != -1
? rom.substring(0, rom.lastIndexOf('.')).replaceAll(/ \(.*\).*/g, '')
: rom;
this.installed = installed;
this.name = Path.name(rom);
}
}
6 changes: 1 addition & 5 deletions ui/sources/entities/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ export class Save {
if (!system || !system.games)
return false;

const game = system.games.find(game => game.rom == `${this.game}.${system.extension}`);
if (!game)
return system.standalone;

return true;
return system.games.find(game => game.name == this.game) != null;
}
}
6 changes: 0 additions & 6 deletions ui/sources/entities/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ export class System {
/** @type {string} */
core_name;

/** @type {string} */
extension;

/** @type {boolean} */
standalone;

/** @type {Game[]} */
games;
}
8 changes: 1 addition & 7 deletions ui/sources/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { createRoot } from 'react-dom/client';
import { Redirect, Route } from 'react-router';
import { IonReactMemoryRouter } from '@ionic/react-router';
import { IonApp, IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs, setupIonicReact, useIonLoading } from '@ionic/react';
import { cloudDownload, gameController, keyOutline, save } from 'ionicons/icons';
import { gameController, keyOutline, save } from 'ionicons/icons';
import { HomePage } from './pages/home-page';
import { InstallPage } from './pages/install-page';
import { SavesPage } from './pages/saves-page';
import { CheatsPage } from './pages/cheats-page';
import Database from './services/database';
Expand Down Expand Up @@ -67,7 +66,6 @@ function Junie() {
<IonTabs>
<IonRouterOutlet>
<Route exact path="/home" component={HomePage} />
<Route exact path="/install" component={InstallPage} />
<Route exact path="/saves" component={SavesPage} />
<Route exact path="/cheats" component={CheatsPage} />
<Route exact path="/" render={() => <Redirect to="/home" />} />
Expand All @@ -78,10 +76,6 @@ function Junie() {
<IonIcon icon={gameController} />
<IonLabel>Games</IonLabel>
</IonTabButton>
<IonTabButton tab="install" href="/install">
<IonIcon icon={cloudDownload} />
<IonLabel>Install</IonLabel>
</IonTabButton>
<IonTabButton tab="saves" href="/saves">
<IonIcon icon={save} />
<IonLabel>Saves</IonLabel>
Expand Down
161 changes: 127 additions & 34 deletions ui/sources/modals/games-modal.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,86 @@
import { IonButton, IonButtons, IonCard, IonContent, IonHeader, IonIcon, IonItem, IonLabel, IonPage, IonProgressBar, IonTitle, IonToolbar, useIonAlert } from '@ionic/react';
import { cloudDownloadOutline, trashOutline } from 'ionicons/icons';
import { useState } from 'react';
import { IonButton, IonButtons, IonCard, IonContent, IonHeader, IonIcon, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonPage, IonProgressBar, IonTitle, IonToolbar, useIonAlert, useIonModal } from '@ionic/react';
import { add, closeOutline, cloudDownloadOutline, playOutline } from 'ionicons/icons';
import { useRef, useState } from 'react';
import { useToast } from '../hooks/toast';
import { CoreModal } from './core-modal';
import { System } from '../entities/system';
import { Game } from '../entities/game';
import Requests from '../services/requests';
import Files from '../services/files';
import Path from '../services/path';

const GameCard = ({ game, status, download, play }) => {
return (
<IonItem color="light">
<IonLabel>
<h2>{Path.clean(game.name)}</h2>
<h3>{game.system}</h3>
</IonLabel>
{status.game == game.name &&
<IonProgressBar value={status.progress}></IonProgressBar>
}
{status.game != game.name && !game.installed &&
<IonButton onClick={() => download(game)} disabled={!!status.game} fill="clear">
<IonIcon slot="icon-only" icon={cloudDownloadOutline} />
</IonButton>
}
{status.game != game.name && game.installed &&
<IonButton onClick={() => play(game)} disabled={!!status.game} fill="clear">
<IonIcon slot="icon-only" icon={playOutline} />
</IonButton>
}
</IonItem>
);
}

/**
* @param {Object} parameters
* @param {string} parameters.system
* @param {System} parameters.system
* @param {() => void} parameters.close
* @returns {JSX.Element}
*/
export const GamesModal = ({ system, close }) => {
const [download, setDownload] = useState({ game: null, progress: 0 });
const list = useRef(/** @type {HTMLIonListElement} */ (null));
const input = useRef(/** @type {HTMLInputElement} */ (null));

const sort = () => [...system.games.sort((g1, g2) => g1.rom < g2.rom ? -1 : 1)];

const [game, setGame] = useState(null);
const [games, setGames] = useState(sort(system.games));
const [status, setStatus] = useState({ game: null, progress: 0 });

const [start, stop] = useIonModal(CoreModal, { system, game, close: () => stop() });

const [present, dismiss] = useToast('Game successfully installed!');
const [alert] = useIonAlert();

/**
* @param {FileList} input
* @returns {Promise<void>}
*/
const install = async (input) => {
if (!input?.length)
return;

const file = input[0];

const buffer = new Uint8Array(await file.arrayBuffer())
await Files.Games.add(system.name, file.name, buffer);

const games = await Files.Games.get();
system.games.push(games.find(game => game.rom == file.name));

setGames(sort(system.games));
}

/**
* @param {Game} game
* @returns {Promise<void>}
*/
const install = async (game) => {
setDownload({ game: game.name, progress: 0 });
const download = async (game) => {
setStatus({ game: game.name, progress: 0 });
const data = await Requests.fetchGame(system, game, progress => {
setDownload({ game: game.name, progress });
setStatus({ game: game.name, progress });
});

if (!data) {
Expand All @@ -34,7 +89,7 @@ export const GamesModal = ({ system, close }) => {
message: `${game.name} (${system.name})`,
buttons: [ 'OK' ],
});
setDownload({ game: null, progress: 0 });
setStatus({ game: null, progress: 0 });
return;
}

Expand All @@ -44,7 +99,8 @@ export const GamesModal = ({ system, close }) => {
dismiss();
present(`${game.name} (${system.name})`);

setDownload({ game: null, progress: 0 });
setStatus({ game: null, progress: 0 });
setGames(sort(system.games));
}

/**
Expand All @@ -55,7 +111,17 @@ export const GamesModal = ({ system, close }) => {
await Files.Games.remove(game.system, game.rom);
game.installed = false;

setDownload({ game: null, progress: 0 });
await list.current.closeSlidingItems();
setGames(sort(system.games));
}

/**
* @param {Game} game
* @returns {void}
*/
const play = (game) => {
setGame(game);
start({ cssClass: 'fullscreen' });
}

return (
Expand All @@ -65,35 +131,62 @@ export const GamesModal = ({ system, close }) => {
<IonToolbar>
<IonTitle>Games</IonTitle>
<IonButtons slot="end">
<IonButton onClick={close}>Close</IonButton>
<IonButton onClick={() => input.current.click()}>
<input type="file" ref={input} onChange={e => install(e.target.files)} hidden />
<IonIcon slot="icon-only" icon={add} />
</IonButton>
<IonButton onClick={close}>
<IonIcon slot="icon-only" icon={closeOutline} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>

<IonContent className="games">
{system.games.map(game =>
<IonCard key={game.rom}>
<IonItem color="light">
<IonLabel>
<h2>{game.name}</h2>
<h3>{game.system}</h3>
<IonList lines="none" ref={list}>
<IonItemGroup>
<IonItemDivider>
<IonLabel>Installed</IonLabel>
</IonItemDivider>

{games.filter(game => game.installed).map(game => (
<IonCard key={game.rom}>
<IonItemSliding>
<GameCard game={game} status={status} download={download} play={play} />
<IonItemOptions side="end">
<IonItemOption color="danger" onClick={() => remove(game)}>Delete</IonItemOption>
</IonItemOptions>
</IonItemSliding>
</IonCard>
))}

{!games.filter(game => game.installed).length &&
<IonLabel className="empty">
You haven't installed any game yet.
</IonLabel>
{download.game == game.name &&
<IonProgressBar value={download.progress}></IonProgressBar>
}
{download.game != game.name && !game.installed &&
<IonButton onClick={() => install(game)} disabled={!!download.game} fill="clear">
<IonIcon slot="icon-only" icon={cloudDownloadOutline} />
</IonButton>
}
{download.game != game.name && game.installed &&
<IonButton onClick={() => remove(game)} disabled={!!download.game} fill="clear">
<IonIcon slot="icon-only" icon={trashOutline} color="medium" />
</IonButton>
}
</IonItem>
</IonCard>
)}
}

</IonItemGroup>
<IonItemGroup>
<IonItemDivider>
<IonLabel>Available</IonLabel>
</IonItemDivider>

{games.filter(game => !game.installed).map(game => (
<IonCard key={game.rom}>
<GameCard game={game} status={status} download={download} play={play} />
</IonCard>
))}

{!games.filter(game => !game.installed).length &&
<IonLabel className="empty">
No game is available to download.<br />
Try refreshing the library from the home page.
</IonLabel>
}

</IonItemGroup>
</IonList>
</IonContent>

</IonPage>
Expand Down
Loading

0 comments on commit 876e667

Please sign in to comment.