Clicking Play at the center of a gallery cover now runs the shared-element view transition into the /ejs (EmulatorJS) or /ruffle hero, matching the card → details morph. - useGameActions gains an optional `coverEl` resolver; `play()` wraps the navigation in `morphTransition` (cover → player hero, same `rom-cover-<id>` tag the player paints statically) and awaits the push so the snapshot is taken after the player renders. GameCard supplies its GameCover box. - The player heroes only seeded `rom` from `currentRom` (set via GameDetails), so a direct gallery→play left `rom` null and the `v-if`-gated hero never rendered — nothing to morph into. Seed a lightweight `heroSeed` SimpleRom from the gallery store (new `galleryRoms.getRomById`) so the cover paints its morph tag immediately; `rom` fills in on mount. Play is disabled until the full payload loads. - Enable hover-motion on both player heroes so the cover spin / hover video work there too. - Arcade systems (arcade / neogeoaes / neogeomvs) skip the cartridge slot-in animation (new `isArcadeSystem`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 KiB
RomM Frontend Architecture
Comprehensive documentation of the RomM frontend: a Vue 3 single-page application powering the retro gaming platform UI.
Table of Contents
- Overview
- High-Level Architecture
- Directory Structure
- Application Lifecycle
- Routing & Navigation
- State Management (Pinia Stores)
- API & Data Layer
- Component Architecture
- Views & Pages
- Console Mode
- Emulation Integration
- Theming & Styling
- Internationalization (i18n)
- Real-Time Communication
- Caching Strategy
- Utilities & Composables
- Build & Tooling
- Type System
1. Overview
| Property | Value |
|---|---|
| Framework | Vue 3.4.27 (Composition API, <script setup>) |
| Build Tool | Vite 6.4.2 |
| Language | TypeScript 5.7.3 (noImplicitAny: true) |
| UI Library | Vuetify 3.9.2 (Material Design) |
| CSS | Tailwind CSS 4.0.0 + Vuetify themes |
| State Management | Pinia 3.0.1 (18 stores) |
| Routing | Vue Router 4.3.2 |
| HTTP Client | Axios 1.15.0 |
| i18n | vue-i18n 11.1.10 (17 languages) |
| Real-time | Socket.IO Client 4.7.5 |
| Icons | Material Design Icons (MDI) 7.4.47 |
| Node | 24 (via .nvmrc) |
Total: ~216 Vue components (168 under components/, rest in views/console/layouts), 18 Pinia stores, 17 API service modules, 36 named routes across 3 layouts.
2. High-Level Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Browser / PWA │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Vue Router │ │ Pinia Stores │ │ Mitt Emitter │ │
│ │ (36 routes) │ │ (18 stores) │ │ (80+ events) │ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬──────────┘ │
│ │ │ │ │
│ ┌──────v──────────────────v─────────────────────v──────────┐ │
│ │ Components (~216) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Gallery │ │ Details │ │ Settings │ │ Console │ │ │
│ │ │ Mode │ │ Page │ │ Pages │ │ Mode │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────v───────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ ┌─────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐ │ │
│ │ │ Axios │ │ Socket.IO │ │ Cache │ │ Compose │ │ │
│ │ │ API │ │ Client │ │ Service │ │ -ables │ │ │
│ │ └────┬────┘ └─────┬─────┘ └────┬─────┘ └─────────┘ │ │
│ └───────┼──────────────┼─────────────┼─────────────────────┘ │
│ │ │ │ │
├───────────┼──────────────┼─────────────┼─────────────────────────┤
│ v v v │
│ Backend API WebSocket /ws Browser Cache API │
│ (/api/*) (/ws/socket.io) (IndexedDB) │
└─────────────────────────────────────────────────────────────────┘
Layered Architecture
┌─────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ views/ Page-level route components │
│ layouts/ Auth, Main, Console layouts │
│ components/ Feature & common components │
│ console/ TV/gamepad-optimized UI │
├─────────────────────────────────────────────────────────┤
│ STATE LAYER │
│ stores/ 18 Pinia stores (auth, roms, config...) │
│ composables/ Reusable stateful logic │
├─────────────────────────────────────────────────────────┤
│ DATA LAYER │
│ services/api/ 17 Axios-based API modules │
│ services/cache/ Browser Cache API wrapper │
│ services/socket Socket.IO client │
├─────────────────────────────────────────────────────────┤
│ INFRASTRUCTURE LAYER │
│ plugins/ Vuetify, Pinia, i18n, Router │
│ styles/ Themes, global CSS │
│ locales/ 17 language packs │
│ types/ TypeScript definitions │
│ utils/ Helpers (formatting, emulation, covers) │
│ __generated__/ OpenAPI-generated types │
└─────────────────────────────────────────────────────────┘
3. Directory Structure
frontend/
├── index.html # HTML entry point (<div id="app">)
├── package.json # Dependencies & scripts
├── vite.config.js # Vite build config with plugins
├── tsconfig.json # TypeScript configuration
├── eslint.config.js # ESLint flat config
├── .nvmrc # Node 24
│
├── assets/ # Static assets (logos, platform icons)
├── public/ # Public static files
│
└── src/
├── main.ts # Entry point: app creation & plugin init
├── RomM.vue # Root component
│
├── plugins/ # Vue plugin setup
│ ├── index.ts # Plugin registration (Vuetify, Pinia, i18n, Mitt)
│ ├── router.ts # Vue Router (36 routes, guards, permissions)
│ ├── vuetify.ts # Vuetify instance (themes, icons)
│ ├── pinia.ts # Pinia store with router injection
│ ├── pinia.d.ts # Pinia type augmentation ($router)
│ ├── mdeditor.ts # Markdown editor with XSS plugin
│ └── transition/ # View Transitions API polyfill
│
├── layouts/ # Layout wrappers
│ ├── Auth.vue # Authentication pages layout
│ └── Main.vue # Authenticated pages layout (+ all dialogs)
│
├── views/ # Page-level components (routes)
│ ├── Auth/ # Setup, Login, ResetPassword, Register
│ ├── Gallery/ # Platform, Search, Collection variants
│ ├── Player/ # EmulatorJS, RuffleRS
│ ├── Settings/ # Profile, UI, Library, Metadata, Admin, Stats
│ ├── Home.vue # Dashboard
│ ├── GameDetails.vue # ROM detail page (8 tabs)
│ ├── Scan.vue # Library scan
│ ├── Patcher.vue # ROM patcher
│ ├── Pair.vue # Device pairing
│ └── 404.vue # Not found
│
├── components/ # ~168 Vue components
│ ├── common/ # Shared, reusable
│ │ ├── Collection/ # Collection cards, dialogs (9 components)
│ │ ├── Dialog/ # LoadingDialog, SearchCover
│ │ ├── EmptyStates/ # 8 empty state variants
│ │ ├── Game/ # Game cards, dialogs, controls (48+ components)
│ │ ├── Navigation/ # AppBar, drawers, nav buttons (13 components)
│ │ ├── Notifications/ # Snackbar, upload progress
│ │ └── Platform/ # Platform cards, dialogs (8 components)
│ ├── Details/ # Game detail sub-components (14+)
│ ├── Gallery/ # Gallery app bar, filters, skeleton
│ ├── Home/ # Dashboard sections (8 components)
│ ├── Scan/ # Scan platform component
│ └── Settings/ # Settings sub-components (25+)
│ ├── Administration/ # Users, tokens, tasks
│ ├── ClientApiTokens/ # API token list, create, pair
│ ├── LibraryManagement/ # Platform bindings, exclusions
│ ├── MetadataSources/ # Provider config & priority
│ ├── ServerStats/ # Library stats widgets
│ ├── UserInterface/ # Theme, view, locale
│ └── UserProfile/ # Profile, password, avatar
│
├── console/ # Console mode (TV/gamepad UI)
│ ├── Layout.vue # Console layout with input bus
│ ├── index.css # Console-specific styles
│ ├── views/ # Console pages
│ │ ├── Home.vue # Platform grid, collections
│ │ ├── GamesList.vue # ROM grid for platform/collection
│ │ ├── Game.vue # Game details with spatial nav
│ │ └── Play.vue # Emulator in console mode
│ ├── components/ # Console-specific components (12)
│ │ ├── GameCard.vue, SystemCard.vue, CollectionCard.vue
│ │ ├── BackButton.vue, NavigationHint.vue
│ │ ├── ScreenshotLightbox.vue, SettingsModal.vue
│ │ └── ArrowKeysIcon.vue, DPadIcon.vue, FaceButtons.vue
│ ├── composables/ # Console-specific composables
│ │ ├── useConsoleTheme.ts # Theme management
│ │ ├── useThemeAssets.ts # Asset path resolution
│ │ ├── useBackgroundArt.ts # Double-buffered backgrounds
│ │ ├── useSpatialNav.ts # Grid navigation
│ │ ├── useElementRegistry.ts # Focus management
│ │ ├── useInputScope.ts # Scoped input handling
│ │ └── useRovingDom.ts # ARIA roving tabindex
│ ├── input/ # Input system
│ │ ├── bus.ts # Stack-based input scope manager
│ │ ├── actions.ts # 12 input actions
│ │ ├── config.ts # Keyboard + gamepad mappings
│ │ ├── keyboard.ts # Keyboard listener
│ │ └── gamepad.ts # Gamepad polling (rAF)
│ ├── constants/ # Console constants (sizes, timings, themes)
│ └── utils/ # Console helpers
│ ├── sfx.ts # Procedural Web Audio SFX
│ └── assetResolver.ts # Theme-aware asset loading
│
├── stores/ # 18 Pinia stores
│ ├── auth.ts # Current user & scopes
│ ├── roms.ts # ROM library (largest store, 400+ lines)
│ ├── platforms.ts # Platform catalog
│ ├── collections.ts # Regular, virtual, smart collections
│ ├── config.ts # Server configuration
│ ├── heartbeat.ts # Server status & feature flags
│ ├── galleryFilter.ts # 13+ filter types
│ ├── galleryView.ts # View mode, aspect ratio
│ ├── navigation.ts # Drawer & nav state
│ ├── console.ts # Console mode navigation indices
│ ├── scanning.ts # Scan progress
│ ├── tasks.ts # Background task status
│ ├── upload.ts # Upload progress tracking
│ ├── download.ts # Download queue
│ ├── playing.ts # Emulator state
│ ├── language.ts # Locale selection
│ ├── notifications.ts # Toast queue
│ └── users.ts # User list (admin)
│
├── services/ # Data fetching & communication
│ ├── api/ # 16 Axios API modules (+ index.ts client)
│ │ ├── index.ts # Axios instance (CSRF, interceptors)
│ │ ├── rom.ts # ROM CRUD, upload, download
│ │ ├── platform.ts # Platform CRUD
│ │ ├── collection.ts # Collection operations
│ │ ├── user.ts # User management
│ │ ├── identity.ts # Login, logout, password reset
│ │ ├── config.ts # Backend configuration
│ │ ├── task.ts # Task monitoring
│ │ ├── firmware.ts # Firmware uploads
│ │ ├── save.ts # Game saves
│ │ ├── state.ts # Save states
│ │ ├── screenshot.ts # Screenshots
│ │ ├── setup.ts # Setup wizard
│ │ ├── sgdb.ts # SteamGridDB covers
│ │ ├── export.ts # Gamelist.xml + Pegasus exports
│ │ ├── play-session.ts # Play session tracking
│ │ └── client-token.ts # API token management
│ ├── cache/ # Experimental response cache
│ │ ├── index.ts # Browser Cache API wrapper
│ │ └── api.ts # Cached API service
│ └── socket.ts # Socket.IO client
│
├── composables/ # Vue 3 composition utilities
│ ├── useUISettings.ts # Settings sync (localStorage ↔ backend)
│ ├── useFavoriteToggle.ts # Favorites collection management
│ ├── useGameAnimation.ts # CD spin, cartridge load, video hover
│ └── useAutoScroll.ts # Auto-scroll on content change
│
├── styles/ # Global styles
│ ├── themes.ts # Dark/light theme definitions
│ ├── common.css # Utility classes
│ ├── fonts.css # Font definitions
│ └── scrollbar.css # Custom scrollbar
│
├── locales/ # i18n translations
│ ├── index.ts # Loader with dynamic imports
│ ├── en_US/ # English (default)
│ ├── en_GB/, fr_FR/, de_DE/, es_ES/, it_IT/, ja_JP/
│ ├── ko_KR/, pt_BR/, pl_PL/, ro_RO/, ru_RU/
│ ├── zh_CN/, zh_TW/, cs_CZ/, hu_HU/, bg_BG/
│ └── (each has: collection, common, console, detail,
│ emulator, gallery, home, library, login,
│ navigation, patcher, platform, scan, settings, task)
│
├── types/ # TypeScript definitions
│ ├── emitter.d.ts # 80+ event types
│ ├── main.d.ts # Global augmentations
│ ├── rom.d.ts # ROM selection types
│ ├── user.d.ts # User form types
│ ├── ruffle.d.ts # Flash emulator types
│ ├── rompatcher.d.ts # ROM patcher types
│ └── index.ts # Utility types
│
├── utils/ # Helper functions
│ ├── index.ts # ~825 lines of utilities
│ ├── covers.ts # Procedural SVG cover generation
│ ├── formData.ts # FormData builder
│ ├── tasks.ts # Task status maps
│ └── indexdb-monitor.ts # IndexedDB change detection
│
└── __generated__/ # OpenAPI codegen output
└── models/ # Generated TypeScript interfaces
4. Application Lifecycle
Startup Sequence
index.html
└── <script type="module" src="src/main.ts">
│
├── Create Vue app with RomM.vue as root
├── Register plugins (Vuetify, Pinia, i18n, Mitt, MD Editor)
├── Install Vue Router
│
├── Initialize critical stores (before mount):
│ ├── authStore.fetchCurrentUser()
│ ├── configStore.fetchConfig()
│ ├── heartbeatStore.fetchHeartbeat()
│ └── tasksStore.fetchTasks()
│
└── app.mount("#app")
Plugin Registration Order
1. Vuetify : Material Design components, themes, icons
2. Pinia : State management (with router injection)
3. vue-i18n : Internationalization (17 locales)
4. Mitt : Event emitter (provided as 'emitter')
5. MD Editor : Markdown editor with XSS plugin
6. Vue Router : Navigation with guards
Request Lifecycle
Component Action
│
├─ Store Action (e.g., romsStore.fetchRoms())
│ │
│ ├─ Cache check (if experimental cache enabled)
│ │ ├─ Cache hit → return cached, fire background update
│ │ └─ Cache miss → continue to API
│ │
│ ├─ API Service call (e.g., romApi.getRoms(params))
│ │ │
│ │ ├─ Axios request interceptor:
│ │ │ ├─ Add CSRF token (x-csrftoken from cookie)
│ │ │ └─ Track in inflight set
│ │ │
│ │ ├─ HTTP request to /api/*
│ │ │
│ │ └─ Axios response interceptor:
│ │ ├─ Remove from inflight set
│ │ ├─ 403 → clear session, redirect to login
│ │ └─ Emit 'network-quiesced' when all requests complete
│ │
│ └─ Store mutation (update reactive state)
│
└─ Component reacts via reactive refs/getters
5. Routing & Navigation
Route Map
/ (root)
│
├── Auth Layout (public)
│ ├── /setup → Setup wizard (3 steps)
│ ├── /login → Login (password + OIDC)
│ ├── /reset-password → Password reset
│ └── /register → Invite-based registration
│
├── /pair → Device pairing (standalone, no layout)
│
├── Main Layout (authenticated)
│ ├── / → Home dashboard
│ ├── /search → Global ROM search
│ ├── /platform/:platform → Platform gallery
│ ├── /collection/:collection → Regular collection
│ ├── /collection/virtual/:id → Virtual collection
│ ├── /collection/smart/:id → Smart collection
│ ├── /rom/:rom → Game details (8 tabs)
│ ├── /rom/:rom/ejs → EmulatorJS player
│ ├── /rom/:rom/ruffle → Ruffle Flash player
│ ├── /april-fools → April Fools easter egg (toggleable)
│ ├── /scan → Library scanner [platforms.write]
│ ├── /patcher → ROM patcher
│ ├── /user/:user → User profile
│ ├── /user-interface → UI settings
│ ├── /library-management → Library config [platforms.write]
│ ├── /metadata-sources → Metadata provider status
│ ├── /client-api-tokens → API token management [me.write]
│ ├── /administration → User & task admin [users.write]
│ ├── /server-stats → Library statistics
│ └── /* → 404
│
└── Console Layout (authenticated, TV/gamepad)
├── /console → Console home
├── /console/platform/:id → Console game list
├── /console/collection/:id → Console collection
├── /console/collection/smart/:id
├── /console/collection/virtual/:id
├── /console/rom/:rom → Console game details
└── /console/rom/:rom/play → Console emulator
Route Guards
| Guard | Type | Behavior |
|---|---|---|
Global beforeEach |
Navigation | Setup wizard redirect, auth check, scope validation |
ROM beforeEnter |
Per-route | Pre-fetches ROM data before rendering |
Global beforeResolve |
Navigation | View Transitions API animation |
| Scroll behavior | Router config | Restores saved scroll position on back/forward |
Permission-Protected Routes
| Route | Required Scope |
|---|---|
/scan |
platforms.write |
/library-management |
platforms.write |
/client-api-tokens |
me.write |
/administration |
users.write |
6. State Management (Pinia Stores)
Store Overview
┌─────────────────────────────────────────────────────┐
│ PINIA STORES │
├──────────────┬──────────────────────────────────────┤
│ Core Data │ roms, platforms, collections, users │
├──────────────┼──────────────────────────────────────┤
│ Auth & Config│ auth, config, heartbeat │
├──────────────┼──────────────────────────────────────┤
│ UI State │ navigation, galleryFilter, galleryView│
│ │ language, notifications, console │
├──────────────┼──────────────────────────────────────┤
│ Operations │ scanning, tasks, upload, download, │
│ │ playing │
└──────────────┴──────────────────────────────────────┘
Key Stores in Detail
roms (largest store, ~400 lines)
| State | Type | Description |
|---|---|---|
_allRoms |
SimpleRom[] |
Current page of ROMs |
currentPlatform |
Platform | null |
Active platform filter |
currentCollection |
Collection | null |
Active collection filter |
currentRom |
DetailedRom | null |
Selected ROM details |
recentRoms |
SimpleRom[] |
Recently added |
continuePlayingRoms |
SimpleRom[] |
Recently played |
selectedIDs |
Set<number> |
Multi-select state |
fetchOffset / fetchTotalRoms |
number |
Pagination cursor |
orderBy / orderDir |
string |
Sort (persisted to localStorage) |
characterIndex |
Record<string, number> |
A-Z jump index |
Key actions: fetchRoms(), fetchRecentRoms(), fetchContinuePlayingRoms(), add(), update(), remove(), resetPagination()
galleryFilter
Manages 13+ filter dimensions with logic operators:
| Filter | Type | Logic |
|---|---|---|
| Genres | string[] |
any / all / none |
| Franchises | string[] |
any / all / none |
| Collections | string[] |
any / all / none |
| Companies | string[] |
any / all / none |
| Age Ratings | string[] |
any / all / none |
| Regions | string[] |
any / all / none |
| Languages | string[] |
any / all / none |
| Player Counts | string[] |
any / all / none |
| Statuses | string[] |
any / all / none |
| Matched | boolean | null |
toggle |
| Favorites | boolean | null |
toggle |
| Duplicates | boolean | null |
toggle |
| Playable | boolean | null |
toggle |
| RetroAchievements | boolean | null |
toggle |
| Missing | boolean | null |
toggle |
| Verified | boolean | null |
toggle |
collections
Manages three collection types:
| Type | State | Description |
|---|---|---|
| Regular | allCollections |
User-created collections |
| Virtual | virtualCollections |
Auto-generated by platform/genre |
| Smart | smartCollections |
Filter-criteria based |
| Favorite | favoriteCollection |
Special favorite collection |
heartbeat
Server capability flags used throughout the UI:
METADATA_SOURCES: { IGDB, SS, MOBY, RA, STEAMGRIDDB, LAUNCHBOX, ... }
EMULATION: { DISABLE_EMULATOR_JS, DISABLE_RUFFLE_RS }
FRONTEND: { DISABLE_USERPASS_LOGIN, DISABLE_LOGS_VIEWER, YOUTUBE_BASE_URL }
OIDC: { ENABLED, AUTOLOGIN, PROVIDER, RP_INITIATED_LOGOUT }
TASKS: { scheduled task configurations }
Persistence Strategy
| Storage | What | Examples |
|---|---|---|
| localStorage | UI preferences | View mode, sort order, theme, drawer state, boxart style |
| Backend (user.ui_settings) | Synced preferences | Same as localStorage, synced via useUISettings composable |
| In-memory (Pinia) | Session data | ROMs, platforms, collections, auth state |
| Browser Cache API | API responses | Optional experimental cache with background updates |
7. API & Data Layer
Axios Client Setup
Location: services/api/index.ts
const api = axios.create({
baseURL: "/api",
timeout: 120000, // 2 minutes
});
Request Interceptor:
- Injects CSRF token from
romm_csrftokencookie asx-csrftokenheader - Tracks inflight requests in a Set
Response Interceptor:
- On 403: clears session cookie, refetches CSRF, redirects to
/login - Fires
network-quiescedcustom event when all requests complete (250ms debounce)
API Service Modules
| Module | Key Endpoints |
|---|---|
rom.ts |
CRUD, chunked upload, download, search, notes |
collection.ts |
CRUD for regular/smart/virtual + ROM association |
platform.ts |
CRUD, supported list |
user.ts |
CRUD, profile, RA refresh, invite links |
identity.ts |
Login, logout, forgot/reset password |
config.ts |
Platform bindings, versions, exclusions |
task.ts |
List, status, run |
firmware.ts |
Upload, list, delete |
save.ts |
Upload, update, delete |
state.ts |
Upload, update, delete |
screenshot.ts |
Upload, update |
setup.ts |
Library structure, platform creation |
sgdb.ts |
Cover art search |
export.ts |
Gamelist.xml export, Pegasus export |
play-session.ts |
Play session ingestion & listing |
client-token.ts |
Token CRUD, pair, exchange |
Chunked Upload System (rom.ts)
1. POST /roms/upload/start
Headers: X-Upload-Filename, X-Upload-Total-Size, X-Upload-Total-Chunks
→ Returns upload_id
2. PUT /roms/upload/{upload_id} (per 10MB chunk)
Headers: X-Chunk-Number, X-Chunk-Size
→ Retry: 3 attempts with exponential backoff
3. POST /roms/upload/{upload_id}/complete
→ 10-minute timeout for assembly
On failure: POST /roms/upload/{upload_id}/cancel
Key Data Flows
ROM Gallery Loading:
Component mount → romsStore.fetchRoms()
→ cachedApiService.getRoms(params, onBackgroundUpdate)
→ Cache hit? Return cached + background refresh
→ API call: GET /api/roms?platform_id=...&limit=72&offset=0&...
→ _postFetchRoms(): update ROMs, pagination, character index, filter values
→ Components react via reactive getters
Filter & Search:
User sets filter → galleryFilterStore.setSelected*()
→ Component detects change → romsStore.fetchRoms()
→ _buildRequestParams() merges all 13+ filter dimensions
→ API returns filtered paginated results
→ _postFetchRoms() updates available filter values from response
Settings Sync:
User changes setting → localStorage updated
→ useUISettings watcher fires
→ PUT /api/users/{id} with ui_settings JSON
→ Backend returns updated user
→ authStore.setCurrentUser(data)
→ On next login: user.ui_settings hydrates localStorage
8. Component Architecture
Organization Pattern
Feature-based hybrid with three tiers:
Tier 1: Common (shared, reusable)
├── Collection/ Cards, list items, 6 dialogs
├── Dialog/ Loading, SearchCover
├── EmptyStates/ 8 variants (game, platform, collection, firmware, saves...)
├── Game/ Cards, 14 dialogs, PlayBtn, FavBtn, VirtualTable (48+)
├── Navigation/ AppBar, 3 drawers, 10 nav buttons
├── Platform/ Cards, PlatformIcon, 3 dialogs
└── Notifications/ Snackbar, upload progress
Tier 2: Feature-specific
├── Details/ Game detail tabs (14+ sub-components)
├── Gallery/ AppBar variants, filters, skeleton
├── Home/ Dashboard sections (8 components)
├── Scan/ Scan platform component
└── Settings/ 25+ settings sub-components
Tier 3: Console Mode
└── console/ 12 components + 7 composables + input system
Component Communication
┌─────────────────┐ props/emit ┌─────────────────┐
│ Parent │ ←───────────────→ │ Child │
│ Component │ │ Component │
└────────┬────────┘ └────────┬────────┘
│ │
store refs store refs
│ │
v v
┌─────────────────────────────────────────────────────────┐
│ Pinia Stores │
└─────────────────────────────────────────────────────────┘
│
mitt events (80+ types)
│
v
┌─────────────────────────────────────────────────────────┐
│ Cross-Component Events │
│ showEditRomDialog, snackbarShow, playGame, etc. │
└─────────────────────────────────────────────────────────┘
Patterns used:
- Props/emit for parent-child communication
- Pinia stores for shared state across components
- Mitt emitter for loosely-coupled cross-component events (dialog triggers, notifications)
- Provide/inject for console input scoping
Dialog System
All dialogs use Vuetify's v-dialog wrapped in a custom RDialog component:
RDialog (wrapper)
├── Header slot (title + close button)
├── Toolbar slot (optional)
├── Prepend slot
├── Content slot (scrollable)
├── Append slot
└── Footer slot (actions)
15 game dialogs: EditRom (with 4 sub-components), UploadRom, DeleteRom, MatchRom, NoteDialog, ShowQRCode, CopyDownloadLink, SelectSave, UploadSaves, DeleteSaves, SelectState, UploadStates, DeleteStates
All triggered via Mitt events, rendered in Main.vue layout.
9. Views & Pages
Home Dashboard (/)
| Section | Data Source | Toggleable |
|---|---|---|
| Stats cards | GET /api/stats |
Yes (localStorage) |
| Recently added | romsStore.fetchRecentRoms() |
Yes |
| Continue playing | romsStore.fetchContinuePlayingRoms() |
Yes |
| Platforms grid | platformsStore |
Yes |
| Collections | collectionsStore |
Yes |
| Smart collections | collectionsStore |
Yes |
| Virtual collections | collectionsStore |
Yes |
Platform Gallery (/platform/:platform)
- Grid or table view (3 sizes + list)
- Infinite scroll pagination (72 per page)
- Multi-select for bulk operations
- 3D tilt effect on cards (vanilla-tilt)
- Virtual table for list mode performance
Game Details (/rom/:rom)
8-tab interface:
| Tab | Content |
|---|---|
| Details | File info + game metadata |
| Manual | PDF viewer (if available) |
| Save Data | Save file management |
| Personal | Notes, rating, play time, status |
| How Long To Beat | Playtime estimates (if HLTB data) |
| Additional Content | DLC/expansions (mobile) |
| Related Games | Remakes/remasters (mobile) |
| Screenshots | Screenshot gallery |
Scan (/scan)
- Platform multi-select
- Metadata source selection with priority ordering
- Real-time progress via Socket.IO
- Log auto-scroll
- Hash calculation toggle
ROM Patcher (/patcher)
Supports: .ips, .ups, .bps, .ppf, .rup, .aps, .bdf, .pmsr, .vcdiff
- Drag-and-drop ROM + patch files
- Platform selection for output
- Save locally or upload to RomM
10. Console Mode
A complete TV/gamepad-optimized interface under /console/.
Architecture
Console Layout
├── Input Bus (keyboard + gamepad → actions)
├── Theme System (CSS variables per theme)
├── Spatial Navigation (grid-based focus)
├── Sound Effects (Web Audio synthesis)
│
├── Home View
│ ├── Platform cards (spatial nav)
│ ├── Continue playing
│ └── Collections grid
│
├── Games List View
│ ├── Game cards with lazy loading
│ └── Virtual scrolling
│
├── Game Detail View
│ ├── Description, metadata, screenshots
│ ├── Save state management
│ └── Play button → Emulator
│
└── Play View
└── EmulatorJS with save/state/BIOS selection
Input System
Hardware Input (keyboard / gamepad)
│
├── Keyboard Listener (keydown → action mapping)
│ └── Ignores when focused on INPUT/TEXTAREA
│
├── Gamepad Poller (requestAnimationFrame loop)
│ ├── Button press detection (with repeat delay)
│ └── Analog stick threshold (0.2)
│
└── Input Bus (stack-based scope manager)
├── Global shortcuts (always active)
├── Scoped listeners (context-dependent)
└── Action dispatch with SFX feedback
12 Input Actions: moveUp, moveDown, moveLeft, moveRight, confirm, back, menu, delete, tabNext, tabPrev, toggleFavorite
Repeat Timing: 350ms initial delay, 120ms repeat
Procedural Sound Effects (Web Audio API)
| Sound | Frequency | Duration | When |
|---|---|---|---|
move |
860Hz | 20ms | Navigation |
confirm |
680→880Hz sweep | 19ms | Selection |
back |
300Hz | 85ms | Return |
error |
180Hz + 140Hz | 180ms | Failure |
delete |
260Hz + 180Hz | 120ms | Destructive action |
favorite |
600Hz + 950Hz | Dual burst | Toggle |
All synthesized with sine/noise blend, exponential envelopes, low-pass filter, and waveshaper saturation.
Console Composables
| Composable | Purpose |
|---|---|
useSpatialNav |
Grid navigation with boundary enforcement |
useConsoleTheme |
Theme CSS variable injection |
useThemeAssets |
Format-aware asset resolution (SVG > PNG > JPG) |
useBackgroundArt |
Double-buffered background transitions |
useElementRegistry |
Focus element tracking per section |
useInputScope |
Dependency-injected input subscription |
useRovingDom |
ARIA roving tabindex with auto-scroll |
11. Emulation Integration
EmulatorJS
Location: views/Player/EmulatorJS/
| Feature | Details |
|---|---|
| Core selection | Platform-specific core mapping (40+ platforms) |
| BIOS/firmware | Selectable from uploaded firmware |
| Save management | Upload, download, delete saves & states |
| Multi-disc | Disc selection for multi-file games |
| Cache | IndexedDB cache for game data |
| Fullscreen | With keyboard lock |
| Netplay | Socket.IO-based multiplayer |
| Controls | Per-core configurable via config.yml |
Ruffle (Flash)
Location: views/Player/RuffleRS/
- SWF/Flash game emulation via Ruffle 0.2.0-nightly
- Fullscreen support
- Background color customization
Platform Detection
utils/index.ts provides:
getSupportedEJSCores(platform): maps platforms to EmulatorJS coresisEJSEmulationSupported(rom): checks WebGL + server configisRuffleEmulationSupported(rom): checks Flash platformisCDBasedSystem(platform): 31 CD-based platforms for animation logic
12. Theming & Styling
Theme System
Location: styles/themes.ts
| Theme | Background | Primary | Accent |
|---|---|---|---|
| Dark | #0D1117 |
#8B74E8 |
#E1A38D |
| Light | #F2F4F8 |
#371f69 |
#E1A38D |
Detection priority: settings.theme localStorage → prefers-color-scheme media query → dark default
Vuetify handles theme switching. Additional shared brand colors: romm-red, romm-green, romm-blue, romm-gold.
CSS Stack
| Layer | Technology | Scope |
|---|---|---|
| Component | Vuetify classes + scoped <style> |
Per-component |
| Utility | Tailwind CSS 4.0 | Inline utility classes |
| Global | styles/common.css |
App-wide utilities |
| Scrollbar | styles/scrollbar.css |
Custom scrollbar |
| Console | console/index.css |
Console mode only |
Procedural Cover Generation
utils/covers.ts generates SVG covers with:
- Hash-based deterministic gradients (consistent per game)
- Collection covers with multi-image grid
- Favorite covers with star icon
- Missing/unmatched covers with icons
- Aspect-ratio-aware empty placeholders
13. Internationalization (i18n)
Setup
- Library: vue-i18n 11.1.10 (Composition API mode)
- Locale loading: Dynamic glob import from
locales/{lang}/*.json - Default:
en_US - Fallback:
en_US
Supported Languages (17)
| Code | Language |
|---|---|
en_US |
English (US, default) |
en_GB |
English (UK) |
fr_FR |
French |
de_DE |
German |
es_ES |
Spanish |
it_IT |
Italian |
ja_JP |
Japanese |
ko_KR |
Korean |
pt_BR |
Portuguese (Brazil) |
pl_PL |
Polish |
ro_RO |
Romanian |
ru_RU |
Russian |
zh_CN |
Chinese (Simplified) |
zh_TW |
Chinese (Traditional) |
cs_CZ |
Czech (custom plural rules) |
hu_HU |
Hungarian |
bg_BG |
Bulgarian |
Namespace Organization
Each locale directory contains translation files per feature:
collection, common, console, detail, emulator, gallery, home, library, login, navigation, patcher, platform, scan, settings, task
14. Real-Time Communication
Socket.IO Client
Location: services/socket.ts
io({
path: "/ws/socket.io/",
transports: ["websocket", "polling"],
autoConnect: false,
});
Usage: Manually connected during upload and scan operations.
Events consumed:
scan:update_stats: live scan progress (platform/ROM counts)scan:log: scan log messagesscan:stop: scan completion
Dev proxy: Vite proxies /ws to backend with WebSocket upgrade support.
15. Caching Strategy
Experimental Browser Cache
Location: services/cache/
Opt-in: localStorage.settings.enableExperimentalCache
Request Flow with Cache:
┌──────────┐ cache hit ┌──────────┐
│ Component├───────────────→│ Cached │ → Immediate render
│ │ │ Response │
│ │ meanwhile │ │
│ │◄───────────────│ Background│ → API fetch
│ │ onBackgroundUpdate │ → Update if different
└──────────┘ └──────────┘
Features:
- Browser Cache API (requires HTTPS)
- Request deduplication (concurrent identical requests share promise)
- Background update callbacks (stale-while-revalidate pattern)
- Pattern-based cache clearing
- Used for ROM lists and recent/continue playing data
16. Utilities & Composables
Global Composables
| Composable | Purpose | Key Features |
|---|---|---|
useUISettings |
Settings persistence | Singleton, localStorage ↔ backend bidirectional sync, 25+ settings |
useFavoriteToggle |
Favorites management | Auto-creates Favorites collection, toggle with notifications |
useGameAnimation |
Card animations | CD spin (5000 deg/s), cartridge load, video hover (1.5s delay) |
useAutoScroll |
Scroll management | Throttled (50ms), mutation observer, respects user scroll |
Utility Functions (utils/index.ts, ~825 lines)
Display:
formatBytes(): human-readable sizes (B through PB)formatTimestamp(): locale-aware datesformatRelativeDate(): relative time strings
Emojis & Localization:
regionToEmoji(): 50+ region codes → country flagslanguageToEmoji(): 40+ language codes → country flags
Emulation Support:
getSupportedEJSCores(): platform → EmulatorJS core mappingisEJSEmulationSupported(): WebGL + config checkisCDBasedSystem(): 31 CD-based platformsisArcadeSystem(): 3 arcade platforms
Game Status:
romStatusMap: 8 statuses with emoji, text, i18n keys- Status enum:
unplayed,now_playing,backlogged,paused,completed,100%,retired,never_playing
Layout:
views: 3 view modes with responsive grid configurationscalculateMainLayoutWidth(): dynamic width based on drawer state
Task Display:
convertCronExpression(): human-readable cron (via cronstrue)- Task status/type maps with colors and icons
Cover Generation (utils/covers.ts)
Procedural SVG generation for:
- Collection covers (multi-image grid with deterministic gradients)
- Favorite covers (star icon themed)
- Missing covers (question mark icon)
- Unmatched covers (warning icon)
- Empty placeholders (aspect-ratio-aware)
17. Build & Tooling
Vite Configuration
| Feature | Config |
|---|---|
| Target | ESNext |
| Dev port | 3000 (8443 with HTTPS) |
| Backend proxy | /api/* → http://127.0.0.1:5000 |
| WebSocket proxy | /ws, /netplay → backend with upgrade |
| Allowed hosts | localhost, 127.0.0.1, romm.dev |
Plugins:
- Tailwind CSS (
@tailwindcss/vite) - Vue 3 (
@vitejs/plugin-vue) - Vuetify auto-import (
vite-plugin-vuetify, 57 pre-optimized components) - PWA (
vite-plugin-pwa, service worker, installable) - HTTPS (
vite-plugin-mkcert, optional dev HTTPS) - Static copy (ROM patcher JS assets)
Scripts
| Script | Command | Purpose |
|---|---|---|
dev |
vite --host |
Development server |
build |
vite build |
Production build |
preview |
vite preview |
Preview production build |
typecheck |
vue-tsc |
TypeScript validation |
generate |
openapi-typescript-codegen |
Generate types from backend OpenAPI |
lint |
eslint |
Lint .vue, .js, .ts files |
OpenAPI Code Generation
npm run generate
# Fetches http://127.0.0.1:3000/openapi.json
# Generates TypeScript interfaces in __generated__/models/
Generated types used throughout stores and API services for type-safe backend communication.
ESLint Configuration
- Flat config (
eslint.config.js) - Vue plugin with essential rules
- TypeScript-ESLint integration
- Vue accessibility plugin (
eslint-plugin-vuejs-accessibility)
18. Type System
Generated Types (__generated__/models/)
Auto-generated from backend OpenAPI schema:
| Type | Description |
|---|---|
SimpleRomSchema |
ROM in list view (covers, metadata IDs, user data) |
DetailedRomSchema |
Full ROM with all relationships |
SearchRomSchema |
Minimal search result |
PlatformSchema |
Platform with ROM count |
UserSchema |
User with role and settings |
CollectionSchema |
Collection with ROM IDs |
VirtualCollectionSchema |
Auto-generated collection |
SmartCollectionSchema |
Filter-based collection |
SaveSchema / StateSchema / ScreenshotSchema |
Asset types |
FirmwareSchema |
BIOS file info |
HeartbeatResponse |
Server status and capabilities |
ConfigResponse |
Full server configuration |
ScanStats |
Scan progress counters |
TaskInfo / TaskStatusResponse |
Background task data |
GetRomsResponse |
Paginated ROM list with filter values |
Custom Types
| File | Types |
|---|---|
emitter.d.ts |
SnackbarStatus, Events (80+ event signatures) |
rom.d.ts |
RomSelectEvent |
user.d.ts |
UserItem (extends User with password + avatar) |
ruffle.d.ts |
RufflePlayerElement, RuffleSourceAPI |
rompatcher.d.ts |
ROM patching library interfaces |
main.d.ts |
Global augmentations |
index.ts |
isKeyof<T>, ExtractPiniaStoreType<D>, ValueOf<T> |
Path Alias
"@/*" → "./src/*"
Used throughout: import { ... } from "@/stores/roms".
Appendix: Key Design Patterns
| Pattern | Where | Purpose |
|---|---|---|
| Composition API | All components | <script setup> with reactive refs |
| Pinia stores | stores/ |
Centralized state with actions/getters |
| Mitt event bus | Cross-component | Loosely-coupled dialog/notification triggers |
| Composables | composables/ |
Reusable stateful logic (singleton where needed) |
| Stale-while-revalidate | services/cache/ |
Return cached, update in background |
| Chunked upload | services/api/rom.ts |
10MB chunks with retry |
| Spatial navigation | console/ |
Grid-based focus for gamepad/keyboard |
| Input scoping | console/input/bus.ts |
Stack-based context for input handling |
| Procedural audio | console/utils/sfx.ts |
Web Audio API synthesis |
| Double buffering | useBackgroundArt |
Smooth background transitions |
| View Transitions | plugins/transition/ |
CSS View Transitions API |
| OpenAPI codegen | __generated__/ |
Type-safe API communication |
| Feature flags | heartbeatStore |
Server-driven UI feature toggling |