├── .env.example ├── src ├── react-app-env.d.ts ├── index.css ├── components │ ├── Win │ │ ├── tools │ │ │ ├── min-max.ts │ │ │ ├── descriptors.ts │ │ │ └── set-descriptors.ts │ │ ├── components │ │ │ ├── Content.tsx │ │ │ ├── buttons │ │ │ │ ├── FullscreenButton.tsx │ │ │ │ ├── CloseFullscreenButton.tsx │ │ │ │ ├── CloseButton.tsx │ │ │ │ └── lib │ │ │ │ │ └── fullscreen-styled.ts │ │ │ ├── Footer.tsx │ │ │ ├── Search.tsx │ │ │ └── Header.tsx │ │ ├── interfaces │ │ │ └── descriptor.interface.ts │ │ ├── WinManager.tsx │ │ └── Win.tsx │ ├── FileIcon │ │ ├── GameIcon.tsx │ │ ├── SystemIcon.tsx │ │ └── FileIconStyled.tsx │ ├── Loading.tsx │ ├── Video.tsx │ ├── LazyImage.tsx │ ├── Rom.tsx │ ├── Game.tsx │ ├── MediaGallery.tsx │ ├── System.tsx │ ├── RomTable.tsx │ ├── Desktop.tsx │ ├── SystemWindow.tsx │ └── GameWindow.tsx ├── index.tsx ├── interfaces │ ├── system.interface.ts │ ├── game.interface.ts │ └── rom.interface.ts ├── tools │ ├── guid.ts │ ├── file.ts │ └── media.ts ├── services │ ├── api.service.ts │ └── system.service.ts ├── contexts │ ├── win.context.ts │ ├── win-manager.context.ts │ └── toast.context.ts ├── hooks │ ├── use-effect-on-update.ts │ ├── use-on-drag.ts │ └── use-on-resize.ts ├── App.tsx └── data │ └── systems.json ├── public ├── robots.txt ├── favicon.ico ├── systems │ ├── icons │ │ ├── 3do.png │ │ ├── c64.png │ │ ├── gba.png │ │ ├── n64.png │ │ ├── nds.png │ │ ├── nes.png │ │ ├── psp.png │ │ ├── psx.png │ │ ├── sms.png │ │ ├── vb.png │ │ ├── wii.png │ │ ├── 32x_eu.png │ │ ├── 32x_jp.png │ │ ├── 32x_na.png │ │ ├── lynx.png │ │ ├── pcfx.png │ │ ├── saturn.png │ │ ├── segacd.png │ │ ├── sg1000.png │ │ ├── tg16.png │ │ ├── tgcd.png │ │ ├── famicom.png │ │ ├── gameboy.png │ │ ├── gamecube.png │ │ ├── gamegear.png │ │ ├── genesis.png │ │ ├── missing.png │ │ ├── odyssey2.png │ │ ├── pcengine.png │ │ ├── snes_usa.png │ │ ├── vectrex.png │ │ ├── atari2600.png │ │ ├── atari5200.png │ │ ├── atari7800.png │ │ ├── megadrive.png │ │ ├── pcenginecd.png │ │ ├── pokemonmini.png │ │ ├── snes_eujap.png │ │ ├── supergrafx.png │ │ ├── wonderswan.png │ │ ├── colecovision.png │ │ ├── intellivision.png │ │ └── neogeopocket.png │ └── photos │ │ ├── Amiga.png │ │ ├── Lua.png │ │ ├── MAME.png │ │ ├── PC-FX.png │ │ ├── Wii.png │ │ ├── Apple 2.png │ │ ├── DOSBox.png │ │ ├── Genesis.png │ │ ├── Infocom.png │ │ ├── Neo Geo.png │ │ ├── ScummVM.png │ │ ├── Sega CD.png │ │ ├── Vectrex.png │ │ ├── Atari 2600.png │ │ ├── Atari 5200.png │ │ ├── Atari 7800.png │ │ ├── Atari 800.png │ │ ├── Atari Lynx.png │ │ ├── Atari ST.png │ │ ├── Dragon 32.png │ │ ├── Dreamcast.png │ │ ├── Game Boy.png │ │ ├── Game Gear.png │ │ ├── GameCube.png │ │ ├── Macintosh.png │ │ ├── Megadrive.png │ │ ├── Odyssey 2.png │ │ ├── Oric Atmos.png │ │ ├── PC Engine.png │ │ ├── SAM Coupé.png │ │ ├── SuperGrafX.png │ │ ├── TI-99:4A.png │ │ ├── AdvanceMAME.png │ │ ├── Amstrad CPC.png │ │ ├── Atari Jaguar.png │ │ ├── Colecovision.png │ │ ├── Commodore 64.png │ │ ├── Game & Watch.png │ │ ├── LaserActive.png │ │ ├── Neo Geo MVS.png │ │ ├── Nintendo 64.png │ │ ├── Nintendo DS.png │ │ ├── PlayStation.png │ │ ├── Sega SG-1000.png │ │ ├── Sega Saturn.png │ │ ├── Game Boy Color.png │ │ ├── Game Boy Pocket.png │ │ ├── Intellivision.png │ │ ├── Neo Geo Pocket.png │ │ ├── Panasonic 3do.png │ │ ├── PlayStation 2.png │ │ ├── Sega Mark III.png │ │ ├── Sinclair ZX81.png │ │ ├── TRS-80 Model 3.png │ │ ├── TurboGrafx 16.png │ │ ├── Final Burn Alpha.png │ │ ├── Game Boy Advance.png │ │ ├── Bandai Wonder Swan.png │ │ ├── Neo Geo Pocket Color.png │ │ ├── Nintendo Virtual-Boy.png │ │ ├── PlayStation Portable.png │ │ ├── Sega Master System 2.png │ │ ├── Sega Master System.png │ │ ├── Sinclair ZX Spectrum.png │ │ ├── Sony HitBit (MSX 1).png │ │ ├── Tandy Color Computer.png │ │ ├── PC Engine Super CDRom2.png │ │ ├── Philips Videopac G7400.png │ │ ├── Bandai Wonder Swan Color.png │ │ ├── Nintendo Family Computer.png │ │ ├── Philips VG 8235 (MSX 2).png │ │ ├── Sega Genesis Model2 32X.png │ │ ├── Nintendo Entertainment System.png │ │ ├── Super Nintendo Entertainment System.png │ │ └── Nintendo Family Computer - Famicom Disk System.png └── index.html ├── screenshot.png ├── .gitignore ├── tsconfig.json ├── package.json └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_URL= 2 | REACT_APP_API_URL=/api 3 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body, #root { 2 | width: 100vw; 3 | height: 100vh; 4 | } 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/screenshot.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/favicon.ico -------------------------------------------------------------------------------- /public/systems/icons/3do.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/3do.png -------------------------------------------------------------------------------- /public/systems/icons/c64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/c64.png -------------------------------------------------------------------------------- /public/systems/icons/gba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/gba.png -------------------------------------------------------------------------------- /public/systems/icons/n64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/n64.png -------------------------------------------------------------------------------- /public/systems/icons/nds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/nds.png -------------------------------------------------------------------------------- /public/systems/icons/nes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/nes.png -------------------------------------------------------------------------------- /public/systems/icons/psp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/psp.png -------------------------------------------------------------------------------- /public/systems/icons/psx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/psx.png -------------------------------------------------------------------------------- /public/systems/icons/sms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/sms.png -------------------------------------------------------------------------------- /public/systems/icons/vb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/vb.png -------------------------------------------------------------------------------- /public/systems/icons/wii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/wii.png -------------------------------------------------------------------------------- /public/systems/icons/32x_eu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/32x_eu.png -------------------------------------------------------------------------------- /public/systems/icons/32x_jp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/32x_jp.png -------------------------------------------------------------------------------- /public/systems/icons/32x_na.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/32x_na.png -------------------------------------------------------------------------------- /public/systems/icons/lynx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/lynx.png -------------------------------------------------------------------------------- /public/systems/icons/pcfx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/pcfx.png -------------------------------------------------------------------------------- /public/systems/icons/saturn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/saturn.png -------------------------------------------------------------------------------- /public/systems/icons/segacd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/segacd.png -------------------------------------------------------------------------------- /public/systems/icons/sg1000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/sg1000.png -------------------------------------------------------------------------------- /public/systems/icons/tg16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/tg16.png -------------------------------------------------------------------------------- /public/systems/icons/tgcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/tgcd.png -------------------------------------------------------------------------------- /public/systems/photos/Amiga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Amiga.png -------------------------------------------------------------------------------- /public/systems/photos/Lua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Lua.png -------------------------------------------------------------------------------- /public/systems/photos/MAME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/MAME.png -------------------------------------------------------------------------------- /public/systems/photos/PC-FX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/PC-FX.png -------------------------------------------------------------------------------- /public/systems/photos/Wii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Wii.png -------------------------------------------------------------------------------- /public/systems/icons/famicom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/famicom.png -------------------------------------------------------------------------------- /public/systems/icons/gameboy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/gameboy.png -------------------------------------------------------------------------------- /public/systems/icons/gamecube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/gamecube.png -------------------------------------------------------------------------------- /public/systems/icons/gamegear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/gamegear.png -------------------------------------------------------------------------------- /public/systems/icons/genesis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/genesis.png -------------------------------------------------------------------------------- /public/systems/icons/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/missing.png -------------------------------------------------------------------------------- /public/systems/icons/odyssey2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/odyssey2.png -------------------------------------------------------------------------------- /public/systems/icons/pcengine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/pcengine.png -------------------------------------------------------------------------------- /public/systems/icons/snes_usa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/snes_usa.png -------------------------------------------------------------------------------- /public/systems/icons/vectrex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/vectrex.png -------------------------------------------------------------------------------- /public/systems/photos/Apple 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Apple 2.png -------------------------------------------------------------------------------- /public/systems/photos/DOSBox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/DOSBox.png -------------------------------------------------------------------------------- /public/systems/photos/Genesis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Genesis.png -------------------------------------------------------------------------------- /public/systems/photos/Infocom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Infocom.png -------------------------------------------------------------------------------- /public/systems/photos/Neo Geo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Neo Geo.png -------------------------------------------------------------------------------- /public/systems/photos/ScummVM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/ScummVM.png -------------------------------------------------------------------------------- /public/systems/photos/Sega CD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sega CD.png -------------------------------------------------------------------------------- /public/systems/photos/Vectrex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Vectrex.png -------------------------------------------------------------------------------- /public/systems/icons/atari2600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/atari2600.png -------------------------------------------------------------------------------- /public/systems/icons/atari5200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/atari5200.png -------------------------------------------------------------------------------- /public/systems/icons/atari7800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/atari7800.png -------------------------------------------------------------------------------- /public/systems/icons/megadrive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/megadrive.png -------------------------------------------------------------------------------- /public/systems/icons/pcenginecd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/pcenginecd.png -------------------------------------------------------------------------------- /public/systems/icons/pokemonmini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/pokemonmini.png -------------------------------------------------------------------------------- /public/systems/icons/snes_eujap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/snes_eujap.png -------------------------------------------------------------------------------- /public/systems/icons/supergrafx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/supergrafx.png -------------------------------------------------------------------------------- /public/systems/icons/wonderswan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/wonderswan.png -------------------------------------------------------------------------------- /public/systems/photos/Atari 2600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Atari 2600.png -------------------------------------------------------------------------------- /public/systems/photos/Atari 5200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Atari 5200.png -------------------------------------------------------------------------------- /public/systems/photos/Atari 7800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Atari 7800.png -------------------------------------------------------------------------------- /public/systems/photos/Atari 800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Atari 800.png -------------------------------------------------------------------------------- /public/systems/photos/Atari Lynx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Atari Lynx.png -------------------------------------------------------------------------------- /public/systems/photos/Atari ST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Atari ST.png -------------------------------------------------------------------------------- /public/systems/photos/Dragon 32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Dragon 32.png -------------------------------------------------------------------------------- /public/systems/photos/Dreamcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Dreamcast.png -------------------------------------------------------------------------------- /public/systems/photos/Game Boy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Game Boy.png -------------------------------------------------------------------------------- /public/systems/photos/Game Gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Game Gear.png -------------------------------------------------------------------------------- /public/systems/photos/GameCube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/GameCube.png -------------------------------------------------------------------------------- /public/systems/photos/Macintosh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Macintosh.png -------------------------------------------------------------------------------- /public/systems/photos/Megadrive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Megadrive.png -------------------------------------------------------------------------------- /public/systems/photos/Odyssey 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Odyssey 2.png -------------------------------------------------------------------------------- /public/systems/photos/Oric Atmos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Oric Atmos.png -------------------------------------------------------------------------------- /public/systems/photos/PC Engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/PC Engine.png -------------------------------------------------------------------------------- /public/systems/photos/SAM Coupé.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/SAM Coupé.png -------------------------------------------------------------------------------- /public/systems/photos/SuperGrafX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/SuperGrafX.png -------------------------------------------------------------------------------- /public/systems/photos/TI-99:4A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/TI-99:4A.png -------------------------------------------------------------------------------- /public/systems/icons/colecovision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/colecovision.png -------------------------------------------------------------------------------- /public/systems/icons/intellivision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/intellivision.png -------------------------------------------------------------------------------- /public/systems/icons/neogeopocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/icons/neogeopocket.png -------------------------------------------------------------------------------- /public/systems/photos/AdvanceMAME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/AdvanceMAME.png -------------------------------------------------------------------------------- /public/systems/photos/Amstrad CPC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Amstrad CPC.png -------------------------------------------------------------------------------- /public/systems/photos/Atari Jaguar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Atari Jaguar.png -------------------------------------------------------------------------------- /public/systems/photos/Colecovision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Colecovision.png -------------------------------------------------------------------------------- /public/systems/photos/Commodore 64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Commodore 64.png -------------------------------------------------------------------------------- /public/systems/photos/Game & Watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Game & Watch.png -------------------------------------------------------------------------------- /public/systems/photos/LaserActive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/LaserActive.png -------------------------------------------------------------------------------- /public/systems/photos/Neo Geo MVS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Neo Geo MVS.png -------------------------------------------------------------------------------- /public/systems/photos/Nintendo 64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Nintendo 64.png -------------------------------------------------------------------------------- /public/systems/photos/Nintendo DS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Nintendo DS.png -------------------------------------------------------------------------------- /public/systems/photos/PlayStation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/PlayStation.png -------------------------------------------------------------------------------- /public/systems/photos/Sega SG-1000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sega SG-1000.png -------------------------------------------------------------------------------- /public/systems/photos/Sega Saturn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sega Saturn.png -------------------------------------------------------------------------------- /public/systems/photos/Game Boy Color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Game Boy Color.png -------------------------------------------------------------------------------- /public/systems/photos/Game Boy Pocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Game Boy Pocket.png -------------------------------------------------------------------------------- /public/systems/photos/Intellivision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Intellivision.png -------------------------------------------------------------------------------- /public/systems/photos/Neo Geo Pocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Neo Geo Pocket.png -------------------------------------------------------------------------------- /public/systems/photos/Panasonic 3do.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Panasonic 3do.png -------------------------------------------------------------------------------- /public/systems/photos/PlayStation 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/PlayStation 2.png -------------------------------------------------------------------------------- /public/systems/photos/Sega Mark III.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sega Mark III.png -------------------------------------------------------------------------------- /public/systems/photos/Sinclair ZX81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sinclair ZX81.png -------------------------------------------------------------------------------- /public/systems/photos/TRS-80 Model 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/TRS-80 Model 3.png -------------------------------------------------------------------------------- /public/systems/photos/TurboGrafx 16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/TurboGrafx 16.png -------------------------------------------------------------------------------- /public/systems/photos/Final Burn Alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Final Burn Alpha.png -------------------------------------------------------------------------------- /public/systems/photos/Game Boy Advance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Game Boy Advance.png -------------------------------------------------------------------------------- /public/systems/photos/Bandai Wonder Swan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Bandai Wonder Swan.png -------------------------------------------------------------------------------- /public/systems/photos/Neo Geo Pocket Color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Neo Geo Pocket Color.png -------------------------------------------------------------------------------- /public/systems/photos/Nintendo Virtual-Boy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Nintendo Virtual-Boy.png -------------------------------------------------------------------------------- /public/systems/photos/PlayStation Portable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/PlayStation Portable.png -------------------------------------------------------------------------------- /public/systems/photos/Sega Master System 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sega Master System 2.png -------------------------------------------------------------------------------- /public/systems/photos/Sega Master System.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sega Master System.png -------------------------------------------------------------------------------- /public/systems/photos/Sinclair ZX Spectrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sinclair ZX Spectrum.png -------------------------------------------------------------------------------- /public/systems/photos/Sony HitBit (MSX 1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sony HitBit (MSX 1).png -------------------------------------------------------------------------------- /public/systems/photos/Tandy Color Computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Tandy Color Computer.png -------------------------------------------------------------------------------- /public/systems/photos/PC Engine Super CDRom2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/PC Engine Super CDRom2.png -------------------------------------------------------------------------------- /public/systems/photos/Philips Videopac G7400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Philips Videopac G7400.png -------------------------------------------------------------------------------- /src/components/Win/tools/min-max.ts: -------------------------------------------------------------------------------- 1 | export function minMax(min: number, value: number, max: number) { 2 | return Math.min(max, Math.max(min, value)); 3 | } 4 | -------------------------------------------------------------------------------- /public/systems/photos/Bandai Wonder Swan Color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Bandai Wonder Swan Color.png -------------------------------------------------------------------------------- /public/systems/photos/Nintendo Family Computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Nintendo Family Computer.png -------------------------------------------------------------------------------- /public/systems/photos/Philips VG 8235 (MSX 2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Philips VG 8235 (MSX 2).png -------------------------------------------------------------------------------- /public/systems/photos/Sega Genesis Model2 32X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Sega Genesis Model2 32X.png -------------------------------------------------------------------------------- /public/systems/photos/Nintendo Entertainment System.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Nintendo Entertainment System.png -------------------------------------------------------------------------------- /src/components/FileIcon/GameIcon.tsx: -------------------------------------------------------------------------------- 1 | import { FileIconStyled } from './FileIconStyled'; 2 | 3 | export const GameIcon = FileIconStyled({ iconOutSize: 100, iconSize: 90 }); 4 | -------------------------------------------------------------------------------- /src/components/FileIcon/SystemIcon.tsx: -------------------------------------------------------------------------------- 1 | import { FileIconStyled } from './FileIconStyled'; 2 | 3 | export const SystemIcon = FileIconStyled({ iconOutSize: 50, iconSize: 32 }); 4 | -------------------------------------------------------------------------------- /public/systems/photos/Super Nintendo Entertainment System.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Super Nintendo Entertainment System.png -------------------------------------------------------------------------------- /public/systems/photos/Nintendo Family Computer - Famicom Disk System.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-game-library-frontend/main/public/systems/photos/Nintendo Family Computer - Famicom Disk System.png -------------------------------------------------------------------------------- /src/components/Win/components/Content.tsx: -------------------------------------------------------------------------------- 1 | import { Box, styled } from '@mui/material'; 2 | 3 | export const Content = styled(Box)` 4 | background: #1d1f21; 5 | flex-grow: 1; 6 | position: relative; 7 | `; 8 | -------------------------------------------------------------------------------- /src/components/Win/components/buttons/FullscreenButton.tsx: -------------------------------------------------------------------------------- 1 | import OpenInFullIcon from '@mui/icons-material/OpenInFull'; 2 | import { fullscreenStyled } from './lib/fullscreen-styled'; 3 | 4 | export const FullscreenButton = fullscreenStyled(OpenInFullIcon); 5 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Box, CircularProgress } from '@mui/material'; 2 | 3 | export const Loading = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /src/components/Win/components/buttons/CloseFullscreenButton.tsx: -------------------------------------------------------------------------------- 1 | import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; 2 | import { fullscreenStyled } from './lib/fullscreen-styled'; 3 | 4 | export const CloseFullscreenButton = fullscreenStyled(CloseFullscreenIcon); 5 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/interfaces/system.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ISystem { 2 | id: string, 3 | name: string, 4 | picture?: string, 5 | pictures?: string[], 6 | icon?: string, 7 | icons?: string[], 8 | section: 'computers' |'consoles' | 'handhelds' | 'arcades' | 'others' 9 | } 10 | -------------------------------------------------------------------------------- /src/tools/guid.ts: -------------------------------------------------------------------------------- 1 | function s4() { 2 | return Math.floor((1 + Math.random()) * 0x10000) 3 | .toString(16) 4 | .substring(1); 5 | } 6 | 7 | export function guid() { 8 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 9 | s4() + '-' + s4() + s4() + s4(); 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/game.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IMedia { 2 | type: string; 3 | region: string; 4 | url: string; 5 | } 6 | 7 | export interface IGame { 8 | id: string; 9 | name: string; 10 | genres: string[]; 11 | medias: IMedia[]; 12 | synopsis: string; 13 | players: number; 14 | grade?: number; 15 | date?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/tools/file.ts: -------------------------------------------------------------------------------- 1 | export function formatFileSize(size: number, unit: 'o' | 'b' = 'o') { 2 | if (!size) { 3 | return '-'; 4 | } 5 | const k = 1024; 6 | const sizes = ['', 'K', 'M', 'G']; 7 | const i = Math.floor(Math.log(size)/Math.log(k)); 8 | const rounded = parseFloat((size/Math.pow(k,i)).toFixed(1)); 9 | return `${rounded} ${sizes[i]}${unit}` 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Win/interfaces/descriptor.interface.ts: -------------------------------------------------------------------------------- 1 | import { WinPayload } from '../../../contexts/win-manager.context'; 2 | 3 | export interface IDescriptor { 4 | id: string; 5 | zIndex: number; 6 | payload: WinPayload; 7 | options: { 8 | search?: boolean; 9 | }; 10 | state: { 11 | footer?: [string, string, string]; 12 | searching?: boolean; 13 | searched?: string; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .idea 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /src/components/Video.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, forwardRef } from 'react'; 2 | 3 | type Props = { 4 | url: string, 5 | style?: CSSProperties | undefined; 6 | } 7 | 8 | export const Video = forwardRef(({ url, style }, ref) => { 9 | return ( 10 | 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/Win/components/buttons/CloseButton.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material'; 2 | import CloseIcon from '@mui/icons-material/Close'; 3 | 4 | export const CloseButton = styled(CloseIcon)` 5 | color: #fa5e57; 6 | background: #fa5e57; 7 | border-radius: 100px; 8 | width: 16px; 9 | height: 16px; 10 | cursor: pointer; 11 | border: 1px solid #fa5e57; 12 | 13 | &:hover { 14 | color: #730a00; 15 | border-color: #e14039; 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from 'axios'; 2 | const api = `${process.env.REACT_APP_API_URL || ''}`; 3 | 4 | async function get(endpoint: string): Promise { 5 | const { data } = await axios.request>({ 6 | url: `${api}/${endpoint}`, 7 | method: 'GET', 8 | headers: { 'Content-Type': 'application/json' }, 9 | }); 10 | return data; 11 | } 12 | 13 | export const apiService = { 14 | get, 15 | }; 16 | -------------------------------------------------------------------------------- /src/interfaces/rom.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IArchive { 2 | name: string; 3 | url: string; 4 | crc: string; 5 | md5: string; 6 | size: number; 7 | } 8 | 9 | export interface IGeneric { 10 | crc: string; 11 | md5: string; 12 | size: number; 13 | } 14 | 15 | export interface IFile { 16 | name: string; 17 | crc: string; 18 | md5: string; 19 | size: number; 20 | generic: IGeneric; 21 | } 22 | 23 | export interface IRom { 24 | id: string; 25 | archive: IArchive; 26 | files: IFile[]; 27 | } 28 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Game Library 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/contexts/win.context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export type WinContextType = { 4 | zIndex: number; 5 | searching?: boolean; 6 | searched?: string; 7 | footer?: [string, string, string]; 8 | close: () => void; 9 | focus: () => void; 10 | setFooter: (left?: string, center?: string, right?: string) => void; 11 | setSearched: (searched: string) => void; 12 | } 13 | 14 | export const WinContext = createContext({ 15 | zIndex: 0, 16 | close: () => {}, 17 | focus: () => {}, 18 | setFooter: () => {}, 19 | setSearched: () => {}, 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/Win/components/buttons/lib/fullscreen-styled.ts: -------------------------------------------------------------------------------- 1 | import { OverridableComponent } from '@mui/material/OverridableComponent'; 2 | import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon'; 3 | import { styled } from '@mui/material'; 4 | 5 | export function fullscreenStyled(icon: OverridableComponent) { 6 | return styled(icon)` 7 | color: #28c941; 8 | background: #28c941; 9 | border-radius: 100px; 10 | width: 16px; 11 | height: 16px; 12 | cursor: pointer; 13 | border: 1px solid #28c941; 14 | padding: 1px; 15 | 16 | &:hover { 17 | color: #046201; 18 | border-color: #13aa27; 19 | } 20 | `; 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/contexts/win-manager.context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { SystemWindowData } from '../components/SystemWindow'; 3 | import { GameWindowData } from '../components/GameWindow'; 4 | 5 | export type WinPayload = SystemWindowData | GameWindowData; 6 | 7 | export type WinPayloadEqualFunction = (payloadA: WinPayload, payloadB: WinPayload) => boolean; 8 | 9 | export type WinOptions = { 10 | equals?: WinPayloadEqualFunction; 11 | search?: boolean; 12 | } 13 | 14 | type WindowManagerContextType = { 15 | openNewWindow: (payload: WinPayload, options?: WinOptions) => void; 16 | } 17 | 18 | export const WinManagerContext = createContext({ 19 | openNewWindow: () => {}, 20 | }); 21 | -------------------------------------------------------------------------------- /src/hooks/use-effect-on-update.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback, useEffect, useRef } from 'react'; 2 | 3 | // Based on https://stackoverflow.com/questions/53253940/make-react-useeffect-hook-not-run-on-initial-render 4 | 5 | export const useEffectOnUpdate = (effect: EffectCallback, deps: Array): void => { 6 | const mounted = useRef(false); 7 | 8 | useEffect(() => { 9 | if (mounted.current) { 10 | const unmount = effect(); 11 | return () => unmount && unmount(); 12 | } else { 13 | mounted.current = true; 14 | } 15 | // eslint-disable-next-line 16 | }, deps); 17 | 18 | // Reset on unmount for the next mount. 19 | useEffect(() => { 20 | return () => { 21 | mounted.current = false; 22 | }; 23 | }, []); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/LazyImage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | type Props = { 4 | src: string; 5 | alt: string; 6 | size: number; 7 | } 8 | 9 | const compatible = 'IntersectionObserver' in window; 10 | 11 | export const LazyImage = ({ src, alt, size }: Props) => { 12 | const [visible, setVisible] = useState(!compatible); 13 | const ref = useRef(null); 14 | 15 | useEffect(() => { 16 | if (!visible && ref.current) { 17 | const observer = new IntersectionObserver(([{ intersectionRatio }]) => { 18 | if (intersectionRatio > 0) { 19 | setVisible(true); 20 | } 21 | }); 22 | observer.observe(ref.current); 23 | return () => observer.disconnect(); 24 | } 25 | }, [visible, ref]); 26 | 27 | return {alt}; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Rom.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { Link } from '@mui/material'; 3 | import SportsEsportsIcon from '@mui/icons-material/SportsEsports'; 4 | import { IRom } from '../interfaces/rom.interface'; 5 | import { FileIconStyled } from './FileIcon/FileIconStyled'; 6 | import { WinContext } from '../contexts/win.context'; 7 | import { formatFileSize } from '../tools/file'; 8 | 9 | type Props = { 10 | rom: IRom 11 | } 12 | 13 | const RomIcon = FileIconStyled({ icon: SportsEsportsIcon, iconSize: 64, iconOutSize: 80}) 14 | 15 | export const Rom = ({ rom }: Props) => { 16 | const { setFooter } = useContext(WinContext); 17 | return ( 18 | 19 | setFooter('', rom.archive.name, formatFileSize(rom.archive.size))} 22 | onMouseLeave={() => setFooter()} 23 | /> 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/contexts/toast.context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, SyntheticEvent, MouseEvent, useMemo, useState } from 'react'; 2 | 3 | type ToastContextValue = { 4 | showError: (e: Error | string) => void; 5 | } 6 | 7 | export const ToastContext = createContext({ 8 | showError: () => 0, 9 | }) 10 | 11 | export const useToastContext = () => { 12 | const [open, setOpen] = useState(false); 13 | const [error, setError] = useState(); 14 | 15 | const toastContextValue: ToastContextValue = useMemo(() => ({ 16 | showError: (e: Error | string) => { 17 | setError(e); 18 | setOpen(true); 19 | } 20 | }), []); 21 | 22 | const snackbarProps = useMemo(() => ({ 23 | open, 24 | onClose(event: SyntheticEvent | MouseEvent, reason?: string) { 25 | if (reason === 'clickaway') { 26 | return; 27 | } 28 | setOpen(false); 29 | } 30 | }), [open]); 31 | 32 | return { 33 | error, 34 | toastContextValue, 35 | snackbarProps, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Win/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { Box, styled, Typography } from '@mui/material'; 3 | 4 | export const StyledBox = styled(Box)` 5 | padding: 2px 10px; 6 | border-top: 1px solid #000; 7 | height: 30px; 8 | background: #202122; 9 | border-bottom-left-radius: 10px; 10 | border-bottom-right-radius: 10px; 11 | 12 | display: flex;; 13 | font-size: 12px; 14 | align-items: center; 15 | 16 | * { 17 | font-size: 12px; 18 | } 19 | 20 | > *:first-of-type { 21 | text-align: left; 22 | max-width: 35%; 23 | } 24 | 25 | > *:nth-of-type(2) { 26 | text-align: center; 27 | flex-grow: 1; 28 | } 29 | 30 | > *:last-of-type { 31 | text-align: right; 32 | max-width: 25%; 33 | } 34 | `; 35 | 36 | export const Footer = memo(({ content }: { content?: [string, string, string]}) => { 37 | return ( 38 | 39 | {content && content.map((value, index) => {value}) } 40 | 41 | ); 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/Game.tsx: -------------------------------------------------------------------------------- 1 | import { GameIcon } from './FileIcon/GameIcon'; 2 | import { getDefaultMedia } from '../tools/media'; 3 | import { useContext } from 'react'; 4 | import { WinContext } from '../contexts/win.context'; 5 | import { ScrapedGame } from '../services/system.service'; 6 | import { formatFileSize } from '../tools/file'; 7 | 8 | type Props = { 9 | data: ScrapedGame; 10 | onDoubleClick?: () => void; 11 | } 12 | 13 | export const Game = ({ data: { game, roms }, onDoubleClick }: Props) => { 14 | const { setFooter } = useContext(WinContext); 15 | const media = getDefaultMedia(game.medias); 16 | const url = media?.url || `${process.env.PUBLIC_URL}/systems/icons/missing.png`; 17 | const resume = roms.length > 1 ? `${roms.length} roms` : '1 rom'; 18 | const size = roms.reduce((sum, rom) => sum + rom.archive.size, 0); 19 | return setFooter(resume, game.name, `${roms.length > 1 ? 'total: ' : ''}${formatFileSize(size)}`)} onMouseLeave={() => setFooter()}/>; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/MediaGallery.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Box } from '@mui/material'; 3 | import { IMedia } from '../interfaces/game.interface'; 4 | import { getMedia, MediaType } from '../tools/media'; 5 | 6 | export const Gallery = ({ medias, onClick } : { medias: IMedia[], onClick: (media: IMedia) => void }) => { 7 | const selection: IMedia[] = useMemo( 8 | () => [ 9 | getMedia(medias, MediaType.box2D), 10 | getMedia(medias, MediaType.screen), 11 | getMedia(medias, MediaType.title) 12 | ].filter((media): media is IMedia => Boolean(media)) 13 | , [medias] 14 | ); 15 | 16 | if (selection.length < 2) { 17 | return null; 18 | } 19 | 20 | return ( 21 | 22 | { selection.map(media => ( 23 | {media.type} onClick(media)} 29 | /> 30 | ) ) } 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Win/components/Search.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from 'react'; 2 | import { Box, InputAdornment, styled, TextField } from '@mui/material'; 3 | import SearchIcon from '@mui/icons-material/Search'; 4 | 5 | const StyledBox = styled(Box)` 6 | position: absolute; 7 | bottom: 0; 8 | right: 0; 9 | padding: 4px 20px 8px 12px; 10 | border-top-right-radius: 10px; 11 | display: flex; 12 | align-items: flex-end; 13 | width: 350px; 14 | max-width: 100%; 15 | > div { 16 | background-color: #383838; 17 | border-radius: 4px; 18 | } 19 | `; 20 | 21 | export const Search = ({ onChange }: { onChange: (e: ChangeEvent) => void}) => ( 22 | 23 | 32 | 33 | 34 | ), 35 | }} 36 | onChange={onChange} 37 | /> 38 | 39 | ); 40 | -------------------------------------------------------------------------------- /src/hooks/use-on-drag.ts: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler, useEffect, useMemo, useState } from 'react'; 2 | 3 | type Props = { 4 | draggable?: boolean; 5 | onDragMove: (e: MouseEvent) => void; 6 | } 7 | 8 | export const useOnDrag = ({ onDragMove, draggable = true }: Props) => { 9 | const [isDragging, setIsDragging] = useState(false); 10 | 11 | useEffect(() => { 12 | function onMouseMove(e: MouseEvent) { 13 | if (isDragging && draggable) { 14 | onDragMove(e); 15 | e.preventDefault(); 16 | } 17 | } 18 | function onMouseUp() { 19 | setIsDragging(false); 20 | } 21 | window.addEventListener('mousemove', onMouseMove); 22 | window.addEventListener('mouseup', onMouseUp); 23 | 24 | return () => { 25 | window.removeEventListener('mousemove', onMouseMove); 26 | window.removeEventListener('mouseup', onMouseUp); 27 | }; 28 | }, [isDragging, onDragMove, draggable]); 29 | 30 | return useMemo(() => { 31 | const onMouseDown: MouseEventHandler = () => { 32 | setIsDragging(true); 33 | }; 34 | return { onMouseDown }; 35 | }, []); 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "game-library-frontend", 3 | "version": "0.1.0", 4 | "license": "CC-BY-NC-SA-4.0", 5 | "engines": { 6 | "node": "16.x" 7 | }, 8 | "dependencies": { 9 | "@emotion/react": "^11.4.1", 10 | "@emotion/styled": "^11.3.0", 11 | "@fontsource/roboto": "^4.5.1", 12 | "@mui/icons-material": "^5.0.3", 13 | "@mui/material": "^5.0.2", 14 | "@types/axios": "^0.14.0", 15 | "@types/node": "^12.0.0", 16 | "@types/react": "^17.0.0", 17 | "@types/react-dom": "^17.0.0", 18 | "axios": "^0.22.0", 19 | "react": "^17.0.2", 20 | "react-dom": "^17.0.2", 21 | "react-scripts": "4.0.3", 22 | "typescript": "^4.1.2" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/services/system.service.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from './api.service'; 2 | import systems from '../data/systems.json'; 3 | import { ISystem } from '../interfaces/system.interface'; 4 | import { IGame } from '../interfaces/game.interface'; 5 | import { IRom } from '../interfaces/rom.interface'; 6 | 7 | export type SystemStatus = { 8 | system: string; 9 | games: number; 10 | roms: number; 11 | scraps: number; 12 | } 13 | 14 | function getSystem(id: string): ISystem { 15 | const system = (systems as ISystem[]).find(system => system.id === id); 16 | if (!system) { 17 | return { 18 | id, 19 | name: id, 20 | section: 'others', 21 | } 22 | } 23 | return system; 24 | } 25 | 26 | async function getStatuses() { 27 | const { status } = await apiService.get<{ status: SystemStatus[] }>('systems'); 28 | return status; 29 | } 30 | 31 | export type ScrapedGame = { 32 | game: IGame; 33 | roms: IRom[]; 34 | } 35 | 36 | function getSystemContent(systemId: string ) { 37 | return apiService.get<{ scraped: ScrapedGame[], roms: IRom[]}>(`systems/${systemId}`) 38 | } 39 | 40 | export const systemService = { 41 | get: getSystem, 42 | getStatuses, 43 | getSystemContent, 44 | }; 45 | -------------------------------------------------------------------------------- /src/tools/media.ts: -------------------------------------------------------------------------------- 1 | import { IMedia } from '../interfaces/game.interface'; 2 | 3 | const regions = ['wor', 'fr', 'us', 'ss', 'jp'] 4 | 5 | export enum MediaType { 6 | box2D = 'box-2d', 7 | box3D = 'box-3d', 8 | screen = 'ss', 9 | title = 'ss-title', 10 | video = 'video', 11 | videoNormalized = 'video-normalized', 12 | } 13 | 14 | function sortByRegion(a: IMedia, b: IMedia) { 15 | const idxA = regions.indexOf(a.region); 16 | const idxB = regions.indexOf(b.region); 17 | if (idxA < 0) { 18 | return 1; 19 | } 20 | if (idxB < 0) { 21 | return -1; 22 | } 23 | return idxA < idxB ? -1 : 1; 24 | } 25 | 26 | export function getMedia(medias: IMedia[], type: MediaType) { 27 | return medias.filter(media => media.type === type).sort(sortByRegion).shift(); 28 | } 29 | 30 | export function getVideoMedia(medias: IMedia[]) { 31 | return getMedia(medias, MediaType.videoNormalized) || getMedia(medias, MediaType.video); 32 | } 33 | 34 | export function getDefaultMedia(medias: IMedia[]) { 35 | const types = [MediaType.box2D, MediaType.box3D, MediaType.screen, MediaType.title]; 36 | for (const type of types) { 37 | const media = getMedia(medias, type); 38 | if (media) { 39 | return media; 40 | } 41 | } 42 | return medias[0]; 43 | } 44 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, createTheme, CssBaseline, Snackbar, ThemeProvider } from '@mui/material'; 2 | import { Desktop } from './components/Desktop'; 3 | import { useToastContext, ToastContext } from './contexts/toast.context'; 4 | 5 | // https://mui.com/components/typography/#general 6 | import '@fontsource/roboto/300.css'; 7 | import '@fontsource/roboto/400.css'; 8 | import '@fontsource/roboto/500.css'; 9 | import '@fontsource/roboto/700.css'; 10 | 11 | const darkTheme = createTheme({ 12 | palette: { 13 | mode: 'dark', 14 | background: { 15 | default: "#1e1f21" 16 | }, 17 | }, 18 | }); 19 | 20 | function App() { 21 | const { error, toastContextValue, snackbarProps } = useToastContext(); 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {typeof error === 'string' ? error : error?.message || 'Unknown error'} 32 | 33 | 34 | 35 | ); 36 | } 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /src/components/System.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useState } from 'react'; 2 | import { systemService } from '../services/system.service'; 3 | import { SystemIcon } from './FileIcon/SystemIcon'; 4 | import { systemWindowDataEquals } from './SystemWindow'; 5 | import { WinManagerContext } from '../contexts/win-manager.context'; 6 | import { useOnDrag } from '../hooks/use-on-drag'; 7 | 8 | type Props = { 9 | systemId: string; 10 | } 11 | 12 | export const System = ({ systemId }: Props) => { 13 | const { openNewWindow } = useContext(WinManagerContext); 14 | 15 | const [{ top, left }, setProperties] = useState({ top: 0, left: 0 }); 16 | 17 | const onDragMove = useCallback((e: MouseEvent) => { 18 | setProperties(props => ({ top: props.top + e.movementY, left: props.left + e.movementX })) 19 | }, []); 20 | 21 | const onDoubleClick = useCallback(() => openNewWindow({ systemId }, { equals: systemWindowDataEquals, search: true }), [openNewWindow, systemId]); 22 | 23 | const system = systemService.get(systemId); 24 | return ( 25 | 32 | ) 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/RomTable.tsx: -------------------------------------------------------------------------------- 1 | import { IRom } from '../interfaces/rom.interface'; 2 | import { Link, Paper, styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; 3 | import { formatFileSize } from '../tools/file'; 4 | 5 | const StyledPaper = styled(Paper)` 6 | background-color: #24282c; 7 | ` 8 | 9 | export const RomTable = ({ roms }: { roms: IRom[] }) => { 10 | return ( 11 | 12 | 13 | 14 | 15 | Fichier 16 | Poids 17 | 18 | 19 | 20 | {roms.map((rom) => ( 21 | 22 | 23 | 24 | {rom.archive.name} 25 | 26 | 27 | {formatFileSize(rom.archive.size)} 28 | 29 | ))} 30 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Win/tools/descriptors.ts: -------------------------------------------------------------------------------- 1 | import { IDescriptor } from '../interfaces/descriptor.interface'; 2 | 3 | export function getDescriptorById(descriptors: IDescriptor[], id: string): IDescriptor { 4 | const descriptor = descriptors.find(descriptor => descriptor.id === id); 5 | if (!descriptor) { 6 | throw new Error('descriptor not found'); 7 | } 8 | return descriptor; 9 | } 10 | 11 | export function getFocusedDescriptor(descriptors: IDescriptor[]): IDescriptor { 12 | if (!descriptors.length) { 13 | throw new Error('empty descriptor list'); 14 | } 15 | return descriptors.reduce((selected, descriptor) => selected.zIndex > descriptor.zIndex ? selected : descriptor); 16 | } 17 | 18 | export function getDescriptor(descriptors: IDescriptor[], id?: string): IDescriptor { 19 | return id ? getDescriptorById(descriptors, id) : getFocusedDescriptor(descriptors); 20 | } 21 | 22 | export function getMaxZIndex(descriptors: IDescriptor[]): number { 23 | return descriptors.length ? getFocusedDescriptor(descriptors).zIndex : 0; 24 | } 25 | 26 | /** 27 | * Return the descriptor list with the corresponding one updated to get the max zIndex value 28 | */ 29 | export function focusedDescriptor(descriptors: IDescriptor[], id: string) { 30 | const focused = getFocusedDescriptor(descriptors); 31 | if (focused.id === id) { 32 | return descriptors; 33 | } 34 | return descriptors.map(descriptor => descriptor.id === id ? {...descriptor, zIndex: focused.zIndex + 1} : descriptor); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Desktop.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react'; 2 | import { Grid } from '@mui/material'; 3 | import { systemService, SystemStatus } from '../services/system.service'; 4 | import { Loading } from './Loading'; 5 | import { System } from './System'; 6 | import { WinManager } from './Win/WinManager'; 7 | import { isSystemWindowData, SystemWindow } from './SystemWindow'; 8 | import { GameWindow } from './GameWindow'; 9 | import { ToastContext } from '../contexts/toast.context'; 10 | import { WinPayload } from '../contexts/win-manager.context'; 11 | 12 | function windowRenderer(payload: WinPayload) { 13 | if (isSystemWindowData(payload)) { 14 | return ; 15 | } 16 | return ; 17 | } 18 | 19 | export const Desktop = () => { 20 | const [loading, setLoading] = useState(true); 21 | const [statuses, setStatuses] = useState(); 22 | const { showError } = useContext(ToastContext); 23 | 24 | useEffect(() => { 25 | systemService 26 | .getStatuses() 27 | .then(statuses => { 28 | setStatuses(statuses.sort((a, b) => systemService.get(a.system).name.toLowerCase() < systemService.get(b.system).name.toLowerCase() ? -1 : 1)); 29 | }) 30 | .catch(showError) 31 | .finally(() => setLoading(false)); 32 | }, [showError]); 33 | 34 | return ( 35 | 36 | { loading && } 37 | { statuses && ( 38 | 39 | { statuses.map(status => ) } 40 | 41 | ) } 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Win/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { memo, MouseEventHandler } from 'react'; 2 | import { Box, styled, Typography } from '@mui/material'; 3 | import { FullscreenButton } from './buttons/FullscreenButton'; 4 | import { CloseFullscreenButton } from './buttons/CloseFullscreenButton'; 5 | import { CloseButton } from './buttons/CloseButton'; 6 | 7 | const StyledHeader = styled(Box)` 8 | padding: 2px 10px; 9 | border-bottom: 1px solid #000; 10 | height: 30px; 11 | display: flex; 12 | align-items: center; 13 | 14 | > div { 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | > div:first-of-type { 20 | font-size: 14px; 21 | user-select: none; 22 | max-width: 80%; 23 | img { 24 | max-height: 18px; 25 | width: auto; 26 | vertical-align: middle; 27 | margin-right: 5px; 28 | } 29 | * { 30 | font-size: 14px; 31 | } 32 | } 33 | 34 | > div:nth-of-type(2) { 35 | flex-grow: 1; 36 | } 37 | 38 | > div:last-of-type { 39 | gap: 10px; 40 | } 41 | `; 42 | 43 | type Props = { 44 | img?: string; 45 | title: string; 46 | fullscreen: boolean; 47 | onFullScreenClick: () => void; 48 | onDoubleClick: () => void; 49 | onCloseClick: () => void; 50 | onMouseDown: MouseEventHandler; 51 | } 52 | 53 | export const Header = memo(({ img, title, fullscreen, onFullScreenClick, onDoubleClick, onMouseDown, onCloseClick }: Props) => { 54 | return ( 55 | 56 |
57 | {Boolean(img) && {title}} 58 | {title} 59 |
60 |
61 |
62 | { !fullscreen && } 63 | { fullscreen && } 64 | 65 |
66 | 67 | ); 68 | }); 69 | -------------------------------------------------------------------------------- /src/components/FileIcon/FileIconStyled.tsx: -------------------------------------------------------------------------------- 1 | import { ElementType, createElement } from 'react'; 2 | import { Box, styled, Theme } from '@mui/material'; 3 | import { SxProps } from '@mui/system'; 4 | import { LazyImage } from '../LazyImage'; 5 | 6 | type BoxStyledProps = { 7 | iconOutSize: number; 8 | iconSize: number; 9 | icon?: ElementType; 10 | }; 11 | 12 | function boxStyled({ iconOutSize, iconSize }: BoxStyledProps) { 13 | return styled(Box)` 14 | width: 100px; 15 | background-color: transparent; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | user-select: none; 20 | 21 | div { 22 | width: ${iconOutSize}px; 23 | height: ${iconOutSize}px; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | border-radius: 3px; 28 | margin-bottom: 2px; 29 | } 30 | 31 | img { 32 | max-width: ${iconSize}px; 33 | max-height: ${iconSize}px; 34 | } 35 | 36 | svg { 37 | width: ${iconSize}px; 38 | height: ${iconSize}px; 39 | } 40 | 41 | p { 42 | font-size: 12px; 43 | text-align: center; 44 | margin: 0; 45 | border-radius: 3px; 46 | padding: 0 5px; 47 | } 48 | 49 | &:hover { 50 | p, div { 51 | background: #414244; 52 | } 53 | } 54 | `; 55 | } 56 | 57 | type Props = { 58 | label: string; 59 | img?: string; 60 | sx?: SxProps; 61 | onDoubleClick?: () => void; 62 | onMouseEnter?: () => void; 63 | onMouseLeave?: () => void; 64 | } 65 | 66 | export function FileIconStyled({ icon, iconOutSize, iconSize }: BoxStyledProps) { 67 | const StyledBox = boxStyled({ iconOutSize, iconSize }); 68 | return ({ img, label, ...props }: Props) => ( 69 | 70 |
71 | { img ? : null } 72 | { icon && createElement(icon) } 73 |
74 |

{ label }

75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/components/Win/tools/set-descriptors.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from 'react'; 2 | import { IDescriptor } from '../interfaces/descriptor.interface'; 3 | import { getDescriptor } from './descriptors'; 4 | 5 | /** 6 | * Remove a descriptor by its id or the focused one and call the SetState function 7 | */ 8 | export function removeOneAndSetDescriptors(setDescriptors: Dispatch>, id?: string) { 9 | let updated = false; 10 | setDescriptors(descriptors => { 11 | if (descriptors.length) { 12 | updated = true; 13 | const target = getDescriptor(descriptors, id); 14 | return descriptors.filter(descriptor => descriptor !== target); 15 | } 16 | return descriptors; 17 | }); 18 | return updated; 19 | } 20 | 21 | export function updateStateAndSetDescriptors>(setDescriptors : Dispatch>, state: T, id?: string, preCheck?: (descriptor: IDescriptor, state: T) => boolean | undefined) { 22 | let updated = false; 23 | setDescriptors(descriptors => { 24 | if (descriptors.length) { 25 | const target = getDescriptor(descriptors, id); 26 | if (!preCheck || preCheck(target, state)) { 27 | updated = true; 28 | return descriptors.map(descriptor => descriptor !== target ? descriptor : {...descriptor, state: {...descriptor.state, ...state}}); 29 | } 30 | } 31 | return descriptors; 32 | }); 33 | return updated; 34 | } 35 | 36 | export function rotateAndSetDescriptors(setDescriptors: Dispatch>, direction: -1 | 1) { 37 | setDescriptors(descriptors => { 38 | if (descriptors.length > 1) { 39 | const items = descriptors.slice().sort((a, b) => a.zIndex - b.zIndex); 40 | if (direction < 0) { 41 | items.push(items.shift()!); 42 | } else { 43 | items.unshift(items.pop()!); 44 | } 45 | return items.map((descriptor, index) => ({...descriptor, zIndex: index + 1})); 46 | } 47 | return descriptors; 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Game Library (frontend) 2 | 3 | This frontend is using [CRA](https://create-react-app.dev/), [React](https://reactjs.org/) and [MUI](https://mui.com/). 4 | 5 | ## Links 6 | 7 | - [Backend](https://github.com/jbdemonte/game-library-backend) 8 | - [Online demo](https://jb.demonte.fr/demos/game-library/) 9 | - [dev.to article](https://dev.to/jbdemonte/create-a-window-manager-with-react-3mak) 10 | 11 | ## Screenshot 12 | 13 | ![screenshot](screenshot.png) 14 | 15 | ## Development 16 | 17 | Copy `.env.example` to `.env` and modify it. 18 | 19 | Install the dependencies using `yarn` (or `npm`): 20 | 21 | ```shell 22 | yarn 23 | ``` 24 | 25 | Start the development server: 26 | 27 | ```shell 28 | yarn start 29 | ``` 30 | 31 | ## Production 32 | 33 | Build the project: 34 | 35 | ```shell 36 | yarn build 37 | ``` 38 | 39 | It produces the `build` folder which contains the static files. 40 | 41 | The [backend](https://github.com/jbdemonte/game-library-backend) provides a docker build which automatically includes this frontend. 42 | 43 | 44 | ## Credits 45 | 46 | - Consoles Icons from [OpenEmu](https://openemu.org/). 47 | - Medias from [Screenscraper](https://www.screenscraper.fr/). 48 | 49 | ## Licence 50 | 51 |

52 | Game Library (frontend) by Jean-Baptiste Demonte is licensed under CC BY-NC-SA 4.0 53 | 54 | 55 | 56 | 57 | 58 |

59 | -------------------------------------------------------------------------------- /src/hooks/use-on-resize.ts: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler, useMemo, useState, useEffect } from 'react'; 2 | import { useEffectOnUpdate } from './use-effect-on-update'; 3 | 4 | 5 | type Props = { 6 | onCursorChange: (cursor: string) => void; 7 | onMouseDown?: (e: React.MouseEvent) => void; 8 | onResize: (offset: { top: number, left: number, bottom: number, right: number }) => void; 9 | onChange: (resizing: boolean) => void; 10 | resizable?: boolean; 11 | } 12 | 13 | export const useOnResize = ({ onCursorChange, onResize, onChange, resizable, onMouseDown: onMouseDownParent }: Props) => { 14 | const [, setCursor] = useState('auto'); 15 | const [{ top, left, bottom, right, resizing }, setResizing] = useState({ top: false, left: false, bottom: false, right: false, resizing: false }); 16 | 17 | useEffectOnUpdate(() => { 18 | onChange(resizing) 19 | }, [resizing]); 20 | 21 | useEffect(() => { 22 | function onMouseMove(e: MouseEvent) { 23 | if (resizing && resizable) { 24 | onResize({ 25 | top: top ? e.movementY : 0, 26 | left: left ? e.movementX : 0, 27 | bottom: bottom ? e.movementY : 0, 28 | right: right ? e.movementX : 0, 29 | }); 30 | e.preventDefault(); 31 | } 32 | } 33 | function onMouseUp() { 34 | setResizing({ top: false, left: false, bottom: false, right: false, resizing: false }); 35 | } 36 | window.addEventListener('mousemove', onMouseMove); 37 | window.addEventListener('mouseup', onMouseUp); 38 | 39 | return () => { 40 | window.removeEventListener('mousemove', onMouseMove); 41 | window.removeEventListener('mouseup', onMouseUp); 42 | }; 43 | }, [resizing, top, left, bottom, right, onResize, resizable]); 44 | 45 | return useMemo(() => { 46 | const onMouseMove: MouseEventHandler = (e: React.MouseEvent) => { 47 | const { top, left, bottom, right } = getActiveSides(e); 48 | const cursor = getCursor(top, right, bottom, left); 49 | setCursor(current => { 50 | if (current !== cursor) { 51 | onCursorChange(cursor); 52 | } 53 | return cursor; 54 | }); 55 | 56 | }; 57 | 58 | const onMouseDown: MouseEventHandler = (e: React.MouseEvent) => { 59 | const { top, left, bottom, right } = getActiveSides(e); 60 | if (top || left || bottom || right) { 61 | setResizing({ top, left, bottom, right, resizing: true }); 62 | } 63 | if (onMouseDownParent) { 64 | onMouseDownParent(e); 65 | } 66 | }; 67 | 68 | return resizable && !resizing ? { onMouseMove, onMouseDown } : {}; 69 | }, [onCursorChange, onMouseDownParent, resizable, resizing]); 70 | } 71 | 72 | function getActiveSides(e: React.MouseEvent) { 73 | const size = 8; 74 | const element = e.currentTarget; 75 | const rect = element.getBoundingClientRect(); 76 | const x = Math.floor(e.clientX - rect.left); 77 | const y = Math.floor(e.clientY - rect.top); 78 | const width = element.offsetWidth; 79 | const height = element.offsetHeight; 80 | 81 | const left = x <= size; 82 | const right = x >= width - size; 83 | const top = y <= size; 84 | const bottom = y >= height - size; 85 | return { 86 | top, right, bottom, left 87 | }; 88 | } 89 | 90 | 91 | function getCursor(top: boolean, right: boolean, bottom: boolean, left: boolean): string { 92 | const cursors = [ 93 | ['nwse-resize', 'ns-resize', 'nesw-resize'], 94 | ['ew-resize', 'auto', 'ew-resize'], 95 | ['nesw-resize', 'ns-resize', 'nwse-resize'], 96 | ]; 97 | const x = left ? 0 : right ? 2 : 1; 98 | const y = top ? 0 : bottom ? 2 : 1; 99 | return cursors[y][x]; 100 | } 101 | -------------------------------------------------------------------------------- /src/components/SystemWindow.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useMemo, useState } from 'react'; 2 | import { Box, Grid } from '@mui/material'; 3 | import { ScrapedGame, systemService } from '../services/system.service'; 4 | import { Win } from './Win/Win'; 5 | import { Loading } from './Loading'; 6 | import { Game } from './Game'; 7 | import { ToastContext } from '../contexts/toast.context'; 8 | import { IRom } from '../interfaces/rom.interface'; 9 | import { Rom } from './Rom'; 10 | import { gameWindowDataEquals } from './GameWindow'; 11 | import { WinManagerContext, WinPayload } from '../contexts/win-manager.context'; 12 | import { formatFileSize } from '../tools/file'; 13 | import { WinContext } from '../contexts/win.context'; 14 | 15 | export type SystemWindowData = { 16 | systemId: string; 17 | } 18 | 19 | export const isSystemWindowData = (data: any): data is SystemWindowData => { 20 | return data.hasOwnProperty('systemId'); 21 | } 22 | 23 | export function systemWindowDataEquals(payloadA: WinPayload, payloadB: WinPayload) { 24 | return isSystemWindowData(payloadA) && isSystemWindowData(payloadB) && payloadA.systemId === payloadB.systemId; 25 | } 26 | 27 | function sumRomSizes(roms: IRom[]): number { 28 | return roms.reduce((sum, rom) => sum + rom.archive.size, 0); 29 | } 30 | 31 | export const SystemWindow = ({ systemId }: SystemWindowData) => { 32 | const system = systemService.get(systemId); 33 | const [content, setContent] = useState<{ scraped: ScrapedGame[], roms: IRom[] }>(); 34 | const { showError } = useContext(ToastContext); 35 | const { openNewWindow } = useContext(WinManagerContext); 36 | const { searched } = useContext(WinContext); 37 | 38 | useEffect(() => { 39 | systemService 40 | .getSystemContent(systemId) 41 | .then(content => { 42 | 43 | setContent({ 44 | scraped: content.scraped.sort((a, b) => a.game.name.toLowerCase() < b.game.name.toLowerCase() ? -1 : 1), 45 | roms: content.roms.sort((a, b) => a.archive.name.toLowerCase() < b.archive.name.toLowerCase() ? -1 : 1), 46 | }) 47 | }) 48 | .catch(showError) 49 | }, [systemId, showError]); 50 | 51 | const footer: [string, string, string] = useMemo(() => { 52 | if (content) { 53 | const romCount = content.scraped.reduce((sum, scraped) => sum + scraped.roms.length, 0) + content.roms.length; 54 | const totalSize = content.scraped.reduce((sum, scraped) => sum + sumRomSizes(scraped.roms), 0) + sumRomSizes(content.roms); 55 | 56 | const gameCountLabel = content.scraped.length > 1 ? `${content.scraped.length} scraped games` : (content.scraped.length ? '1 scraped game' : ''); 57 | const unknownGameLabel = content.roms.length > 1 ? `${content.roms.length} unknown games` : (content.roms.length ? '1 unknown game' : ''); 58 | 59 | return [ 60 | `${gameCountLabel}${gameCountLabel && unknownGameLabel ? ', ' : ''}${unknownGameLabel}`, 61 | '', 62 | `Total: ${romCount > 1 ? `${romCount} roms, ` : romCount === 1 ? '1 rom, ' : ''}${formatFileSize(totalSize)}` 63 | ] 64 | 65 | } 66 | return ['', 'loading', '']; 67 | 68 | }, [content]); 69 | 70 | const filterRoms = (roms: IRom[]) => { 71 | return searched ? roms.filter(rom => rom.archive.name.toLowerCase().includes(searched)) : roms; 72 | } 73 | 74 | const filterScraped = (scrapedGames: ScrapedGame[]) => { 75 | return searched ? scrapedGames.filter(scrapedGame => scrapedGame.game.name.includes(searched) || filterRoms(scrapedGame.roms).length) : scrapedGames; 76 | } 77 | 78 | return ( 79 | 80 | { content ? ( 81 | 82 | 83 | { filterScraped(content.scraped).map((gameData) => openNewWindow({ gameData }, { equals: gameWindowDataEquals })} />)} 84 | { filterRoms(content.roms).map((rom) => )} 85 | 86 | 87 | ) : } 88 | 89 | ) 90 | }; 91 | -------------------------------------------------------------------------------- /src/components/GameWindow.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react'; 2 | import { Box, Typography } from '@mui/material'; 3 | import PersonIcon from '@mui/icons-material/Person'; 4 | import PeopleIcon from '@mui/icons-material/People'; 5 | import HelpIcon from '@mui/icons-material/Help'; 6 | import StarRateIcon from '@mui/icons-material/StarRate'; 7 | import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; 8 | import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'; 9 | import CancelIcon from '@mui/icons-material/Cancel'; 10 | import { ScrapedGame } from '../services/system.service'; 11 | import { Win } from './Win/Win'; 12 | import { getDefaultMedia, getVideoMedia } from '../tools/media'; 13 | import { IGame } from '../interfaces/game.interface'; 14 | import { Video } from './Video'; 15 | import { RomTable } from './RomTable'; 16 | import { Gallery } from './MediaGallery'; 17 | import { WinPayload } from '../contexts/win-manager.context'; 18 | 19 | export type GameWindowData = { 20 | gameData: ScrapedGame; 21 | } 22 | 23 | export const isGameWindowData = (data: any): data is GameWindowData => { 24 | return data.hasOwnProperty('gameData'); 25 | } 26 | 27 | export function gameWindowDataEquals(payloadA: WinPayload, payloadB: WinPayload) { 28 | return isGameWindowData(payloadA) && isGameWindowData(payloadB) && payloadA.gameData.game.id === payloadB.gameData.game.id; 29 | } 30 | 31 | const DetailsBox = ({ game, playVideo }: { game: IGame, playVideo?: () => void}) => ( 32 | 33 | 34 | 35 | { game.players > 1 && } 36 | { game.players === 1 && } 37 | { game.players > 1 ? `${game.players} players` : `1 player` } 38 | 39 | 40 | { game.genres[0] || '' } 41 | 42 | 43 | { game.grade ?? '?' } / 20 44 | 45 | 46 | { game.date ?? '?'} 47 | 48 | { playVideo && ( 49 | 50 | Video 51 | 52 | )} 53 | 54 | 55 | ); 56 | 57 | export const GameWindow = ({ gameData: { game, roms } }: GameWindowData) => { 58 | const [showVideo, setShowVideo] = useState(false); 59 | const [media, setMedia] = useState(() => getDefaultMedia(game.medias)) 60 | const video = getVideoMedia(game.medias); 61 | 62 | const icon = useMemo(() => getDefaultMedia(game.medias), [game]); 63 | 64 | return ( 65 | 66 | 67 | 68 | 69 | { 70 | showVideo && video ? 71 | 72 | 75 | : 76 | {game.name} 81 | } 82 | setShowVideo(true) : undefined } /> 83 | 84 | 85 | { setMedia(media); setShowVideo(false); }}/> 86 | 87 | 88 | { game.name } 89 | 90 | 91 | 92 | { game.synopsis } 93 | 94 | 95 | 96 | 97 | 98 | 99 | ) 100 | }; 101 | -------------------------------------------------------------------------------- /src/components/Win/WinManager.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactElement, useEffect, useMemo, useState } from 'react'; 2 | import { Box } from '@mui/material'; 3 | import { WinContext, WinContextType } from '../../contexts/win.context'; 4 | import { guid } from '../../tools/guid'; 5 | import { WinManagerContext, WinOptions, WinPayload } from '../../contexts/win-manager.context'; 6 | import { IDescriptor } from './interfaces/descriptor.interface'; 7 | import { getMaxZIndex, focusedDescriptor } from './tools/descriptors'; 8 | import { removeOneAndSetDescriptors, rotateAndSetDescriptors, updateStateAndSetDescriptors } from './tools/set-descriptors'; 9 | 10 | type Props = { 11 | render: (payload: WinPayload) => ReactElement; 12 | } 13 | 14 | export const WinManager: FC = ({ render, children }) => { 15 | const [descriptors, setDescriptors] = useState([]); 16 | 17 | useEffect(() => { 18 | function keydown(event: KeyboardEvent) { 19 | if (event.defaultPrevented) { 20 | return; // Do nothing if the event was already processed 21 | } 22 | 23 | // command + F or ctrl + F 24 | if (event.key === 'f' && (event.metaKey || event.ctrlKey)) { 25 | updateStateAndSetDescriptors(setDescriptors, { searching: true }, '', (descriptor) => descriptor.options.search && !descriptor.state.searching ); 26 | event.preventDefault(); 27 | } 28 | 29 | if (event.key === 'Escape') { 30 | const updated = updateStateAndSetDescriptors(setDescriptors, { searching: false, searched: '' }, '', (descriptor) => descriptor.options.search && descriptor.state.searching ); 31 | if (!updated) { 32 | removeOneAndSetDescriptors(setDescriptors); 33 | } 34 | event.preventDefault(); 35 | } 36 | 37 | if (event.key === 'Tab') { 38 | rotateAndSetDescriptors(setDescriptors, event.shiftKey || event.metaKey ? -1 : 1); 39 | event.preventDefault(); 40 | } 41 | } 42 | 43 | window.addEventListener('keydown', keydown); 44 | return () => { 45 | window.removeEventListener('keydown', keydown); 46 | }; 47 | }, []); 48 | 49 | const winManagerContextValue = useMemo(() => ({ 50 | openNewWindow: (payload: WinPayload, { equals, ...options }: WinOptions = {}) => { 51 | setDescriptors(descriptors => { 52 | const existing = equals ? descriptors.find(descriptor => equals(payload, descriptor.payload)) : undefined; 53 | if (existing) { 54 | return focusedDescriptor(descriptors, existing.id); 55 | } 56 | // add a new descriptor 57 | return descriptors.concat([{ 58 | id: guid(), 59 | zIndex: 1 + getMaxZIndex(descriptors), 60 | payload, 61 | options, 62 | state: {}, 63 | }]); 64 | }) 65 | } 66 | }), []); 67 | 68 | const windowHandlers: Omit = useMemo(() => ({ 69 | close: (id: string) => { 70 | removeOneAndSetDescriptors(setDescriptors, id); 71 | }, 72 | focus: (id: string) => { 73 | setDescriptors(descriptors => focusedDescriptor(descriptors, id)); 74 | }, 75 | setFooter: (id: string, left: string = '', center: string = '', right: string = '') => { 76 | updateStateAndSetDescriptors(setDescriptors, { footer: (left || center || right) ? [left, center, right] : undefined }, id); 77 | }, 78 | setSearched: (id: string, searched: string = '') => { 79 | updateStateAndSetDescriptors(setDescriptors, { searched }, id); 80 | }, 81 | }), []); 82 | 83 | return ( 84 | 85 | { children } 86 | 87 | { descriptors.map(descriptor => ) } 88 | 89 | 90 | ); 91 | } 92 | 93 | type GenericWinProps = { 94 | descriptor: IDescriptor; 95 | close: (id: string) => void; 96 | focus: (id: string) => void; 97 | setFooter: (id: string, left?: string, center?: string, right?: string) => void; 98 | setSearched: (id: string, searched: string) => void; 99 | render: (payload: WinPayload) => ReactElement; 100 | } 101 | 102 | const GenericWin = ({ descriptor, close, focus, setFooter, setSearched, render }: GenericWinProps) => { 103 | const contextValue: WinContextType = useMemo(() => ({ 104 | searching: descriptor.state.searching, 105 | searched: descriptor.state.searched, 106 | zIndex: descriptor.zIndex, 107 | close: close.bind(null, descriptor.id), 108 | focus: focus.bind(null, descriptor.id), 109 | footer: descriptor.state.footer ? [...descriptor.state.footer] : undefined, 110 | setFooter: setFooter.bind(null, descriptor.id), 111 | setSearched: setSearched.bind(null, descriptor.id) 112 | }), [descriptor, close, focus, setFooter, setSearched]); 113 | return ( 114 | 115 | { render(descriptor.payload) } 116 | 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /src/components/Win/Win.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, FC, useCallback, useContext, useEffect, useState } from 'react'; 2 | import { Box, styled } from '@mui/material'; 3 | import { useOnDrag } from '../../hooks/use-on-drag'; 4 | import { useOnResize } from '../../hooks/use-on-resize'; 5 | import { Header } from './components/Header'; 6 | import { Footer } from './components/Footer'; 7 | import { Content } from './components/Content'; 8 | import { WinContext } from '../../contexts/win.context'; 9 | import { Search } from './components/Search'; 10 | import { minMax } from './tools/min-max'; 11 | 12 | type Props = { 13 | title: string; 14 | img?: string; 15 | footer?: [string, string, string]; 16 | } 17 | 18 | const StyledBox = styled(Box)` 19 | position: absolute; 20 | pointer-events: auto; 21 | border-radius: 10px; 22 | border: 1px solid #747676; 23 | background: #353738; 24 | display: flex; 25 | flex-direction: column; 26 | `; 27 | 28 | function randomOffset(size: number, percent: number) { 29 | const sign = Math.sign(Math.random() - 0.5); 30 | return sign * Math.floor((size * percent / 100) * Math.random()); 31 | } 32 | 33 | const WinMinSize = 250; 34 | 35 | export const Win: FC = ({ img, title, footer: defaultFooter, children }) => { 36 | const { footer, zIndex, close, focus, searching, setSearched } = useContext(WinContext); 37 | const [resizing, setResizing] = useState(false); 38 | const [{ top, left, width, height, cursor, fullscreen }, setProperties] = useState(() => { 39 | const height = window.innerHeight / 2; 40 | const width = window.innerWidth / 2.5; 41 | const randX = randomOffset(window.innerWidth - width, 20); 42 | const randY = randomOffset(window.innerHeight - height, 20); 43 | return { 44 | width, 45 | height, 46 | top: Math.floor((window.innerHeight - height) / 2) + randY, 47 | left: Math.floor((window.innerWidth - width) / 2) + randX, 48 | cursor: 'auto', 49 | fullscreen: false 50 | }}); 51 | 52 | const onCursorChange = useCallback((cursor: string) => setProperties(properties => ({ ...properties, cursor })), []); 53 | 54 | const onResize = useCallback((offset: { top: number, left: number, bottom: number, right: number }) => { 55 | setProperties(properties => { 56 | let top = properties.top + offset.top; 57 | let left = properties.left + offset.left; 58 | let height = properties.height + offset.bottom - offset.top; 59 | let width = properties.width + offset.right - offset.left; 60 | 61 | // keep top side in the window 62 | if (top < 0) { 63 | height += top; 64 | top = 0; 65 | } 66 | 67 | // keep left side in the window 68 | if (left < 0) { 69 | width += left; 70 | left = 0; 71 | } 72 | 73 | // respect min height 74 | if (height < WinMinSize) { 75 | if (offset.top) { 76 | top -= WinMinSize - height; 77 | } 78 | height = WinMinSize; 79 | } 80 | 81 | // respect min width 82 | if (width < WinMinSize) { 83 | if (offset.left) { 84 | left -= WinMinSize - width; 85 | } 86 | width = WinMinSize; 87 | } 88 | 89 | // keep left side in the window 90 | if (left + width > window.innerWidth) { 91 | width = window.innerWidth - left; 92 | } 93 | 94 | // keep bottom side in the window 95 | if (top + height > window.innerHeight) { 96 | height = window.innerHeight - top; 97 | } 98 | 99 | const updated = {...properties, top, left, width, height }; 100 | 101 | return arePropertiesEquals(properties, updated) ? properties : updated; 102 | }); 103 | }, []); 104 | 105 | const toggleFullScreen = useCallback(() => { 106 | setProperties(properties => ({ ...properties, fullscreen: !properties.fullscreen })); 107 | }, []); 108 | 109 | const onMouseDown = useCallback(() => focus(), [focus]); 110 | 111 | const onDragMove = useCallback((e: MouseEvent) => { 112 | setProperties(properties => ({ 113 | ...properties, 114 | top: minMax(0, properties.top + e.movementY, window.innerHeight - properties.height), 115 | left: minMax(0, properties.left + e.movementX, window.innerWidth - properties.width), 116 | })) 117 | }, []); 118 | 119 | const onChange = useCallback((e: ChangeEvent) => { 120 | setSearched(e.target.value.toLowerCase().trim()); 121 | }, [setSearched]); 122 | 123 | // Resize the box when the window size change (reduce it size, and move its position) 124 | useEffect(() => { 125 | const onWindowResize = () => { 126 | setProperties(properties => { 127 | let { top, left, width, height } = properties; 128 | if (left + width > window.innerWidth) { 129 | width = window.innerWidth - left; 130 | if (width < WinMinSize) { 131 | left = Math.max(0, left - (WinMinSize - width)); 132 | width = WinMinSize; 133 | } 134 | } 135 | if (top + height > window.innerHeight) { 136 | height = window.innerHeight - top; 137 | if (height < WinMinSize) { 138 | top = Math.max(0, top - (WinMinSize - height)); 139 | height = WinMinSize; 140 | } 141 | } 142 | const updated = {...properties, top, left, width, height }; 143 | return arePropertiesEquals(properties, updated) ? properties : updated; 144 | }); 145 | }; 146 | 147 | window.addEventListener('resize', onWindowResize); 148 | return () => window.removeEventListener('resize', onWindowResize); 149 | }, []); 150 | 151 | return ( 152 | 162 |
171 | 172 | {children} 173 | { searching && } 174 | 175 |